@hasna/economy 0.3.27 → 0.4.0
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 +101 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +9 -16
- package/SECURITY.md +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/index.js +1182 -862
- package/dist/db/cloud.d.ts +16 -8
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +58 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +236 -27
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts +19 -0
- package/dist/ingest/loops.d.ts.map +1 -1
- package/dist/lib/accounts-store.d.ts +83 -0
- package/dist/lib/accounts-store.d.ts.map +1 -0
- package/dist/lib/accounts.d.ts +2 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/api-display-url.d.ts +19 -0
- package/dist/lib/api-display-url.d.ts.map +1 -0
- package/dist/lib/autosync-gate.d.ts +15 -0
- package/dist/lib/autosync-gate.d.ts.map +1 -0
- package/dist/lib/cloud-ingest.d.ts +15 -1
- package/dist/lib/cloud-ingest.d.ts.map +1 -1
- package/dist/lib/cloud-storage.d.ts +166 -10
- package/dist/lib/cloud-storage.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/model-config.d.ts +1 -1
- package/dist/lib/model-config.d.ts.map +1 -1
- package/dist/lib/serve-auth.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +6 -3
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/lib/test-hermetic-accounts.d.ts +3 -2
- package/dist/lib/test-hermetic-accounts.d.ts.map +1 -1
- package/dist/lib/test-hermetic-fleet-env.d.ts +4 -0
- package/dist/lib/test-hermetic-fleet-env.d.ts.map +1 -0
- package/dist/mcp/agent-registry.d.ts +85 -0
- package/dist/mcp/agent-registry.d.ts.map +1 -0
- package/dist/mcp/harness.d.ts +26 -0
- package/dist/mcp/harness.d.ts.map +1 -0
- package/dist/mcp/index.js +1387 -311
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +468 -24
- package/dist/server/index.js +595 -164
- package/dist/server/serve.d.ts +0 -2
- package/dist/server/serve.d.ts.map +1 -1
- package/docs/cli.md +2 -3
- package/docs/configuration.md +23 -10
- package/docs/ingestion.md +1 -1
- package/docs/mcp.md +4 -2
- package/docs/otel.md +8 -2
- package/docs/rest-api.md +2 -2
- package/package.json +7 -11
- package/postinstall.js +82 -0
- package/dashboard/README.md +0 -26
- package/dist/cli/brains.d.ts +0 -3
- package/dist/cli/brains.d.ts.map +0 -1
package/dist/server/index.js
CHANGED
|
@@ -595,7 +595,60 @@ import { execFileSync } from "child_process";
|
|
|
595
595
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
596
596
|
import { hostname, platform } from "os";
|
|
597
597
|
import { homedir } from "os";
|
|
598
|
-
import { join } from "path";
|
|
598
|
+
import { join, resolve } from "path";
|
|
599
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
600
|
+
import { join as pathsResolverJoin } from "path";
|
|
601
|
+
function pathsResolverAssertApp(app) {
|
|
602
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
603
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
604
|
+
}
|
|
605
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
606
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function pathsResolverAssertKind(kind) {
|
|
610
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
611
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function pathsResolverBaseDir(kind, options) {
|
|
615
|
+
pathsResolverAssertKind(kind);
|
|
616
|
+
const env = options.env ?? process.env;
|
|
617
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
618
|
+
if (typeof override === "string" && override.length > 0)
|
|
619
|
+
return override;
|
|
620
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
621
|
+
const platform2 = options.platform ?? process.platform;
|
|
622
|
+
if (platform2 === "darwin") {
|
|
623
|
+
switch (kind) {
|
|
624
|
+
case "config":
|
|
625
|
+
case "data":
|
|
626
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
627
|
+
case "cache":
|
|
628
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
629
|
+
case "state":
|
|
630
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
switch (kind) {
|
|
634
|
+
case "config":
|
|
635
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
636
|
+
case "data":
|
|
637
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
638
|
+
case "state":
|
|
639
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
640
|
+
case "cache":
|
|
641
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function pathsResolverResolve(kind, options) {
|
|
645
|
+
pathsResolverAssertApp(options.app);
|
|
646
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
647
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
648
|
+
}
|
|
649
|
+
function dataDir(options) {
|
|
650
|
+
return pathsResolverResolve("data", options);
|
|
651
|
+
}
|
|
599
652
|
function normalizeMachineId(value) {
|
|
600
653
|
const id = value?.trim().toLowerCase().split(".")[0];
|
|
601
654
|
return id && id.length > 0 ? id : null;
|
|
@@ -618,7 +671,7 @@ function macHostMachineId() {
|
|
|
618
671
|
return null;
|
|
619
672
|
}
|
|
620
673
|
function getMachineId() {
|
|
621
|
-
const envMachine = normalizeMachineId(process.env["
|
|
674
|
+
const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
|
|
622
675
|
if (envMachine)
|
|
623
676
|
return envMachine;
|
|
624
677
|
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
@@ -626,21 +679,48 @@ function getMachineId() {
|
|
|
626
679
|
return macHostMachineId() ?? hostMachine;
|
|
627
680
|
return hostMachine;
|
|
628
681
|
}
|
|
629
|
-
function
|
|
682
|
+
function getHomeDir() {
|
|
630
683
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
684
|
+
if (!home)
|
|
685
|
+
throw new Error("Could not resolve the user home directory");
|
|
686
|
+
return home;
|
|
687
|
+
}
|
|
688
|
+
function getResolverDataRoot() {
|
|
689
|
+
return dataDir({
|
|
690
|
+
app: "economy",
|
|
691
|
+
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
function getLegacyDataRoot() {
|
|
695
|
+
return join(getHomeDir(), ".hasna", "economy");
|
|
696
|
+
}
|
|
697
|
+
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
698
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
699
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
700
|
+
return true;
|
|
701
|
+
return existsSync(join(resolved, "economy.db"));
|
|
702
|
+
}
|
|
703
|
+
function getExactDataRoot() {
|
|
704
|
+
const dir = process.env["HASNA_ECONOMY_HOME"] ?? process.env["ECONOMY_HOME"];
|
|
705
|
+
if (dir && dir.trim())
|
|
706
|
+
return dir.trim();
|
|
707
|
+
return;
|
|
708
|
+
}
|
|
709
|
+
function getDataDir() {
|
|
710
|
+
const exact = getExactDataRoot();
|
|
711
|
+
const effective = exact ? resolve(exact) : adoptResolverDataRoot(getResolverDataRoot()) ? resolve(getResolverDataRoot()) : getLegacyDataRoot();
|
|
712
|
+
const oldDir = join(getHomeDir(), ".economy");
|
|
713
|
+
if (existsSync(oldDir) && !existsSync(effective)) {
|
|
714
|
+
mkdirSync(effective, { recursive: true });
|
|
635
715
|
for (const file of readdirSync(oldDir)) {
|
|
636
716
|
const oldPath = join(oldDir, file);
|
|
637
717
|
if (statSync(oldPath).isFile()) {
|
|
638
|
-
copyFileSync(oldPath, join(
|
|
718
|
+
copyFileSync(oldPath, join(effective, file));
|
|
639
719
|
}
|
|
640
720
|
}
|
|
641
721
|
}
|
|
642
|
-
mkdirSync(
|
|
643
|
-
return
|
|
722
|
+
mkdirSync(effective, { recursive: true });
|
|
723
|
+
return effective;
|
|
644
724
|
}
|
|
645
725
|
function getDbPath() {
|
|
646
726
|
if (process.env["HASNA_ECONOMY_DB_PATH"])
|
|
@@ -1898,6 +1978,10 @@ function getIngestState(db, source, key) {
|
|
|
1898
1978
|
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
|
|
1899
1979
|
return row?.value ?? null;
|
|
1900
1980
|
}
|
|
1981
|
+
function loadIngestState(db, source) {
|
|
1982
|
+
const rows = db.prepare(`SELECT key, value FROM ingest_state WHERE source = ?`).all(source);
|
|
1983
|
+
return new Map(rows.map((row) => [row.key, row.value]));
|
|
1984
|
+
}
|
|
1901
1985
|
function setIngestState(db, source, key, value) {
|
|
1902
1986
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
1903
1987
|
}
|
|
@@ -2336,7 +2420,7 @@ function bulkIngest(db, body) {
|
|
|
2336
2420
|
const total = Object.values(ingested).reduce((a, b) => a + b, 0);
|
|
2337
2421
|
return { ingested, total };
|
|
2338
2422
|
}
|
|
2339
|
-
var numOr = (v, d = 0) => {
|
|
2423
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, numOr = (v, d = 0) => {
|
|
2340
2424
|
if (v == null)
|
|
2341
2425
|
return d;
|
|
2342
2426
|
const num = Number(v);
|
|
@@ -2344,6 +2428,13 @@ var numOr = (v, d = 0) => {
|
|
|
2344
2428
|
}, strOr = (v, d = "") => v == null ? d : String(v);
|
|
2345
2429
|
var init_database = __esm(() => {
|
|
2346
2430
|
init_sqlite_adapter();
|
|
2431
|
+
PATHS_RESOLVER_KIND_ENV = {
|
|
2432
|
+
config: "HASNA_CONFIG_HOME",
|
|
2433
|
+
data: "HASNA_DATA_HOME",
|
|
2434
|
+
state: "HASNA_STATE_HOME",
|
|
2435
|
+
cache: "HASNA_CACHE_HOME"
|
|
2436
|
+
};
|
|
2437
|
+
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2347
2438
|
});
|
|
2348
2439
|
|
|
2349
2440
|
// src/db/dialect.ts
|
|
@@ -2386,8 +2477,8 @@ function sqliteToPostgres(sql) {
|
|
|
2386
2477
|
// src/db/sync-pg.ts
|
|
2387
2478
|
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
2388
2479
|
import { fileURLToPath } from "url";
|
|
2389
|
-
import { dirname, join as
|
|
2390
|
-
import { existsSync as
|
|
2480
|
+
import { dirname, join as join11 } from "path";
|
|
2481
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2391
2482
|
function translateSqliteDates(sql) {
|
|
2392
2483
|
let s = sql;
|
|
2393
2484
|
const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
|
|
@@ -2412,12 +2503,12 @@ function translateSqliteDates(sql) {
|
|
|
2412
2503
|
function resolveWorkerPath() {
|
|
2413
2504
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2414
2505
|
const candidates = [
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2506
|
+
join11(here, "pg-sync-worker.ts"),
|
|
2507
|
+
join11(here, "pg-sync-worker.js"),
|
|
2508
|
+
join11(here, "..", "db", "pg-sync-worker.js")
|
|
2418
2509
|
];
|
|
2419
2510
|
for (const candidate of candidates)
|
|
2420
|
-
if (
|
|
2511
|
+
if (existsSync12(candidate))
|
|
2421
2512
|
return candidate;
|
|
2422
2513
|
return candidates[0];
|
|
2423
2514
|
}
|
|
@@ -2874,7 +2965,7 @@ __export(exports_billing, {
|
|
|
2874
2965
|
syncGeminiBilling: () => syncGeminiBilling,
|
|
2875
2966
|
syncAnthropicBilling: () => syncAnthropicBilling
|
|
2876
2967
|
});
|
|
2877
|
-
import { readFileSync as
|
|
2968
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
2878
2969
|
function getAnthropicAdminKey() {
|
|
2879
2970
|
return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
2880
2971
|
}
|
|
@@ -3101,7 +3192,7 @@ async function syncGeminiBilling(db, opts = {}) {
|
|
|
3101
3192
|
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
3102
3193
|
const fromDateStr = toISODate(start);
|
|
3103
3194
|
const toDateStr = toISODate(end);
|
|
3104
|
-
const rows = parseBillingRows(
|
|
3195
|
+
const rows = parseBillingRows(readFileSync9(exportPath, "utf-8"));
|
|
3105
3196
|
clearBillingRange(db, "gemini", fromDateStr, toDateStr);
|
|
3106
3197
|
const updatedAt = new Date().toISOString();
|
|
3107
3198
|
let totalUsd = 0;
|
|
@@ -3165,16 +3256,16 @@ var init_open_projects = __esm(() => {
|
|
|
3165
3256
|
});
|
|
3166
3257
|
|
|
3167
3258
|
// src/lib/config.ts
|
|
3168
|
-
import { existsSync as
|
|
3169
|
-
import { dirname as dirname2, join as
|
|
3259
|
+
import { existsSync as existsSync13, readFileSync as readFileSync10, writeFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
3260
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
3170
3261
|
function getConfigPath() {
|
|
3171
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
3262
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join12(getDataDir(), "config.json");
|
|
3172
3263
|
}
|
|
3173
3264
|
function loadConfig() {
|
|
3174
3265
|
try {
|
|
3175
3266
|
const configPath = getConfigPath();
|
|
3176
|
-
if (
|
|
3177
|
-
const raw =
|
|
3267
|
+
if (existsSync13(configPath)) {
|
|
3268
|
+
const raw = readFileSync10(configPath, "utf-8");
|
|
3178
3269
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
3179
3270
|
}
|
|
3180
3271
|
} catch {}
|
|
@@ -3319,9 +3410,9 @@ function isAgent(value) {
|
|
|
3319
3410
|
// src/ingest/claude.ts
|
|
3320
3411
|
init_database();
|
|
3321
3412
|
init_pricing();
|
|
3322
|
-
import { readdirSync as readdirSync2, readFileSync, existsSync as
|
|
3323
|
-
import { homedir as
|
|
3324
|
-
import { join as
|
|
3413
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
3414
|
+
import { homedir as homedir3 } from "os";
|
|
3415
|
+
import { join as join3, basename } from "path";
|
|
3325
3416
|
|
|
3326
3417
|
// src/lib/savings.ts
|
|
3327
3418
|
function periodWhere2(period, column) {
|
|
@@ -3477,8 +3568,319 @@ function defaultCostBasisForAgent(agent) {
|
|
|
3477
3568
|
return "estimated";
|
|
3478
3569
|
}
|
|
3479
3570
|
|
|
3571
|
+
// src/lib/accounts-store.ts
|
|
3572
|
+
import { existsSync as existsSync2, readFileSync } from "fs";
|
|
3573
|
+
import { homedir as homedir2 } from "os";
|
|
3574
|
+
import { join as join2 } from "path";
|
|
3575
|
+
import { z } from "zod";
|
|
3576
|
+
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
3577
|
+
var ACCOUNTS_APP = "accounts";
|
|
3578
|
+
var profileNameSchema = z.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
|
|
3579
|
+
var profileSchema = z.object({
|
|
3580
|
+
name: z.string(),
|
|
3581
|
+
tool: z.string(),
|
|
3582
|
+
email: z.string().optional(),
|
|
3583
|
+
displayName: z.string().optional(),
|
|
3584
|
+
dir: z.string(),
|
|
3585
|
+
description: z.string().optional(),
|
|
3586
|
+
createdAt: z.string(),
|
|
3587
|
+
lastUsedAt: z.string().optional()
|
|
3588
|
+
});
|
|
3589
|
+
var toolDefSchema = z.object({
|
|
3590
|
+
id: z.string(),
|
|
3591
|
+
label: z.string(),
|
|
3592
|
+
envVar: z.string(),
|
|
3593
|
+
defaultDir: z.string(),
|
|
3594
|
+
bin: z.string()
|
|
3595
|
+
});
|
|
3596
|
+
var storeSchema = z.object({
|
|
3597
|
+
version: z.literal(1),
|
|
3598
|
+
current: z.record(z.string(), z.string()).default({}),
|
|
3599
|
+
applied: z.record(z.string(), z.string()).default({}),
|
|
3600
|
+
toolLocks: z.record(z.string(), z.string()).default({}),
|
|
3601
|
+
profiles: z.array(profileSchema).default([]),
|
|
3602
|
+
tools: z.array(toolDefSchema).default([])
|
|
3603
|
+
});
|
|
3604
|
+
function validateEnvPath(value, label) {
|
|
3605
|
+
const trimmed = value.trim();
|
|
3606
|
+
if (!trimmed || trimmed.includes("\x00") || /[\r\n]/.test(trimmed))
|
|
3607
|
+
throw new Error(`invalid ${label}`);
|
|
3608
|
+
return trimmed;
|
|
3609
|
+
}
|
|
3610
|
+
function accountsHome() {
|
|
3611
|
+
const override = process.env.ACCOUNTS_HOME;
|
|
3612
|
+
if (override && override.trim())
|
|
3613
|
+
return validateEnvPath(override, "ACCOUNTS_HOME");
|
|
3614
|
+
return join2(homedir2(), ".hasna", "accounts");
|
|
3615
|
+
}
|
|
3616
|
+
function storePath() {
|
|
3617
|
+
const override = process.env.ACCOUNTS_STORE_PATH;
|
|
3618
|
+
if (override && override.trim())
|
|
3619
|
+
return validateEnvPath(override, "ACCOUNTS_STORE_PATH");
|
|
3620
|
+
return join2(accountsHome(), "accounts.json");
|
|
3621
|
+
}
|
|
3622
|
+
var EMPTY_STORE = {
|
|
3623
|
+
version: 1,
|
|
3624
|
+
current: {},
|
|
3625
|
+
applied: {},
|
|
3626
|
+
toolLocks: {},
|
|
3627
|
+
profiles: [],
|
|
3628
|
+
tools: []
|
|
3629
|
+
};
|
|
3630
|
+
function parseStoreFile() {
|
|
3631
|
+
const path = storePath();
|
|
3632
|
+
if (!existsSync2(path))
|
|
3633
|
+
return structuredClone(EMPTY_STORE);
|
|
3634
|
+
let raw;
|
|
3635
|
+
try {
|
|
3636
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
3637
|
+
} catch (err) {
|
|
3638
|
+
throw new Error(`could not parse store at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3639
|
+
}
|
|
3640
|
+
const parsed = storeSchema.safeParse(raw);
|
|
3641
|
+
if (!parsed.success) {
|
|
3642
|
+
throw new Error(`invalid store at ${path}: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
|
|
3643
|
+
}
|
|
3644
|
+
return parsed.data;
|
|
3645
|
+
}
|
|
3646
|
+
function loadAppliedMap() {
|
|
3647
|
+
const applied = {};
|
|
3648
|
+
for (const [toolId, name] of Object.entries(parseStoreFile().applied)) {
|
|
3649
|
+
if (name && profileNameSchema.safeParse(name).success)
|
|
3650
|
+
applied[toolId] = name;
|
|
3651
|
+
}
|
|
3652
|
+
return applied;
|
|
3653
|
+
}
|
|
3654
|
+
function appliedProfileName(toolId) {
|
|
3655
|
+
return loadAppliedMap()[toolId];
|
|
3656
|
+
}
|
|
3657
|
+
var BUILTIN_TOOLS = [
|
|
3658
|
+
{ id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
|
|
3659
|
+
{ id: "codex-app", label: "Codex App", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "/Applications/Codex.app/Contents/MacOS/Codex" },
|
|
3660
|
+
{ id: "codex", label: "Codex CLI", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "codex" },
|
|
3661
|
+
{ id: "codewith", label: "Codewith", envVar: "CODEWITH_HOME", defaultDir: join2(homedir2(), ".codewith"), bin: "codewith" },
|
|
3662
|
+
{ id: "takumi", label: "Takumi", envVar: "TAKUMI_CONFIG_DIR", defaultDir: join2(homedir2(), ".takumi"), bin: "takumi" },
|
|
3663
|
+
{ id: "gemini", label: "Gemini CLI", envVar: "GEMINI_CONFIG_DIR", defaultDir: join2(homedir2(), ".gemini"), bin: "gemini" },
|
|
3664
|
+
{ id: "opencode", label: "opencode", envVar: "OPENCODE_CONFIG_DIR", defaultDir: join2(homedir2(), ".config", "opencode"), bin: "opencode" },
|
|
3665
|
+
{ id: "cursor", label: "Cursor Agent", envVar: "CURSOR_CONFIG_DIR", defaultDir: join2(homedir2(), ".cursor"), bin: "cursor-agent" },
|
|
3666
|
+
{ id: "pi", label: "Pi Coding Agent", envVar: "PI_CODING_AGENT_HOME", defaultDir: join2(homedir2(), ".pi"), bin: "pi" },
|
|
3667
|
+
{ id: "hermes", label: "Hermes", envVar: "HERMES_HOME", defaultDir: join2(homedir2(), ".hermes"), bin: "hermes" },
|
|
3668
|
+
{ id: "kimi", label: "Kimi Code", envVar: "KIMI_CODE_HOME", defaultDir: join2(homedir2(), ".kimi-code"), bin: "kimi" },
|
|
3669
|
+
{ id: "grok", label: "Grok Build", envVar: "HOME", defaultDir: join2(homedir2(), ".grok"), bin: "grok" }
|
|
3670
|
+
];
|
|
3671
|
+
|
|
3672
|
+
class LocalStore {
|
|
3673
|
+
transport = "local";
|
|
3674
|
+
async listProfiles(tool) {
|
|
3675
|
+
const profiles = parseStoreFile().profiles;
|
|
3676
|
+
const filtered = tool ? profiles.filter((profile) => profile.tool === tool) : profiles;
|
|
3677
|
+
return filtered.slice().sort((a, b) => a.tool.localeCompare(b.tool) || a.name.localeCompare(b.name));
|
|
3678
|
+
}
|
|
3679
|
+
async findProfile(name, tool) {
|
|
3680
|
+
const matches = parseStoreFile().profiles.filter((profile) => profile.name === name && (!tool || profile.tool === tool));
|
|
3681
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
3682
|
+
}
|
|
3683
|
+
async currentProfile(tool) {
|
|
3684
|
+
const store = parseStoreFile();
|
|
3685
|
+
const name = store.current[tool];
|
|
3686
|
+
if (!name)
|
|
3687
|
+
return;
|
|
3688
|
+
return store.profiles.find((profile) => profile.name === name && profile.tool === tool);
|
|
3689
|
+
}
|
|
3690
|
+
async listTools() {
|
|
3691
|
+
const custom = parseStoreFile().tools;
|
|
3692
|
+
const byId = new Map;
|
|
3693
|
+
for (const tool of BUILTIN_TOOLS)
|
|
3694
|
+
byId.set(tool.id, tool);
|
|
3695
|
+
for (const tool of custom)
|
|
3696
|
+
byId.set(tool.id, tool);
|
|
3697
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
var RETIRED_STORAGE_MODE_KEYS = [
|
|
3701
|
+
"HASNA_ACCOUNTS_STORAGE_MODE",
|
|
3702
|
+
"ACCOUNTS_STORAGE_MODE",
|
|
3703
|
+
"HASNA_ACCOUNTS_MODE",
|
|
3704
|
+
"ACCOUNTS_MODE"
|
|
3705
|
+
];
|
|
3706
|
+
function assertNoRetiredStorageMode(env) {
|
|
3707
|
+
const legacyKey = RETIRED_STORAGE_MODE_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
|
|
3708
|
+
if (!legacyKey)
|
|
3709
|
+
return;
|
|
3710
|
+
throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The accounts registry uses the local JSON store, or the HTTP API selected by ` + `HASNA_ACCOUNTS_API_URL + HASNA_ACCOUNTS_API_KEY.`);
|
|
3711
|
+
}
|
|
3712
|
+
var RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
3713
|
+
|
|
3714
|
+
class AccountsApiError extends Error {
|
|
3715
|
+
status;
|
|
3716
|
+
body;
|
|
3717
|
+
constructor(method, path, status, body) {
|
|
3718
|
+
super(`accounts cloud request failed: ${method} ${path} -> ${status}`);
|
|
3719
|
+
this.name = "AccountsApiError";
|
|
3720
|
+
this.status = status;
|
|
3721
|
+
this.body = body;
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
function isNotFound(err) {
|
|
3725
|
+
return Boolean(err && typeof err === "object" && err.status === 404);
|
|
3726
|
+
}
|
|
3727
|
+
function sleep(ms) {
|
|
3728
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3729
|
+
}
|
|
3730
|
+
async function cloudRequest(baseUrl, apiKey, path, query) {
|
|
3731
|
+
const rel = path.startsWith("/") ? path : `/${path}`;
|
|
3732
|
+
const url = query && Object.keys(query).length > 0 ? `${baseUrl}${rel}?${new URLSearchParams(query).toString()}` : `${baseUrl}${rel}`;
|
|
3733
|
+
let lastError;
|
|
3734
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
3735
|
+
const controller = new AbortController;
|
|
3736
|
+
const timer = setTimeout(() => controller.abort(), 30000);
|
|
3737
|
+
let res;
|
|
3738
|
+
try {
|
|
3739
|
+
res = await fetch(url, {
|
|
3740
|
+
method: "GET",
|
|
3741
|
+
headers: {
|
|
3742
|
+
"x-api-key": apiKey,
|
|
3743
|
+
Authorization: `Bearer ${apiKey}`,
|
|
3744
|
+
Accept: "application/json"
|
|
3745
|
+
},
|
|
3746
|
+
signal: controller.signal
|
|
3747
|
+
});
|
|
3748
|
+
} catch (error) {
|
|
3749
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
3750
|
+
clearTimeout(timer);
|
|
3751
|
+
if (attempt < 2) {
|
|
3752
|
+
const backoff = Math.min(2000, 200 * 2 ** attempt);
|
|
3753
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
3754
|
+
await sleep(backoff + jitter);
|
|
3755
|
+
}
|
|
3756
|
+
continue;
|
|
3757
|
+
} finally {
|
|
3758
|
+
clearTimeout(timer);
|
|
3759
|
+
}
|
|
3760
|
+
const text = await res.text();
|
|
3761
|
+
let parsed;
|
|
3762
|
+
if (text.length > 0) {
|
|
3763
|
+
try {
|
|
3764
|
+
parsed = JSON.parse(text);
|
|
3765
|
+
} catch {
|
|
3766
|
+
parsed = text;
|
|
3767
|
+
}
|
|
3768
|
+
}
|
|
3769
|
+
if (!res.ok) {
|
|
3770
|
+
lastError = new AccountsApiError("GET", rel, res.status, parsed);
|
|
3771
|
+
if (attempt < 2 && RETRY_STATUSES.includes(res.status)) {
|
|
3772
|
+
const backoff = Math.min(2000, 200 * 2 ** attempt);
|
|
3773
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
3774
|
+
await sleep(backoff + jitter);
|
|
3775
|
+
continue;
|
|
3776
|
+
}
|
|
3777
|
+
break;
|
|
3778
|
+
}
|
|
3779
|
+
return parsed;
|
|
3780
|
+
}
|
|
3781
|
+
throw lastError;
|
|
3782
|
+
}
|
|
3783
|
+
function toProfile(account) {
|
|
3784
|
+
const profile = {
|
|
3785
|
+
name: String(account.name),
|
|
3786
|
+
tool: String(account.tool),
|
|
3787
|
+
dir: typeof account.dir === "string" ? account.dir : "",
|
|
3788
|
+
createdAt: String(account.createdAt)
|
|
3789
|
+
};
|
|
3790
|
+
if (typeof account.email === "string")
|
|
3791
|
+
profile.email = account.email;
|
|
3792
|
+
if (typeof account.displayName === "string")
|
|
3793
|
+
profile.displayName = account.displayName;
|
|
3794
|
+
if (typeof account.description === "string")
|
|
3795
|
+
profile.description = account.description;
|
|
3796
|
+
if (typeof account.lastUsedAt === "string")
|
|
3797
|
+
profile.lastUsedAt = account.lastUsedAt;
|
|
3798
|
+
return profile;
|
|
3799
|
+
}
|
|
3800
|
+
|
|
3801
|
+
class ApiStore {
|
|
3802
|
+
baseUrl;
|
|
3803
|
+
apiKey;
|
|
3804
|
+
transport = "api";
|
|
3805
|
+
constructor(baseUrl, apiKey) {
|
|
3806
|
+
this.baseUrl = baseUrl;
|
|
3807
|
+
this.apiKey = apiKey;
|
|
3808
|
+
}
|
|
3809
|
+
async listProfiles(tool) {
|
|
3810
|
+
const raw = await cloudRequest(this.baseUrl, this.apiKey, "/accounts", tool ? { tool } : undefined);
|
|
3811
|
+
const accounts = Array.isArray(raw?.accounts) ? raw.accounts : [];
|
|
3812
|
+
return accounts.map((account) => toProfile(account)).sort((a, b) => a.tool.localeCompare(b.tool) || a.name.localeCompare(b.name));
|
|
3813
|
+
}
|
|
3814
|
+
async findProfile(name, tool) {
|
|
3815
|
+
if (tool) {
|
|
3816
|
+
try {
|
|
3817
|
+
const account = await cloudRequest(this.baseUrl, this.apiKey, `/accounts/${encodeURIComponent(tool)}/${encodeURIComponent(name)}`);
|
|
3818
|
+
return account ? toProfile(account) : undefined;
|
|
3819
|
+
} catch (err) {
|
|
3820
|
+
if (isNotFound(err))
|
|
3821
|
+
return;
|
|
3822
|
+
throw err;
|
|
3823
|
+
}
|
|
3824
|
+
}
|
|
3825
|
+
const matches = (await this.listProfiles()).filter((profile) => profile.name === name);
|
|
3826
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
3827
|
+
}
|
|
3828
|
+
async currentProfile(tool) {
|
|
3829
|
+
let current;
|
|
3830
|
+
try {
|
|
3831
|
+
const raw = await cloudRequest(this.baseUrl, this.apiKey, `/current/${encodeURIComponent(tool)}`);
|
|
3832
|
+
current = raw && typeof raw === "object" ? raw : null;
|
|
3833
|
+
} catch (err) {
|
|
3834
|
+
if (isNotFound(err))
|
|
3835
|
+
return;
|
|
3836
|
+
throw err;
|
|
3837
|
+
}
|
|
3838
|
+
if (!current || typeof current.name !== "string")
|
|
3839
|
+
return;
|
|
3840
|
+
return this.findProfile(current.name, tool);
|
|
3841
|
+
}
|
|
3842
|
+
async listTools() {
|
|
3843
|
+
const raw = await cloudRequest(this.baseUrl, this.apiKey, "/tools");
|
|
3844
|
+
const tools = Array.isArray(raw?.tools) ? raw.tools : [];
|
|
3845
|
+
const custom = [];
|
|
3846
|
+
for (const item of tools) {
|
|
3847
|
+
if (item === null || typeof item !== "object")
|
|
3848
|
+
continue;
|
|
3849
|
+
const record = item;
|
|
3850
|
+
if (record.builtin !== false)
|
|
3851
|
+
continue;
|
|
3852
|
+
const parsed = toolDefSchema.safeParse(item);
|
|
3853
|
+
if (!parsed.success) {
|
|
3854
|
+
throw new Error(`invalid custom tool "${String(record.id)}" returned by accounts-serve: ` + parsed.error.issues.map((issue) => issue.message).join("; "));
|
|
3855
|
+
}
|
|
3856
|
+
custom.push(parsed.data);
|
|
3857
|
+
}
|
|
3858
|
+
const byId = new Map;
|
|
3859
|
+
for (const tool of BUILTIN_TOOLS)
|
|
3860
|
+
byId.set(tool.id, tool);
|
|
3861
|
+
for (const tool of custom)
|
|
3862
|
+
byId.set(tool.id, tool);
|
|
3863
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
function resolveStore(env = process.env, options = {}) {
|
|
3867
|
+
assertNoRetiredStorageMode(env);
|
|
3868
|
+
const clientEnv = env;
|
|
3869
|
+
try {
|
|
3870
|
+
const credential = resolveCredential(ACCOUNTS_APP, clientEnv, options.credentials);
|
|
3871
|
+
if (credential === null)
|
|
3872
|
+
return new LocalStore;
|
|
3873
|
+
const resolution = resolveClientTransport(ACCOUNTS_APP, clientEnv, {
|
|
3874
|
+
credentials: credential ? { ...options.credentials, apiKey: credential.apiKey } : options.credentials
|
|
3875
|
+
});
|
|
3876
|
+
return new ApiStore(resolution.baseUrl, credential.apiKey);
|
|
3877
|
+
} catch (err) {
|
|
3878
|
+
console.warn(`accounts: API configuration unusable (${err instanceof Error ? err.message : String(err)}); using local store`);
|
|
3879
|
+
return new LocalStore;
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3480
3883
|
// src/lib/accounts.ts
|
|
3481
|
-
import { appliedProfileName, resolveStore } from "@hasna/accounts";
|
|
3482
3884
|
var AGENT_ACCOUNT_TOOLS = {
|
|
3483
3885
|
claude: ["claude"],
|
|
3484
3886
|
takumi: ["takumi", "claude"],
|
|
@@ -3550,11 +3952,11 @@ async function profileForEnvDir(store, tool, env) {
|
|
|
3550
3952
|
const normalized = normalizeDir(configuredDir);
|
|
3551
3953
|
return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
3552
3954
|
}
|
|
3553
|
-
async function resolveAccountForAgent(agent, env = process.env) {
|
|
3955
|
+
async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
3554
3956
|
const override = envOverride(agent, env);
|
|
3555
3957
|
if (override)
|
|
3556
3958
|
return override;
|
|
3557
|
-
const store = resolveStore(env);
|
|
3959
|
+
const store = resolveStore(env, options);
|
|
3558
3960
|
const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
|
|
3559
3961
|
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
3560
3962
|
const tool = tools.get(toolId);
|
|
@@ -3592,8 +3994,8 @@ function withAccount(record, account) {
|
|
|
3592
3994
|
function autoDetectProject(cwd, projects) {
|
|
3593
3995
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
3594
3996
|
}
|
|
3595
|
-
var CLAUDE_PROJECTS_DIR =
|
|
3596
|
-
var TAKUMI_PROJECTS_DIR =
|
|
3997
|
+
var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join3(homedir3(), ".claude", "projects");
|
|
3998
|
+
var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
|
|
3597
3999
|
function dirNameToPath(dirName) {
|
|
3598
4000
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
3599
4001
|
}
|
|
@@ -3603,9 +4005,9 @@ function collectJsonlFiles(projectDir) {
|
|
|
3603
4005
|
try {
|
|
3604
4006
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
3605
4007
|
if (entry.isDirectory())
|
|
3606
|
-
walk(
|
|
4008
|
+
walk(join3(dir, entry.name));
|
|
3607
4009
|
else if (entry.name.endsWith(".jsonl"))
|
|
3608
|
-
files.push(
|
|
4010
|
+
files.push(join3(dir, entry.name));
|
|
3609
4011
|
}
|
|
3610
4012
|
} catch {}
|
|
3611
4013
|
}
|
|
@@ -3619,7 +4021,7 @@ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_D
|
|
|
3619
4021
|
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
3620
4022
|
}
|
|
3621
4023
|
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
3622
|
-
if (!
|
|
4024
|
+
if (!existsSync3(projectsDir)) {
|
|
3623
4025
|
if (verbose)
|
|
3624
4026
|
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
3625
4027
|
return { files: 0, requests: 0, sessions: 0 };
|
|
@@ -3630,9 +4032,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
3630
4032
|
const touchedSessions = new Set;
|
|
3631
4033
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
3632
4034
|
const account = await resolveAccountForAgent(agentName);
|
|
4035
|
+
const ingestState = loadIngestState(db, agentName);
|
|
3633
4036
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
3634
4037
|
for (const projectDirEntry of projectDirs) {
|
|
3635
|
-
const projectDirPath =
|
|
4038
|
+
const projectDirPath = join3(projectsDir, projectDirEntry.name);
|
|
3636
4039
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
3637
4040
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
3638
4041
|
for (const filePath of jsonlFiles) {
|
|
@@ -3643,12 +4046,12 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
3643
4046
|
} catch {
|
|
3644
4047
|
continue;
|
|
3645
4048
|
}
|
|
3646
|
-
const processed =
|
|
4049
|
+
const processed = ingestState.get(stateKey) ?? null;
|
|
3647
4050
|
if (processed === fileMtime)
|
|
3648
4051
|
continue;
|
|
3649
4052
|
let lines;
|
|
3650
4053
|
try {
|
|
3651
|
-
lines =
|
|
4054
|
+
lines = readFileSync2(filePath, "utf-8").split(`
|
|
3652
4055
|
`).filter((l) => l.trim());
|
|
3653
4056
|
} catch {
|
|
3654
4057
|
continue;
|
|
@@ -3768,14 +4171,14 @@ function supportsClaudeDataResidencyPricing(model) {
|
|
|
3768
4171
|
// src/ingest/codex.ts
|
|
3769
4172
|
init_database();
|
|
3770
4173
|
init_pricing();
|
|
3771
|
-
import { existsSync as
|
|
3772
|
-
import { homedir as
|
|
3773
|
-
import { join as
|
|
4174
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
|
|
4175
|
+
import { homedir as homedir4 } from "os";
|
|
4176
|
+
import { join as join4, basename as basename2 } from "path";
|
|
3774
4177
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
3775
|
-
var DEFAULT_CODEX_DB_PATH =
|
|
3776
|
-
var DEFAULT_CODEX_CONFIG_PATH =
|
|
3777
|
-
var DEFAULT_CODEWITH_DB_PATH =
|
|
3778
|
-
var DEFAULT_CODEWITH_CONFIG_PATH =
|
|
4178
|
+
var DEFAULT_CODEX_DB_PATH = join4(homedir4(), ".codex", "state_5.sqlite");
|
|
4179
|
+
var DEFAULT_CODEX_CONFIG_PATH = join4(homedir4(), ".codex", "config.toml");
|
|
4180
|
+
var DEFAULT_CODEWITH_DB_PATH = join4(homedir4(), ".codewith", "state_5.sqlite");
|
|
4181
|
+
var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir4(), ".codewith", "config.toml");
|
|
3779
4182
|
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
3780
4183
|
function codexDbPath() {
|
|
3781
4184
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
@@ -3817,10 +4220,10 @@ function codexSources() {
|
|
|
3817
4220
|
});
|
|
3818
4221
|
}
|
|
3819
4222
|
function readCodexModel(configPath = codexConfigPath()) {
|
|
3820
|
-
if (!
|
|
4223
|
+
if (!existsSync4(configPath))
|
|
3821
4224
|
return "gpt-5-codex";
|
|
3822
4225
|
try {
|
|
3823
|
-
const content =
|
|
4226
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
3824
4227
|
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
3825
4228
|
return match?.[1] ?? "gpt-5-codex";
|
|
3826
4229
|
} catch {
|
|
@@ -3858,12 +4261,12 @@ function openCodexDb(dbPath, verbose) {
|
|
|
3858
4261
|
return null;
|
|
3859
4262
|
}
|
|
3860
4263
|
function readTokenEvents(rolloutPath) {
|
|
3861
|
-
if (!rolloutPath || !
|
|
4264
|
+
if (!rolloutPath || !existsSync4(rolloutPath))
|
|
3862
4265
|
return [];
|
|
3863
4266
|
const fallbackUsages = new Map;
|
|
3864
4267
|
let fallbackTimestamp;
|
|
3865
4268
|
let aggregate = null;
|
|
3866
|
-
for (const line of
|
|
4269
|
+
for (const line of readFileSync3(rolloutPath, "utf-8").split(`
|
|
3867
4270
|
`)) {
|
|
3868
4271
|
if (!line.trim())
|
|
3869
4272
|
continue;
|
|
@@ -3939,7 +4342,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
3939
4342
|
let requests = 0;
|
|
3940
4343
|
const account = await resolveAccountForAgent("codex");
|
|
3941
4344
|
for (const source of codexSources()) {
|
|
3942
|
-
if (!
|
|
4345
|
+
if (!existsSync4(source.dbPath)) {
|
|
3943
4346
|
if (verbose)
|
|
3944
4347
|
console.log(`${source.label} DB not found:`, source.dbPath);
|
|
3945
4348
|
continue;
|
|
@@ -4020,11 +4423,11 @@ async function ingestCodex(db, verbose = false) {
|
|
|
4020
4423
|
// src/ingest/gemini.ts
|
|
4021
4424
|
init_database();
|
|
4022
4425
|
init_pricing();
|
|
4023
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
4024
|
-
import { homedir as
|
|
4025
|
-
import { join as
|
|
4026
|
-
var DEFAULT_GEMINI_TMP_DIR =
|
|
4027
|
-
var DEFAULT_GEMINI_HISTORY_DIR =
|
|
4426
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync5, statSync as statSync3 } from "fs";
|
|
4427
|
+
import { homedir as homedir5 } from "os";
|
|
4428
|
+
import { join as join5, basename as basename3 } from "path";
|
|
4429
|
+
var DEFAULT_GEMINI_TMP_DIR = join5(homedir5(), ".gemini", "tmp");
|
|
4430
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir5(), ".gemini", "history");
|
|
4028
4431
|
function geminiTmpDir() {
|
|
4029
4432
|
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
4030
4433
|
}
|
|
@@ -4041,12 +4444,12 @@ function numberField(...values) {
|
|
|
4041
4444
|
function listProjectDirs(...roots) {
|
|
4042
4445
|
const dirs = new Set;
|
|
4043
4446
|
for (const root of roots) {
|
|
4044
|
-
if (!
|
|
4447
|
+
if (!existsSync5(root))
|
|
4045
4448
|
continue;
|
|
4046
4449
|
try {
|
|
4047
4450
|
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
4048
4451
|
if (entry.isDirectory())
|
|
4049
|
-
dirs.add(
|
|
4452
|
+
dirs.add(join5(root, entry.name));
|
|
4050
4453
|
}
|
|
4051
4454
|
} catch {}
|
|
4052
4455
|
}
|
|
@@ -4057,17 +4460,17 @@ function projectRoot(projectDir, chatData) {
|
|
|
4057
4460
|
return chatData.projectPath;
|
|
4058
4461
|
if (chatData.project_path)
|
|
4059
4462
|
return chatData.project_path;
|
|
4060
|
-
const rootFile =
|
|
4463
|
+
const rootFile = join5(projectDir, ".project_root");
|
|
4061
4464
|
try {
|
|
4062
|
-
if (
|
|
4063
|
-
return
|
|
4465
|
+
if (existsSync5(rootFile))
|
|
4466
|
+
return readFileSync4(rootFile, "utf-8").trim();
|
|
4064
4467
|
} catch {}
|
|
4065
4468
|
return "";
|
|
4066
4469
|
}
|
|
4067
4470
|
async function ingestGemini(db, verbose) {
|
|
4068
4471
|
const tmpDir = geminiTmpDir();
|
|
4069
4472
|
const historyDir = geminiHistoryDir();
|
|
4070
|
-
if (!
|
|
4473
|
+
if (!existsSync5(tmpDir) && !existsSync5(historyDir)) {
|
|
4071
4474
|
if (verbose)
|
|
4072
4475
|
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
4073
4476
|
return { sessions: 0, requests: 0 };
|
|
@@ -4079,17 +4482,17 @@ async function ingestGemini(db, verbose) {
|
|
|
4079
4482
|
const account = await resolveAccountForAgent("gemini");
|
|
4080
4483
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
4081
4484
|
for (const projectDir of projectDirs) {
|
|
4082
|
-
const chatsDir =
|
|
4083
|
-
if (!
|
|
4485
|
+
const chatsDir = join5(projectDir, "chats");
|
|
4486
|
+
if (!existsSync5(chatsDir))
|
|
4084
4487
|
continue;
|
|
4085
4488
|
let chatFiles = [];
|
|
4086
4489
|
try {
|
|
4087
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) =>
|
|
4490
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join5(chatsDir, f));
|
|
4088
4491
|
} catch {
|
|
4089
4492
|
continue;
|
|
4090
4493
|
}
|
|
4091
4494
|
for (const filePath of chatFiles) {
|
|
4092
|
-
const stateKey = filePath.replace(
|
|
4495
|
+
const stateKey = filePath.replace(homedir5(), "~");
|
|
4093
4496
|
let fileMtime = "0";
|
|
4094
4497
|
try {
|
|
4095
4498
|
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
@@ -4101,7 +4504,7 @@ async function ingestGemini(db, verbose) {
|
|
|
4101
4504
|
continue;
|
|
4102
4505
|
let chatData;
|
|
4103
4506
|
try {
|
|
4104
|
-
chatData = JSON.parse(
|
|
4507
|
+
chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
4105
4508
|
} catch {
|
|
4106
4509
|
continue;
|
|
4107
4510
|
}
|
|
@@ -4179,15 +4582,15 @@ async function ingestGemini(db, verbose) {
|
|
|
4179
4582
|
// src/ingest/opencode.ts
|
|
4180
4583
|
init_database();
|
|
4181
4584
|
init_pricing();
|
|
4182
|
-
import { existsSync as
|
|
4183
|
-
import { homedir as
|
|
4184
|
-
import { join as
|
|
4185
|
-
var OPENCODE_STORAGE =
|
|
4585
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
4586
|
+
import { homedir as homedir6 } from "os";
|
|
4587
|
+
import { join as join6 } from "path";
|
|
4588
|
+
var OPENCODE_STORAGE = join6(homedir6(), ".local", "share", "opencode", "storage");
|
|
4186
4589
|
function walkJsonFiles(dir, acc = []) {
|
|
4187
|
-
if (!
|
|
4590
|
+
if (!existsSync6(dir))
|
|
4188
4591
|
return acc;
|
|
4189
4592
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
4190
|
-
const p =
|
|
4593
|
+
const p = join6(dir, entry.name);
|
|
4191
4594
|
if (entry.isDirectory())
|
|
4192
4595
|
walkJsonFiles(p, acc);
|
|
4193
4596
|
else if (entry.name.endsWith(".json"))
|
|
@@ -4207,7 +4610,7 @@ function parseSessionIdFromPath(filePath) {
|
|
|
4207
4610
|
return null;
|
|
4208
4611
|
}
|
|
4209
4612
|
async function ingestOpenCode(db, verbose = false) {
|
|
4210
|
-
const messageDir =
|
|
4613
|
+
const messageDir = join6(OPENCODE_STORAGE, "message");
|
|
4211
4614
|
const files = walkJsonFiles(messageDir);
|
|
4212
4615
|
let requests = 0;
|
|
4213
4616
|
const touched = new Set;
|
|
@@ -4222,7 +4625,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
4222
4625
|
continue;
|
|
4223
4626
|
let parsed;
|
|
4224
4627
|
try {
|
|
4225
|
-
parsed = JSON.parse(
|
|
4628
|
+
parsed = JSON.parse(readFileSync5(file, "utf-8"));
|
|
4226
4629
|
} catch {
|
|
4227
4630
|
continue;
|
|
4228
4631
|
}
|
|
@@ -4409,15 +4812,15 @@ async function ingestCursor(db, verbose = false) {
|
|
|
4409
4812
|
|
|
4410
4813
|
// src/ingest/pi.ts
|
|
4411
4814
|
init_database();
|
|
4412
|
-
import { existsSync as
|
|
4413
|
-
import { homedir as
|
|
4414
|
-
import { join as
|
|
4415
|
-
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ??
|
|
4815
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
4816
|
+
import { homedir as homedir7 } from "os";
|
|
4817
|
+
import { join as join7 } from "path";
|
|
4818
|
+
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join7(homedir7(), ".pi", "agent", "sessions");
|
|
4416
4819
|
function walkSessions(dir, acc = []) {
|
|
4417
|
-
if (!
|
|
4820
|
+
if (!existsSync7(dir))
|
|
4418
4821
|
return acc;
|
|
4419
4822
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
4420
|
-
const p =
|
|
4823
|
+
const p = join7(dir, entry.name);
|
|
4421
4824
|
if (entry.isDirectory())
|
|
4422
4825
|
walkSessions(p, acc);
|
|
4423
4826
|
else if (entry.name.endsWith(".json"))
|
|
@@ -4439,7 +4842,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
4439
4842
|
continue;
|
|
4440
4843
|
let data;
|
|
4441
4844
|
try {
|
|
4442
|
-
data = JSON.parse(
|
|
4845
|
+
data = JSON.parse(readFileSync6(file, "utf-8"));
|
|
4443
4846
|
} catch {
|
|
4444
4847
|
continue;
|
|
4445
4848
|
}
|
|
@@ -4503,10 +4906,10 @@ async function ingestPi(db, verbose = false) {
|
|
|
4503
4906
|
|
|
4504
4907
|
// src/ingest/hermes.ts
|
|
4505
4908
|
init_database();
|
|
4506
|
-
import { existsSync as
|
|
4507
|
-
import { homedir as
|
|
4508
|
-
import { join as
|
|
4509
|
-
var HERMES_DB =
|
|
4909
|
+
import { existsSync as existsSync8, statSync as statSync6 } from "fs";
|
|
4910
|
+
import { homedir as homedir8 } from "os";
|
|
4911
|
+
import { join as join8 } from "path";
|
|
4912
|
+
var HERMES_DB = join8(homedir8(), ".hermes", "state.db");
|
|
4510
4913
|
function mapCostBasis(billingMode) {
|
|
4511
4914
|
if (billingMode === "subscription")
|
|
4512
4915
|
return "subscription_included";
|
|
@@ -4515,7 +4918,7 @@ function mapCostBasis(billingMode) {
|
|
|
4515
4918
|
return defaultCostBasisForAgent("hermes");
|
|
4516
4919
|
}
|
|
4517
4920
|
async function ingestHermes(db, verbose = false) {
|
|
4518
|
-
if (!
|
|
4921
|
+
if (!existsSync8(HERMES_DB)) {
|
|
4519
4922
|
return { sessions: 0, requests: 0 };
|
|
4520
4923
|
}
|
|
4521
4924
|
const { Database: Sqlite } = await import("bun:sqlite");
|
|
@@ -4595,14 +4998,81 @@ function statSyncSafe(path) {
|
|
|
4595
4998
|
// src/ingest/loops.ts
|
|
4596
4999
|
init_database();
|
|
4597
5000
|
init_pricing();
|
|
4598
|
-
import { existsSync as
|
|
4599
|
-
import { homedir as
|
|
4600
|
-
import { join as
|
|
5001
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5002
|
+
import { homedir as homedir9 } from "os";
|
|
5003
|
+
import { join as join9 } from "path";
|
|
4601
5004
|
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
4602
|
-
|
|
5005
|
+
import { homedir as pathsResolverHomedir2 } from "os";
|
|
5006
|
+
import { join as pathsResolverJoin2 } from "path";
|
|
5007
|
+
var PATHS_RESOLVER_KIND_ENV2 = {
|
|
5008
|
+
config: "HASNA_CONFIG_HOME",
|
|
5009
|
+
data: "HASNA_DATA_HOME",
|
|
5010
|
+
state: "HASNA_STATE_HOME",
|
|
5011
|
+
cache: "HASNA_CACHE_HOME"
|
|
5012
|
+
};
|
|
5013
|
+
var PATHS_RESOLVER_APP_SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5014
|
+
function pathsResolverAssertApp2(app) {
|
|
5015
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
5016
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
5017
|
+
}
|
|
5018
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE2.test(app)) {
|
|
5019
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
5020
|
+
}
|
|
5021
|
+
}
|
|
5022
|
+
function pathsResolverAssertKind2(kind) {
|
|
5023
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV2).includes(kind)) {
|
|
5024
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV2).join(", ")}`);
|
|
5025
|
+
}
|
|
5026
|
+
}
|
|
5027
|
+
function pathsResolverBaseDir2(kind, options) {
|
|
5028
|
+
pathsResolverAssertKind2(kind);
|
|
5029
|
+
const env = options.env ?? process.env;
|
|
5030
|
+
const override = env[PATHS_RESOLVER_KIND_ENV2[kind]];
|
|
5031
|
+
if (typeof override === "string" && override.length > 0)
|
|
5032
|
+
return override;
|
|
5033
|
+
const home = options.home ?? pathsResolverHomedir2();
|
|
5034
|
+
const platform2 = options.platform ?? process.platform;
|
|
5035
|
+
if (platform2 === "darwin") {
|
|
5036
|
+
switch (kind) {
|
|
5037
|
+
case "config":
|
|
5038
|
+
case "data":
|
|
5039
|
+
return pathsResolverJoin2(home, "Library", "Application Support", "Hasna");
|
|
5040
|
+
case "cache":
|
|
5041
|
+
return pathsResolverJoin2(home, "Library", "Caches", "Hasna");
|
|
5042
|
+
case "state":
|
|
5043
|
+
return pathsResolverJoin2(home, "Library", "Logs", "Hasna");
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
switch (kind) {
|
|
5047
|
+
case "config":
|
|
5048
|
+
return pathsResolverJoin2(home, ".config", "hasna");
|
|
5049
|
+
case "data":
|
|
5050
|
+
return pathsResolverJoin2(home, ".local", "share", "hasna");
|
|
5051
|
+
case "state":
|
|
5052
|
+
return pathsResolverJoin2(home, ".local", "state", "hasna");
|
|
5053
|
+
case "cache":
|
|
5054
|
+
return pathsResolverJoin2(home, ".cache", "hasna");
|
|
5055
|
+
}
|
|
5056
|
+
}
|
|
5057
|
+
function pathsResolverResolve2(kind, options) {
|
|
5058
|
+
pathsResolverAssertApp2(options.app);
|
|
5059
|
+
const appSegment = options.internal === true ? pathsResolverJoin2("internal", options.app) : options.app;
|
|
5060
|
+
return pathsResolverJoin2(pathsResolverBaseDir2(kind, options), appSegment);
|
|
5061
|
+
}
|
|
5062
|
+
function dataDir2(options) {
|
|
5063
|
+
return pathsResolverResolve2("data", options);
|
|
5064
|
+
}
|
|
4603
5065
|
var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
|
|
5066
|
+
function defaultLoopsDbPath() {
|
|
5067
|
+
const resolved = dataDir2({
|
|
5068
|
+
app: "loops",
|
|
5069
|
+
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
5070
|
+
});
|
|
5071
|
+
const legacy = join9(process.env["HOME"] || process.env["USERPROFILE"] || homedir9(), ".hasna", "loops", "loops.db");
|
|
5072
|
+
return existsSync9(join9(resolved, "loops.db")) ? join9(resolved, "loops.db") : legacy;
|
|
5073
|
+
}
|
|
4604
5074
|
function loopsDbPath() {
|
|
4605
|
-
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ??
|
|
5075
|
+
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
|
|
4606
5076
|
}
|
|
4607
5077
|
function loopsModel() {
|
|
4608
5078
|
return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
|
|
@@ -4691,7 +5161,7 @@ function readLoopGoalRuns(loopsDb) {
|
|
|
4691
5161
|
}
|
|
4692
5162
|
async function ingestLoops(db, verbose = false) {
|
|
4693
5163
|
const dbPath = loopsDbPath();
|
|
4694
|
-
if (!
|
|
5164
|
+
if (!existsSync9(dbPath)) {
|
|
4695
5165
|
if (verbose)
|
|
4696
5166
|
console.log("OpenLoops DB not found:", dbPath);
|
|
4697
5167
|
return { sessions: 0, requests: 0 };
|
|
@@ -4781,30 +5251,30 @@ async function ingestLoops(db, verbose = false) {
|
|
|
4781
5251
|
|
|
4782
5252
|
// src/ingest/claude-quota.ts
|
|
4783
5253
|
init_database();
|
|
4784
|
-
import { existsSync as
|
|
5254
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
4785
5255
|
|
|
4786
5256
|
// src/lib/paths.ts
|
|
4787
|
-
import { homedir as
|
|
4788
|
-
import { join as
|
|
4789
|
-
function
|
|
4790
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ??
|
|
5257
|
+
import { homedir as homedir10 } from "os";
|
|
5258
|
+
import { join as join10 } from "path";
|
|
5259
|
+
function getHomeDir2() {
|
|
5260
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir10();
|
|
4791
5261
|
}
|
|
4792
5262
|
function agentPaths() {
|
|
4793
|
-
const home =
|
|
5263
|
+
const home = getHomeDir2();
|
|
4794
5264
|
return {
|
|
4795
|
-
claudeProjects:
|
|
4796
|
-
claudeCredentials:
|
|
4797
|
-
takumiProjects:
|
|
4798
|
-
codexDir:
|
|
4799
|
-
codexDb:
|
|
4800
|
-
codexAuth:
|
|
4801
|
-
codexConfig:
|
|
4802
|
-
geminiTmp:
|
|
4803
|
-
geminiHistory:
|
|
4804
|
-
opencodeMessages:
|
|
4805
|
-
piSessions:
|
|
4806
|
-
hermesDir:
|
|
4807
|
-
hermesDb:
|
|
5265
|
+
claudeProjects: join10(home, ".claude", "projects"),
|
|
5266
|
+
claudeCredentials: join10(home, ".claude", ".credentials.json"),
|
|
5267
|
+
takumiProjects: join10(home, ".takumi", "projects"),
|
|
5268
|
+
codexDir: join10(home, ".codex"),
|
|
5269
|
+
codexDb: join10(home, ".codex", "state_5.sqlite"),
|
|
5270
|
+
codexAuth: join10(home, ".codex", "auth.json"),
|
|
5271
|
+
codexConfig: join10(home, ".codex", "config.toml"),
|
|
5272
|
+
geminiTmp: join10(home, ".gemini", "tmp"),
|
|
5273
|
+
geminiHistory: join10(home, ".gemini", "history"),
|
|
5274
|
+
opencodeMessages: join10(home, ".local", "share", "opencode", "storage", "message"),
|
|
5275
|
+
piSessions: join10(home, ".pi", "agent", "sessions"),
|
|
5276
|
+
hermesDir: join10(home, ".hermes"),
|
|
5277
|
+
hermesDb: join10(home, ".hermes", "state.db")
|
|
4808
5278
|
};
|
|
4809
5279
|
}
|
|
4810
5280
|
|
|
@@ -4816,10 +5286,10 @@ function readClaudeToken() {
|
|
|
4816
5286
|
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
4817
5287
|
if (fromEnv)
|
|
4818
5288
|
return { token: fromEnv };
|
|
4819
|
-
if (!
|
|
5289
|
+
if (!existsSync10(CREDENTIALS_PATH))
|
|
4820
5290
|
return null;
|
|
4821
5291
|
try {
|
|
4822
|
-
const creds = JSON.parse(
|
|
5292
|
+
const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
|
|
4823
5293
|
const oauth = creds.claudeAiOauth;
|
|
4824
5294
|
if (!oauth?.accessToken)
|
|
4825
5295
|
return null;
|
|
@@ -4955,17 +5425,17 @@ async function ingestClaudeQuota(db, verbose = false) {
|
|
|
4955
5425
|
|
|
4956
5426
|
// src/ingest/codex-quota.ts
|
|
4957
5427
|
init_database();
|
|
4958
|
-
import { existsSync as
|
|
5428
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
4959
5429
|
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
4960
5430
|
function readCodexAuth() {
|
|
4961
5431
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
4962
5432
|
if (fromEnv)
|
|
4963
5433
|
return { token: fromEnv, authMode: "chatgpt" };
|
|
4964
5434
|
const authPath = agentPaths().codexAuth;
|
|
4965
|
-
if (!
|
|
5435
|
+
if (!existsSync11(authPath))
|
|
4966
5436
|
return null;
|
|
4967
5437
|
try {
|
|
4968
|
-
const auth = JSON.parse(
|
|
5438
|
+
const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
|
|
4969
5439
|
const token = auth.tokens?.access_token;
|
|
4970
5440
|
if (!token)
|
|
4971
5441
|
return null;
|
|
@@ -5950,7 +6420,7 @@ init_database();
|
|
|
5950
6420
|
|
|
5951
6421
|
// src/lib/serve-auth.ts
|
|
5952
6422
|
function getServeApiToken() {
|
|
5953
|
-
return process.env["
|
|
6423
|
+
return process.env["HASNA_ECONOMY_API_TOKEN"];
|
|
5954
6424
|
}
|
|
5955
6425
|
function getServeBindHost() {
|
|
5956
6426
|
const explicit = process.env["ECONOMY_BIND"] ?? process.env["ECONOMY_HOST"];
|
|
@@ -5974,13 +6444,11 @@ function isAuthorizedRequest(req, path) {
|
|
|
5974
6444
|
|
|
5975
6445
|
// src/server/serve.ts
|
|
5976
6446
|
import { randomUUID } from "crypto";
|
|
5977
|
-
import { existsSync as existsSync13 } from "fs";
|
|
5978
|
-
import { resolve, sep } from "path";
|
|
5979
6447
|
// package.json
|
|
5980
6448
|
var package_default = {
|
|
5981
6449
|
name: "@hasna/economy",
|
|
5982
|
-
version: "0.
|
|
5983
|
-
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API
|
|
6450
|
+
version: "0.4.0",
|
|
6451
|
+
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",
|
|
5984
6452
|
repository: {
|
|
5985
6453
|
type: "git",
|
|
5986
6454
|
url: "git+https://github.com/hasna/economy.git"
|
|
@@ -6011,7 +6479,7 @@ var package_default = {
|
|
|
6011
6479
|
files: [
|
|
6012
6480
|
"dist",
|
|
6013
6481
|
"docs",
|
|
6014
|
-
"
|
|
6482
|
+
"postinstall.js",
|
|
6015
6483
|
"LICENSE",
|
|
6016
6484
|
"README.md",
|
|
6017
6485
|
"CHANGELOG.md",
|
|
@@ -6020,12 +6488,11 @@ var package_default = {
|
|
|
6020
6488
|
"CODE_OF_CONDUCT.md"
|
|
6021
6489
|
],
|
|
6022
6490
|
scripts: {
|
|
6023
|
-
build: "
|
|
6491
|
+
build: "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external && bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --target bun --packages external && tsc --emitDeclarationOnly --outDir dist",
|
|
6024
6492
|
"build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
|
|
6025
6493
|
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
|
|
6026
6494
|
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
6027
6495
|
"build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
6028
|
-
"build:dashboard": "cd dashboard && bun run build",
|
|
6029
6496
|
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
6030
6497
|
prepack: "bun run build && bun run artifact-scan",
|
|
6031
6498
|
typecheck: "tsc --noEmit",
|
|
@@ -6034,7 +6501,7 @@ var package_default = {
|
|
|
6034
6501
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
6035
6502
|
"dev:serve": "bun run src/server/index.ts",
|
|
6036
6503
|
"dev:otel": "bun run src/otel/index.ts",
|
|
6037
|
-
postinstall: "
|
|
6504
|
+
postinstall: "node postinstall.js"
|
|
6038
6505
|
},
|
|
6039
6506
|
keywords: [
|
|
6040
6507
|
"economy",
|
|
@@ -6055,11 +6522,8 @@ var package_default = {
|
|
|
6055
6522
|
access: "public"
|
|
6056
6523
|
},
|
|
6057
6524
|
dependencies: {
|
|
6058
|
-
"@hasna/
|
|
6059
|
-
"@hasna/agent-registry": "^0.1.0",
|
|
6060
|
-
"@hasna/contracts": "0.13.4",
|
|
6525
|
+
"@hasna/contracts": "1.0.2",
|
|
6061
6526
|
"@hasna/events": "^0.1.16",
|
|
6062
|
-
"@hasna/mcp-harness": "^0.1.0",
|
|
6063
6527
|
"@hasna/projects": "1.0.0",
|
|
6064
6528
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
6065
6529
|
chalk: "^5.4.1",
|
|
@@ -6068,7 +6532,7 @@ var package_default = {
|
|
|
6068
6532
|
zod: "^3.24.2"
|
|
6069
6533
|
},
|
|
6070
6534
|
devDependencies: {
|
|
6071
|
-
"@types/bun": "
|
|
6535
|
+
"@types/bun": "1.3.14",
|
|
6072
6536
|
"@types/pg": "^8.20.0",
|
|
6073
6537
|
"bun-types": "latest",
|
|
6074
6538
|
typescript: "^5.7.2"
|
|
@@ -7746,7 +8210,6 @@ var APP = "economy";
|
|
|
7746
8210
|
var ECONOMY_WRITE_SCOPE = `${APP}:write`;
|
|
7747
8211
|
var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
7748
8212
|
var SYNC_SOURCES = ["all", ...AGENTS, "loops"];
|
|
7749
|
-
var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
7750
8213
|
function json(data, status = 200) {
|
|
7751
8214
|
return new Response(JSON.stringify(data), {
|
|
7752
8215
|
status,
|
|
@@ -7799,38 +8262,6 @@ function requiredScopesForRequest(method, path) {
|
|
|
7799
8262
|
function stringArray(value) {
|
|
7800
8263
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
7801
8264
|
}
|
|
7802
|
-
function dashboardPath(root, pathname) {
|
|
7803
|
-
let decoded;
|
|
7804
|
-
try {
|
|
7805
|
-
decoded = decodeURIComponent(pathname);
|
|
7806
|
-
} catch {
|
|
7807
|
-
return null;
|
|
7808
|
-
}
|
|
7809
|
-
const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
|
|
7810
|
-
const rootPath = resolve(root);
|
|
7811
|
-
const filePath = resolve(rootPath, relativePath);
|
|
7812
|
-
return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
|
|
7813
|
-
}
|
|
7814
|
-
var FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
|
|
7815
|
-
function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
7816
|
-
return async function fetch2(req) {
|
|
7817
|
-
const url = new URL(req.url);
|
|
7818
|
-
if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
|
|
7819
|
-
return apiHandler(req);
|
|
7820
|
-
}
|
|
7821
|
-
if (existsSync13(dashboardDir)) {
|
|
7822
|
-
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
7823
|
-
if (filePath && existsSync13(filePath)) {
|
|
7824
|
-
return new Response(Bun.file(filePath));
|
|
7825
|
-
}
|
|
7826
|
-
const indexPath = dashboardPath(dashboardDir, "/");
|
|
7827
|
-
if (indexPath && existsSync13(indexPath)) {
|
|
7828
|
-
return new Response(Bun.file(indexPath));
|
|
7829
|
-
}
|
|
7830
|
-
}
|
|
7831
|
-
return apiHandler(req);
|
|
7832
|
-
};
|
|
7833
|
-
}
|
|
7834
8265
|
function applyFields(obj, fields) {
|
|
7835
8266
|
if (!fields || fields.length === 0)
|
|
7836
8267
|
return obj;
|
|
@@ -8354,7 +8785,7 @@ function startServer(port = 3456, options = {}) {
|
|
|
8354
8785
|
const server = Bun.serve({
|
|
8355
8786
|
port,
|
|
8356
8787
|
hostname: hostname2,
|
|
8357
|
-
fetch:
|
|
8788
|
+
fetch: apiHandler
|
|
8358
8789
|
});
|
|
8359
8790
|
const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
|
|
8360
8791
|
log(`economy-serve listening on ${address}`);
|