@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.
@@ -2384,7 +2384,7 @@ import { join as join3, basename } from "path";
2384
2384
  // src/lib/accounts-store.ts
2385
2385
  import { homedir as homedir2 } from "os";
2386
2386
  import { join as join2 } from "path";
2387
- import { z } from "zod";
2387
+ import { z as z2 } from "zod";
2388
2388
  import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
2389
2389
 
2390
2390
  // src/lib/cloud-storage.ts
@@ -2515,32 +2515,61 @@ function economyCloudStorage(env = process.env) {
2515
2515
  return cache;
2516
2516
  }
2517
2517
 
2518
- // src/lib/accounts-store.ts
2519
- var profileNameSchema = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
2518
+ // src/lib/subscriptions-registry.ts
2519
+ import { execFile } from "child_process";
2520
+ import { promisify } from "util";
2521
+ import { isAbsolute } from "path";
2522
+ import { z } from "zod";
2523
+ var execute = promisify(execFile);
2524
+ var nonempty = z.string().min(1);
2520
2525
  var profileSchema = z.object({
2521
- name: z.string(),
2522
- tool: z.string(),
2526
+ name: nonempty,
2527
+ tool: nonempty,
2523
2528
  email: z.string().optional(),
2524
2529
  displayName: z.string().optional(),
2525
- dir: z.string(),
2530
+ dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
2531
+ createdAt: nonempty,
2532
+ lastUsedAt: z.string().optional(),
2526
2533
  description: z.string().optional(),
2527
- createdAt: z.string(),
2528
- lastUsedAt: z.string().optional()
2534
+ active: z.boolean(),
2535
+ applied: z.boolean(),
2536
+ switchedAway: z.object({ profile: nonempty, email: z.string().optional() }).optional()
2529
2537
  });
2530
- var toolDefSchema = z.object({
2531
- id: z.string(),
2532
- label: z.string(),
2533
- envVar: z.string(),
2534
- defaultDir: z.string(),
2535
- bin: z.string()
2538
+ var toolsSchema = z.array(z.object({
2539
+ id: nonempty,
2540
+ label: nonempty,
2541
+ envVar: nonempty,
2542
+ defaultDir: nonempty,
2543
+ bin: nonempty
2544
+ }));
2545
+
2546
+ // src/lib/accounts-store.ts
2547
+ var profileNameSchema = z2.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
2548
+ var profileSchema2 = z2.object({
2549
+ name: z2.string(),
2550
+ tool: z2.string(),
2551
+ email: z2.string().optional(),
2552
+ displayName: z2.string().optional(),
2553
+ dir: z2.string(),
2554
+ description: z2.string().optional(),
2555
+ createdAt: z2.string(),
2556
+ lastUsedAt: z2.string().optional(),
2557
+ switchedAway: z2.object({ profile: z2.string().min(1), email: z2.string().optional() }).optional()
2536
2558
  });
2537
- var storeSchema = z.object({
2538
- version: z.literal(1),
2539
- current: z.record(z.string(), z.string()).default({}),
2540
- applied: z.record(z.string(), z.string()).default({}),
2541
- toolLocks: z.record(z.string(), z.string()).default({}),
2542
- profiles: z.array(profileSchema).default([]),
2543
- tools: z.array(toolDefSchema).default([])
2559
+ var toolDefSchema = z2.object({
2560
+ id: z2.string(),
2561
+ label: z2.string(),
2562
+ envVar: z2.string(),
2563
+ defaultDir: z2.string(),
2564
+ bin: z2.string()
2565
+ });
2566
+ var storeSchema = z2.object({
2567
+ version: z2.literal(1),
2568
+ current: z2.record(z2.string(), z2.string()).default({}),
2569
+ applied: z2.record(z2.string(), z2.string()).default({}),
2570
+ toolLocks: z2.record(z2.string(), z2.string()).default({}),
2571
+ profiles: z2.array(profileSchema2).default([]),
2572
+ tools: z2.array(toolDefSchema).default([])
2544
2573
  });
2545
2574
  var BUILTIN_TOOLS = [
2546
2575
  { id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
@@ -3345,11 +3374,11 @@ async function withIngestJournalLock(directory, action) {
3345
3374
  }
3346
3375
 
3347
3376
  // src/lib/ingest-journal-operation.ts
3348
- import { z as z2 } from "zod";
3349
- var ingestJournalOperationSchema = z2.discriminatedUnion("action", [
3350
- z2.object({ action: z2.literal("status") }).strict(),
3351
- z2.object({ action: z2.literal("reconcile") }).strict(),
3352
- z2.object({ action: z2.literal("maintain"), confirm: z2.literal(true), limit: z2.number().int().min(1).max(32).default(32) }).strict()
3377
+ import { z as z3 } from "zod";
3378
+ var ingestJournalOperationSchema = z3.discriminatedUnion("action", [
3379
+ z3.object({ action: z3.literal("status") }).strict(),
3380
+ z3.object({ action: z3.literal("reconcile") }).strict(),
3381
+ z3.object({ action: z3.literal("maintain"), confirm: z3.literal(true), limit: z3.number().int().min(1).max(32).default(32) }).strict()
3353
3382
  ]);
3354
3383
 
3355
3384
  // src/lib/cloud-ingest.ts
@@ -3458,7 +3487,7 @@ async function pushIngestRows(cloud, scratch) {
3458
3487
  // package.json
3459
3488
  var package_default = {
3460
3489
  name: "@hasna/economy",
3461
- version: "0.5.3",
3490
+ version: "0.5.4",
3462
3491
  description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
3463
3492
  repository: {
3464
3493
  type: "git",
@@ -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.4",
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",
@@ -8819,7 +8819,7 @@ import { join as join5, basename } from "path";
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()
8853
8884
  });
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()
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()
8860
8958
  });
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([])
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()
8965
+ });
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)