@hasna/economy 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +7 -3
- package/dist/chunks/{index-s9mvven6.js → index-61c0vz9p.js} +223 -100
- package/dist/chunks/{index-bn04v378.js → index-7k1xt8mj.js} +1 -1
- package/dist/chunks/{index-1vz1arz5.js → index-pnnqdv3r.js} +1 -1
- package/dist/chunks/{index-mhrkwvtk.js → index-z9psed59.js} +1 -1
- package/dist/chunks/{serve-8yqe7nhs.js → serve-zp5a3qzw.js} +2 -2
- package/dist/chunks/{tui-fe94bvp9.js → tui-2yea642b.js} +3 -3
- package/dist/chunks/{watch-ct8dvrpr.js → watch-811vncjz.js} +3 -3
- package/dist/cli/index.js +10 -10
- package/dist/index.js +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts.map +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/paths.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 +290 -167
- package/dist/otel/index.js +87 -65
- package/dist/server/index.js +231 -110
- package/dist/server/service-worker.js +222 -101
- package/package.json +1 -1
|
@@ -4288,14 +4288,14 @@ function sourceTimeMillis(value) {
|
|
|
4288
4288
|
init_database();
|
|
4289
4289
|
init_pricing();
|
|
4290
4290
|
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync5, statSync as statSync2 } from "fs";
|
|
4291
|
-
import { homedir as
|
|
4292
|
-
import { join as
|
|
4291
|
+
import { homedir as homedir4 } from "os";
|
|
4292
|
+
import { join as join5, basename } from "path";
|
|
4293
4293
|
|
|
4294
4294
|
// src/lib/accounts-store.ts
|
|
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()
|
|
4360
|
+
});
|
|
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()
|
|
4329
4434
|
});
|
|
4330
|
-
var toolDefSchema =
|
|
4331
|
-
id:
|
|
4332
|
-
label:
|
|
4333
|
-
envVar:
|
|
4334
|
-
defaultDir:
|
|
4335
|
-
bin:
|
|
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)
|
|
@@ -4788,12 +4912,41 @@ function withAccount(record, account) {
|
|
|
4788
4912
|
};
|
|
4789
4913
|
}
|
|
4790
4914
|
|
|
4915
|
+
// src/lib/paths.ts
|
|
4916
|
+
import { homedir as homedir3 } from "os";
|
|
4917
|
+
import { join as join4 } from "path";
|
|
4918
|
+
function getHomeDir2() {
|
|
4919
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir3();
|
|
4920
|
+
}
|
|
4921
|
+
function agentPaths() {
|
|
4922
|
+
const home = getHomeDir2();
|
|
4923
|
+
const claudeHome = process.env["CLAUDE_CONFIG_DIR"] || join4(home, ".claude");
|
|
4924
|
+
const codexHome = process.env["CODEX_HOME"] || join4(home, ".codex");
|
|
4925
|
+
return {
|
|
4926
|
+
claudeProjects: join4(claudeHome, "projects"),
|
|
4927
|
+
claudeCredentials: join4(claudeHome, ".credentials.json"),
|
|
4928
|
+
takumiProjects: join4(home, ".takumi", "projects"),
|
|
4929
|
+
codexDir: codexHome,
|
|
4930
|
+
codexDb: join4(codexHome, "state_5.sqlite"),
|
|
4931
|
+
codexAuth: join4(codexHome, "auth.json"),
|
|
4932
|
+
codexConfig: join4(codexHome, "config.toml"),
|
|
4933
|
+
geminiTmp: join4(home, ".gemini", "tmp"),
|
|
4934
|
+
geminiHistory: join4(home, ".gemini", "history"),
|
|
4935
|
+
opencodeMessages: join4(home, ".local", "share", "opencode", "storage", "message"),
|
|
4936
|
+
piSessions: join4(home, ".pi", "agent", "sessions"),
|
|
4937
|
+
hermesDir: join4(home, ".hermes"),
|
|
4938
|
+
hermesDb: join4(home, ".hermes", "state.db")
|
|
4939
|
+
};
|
|
4940
|
+
}
|
|
4941
|
+
|
|
4791
4942
|
// src/ingest/claude.ts
|
|
4792
4943
|
function autoDetectProject(cwd, projects) {
|
|
4793
4944
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
4794
4945
|
}
|
|
4795
|
-
var
|
|
4796
|
-
|
|
4946
|
+
var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join5(homedir4(), ".takumi", "projects");
|
|
4947
|
+
function claudeProjectsDir() {
|
|
4948
|
+
return process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? agentPaths().claudeProjects;
|
|
4949
|
+
}
|
|
4797
4950
|
function dirNameToPath(dirName) {
|
|
4798
4951
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
4799
4952
|
}
|
|
@@ -4803,9 +4956,9 @@ function collectJsonlFiles(projectDir) {
|
|
|
4803
4956
|
try {
|
|
4804
4957
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
4805
4958
|
if (entry.isDirectory())
|
|
4806
|
-
walk(
|
|
4959
|
+
walk(join5(dir, entry.name));
|
|
4807
4960
|
else if (entry.name.endsWith(".jsonl"))
|
|
4808
|
-
files.push(
|
|
4961
|
+
files.push(join5(dir, entry.name));
|
|
4809
4962
|
}
|
|
4810
4963
|
} catch {
|
|
4811
4964
|
collectionProblem("failed", "source_unreadable");
|
|
@@ -4814,7 +4967,7 @@ function collectJsonlFiles(projectDir) {
|
|
|
4814
4967
|
walk(projectDir);
|
|
4815
4968
|
return files;
|
|
4816
4969
|
}
|
|
4817
|
-
async function ingestClaude(db, verbose = false, projectsDir =
|
|
4970
|
+
async function ingestClaude(db, verbose = false, projectsDir = claudeProjectsDir()) {
|
|
4818
4971
|
return ingestJsonlProjects(db, projectsDir, "claude", verbose);
|
|
4819
4972
|
}
|
|
4820
4973
|
async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
|
|
@@ -4836,7 +4989,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
4836
4989
|
const ingestState = loadIngestState(db, agentName);
|
|
4837
4990
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
4838
4991
|
for (const projectDirEntry of projectDirs) {
|
|
4839
|
-
const projectDirPath =
|
|
4992
|
+
const projectDirPath = join5(projectsDir, projectDirEntry.name);
|
|
4840
4993
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
4841
4994
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
4842
4995
|
for (const filePath of jsonlFiles) {
|
|
@@ -4987,24 +5140,19 @@ init_database();
|
|
|
4987
5140
|
init_pricing();
|
|
4988
5141
|
import { existsSync as existsSync6, readFileSync as readFileSync3, statSync as statSync3 } from "fs";
|
|
4989
5142
|
import { createHash as createHash4 } from "crypto";
|
|
4990
|
-
import {
|
|
4991
|
-
import { join as join5, basename as basename2 } from "path";
|
|
4992
|
-
var DEFAULT_CODEX_DB_PATH = join5(homedir4(), ".codex", "state_5.sqlite");
|
|
4993
|
-
var DEFAULT_CODEX_CONFIG_PATH = join5(homedir4(), ".codex", "config.toml");
|
|
4994
|
-
var DEFAULT_CODEWITH_DB_PATH = join5(homedir4(), ".codewith", "state_5.sqlite");
|
|
4995
|
-
var DEFAULT_CODEWITH_CONFIG_PATH = join5(homedir4(), ".codewith", "config.toml");
|
|
5143
|
+
import { join as join6, basename as basename2 } from "path";
|
|
4996
5144
|
var CODEX_INGEST_VERSION = "rollout-events-v4";
|
|
4997
5145
|
function codexDbPath() {
|
|
4998
|
-
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ??
|
|
5146
|
+
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? agentPaths().codexDb;
|
|
4999
5147
|
}
|
|
5000
5148
|
function codexConfigPath() {
|
|
5001
|
-
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ??
|
|
5149
|
+
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? agentPaths().codexConfig;
|
|
5002
5150
|
}
|
|
5003
5151
|
function codewithDbPath() {
|
|
5004
|
-
return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ??
|
|
5152
|
+
return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? join6(getHomeDir2(), ".codewith", "state_5.sqlite");
|
|
5005
5153
|
}
|
|
5006
5154
|
function codewithConfigPath() {
|
|
5007
|
-
return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ??
|
|
5155
|
+
return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? join6(getHomeDir2(), ".codewith", "config.toml");
|
|
5008
5156
|
}
|
|
5009
5157
|
function codexSources() {
|
|
5010
5158
|
const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
|
|
@@ -5016,7 +5164,7 @@ function codexSources() {
|
|
|
5016
5164
|
sessionPrefix: "codex",
|
|
5017
5165
|
stateSource: "codex"
|
|
5018
5166
|
}];
|
|
5019
|
-
if (!explicitCodexPath || explicitCodewithPath) {
|
|
5167
|
+
if (!explicitCodexPath && !process.env["CODEX_HOME"] || explicitCodewithPath) {
|
|
5020
5168
|
sources.push({
|
|
5021
5169
|
label: "Codewith",
|
|
5022
5170
|
dbPath: codewithDbPath(),
|
|
@@ -5256,9 +5404,9 @@ init_database();
|
|
|
5256
5404
|
init_pricing();
|
|
5257
5405
|
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync7, statSync as statSync4 } from "fs";
|
|
5258
5406
|
import { homedir as homedir5 } from "os";
|
|
5259
|
-
import { join as
|
|
5260
|
-
var DEFAULT_GEMINI_TMP_DIR =
|
|
5261
|
-
var DEFAULT_GEMINI_HISTORY_DIR =
|
|
5407
|
+
import { join as join7, basename as basename3 } from "path";
|
|
5408
|
+
var DEFAULT_GEMINI_TMP_DIR = join7(homedir5(), ".gemini", "tmp");
|
|
5409
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join7(homedir5(), ".gemini", "history");
|
|
5262
5410
|
function geminiTmpDir() {
|
|
5263
5411
|
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
5264
5412
|
}
|
|
@@ -5280,7 +5428,7 @@ function listProjectDirs(...roots) {
|
|
|
5280
5428
|
try {
|
|
5281
5429
|
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
5282
5430
|
if (entry.isDirectory())
|
|
5283
|
-
dirs.add(
|
|
5431
|
+
dirs.add(join7(root, entry.name));
|
|
5284
5432
|
}
|
|
5285
5433
|
} catch {
|
|
5286
5434
|
collectionProblem("failed", "source_unreadable");
|
|
@@ -5293,7 +5441,7 @@ function projectRoot(projectDir, chatData) {
|
|
|
5293
5441
|
return chatData.projectPath;
|
|
5294
5442
|
if (chatData.project_path)
|
|
5295
5443
|
return chatData.project_path;
|
|
5296
|
-
const rootFile =
|
|
5444
|
+
const rootFile = join7(projectDir, ".project_root");
|
|
5297
5445
|
try {
|
|
5298
5446
|
if (existsSync7(rootFile))
|
|
5299
5447
|
return readFileSync4(rootFile, "utf-8").trim();
|
|
@@ -5318,12 +5466,12 @@ async function ingestGemini(db, verbose) {
|
|
|
5318
5466
|
const account = await resolveAccountForAgent("gemini");
|
|
5319
5467
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
5320
5468
|
for (const projectDir of projectDirs) {
|
|
5321
|
-
const chatsDir =
|
|
5469
|
+
const chatsDir = join7(projectDir, "chats");
|
|
5322
5470
|
if (!existsSync7(chatsDir))
|
|
5323
5471
|
continue;
|
|
5324
5472
|
let chatFiles = [];
|
|
5325
5473
|
try {
|
|
5326
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) =>
|
|
5474
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join7(chatsDir, f));
|
|
5327
5475
|
} catch {
|
|
5328
5476
|
collectionProblem("failed", "source_unreadable");
|
|
5329
5477
|
continue;
|
|
@@ -5435,13 +5583,13 @@ init_database();
|
|
|
5435
5583
|
init_pricing();
|
|
5436
5584
|
import { existsSync as existsSync8, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
5437
5585
|
import { homedir as homedir6 } from "os";
|
|
5438
|
-
import { join as
|
|
5439
|
-
var OPENCODE_STORAGE =
|
|
5586
|
+
import { join as join8 } from "path";
|
|
5587
|
+
var OPENCODE_STORAGE = join8(homedir6(), ".local", "share", "opencode", "storage");
|
|
5440
5588
|
function walkJsonFiles(dir, acc = []) {
|
|
5441
5589
|
if (!existsSync8(dir))
|
|
5442
5590
|
return acc;
|
|
5443
5591
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
5444
|
-
const p =
|
|
5592
|
+
const p = join8(dir, entry.name);
|
|
5445
5593
|
if (entry.isDirectory())
|
|
5446
5594
|
walkJsonFiles(p, acc);
|
|
5447
5595
|
else if (entry.name.endsWith(".json"))
|
|
@@ -5461,7 +5609,7 @@ function parseSessionIdFromPath(filePath) {
|
|
|
5461
5609
|
return null;
|
|
5462
5610
|
}
|
|
5463
5611
|
async function ingestOpenCode(db, verbose = false, sourceDir = OPENCODE_STORAGE) {
|
|
5464
|
-
const messageDir =
|
|
5612
|
+
const messageDir = join8(sourceDir, "message");
|
|
5465
5613
|
if (!existsSync8(messageDir))
|
|
5466
5614
|
collectionProblem("missing", "source_missing");
|
|
5467
5615
|
const files = walkJsonFiles(messageDir);
|
|
@@ -5656,13 +5804,13 @@ async function ingestCursor(db, verbose = false) {
|
|
|
5656
5804
|
init_database();
|
|
5657
5805
|
import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync6 } from "fs";
|
|
5658
5806
|
import { homedir as homedir7 } from "os";
|
|
5659
|
-
import { join as
|
|
5660
|
-
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ??
|
|
5807
|
+
import { join as join9 } from "path";
|
|
5808
|
+
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join9(homedir7(), ".pi", "agent", "sessions");
|
|
5661
5809
|
function walkSessions(dir, acc = []) {
|
|
5662
5810
|
if (!existsSync9(dir))
|
|
5663
5811
|
return acc;
|
|
5664
5812
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
5665
|
-
const p =
|
|
5813
|
+
const p = join9(dir, entry.name);
|
|
5666
5814
|
if (entry.isDirectory())
|
|
5667
5815
|
walkSessions(p, acc);
|
|
5668
5816
|
else if (entry.name.endsWith(".json"))
|
|
@@ -5763,8 +5911,8 @@ async function ingestPi(db, verbose = false, sourceDir = PI_SESSION_DIR) {
|
|
|
5763
5911
|
init_database();
|
|
5764
5912
|
import { existsSync as existsSync10, statSync as statSync7 } from "fs";
|
|
5765
5913
|
import { homedir as homedir8 } from "os";
|
|
5766
|
-
import { join as
|
|
5767
|
-
var HERMES_DB =
|
|
5914
|
+
import { join as join10 } from "path";
|
|
5915
|
+
var HERMES_DB = join10(homedir8(), ".hermes", "state.db");
|
|
5768
5916
|
async function ingestHermes(db, verbose = false, sourcePath = HERMES_DB) {
|
|
5769
5917
|
if (!existsSync10(sourcePath)) {
|
|
5770
5918
|
collectionProblem("missing", "source_missing");
|
|
@@ -5834,7 +5982,7 @@ init_database();
|
|
|
5834
5982
|
init_pricing();
|
|
5835
5983
|
import { existsSync as existsSync11 } from "fs";
|
|
5836
5984
|
import { homedir as homedir9 } from "os";
|
|
5837
|
-
import { join as
|
|
5985
|
+
import { join as join11 } from "path";
|
|
5838
5986
|
import { homedir as pathsResolverHomedir2 } from "os";
|
|
5839
5987
|
import { join as pathsResolverJoin2 } from "path";
|
|
5840
5988
|
var PATHS_RESOLVER_KIND_ENV2 = {
|
|
@@ -5901,8 +6049,8 @@ function defaultLoopsDbPath() {
|
|
|
5901
6049
|
app: "loops",
|
|
5902
6050
|
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
5903
6051
|
});
|
|
5904
|
-
const legacy =
|
|
5905
|
-
return existsSync11(
|
|
6052
|
+
const legacy = join11(process.env["HOME"] || process.env["USERPROFILE"] || homedir9(), ".hasna", "loops", "loops.db");
|
|
6053
|
+
return existsSync11(join11(resolved, "loops.db")) ? join11(resolved, "loops.db") : legacy;
|
|
5906
6054
|
}
|
|
5907
6055
|
function loopsDbPath() {
|
|
5908
6056
|
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
|
|
@@ -6086,33 +6234,6 @@ async function ingestLoops(db, verbose = false) {
|
|
|
6086
6234
|
|
|
6087
6235
|
// src/ingest/claude-quota.ts
|
|
6088
6236
|
import { existsSync as existsSync12, readFileSync as readFileSync7 } from "fs";
|
|
6089
|
-
|
|
6090
|
-
// src/lib/paths.ts
|
|
6091
|
-
import { homedir as homedir10 } from "os";
|
|
6092
|
-
import { join as join11 } from "path";
|
|
6093
|
-
function getHomeDir2() {
|
|
6094
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir10();
|
|
6095
|
-
}
|
|
6096
|
-
function agentPaths() {
|
|
6097
|
-
const home = getHomeDir2();
|
|
6098
|
-
return {
|
|
6099
|
-
claudeProjects: join11(home, ".claude", "projects"),
|
|
6100
|
-
claudeCredentials: join11(home, ".claude", ".credentials.json"),
|
|
6101
|
-
takumiProjects: join11(home, ".takumi", "projects"),
|
|
6102
|
-
codexDir: join11(home, ".codex"),
|
|
6103
|
-
codexDb: join11(home, ".codex", "state_5.sqlite"),
|
|
6104
|
-
codexAuth: join11(home, ".codex", "auth.json"),
|
|
6105
|
-
codexConfig: join11(home, ".codex", "config.toml"),
|
|
6106
|
-
geminiTmp: join11(home, ".gemini", "tmp"),
|
|
6107
|
-
geminiHistory: join11(home, ".gemini", "history"),
|
|
6108
|
-
opencodeMessages: join11(home, ".local", "share", "opencode", "storage", "message"),
|
|
6109
|
-
piSessions: join11(home, ".pi", "agent", "sessions"),
|
|
6110
|
-
hermesDir: join11(home, ".hermes"),
|
|
6111
|
-
hermesDb: join11(home, ".hermes", "state.db")
|
|
6112
|
-
};
|
|
6113
|
-
}
|
|
6114
|
-
|
|
6115
|
-
// src/ingest/claude-quota.ts
|
|
6116
6237
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
6117
6238
|
var OAUTH_BETA = "oauth-2025-04-20";
|
|
6118
6239
|
function readClaudeToken() {
|
|
@@ -7305,7 +7426,7 @@ import { randomUUID } from "crypto";
|
|
|
7305
7426
|
// package.json
|
|
7306
7427
|
var package_default = {
|
|
7307
7428
|
name: "@hasna/economy",
|
|
7308
|
-
version: "0.5.
|
|
7429
|
+
version: "0.5.5",
|
|
7309
7430
|
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
7431
|
repository: {
|
|
7311
7432
|
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.5",
|
|
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",
|