@hasna/economy 0.3.10 → 0.3.12
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 +25 -0
- package/dist/cli/index.js +286 -575
- package/dist/db/cloud.d.ts +1 -3
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/pg-migrations.d.ts +1 -1
- package/dist/index.js +16 -443
- package/dist/lib/cloud-ingest.d.ts +57 -0
- package/dist/lib/cloud-ingest.d.ts.map +1 -0
- package/dist/lib/cloud-storage.d.ts +5 -5
- package/dist/lib/contracts-client/index.d.ts.map +1 -1
- package/dist/lib/contracts-client/storage.d.ts +2 -82
- package/dist/lib/contracts-client/storage.d.ts.map +1 -1
- package/dist/lib/contracts-client/transport.d.ts +1 -147
- package/dist/lib/contracts-client/transport.d.ts.map +1 -1
- package/dist/lib/sync-maintenance.d.ts +15 -0
- package/dist/lib/sync-maintenance.d.ts.map +1 -0
- package/dist/mcp/index.js +202 -479
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +8 -8
- package/dist/server/index.js +57 -24
- package/dist/server/serve.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -1
- package/docs/cli.md +3 -3
- package/docs/mcp.md +1 -1
- package/package.json +6 -6
- package/dist/lib/contracts-client/mode.d.ts +0 -20
- package/dist/lib/contracts-client/mode.d.ts.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -76,15 +76,15 @@ var init_sqlite_adapter = () => {};
|
|
|
76
76
|
// src/lib/pricing.ts
|
|
77
77
|
var exports_pricing = {};
|
|
78
78
|
__export(exports_pricing, {
|
|
79
|
-
|
|
80
|
-
getPricingFromRows: () => getPricingFromRows,
|
|
81
|
-
getPricingFromDb: () => getPricingFromDb,
|
|
82
|
-
getPricing: () => getPricing,
|
|
83
|
-
estimateCostFromRows: () => estimateCostFromRows,
|
|
84
|
-
ensurePricingSeeded: () => ensurePricingSeeded,
|
|
85
|
-
computeCostFromDb: () => computeCostFromDb,
|
|
79
|
+
DEFAULT_PRICING: () => DEFAULT_PRICING,
|
|
86
80
|
computeCost: () => computeCost,
|
|
87
|
-
|
|
81
|
+
computeCostFromDb: () => computeCostFromDb,
|
|
82
|
+
ensurePricingSeeded: () => ensurePricingSeeded,
|
|
83
|
+
estimateCostFromRows: () => estimateCostFromRows,
|
|
84
|
+
getPricing: () => getPricing,
|
|
85
|
+
getPricingFromDb: () => getPricingFromDb,
|
|
86
|
+
getPricingFromRows: () => getPricingFromRows,
|
|
87
|
+
normalizeModelName: () => normalizeModelName
|
|
88
88
|
});
|
|
89
89
|
function normalizeModelName(raw) {
|
|
90
90
|
return raw.trim().toLowerCase().replace(/^models\//, "").replace(/^[a-z0-9_.-]+\//, "").replace(/:.+$/, "").replace(/-\d{8}$/, "").replace(/-\d{4}-\d{2}-\d{2}$/, "");
|
|
@@ -591,68 +591,6 @@ var init_pricing = __esm(() => {
|
|
|
591
591
|
});
|
|
592
592
|
|
|
593
593
|
// src/db/database.ts
|
|
594
|
-
var exports_database = {};
|
|
595
|
-
__export(exports_database, {
|
|
596
|
-
upsertUsageSnapshot: () => upsertUsageSnapshot,
|
|
597
|
-
upsertSubscription: () => upsertSubscription,
|
|
598
|
-
upsertSession: () => upsertSession,
|
|
599
|
-
upsertRequest: () => upsertRequest,
|
|
600
|
-
upsertProject: () => upsertProject,
|
|
601
|
-
upsertModelPricing: () => upsertModelPricing,
|
|
602
|
-
upsertGoal: () => upsertGoal,
|
|
603
|
-
upsertCostCenter: () => upsertCostCenter,
|
|
604
|
-
upsertBudget: () => upsertBudget,
|
|
605
|
-
upsertBillingDaily: () => upsertBillingDaily,
|
|
606
|
-
setIngestState: () => setIngestState,
|
|
607
|
-
seedModelPricing: () => seedModelPricing,
|
|
608
|
-
rollupSession: () => rollupSession,
|
|
609
|
-
queryZeroCostTokenizedModels: () => queryZeroCostTokenizedModels,
|
|
610
|
-
queryUsageSnapshots: () => queryUsageSnapshots,
|
|
611
|
-
queryTopSessions: () => queryTopSessions,
|
|
612
|
-
querySummary: () => querySummary,
|
|
613
|
-
querySessions: () => querySessions,
|
|
614
|
-
queryRequestsSince: () => queryRequestsSince,
|
|
615
|
-
queryProjectBreakdownSince: () => queryProjectBreakdownSince,
|
|
616
|
-
queryProjectBreakdown: () => queryProjectBreakdown,
|
|
617
|
-
queryModelBreakdownSince: () => queryModelBreakdownSince,
|
|
618
|
-
queryModelBreakdown: () => queryModelBreakdown,
|
|
619
|
-
queryHourlyBreakdown: () => queryHourlyBreakdown,
|
|
620
|
-
queryDailyBreakdown: () => queryDailyBreakdown,
|
|
621
|
-
queryCostCenterBreakdown: () => queryCostCenterBreakdown,
|
|
622
|
-
queryBillingSummary: () => queryBillingSummary,
|
|
623
|
-
queryAgentBreakdownSince: () => queryAgentBreakdownSince,
|
|
624
|
-
queryAgentBreakdown: () => queryAgentBreakdown,
|
|
625
|
-
queryAccountBreakdownSince: () => queryAccountBreakdownSince,
|
|
626
|
-
queryAccountBreakdown: () => queryAccountBreakdown,
|
|
627
|
-
openDatabase: () => openDatabase,
|
|
628
|
-
listSubscriptions: () => listSubscriptions,
|
|
629
|
-
listProjects: () => listProjects,
|
|
630
|
-
listModelPricing: () => listModelPricing,
|
|
631
|
-
listMachines: () => listMachines,
|
|
632
|
-
listMachineRegistry: () => listMachineRegistry,
|
|
633
|
-
listGoals: () => listGoals,
|
|
634
|
-
listBudgets: () => listBudgets,
|
|
635
|
-
insertFeedback: () => insertFeedback,
|
|
636
|
-
getSessionDetail: () => getSessionDetail,
|
|
637
|
-
getProject: () => getProject,
|
|
638
|
-
getModelPricing: () => getModelPricing,
|
|
639
|
-
getMachineId: () => getMachineId,
|
|
640
|
-
getIngestState: () => getIngestState,
|
|
641
|
-
getGoalStatuses: () => getGoalStatuses,
|
|
642
|
-
getDbPath: () => getDbPath,
|
|
643
|
-
getDataDir: () => getDataDir,
|
|
644
|
-
getCostCenter: () => getCostCenter,
|
|
645
|
-
getBudgetStatuses: () => getBudgetStatuses,
|
|
646
|
-
deleteSubscription: () => deleteSubscription,
|
|
647
|
-
deleteProject: () => deleteProject,
|
|
648
|
-
deleteModelPricing: () => deleteModelPricing,
|
|
649
|
-
deleteGoal: () => deleteGoal,
|
|
650
|
-
deleteBudget: () => deleteBudget,
|
|
651
|
-
dedupeRequests: () => dedupeRequests,
|
|
652
|
-
countBillingRecords: () => countBillingRecords,
|
|
653
|
-
clearBillingRange: () => clearBillingRange,
|
|
654
|
-
bulkIngest: () => bulkIngest
|
|
655
|
-
});
|
|
656
594
|
import { execFileSync } from "child_process";
|
|
657
595
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
658
596
|
import { hostname, platform } from "os";
|
|
@@ -1066,9 +1004,6 @@ function upsertCostCenter(db, center) {
|
|
|
1066
1004
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1067
1005
|
`).run(center.id, center.kind, center.name, center.repo_path ?? null, center.labels_json ?? "{}", center.created_at, updatedAt);
|
|
1068
1006
|
}
|
|
1069
|
-
function getCostCenter(db, id) {
|
|
1070
|
-
return db.prepare(`SELECT * FROM cost_centers WHERE id = ?`).get(id);
|
|
1071
|
-
}
|
|
1072
1007
|
function upsertRequest(db, req) {
|
|
1073
1008
|
const now = req.updated_at ?? new Date().toISOString();
|
|
1074
1009
|
db.prepare(`
|
|
@@ -2190,6 +2125,7 @@ function ingestSpecs() {
|
|
|
2190
2125
|
"timestamp",
|
|
2191
2126
|
"source_request_id",
|
|
2192
2127
|
"machine_id",
|
|
2128
|
+
"cost_center_id",
|
|
2193
2129
|
"attribution_tag",
|
|
2194
2130
|
"account_key",
|
|
2195
2131
|
"account_tool",
|
|
@@ -2216,6 +2152,7 @@ function ingestSpecs() {
|
|
|
2216
2152
|
strOr(r["timestamp"]),
|
|
2217
2153
|
r["source_request_id"] == null ? null : strOr(r["source_request_id"]),
|
|
2218
2154
|
strOr(r["machine_id"]),
|
|
2155
|
+
r["cost_center_id"] == null ? null : strOr(r["cost_center_id"]),
|
|
2219
2156
|
strOr(r["attribution_tag"]),
|
|
2220
2157
|
strOr(r["account_key"]),
|
|
2221
2158
|
strOr(r["account_tool"]),
|
|
@@ -2240,6 +2177,7 @@ function ingestSpecs() {
|
|
|
2240
2177
|
"total_tokens",
|
|
2241
2178
|
"request_count",
|
|
2242
2179
|
"machine_id",
|
|
2180
|
+
"cost_center_id",
|
|
2243
2181
|
"attribution_tag",
|
|
2244
2182
|
"account_key",
|
|
2245
2183
|
"account_tool",
|
|
@@ -2260,6 +2198,7 @@ function ingestSpecs() {
|
|
|
2260
2198
|
numOr(r["total_tokens"]),
|
|
2261
2199
|
numOr(r["request_count"]),
|
|
2262
2200
|
strOr(r["machine_id"]),
|
|
2201
|
+
r["cost_center_id"] == null ? null : strOr(r["cost_center_id"]),
|
|
2263
2202
|
strOr(r["attribution_tag"]),
|
|
2264
2203
|
strOr(r["account_key"]),
|
|
2265
2204
|
strOr(r["account_tool"]),
|
|
@@ -3441,448 +3380,14 @@ function queryModelEfficiency(db) {
|
|
|
3441
3380
|
FROM requests GROUP BY model ORDER BY cost DESC`).all();
|
|
3442
3381
|
}
|
|
3443
3382
|
|
|
3444
|
-
// src/lib/contracts-client/mode.ts
|
|
3445
|
-
function normalizeStorageMode(value) {
|
|
3446
|
-
const normalized = value.trim().toLowerCase().replace(/-/g, "_");
|
|
3447
|
-
if (normalized === "local")
|
|
3448
|
-
return { mode: "local", deprecatedAlias: null };
|
|
3449
|
-
if (normalized === "cloud")
|
|
3450
|
-
return { mode: "cloud", deprecatedAlias: null };
|
|
3451
|
-
if (DEPRECATED_STORAGE_MODE_ALIASES.includes(normalized)) {
|
|
3452
|
-
return { mode: "cloud", deprecatedAlias: normalized };
|
|
3453
|
-
}
|
|
3454
|
-
throw new Error(`Unknown storage mode: ${value}. Use local or cloud.`);
|
|
3455
|
-
}
|
|
3456
|
-
function envToken(name) {
|
|
3457
|
-
return name.toUpperCase().replace(/-/g, "_");
|
|
3458
|
-
}
|
|
3459
|
-
var DEPRECATED_STORAGE_MODE_ALIASES;
|
|
3460
|
-
var init_mode = __esm(() => {
|
|
3461
|
-
DEPRECATED_STORAGE_MODE_ALIASES = ["remote", "hybrid", "self_hosted"];
|
|
3462
|
-
});
|
|
3463
|
-
|
|
3464
|
-
// src/lib/contracts-client/transport.ts
|
|
3465
|
-
function defaultCloudBaseUrl(name) {
|
|
3466
|
-
return `https://${name}.hasna.xyz`;
|
|
3467
|
-
}
|
|
3468
|
-
function clientTransportEnvKeys(name) {
|
|
3469
|
-
const token = envToken(name);
|
|
3470
|
-
return {
|
|
3471
|
-
modeKeys: [
|
|
3472
|
-
`HASNA_${token}_STORAGE_MODE`,
|
|
3473
|
-
`HASNA_${token}_MODE`,
|
|
3474
|
-
`${token}_STORAGE_MODE`,
|
|
3475
|
-
`${token}_MODE`
|
|
3476
|
-
],
|
|
3477
|
-
apiUrlKeys: [`HASNA_${token}_API_URL`, `${token}_API_URL`],
|
|
3478
|
-
apiKeyKeys: [`HASNA_${token}_API_KEY`, `${token}_API_KEY`]
|
|
3479
|
-
};
|
|
3480
|
-
}
|
|
3481
|
-
function firstEnv(env, keys) {
|
|
3482
|
-
for (const key of keys) {
|
|
3483
|
-
const value = env[key]?.trim();
|
|
3484
|
-
if (value)
|
|
3485
|
-
return { key, value };
|
|
3486
|
-
}
|
|
3487
|
-
return null;
|
|
3488
|
-
}
|
|
3489
|
-
function toV1BaseUrl(apiUrl) {
|
|
3490
|
-
const url = new URL(apiUrl);
|
|
3491
|
-
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
3492
|
-
throw new Error("API URL must use http or https.");
|
|
3493
|
-
}
|
|
3494
|
-
let path = url.pathname.replace(/\/+$/, "");
|
|
3495
|
-
if (path.endsWith("/v1"))
|
|
3496
|
-
path = path.slice(0, -"/v1".length);
|
|
3497
|
-
url.pathname = `${path}/v1`;
|
|
3498
|
-
url.search = "";
|
|
3499
|
-
url.hash = "";
|
|
3500
|
-
return url.toString().replace(/\/+$/, "");
|
|
3501
|
-
}
|
|
3502
|
-
function resolveClientTransport(name, env = process.env) {
|
|
3503
|
-
const keys = clientTransportEnvKeys(name);
|
|
3504
|
-
const modeHit = firstEnv(env, keys.modeKeys);
|
|
3505
|
-
const urlHit = firstEnv(env, keys.apiUrlKeys);
|
|
3506
|
-
const keyHit = firstEnv(env, keys.apiKeyKeys);
|
|
3507
|
-
let mode = "local";
|
|
3508
|
-
let deprecatedAlias = null;
|
|
3509
|
-
let modeSource = "default";
|
|
3510
|
-
const warnings = [];
|
|
3511
|
-
if (modeHit) {
|
|
3512
|
-
const normalized = normalizeStorageMode(modeHit.value);
|
|
3513
|
-
mode = normalized.mode;
|
|
3514
|
-
deprecatedAlias = normalized.deprecatedAlias;
|
|
3515
|
-
modeSource = modeHit.key;
|
|
3516
|
-
if (deprecatedAlias) {
|
|
3517
|
-
warnings.push(`Deprecated mode '${deprecatedAlias}' from ${modeHit.key} is treated as 'cloud'. Prefer ${keys.modeKeys[0]}=cloud.`);
|
|
3518
|
-
}
|
|
3519
|
-
} else if (urlHit && keyHit) {
|
|
3520
|
-
mode = "cloud";
|
|
3521
|
-
modeSource = `${urlHit.key}+${keyHit.key}`;
|
|
3522
|
-
} else if (urlHit) {
|
|
3523
|
-
warnings.push(`${urlHit.key} is set but no API key is present (${keys.apiKeyKeys[0]}). ` + `Refusing to fall back to the local store, which would silently serve a different dataset. ` + `Set ${keys.apiKeyKeys[0]} to enable the cloud client, or set ${keys.modeKeys[0]}=local ` + `to use the local store deliberately.`);
|
|
3524
|
-
return {
|
|
3525
|
-
transport: "local",
|
|
3526
|
-
mode: "local",
|
|
3527
|
-
deprecatedAlias,
|
|
3528
|
-
modeSource: urlHit.key,
|
|
3529
|
-
baseUrl: null,
|
|
3530
|
-
apiUrlSource: null,
|
|
3531
|
-
apiKeyPresent: false,
|
|
3532
|
-
apiKeySource: null,
|
|
3533
|
-
misconfigured: true,
|
|
3534
|
-
warning: warnings.join(" ")
|
|
3535
|
-
};
|
|
3536
|
-
}
|
|
3537
|
-
if (mode === "local") {
|
|
3538
|
-
return {
|
|
3539
|
-
transport: "local",
|
|
3540
|
-
mode,
|
|
3541
|
-
deprecatedAlias,
|
|
3542
|
-
modeSource,
|
|
3543
|
-
baseUrl: null,
|
|
3544
|
-
apiUrlSource: null,
|
|
3545
|
-
apiKeyPresent: Boolean(keyHit),
|
|
3546
|
-
apiKeySource: keyHit ? keyHit.key : null,
|
|
3547
|
-
misconfigured: false,
|
|
3548
|
-
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
3549
|
-
};
|
|
3550
|
-
}
|
|
3551
|
-
if (!keyHit) {
|
|
3552
|
-
warnings.push(`${modeSource}=cloud but no API key is set (${keys.apiKeyKeys[0]}). Refusing to route to cloud; using local store. Set ${keys.apiKeyKeys[0]} to enable the cloud client.`);
|
|
3553
|
-
return {
|
|
3554
|
-
transport: "local",
|
|
3555
|
-
mode,
|
|
3556
|
-
deprecatedAlias,
|
|
3557
|
-
modeSource,
|
|
3558
|
-
baseUrl: null,
|
|
3559
|
-
apiUrlSource: null,
|
|
3560
|
-
apiKeyPresent: false,
|
|
3561
|
-
apiKeySource: null,
|
|
3562
|
-
misconfigured: true,
|
|
3563
|
-
warning: warnings.join(" ")
|
|
3564
|
-
};
|
|
3565
|
-
}
|
|
3566
|
-
const rawUrl = urlHit?.value ?? defaultCloudBaseUrl(name);
|
|
3567
|
-
const apiUrlSource = urlHit ? urlHit.key : "default";
|
|
3568
|
-
let baseUrl;
|
|
3569
|
-
try {
|
|
3570
|
-
baseUrl = toV1BaseUrl(rawUrl);
|
|
3571
|
-
} catch (error) {
|
|
3572
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3573
|
-
warnings.push(`Invalid API URL from ${apiUrlSource}: ${message}. Using local store.`);
|
|
3574
|
-
return {
|
|
3575
|
-
transport: "local",
|
|
3576
|
-
mode,
|
|
3577
|
-
deprecatedAlias,
|
|
3578
|
-
modeSource,
|
|
3579
|
-
baseUrl: null,
|
|
3580
|
-
apiUrlSource: null,
|
|
3581
|
-
apiKeyPresent: true,
|
|
3582
|
-
apiKeySource: keyHit.key,
|
|
3583
|
-
misconfigured: true,
|
|
3584
|
-
warning: warnings.join(" ")
|
|
3585
|
-
};
|
|
3586
|
-
}
|
|
3587
|
-
return {
|
|
3588
|
-
transport: "cloud-http",
|
|
3589
|
-
mode,
|
|
3590
|
-
deprecatedAlias,
|
|
3591
|
-
modeSource,
|
|
3592
|
-
baseUrl,
|
|
3593
|
-
apiUrlSource,
|
|
3594
|
-
apiKeyPresent: true,
|
|
3595
|
-
apiKeySource: keyHit.key,
|
|
3596
|
-
misconfigured: false,
|
|
3597
|
-
warning: warnings.length > 0 ? warnings.join(" ") : null
|
|
3598
|
-
};
|
|
3599
|
-
}
|
|
3600
|
-
function appendQuery(path, query) {
|
|
3601
|
-
if (!query)
|
|
3602
|
-
return path;
|
|
3603
|
-
const params = query instanceof URLSearchParams ? query : new URLSearchParams;
|
|
3604
|
-
if (!(query instanceof URLSearchParams)) {
|
|
3605
|
-
for (const [key, value] of Object.entries(query)) {
|
|
3606
|
-
if (value === null || value === undefined)
|
|
3607
|
-
continue;
|
|
3608
|
-
if (Array.isArray(value)) {
|
|
3609
|
-
for (const v of value)
|
|
3610
|
-
params.append(key, String(v));
|
|
3611
|
-
} else {
|
|
3612
|
-
params.append(key, String(value));
|
|
3613
|
-
}
|
|
3614
|
-
}
|
|
3615
|
-
}
|
|
3616
|
-
const qs = params.toString();
|
|
3617
|
-
if (!qs)
|
|
3618
|
-
return path;
|
|
3619
|
-
return `${path}${path.includes("?") ? "&" : "?"}${qs}`;
|
|
3620
|
-
}
|
|
3621
|
-
function createHasnaHttpTransport(options) {
|
|
3622
|
-
const fetchImpl = options.fetchImpl ?? ((input, init) => fetch(input, init));
|
|
3623
|
-
const base = options.baseUrl.replace(/\/+$/, "");
|
|
3624
|
-
const timeoutMs = options.timeoutMs ?? 30000;
|
|
3625
|
-
const sleep = options.sleepImpl ?? defaultSleep;
|
|
3626
|
-
const defaultRetry = options.retry;
|
|
3627
|
-
function resolveRetry(callRetry) {
|
|
3628
|
-
const chosen = callRetry !== undefined ? callRetry : defaultRetry;
|
|
3629
|
-
if (chosen === false)
|
|
3630
|
-
return null;
|
|
3631
|
-
const r = chosen ?? {};
|
|
3632
|
-
return {
|
|
3633
|
-
retries: r.retries ?? 2,
|
|
3634
|
-
baseDelayMs: r.baseDelayMs ?? 200,
|
|
3635
|
-
maxDelayMs: r.maxDelayMs ?? 2000,
|
|
3636
|
-
retryStatuses: r.retryStatuses ?? [...DEFAULT_RETRY_STATUSES]
|
|
3637
|
-
};
|
|
3638
|
-
}
|
|
3639
|
-
async function once(method, rel, url, body, opts) {
|
|
3640
|
-
const headers = {
|
|
3641
|
-
"x-api-key": options.apiKey,
|
|
3642
|
-
Authorization: `Bearer ${options.apiKey}`,
|
|
3643
|
-
Accept: "application/json",
|
|
3644
|
-
...options.headers ?? {},
|
|
3645
|
-
...opts.headers ?? {}
|
|
3646
|
-
};
|
|
3647
|
-
if (opts.idempotencyKey)
|
|
3648
|
-
headers["Idempotency-Key"] = opts.idempotencyKey;
|
|
3649
|
-
const init = { method, headers };
|
|
3650
|
-
if (body !== undefined) {
|
|
3651
|
-
headers["Content-Type"] = "application/json";
|
|
3652
|
-
init.body = JSON.stringify(body);
|
|
3653
|
-
}
|
|
3654
|
-
const controller = new AbortController;
|
|
3655
|
-
const onAbort = () => controller.abort();
|
|
3656
|
-
if (opts.signal) {
|
|
3657
|
-
if (opts.signal.aborted)
|
|
3658
|
-
controller.abort();
|
|
3659
|
-
else
|
|
3660
|
-
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
3661
|
-
}
|
|
3662
|
-
const timer = setTimeout(() => controller.abort(), opts.timeoutMs ?? timeoutMs);
|
|
3663
|
-
init.signal = controller.signal;
|
|
3664
|
-
let response;
|
|
3665
|
-
try {
|
|
3666
|
-
response = await fetchImpl(url, init);
|
|
3667
|
-
} catch (error) {
|
|
3668
|
-
const err = error instanceof Error ? error : new Error(String(error));
|
|
3669
|
-
if (opts.signal?.aborted)
|
|
3670
|
-
return { ok: false, retryable: false, error: err };
|
|
3671
|
-
return { ok: false, retryable: true, error: err };
|
|
3672
|
-
} finally {
|
|
3673
|
-
clearTimeout(timer);
|
|
3674
|
-
if (opts.signal)
|
|
3675
|
-
opts.signal.removeEventListener("abort", onAbort);
|
|
3676
|
-
}
|
|
3677
|
-
const text = await response.text();
|
|
3678
|
-
let parsed = undefined;
|
|
3679
|
-
if (text.length > 0) {
|
|
3680
|
-
try {
|
|
3681
|
-
parsed = JSON.parse(text);
|
|
3682
|
-
} catch {
|
|
3683
|
-
parsed = text;
|
|
3684
|
-
}
|
|
3685
|
-
}
|
|
3686
|
-
if (!response.ok) {
|
|
3687
|
-
const retry = resolveRetry(opts.retry);
|
|
3688
|
-
const retryable = retry ? retry.retryStatuses.includes(response.status) : false;
|
|
3689
|
-
return { ok: false, retryable, error: new HasnaHttpError(method, rel, response.status, parsed) };
|
|
3690
|
-
}
|
|
3691
|
-
return { ok: true, value: parsed };
|
|
3692
|
-
}
|
|
3693
|
-
async function request(method, path, body, opts = {}) {
|
|
3694
|
-
const upper = method.toUpperCase();
|
|
3695
|
-
const rel = appendQuery(path.startsWith("/") ? path : `/${path}`, opts.query);
|
|
3696
|
-
const url = `${base}${rel}`;
|
|
3697
|
-
const retry = resolveRetry(opts.retry);
|
|
3698
|
-
const methodRetryable = IDEMPOTENT_METHODS.has(upper) || Boolean(opts.idempotencyKey);
|
|
3699
|
-
const maxAttempts = retry && methodRetryable ? retry.retries + 1 : 1;
|
|
3700
|
-
let last = null;
|
|
3701
|
-
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
3702
|
-
const result = await once(upper, rel, url, body, opts);
|
|
3703
|
-
if (result.ok)
|
|
3704
|
-
return result.value;
|
|
3705
|
-
last = result;
|
|
3706
|
-
const canRetry = retry !== null && methodRetryable && result.retryable && attempt < maxAttempts;
|
|
3707
|
-
if (!canRetry)
|
|
3708
|
-
break;
|
|
3709
|
-
const backoff = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** (attempt - 1));
|
|
3710
|
-
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
3711
|
-
await sleep(backoff + jitter);
|
|
3712
|
-
}
|
|
3713
|
-
throw last.error;
|
|
3714
|
-
}
|
|
3715
|
-
return {
|
|
3716
|
-
baseUrl: base,
|
|
3717
|
-
request,
|
|
3718
|
-
get: (path, opts) => request("GET", path, undefined, opts),
|
|
3719
|
-
post: (path, body, opts) => request("POST", path, body, opts),
|
|
3720
|
-
put: (path, body, opts) => request("PUT", path, body, opts),
|
|
3721
|
-
patch: (path, body, opts) => request("PATCH", path, body, opts),
|
|
3722
|
-
del: (path, body, opts) => request("DELETE", path, body, opts)
|
|
3723
|
-
};
|
|
3724
|
-
}
|
|
3725
|
-
function createClientTransport(name, env = process.env, overrides) {
|
|
3726
|
-
const resolution = resolveClientTransport(name, env);
|
|
3727
|
-
if (resolution.misconfigured) {
|
|
3728
|
-
throw new Error(resolution.warning ?? `Client for '${name}' is misconfigured for cloud mode.`);
|
|
3729
|
-
}
|
|
3730
|
-
if (resolution.transport === "local" || !resolution.baseUrl) {
|
|
3731
|
-
return { transport: "local", client: null, resolution };
|
|
3732
|
-
}
|
|
3733
|
-
const keys = clientTransportEnvKeys(name);
|
|
3734
|
-
const apiKey = firstEnv(env, keys.apiKeyKeys)?.value;
|
|
3735
|
-
if (!apiKey) {
|
|
3736
|
-
throw new Error(`Client for '${name}' resolved to cloud-http without an API key.`);
|
|
3737
|
-
}
|
|
3738
|
-
return {
|
|
3739
|
-
transport: "cloud-http",
|
|
3740
|
-
client: createHasnaHttpTransport({
|
|
3741
|
-
name,
|
|
3742
|
-
baseUrl: resolution.baseUrl,
|
|
3743
|
-
apiKey,
|
|
3744
|
-
...overrides?.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
3745
|
-
...overrides?.headers ? { headers: overrides.headers } : {},
|
|
3746
|
-
...overrides?.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
3747
|
-
...overrides?.retry !== undefined ? { retry: overrides.retry } : {},
|
|
3748
|
-
...overrides?.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
3749
|
-
}),
|
|
3750
|
-
resolution
|
|
3751
|
-
};
|
|
3752
|
-
}
|
|
3753
|
-
var HasnaHttpError, DEFAULT_RETRY_STATUSES, IDEMPOTENT_METHODS, defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
3754
|
-
var init_transport = __esm(() => {
|
|
3755
|
-
init_mode();
|
|
3756
|
-
HasnaHttpError = class HasnaHttpError extends Error {
|
|
3757
|
-
status;
|
|
3758
|
-
method;
|
|
3759
|
-
path;
|
|
3760
|
-
body;
|
|
3761
|
-
constructor(method, path, status, body) {
|
|
3762
|
-
super(`Hasna cloud request failed: ${method} ${path} -> ${status}`);
|
|
3763
|
-
this.name = "HasnaHttpError";
|
|
3764
|
-
this.status = status;
|
|
3765
|
-
this.method = method;
|
|
3766
|
-
this.path = path;
|
|
3767
|
-
this.body = body;
|
|
3768
|
-
}
|
|
3769
|
-
};
|
|
3770
|
-
DEFAULT_RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
3771
|
-
IDEMPOTENT_METHODS = new Set(["GET", "HEAD", "PUT", "DELETE", "OPTIONS"]);
|
|
3772
|
-
});
|
|
3773
|
-
|
|
3774
3383
|
// src/lib/contracts-client/storage.ts
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
if (!trimmed)
|
|
3778
|
-
throw new Error("resource must be a non-empty path segment");
|
|
3779
|
-
return `/${trimmed}`;
|
|
3780
|
-
}
|
|
3781
|
-
function entityPath(resource, id) {
|
|
3782
|
-
if (id === undefined || id === null || `${id}`.length === 0) {
|
|
3783
|
-
throw new Error("id must be a non-empty string");
|
|
3784
|
-
}
|
|
3785
|
-
return `${resourcePath(resource)}/${encodeURIComponent(String(id))}`;
|
|
3786
|
-
}
|
|
3787
|
-
function newIdempotencyKey() {
|
|
3788
|
-
const g = globalThis;
|
|
3789
|
-
if (g.crypto?.randomUUID)
|
|
3790
|
-
return g.crypto.randomUUID();
|
|
3791
|
-
return `idmp_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 12)}`;
|
|
3792
|
-
}
|
|
3793
|
-
function extractItems(raw) {
|
|
3794
|
-
if (Array.isArray(raw))
|
|
3795
|
-
return raw;
|
|
3796
|
-
if (raw && typeof raw === "object") {
|
|
3797
|
-
const obj = raw;
|
|
3798
|
-
for (const key of ["items", "data", "results", "rows", "records"]) {
|
|
3799
|
-
if (Array.isArray(obj[key]))
|
|
3800
|
-
return obj[key];
|
|
3801
|
-
}
|
|
3802
|
-
}
|
|
3803
|
-
return [];
|
|
3804
|
-
}
|
|
3805
|
-
function extractTotal(raw) {
|
|
3806
|
-
if (raw && typeof raw === "object") {
|
|
3807
|
-
const obj = raw;
|
|
3808
|
-
for (const key of ["total", "count", "totalCount", "total_count"]) {
|
|
3809
|
-
if (typeof obj[key] === "number")
|
|
3810
|
-
return obj[key];
|
|
3811
|
-
}
|
|
3812
|
-
}
|
|
3813
|
-
return null;
|
|
3814
|
-
}
|
|
3815
|
-
function extractCursor(raw) {
|
|
3816
|
-
if (raw && typeof raw === "object") {
|
|
3817
|
-
const obj = raw;
|
|
3818
|
-
for (const key of ["cursor", "nextCursor", "next_cursor", "next"]) {
|
|
3819
|
-
if (typeof obj[key] === "string")
|
|
3820
|
-
return obj[key];
|
|
3821
|
-
}
|
|
3822
|
-
}
|
|
3823
|
-
return null;
|
|
3824
|
-
}
|
|
3825
|
-
function createHasnaStorageClient(name, transport) {
|
|
3826
|
-
return {
|
|
3827
|
-
name,
|
|
3828
|
-
baseUrl: transport.baseUrl,
|
|
3829
|
-
transport,
|
|
3830
|
-
async list(resource, options = {}) {
|
|
3831
|
-
const raw = await transport.get(resourcePath(resource), options);
|
|
3832
|
-
return {
|
|
3833
|
-
items: extractItems(raw),
|
|
3834
|
-
total: extractTotal(raw),
|
|
3835
|
-
cursor: extractCursor(raw),
|
|
3836
|
-
raw
|
|
3837
|
-
};
|
|
3838
|
-
},
|
|
3839
|
-
async get(resource, id, options = {}) {
|
|
3840
|
-
try {
|
|
3841
|
-
return await transport.get(entityPath(resource, id), options);
|
|
3842
|
-
} catch (error) {
|
|
3843
|
-
if (error instanceof HasnaHttpError && error.status === 404)
|
|
3844
|
-
return null;
|
|
3845
|
-
throw error;
|
|
3846
|
-
}
|
|
3847
|
-
},
|
|
3848
|
-
async create(resource, body, options = {}) {
|
|
3849
|
-
const { idempotencyKey, ...rest } = options;
|
|
3850
|
-
return transport.post(resourcePath(resource), body, {
|
|
3851
|
-
...rest,
|
|
3852
|
-
idempotencyKey: idempotencyKey ?? newIdempotencyKey()
|
|
3853
|
-
});
|
|
3854
|
-
},
|
|
3855
|
-
async update(resource, id, patch, options = {}) {
|
|
3856
|
-
const { method = "PATCH", idempotencyKey, ...rest } = options;
|
|
3857
|
-
const call = method === "PUT" ? transport.put : transport.patch;
|
|
3858
|
-
return call(entityPath(resource, id), patch, { ...rest, ...idempotencyKey ? { idempotencyKey } : {} });
|
|
3859
|
-
},
|
|
3860
|
-
async delete(resource, id, options = {}) {
|
|
3861
|
-
try {
|
|
3862
|
-
await transport.del(entityPath(resource, id), undefined, options);
|
|
3863
|
-
} catch (error) {
|
|
3864
|
-
if (error instanceof HasnaHttpError && error.status === 404)
|
|
3865
|
-
return;
|
|
3866
|
-
throw error;
|
|
3867
|
-
}
|
|
3868
|
-
}
|
|
3869
|
-
};
|
|
3870
|
-
}
|
|
3871
|
-
function resolveStorageClient(name, env = process.env, overrides) {
|
|
3872
|
-
const wired = createClientTransport(name, env, overrides);
|
|
3873
|
-
if (wired.transport === "cloud-http") {
|
|
3874
|
-
return { transport: "cloud-http", client: createHasnaStorageClient(name, wired.client) };
|
|
3875
|
-
}
|
|
3876
|
-
return { transport: "local", client: null };
|
|
3877
|
-
}
|
|
3878
|
-
var init_storage = __esm(() => {
|
|
3879
|
-
init_transport();
|
|
3880
|
-
});
|
|
3384
|
+
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
3385
|
+
var init_storage = () => {};
|
|
3881
3386
|
|
|
3882
3387
|
// src/lib/cloud-storage.ts
|
|
3883
3388
|
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
3884
3389
|
const resolved = resolveStorageClient(ECONOMY_APP, env, overrides);
|
|
3885
|
-
return resolved.transport === "
|
|
3390
|
+
return resolved.transport === "http" ? { active: true, client: resolved.client } : { active: false, client: null };
|
|
3886
3391
|
}
|
|
3887
3392
|
function economyCloudStorage(env = process.env) {
|
|
3888
3393
|
if (cache === undefined)
|
|
@@ -4340,12 +3845,12 @@ function isAuthorizedRequest(req, path) {
|
|
|
4340
3845
|
// src/cli/commands/tui.ts
|
|
4341
3846
|
var exports_tui = {};
|
|
4342
3847
|
__export(exports_tui, {
|
|
4343
|
-
|
|
4344
|
-
printWaybarJson: () => printWaybarJson,
|
|
4345
|
-
printStatusLine: () => printStatusLine,
|
|
4346
|
-
gatherStatusData: () => gatherStatusData,
|
|
3848
|
+
buildStatusLine: () => buildStatusLine,
|
|
4347
3849
|
buildWaybarJson: () => buildWaybarJson,
|
|
4348
|
-
|
|
3850
|
+
gatherStatusData: () => gatherStatusData,
|
|
3851
|
+
printStatusLine: () => printStatusLine,
|
|
3852
|
+
printWaybarJson: () => printWaybarJson,
|
|
3853
|
+
runTui: () => runTui
|
|
4349
3854
|
});
|
|
4350
3855
|
import chalk3 from "chalk";
|
|
4351
3856
|
function fmt(usd) {
|
|
@@ -6144,11 +5649,11 @@ async function syncAll(db, opts = {}) {
|
|
|
6144
5649
|
const all = !anySpecific;
|
|
6145
5650
|
const result = { deduped: 0 };
|
|
6146
5651
|
if (all || opts.claude) {
|
|
6147
|
-
result.claude = await ingestClaude(db, opts.verbose);
|
|
5652
|
+
result.claude = await ingestClaude(db, opts.verbose, opts.projectsDir);
|
|
6148
5653
|
result.claudeQuota = await ingestClaudeQuota(db, opts.verbose);
|
|
6149
5654
|
}
|
|
6150
5655
|
if (all || opts.takumi)
|
|
6151
|
-
result.takumi = await ingestTakumi(db, opts.verbose);
|
|
5656
|
+
result.takumi = await ingestTakumi(db, opts.verbose, opts.projectsDir);
|
|
6152
5657
|
if (all || opts.codex) {
|
|
6153
5658
|
result.codex = await ingestCodex(db, opts.verbose);
|
|
6154
5659
|
result.codexQuota = await ingestCodexQuota(db, opts.verbose);
|
|
@@ -6186,9 +5691,9 @@ var init_sync_all = __esm(() => {
|
|
|
6186
5691
|
// src/ingest/billing.ts
|
|
6187
5692
|
var exports_billing = {};
|
|
6188
5693
|
__export(exports_billing, {
|
|
6189
|
-
|
|
5694
|
+
syncAnthropicBilling: () => syncAnthropicBilling,
|
|
6190
5695
|
syncGeminiBilling: () => syncGeminiBilling,
|
|
6191
|
-
|
|
5696
|
+
syncOpenAIBilling: () => syncOpenAIBilling
|
|
6192
5697
|
});
|
|
6193
5698
|
import { readFileSync as readFileSync10 } from "fs";
|
|
6194
5699
|
function getAnthropicAdminKey() {
|
|
@@ -6483,15 +5988,15 @@ var init_open_projects = __esm(() => {
|
|
|
6483
5988
|
// src/lib/config.ts
|
|
6484
5989
|
var exports_config = {};
|
|
6485
5990
|
__export(exports_config, {
|
|
6486
|
-
|
|
6487
|
-
saveConfig: () => saveConfig2,
|
|
5991
|
+
getConfigValue: () => getConfigValue,
|
|
6488
5992
|
loadConfig: () => loadConfig2,
|
|
6489
|
-
|
|
5993
|
+
saveConfig: () => saveConfig2,
|
|
5994
|
+
setConfigValue: () => setConfigValue
|
|
6490
5995
|
});
|
|
6491
5996
|
import { existsSync as existsSync13, readFileSync as readFileSync11, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
6492
|
-
import { dirname as dirname2, join as
|
|
5997
|
+
import { dirname as dirname2, join as join14 } from "path";
|
|
6493
5998
|
function getConfigPath() {
|
|
6494
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
5999
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join14(getDataDir(), "config.json");
|
|
6495
6000
|
}
|
|
6496
6001
|
function loadConfig2() {
|
|
6497
6002
|
try {
|
|
@@ -6880,7 +6385,7 @@ function sqliteToPostgres(sql) {
|
|
|
6880
6385
|
// src/db/sync-pg.ts
|
|
6881
6386
|
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
6882
6387
|
import { fileURLToPath } from "url";
|
|
6883
|
-
import { dirname as dirname3, join as
|
|
6388
|
+
import { dirname as dirname3, join as join15 } from "path";
|
|
6884
6389
|
import { existsSync as existsSync15 } from "fs";
|
|
6885
6390
|
function translateSqliteDates(sql) {
|
|
6886
6391
|
let s = sql;
|
|
@@ -6906,9 +6411,9 @@ function translateSqliteDates(sql) {
|
|
|
6906
6411
|
function resolveWorkerPath() {
|
|
6907
6412
|
const here = dirname3(fileURLToPath(import.meta.url));
|
|
6908
6413
|
const candidates = [
|
|
6909
|
-
|
|
6910
|
-
|
|
6911
|
-
|
|
6414
|
+
join15(here, "pg-sync-worker.ts"),
|
|
6415
|
+
join15(here, "pg-sync-worker.js"),
|
|
6416
|
+
join15(here, "..", "db", "pg-sync-worker.js")
|
|
6912
6417
|
];
|
|
6913
6418
|
for (const candidate of candidates)
|
|
6914
6419
|
if (existsSync15(candidate))
|
|
@@ -7014,13 +6519,18 @@ var init_pg_migrate = () => {};
|
|
|
7014
6519
|
var init_pg_migrations = () => {};
|
|
7015
6520
|
|
|
7016
6521
|
// src/db/cloud.ts
|
|
7017
|
-
import { assertNoLegacyStorageMode } from "@hasna/contracts/server-backend";
|
|
7018
6522
|
import pg2 from "pg";
|
|
6523
|
+
function assertNoRetiredBackendKey(env) {
|
|
6524
|
+
const legacyKey = RETIRED_BACKEND_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
|
|
6525
|
+
if (!legacyKey)
|
|
6526
|
+
return;
|
|
6527
|
+
throw new Error(`${legacyKey} was removed. Delete the retired variable; ` + "set HASNA_ECONOMY_DATABASE_URL to select the postgresql server backend, " + "or leave it unset for sqlite.");
|
|
6528
|
+
}
|
|
7019
6529
|
function getCloudDatabaseUrl(env = process.env) {
|
|
7020
6530
|
return env["HASNA_ECONOMY_DATABASE_URL"]?.trim() || env["ECONOMY_DATABASE_URL"]?.trim() || env["DATABASE_URL"]?.trim() || undefined;
|
|
7021
6531
|
}
|
|
7022
6532
|
function resolveEconomyServerBackend(env = process.env) {
|
|
7023
|
-
|
|
6533
|
+
assertNoRetiredBackendKey(env);
|
|
7024
6534
|
return getCloudDatabaseUrl(env) ? "postgresql" : "sqlite";
|
|
7025
6535
|
}
|
|
7026
6536
|
function isPostgresBackend(env = process.env) {
|
|
@@ -7059,10 +6569,17 @@ function authClientFromPool(pool) {
|
|
|
7059
6569
|
}
|
|
7060
6570
|
};
|
|
7061
6571
|
}
|
|
6572
|
+
var RETIRED_BACKEND_KEYS;
|
|
7062
6573
|
var init_cloud = __esm(() => {
|
|
7063
6574
|
init_sync_pg();
|
|
7064
6575
|
init_pg_migrate();
|
|
7065
6576
|
init_pg_migrations();
|
|
6577
|
+
RETIRED_BACKEND_KEYS = [
|
|
6578
|
+
"HASNA_ECONOMY_STORAGE_MODE",
|
|
6579
|
+
"HASNA_ECONOMY_MODE",
|
|
6580
|
+
"ECONOMY_STORAGE_MODE",
|
|
6581
|
+
"ECONOMY_MODE"
|
|
6582
|
+
];
|
|
7066
6583
|
});
|
|
7067
6584
|
|
|
7068
6585
|
// src/openapi.ts
|
|
@@ -8709,10 +8226,10 @@ var init_openapi = __esm(() => {
|
|
|
8709
8226
|
// src/server/serve.ts
|
|
8710
8227
|
var exports_serve = {};
|
|
8711
8228
|
__export(exports_serve, {
|
|
8712
|
-
|
|
8713
|
-
openApiDocument: () => openApiDocument,
|
|
8229
|
+
createHandler: () => createHandler,
|
|
8714
8230
|
createServerFetch: () => createServerFetch,
|
|
8715
|
-
|
|
8231
|
+
openApiDocument: () => openApiDocument,
|
|
8232
|
+
startServer: () => startServer
|
|
8716
8233
|
});
|
|
8717
8234
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
8718
8235
|
import { existsSync as existsSync16 } from "fs";
|
|
@@ -9101,7 +8618,24 @@ function createHandler(db, options = {}) {
|
|
|
9101
8618
|
const body = await jsonBody(req);
|
|
9102
8619
|
if (!body)
|
|
9103
8620
|
return err("invalid JSON body");
|
|
9104
|
-
|
|
8621
|
+
const result = bulkIngest(db, body);
|
|
8622
|
+
const reqRows = Array.isArray(body?.["requests"]) ? body["requests"] : [];
|
|
8623
|
+
const touchedSessions = new Set;
|
|
8624
|
+
for (const r of reqRows) {
|
|
8625
|
+
const sid = r?.["session_id"];
|
|
8626
|
+
if (typeof sid === "string" && sid)
|
|
8627
|
+
touchedSessions.add(sid);
|
|
8628
|
+
}
|
|
8629
|
+
for (const sid of touchedSessions) {
|
|
8630
|
+
const hasReq = db.prepare(`SELECT COUNT(*) AS c FROM requests WHERE session_id = ?`).get(sid);
|
|
8631
|
+
if (Number(hasReq?.c ?? 0) > 0)
|
|
8632
|
+
rollupSession(db, sid);
|
|
8633
|
+
}
|
|
8634
|
+
try {
|
|
8635
|
+
const { checkAndFireWebhooks: checkAndFireWebhooks2 } = await Promise.resolve().then(() => (init_webhooks(), exports_webhooks));
|
|
8636
|
+
await checkAndFireWebhooks2(db);
|
|
8637
|
+
} catch {}
|
|
8638
|
+
return ok(result);
|
|
9105
8639
|
}
|
|
9106
8640
|
if (path === "/api/usage" && method === "GET") {
|
|
9107
8641
|
const period = url.searchParams.get("period") ?? "month";
|
|
@@ -9348,16 +8882,16 @@ var init_serve = __esm(() => {
|
|
|
9348
8882
|
// src/cli/commands/menubar.ts
|
|
9349
8883
|
var exports_menubar = {};
|
|
9350
8884
|
__export(exports_menubar, {
|
|
9351
|
-
|
|
9352
|
-
menubarStop: () => menubarStop,
|
|
8885
|
+
menubarInstall: () => menubarInstall,
|
|
9353
8886
|
menubarStart: () => menubarStart,
|
|
9354
|
-
|
|
8887
|
+
menubarStop: () => menubarStop,
|
|
8888
|
+
menubarUninstall: () => menubarUninstall
|
|
9355
8889
|
});
|
|
9356
8890
|
import chalk6 from "chalk";
|
|
9357
8891
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
9358
8892
|
import { cpSync, existsSync as existsSync17, mkdirSync as mkdirSync4, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
9359
8893
|
import { tmpdir, arch } from "os";
|
|
9360
|
-
import { join as
|
|
8894
|
+
import { join as join16 } from "path";
|
|
9361
8895
|
function getArch() {
|
|
9362
8896
|
return arch() === "arm64" ? "arm64" : "x86_64";
|
|
9363
8897
|
}
|
|
@@ -9399,8 +8933,8 @@ async function menubarInstall(opts) {
|
|
|
9399
8933
|
console.error(chalk6.red(`\u2717 Failed to fetch release info: ${e instanceof Error ? e.message : String(e)}`));
|
|
9400
8934
|
process.exit(1);
|
|
9401
8935
|
}
|
|
9402
|
-
const zipPath =
|
|
9403
|
-
const extractDir =
|
|
8936
|
+
const zipPath = join16(tmpdir(), `economy-bar-${cpuArch}.zip`);
|
|
8937
|
+
const extractDir = join16(tmpdir(), "economy-bar-extracted");
|
|
9404
8938
|
console.log(chalk6.cyan(`\u2192 Downloading ${assetUrl}...`));
|
|
9405
8939
|
try {
|
|
9406
8940
|
const res = await fetch(assetUrl, { signal: AbortSignal.timeout(60000) });
|
|
@@ -9420,7 +8954,7 @@ async function menubarInstall(opts) {
|
|
|
9420
8954
|
execFileSync3("unzip", ["-q", zipPath, "-d", extractDir], { stdio: "ignore" });
|
|
9421
8955
|
if (isInstalled())
|
|
9422
8956
|
rmSync(APP_PATH, { recursive: true, force: true });
|
|
9423
|
-
cpSync(
|
|
8957
|
+
cpSync(join16(extractDir, "Economy Bar.app"), APP_PATH, { recursive: true });
|
|
9424
8958
|
try {
|
|
9425
8959
|
execFileSync3("xattr", ["-rd", "com.apple.quarantine", APP_PATH], { stdio: "ignore" });
|
|
9426
8960
|
} catch {}
|
|
@@ -11296,14 +10830,171 @@ init_billing();
|
|
|
11296
10830
|
init_package_metadata();
|
|
11297
10831
|
init_pricing();
|
|
11298
10832
|
init_store();
|
|
11299
|
-
init_billing_diff();
|
|
11300
|
-
init_transport();
|
|
11301
10833
|
import { execSync } from "child_process";
|
|
10834
|
+
|
|
10835
|
+
// src/lib/cloud-ingest.ts
|
|
10836
|
+
init_database();
|
|
10837
|
+
init_pricing();
|
|
10838
|
+
init_sync_all();
|
|
10839
|
+
init_billing();
|
|
10840
|
+
import { join as join13 } from "path";
|
|
10841
|
+
|
|
10842
|
+
// src/lib/sync-maintenance.ts
|
|
10843
|
+
init_database();
|
|
10844
|
+
function backfillMachineId(db) {
|
|
10845
|
+
const machine = getMachineId();
|
|
10846
|
+
const reqCount = db.prepare(`UPDATE requests SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
|
|
10847
|
+
const sessCount = db.prepare(`UPDATE sessions SET machine_id = ? WHERE machine_id = '' OR machine_id IS NULL`).run(machine);
|
|
10848
|
+
return { requests: Number(reqCount.changes), sessions: Number(sessCount.changes) };
|
|
10849
|
+
}
|
|
10850
|
+
async function recalculateZeroCostRequests(db) {
|
|
10851
|
+
const { computeCostFromDb: computeCostFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
|
|
10852
|
+
const zeroRows = db.prepare(`SELECT id, model, input_tokens, output_tokens, cache_read_tokens, cache_create_tokens, cache_create_5m_tokens, cache_create_1h_tokens
|
|
10853
|
+
FROM requests
|
|
10854
|
+
WHERE cost_usd = 0
|
|
10855
|
+
AND (input_tokens > 0 OR output_tokens > 0 OR cache_read_tokens > 0 OR cache_create_tokens > 0)`).all();
|
|
10856
|
+
let fixed = 0;
|
|
10857
|
+
for (const r of zeroRows) {
|
|
10858
|
+
const cache5m = r.cache_create_5m_tokens ?? r.cache_create_tokens;
|
|
10859
|
+
const cost = computeCostFromDb2(db, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, cache5m, r.cache_create_1h_tokens ?? 0);
|
|
10860
|
+
if (cost > 0) {
|
|
10861
|
+
db.prepare(`UPDATE requests SET cost_usd = ? WHERE id = ?`).run(cost, r.id);
|
|
10862
|
+
fixed++;
|
|
10863
|
+
}
|
|
10864
|
+
}
|
|
10865
|
+
if (fixed > 0) {
|
|
10866
|
+
const touchedSessions = new Set(zeroRows.map((r) => {
|
|
10867
|
+
const row = db.prepare(`SELECT session_id FROM requests WHERE id = ?`).get(r.id);
|
|
10868
|
+
return row?.session_id;
|
|
10869
|
+
}).filter(Boolean));
|
|
10870
|
+
for (const sid of touchedSessions)
|
|
10871
|
+
rollupSession(db, sid);
|
|
10872
|
+
}
|
|
10873
|
+
return { fixed, total: zeroRows.length, buckets: queryZeroCostTokenizedModels(db, 8) };
|
|
10874
|
+
}
|
|
10875
|
+
|
|
10876
|
+
// src/lib/cloud-ingest.ts
|
|
10877
|
+
var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions"];
|
|
10878
|
+
function getIngestCachePath() {
|
|
10879
|
+
if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
|
|
10880
|
+
return process.env["HASNA_ECONOMY_INGEST_CACHE"];
|
|
10881
|
+
return join13(getDataDir(), "ingest-cache.db");
|
|
10882
|
+
}
|
|
10883
|
+
function copyIngestState(from, to) {
|
|
10884
|
+
const rows = from.prepare(`SELECT source, key, value FROM ingest_state`).all();
|
|
10885
|
+
to.prepare(`DELETE FROM ingest_state`).run();
|
|
10886
|
+
const insert = to.prepare(`INSERT INTO ingest_state (source, key, value) VALUES (?, ?, ?)`);
|
|
10887
|
+
for (const row of rows)
|
|
10888
|
+
insert.run(row.source, row.key, row.value);
|
|
10889
|
+
}
|
|
10890
|
+
function exportIngestRows(db) {
|
|
10891
|
+
const body = {};
|
|
10892
|
+
for (const table2 of CLOUD_INGEST_TABLES) {
|
|
10893
|
+
const rows = db.prepare(`SELECT * FROM ${table2}`).all();
|
|
10894
|
+
if (rows.length > 0)
|
|
10895
|
+
body[table2] = rows;
|
|
10896
|
+
}
|
|
10897
|
+
return body;
|
|
10898
|
+
}
|
|
10899
|
+
function unwrapEnvelope(raw) {
|
|
10900
|
+
if (raw && typeof raw === "object" && "data" in raw) {
|
|
10901
|
+
const data = raw.data;
|
|
10902
|
+
return data && typeof data === "object" ? data : {};
|
|
10903
|
+
}
|
|
10904
|
+
return raw ?? {};
|
|
10905
|
+
}
|
|
10906
|
+
async function postIngest(cloud, body) {
|
|
10907
|
+
const raw = await cloud.client.transport.post("/ingest", body);
|
|
10908
|
+
const payload = unwrapEnvelope(raw);
|
|
10909
|
+
return {
|
|
10910
|
+
ingested: payload["ingested"] ?? {},
|
|
10911
|
+
total: Number(payload["total"] ?? 0)
|
|
10912
|
+
};
|
|
10913
|
+
}
|
|
10914
|
+
async function syncAllToCloud(cloud, opts = {}) {
|
|
10915
|
+
const scratch = openDatabase(":memory:", true);
|
|
10916
|
+
ensurePricingSeeded(scratch);
|
|
10917
|
+
const cache2 = openDatabase(opts.cachePath ?? getIngestCachePath(), true);
|
|
10918
|
+
try {
|
|
10919
|
+
if (opts.force)
|
|
10920
|
+
cache2.prepare(`DELETE FROM ingest_state`).run();
|
|
10921
|
+
copyIngestState(cache2, scratch);
|
|
10922
|
+
const result = await syncAll(scratch, opts);
|
|
10923
|
+
if (opts.backfillMachine)
|
|
10924
|
+
backfillMachineId(scratch);
|
|
10925
|
+
if (opts.recalculate)
|
|
10926
|
+
await recalculateZeroCostRequests(scratch);
|
|
10927
|
+
const body = exportIngestRows(scratch);
|
|
10928
|
+
if (Object.keys(body).length === 0) {
|
|
10929
|
+
copyIngestState(scratch, cache2);
|
|
10930
|
+
return { ...result, posted: false };
|
|
10931
|
+
}
|
|
10932
|
+
const response = await postIngest(cloud, body);
|
|
10933
|
+
copyIngestState(scratch, cache2);
|
|
10934
|
+
return { ...result, posted: true, ingested: response.ingested, total: response.total };
|
|
10935
|
+
} finally {
|
|
10936
|
+
try {
|
|
10937
|
+
scratch.close();
|
|
10938
|
+
} catch {}
|
|
10939
|
+
try {
|
|
10940
|
+
cache2.close();
|
|
10941
|
+
} catch {}
|
|
10942
|
+
}
|
|
10943
|
+
}
|
|
10944
|
+
async function billingSyncToCloud(cloud, opts = {}) {
|
|
10945
|
+
const scratch = openDatabase(":memory:", true);
|
|
10946
|
+
try {
|
|
10947
|
+
const days = opts.days ?? 31;
|
|
10948
|
+
const providers = {};
|
|
10949
|
+
const attempts = [
|
|
10950
|
+
["anthropic", () => syncAnthropicBilling(scratch, { days })],
|
|
10951
|
+
["openai", () => syncOpenAIBilling(scratch, { days })],
|
|
10952
|
+
["gemini", () => syncGeminiBilling(scratch, { days })]
|
|
10953
|
+
];
|
|
10954
|
+
const anySelected = opts.anthropic || opts.openai || opts.gemini;
|
|
10955
|
+
let totalUsd = 0;
|
|
10956
|
+
for (const [provider, run] of attempts) {
|
|
10957
|
+
if (anySelected && !opts[provider])
|
|
10958
|
+
continue;
|
|
10959
|
+
{
|
|
10960
|
+
try {
|
|
10961
|
+
const r = await run();
|
|
10962
|
+
providers[provider] = { ok: true, days: r.days, totalUsd: r.totalUsd };
|
|
10963
|
+
totalUsd += r.totalUsd;
|
|
10964
|
+
} catch (e) {
|
|
10965
|
+
providers[provider] = { ok: false, error: e instanceof Error ? e.message : String(e) };
|
|
10966
|
+
}
|
|
10967
|
+
}
|
|
10968
|
+
}
|
|
10969
|
+
const body = exportIngestRows(scratch);
|
|
10970
|
+
if (Object.keys(body).length === 0)
|
|
10971
|
+
return { providers, totalUsd, posted: false };
|
|
10972
|
+
const response = await postIngest(cloud, body);
|
|
10973
|
+
return { providers, totalUsd, posted: true, ingested: response.ingested, total: response.total };
|
|
10974
|
+
} finally {
|
|
10975
|
+
try {
|
|
10976
|
+
scratch.close();
|
|
10977
|
+
} catch {}
|
|
10978
|
+
}
|
|
10979
|
+
}
|
|
10980
|
+
|
|
10981
|
+
// src/cli/index.ts
|
|
10982
|
+
init_cloud_storage();
|
|
10983
|
+
init_billing_diff();
|
|
10984
|
+
|
|
10985
|
+
// src/lib/contracts-client/transport.ts
|
|
10986
|
+
import { HasnaHttpError } from "@hasna/contracts/client";
|
|
10987
|
+
|
|
10988
|
+
// src/cli/index.ts
|
|
11302
10989
|
var program = new Command;
|
|
11303
10990
|
program.name("economy").description("AI coding cost tracker \u2014 Claude, Takumi, Codex, Gemini, OpenCode, Cursor, Pi, Hermes").version(packageMetadata.version);
|
|
11304
10991
|
async function autoSync(opts = {}) {
|
|
11305
|
-
if (isCloudStore())
|
|
10992
|
+
if (isCloudStore()) {
|
|
10993
|
+
const cloud = economyCloudStorage();
|
|
10994
|
+
if (cloud.active)
|
|
10995
|
+
await syncAllToCloud(cloud, opts);
|
|
11306
10996
|
return;
|
|
10997
|
+
}
|
|
11307
10998
|
const db = openDatabase();
|
|
11308
10999
|
ensurePricingSeeded(db);
|
|
11309
11000
|
await syncAll(db, opts);
|
|
@@ -11518,7 +11209,34 @@ program.action(async () => {
|
|
|
11518
11209
|
});
|
|
11519
11210
|
program.command("sync").description("Ingest cost data from all coding agents").option("--claude", "Only ingest Claude Code telemetry").option("--takumi", "Only ingest Takumi sessions").option("--codex", "Only ingest Codex sessions").option("--gemini", "Only ingest Gemini CLI sessions").option("--opencode", "Only ingest OpenCode sessions").option("--cursor", "Only ingest Cursor usage").option("--pi", "Only ingest Pi sessions").option("--hermes", "Only ingest Hermes sessions").option("--loops", "Only ingest OpenLoops orchestration/judge token usage").option("-v, --verbose", "Verbose output").option("--force", "Force re-process all files (ignore mtime cache)").option("--backfill-machine", "Tag existing records that have no machine_id with current hostname").option("--recalculate", "Recalculate costs for all requests with cost_usd = 0").action(async (opts) => {
|
|
11520
11211
|
if (isCloudStore()) {
|
|
11521
|
-
|
|
11212
|
+
const cloud = economyCloudStorage();
|
|
11213
|
+
if (!cloud.active) {
|
|
11214
|
+
console.log(chalk7.yellow("cloud mode: sync transport unavailable (set HASNA_ECONOMY_API_URL and HASNA_ECONOMY_API_KEY)"));
|
|
11215
|
+
return;
|
|
11216
|
+
}
|
|
11217
|
+
const result2 = await syncAllToCloud(cloud, {
|
|
11218
|
+
claude: opts.claude,
|
|
11219
|
+
takumi: opts.takumi,
|
|
11220
|
+
codex: opts.codex,
|
|
11221
|
+
gemini: opts.gemini,
|
|
11222
|
+
opencode: opts.opencode,
|
|
11223
|
+
cursor: opts.cursor,
|
|
11224
|
+
pi: opts.pi,
|
|
11225
|
+
hermes: opts.hermes,
|
|
11226
|
+
loops: opts.loops,
|
|
11227
|
+
verbose: opts.verbose,
|
|
11228
|
+
force: opts.force,
|
|
11229
|
+
backfillMachine: opts.backfillMachine,
|
|
11230
|
+
recalculate: opts.recalculate
|
|
11231
|
+
});
|
|
11232
|
+
if (result2.posted) {
|
|
11233
|
+
const parts = Object.entries(result2.ingested ?? {}).filter(([, n]) => n > 0).map(([table2, n]) => `${n} ${table2}`);
|
|
11234
|
+
console.log(chalk7.green(`\u2713 pushed ${parts.join(", ")} to the shared API (${result2.total} rows)`));
|
|
11235
|
+
} else {
|
|
11236
|
+
console.log(chalk7.dim("\u2713 no new provider data to push (mtime cache current)"));
|
|
11237
|
+
}
|
|
11238
|
+
console.log(chalk7.bold.green(`
|
|
11239
|
+
\u2713 Sync complete`));
|
|
11522
11240
|
return;
|
|
11523
11241
|
}
|
|
11524
11242
|
const db = openDatabase();
|
|
@@ -11557,34 +11275,14 @@ program.command("sync").description("Ingest cost data from all coding agents").o
|
|
|
11557
11275
|
console.log(chalk7.green(`\u2713 deduped ${result.deduped}`));
|
|
11558
11276
|
if (opts.backfillMachine) {
|
|
11559
11277
|
const machine = getMachineId();
|
|
11560
|
-
const
|
|
11561
|
-
|
|
11562
|
-
console.log(chalk7.cyan(`\u2192 Backfilled machine_id='${machine}': ${reqCount.changes} requests, ${sessCount.changes} sessions`));
|
|
11278
|
+
const r = backfillMachineId(db);
|
|
11279
|
+
console.log(chalk7.cyan(`\u2192 Backfilled machine_id='${machine}': ${r.requests} requests, ${r.sessions} sessions`));
|
|
11563
11280
|
}
|
|
11564
11281
|
if (opts.recalculate) {
|
|
11565
|
-
const {
|
|
11566
|
-
const
|
|
11567
|
-
|
|
11568
|
-
|
|
11569
|
-
const cache5m = r.cache_create_5m_tokens ?? r.cache_create_tokens;
|
|
11570
|
-
const cost = computeCostFromDb2(db, r.model, r.input_tokens, r.output_tokens, r.cache_read_tokens, cache5m, r.cache_create_1h_tokens ?? 0);
|
|
11571
|
-
if (cost > 0) {
|
|
11572
|
-
db.prepare(`UPDATE requests SET cost_usd = ? WHERE id = ?`).run(cost, r.id);
|
|
11573
|
-
fixed++;
|
|
11574
|
-
}
|
|
11575
|
-
}
|
|
11576
|
-
if (fixed > 0) {
|
|
11577
|
-
const touchedSessions = new Set(zeroRows.map((r) => {
|
|
11578
|
-
const row = db.prepare(`SELECT session_id FROM requests WHERE id = ?`).get(r.id);
|
|
11579
|
-
return row?.session_id;
|
|
11580
|
-
}).filter(Boolean));
|
|
11581
|
-
const { rollupSession: rollupSession2 } = await Promise.resolve().then(() => (init_database(), exports_database));
|
|
11582
|
-
for (const sid of touchedSessions) {
|
|
11583
|
-
rollupSession2(db, sid);
|
|
11584
|
-
}
|
|
11585
|
-
}
|
|
11586
|
-
console.log(chalk7.cyan(`\u2192 Recalculated: ${fixed}/${zeroRows.length} zero-cost requests now have pricing`));
|
|
11587
|
-
const zeroCostBuckets = queryZeroCostTokenizedModels(db, 8);
|
|
11282
|
+
const { getPricingFromDb: getPricingFromDb2 } = await Promise.resolve().then(() => (init_pricing(), exports_pricing));
|
|
11283
|
+
const r = await recalculateZeroCostRequests(db);
|
|
11284
|
+
console.log(chalk7.cyan(`\u2192 Recalculated: ${r.fixed}/${r.total} zero-cost requests now have pricing`));
|
|
11285
|
+
const zeroCostBuckets = r.buckets;
|
|
11588
11286
|
if (zeroCostBuckets.length > 0) {
|
|
11589
11287
|
console.log(chalk7.yellow("\u2192 Zero-cost token buckets remain:"));
|
|
11590
11288
|
for (const row of zeroCostBuckets) {
|
|
@@ -12410,13 +12108,26 @@ program.command("remove <type> <id>").alias("rm").description("Remove a record.
|
|
|
12410
12108
|
});
|
|
12411
12109
|
var billingCmd = program.command("billing").description("Pull actual billing from provider billing sources (ground truth)");
|
|
12412
12110
|
billingCmd.command("sync").description("Sync actual billing from Anthropic, OpenAI, and Gemini billing sources").option("--days <n>", "Days of history to fetch", "31").option("--anthropic", "Only sync Anthropic").option("--openai", "Only sync OpenAI").option("--gemini", "Only sync Gemini").action(async (opts) => {
|
|
12413
|
-
if (isCloudStore()) {
|
|
12414
|
-
console.log(chalk7.yellow("cloud mode: billing ingest is a local-only operation; nothing to sync (reads route to the cloud API)"));
|
|
12415
|
-
return;
|
|
12416
|
-
}
|
|
12417
12111
|
const days = parsePositiveCliInteger(opts.days ?? "31", "--days");
|
|
12418
12112
|
if (days > 366)
|
|
12419
12113
|
fail("--days must be between 1 and 366");
|
|
12114
|
+
if (isCloudStore()) {
|
|
12115
|
+
const cloud = economyCloudStorage();
|
|
12116
|
+
if (!cloud.active) {
|
|
12117
|
+
console.log(chalk7.yellow("cloud mode: billing sync transport unavailable (set HASNA_ECONOMY_API_URL and HASNA_ECONOMY_API_KEY)"));
|
|
12118
|
+
return;
|
|
12119
|
+
}
|
|
12120
|
+
const result = await billingSyncToCloud(cloud, { days, anthropic: opts.anthropic, openai: opts.openai, gemini: opts.gemini });
|
|
12121
|
+
for (const [provider, p] of Object.entries(result.providers)) {
|
|
12122
|
+
if (p.ok)
|
|
12123
|
+
console.log(chalk7.green(`\u2713 ${provider} billing: ${p.days} days, $${p.totalUsd.toFixed(2)}`));
|
|
12124
|
+
else
|
|
12125
|
+
console.log(chalk7.red(`\u2717 ${provider}: ${p.error}`));
|
|
12126
|
+
}
|
|
12127
|
+
if (result.posted)
|
|
12128
|
+
console.log(chalk7.green(`\u2713 pushed ${result.total} billing rows to the shared API`));
|
|
12129
|
+
return;
|
|
12130
|
+
}
|
|
12420
12131
|
const db = openDatabase();
|
|
12421
12132
|
const doAll = !opts.anthropic && !opts.openai && !opts.gemini;
|
|
12422
12133
|
if (opts.anthropic || doAll) {
|