@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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Read Codex and Claude usage and quota credentials from their selected native profile directories while preserving explicit Economy source overrides. A missing selected profile stays missing instead of reading another account's default directory. An explicitly selected Codex profile includes the legacy Codewith reader only when its source is explicitly configured too.
|
|
8
|
+
|
|
9
|
+
## 0.5.4
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 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.
|
|
14
|
+
|
|
3
15
|
## 0.5.3
|
|
4
16
|
|
|
5
17
|
### 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
|
|
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.
|
|
@@ -128,9 +128,13 @@ economy sync --recalculate
|
|
|
128
128
|
economy sync --backfill-machine
|
|
129
129
|
```
|
|
130
130
|
|
|
131
|
-
Full sync also imports active project metadata from `@hasna/projects` when the registry is available.
|
|
131
|
+
Full sync also imports active project metadata from `@hasna/projects` when the registry is available. Codex reads `CODEX_HOME/state_5.sqlite` and Claude reads `CLAUDE_CONFIG_DIR/projects` when those native profile directories are set; otherwise they use `~/.codex/state_5.sqlite` and `~/.claude/projects`. Explicit `HASNA_ECONOMY_CODEX_DB_PATH` and `HASNA_ECONOMY_CLAUDE_PROJECTS_DIR` overrides take precedence. Codex configuration follows `CODEX_HOME/config.toml` unless `HASNA_ECONOMY_CODEX_CONFIG_PATH` is set. Quota credentials follow those same native profile directories, retaining the existing OAuth-token environment overrides. A missing selected source or credential is reported as missing, without falling back to another profile.
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
The existing Codewith compatibility reader uses `~/.codewith/state_5.sqlite`, with `HASNA_ECONOMY_CODEWITH_DB_PATH` as its explicit override. When `CODEX_HOME` or `HASNA_ECONOMY_CODEX_DB_PATH` explicitly selects a Codex source, the compatibility reader runs only if `HASNA_ECONOMY_CODEWITH_DB_PATH` is also set.
|
|
134
|
+
|
|
135
|
+
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.
|
|
136
|
+
|
|
137
|
+
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
138
|
|
|
135
139
|
Session drilldown can be scoped to an account key, account name, or email:
|
|
136
140
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
LOCAL_STORAGE_OPT_IN_KEYS
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-pnnqdv3r.js";
|
|
5
5
|
import {
|
|
6
6
|
assessCostFromDb,
|
|
7
7
|
getIngestState,
|
|
@@ -71,41 +71,148 @@ function sourceTimeMillis(value) {
|
|
|
71
71
|
|
|
72
72
|
// src/ingest/claude.ts
|
|
73
73
|
import { readdirSync, readFileSync as readFileSync2, existsSync as existsSync2, statSync } from "fs";
|
|
74
|
-
import { homedir as
|
|
75
|
-
import { join as
|
|
74
|
+
import { homedir as homedir3 } from "os";
|
|
75
|
+
import { join as join3, basename } from "path";
|
|
76
76
|
|
|
77
77
|
// src/lib/accounts-store.ts
|
|
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
|
-
|
|
84
|
-
|
|
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:
|
|
87
|
-
tool:
|
|
116
|
+
name: nonempty,
|
|
117
|
+
tool: nonempty,
|
|
88
118
|
email: z.string().optional(),
|
|
89
119
|
displayName: z.string().optional(),
|
|
90
|
-
dir:
|
|
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
|
-
|
|
93
|
-
|
|
124
|
+
active: z.boolean(),
|
|
125
|
+
applied: z.boolean(),
|
|
126
|
+
switchedAway: z.object({ profile: nonempty, email: z.string().optional() }).optional()
|
|
127
|
+
});
|
|
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()
|
|
94
201
|
});
|
|
95
|
-
var toolDefSchema =
|
|
96
|
-
id:
|
|
97
|
-
label:
|
|
98
|
-
envVar:
|
|
99
|
-
defaultDir:
|
|
100
|
-
bin:
|
|
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 =
|
|
103
|
-
version:
|
|
104
|
-
current:
|
|
105
|
-
applied:
|
|
106
|
-
toolLocks:
|
|
107
|
-
profiles:
|
|
108
|
-
tools:
|
|
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(
|
|
507
|
+
const credential = resolveCredential(app, clientEnv, credentials);
|
|
392
508
|
if (credential === null) {
|
|
393
509
|
try {
|
|
394
|
-
resolveClientTransport(
|
|
510
|
+
resolveClientTransport(app, clientEnv, credentials ? { credentials } : {});
|
|
395
511
|
} catch (err) {
|
|
396
512
|
if (isNoAccountsCredentialConfigurationError(err))
|
|
397
|
-
return
|
|
513
|
+
return null;
|
|
398
514
|
throw err;
|
|
399
515
|
}
|
|
400
|
-
return
|
|
516
|
+
return null;
|
|
401
517
|
}
|
|
402
518
|
if (credential.tier === "pointer") {
|
|
403
|
-
throw new Error(
|
|
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(
|
|
406
|
-
credentials: { ...
|
|
521
|
+
const resolution = resolveClientTransport(app, clientEnv, {
|
|
522
|
+
credentials: { ...credentials, apiKey: credential.apiKey }
|
|
407
523
|
});
|
|
408
|
-
return
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -553,12 +679,41 @@ function withAccount(record, account) {
|
|
|
553
679
|
};
|
|
554
680
|
}
|
|
555
681
|
|
|
682
|
+
// src/lib/paths.ts
|
|
683
|
+
import { homedir as homedir2 } from "os";
|
|
684
|
+
import { join as join2 } from "path";
|
|
685
|
+
function getHomeDir() {
|
|
686
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir2();
|
|
687
|
+
}
|
|
688
|
+
function agentPaths() {
|
|
689
|
+
const home = getHomeDir();
|
|
690
|
+
const claudeHome = process.env["CLAUDE_CONFIG_DIR"] || join2(home, ".claude");
|
|
691
|
+
const codexHome = process.env["CODEX_HOME"] || join2(home, ".codex");
|
|
692
|
+
return {
|
|
693
|
+
claudeProjects: join2(claudeHome, "projects"),
|
|
694
|
+
claudeCredentials: join2(claudeHome, ".credentials.json"),
|
|
695
|
+
takumiProjects: join2(home, ".takumi", "projects"),
|
|
696
|
+
codexDir: codexHome,
|
|
697
|
+
codexDb: join2(codexHome, "state_5.sqlite"),
|
|
698
|
+
codexAuth: join2(codexHome, "auth.json"),
|
|
699
|
+
codexConfig: join2(codexHome, "config.toml"),
|
|
700
|
+
geminiTmp: join2(home, ".gemini", "tmp"),
|
|
701
|
+
geminiHistory: join2(home, ".gemini", "history"),
|
|
702
|
+
opencodeMessages: join2(home, ".local", "share", "opencode", "storage", "message"),
|
|
703
|
+
piSessions: join2(home, ".pi", "agent", "sessions"),
|
|
704
|
+
hermesDir: join2(home, ".hermes"),
|
|
705
|
+
hermesDb: join2(home, ".hermes", "state.db")
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
|
|
556
709
|
// src/ingest/claude.ts
|
|
557
710
|
function autoDetectProject(cwd, projects) {
|
|
558
711
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
559
712
|
}
|
|
560
|
-
var
|
|
561
|
-
|
|
713
|
+
var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
|
|
714
|
+
function claudeProjectsDir() {
|
|
715
|
+
return process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? agentPaths().claudeProjects;
|
|
716
|
+
}
|
|
562
717
|
function dirNameToPath(dirName) {
|
|
563
718
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
564
719
|
}
|
|
@@ -568,9 +723,9 @@ function collectJsonlFiles(projectDir) {
|
|
|
568
723
|
try {
|
|
569
724
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
570
725
|
if (entry.isDirectory())
|
|
571
|
-
walk(
|
|
726
|
+
walk(join3(dir, entry.name));
|
|
572
727
|
else if (entry.name.endsWith(".jsonl"))
|
|
573
|
-
files.push(
|
|
728
|
+
files.push(join3(dir, entry.name));
|
|
574
729
|
}
|
|
575
730
|
} catch {
|
|
576
731
|
collectionProblem("failed", "source_unreadable");
|
|
@@ -579,7 +734,7 @@ function collectJsonlFiles(projectDir) {
|
|
|
579
734
|
walk(projectDir);
|
|
580
735
|
return files;
|
|
581
736
|
}
|
|
582
|
-
async function ingestClaude(db, verbose = false, projectsDir =
|
|
737
|
+
async function ingestClaude(db, verbose = false, projectsDir = claudeProjectsDir()) {
|
|
583
738
|
return ingestJsonlProjects(db, projectsDir, "claude", verbose);
|
|
584
739
|
}
|
|
585
740
|
async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
|
|
@@ -601,7 +756,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
601
756
|
const ingestState = loadIngestState(db, agentName);
|
|
602
757
|
const projectDirs = readdirSync(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
603
758
|
for (const projectDirEntry of projectDirs) {
|
|
604
|
-
const projectDirPath =
|
|
759
|
+
const projectDirPath = join3(projectsDir, projectDirEntry.name);
|
|
605
760
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
606
761
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
607
762
|
for (const filePath of jsonlFiles) {
|
|
@@ -750,24 +905,19 @@ function supportsClaudeDataResidencyPricing(model) {
|
|
|
750
905
|
// src/ingest/codex.ts
|
|
751
906
|
import { existsSync as existsSync3, readFileSync as readFileSync3, statSync as statSync2 } from "fs";
|
|
752
907
|
import { createHash } from "crypto";
|
|
753
|
-
import {
|
|
754
|
-
import { join as join3, basename as basename2 } from "path";
|
|
755
|
-
var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
|
|
756
|
-
var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
|
|
757
|
-
var DEFAULT_CODEWITH_DB_PATH = join3(homedir3(), ".codewith", "state_5.sqlite");
|
|
758
|
-
var DEFAULT_CODEWITH_CONFIG_PATH = join3(homedir3(), ".codewith", "config.toml");
|
|
908
|
+
import { join as join4, basename as basename2 } from "path";
|
|
759
909
|
var CODEX_INGEST_VERSION = "rollout-events-v4";
|
|
760
910
|
function codexDbPath() {
|
|
761
|
-
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ??
|
|
911
|
+
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? agentPaths().codexDb;
|
|
762
912
|
}
|
|
763
913
|
function codexConfigPath() {
|
|
764
|
-
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ??
|
|
914
|
+
return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? agentPaths().codexConfig;
|
|
765
915
|
}
|
|
766
916
|
function codewithDbPath() {
|
|
767
|
-
return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ??
|
|
917
|
+
return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? join4(getHomeDir(), ".codewith", "state_5.sqlite");
|
|
768
918
|
}
|
|
769
919
|
function codewithConfigPath() {
|
|
770
|
-
return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ??
|
|
920
|
+
return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? join4(getHomeDir(), ".codewith", "config.toml");
|
|
771
921
|
}
|
|
772
922
|
function codexSources() {
|
|
773
923
|
const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
|
|
@@ -779,7 +929,7 @@ function codexSources() {
|
|
|
779
929
|
sessionPrefix: "codex",
|
|
780
930
|
stateSource: "codex"
|
|
781
931
|
}];
|
|
782
|
-
if (!explicitCodexPath || explicitCodewithPath) {
|
|
932
|
+
if (!explicitCodexPath && !process.env["CODEX_HOME"] || explicitCodewithPath) {
|
|
783
933
|
sources.push({
|
|
784
934
|
label: "Codewith",
|
|
785
935
|
dbPath: codewithDbPath(),
|
|
@@ -1017,9 +1167,9 @@ async function ingestCodex(db, verbose = false) {
|
|
|
1017
1167
|
// src/ingest/gemini.ts
|
|
1018
1168
|
import { readdirSync as readdirSync2, readFileSync as readFileSync4, existsSync as existsSync4, statSync as statSync3 } from "fs";
|
|
1019
1169
|
import { homedir as homedir4 } from "os";
|
|
1020
|
-
import { join as
|
|
1021
|
-
var DEFAULT_GEMINI_TMP_DIR =
|
|
1022
|
-
var DEFAULT_GEMINI_HISTORY_DIR =
|
|
1170
|
+
import { join as join5, basename as basename3 } from "path";
|
|
1171
|
+
var DEFAULT_GEMINI_TMP_DIR = join5(homedir4(), ".gemini", "tmp");
|
|
1172
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir4(), ".gemini", "history");
|
|
1023
1173
|
function geminiTmpDir() {
|
|
1024
1174
|
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
1025
1175
|
}
|
|
@@ -1041,7 +1191,7 @@ function listProjectDirs(...roots) {
|
|
|
1041
1191
|
try {
|
|
1042
1192
|
for (const entry of readdirSync2(root, { withFileTypes: true })) {
|
|
1043
1193
|
if (entry.isDirectory())
|
|
1044
|
-
dirs.add(
|
|
1194
|
+
dirs.add(join5(root, entry.name));
|
|
1045
1195
|
}
|
|
1046
1196
|
} catch {
|
|
1047
1197
|
collectionProblem("failed", "source_unreadable");
|
|
@@ -1054,7 +1204,7 @@ function projectRoot(projectDir, chatData) {
|
|
|
1054
1204
|
return chatData.projectPath;
|
|
1055
1205
|
if (chatData.project_path)
|
|
1056
1206
|
return chatData.project_path;
|
|
1057
|
-
const rootFile =
|
|
1207
|
+
const rootFile = join5(projectDir, ".project_root");
|
|
1058
1208
|
try {
|
|
1059
1209
|
if (existsSync4(rootFile))
|
|
1060
1210
|
return readFileSync4(rootFile, "utf-8").trim();
|
|
@@ -1079,12 +1229,12 @@ async function ingestGemini(db, verbose) {
|
|
|
1079
1229
|
const account = await resolveAccountForAgent("gemini");
|
|
1080
1230
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
1081
1231
|
for (const projectDir of projectDirs) {
|
|
1082
|
-
const chatsDir =
|
|
1232
|
+
const chatsDir = join5(projectDir, "chats");
|
|
1083
1233
|
if (!existsSync4(chatsDir))
|
|
1084
1234
|
continue;
|
|
1085
1235
|
let chatFiles = [];
|
|
1086
1236
|
try {
|
|
1087
|
-
chatFiles = readdirSync2(chatsDir).filter((f) => f.endsWith(".json")).map((f) =>
|
|
1237
|
+
chatFiles = readdirSync2(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join5(chatsDir, f));
|
|
1088
1238
|
} catch {
|
|
1089
1239
|
collectionProblem("failed", "source_unreadable");
|
|
1090
1240
|
continue;
|
|
@@ -1194,13 +1344,13 @@ async function ingestGemini(db, verbose) {
|
|
|
1194
1344
|
// src/ingest/opencode.ts
|
|
1195
1345
|
import { existsSync as existsSync5, readFileSync as readFileSync5, readdirSync as readdirSync3, statSync as statSync4 } from "fs";
|
|
1196
1346
|
import { homedir as homedir5 } from "os";
|
|
1197
|
-
import { join as
|
|
1198
|
-
var OPENCODE_STORAGE =
|
|
1347
|
+
import { join as join6 } from "path";
|
|
1348
|
+
var OPENCODE_STORAGE = join6(homedir5(), ".local", "share", "opencode", "storage");
|
|
1199
1349
|
function walkJsonFiles(dir, acc = []) {
|
|
1200
1350
|
if (!existsSync5(dir))
|
|
1201
1351
|
return acc;
|
|
1202
1352
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
1203
|
-
const p =
|
|
1353
|
+
const p = join6(dir, entry.name);
|
|
1204
1354
|
if (entry.isDirectory())
|
|
1205
1355
|
walkJsonFiles(p, acc);
|
|
1206
1356
|
else if (entry.name.endsWith(".json"))
|
|
@@ -1220,7 +1370,7 @@ function parseSessionIdFromPath(filePath) {
|
|
|
1220
1370
|
return null;
|
|
1221
1371
|
}
|
|
1222
1372
|
async function ingestOpenCode(db, verbose = false, sourceDir = OPENCODE_STORAGE) {
|
|
1223
|
-
const messageDir =
|
|
1373
|
+
const messageDir = join6(sourceDir, "message");
|
|
1224
1374
|
if (!existsSync5(messageDir))
|
|
1225
1375
|
collectionProblem("missing", "source_missing");
|
|
1226
1376
|
const files = walkJsonFiles(messageDir);
|
|
@@ -1412,13 +1562,13 @@ async function ingestCursor(db, verbose = false) {
|
|
|
1412
1562
|
// src/ingest/pi.ts
|
|
1413
1563
|
import { existsSync as existsSync6, readFileSync as readFileSync6, readdirSync as readdirSync4, statSync as statSync5 } from "fs";
|
|
1414
1564
|
import { homedir as homedir6 } from "os";
|
|
1415
|
-
import { join as
|
|
1416
|
-
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ??
|
|
1565
|
+
import { join as join7 } from "path";
|
|
1566
|
+
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join7(homedir6(), ".pi", "agent", "sessions");
|
|
1417
1567
|
function walkSessions(dir, acc = []) {
|
|
1418
1568
|
if (!existsSync6(dir))
|
|
1419
1569
|
return acc;
|
|
1420
1570
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
1421
|
-
const p =
|
|
1571
|
+
const p = join7(dir, entry.name);
|
|
1422
1572
|
if (entry.isDirectory())
|
|
1423
1573
|
walkSessions(p, acc);
|
|
1424
1574
|
else if (entry.name.endsWith(".json"))
|
|
@@ -1518,8 +1668,8 @@ async function ingestPi(db, verbose = false, sourceDir = PI_SESSION_DIR) {
|
|
|
1518
1668
|
// src/ingest/hermes.ts
|
|
1519
1669
|
import { existsSync as existsSync7, statSync as statSync6 } from "fs";
|
|
1520
1670
|
import { homedir as homedir7 } from "os";
|
|
1521
|
-
import { join as
|
|
1522
|
-
var HERMES_DB =
|
|
1671
|
+
import { join as join8 } from "path";
|
|
1672
|
+
var HERMES_DB = join8(homedir7(), ".hermes", "state.db");
|
|
1523
1673
|
async function ingestHermes(db, verbose = false, sourcePath = HERMES_DB) {
|
|
1524
1674
|
if (!existsSync7(sourcePath)) {
|
|
1525
1675
|
collectionProblem("missing", "source_missing");
|
|
@@ -1587,7 +1737,7 @@ function statSyncSafe(path) {
|
|
|
1587
1737
|
// src/ingest/loops.ts
|
|
1588
1738
|
import { existsSync as existsSync8 } from "fs";
|
|
1589
1739
|
import { homedir as homedir8 } from "os";
|
|
1590
|
-
import { join as
|
|
1740
|
+
import { join as join9 } from "path";
|
|
1591
1741
|
import { homedir as pathsResolverHomedir } from "os";
|
|
1592
1742
|
import { join as pathsResolverJoin } from "path";
|
|
1593
1743
|
var PATHS_RESOLVER_KIND_ENV = {
|
|
@@ -1654,8 +1804,8 @@ function defaultLoopsDbPath() {
|
|
|
1654
1804
|
app: "loops",
|
|
1655
1805
|
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
1656
1806
|
});
|
|
1657
|
-
const legacy =
|
|
1658
|
-
return existsSync8(
|
|
1807
|
+
const legacy = join9(process.env["HOME"] || process.env["USERPROFILE"] || homedir8(), ".hasna", "loops", "loops.db");
|
|
1808
|
+
return existsSync8(join9(resolved, "loops.db")) ? join9(resolved, "loops.db") : legacy;
|
|
1659
1809
|
}
|
|
1660
1810
|
function loopsDbPath() {
|
|
1661
1811
|
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
|
|
@@ -1839,33 +1989,6 @@ async function ingestLoops(db, verbose = false) {
|
|
|
1839
1989
|
|
|
1840
1990
|
// src/ingest/claude-quota.ts
|
|
1841
1991
|
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "fs";
|
|
1842
|
-
|
|
1843
|
-
// src/lib/paths.ts
|
|
1844
|
-
import { homedir as homedir9 } from "os";
|
|
1845
|
-
import { join as join9 } from "path";
|
|
1846
|
-
function getHomeDir() {
|
|
1847
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
|
|
1848
|
-
}
|
|
1849
|
-
function agentPaths() {
|
|
1850
|
-
const home = getHomeDir();
|
|
1851
|
-
return {
|
|
1852
|
-
claudeProjects: join9(home, ".claude", "projects"),
|
|
1853
|
-
claudeCredentials: join9(home, ".claude", ".credentials.json"),
|
|
1854
|
-
takumiProjects: join9(home, ".takumi", "projects"),
|
|
1855
|
-
codexDir: join9(home, ".codex"),
|
|
1856
|
-
codexDb: join9(home, ".codex", "state_5.sqlite"),
|
|
1857
|
-
codexAuth: join9(home, ".codex", "auth.json"),
|
|
1858
|
-
codexConfig: join9(home, ".codex", "config.toml"),
|
|
1859
|
-
geminiTmp: join9(home, ".gemini", "tmp"),
|
|
1860
|
-
geminiHistory: join9(home, ".gemini", "history"),
|
|
1861
|
-
opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
|
|
1862
|
-
piSessions: join9(home, ".pi", "agent", "sessions"),
|
|
1863
|
-
hermesDir: join9(home, ".hermes"),
|
|
1864
|
-
hermesDb: join9(home, ".hermes", "state.db")
|
|
1865
|
-
};
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
// src/ingest/claude-quota.ts
|
|
1869
1992
|
var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
|
1870
1993
|
var OAUTH_BETA = "oauth-2025-04-20";
|
|
1871
1994
|
function readClaudeToken() {
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
// package.json
|
|
21
21
|
var package_default = {
|
|
22
22
|
name: "@hasna/economy",
|
|
23
|
-
version: "0.5.
|
|
23
|
+
version: "0.5.5",
|
|
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",
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
} from "./index-w3bvv0ns.js";
|
|
6
6
|
import {
|
|
7
7
|
syncAll
|
|
8
|
-
} from "./index-
|
|
8
|
+
} from "./index-61c0vz9p.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-
|
|
19
|
+
} from "./index-pnnqdv3r.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-
|
|
9
|
+
} from "./index-7k1xt8mj.js";
|
|
10
10
|
import"./index-w3bvv0ns.js";
|
|
11
|
-
import"./index-
|
|
12
|
-
import"./index-
|
|
11
|
+
import"./index-z9psed59.js";
|
|
12
|
+
import"./index-pnnqdv3r.js";
|
|
13
13
|
import"./index-f5jv6q5q.js";
|
|
14
14
|
import"./index-p35cyhc4.js";
|
|
15
15
|
export {
|