@hasna/economy 0.5.3 → 0.5.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.4
4
+
5
+ ### Patch Changes
6
+
7
+ - Resolve account attribution through the current hosted Subscriptions client using its station-local paths and applied bindings. Preserve explicit legacy Accounts connections, reject ambiguous bindings, and avoid attributing an unmatched local profile to another station's shared selection.
8
+
3
9
  ## 0.5.3
4
10
 
5
11
  ### Patch Changes
package/README.md CHANGED
@@ -9,7 +9,7 @@ AI coding cost tracker for Claude Code, Takumi, Codex, Gemini, OpenCode, Cursor,
9
9
 
10
10
  - Ingests local Claude Code, Takumi, Codex, Gemini, OpenCode, Cursor, Pi, and Hermes usage.
11
11
  - Tracks sessions, requests, projects, machines, models, cache tokens, cost centers, budgets, goals, and provider billing.
12
- - Attributes usage to `@hasna/accounts` profiles when agents run under managed account/profile config dirs.
12
+ - Attributes usage to hosted Subscriptions profiles using their station-local configuration directories or applied bindings; explicit legacy Accounts connections remain supported.
13
13
  - Breaks down API-equivalent, metered API, subscription-included, estimated, and unknown cost by account, coding agent, and cost center.
14
14
  - Seeds editable model pricing with input, output, cache-read, 5-minute cache-write, 1-hour cache-write, and context-cache storage rates.
15
15
  - Handles tiered pricing such as Gemini long-prompt rates and OpenAI long-context rates.
@@ -130,7 +130,9 @@ economy sync --backfill-machine
130
130
 
131
131
  Full sync also imports active project metadata from `@hasna/projects` when the registry is available. The Codex source reads both `~/.codex/state_5.sqlite` and the Codewith store at `~/.codewith/state_5.sqlite` by default; explicit `HASNA_ECONOMY_CODEX_DB_PATH` and `HASNA_ECONOMY_CODEWITH_DB_PATH` values override those locations.
132
132
 
133
- Account attribution is automatic when the accounts API (`HASNA_ACCOUNTS_API_KEY`, the accounts credentials file, or the Keychain item `hasna.credentials.accounts.api-key`) resolves a matching active, applied, or env-dir profile for the agent. With **no** accounts credential the on-box registry `~/.hasna/accounts/accounts.json` is **not** read unless `HASNA_ECONOMY_LOCAL=1` is set: attribution is simply omitted, announced once on stderr, rather than filling hosted rows from an on-box file. Account identity is the email address plus coding agent, so `work@example.com` under Codex and Claude is reported as two accounts. You can also force attribution for a process with `ECONOMY_ACCOUNT=tool:name` or agent-specific overrides such as `ECONOMY_CODEX_ACCOUNT=codex:work`.
133
+ Account attribution uses the hosted Subscriptions registry through the installed `subscriptions` CLI and its own credential resolver. Economy reads `subscriptions list --json` and `subscriptions tools list --json` once per resolution, retaining the owning package's station-local paths and applied bindings. It does not activate a profile or copy credentials. A matching explicit agent directory takes precedence; an unmatched or ambiguous directory cannot inherit another profile. With no explicit directory, a station's applied binding can supply attribution; the shared registry's current selection alone cannot identify the local account.
134
+
135
+ An explicitly configured legacy Accounts connection (`HASNA_ACCOUNTS_API_KEY`, its credentials file or Keychain entry) keeps its authority and compatibility behavior. A configured but unusable legacy or Subscriptions connection fails closed. If neither is configured, `~/.hasna/accounts/accounts.json` is **not** read unless `HASNA_ECONOMY_LOCAL=1` is set; attribution is omitted with a diagnostic. The Subscriptions CLI is required when using its registry. Profile counts are not paid-subscription counts, and this read path does not create subscription fees or backfill historical usage. Account identity remains email plus coding agent (profile name when email is unknown). Explicit process overrides such as `ECONOMY_ACCOUNT=tool:name` or `ECONOMY_CODEX_ACCOUNT=codex:work` still take precedence.
134
136
 
135
137
  Session drilldown can be scoped to an account key, account name, or email:
