@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 +6 -0
- package/README.md +4 -2
- package/dist/chunks/{index-s9mvven6.js → index-9arv0b1j.js} +161 -35
- package/dist/chunks/{index-mhrkwvtk.js → index-ct3zdefz.js} +1 -1
- package/dist/chunks/{index-1vz1arz5.js → index-n1wwqxfe.js} +1 -1
- package/dist/chunks/{index-bn04v378.js → index-nheq2nrz.js} +1 -1
- package/dist/chunks/{serve-8yqe7nhs.js → serve-ra46ms69.js} +2 -2
- package/dist/chunks/{tui-fe94bvp9.js → tui-a1tvcw2j.js} +3 -3
- package/dist/chunks/{watch-ct8dvrpr.js → watch-rvd3dn6r.js} +3 -3
- package/dist/cli/index.js +10 -10
- package/dist/index.js +1 -1
- package/dist/lib/accounts-store.d.ts +25 -1
- package/dist/lib/accounts-store.d.ts.map +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/subscriptions-registry.d.ts +26 -0
- package/dist/lib/subscriptions-registry.d.ts.map +1 -0
- package/dist/lib/test-keychain-fixture.d.ts +3 -2
- package/dist/lib/test-keychain-fixture.d.ts.map +1 -1
- package/dist/mcp/index.js +228 -102
- package/dist/otel/index.js +56 -27
- package/dist/server/index.js +169 -45
- package/dist/server/service-worker.js +160 -36
- package/package.json +1 -1
|
@@ -4295,7 +4295,7 @@ import { join as join4, basename } from "path";
|
|
|
4295
4295
|
import { existsSync as existsSync4, readFileSync } from "fs";
|
|
4296
4296
|
import { homedir as homedir2 } from "os";
|
|
4297
4297
|
import { join as join3 } from "path";
|
|
4298
|
-
import { z } from "zod";
|
|
4298
|
+
import { z as z2 } from "zod";
|
|
4299
4299
|
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
4300
4300
|
|
|
4301
4301
|
// src/lib/cloud-storage.ts
|
|
@@ -4314,33 +4314,138 @@ import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
|
|
|
4314
4314
|
var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
|
|
4315
4315
|
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
4316
4316
|
|
|
4317
|
-
// src/lib/
|
|
4318
|
-
|
|
4319
|
-
|
|
4317
|
+
// src/lib/subscriptions-registry.ts
|
|
4318
|
+
import { execFile } from "child_process";
|
|
4319
|
+
import { promisify } from "util";
|
|
4320
|
+
import { isAbsolute } from "path";
|
|
4321
|
+
import { z } from "zod";
|
|
4322
|
+
var execute = promisify(execFile);
|
|
4323
|
+
var readSubscriptionsMetadata = async (args, env) => {
|
|
4324
|
+
let stdout;
|
|
4325
|
+
try {
|
|
4326
|
+
const result = await execute("subscriptions", args, {
|
|
4327
|
+
env,
|
|
4328
|
+
encoding: "utf8",
|
|
4329
|
+
timeout: 30000,
|
|
4330
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
4331
|
+
windowsHide: true
|
|
4332
|
+
});
|
|
4333
|
+
stdout = result.stdout;
|
|
4334
|
+
} catch (error) {
|
|
4335
|
+
const code = error?.code;
|
|
4336
|
+
if (code === "ENOENT")
|
|
4337
|
+
throw new Error("subscriptions: CLI not found on PATH; install the Subscriptions CLI to resolve station account metadata");
|
|
4338
|
+
const status = typeof code === "number" && Number.isInteger(code) ? `exit ${code}` : "execution limit or transport failure";
|
|
4339
|
+
throw new Error(`subscriptions: read-only metadata command failed (${status}); verify the installed CLI and its hosted credentials`);
|
|
4340
|
+
}
|
|
4341
|
+
try {
|
|
4342
|
+
return JSON.parse(stdout);
|
|
4343
|
+
} catch {
|
|
4344
|
+
throw new Error("subscriptions: metadata command returned invalid JSON");
|
|
4345
|
+
}
|
|
4346
|
+
};
|
|
4347
|
+
var nonempty = z.string().min(1);
|
|
4320
4348
|
var profileSchema = z.object({
|
|
4321
|
-
name:
|
|
4322
|
-
tool:
|
|
4349
|
+
name: nonempty,
|
|
4350
|
+
tool: nonempty,
|
|
4323
4351
|
email: z.string().optional(),
|
|
4324
4352
|
displayName: z.string().optional(),
|
|
4325
|
-
dir:
|
|
4353
|
+
dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
|
|
4354
|
+
createdAt: nonempty,
|
|
4355
|
+
lastUsedAt: z.string().optional(),
|
|
4326
4356
|
description: z.string().optional(),
|
|
4327
|
-
|
|
4328
|
-
|
|
4357
|
+
active: z.boolean(),
|
|
4358
|
+
applied: z.boolean(),
|
|
4359
|
+
switchedAway: z.object({ profile: nonempty, email: z.string().optional() }).optional()
|
|
4329
4360
|
});
|
|
4330
|
-
var
|
|
4331
|
-
id:
|
|
4332
|
-
label:
|
|
4333
|
-
envVar:
|
|
4334
|
-
defaultDir:
|
|
4335
|
-
bin:
|
|
4361
|
+
var toolsSchema = z.array(z.object({
|
|
4362
|
+
id: nonempty,
|
|
4363
|
+
label: nonempty,
|
|
4364
|
+
envVar: nonempty,
|
|
4365
|
+
defaultDir: nonempty,
|
|
4366
|
+
bin: nonempty
|
|
4367
|
+
}));
|
|
4368
|
+
function unique(rows, identity) {
|
|
4369
|
+
if (new Set(rows.map(identity)).size !== rows.length) {
|
|
4370
|
+
throw new Error("subscriptions: duplicate identity in metadata response");
|
|
4371
|
+
}
|
|
4372
|
+
return rows;
|
|
4373
|
+
}
|
|
4374
|
+
|
|
4375
|
+
class SubscriptionsRegistry {
|
|
4376
|
+
read;
|
|
4377
|
+
transport = "api";
|
|
4378
|
+
registry = "subscriptions";
|
|
4379
|
+
env;
|
|
4380
|
+
profiles;
|
|
4381
|
+
tools;
|
|
4382
|
+
constructor(baseUrl, env, read = readSubscriptionsMetadata) {
|
|
4383
|
+
this.read = read;
|
|
4384
|
+
this.env = { ...env, HASNA_SUBSCRIPTIONS_API_URL: baseUrl.replace(/\/v1\/?$/, "") };
|
|
4385
|
+
}
|
|
4386
|
+
allProfiles() {
|
|
4387
|
+
return this.profiles ??= this.read(["list", "--json"], this.env).then((raw) => {
|
|
4388
|
+
const parsed = z.array(profileSchema).safeParse(raw);
|
|
4389
|
+
if (!parsed.success)
|
|
4390
|
+
throw new Error("subscriptions: invalid profile metadata response");
|
|
4391
|
+
return unique(parsed.data, (row) => JSON.stringify([row.tool, row.name]));
|
|
4392
|
+
});
|
|
4393
|
+
}
|
|
4394
|
+
async listProfiles(tool) {
|
|
4395
|
+
const rows = await this.allProfiles();
|
|
4396
|
+
return rows.filter((row) => !tool || row.tool === tool);
|
|
4397
|
+
}
|
|
4398
|
+
async findProfile(name, tool) {
|
|
4399
|
+
const matches = (await this.listProfiles(tool)).filter((row) => row.name === name);
|
|
4400
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
4401
|
+
}
|
|
4402
|
+
async appliedProfile(tool) {
|
|
4403
|
+
const matches = (await this.allProfiles()).filter((row) => row.tool === tool && row.applied);
|
|
4404
|
+
if (matches.length > 1)
|
|
4405
|
+
throw new Error("subscriptions: ambiguous station applied-profile binding");
|
|
4406
|
+
return matches[0];
|
|
4407
|
+
}
|
|
4408
|
+
async currentProfile(_tool) {
|
|
4409
|
+
return;
|
|
4410
|
+
}
|
|
4411
|
+
async listTools() {
|
|
4412
|
+
return this.tools ??= this.read(["tools", "list", "--json"], this.env).then((raw) => {
|
|
4413
|
+
const parsed = toolsSchema.safeParse(raw);
|
|
4414
|
+
if (!parsed.success)
|
|
4415
|
+
throw new Error("subscriptions: invalid tool metadata response");
|
|
4416
|
+
return unique(parsed.data, (row) => row.id);
|
|
4417
|
+
});
|
|
4418
|
+
}
|
|
4419
|
+
}
|
|
4420
|
+
|
|
4421
|
+
// src/lib/accounts-store.ts
|
|
4422
|
+
var ACCOUNTS_APP = "accounts";
|
|
4423
|
+
var profileNameSchema = z2.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
|
|
4424
|
+
var profileSchema2 = z2.object({
|
|
4425
|
+
name: z2.string(),
|
|
4426
|
+
tool: z2.string(),
|
|
4427
|
+
email: z2.string().optional(),
|
|
4428
|
+
displayName: z2.string().optional(),
|
|
4429
|
+
dir: z2.string(),
|
|
4430
|
+
description: z2.string().optional(),
|
|
4431
|
+
createdAt: z2.string(),
|
|
4432
|
+
lastUsedAt: z2.string().optional(),
|
|
4433
|
+
switchedAway: z2.object({ profile: z2.string().min(1), email: z2.string().optional() }).optional()
|
|
4434
|
+
});
|
|
4435
|
+
var toolDefSchema = z2.object({
|
|
4436
|
+
id: z2.string(),
|
|
4437
|
+
label: z2.string(),
|
|
4438
|
+
envVar: z2.string(),
|
|
4439
|
+
defaultDir: z2.string(),
|
|
4440
|
+
bin: z2.string()
|
|
4336
4441
|
});
|
|
4337
|
-
var storeSchema =
|
|
4338
|
-
version:
|
|
4339
|
-
current:
|
|
4340
|
-
applied:
|
|
4341
|
-
toolLocks:
|
|
4342
|
-
profiles:
|
|
4343
|
-
tools:
|
|
4442
|
+
var storeSchema = z2.object({
|
|
4443
|
+
version: z2.literal(1),
|
|
4444
|
+
current: z2.record(z2.string(), z2.string()).default({}),
|
|
4445
|
+
applied: z2.record(z2.string(), z2.string()).default({}),
|
|
4446
|
+
toolLocks: z2.record(z2.string(), z2.string()).default({}),
|
|
4447
|
+
profiles: z2.array(profileSchema2).default([]),
|
|
4448
|
+
tools: z2.array(toolDefSchema).default([])
|
|
4344
4449
|
});
|
|
4345
4450
|
function localRegistryAllowed(env = process.env) {
|
|
4346
4451
|
return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => {
|
|
@@ -4622,25 +4727,34 @@ class ApiStore {
|
|
|
4622
4727
|
}
|
|
4623
4728
|
}
|
|
4624
4729
|
function resolveStore(env = process.env, options = {}) {
|
|
4730
|
+
const legacy = resolveRegistryTransport(ACCOUNTS_APP, env, options.credentials);
|
|
4731
|
+
if (legacy)
|
|
4732
|
+
return new ApiStore(legacy.baseUrl, legacy.apiKey);
|
|
4733
|
+
const current = resolveRegistryTransport("subscriptions", env, options.subscriptionsCredentials);
|
|
4734
|
+
if (current)
|
|
4735
|
+
return new SubscriptionsRegistry(current.baseUrl, env, options.readSubscriptionsMetadata);
|
|
4736
|
+
return unconfiguredStore(env);
|
|
4737
|
+
}
|
|
4738
|
+
function resolveRegistryTransport(app, env, credentials) {
|
|
4625
4739
|
const clientEnv = env;
|
|
4626
|
-
const credential = resolveCredential(
|
|
4740
|
+
const credential = resolveCredential(app, clientEnv, credentials);
|
|
4627
4741
|
if (credential === null) {
|
|
4628
4742
|
try {
|
|
4629
|
-
resolveClientTransport(
|
|
4743
|
+
resolveClientTransport(app, clientEnv, credentials ? { credentials } : {});
|
|
4630
4744
|
} catch (err) {
|
|
4631
4745
|
if (isNoAccountsCredentialConfigurationError(err))
|
|
4632
|
-
return
|
|
4746
|
+
return null;
|
|
4633
4747
|
throw err;
|
|
4634
4748
|
}
|
|
4635
|
-
return
|
|
4749
|
+
return null;
|
|
4636
4750
|
}
|
|
4637
4751
|
if (credential.tier === "pointer") {
|
|
4638
|
-
throw new Error(
|
|
4752
|
+
throw new Error(`${app}: ${credential.source} names a secrets-vault item, and the registry adapter resolves ` + "credentials synchronously, so it cannot complete the pointer per request. It is a deliberate " + "selection and is not resolved around. Use a literal tier instead \u2014 the Keychain item " + `hasna.credentials.${app}.api-key, ~/.hasna/${app}/config/credentials, or HASNA_${app.toUpperCase()}_API_KEY.`);
|
|
4639
4753
|
}
|
|
4640
|
-
const resolution = resolveClientTransport(
|
|
4641
|
-
credentials: { ...
|
|
4754
|
+
const resolution = resolveClientTransport(app, clientEnv, {
|
|
4755
|
+
credentials: { ...credentials, apiKey: credential.apiKey }
|
|
4642
4756
|
});
|
|
4643
|
-
return
|
|
4757
|
+
return { baseUrl: resolution.baseUrl, apiKey: credential.apiKey };
|
|
4644
4758
|
}
|
|
4645
4759
|
function isNoAccountsCredentialConfigurationError(error) {
|
|
4646
4760
|
if (typeof error !== "object" || error === null)
|
|
@@ -4657,10 +4771,10 @@ function unconfiguredStore(env) {
|
|
|
4657
4771
|
return new NoRegistryStore;
|
|
4658
4772
|
}
|
|
4659
4773
|
function accountsLocalModeNotice() {
|
|
4660
|
-
return `accounts: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014
|
|
4774
|
+
return `accounts: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 neither Accounts nor Subscriptions credentials resolved, so ` + "attribution reads the on-box JSON registry (~/.hasna/accounts/accounts.json) instead of accounts-serve. " + "For hosted attribution, configure the Subscriptions CLI through its normal sign-in or credential resolver.";
|
|
4661
4775
|
}
|
|
4662
4776
|
function accountsRefusalNotice() {
|
|
4663
|
-
return "accounts:
|
|
4777
|
+
return "accounts: neither Accounts nor Subscriptions credentials resolved (legacy hasna.credentials.accounts.api-key, " + "~/.hasna/accounts/config/credentials, HASNA_ACCOUNTS_API_KEY; current hasna.credentials.subscriptions.api-key, " + "~/.hasna/subscriptions/config/credentials, HASNA_SUBSCRIPTIONS_API_KEY), so spend attribution is omitted. The " + `on-box registry (~/.hasna/accounts/accounts.json) is NOT read without ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 \u2014 ` + "economy fails closed instead of attributing hosted spend from an on-box file.";
|
|
4664
4778
|
}
|
|
4665
4779
|
var accountsLocalNoticePrinted = false;
|
|
4666
4780
|
function announceAccountsLocalMode() {
|
|
@@ -4748,7 +4862,10 @@ async function profileForEnvDir(store, tool, env) {
|
|
|
4748
4862
|
if (!configuredDir)
|
|
4749
4863
|
return null;
|
|
4750
4864
|
const normalized = normalizeDir(configuredDir);
|
|
4751
|
-
|
|
4865
|
+
const matches = (await store.listProfiles(tool.id)).filter((profile) => normalizeDir(profile.dir) === normalized);
|
|
4866
|
+
if (matches.length > 1)
|
|
4867
|
+
throw new Error("account attribution: multiple profiles match the configured directory");
|
|
4868
|
+
return matches[0] ?? null;
|
|
4752
4869
|
}
|
|
4753
4870
|
async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
4754
4871
|
const override = envOverride(agent, env);
|
|
@@ -4762,8 +4879,15 @@ async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
|
4762
4879
|
continue;
|
|
4763
4880
|
const envProfile = await profileForEnvDir(store, tool, env);
|
|
4764
4881
|
if (envProfile)
|
|
4765
|
-
return fromProfile(envProfile, "env");
|
|
4766
|
-
|
|
4882
|
+
return envProfile.switchedAway ? null : fromProfile(envProfile, "env");
|
|
4883
|
+
if (store.registry === "subscriptions" && env[tool.envVar])
|
|
4884
|
+
return null;
|
|
4885
|
+
if (store.appliedProfile) {
|
|
4886
|
+
const applied = await store.appliedProfile(toolId);
|
|
4887
|
+
if (applied)
|
|
4888
|
+
return applied.switchedAway ? null : fromProfile(applied, "applied");
|
|
4889
|
+
}
|
|
4890
|
+
const appliedName = store.registry === "subscriptions" ? undefined : appliedProfileName(toolId);
|
|
4767
4891
|
if (appliedName) {
|
|
4768
4892
|
const applied = await store.findProfile(appliedName, toolId);
|
|
4769
4893
|
if (applied)
|
|
@@ -7305,7 +7429,7 @@ import { randomUUID } from "crypto";
|
|
|
7305
7429
|
// package.json
|
|
7306
7430
|
var package_default = {
|
|
7307
7431
|
name: "@hasna/economy",
|
|
7308
|
-
version: "0.5.
|
|
7432
|
+
version: "0.5.4",
|
|
7309
7433
|
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
7310
7434
|
repository: {
|
|
7311
7435
|
type: "git",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/economy",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "AI coding cost tracker — CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|