@hasna/economy 0.5.2 → 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 +12 -0
- package/README.md +4 -2
- package/dist/chunks/{billing-bkpxy4kv.js → billing-22az60hy.js} +2 -2
- package/dist/chunks/{config-j9mv3ehk.js → config-hsp7ndhj.js} +2 -2
- package/dist/chunks/{index-npsjjv96.js → index-9arv0b1j.js} +162 -36
- package/dist/chunks/{index-hesxq90g.js → index-9wa1f2kv.js} +1 -1
- package/dist/chunks/{index-zpvea08j.js → index-c7h2rxx1.js} +38 -12
- package/dist/chunks/{index-9d2aky06.js → index-ct3zdefz.js} +3 -3
- package/dist/chunks/{index-k540hfww.js → index-dk5rq1rq.js} +1 -1
- package/dist/chunks/{index-ez9thsr7.js → index-f5jv6q5q.js} +38 -12
- package/dist/chunks/{index-dj3tctqh.js → index-n1wwqxfe.js} +14 -13
- package/dist/chunks/{index-kzwqw3ve.js → index-nheq2nrz.js} +2 -2
- package/dist/chunks/{index-2hwzmfgv.js → index-vajzfzgv.js} +38 -12
- package/dist/chunks/{open-projects-6z4pq6mx.js → open-projects-8qm46m3d.js} +1 -1
- package/dist/chunks/{pricing-vf2pycmh.js → pricing-462f8xkm.js} +1 -1
- package/dist/chunks/{pricing-f80p81gr.js → pricing-k9wsjwbz.js} +1 -1
- package/dist/chunks/{pricing-y21mcjsa.js → pricing-vxejj1qa.js} +1 -1
- package/dist/chunks/{serve-p2yarhm8.js → serve-ra46ms69.js} +18 -10
- package/dist/chunks/{sqlite-store-st99garx.js → sqlite-store-13nhr1z0.js} +2 -2
- package/dist/chunks/{sqlite-store-501f32ts.js → sqlite-store-gamn65xp.js} +2 -2
- package/dist/chunks/{sqlite-store-fq4natnf.js → sqlite-store-ht6frztj.js} +2 -2
- package/dist/chunks/{tui-x64hjfrh.js → tui-a1tvcw2j.js} +4 -4
- package/dist/chunks/{watch-bxstjpbx.js → watch-rvd3dn6r.js} +5 -5
- package/dist/chunks/{webhooks-k4fh2t9c.js → webhooks-nw4daw9y.js} +20 -9
- package/dist/cli/index.js +27 -27
- package/dist/db/database.d.ts +2 -2
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/sync-pg.d.ts.map +1 -1
- package/dist/index.js +15 -14
- 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/analytics.d.ts +1 -1
- package/dist/lib/analytics.d.ts.map +1 -1
- package/dist/lib/periods.d.ts +20 -1
- package/dist/lib/periods.d.ts.map +1 -1
- package/dist/lib/spikes.d.ts +3 -3
- package/dist/lib/spikes.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 +245 -118
- package/dist/otel/index.js +56 -27
- package/dist/server/index.js +246 -77
- package/dist/server/service-operations.d.ts.map +1 -1
- package/dist/server/service-worker.js +237 -68
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Resolve account attribution through the current hosted Subscriptions client using its station-local paths and applied bindings. Preserve explicit legacy Accounts connections, reject ambiguous bindings, and avoid attributing an unmatched local profile to another station's shared selection.
|
|
8
|
+
|
|
9
|
+
## 0.5.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Use consistent UTC calendar and rolling intervals for charts, forecasts and spike detection. Bound report windows on both ends, preserve millisecond boundaries for rolling hours, and require consecutive observed days before detecting a daily cost spike.
|
|
14
|
+
|
|
3
15
|
## 0.5.2
|
|
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.
|
|
@@ -130,7 +130,9 @@ economy sync --backfill-machine
|
|
|
130
130
|
|
|
131
131
|
Full sync also imports active project metadata from `@hasna/projects` when the registry is available. The Codex source reads both `~/.codex/state_5.sqlite` and the Codewith store at `~/.codewith/state_5.sqlite` by default; explicit `HASNA_ECONOMY_CODEX_DB_PATH` and `HASNA_ECONOMY_CODEWITH_DB_PATH` values override those locations.
|
|
132
132
|
|
|
133
|
-
Account attribution
|
|
133
|
+
Account attribution uses the hosted Subscriptions registry through the installed `subscriptions` CLI and its own credential resolver. Economy reads `subscriptions list --json` and `subscriptions tools list --json` once per resolution, retaining the owning package's station-local paths and applied bindings. It does not activate a profile or copy credentials. A matching explicit agent directory takes precedence; an unmatched or ambiguous directory cannot inherit another profile. With no explicit directory, a station's applied binding can supply attribution; the shared registry's current selection alone cannot identify the local account.
|
|
134
|
+
|
|
135
|
+
An explicitly configured legacy Accounts connection (`HASNA_ACCOUNTS_API_KEY`, its credentials file or Keychain entry) keeps its authority and compatibility behavior. A configured but unusable legacy or Subscriptions connection fails closed. If neither is configured, `~/.hasna/accounts/accounts.json` is **not** read unless `HASNA_ECONOMY_LOCAL=1` is set; attribution is omitted with a diagnostic. The Subscriptions CLI is required when using its registry. Profile counts are not paid-subscription counts, and this read path does not create subscription fees or backfill historical usage. Account identity remains email plus coding agent (profile name when email is unknown). Explicit process overrides such as `ECONOMY_ACCOUNT=tool:name` or `ECONOMY_CODEX_ACCOUNT=codex:work` still take precedence.
|
|
134
136
|
|
|
135
137
|
Session drilldown can be scoped to an account key, account name, or email:
|
|
136
138
|
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
syncAnthropicBilling,
|
|
4
4
|
syncGeminiBilling,
|
|
5
5
|
syncOpenAIBilling
|
|
6
|
-
} from "./index-
|
|
7
|
-
import"./index-
|
|
6
|
+
} from "./index-dk5rq1rq.js";
|
|
7
|
+
import"./index-f5jv6q5q.js";
|
|
8
8
|
import"./index-p35cyhc4.js";
|
|
9
9
|
export {
|
|
10
10
|
syncOpenAIBilling,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
LOCAL_STORAGE_OPT_IN_KEYS
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-n1wwqxfe.js";
|
|
5
5
|
import {
|
|
6
6
|
assessCostFromDb,
|
|
7
7
|
getIngestState,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
upsertSession,
|
|
20
20
|
upsertSubscription,
|
|
21
21
|
upsertUsageSnapshot
|
|
22
|
-
} from "./index-
|
|
22
|
+
} from "./index-f5jv6q5q.js";
|
|
23
23
|
import {
|
|
24
24
|
__require
|
|
25
25
|
} from "./index-p35cyhc4.js";
|
|
@@ -78,34 +78,141 @@ import { join as join2, basename } from "path";
|
|
|
78
78
|
import { existsSync, readFileSync } from "fs";
|
|
79
79
|
import { homedir } from "os";
|
|
80
80
|
import { join } from "path";
|
|
81
|
-
import { z } from "zod";
|
|
81
|
+
import { z as z2 } from "zod";
|
|
82
82
|
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
83
|
-
|
|
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()
|
|
94
127
|
});
|
|
95
|
-
var
|
|
96
|
-
id:
|
|
97
|
-
label:
|
|
98
|
-
envVar:
|
|
99
|
-
defaultDir:
|
|
100
|
-
bin:
|
|
128
|
+
var toolsSchema = z.array(z.object({
|
|
129
|
+
id: nonempty,
|
|
130
|
+
label: nonempty,
|
|
131
|
+
envVar: nonempty,
|
|
132
|
+
defaultDir: nonempty,
|
|
133
|
+
bin: nonempty
|
|
134
|
+
}));
|
|
135
|
+
function unique(rows, identity) {
|
|
136
|
+
if (new Set(rows.map(identity)).size !== rows.length) {
|
|
137
|
+
throw new Error("subscriptions: duplicate identity in metadata response");
|
|
138
|
+
}
|
|
139
|
+
return rows;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
class SubscriptionsRegistry {
|
|
143
|
+
read;
|
|
144
|
+
transport = "api";
|
|
145
|
+
registry = "subscriptions";
|
|
146
|
+
env;
|
|
147
|
+
profiles;
|
|
148
|
+
tools;
|
|
149
|
+
constructor(baseUrl, env, read = readSubscriptionsMetadata) {
|
|
150
|
+
this.read = read;
|
|
151
|
+
this.env = { ...env, HASNA_SUBSCRIPTIONS_API_URL: baseUrl.replace(/\/v1\/?$/, "") };
|
|
152
|
+
}
|
|
153
|
+
allProfiles() {
|
|
154
|
+
return this.profiles ??= this.read(["list", "--json"], this.env).then((raw) => {
|
|
155
|
+
const parsed = z.array(profileSchema).safeParse(raw);
|
|
156
|
+
if (!parsed.success)
|
|
157
|
+
throw new Error("subscriptions: invalid profile metadata response");
|
|
158
|
+
return unique(parsed.data, (row) => JSON.stringify([row.tool, row.name]));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
async listProfiles(tool) {
|
|
162
|
+
const rows = await this.allProfiles();
|
|
163
|
+
return rows.filter((row) => !tool || row.tool === tool);
|
|
164
|
+
}
|
|
165
|
+
async findProfile(name, tool) {
|
|
166
|
+
const matches = (await this.listProfiles(tool)).filter((row) => row.name === name);
|
|
167
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
168
|
+
}
|
|
169
|
+
async appliedProfile(tool) {
|
|
170
|
+
const matches = (await this.allProfiles()).filter((row) => row.tool === tool && row.applied);
|
|
171
|
+
if (matches.length > 1)
|
|
172
|
+
throw new Error("subscriptions: ambiguous station applied-profile binding");
|
|
173
|
+
return matches[0];
|
|
174
|
+
}
|
|
175
|
+
async currentProfile(_tool) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
async listTools() {
|
|
179
|
+
return this.tools ??= this.read(["tools", "list", "--json"], this.env).then((raw) => {
|
|
180
|
+
const parsed = toolsSchema.safeParse(raw);
|
|
181
|
+
if (!parsed.success)
|
|
182
|
+
throw new Error("subscriptions: invalid tool metadata response");
|
|
183
|
+
return unique(parsed.data, (row) => row.id);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/lib/accounts-store.ts
|
|
189
|
+
var ACCOUNTS_APP = "accounts";
|
|
190
|
+
var profileNameSchema = z2.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
|
|
191
|
+
var profileSchema2 = z2.object({
|
|
192
|
+
name: z2.string(),
|
|
193
|
+
tool: z2.string(),
|
|
194
|
+
email: z2.string().optional(),
|
|
195
|
+
displayName: z2.string().optional(),
|
|
196
|
+
dir: z2.string(),
|
|
197
|
+
description: z2.string().optional(),
|
|
198
|
+
createdAt: z2.string(),
|
|
199
|
+
lastUsedAt: z2.string().optional(),
|
|
200
|
+
switchedAway: z2.object({ profile: z2.string().min(1), email: z2.string().optional() }).optional()
|
|
201
|
+
});
|
|
202
|
+
var toolDefSchema = z2.object({
|
|
203
|
+
id: z2.string(),
|
|
204
|
+
label: z2.string(),
|
|
205
|
+
envVar: z2.string(),
|
|
206
|
+
defaultDir: z2.string(),
|
|
207
|
+
bin: z2.string()
|
|
101
208
|
});
|
|
102
|
-
var storeSchema =
|
|
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)
|
|
@@ -1072,12 +1072,32 @@ function meteredBudgetAmounts(groups, limit, threshold) {
|
|
|
1072
1072
|
function ymd(date2) {
|
|
1073
1073
|
return date2.toISOString().substring(0, 10);
|
|
1074
1074
|
}
|
|
1075
|
-
function
|
|
1075
|
+
function resolveReportingInterval(interval, now = new Date) {
|
|
1076
1076
|
if (!Number.isFinite(now.getTime()))
|
|
1077
1077
|
throw new RangeError("Invalid reporting clock");
|
|
1078
|
-
const start = new Date(
|
|
1078
|
+
const start = new Date(now);
|
|
1079
|
+
start.setUTCHours(0, 0, 0, 0);
|
|
1079
1080
|
const end = new Date(start);
|
|
1080
|
-
|
|
1081
|
+
if (interval.kind === "rolling") {
|
|
1082
|
+
if (!Number.isSafeInteger(interval.count) || interval.count < 1)
|
|
1083
|
+
throw new RangeError("Invalid reporting window");
|
|
1084
|
+
if (interval.unit === "hour") {
|
|
1085
|
+
start.setTime(now.getTime() - interval.count * 3600000);
|
|
1086
|
+
if (!Number.isFinite(start.getTime()))
|
|
1087
|
+
throw new RangeError("Invalid reporting window");
|
|
1088
|
+
return { time_zone: "UTC", start: start.toISOString(), end_exclusive: now.toISOString() };
|
|
1089
|
+
}
|
|
1090
|
+
if (interval.unit !== "day")
|
|
1091
|
+
throw new RangeError("Invalid reporting unit");
|
|
1092
|
+
start.setUTCDate(start.getUTCDate() - interval.count + 1);
|
|
1093
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
1094
|
+
if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()))
|
|
1095
|
+
throw new RangeError("Invalid reporting window");
|
|
1096
|
+
return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
|
|
1097
|
+
}
|
|
1098
|
+
if (interval.kind !== "calendar")
|
|
1099
|
+
throw new RangeError("Invalid reporting interval");
|
|
1100
|
+
switch (interval.period) {
|
|
1081
1101
|
case "today":
|
|
1082
1102
|
end.setUTCDate(end.getUTCDate() + 1);
|
|
1083
1103
|
break;
|
|
@@ -1100,11 +1120,15 @@ function calendarPeriodBounds(period, now = new Date) {
|
|
|
1100
1120
|
end.setUTCFullYear(end.getUTCFullYear() + 1);
|
|
1101
1121
|
break;
|
|
1102
1122
|
case "all":
|
|
1103
|
-
return { time_zone: "UTC",
|
|
1123
|
+
return { time_zone: "UTC", start: null, end_exclusive: null };
|
|
1104
1124
|
default:
|
|
1105
1125
|
throw new RangeError("Invalid reporting period");
|
|
1106
1126
|
}
|
|
1107
|
-
return { time_zone: "UTC",
|
|
1127
|
+
return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
|
|
1128
|
+
}
|
|
1129
|
+
function calendarPeriodBounds(period, now = new Date) {
|
|
1130
|
+
const bounds = resolveReportingInterval({ kind: "calendar", period }, now);
|
|
1131
|
+
return { time_zone: bounds.time_zone, start_date: bounds.start, end_date_exclusive: bounds.end_exclusive };
|
|
1108
1132
|
}
|
|
1109
1133
|
function calendarPeriodWhere(period, column, now = new Date) {
|
|
1110
1134
|
if (!/^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)?$/i.test(column)) {
|
|
@@ -2450,20 +2474,22 @@ function queryAccountBreakdownSince(db, since) {
|
|
|
2450
2474
|
ORDER BY api_equivalent_usd DESC
|
|
2451
2475
|
`).all(since, since);
|
|
2452
2476
|
}
|
|
2453
|
-
function queryDailyBreakdown(db, days = 30, machine) {
|
|
2477
|
+
function queryDailyBreakdown(db, days = 30, machine, now = new Date) {
|
|
2478
|
+
const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
|
|
2454
2479
|
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
2455
|
-
const params = machine ? [
|
|
2480
|
+
const params = machine ? [bounds.start, bounds.end_exclusive, machine] : [bounds.start, bounds.end_exclusive];
|
|
2456
2481
|
return db.prepare(`
|
|
2457
2482
|
SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
2458
2483
|
FROM economy_effective_requests
|
|
2459
|
-
WHERE timestamp >=
|
|
2484
|
+
WHERE timestamp >= ? AND timestamp < ?${machineClause}
|
|
2460
2485
|
GROUP BY DATE(timestamp), agent
|
|
2461
2486
|
ORDER BY date ASC
|
|
2462
2487
|
`).all(...params);
|
|
2463
2488
|
}
|
|
2464
|
-
function queryHourlyBreakdown(db, machine, hours) {
|
|
2465
|
-
const
|
|
2466
|
-
const
|
|
2489
|
+
function queryHourlyBreakdown(db, machine, hours, now = new Date) {
|
|
2490
|
+
const bounds = resolveReportingInterval(hours == null ? { kind: "calendar", period: "today" } : { kind: "rolling", unit: "hour", count: hours }, now);
|
|
2491
|
+
const clauses = hours == null ? ["timestamp >= ? AND timestamp < ?"] : ["STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) >= ? AND STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) < ?"];
|
|
2492
|
+
const params = [bounds.start, bounds.end_exclusive];
|
|
2467
2493
|
if (machine) {
|
|
2468
2494
|
clauses.push("machine_id = ?");
|
|
2469
2495
|
params.push(machine);
|
|
@@ -3369,4 +3395,4 @@ function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cache
|
|
|
3369
3395
|
return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
|
|
3370
3396
|
}
|
|
3371
3397
|
|
|
3372
|
-
export { __require, AGENTS, COST_BASIS, isAgent, subscriptionId, MAX_INGEST_BYTES, stableJson, canonicalIngestBodyHash, validateIngest, calendarPeriodBounds, calendarPeriodWhere, usageSnapshotFilterForPeriod, sessionOverlapWhere, dataDir, cacheDir, getMachineId, getHomeDir, getResolverDataRoot, getLegacyDataRoot, adoptResolverDataRoot, getExactDataRoot, getDataDir, getDbPath, initSchema, upsertCostCenter, getCostCenter, upsertRequest, upsertSession, rollupSession, querySessions, queryTopSessions, querySummary, queryZeroCostTokenizedModels, queryModelBreakdown, queryAgentBreakdown, queryProjectBreakdown, queryAccountBreakdown, queryCostCenterBreakdown, queryProjectBreakdownSince, queryAgentBreakdownSince, queryModelBreakdownSince, queryAccountBreakdownSince, queryDailyBreakdown, queryHourlyBreakdown, upsertProject, getProject, listProjects, deleteProject, upsertBudget, listBudgets, deleteBudget, getBudgetStatuses, upsertGoal, deleteGoal, listGoals, getGoalStatuses, getIngestState, loadIngestState, setIngestState, queryRequestsSince, insertFeedback, upsertBillingDaily, clearBillingRange, queryBillingSummary, countBillingRecords, getSessionDetail, listMachines, upsertModelPricing, getModelPricing, listModelPricing, deleteModelPricing, seedModelPricing, upsertSubscription, listSubscriptions, deleteSubscription, upsertUsageSnapshot, upsertScanReceipt, queryScanReceipts, queryUsageSnapshots, listMachineRegistry, dedupeRequests, bulkIngest, DEFAULT_PRICING, normalizeModelName, ensurePricingSeeded, getPricingFromDb, getPricing, computeCost, computeCostFromDb, UnknownModelPricingError, assessCostFromDb, getPricingFromRows, estimateCostFromRows, assessCostFromRows, hasKnownUsagePrice, requireCurrentCostSummary, requireCurrentBillingSummary };
|
|
3398
|
+
export { __require, AGENTS, COST_BASIS, isAgent, subscriptionId, MAX_INGEST_BYTES, stableJson, canonicalIngestBodyHash, validateIngest, resolveReportingInterval, calendarPeriodBounds, calendarPeriodWhere, usageSnapshotFilterForPeriod, sessionOverlapWhere, dataDir, cacheDir, getMachineId, getHomeDir, getResolverDataRoot, getLegacyDataRoot, adoptResolverDataRoot, getExactDataRoot, getDataDir, getDbPath, initSchema, upsertCostCenter, getCostCenter, upsertRequest, upsertSession, rollupSession, querySessions, queryTopSessions, querySummary, queryZeroCostTokenizedModels, queryModelBreakdown, queryAgentBreakdown, queryProjectBreakdown, queryAccountBreakdown, queryCostCenterBreakdown, queryProjectBreakdownSince, queryAgentBreakdownSince, queryModelBreakdownSince, queryAccountBreakdownSince, queryDailyBreakdown, queryHourlyBreakdown, upsertProject, getProject, listProjects, deleteProject, upsertBudget, listBudgets, deleteBudget, getBudgetStatuses, upsertGoal, deleteGoal, listGoals, getGoalStatuses, getIngestState, loadIngestState, setIngestState, queryRequestsSince, insertFeedback, upsertBillingDaily, clearBillingRange, queryBillingSummary, countBillingRecords, getSessionDetail, listMachines, upsertModelPricing, getModelPricing, listModelPricing, deleteModelPricing, seedModelPricing, upsertSubscription, listSubscriptions, deleteSubscription, upsertUsageSnapshot, upsertScanReceipt, queryScanReceipts, queryUsageSnapshots, listMachineRegistry, dedupeRequests, bulkIngest, DEFAULT_PRICING, normalizeModelName, ensurePricingSeeded, getPricingFromDb, getPricing, computeCost, computeCostFromDb, UnknownModelPricingError, assessCostFromDb, getPricingFromRows, estimateCostFromRows, assessCostFromRows, hasKnownUsagePrice, requireCurrentCostSummary, requireCurrentBillingSummary };
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
querySavingsSummary,
|
|
15
15
|
requireCurrentBillingDiffSummary,
|
|
16
16
|
requireCurrentSavingsReport
|
|
17
|
-
} from "./index-
|
|
17
|
+
} from "./index-n1wwqxfe.js";
|
|
18
18
|
import {
|
|
19
19
|
deleteBudget,
|
|
20
20
|
deleteGoal,
|
|
@@ -59,7 +59,7 @@ import {
|
|
|
59
59
|
upsertSubscription,
|
|
60
60
|
usageSnapshotFilterForPeriod,
|
|
61
61
|
validateIngest
|
|
62
|
-
} from "./index-
|
|
62
|
+
} from "./index-f5jv6q5q.js";
|
|
63
63
|
import {
|
|
64
64
|
__require
|
|
65
65
|
} from "./index-p35cyhc4.js";
|
|
@@ -92,7 +92,7 @@ class LocalStore {
|
|
|
92
92
|
_db;
|
|
93
93
|
async db() {
|
|
94
94
|
if (!this._db) {
|
|
95
|
-
const { openDatabase } = await import("./sqlite-store-
|
|
95
|
+
const { openDatabase } = await import("./sqlite-store-13nhr1z0.js");
|
|
96
96
|
this._db = openDatabase();
|
|
97
97
|
ensurePricingSeeded(this._db);
|
|
98
98
|
}
|
|
@@ -1097,12 +1097,32 @@ function meteredBudgetAmounts(groups, limit, threshold) {
|
|
|
1097
1097
|
function ymd(date2) {
|
|
1098
1098
|
return date2.toISOString().substring(0, 10);
|
|
1099
1099
|
}
|
|
1100
|
-
function
|
|
1100
|
+
function resolveReportingInterval(interval, now = new Date) {
|
|
1101
1101
|
if (!Number.isFinite(now.getTime()))
|
|
1102
1102
|
throw new RangeError("Invalid reporting clock");
|
|
1103
|
-
const start = new Date(
|
|
1103
|
+
const start = new Date(now);
|
|
1104
|
+
start.setUTCHours(0, 0, 0, 0);
|
|
1104
1105
|
const end = new Date(start);
|
|
1105
|
-
|
|
1106
|
+
if (interval.kind === "rolling") {
|
|
1107
|
+
if (!Number.isSafeInteger(interval.count) || interval.count < 1)
|
|
1108
|
+
throw new RangeError("Invalid reporting window");
|
|
1109
|
+
if (interval.unit === "hour") {
|
|
1110
|
+
start.setTime(now.getTime() - interval.count * 3600000);
|
|
1111
|
+
if (!Number.isFinite(start.getTime()))
|
|
1112
|
+
throw new RangeError("Invalid reporting window");
|
|
1113
|
+
return { time_zone: "UTC", start: start.toISOString(), end_exclusive: now.toISOString() };
|
|
1114
|
+
}
|
|
1115
|
+
if (interval.unit !== "day")
|
|
1116
|
+
throw new RangeError("Invalid reporting unit");
|
|
1117
|
+
start.setUTCDate(start.getUTCDate() - interval.count + 1);
|
|
1118
|
+
end.setUTCDate(end.getUTCDate() + 1);
|
|
1119
|
+
if (!Number.isFinite(start.getTime()) || !Number.isFinite(end.getTime()))
|
|
1120
|
+
throw new RangeError("Invalid reporting window");
|
|
1121
|
+
return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
|
|
1122
|
+
}
|
|
1123
|
+
if (interval.kind !== "calendar")
|
|
1124
|
+
throw new RangeError("Invalid reporting interval");
|
|
1125
|
+
switch (interval.period) {
|
|
1106
1126
|
case "today":
|
|
1107
1127
|
end.setUTCDate(end.getUTCDate() + 1);
|
|
1108
1128
|
break;
|
|
@@ -1125,11 +1145,15 @@ function calendarPeriodBounds(period, now = new Date) {
|
|
|
1125
1145
|
end.setUTCFullYear(end.getUTCFullYear() + 1);
|
|
1126
1146
|
break;
|
|
1127
1147
|
case "all":
|
|
1128
|
-
return { time_zone: "UTC",
|
|
1148
|
+
return { time_zone: "UTC", start: null, end_exclusive: null };
|
|
1129
1149
|
default:
|
|
1130
1150
|
throw new RangeError("Invalid reporting period");
|
|
1131
1151
|
}
|
|
1132
|
-
return { time_zone: "UTC",
|
|
1152
|
+
return { time_zone: "UTC", start: ymd(start), end_exclusive: ymd(end) };
|
|
1153
|
+
}
|
|
1154
|
+
function calendarPeriodBounds(period, now = new Date) {
|
|
1155
|
+
const bounds = resolveReportingInterval({ kind: "calendar", period }, now);
|
|
1156
|
+
return { time_zone: bounds.time_zone, start_date: bounds.start, end_date_exclusive: bounds.end_exclusive };
|
|
1133
1157
|
}
|
|
1134
1158
|
function calendarPeriodWhere(period, column, now = new Date) {
|
|
1135
1159
|
if (!/^[a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)?$/i.test(column)) {
|
|
@@ -2518,20 +2542,22 @@ function queryAccountBreakdownSince(db, since) {
|
|
|
2518
2542
|
ORDER BY api_equivalent_usd DESC
|
|
2519
2543
|
`).all(since, since);
|
|
2520
2544
|
}
|
|
2521
|
-
function queryDailyBreakdown(db, days = 30, machine) {
|
|
2545
|
+
function queryDailyBreakdown(db, days = 30, machine, now = new Date) {
|
|
2546
|
+
const bounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: days }, now);
|
|
2522
2547
|
const machineClause = machine ? " AND machine_id = ?" : "";
|
|
2523
|
-
const params = machine ? [
|
|
2548
|
+
const params = machine ? [bounds.start, bounds.end_exclusive, machine] : [bounds.start, bounds.end_exclusive];
|
|
2524
2549
|
return db.prepare(`
|
|
2525
2550
|
SELECT DATE(timestamp) as date, agent, COALESCE(SUM(cost_usd), 0) as cost_usd
|
|
2526
2551
|
FROM economy_effective_requests
|
|
2527
|
-
WHERE timestamp >=
|
|
2552
|
+
WHERE timestamp >= ? AND timestamp < ?${machineClause}
|
|
2528
2553
|
GROUP BY DATE(timestamp), agent
|
|
2529
2554
|
ORDER BY date ASC
|
|
2530
2555
|
`).all(...params);
|
|
2531
2556
|
}
|
|
2532
|
-
function queryHourlyBreakdown(db, machine, hours) {
|
|
2533
|
-
const
|
|
2534
|
-
const
|
|
2557
|
+
function queryHourlyBreakdown(db, machine, hours, now = new Date) {
|
|
2558
|
+
const bounds = resolveReportingInterval(hours == null ? { kind: "calendar", period: "today" } : { kind: "rolling", unit: "hour", count: hours }, now);
|
|
2559
|
+
const clauses = hours == null ? ["timestamp >= ? AND timestamp < ?"] : ["STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) >= ? AND STRFTIME('%Y-%m-%dT%H:%M:%fZ', timestamp) < ?"];
|
|
2560
|
+
const params = [bounds.start, bounds.end_exclusive];
|
|
2535
2561
|
if (machine) {
|
|
2536
2562
|
clauses.push("machine_id = ?");
|
|
2537
2563
|
params.push(machine);
|
|
@@ -3437,4 +3463,4 @@ function computeCostWithPricing(model, pricing, inputTokens, outputTokens, cache
|
|
|
3437
3463
|
return (inputTokens * effective.inputPer1M + outputTokens * effective.outputPer1M + cacheReadTokens * effective.cacheReadPer1M + cacheWriteTokens * effective.cacheWritePer1M + cacheWrite1hTokens * (effective.cacheWrite1hPer1M ?? effective.cacheWritePer1M) + cacheStorageTokenHours * (effective.cacheStoragePer1MHour ?? 0)) / 1e6;
|
|
3438
3464
|
}
|
|
3439
3465
|
|
|
3440
|
-
export { OBSERVATION_PG_MIGRATIONS, IngestError, observationId, subscriptionId, scanReceiptId, AGENTS, isAgent, parseAgent, MAX_INGEST_ROWS, MAX_INGEST_BYTES, stableJson, canonicalIngestBodyHash, validateIngest, INGEST_SCHEMA_SQL, LEGACY_PG_SQL, reconcileIngestReceipt, calendarPeriodBounds, calendarPeriodWhere, usageSnapshotFilterForPeriod, sessionOverlapWhere, dataDir, cacheDir, getMachineId, getHomeDir, getResolverDataRoot, getLegacyDataRoot, adoptResolverDataRoot, getExactDataRoot, getDataDir, getDbPath, initSchema, upsertCostCenter, getCostCenter, upsertRequest, upsertSession, rollupSession, querySessions, queryTopSessions, querySummary, queryZeroCostTokenizedModels, queryModelBreakdown, queryAgentBreakdown, queryProjectBreakdown, queryAccountBreakdown, queryCostCenterBreakdown, queryProjectBreakdownSince, queryAgentBreakdownSince, queryModelBreakdownSince, queryAccountBreakdownSince, queryDailyBreakdown, queryHourlyBreakdown, upsertProject, getProject, listProjects, deleteProject, upsertBudget, listBudgets, deleteBudget, getBudgetStatuses, upsertGoal, deleteGoal, listGoals, getGoalStatuses, getIngestState, loadIngestState, setIngestState, queryRequestsSince, insertFeedback, upsertBillingDaily, clearBillingRange, queryBillingSummary, countBillingRecords, getSessionDetail, listMachines, upsertModelPricing, getModelPricing, listModelPricing, deleteModelPricing, seedModelPricing, upsertSubscription, listSubscriptions, deleteSubscription, upsertUsageSnapshot, upsertScanReceipt, queryScanReceipts, queryUsageSnapshots, listMachineRegistry, dedupeRequests, bulkIngest, DEFAULT_PRICING, normalizeModelName, ensurePricingSeeded, getPricingFromDb, getPricing, computeCost, computeCostFromDb, UnknownModelPricingError, assessCostFromDb, getPricingFromRows, estimateCostFromRows, assessCostFromRows, formatReportedUsd, formatPlanMoney, hasKnownUsagePrice, requireCurrentCostSummary, formatSummaryValue, formatSummaryCoverage, requireCurrentBillingSummary, formatBillingValue };
|
|
3466
|
+
export { OBSERVATION_PG_MIGRATIONS, IngestError, observationId, subscriptionId, scanReceiptId, AGENTS, isAgent, parseAgent, MAX_INGEST_ROWS, MAX_INGEST_BYTES, stableJson, canonicalIngestBodyHash, validateIngest, INGEST_SCHEMA_SQL, LEGACY_PG_SQL, reconcileIngestReceipt, resolveReportingInterval, calendarPeriodBounds, calendarPeriodWhere, usageSnapshotFilterForPeriod, sessionOverlapWhere, dataDir, cacheDir, getMachineId, getHomeDir, getResolverDataRoot, getLegacyDataRoot, adoptResolverDataRoot, getExactDataRoot, getDataDir, getDbPath, initSchema, upsertCostCenter, getCostCenter, upsertRequest, upsertSession, rollupSession, querySessions, queryTopSessions, querySummary, queryZeroCostTokenizedModels, queryModelBreakdown, queryAgentBreakdown, queryProjectBreakdown, queryAccountBreakdown, queryCostCenterBreakdown, queryProjectBreakdownSince, queryAgentBreakdownSince, queryModelBreakdownSince, queryAccountBreakdownSince, queryDailyBreakdown, queryHourlyBreakdown, upsertProject, getProject, listProjects, deleteProject, upsertBudget, listBudgets, deleteBudget, getBudgetStatuses, upsertGoal, deleteGoal, listGoals, getGoalStatuses, getIngestState, loadIngestState, setIngestState, queryRequestsSince, insertFeedback, upsertBillingDaily, clearBillingRange, queryBillingSummary, countBillingRecords, getSessionDetail, listMachines, upsertModelPricing, getModelPricing, listModelPricing, deleteModelPricing, seedModelPricing, upsertSubscription, listSubscriptions, deleteSubscription, upsertUsageSnapshot, upsertScanReceipt, queryScanReceipts, queryUsageSnapshots, listMachineRegistry, dedupeRequests, bulkIngest, DEFAULT_PRICING, normalizeModelName, ensurePricingSeeded, getPricingFromDb, getPricing, computeCost, computeCostFromDb, UnknownModelPricingError, assessCostFromDb, getPricingFromRows, estimateCostFromRows, assessCostFromRows, formatReportedUsd, formatPlanMoney, hasKnownUsagePrice, requireCurrentCostSummary, formatSummaryValue, formatSummaryCoverage, requireCurrentBillingSummary, formatBillingValue };
|
|
@@ -14,12 +14,13 @@ import {
|
|
|
14
14
|
querySummary,
|
|
15
15
|
requireCurrentBillingSummary,
|
|
16
16
|
requireCurrentCostSummary,
|
|
17
|
+
resolveReportingInterval,
|
|
17
18
|
sessionOverlapWhere
|
|
18
|
-
} from "./index-
|
|
19
|
+
} from "./index-f5jv6q5q.js";
|
|
19
20
|
// package.json
|
|
20
21
|
var package_default = {
|
|
21
22
|
name: "@hasna/economy",
|
|
22
|
-
version: "0.5.
|
|
23
|
+
version: "0.5.4",
|
|
23
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",
|
|
24
25
|
repository: {
|
|
25
26
|
type: "git",
|
|
@@ -1230,7 +1231,8 @@ function buildBrief(db, opts = {}) {
|
|
|
1230
1231
|
}
|
|
1231
1232
|
|
|
1232
1233
|
// src/lib/analytics.ts
|
|
1233
|
-
function queryProjectDetail(db, nameOrPath) {
|
|
1234
|
+
function queryProjectDetail(db, nameOrPath, now = new Date) {
|
|
1235
|
+
const dailyBounds = resolveReportingInterval({ kind: "rolling", unit: "day", count: 14 }, now);
|
|
1234
1236
|
const like = `%${nameOrPath}%`;
|
|
1235
1237
|
const sessions = db.prepare(`SELECT id, project_name, project_path, total_cost_usd, total_tokens, started_at
|
|
1236
1238
|
FROM economy_effective_sessions
|
|
@@ -1244,8 +1246,8 @@ function queryProjectDetail(db, nameOrPath) {
|
|
|
1244
1246
|
const daily = db.prepare(`SELECT DATE(r.timestamp) as date, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
1245
1247
|
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
1246
1248
|
WHERE (s.project_name LIKE ? OR s.project_path LIKE ?)
|
|
1247
|
-
AND r.timestamp >=
|
|
1248
|
-
GROUP BY date ORDER BY date ASC`).all(like, like);
|
|
1249
|
+
AND r.timestamp >= ? AND r.timestamp < ?
|
|
1250
|
+
GROUP BY date ORDER BY date ASC`).all(like, like, dailyBounds.start, dailyBounds.end_exclusive);
|
|
1249
1251
|
const models = db.prepare(`SELECT r.model as model, COUNT(*) as requests, COALESCE(SUM(r.cost_usd), 0) as cost_usd
|
|
1250
1252
|
FROM economy_effective_requests r JOIN economy_effective_sessions s ON r.session_id = s.id
|
|
1251
1253
|
WHERE s.project_name LIKE ? OR s.project_path LIKE ?
|
|
@@ -1284,18 +1286,17 @@ function queryRangeStats(db, from, to) {
|
|
|
1284
1286
|
return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
|
|
1285
1287
|
}
|
|
1286
1288
|
function queryForecast(db, now = new Date) {
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1289
|
-
const
|
|
1290
|
-
const
|
|
1289
|
+
const month = resolveReportingInterval({ kind: "calendar", period: "month" }, now);
|
|
1290
|
+
const recent = resolveReportingInterval({ kind: "rolling", unit: "day", count: 7 }, now);
|
|
1291
|
+
const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0)).getUTCDate();
|
|
1292
|
+
const dayOfMonth = now.getUTCDate();
|
|
1293
|
+
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(month.start, recent.end_exclusive);
|
|
1291
1294
|
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
1292
1295
|
const projected = dailyAvg * daysInMonth;
|
|
1293
|
-
const
|
|
1294
|
-
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
1295
|
-
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
1296
|
+
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ?`).get(recent.start, recent.end_exclusive);
|
|
1296
1297
|
const last7DailyAvg = last7.cost / 7;
|
|
1297
1298
|
const dailyCosts = db.prepare(`SELECT DATE(timestamp) as date, COALESCE(SUM(cost_usd),0) as cost_usd
|
|
1298
|
-
FROM economy_effective_requests WHERE
|
|
1299
|
+
FROM economy_effective_requests WHERE timestamp >= ? AND timestamp < ? GROUP BY date ORDER BY cost_usd ASC`).all(month.start, recent.end_exclusive);
|
|
1299
1300
|
return {
|
|
1300
1301
|
day_of_month: dayOfMonth,
|
|
1301
1302
|
days_in_month: daysInMonth,
|