136
138
 
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  LOCAL_STORAGE_OPT_IN_KEYS
4
- } from "./index-1vz1arz5.js";
4
+ } from "./index-n1wwqxfe.js";
5
5
  import {
6
6
  assessCostFromDb,
7
7
  getIngestState,
@@ -78,34 +78,141 @@ import { join as join2, basename } from "path";
78
78
  import { existsSync, readFileSync } from "fs";
79
79
  import { homedir } from "os";
80
80
  import { join } from "path";
81
- import { z } from "zod";
81
+ import { z as z2 } from "zod";
82
82
  import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
83
- var ACCOUNTS_APP = "accounts";
84
- 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");
83
+
84
+ // src/lib/subscriptions-registry.ts
85
+ import { execFile } from "child_process";
86
+ import { promisify } from "util";
87
+ import { isAbsolute } from "path";
88
+ import { z } from "zod";
89
+ var execute = promisify(execFile);
90
+ var readSubscriptionsMetadata = async (args, env) => {
91
+ let stdout;
92
+ try {
93
+ const result = await execute("subscriptions", args, {
94
+ env,
95
+ encoding: "utf8",
96
+ timeout: 30000,
97
+ maxBuffer: 16 * 1024 * 1024,
98
+ windowsHide: true
99
+ });
100
+ stdout = result.stdout;
101
+ } catch (error) {
102
+ const code = error?.code;
103
+ if (code === "ENOENT")
104
+ throw new Error("subscriptions: CLI not found on PATH; install the Subscriptions CLI to resolve station account metadata");
105
+ const status = typeof code === "number" && Number.isInteger(code) ? `exit ${code}` : "execution limit or transport failure";
106
+ throw new Error(`subscriptions: read-only metadata command failed (${status}); verify the installed CLI and its hosted credentials`);
107
+ }
108
+ try {
109
+ return JSON.parse(stdout);
110
+ } catch {
111
+ throw new Error("subscriptions: metadata command returned invalid JSON");
112
+ }
113
+ };
114
+ var nonempty = z.string().min(1);
85
115
  var profileSchema = z.object({
86
- name: z.string(),
87
- tool: z.string(),
116
+ name: nonempty,
117
+ tool: nonempty,
88
118
  email: z.string().optional(),
89
119
  displayName: z.string().optional(),
90
- dir: z.string(),
120
+ dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
121
+ createdAt: nonempty,
122
+ lastUsedAt: z.string().optional(),
91
123
  description: z.string().optional(),
92
- createdAt: z.string(),
93
- lastUsedAt: z.string().optional()
124
+ active: z.boolean(),
125
+ applied: z.boolean(),
126
+ switchedAway: z.object({ profile: nonempty, email: z.string().optional() }).optional()
94
127
  });
95
- var toolDefSchema = z.object({
96
- id: z.string(),
97
- label: z.string(),
98
- envVar: z.string(),
99
- defaultDir: z.string(),
100
- bin: z.string()
128
+ var toolsSchema = z.array(z.object({
129
+ id: nonempty,
130
+ label: nonempty,
131
+ envVar: nonempty,
132
+ defaultDir: nonempty,
133
+ bin: nonempty
134
+ }));
135
+ function unique(rows, identity) {
136
+ if (new Set(rows.map(identity)).size !== rows.length) {
137
+ throw new Error("subscriptions: duplicate identity in metadata response");
138
+ }
139
+ return rows;
140
+ }
141
+
142
+ class SubscriptionsRegistry {
143
+ read;
144
+ transport = "api";
145
+ registry = "subscriptions";
146
+ env;
147
+ profiles;
148
+ tools;
149
+ constructor(baseUrl, env, read = readSubscriptionsMetadata) {
150
+ this.read = read;
151
+ this.env = { ...env, HASNA_SUBSCRIPTIONS_API_URL: baseUrl.replace(/\/v1\/?$/, "") };
152
+ }
153
+ allProfiles() {
154
+ return this.profiles ??= this.read(["list", "--json"], this.env).then((raw) => {
155
+ const parsed = z.array(profileSchema).safeParse(raw);
156
+ if (!parsed.success)
157
+ throw new Error("subscriptions: invalid profile metadata response");
158
+ return unique(parsed.data, (row) => JSON.stringify([row.tool, row.name]));
159
+ });
160
+ }
161
+ async listProfiles(tool) {
162
+ const rows = await this.allProfiles();
163
+ return rows.filter((row) => !tool || row.tool === tool);
164
+ }
165
+ async findProfile(name, tool) {
166
+ const matches = (await this.listProfiles(tool)).filter((row) => row.name === name);
167
+ return matches.length === 1 ? matches[0] : undefined;
168
+ }
169
+ async appliedProfile(tool) {
170
+ const matches = (await this.allProfiles()).filter((row) => row.tool === tool && row.applied);
171
+ if (matches.length > 1)
172
+ throw new Error("subscriptions: ambiguous station applied-profile binding");
173
+ return matches[0];
174
+ }
175
+ async currentProfile(_tool) {
176
+ return;
177
+ }
178
+ async listTools() {
179
+ return this.tools ??= this.read(["tools", "list", "--json"], this.env).then((raw) => {
180
+ const parsed = toolsSchema.safeParse(raw);
181
+ if (!parsed.success)
182
+ throw new Error("subscriptions: invalid tool metadata response");
183
+ return unique(parsed.data, (row) => row.id);
184
+ });
185
+ }
186
+ }
187
+
188
+ // src/lib/accounts-store.ts
189
+ var ACCOUNTS_APP = "accounts";
190
+ 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");
191
+ var profileSchema2 = z2.object({
192
+ name: z2.string(),
193
+ tool: z2.string(),
194
+ email: z2.string().optional(),
195
+ displayName: z2.string().optional(),
196
+ dir: z2.string(),
197
+ description: z2.string().optional(),
198
+ createdAt: z2.string(),
199
+ lastUsedAt: z2.string().optional(),
200
+ switchedAway: z2.object({ profile: z2.string().min(1), email: z2.string().optional() }).optional()
201
+ });
202
+ var toolDefSchema = z2.object({
203
+ id: z2.string(),
204
+ label: z2.string(),
205
+ envVar: z2.string(),
206
+ defaultDir: z2.string(),
207
+ bin: z2.string()
101
208
  });
102
- var storeSchema = z.object({
103
- version: z.literal(1),
104
- current: z.record(z.string(), z.string()).default({}),
105
- applied: z.record(z.string(), z.string()).default({}),
106
- toolLocks: z.record(z.string(), z.string()).default({}),
107
- profiles: z.array(profileSchema).default([]),
108
- tools: z.array(toolDefSchema).default([])
209
+ var storeSchema = z2.object({
210
+ version: z2.literal(1),
211
+ current: z2.record(z2.string(), z2.string()).default({}),
212
+ applied: z2.record(z2.string(), z2.string()).default({}),
213
+ toolLocks: z2.record(z2.string(), z2.string()).default({}),
214
+ profiles: z2.array(profileSchema2).default([]),
215
+ tools: z2.array(toolDefSchema).default([])
109
216
  });
110
217
  function localRegistryAllowed(env = process.env) {
111
218
  return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => {
@@ -387,25 +494,34 @@ class ApiStore {
387
494
  }
388
495
  }
389
496
  function resolveStore(env = process.env, options = {}) {
497
+ const legacy = resolveRegistryTransport(ACCOUNTS_APP, env, options.credentials);
498
+ if (legacy)
499
+ return new ApiStore(legacy.baseUrl, legacy.apiKey);
500
+ const current = resolveRegistryTransport("subscriptions", env, options.subscriptionsCredentials);
501
+ if (current)
502
+ return new SubscriptionsRegistry(current.baseUrl, env, options.readSubscriptionsMetadata);
503
+ return unconfiguredStore(env);
504
+ }
505
+ function resolveRegistryTransport(app, env, credentials) {
390
506
  const clientEnv = env;
391
- const credential = resolveCredential(ACCOUNTS_APP, clientEnv, options.credentials);
507
+ const credential = resolveCredential(app, clientEnv, credentials);
392
508
  if (credential === null) {
393
509
  try {
394
- resolveClientTransport(ACCOUNTS_APP, clientEnv, options.credentials ? { credentials: options.credentials } : {});
510
+ resolveClientTransport(app, clientEnv, credentials ? { credentials } : {});
395
511
  } catch (err) {
396
512
  if (isNoAccountsCredentialConfigurationError(err))
397
- return unconfiguredStore(env);
513
+ return null;
398
514
  throw err;
399
515
  }
400
- return unconfiguredStore(env);
516
+ return null;
401
517
  }
402
518
  if (credential.tier === "pointer") {
403
- 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.");
519
+ 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.`);
404
520
  }
405
- const resolution = resolveClientTransport(ACCOUNTS_APP, clientEnv, {
406
- credentials: { ...options.credentials, apiKey: credential.apiKey }
521
+ const resolution = resolveClientTransport(app, clientEnv, {
522
+ credentials: { ...credentials, apiKey: credential.apiKey }
407
523
  });
408
- return new ApiStore(resolution.baseUrl, credential.apiKey);
524
+ return { baseUrl: resolution.baseUrl, apiKey: credential.apiKey };
409
525
  }
410
526
  function isNoAccountsCredentialConfigurationError(error) {
411
527
  if (typeof error !== "object" || error === null)
@@ -422,10 +538,10 @@ function unconfiguredStore(env) {
422
538
  return new NoRegistryStore;
423
539
  }
424
540
  function accountsLocalModeNotice() {
425
- 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.";
541
+ 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.";
426
542
  }
427
543
  function accountsRefusalNotice() {
428
- 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.";
544
+ 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.";
429
545
  }
430
546
  var accountsLocalNoticePrinted = false;
431
547
  function announceAccountsLocalMode() {
@@ -513,7 +629,10 @@ async function profileForEnvDir(store, tool, env) {
513
629
  if (!configuredDir)
514
630
  return null;
515
631
  const normalized = normalizeDir(configuredDir);
516
- return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
632
+ const matches = (await store.listProfiles(tool.id)).filter((profile) => normalizeDir(profile.dir) === normalized);
633
+ if (matches.length > 1)
634
+ throw new Error("account attribution: multiple profiles match the configured directory");
635
+ return matches[0] ?? null;
517
636
  }
518
637
  async function resolveAccountForAgent(agent, env = process.env, options = {}) {
519
638
  const override = envOverride(agent, env);
@@ -527,8 +646,15 @@ async function resolveAccountForAgent(agent, env = process.env, options = {}) {
527
646
  continue;
528
647
  const envProfile = await profileForEnvDir(store, tool, env);
529
648
  if (envProfile)
530
- return fromProfile(envProfile, "env");
531
- const appliedName = appliedProfileName(toolId);
649
+ return envProfile.switchedAway ? null : fromProfile(envProfile, "env");
650
+ if (store.registry === "subscriptions" && env[tool.envVar])
651
+ return null;
652
+ if (store.appliedProfile) {
653
+ const applied = await store.appliedProfile(toolId);
654
+ if (applied)
655
+ return applied.switchedAway ? null : fromProfile(applied, "applied");
656
+ }
657
+ const appliedName = store.registry === "subscriptions" ? undefined : appliedProfileName(toolId);
532
658
  if (appliedName) {
533
659
  const applied = await store.findProfile(appliedName, toolId);
534
660
  if (applied)
@@ -14,7 +14,7 @@ import {
14
14
  querySavingsSummary,
15
15
  requireCurrentBillingDiffSummary,
16
16
  requireCurrentSavingsReport
17
- } from "./index-1vz1arz5.js";
17
+ } from "./index-n1wwqxfe.js";
18
18
  import {
19
19
  deleteBudget,
20
20
  deleteGoal,
@@ -20,7 +20,7 @@ import {
20
20
  // package.json
21
21
  var package_default = {
22
22
  name: "@hasna/economy",
23
- version: "0.5.3",
23
+ version: "0.5.4",
24
24
  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",
25
25
  repository: {
26
26
  type: "git",
@@ -4,7 +4,7 @@ import {
4
4
  } from "./index-w3bvv0ns.js";
5
5
  import {
6
6
  getStore
7
- } from "./index-mhrkwvtk.js";
7
+ } from "./index-ct3zdefz.js";
8
8
  import {
9
9
  formatReportedUsd,
10
10
  formatSummaryCoverage,
@@ -5,7 +5,7 @@ import {
5
5
  } from "./index-w3bvv0ns.js";
6
6
  import {
7
7
  syncAll
8
- } from "./index-s9mvven6.js";
8
+ } from "./index-9arv0b1j.js";
9
9
  import {
10
10
  buildBrief,
11
11
  packageMetadata,
@@ -16,7 +16,7 @@ import {
16
16
  queryProjectDetail,
17
17
  queryRangeStats,
18
18
  querySavingsSummary
19
- } from "./index-1vz1arz5.js";
19
+ } from "./index-n1wwqxfe.js";
20
20
  import {
21
21
  AGENTS,
22
22
  INGEST_SCHEMA_SQL,
@@ -6,10 +6,10 @@ import {
6
6
  printStatusLine,
7
7
  printWaybarJson,
8
8
  runTui
9
- } from "./index-bn04v378.js";
9
+ } from "./index-nheq2nrz.js";
10
10
  import"./index-w3bvv0ns.js";
11
- import"./index-mhrkwvtk.js";
12
- import"./index-1vz1arz5.js";
11
+ import"./index-ct3zdefz.js";
12
+ import"./index-n1wwqxfe.js";
13
13
  import"./index-f5jv6q5q.js";
14
14
  import"./index-p35cyhc4.js";
15
15
  export {
@@ -2,12 +2,12 @@
2
2
  import {
3
3
  agentPaths,
4
4
  syncAll
5
- } from "./index-s9mvven6.js";
5
+ } from "./index-9arv0b1j.js";
6
6
  import {
7
7
  getStore,
8
8
  isCloudStore
9
- } from "./index-mhrkwvtk.js";
10
- import"./index-1vz1arz5.js";
9
+ } from "./index-ct3zdefz.js";
10
+ import"./index-n1wwqxfe.js";
11
11
  import {
12
12
  formatSummaryValue
13
13
  } from "./index-f5jv6q5q.js";
package/dist/cli/index.js CHANGED
@@ -8,22 +8,22 @@ import {
8
8
  import {
9
9
  buildStatusLine,
10
10
  gatherStatusData
11
- } from "../chunks/index-bn04v378.js";
11
+ } from "../chunks/index-nheq2nrz.js";
12
12
  import"../chunks/index-w3bvv0ns.js";
13
13
  import {
14
14
  agentPaths,
15
15
  syncAll
16
- } from "../chunks/index-s9mvven6.js";
16
+ } from "../chunks/index-9arv0b1j.js";
17
17
  import {
18
18
  getStore,
19
19
  isCloudStore
20
- } from "../chunks/index-mhrkwvtk.js";
20
+ } from "../chunks/index-ct3zdefz.js";
21
21
  import {
22
22
  billingDriftCheck,
23
23
  economyCloudStorage,
24
24
  economyTransportReport,
25
25
  packageMetadata
26
- } from "../chunks/index-1vz1arz5.js";
26
+ } from "../chunks/index-n1wwqxfe.js";
27
27
  import {
28
28
  AGENTS,
29
29
  MAX_INGEST_BYTES,
@@ -1357,23 +1357,23 @@ function registerExtendedCommands(program) {
1357
1357
  console.log();
1358
1358
  });
1359
1359
  program.command("tui").description("Terminal status dashboard (Linux/SSH friendly)").option("--watch", "Live refresh").option("--interval <seconds>", "Refresh interval", "30").action(async (opts) => {
1360
- const { runTui } = await import("../chunks/tui-fe94bvp9.js");
1360
+ const { runTui } = await import("../chunks/tui-a1tvcw2j.js");
1361
1361
  await runTui({
1362
1362
  watch: Boolean(opts.watch),
1363
1363
  interval: Number(opts.interval ?? 30)
1364
1364
  });
1365
1365
  });
1366
1366
  program.command("waybar").description("Print waybar-compatible JSON status line").action(async () => {
1367
- const { printWaybarJson } = await import("../chunks/tui-fe94bvp9.js");
1367
+ const { printWaybarJson } = await import("../chunks/tui-a1tvcw2j.js");
1368
1368
  await printWaybarJson();
1369
1369
  });
1370
1370
  const barCmd = program.command("bar").description("Status bar helpers (Linux)");
1371
1371
  barCmd.command("tui").description("Alias for economy tui").option("--watch", "Live refresh").action(async (opts) => {
1372
- const { runTui } = await import("../chunks/tui-fe94bvp9.js");
1372
+ const { runTui } = await import("../chunks/tui-a1tvcw2j.js");
1373
1373
  await runTui({ watch: Boolean(opts.watch) });
1374
1374
  });
1375
1375
  barCmd.command("waybar").description("Alias for economy waybar").action(async () => {
1376
- const { printWaybarJson } = await import("../chunks/tui-fe94bvp9.js");
1376
+ const { printWaybarJson } = await import("../chunks/tui-a1tvcw2j.js");
1377
1377
  await printWaybarJson();
1378
1378
  });
1379
1379
  program.command("estimate").description("Estimate cost for token counts").requiredOption("--model <model>", "Model name").option("--input <n>", "Input tokens", "0").option("--output <n>", "Output tokens", "0").action(async (opts) => {
@@ -3198,7 +3198,7 @@ program.command("accounts [period]").description("List account usage by email ad
3198
3198
  console.log();
3199
3199
  });
3200
3200
  program.command("watch").description("Live stream of incoming costs").option("--interval <seconds>", "Poll interval in seconds", "10").option("--daemon", "Watch agent data directories and sync on change").option("--agent <agent>", `Filter by agent (${AGENTS.join("|")})`).option("--notify <amount>", "Fire macOS notification when cumulative cost crosses this USD threshold").action(async (opts) => {
3201
- const { watchCosts } = await import("../chunks/watch-ct8dvrpr.js");
3201
+ const { watchCosts } = await import("../chunks/watch-rvd3dn6r.js");
3202
3202
  await watchCosts({
3203
3203
  interval: parsePositiveCliInteger(opts.interval ?? "10", "--interval"),
3204
3204
  daemon: Boolean(opts.daemon),
@@ -3434,7 +3434,7 @@ pricingCmd.command("remove <model>").description("Remove pricing for a model").a
3434
3434
  });
3435
3435
  program.command("serve").description("Start the REST API server").option("-p, --port <port>", "Port", "3456").action(async (opts) => {
3436
3436
  const port = parseCliPort(opts.port ?? "3456", "--port");
3437
- const { startServer } = await import("../chunks/serve-8yqe7nhs.js");
3437
+ const { startServer } = await import("../chunks/serve-ra46ms69.js");
3438
3438
  startServer(port);
3439
3439
  });
3440
3440
  program.command("mcp").description("Show MCP server install commands").option("--claude", "Install into Claude Code").option("--codex", "Install into Codex").option("--gemini", "Install into Gemini CLI").option("--all", "Install into all agents").action(async (opts) => {
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ import { randomUUID } from "crypto";
62
62
  // package.json
63
63
  var package_default = {
64
64
  name: "@hasna/economy",
65
- version: "0.5.3",
65
+ version: "0.5.4",
66
66
  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",
67
67
  repository: {
68
68
  type: "git",
@@ -1,8 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  import { type CredentialChainOptions } from '@hasna/contracts/client';
3
+ import { type SubscriptionsMetadataReader } from './subscriptions-registry.js';
3
4
  /** Test injection for the accounts credential chain (keychain runner, tier-1 key). */
4
5
  export interface ResolveAccountsStoreOptions {
5
6
  credentials?: CredentialChainOptions;
7
+ subscriptionsCredentials?: CredentialChainOptions;
8
+ readSubscriptionsMetadata?: SubscriptionsMetadataReader;
6
9
  }
7
10
  /** Minimal profile record — the fields economy's attribution reads. */
8
11
  declare const profileSchema: z.ZodObject<{
@@ -14,6 +17,16 @@ declare const profileSchema: z.ZodObject<{
14
17
  description: z.ZodOptional<z.ZodString>;
15
18
  createdAt: z.ZodString;
16
19
  lastUsedAt: z.ZodOptional<z.ZodString>;
20
+ switchedAway: z.ZodOptional<z.ZodObject<{
21
+ profile: z.ZodString;
22
+ email: z.ZodOptional<z.ZodString>;
23
+ }, "strip", z.ZodTypeAny, {
24
+ profile: string;
25
+ email?: string | undefined;
26
+ }, {
27
+ profile: string;
28
+ email?: string | undefined;
29
+ }>>;
17
30
  }, "strip", z.ZodTypeAny, {
18
31
  name: string;
19
32
  dir: string;
@@ -23,6 +36,10 @@ declare const profileSchema: z.ZodObject<{
23
36
  email?: string | undefined;
24
37
  displayName?: string | undefined;
25
38
  lastUsedAt?: string | undefined;
39
+ switchedAway?: {
40
+ profile: string;
41
+ email?: string | undefined;
42
+ } | undefined;
26
43
  }, {
27
44
  name: string;
28
45
  dir: string;
@@ -32,6 +49,10 @@ declare const profileSchema: z.ZodObject<{
32
49
  email?: string | undefined;
33
50
  displayName?: string | undefined;
34
51
  lastUsedAt?: string | undefined;
52
+ switchedAway?: {
53
+ profile: string;
54
+ email?: string | undefined;
55
+ } | undefined;
35
56
  }>;
36
57
  /** Minimal tool definition — economy reads `id` and `envVar` (dir matching). */
37
58
  declare const toolDefSchema: z.ZodObject<{
@@ -62,9 +83,11 @@ export type ToolDef = z.infer<typeof toolDefSchema>;
62
83
  */
63
84
  export interface AccountsStore {
64
85
  readonly transport: 'local' | 'api' | 'none';
86
+ readonly registry?: 'accounts' | 'subscriptions';
65
87
  listProfiles(tool?: string): Promise<Profile[]>;
66
88
  findProfile(name: string, tool?: string): Promise<Profile | undefined>;
67
89
  currentProfile(tool: string): Promise<Profile | undefined>;
90
+ appliedProfile?(tool: string): Promise<Profile | undefined>;
68
91
  listTools(): Promise<ToolDef[]>;
69
92
  }
70
93
  /**
@@ -84,7 +107,8 @@ export declare function appliedProfileName(toolId: string): string | undefined;
84
107
  * Stale `*_STORAGE_MODE` / `*_MODE` variables are ignored — they never select
85
108
  * a store and never error (owner directive 2026-08-15).
86
109
  *
87
- * WITH NO CREDENTIAL the on-box JSON registry is served ONLY under economy's
110
+ * With neither a legacy Accounts nor current Subscriptions credential, the
111
+ * on-box JSON registry is served ONLY under economy's
88
112
  * explicit local opt-in (`HASNA_ECONOMY_LOCAL=1` / `ECONOMY_LOCAL=1`); every
89
113
  * other run gets {@link NoRegistryStore} — no on-box read, attribution absent,
90
114
  * one stderr line. Serving the on-box registry to a hosted client was the last
@@ -1 +1 @@
1
- {"version":3,"file":"accounts-store.d.ts","sourceRoot":"","sources":["../../src/lib/accounts-store.ts"],"names":[],"mappings":"AAsCA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAA6C,KAAK,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAMhH,sFAAsF;AACtF,MAAM,WAAW,2BAA2B;IAC1C,WAAW,CAAC,EAAE,sBAAsB,CAAA;CACrC;AAKD,uEAAuE;AACvE,QAAA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;EASjB,CAAA;AAEF,gFAAgF;AAChF,QAAA,MAAM,aAAa;;;;;;;;;;;;;;;;;;EAMjB,CAAA;AAYF,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAA;AACnD,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAA;AAmBnD;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;IAC5C,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;IAC/C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAA;IACtE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAA;IAC1D,SAAS,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;CAChC;AAwDD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGrE;AAwQD;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAgB,YAAY,CAC1B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,2BAAgC,GACxC,aAAa,CAqCf;AA2DD,8DAA8D;AAC9D,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD"}
1
+ {"version":3,"file":"accounts-store.d.ts","sourceRoot":"","sources":["../../src/lib/accounts-store.ts"],"names":[],"mappings":"AA4CA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AACvB,OAAO,EAA6C,KAAK,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAEhH,OAAO,EAAyB,KAAK,2BAA2B,EAAE,MAAM,6BAA6B,CAAA;AAKrG,sFAAsF;AACtF,MAAM,WAAW,2BAA2B;IAC1C,WAAW,CAAC,EAAE,sBAAsB,CAAA;IACpC,wBAAwB,CAAC,EAAE,sBAAsB,CAAA;IACjD,yBAAyB,CAAC,EAAE,2BAA2B,CAAA;CACxD;AAKD,uEAAuE;AACvE,QAAA,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUjB,CAAA;AAEF,gFAAgF;AAChF,QAAA,MAAM,aAAa;;;;;;;;;;;;;;;;;;EAMjB,CAAA;AAYF,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAA;AACnD,MAAM,MAAM,OAAO,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAA;AAmBnD;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO,GAAG,KAAK,GAAG,MAAM,CAAA;IAC5C,QAAQ,CAAC,QAAQ,CAAC,EAAE,UAAU,GAAG,eAAe,CAAA;IAChD,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;IAC/C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAA;IACtE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAA;IAC1D,cAAc,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAA;IAC3D,SAAS,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;CAChC;AAwDD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGrE;AAwQD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,YAAY,CAC1B,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,2BAAgC,GACxC,aAAa,CAMf;AAuGD,8DAA8D;AAC9D,wBAAgB,0BAA0B,IAAI,IAAI,CAEjD"}
@@ -1 +1 @@
1
- {"version":3,"file":"accounts.d.ts","sourceRoot":"","sources":["../../src/lib/accounts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,2BAA2B,EAAE,MAAM,qBAAqB,CAAA;AAExG,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAExC,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,UAAU,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAA;CAC3D;AAqFD,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,KAAK,EACZ,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAwBpC;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAC1C,MAAM,EAAE,CAAC,EACT,OAAO,EAAE,kBAAkB,GAAG,IAAI,GACjC,CAAC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAUjC"}
1
+ {"version":3,"file":"accounts.d.ts","sourceRoot":"","sources":["../../src/lib/accounts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoC,KAAK,2BAA2B,EAAE,MAAM,qBAAqB,CAAA;AAExG,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAA;AAExC,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,YAAY,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,cAAc,EAAE,UAAU,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,CAAA;CAC3D;AAuFD,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,KAAK,EACZ,GAAG,GAAE,MAAM,CAAC,UAAwB,EACpC,OAAO,GAAE,2BAAgC,GACxC,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAmCpC;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAC1C,MAAM,EAAE,CAAC,EACT,OAAO,EAAE,kBAAkB,GAAG,IAAI,GACjC,CAAC,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAUjC"}
@@ -0,0 +1,26 @@
1
+ import type { AccountsStore, Profile, ToolDef } from './accounts-store.js';
2
+ /** Read-only, package-owned metadata. Never project a credential into argv. */
3
+ export type SubscriptionsMetadataReader = (args: string[], env: NodeJS.ProcessEnv) => Promise<unknown>;
4
+ export declare const readSubscriptionsMetadata: SubscriptionsMetadataReader;
5
+ /**
6
+ * The installed Subscriptions package owns station-local paths and applied
7
+ * bindings. Its hosted list combines those with the authoritative API records.
8
+ * Do not copy its home migration rules, read its registry file, or infer that
9
+ * the fleet-wide "current" selection identifies this station's running account.
10
+ */
11
+ export declare class SubscriptionsRegistry implements AccountsStore {
12
+ private readonly read;
13
+ readonly transport: "api";
14
+ readonly registry: "subscriptions";
15
+ private readonly env;
16
+ private profiles?;
17
+ private tools?;
18
+ constructor(baseUrl: string, env: NodeJS.ProcessEnv, read?: SubscriptionsMetadataReader);
19
+ private allProfiles;
20
+ listProfiles(tool?: string): Promise<Profile[]>;
21
+ findProfile(name: string, tool?: string): Promise<Profile | undefined>;
22
+ appliedProfile(tool: string): Promise<Profile | undefined>;
23
+ currentProfile(_tool: string): Promise<Profile | undefined>;
24
+ listTools(): Promise<ToolDef[]>;
25
+ }
26
+ //# sourceMappingURL=subscriptions-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriptions-registry.d.ts","sourceRoot":"","sources":["../../src/lib/subscriptions-registry.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAI1E,+EAA+E;AAC/E,MAAM,MAAM,2BAA2B,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;AAEtG,eAAO,MAAM,yBAAyB,EAAE,2BAoBvC,CAAA;AA+BD;;;;;GAKG;AACH,qBAAa,qBAAsB,YAAW,aAAa;IAOJ,OAAO,CAAC,QAAQ,CAAC,IAAI;IAN1E,QAAQ,CAAC,SAAS,EAAG,KAAK,CAAS;IACnC,QAAQ,CAAC,QAAQ,EAAG,eAAe,CAAS;IAC5C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAmB;IACvC,OAAO,CAAC,QAAQ,CAAC,CAA+C;IAChE,OAAO,CAAC,KAAK,CAAC,CAAoB;gBAEtB,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,CAAC,UAAU,EAAmB,IAAI,8BAA4B;IAMtG,OAAO,CAAC,WAAW;IAQb,YAAY,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAK/C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAKtE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAM1D,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC;IAK3D,SAAS,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;CAOtC"}
@@ -1,8 +1,9 @@
1
1
  import type { CredentialChainOptions } from '@hasna/contracts/client';
2
2
  export declare const FIXTURE_STATION = "economy-hermetic-fixture";
3
- export declare function fixtureKeychainArgs(app: 'economy' | 'accounts', kind: 'api-key' | 'api-url'): string[];
4
- export declare function absentFixtureKeychain(app: 'economy' | 'accounts'): {
3
+ export declare function fixtureKeychainArgs(app: 'economy' | 'accounts' | 'subscriptions', kind: 'api-key' | 'api-url'): string[];
4
+ export declare function absentFixtureKeychain(app: 'economy' | 'accounts' | 'subscriptions'): {
5
5
  credentials: CredentialChainOptions;
6
+ subscriptionsCredentials?: CredentialChainOptions;
6
7
  };
7
8
  /** Explicit --preload is confined to the test child; installed clients are unchanged. */
8
9
  export declare function fixtureBunArgs(home: string): string[];
@@ -1 +1 @@
1
- {"version":3,"file":"test-keychain-fixture.d.ts","sourceRoot":"","sources":["../../src/lib/test-keychain-fixture.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAErE,eAAO,MAAM,eAAe,6BAA6B,CAAA;AAEzD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,SAAS,GAAG,UAAU,EAAE,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,EAAE,CAEtG;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,SAAS,GAAG,UAAU,GAAG;IAAE,WAAW,EAAE,sBAAsB,CAAA;CAAE,CAO1G;AAED,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAerD"}
1
+ {"version":3,"file":"test-keychain-fixture.d.ts","sourceRoot":"","sources":["../../src/lib/test-keychain-fixture.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,yBAAyB,CAAA;AAErE,eAAO,MAAM,eAAe,6BAA6B,CAAA;AAEzD,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,SAAS,GAAG,UAAU,GAAG,eAAe,EAAE,IAAI,EAAE,SAAS,GAAG,SAAS,GAAG,MAAM,EAAE,CAExH;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,SAAS,GAAG,UAAU,GAAG,eAAe,GAAG;IAAE,WAAW,EAAE,sBAAsB,CAAC;IAAC,wBAAwB,CAAC,EAAE,sBAAsB,CAAA;CAAE,CAS/K;AAED,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAerD"}