@hasna/economy 0.3.28 → 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 +95 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +6 -13
- package/SECURITY.md +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/index.js +1135 -853
- package/dist/db/cloud.d.ts +16 -8
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +19 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +198 -19
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts +9 -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/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 +1340 -302
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +430 -16
- package/dist/server/index.js +548 -155
- 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 +5 -11
- package/postinstall.js +52 -1
- 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
|
@@ -596,7 +596,59 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
|
596
596
|
import { hostname, platform } from "os";
|
|
597
597
|
import { homedir } from "os";
|
|
598
598
|
import { join, resolve } from "path";
|
|
599
|
-
import {
|
|
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
|
+
}
|
|
600
652
|
function normalizeMachineId(value) {
|
|
601
653
|
const id = value?.trim().toLowerCase().split(".")[0];
|
|
602
654
|
return id && id.length > 0 ? id : null;
|
|
@@ -619,7 +671,7 @@ function macHostMachineId() {
|
|
|
619
671
|
return null;
|
|
620
672
|
}
|
|
621
673
|
function getMachineId() {
|
|
622
|
-
const envMachine = normalizeMachineId(process.env["
|
|
674
|
+
const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
|
|
623
675
|
if (envMachine)
|
|
624
676
|
return envMachine;
|
|
625
677
|
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
@@ -1926,6 +1978,10 @@ function getIngestState(db, source, key) {
|
|
|
1926
1978
|
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
|
|
1927
1979
|
return row?.value ?? null;
|
|
1928
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
|
+
}
|
|
1929
1985
|
function setIngestState(db, source, key, value) {
|
|
1930
1986
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
1931
1987
|
}
|
|
@@ -2364,7 +2420,7 @@ function bulkIngest(db, body) {
|
|
|
2364
2420
|
const total = Object.values(ingested).reduce((a, b) => a + b, 0);
|
|
2365
2421
|
return { ingested, total };
|
|
2366
2422
|
}
|
|
2367
|
-
var numOr = (v, d = 0) => {
|
|
2423
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE, numOr = (v, d = 0) => {
|
|
2368
2424
|
if (v == null)
|
|
2369
2425
|
return d;
|
|
2370
2426
|
const num = Number(v);
|
|
@@ -2372,6 +2428,13 @@ var numOr = (v, d = 0) => {
|
|
|
2372
2428
|
}, strOr = (v, d = "") => v == null ? d : String(v);
|
|
2373
2429
|
var init_database = __esm(() => {
|
|
2374
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]+)*$/;
|
|
2375
2438
|
});
|
|
2376
2439
|
|
|
2377
2440
|
// src/db/dialect.ts
|
|
@@ -2414,8 +2477,8 @@ function sqliteToPostgres(sql) {
|
|
|
2414
2477
|
// src/db/sync-pg.ts
|
|
2415
2478
|
import { Worker, MessageChannel, receiveMessageOnPort } from "worker_threads";
|
|
2416
2479
|
import { fileURLToPath } from "url";
|
|
2417
|
-
import { dirname, join as
|
|
2418
|
-
import { existsSync as
|
|
2480
|
+
import { dirname, join as join11 } from "path";
|
|
2481
|
+
import { existsSync as existsSync12 } from "fs";
|
|
2419
2482
|
function translateSqliteDates(sql) {
|
|
2420
2483
|
let s = sql;
|
|
2421
2484
|
const isoDate = (expr) => `to_char(${expr}, 'YYYY-MM-DD')`;
|
|
@@ -2440,12 +2503,12 @@ function translateSqliteDates(sql) {
|
|
|
2440
2503
|
function resolveWorkerPath() {
|
|
2441
2504
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
2442
2505
|
const candidates = [
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2506
|
+
join11(here, "pg-sync-worker.ts"),
|
|
2507
|
+
join11(here, "pg-sync-worker.js"),
|
|
2508
|
+
join11(here, "..", "db", "pg-sync-worker.js")
|
|
2446
2509
|
];
|
|
2447
2510
|
for (const candidate of candidates)
|
|
2448
|
-
if (
|
|
2511
|
+
if (existsSync12(candidate))
|
|
2449
2512
|
return candidate;
|
|
2450
2513
|
return candidates[0];
|
|
2451
2514
|
}
|
|
@@ -2902,7 +2965,7 @@ __export(exports_billing, {
|
|
|
2902
2965
|
syncGeminiBilling: () => syncGeminiBilling,
|
|
2903
2966
|
syncAnthropicBilling: () => syncAnthropicBilling
|
|
2904
2967
|
});
|
|
2905
|
-
import { readFileSync as
|
|
2968
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
2906
2969
|
function getAnthropicAdminKey() {
|
|
2907
2970
|
return process.env["HASNAXYZ_ANTHROPIC_LIVE_ADMIN_API_KEY"] ?? process.env["ANTHROPIC_ADMIN_API_KEY"] ?? null;
|
|
2908
2971
|
}
|
|
@@ -3129,7 +3192,7 @@ async function syncGeminiBilling(db, opts = {}) {
|
|
|
3129
3192
|
const start = opts.fromDate ? new Date(opts.fromDate) : new Date(end.getTime() - days * 24 * 3600000);
|
|
3130
3193
|
const fromDateStr = toISODate(start);
|
|
3131
3194
|
const toDateStr = toISODate(end);
|
|
3132
|
-
const rows = parseBillingRows(
|
|
3195
|
+
const rows = parseBillingRows(readFileSync9(exportPath, "utf-8"));
|
|
3133
3196
|
clearBillingRange(db, "gemini", fromDateStr, toDateStr);
|
|
3134
3197
|
const updatedAt = new Date().toISOString();
|
|
3135
3198
|
let totalUsd = 0;
|
|
@@ -3193,16 +3256,16 @@ var init_open_projects = __esm(() => {
|
|
|
3193
3256
|
});
|
|
3194
3257
|
|
|
3195
3258
|
// src/lib/config.ts
|
|
3196
|
-
import { existsSync as
|
|
3197
|
-
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";
|
|
3198
3261
|
function getConfigPath() {
|
|
3199
|
-
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ??
|
|
3262
|
+
return process.env["HASNA_ECONOMY_CONFIG_PATH"] ?? join12(getDataDir(), "config.json");
|
|
3200
3263
|
}
|
|
3201
3264
|
function loadConfig() {
|
|
3202
3265
|
try {
|
|
3203
3266
|
const configPath = getConfigPath();
|
|
3204
|
-
if (
|
|
3205
|
-
const raw =
|
|
3267
|
+
if (existsSync13(configPath)) {
|
|
3268
|
+
const raw = readFileSync10(configPath, "utf-8");
|
|
3206
3269
|
return { ...DEFAULTS, ...JSON.parse(raw) };
|
|
3207
3270
|
}
|
|
3208
3271
|
} catch {}
|
|
@@ -3347,9 +3410,9 @@ function isAgent(value) {
|
|
|
3347
3410
|
// src/ingest/claude.ts
|
|
3348
3411
|
init_database();
|
|
3349
3412
|
init_pricing();
|
|
3350
|
-
import { readdirSync as readdirSync2, readFileSync, existsSync as
|
|
3351
|
-
import { homedir as
|
|
3352
|
-
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";
|
|
3353
3416
|
|
|
3354
3417
|
// src/lib/savings.ts
|
|
3355
3418
|
function periodWhere2(period, column) {
|
|
@@ -3505,8 +3568,319 @@ function defaultCostBasisForAgent(agent) {
|
|
|
3505
3568
|
return "estimated";
|
|
3506
3569
|
}
|
|
3507
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
|
+
|
|
3508
3883
|
// src/lib/accounts.ts
|
|
3509
|
-
import { appliedProfileName, resolveStore } from "@hasna/accounts";
|
|
3510
3884
|
var AGENT_ACCOUNT_TOOLS = {
|
|
3511
3885
|
claude: ["claude"],
|
|
3512
3886
|
takumi: ["takumi", "claude"],
|
|
@@ -3578,11 +3952,11 @@ async function profileForEnvDir(store, tool, env) {
|
|
|
3578
3952
|
const normalized = normalizeDir(configuredDir);
|
|
3579
3953
|
return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
3580
3954
|
}
|
|
3581
|
-
async function resolveAccountForAgent(agent, env = process.env) {
|
|
3955
|
+
async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
3582
3956
|
const override = envOverride(agent, env);
|
|
3583
3957
|
if (override)
|
|
3584
3958
|
return override;
|
|
3585
|
-
const store = resolveStore(env);
|
|
3959
|
+
const store = resolveStore(env, options);
|
|
3586
3960
|
const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
|
|
3587
3961
|
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
3588
3962
|
const tool = tools.get(toolId);
|
|
@@ -3620,8 +3994,8 @@ function withAccount(record, account) {
|
|
|
3620
3994
|
function autoDetectProject(cwd, projects) {
|
|
3621
3995
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
3622
3996
|
}
|
|
3623
|
-
var CLAUDE_PROJECTS_DIR =
|
|
3624
|
-
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");
|
|
3625
3999
|
function dirNameToPath(dirName) {
|
|
3626
4000
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
3627
4001
|
}
|
|
@@ -3631,9 +4005,9 @@ function collectJsonlFiles(projectDir) {
|
|
|
3631
4005
|
try {
|
|
3632
4006
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
3633
4007
|
if (entry.isDirectory())
|
|
3634
|
-
walk(
|
|
4008
|
+
walk(join3(dir, entry.name));
|
|
3635
4009
|
else if (entry.name.endsWith(".jsonl"))
|
|
3636
|
-
files.push(
|
|
4010
|
+
files.push(join3(dir, entry.name));
|
|
3637
4011
|
}
|
|
3638
4012
|
} catch {}
|
|
3639
4013
|
}
|
|
@@ -3647,7 +4021,7 @@ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_D
|
|
|
3647
4021
|
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
3648
4022
|
}
|
|
3649
4023
|
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
3650
|
-
if (!
|
|
4024
|
+
if (!existsSync3(projectsDir)) {
|
|
3651
4025
|
if (verbose)
|
|
3652
4026
|
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
3653
4027
|
return { files: 0, requests: 0, sessions: 0 };
|
|
@@ -3658,9 +4032,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
3658
4032
|
const touchedSessions = new Set;
|
|
3659
4033
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
3660
4034
|
const account = await resolveAccountForAgent(agentName);
|
|
4035
|
+
const ingestState = loadIngestState(db, agentName);
|
|
3661
4036
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
3662
4037
|
for (const projectDirEntry of projectDirs) {
|
|
3663
|
-
const projectDirPath =
|
|
4038
|
+
const projectDirPath = join3(projectsDir, projectDirEntry.name);
|
|
3664
4039
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
3665
4040
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
3666
4041
|
for (const filePath of jsonlFiles) {
|
|
@@ -3671,12 +4046,12 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
3671
4046
|
} catch {
|
|
3672
4047
|
continue;
|
|
3673
4048
|
}
|
|
3674
|
-
const processed =
|
|
4049
|
+
const processed = ingestState.get(stateKey) ?? null;
|
|
3675
4050
|
if (processed === fileMtime)
|
|
3676
4051
|
continue;
|
|
3677
4052
|
let lines;
|
|
3678
4053
|
try {
|
|
3679
|
-
lines =
|
|
4054
|
+
lines = readFileSync2(filePath, "utf-8").split(`
|
|
3680
4055
|
`).filter((l) => l.trim());
|
|
3681
4056
|
} catch {
|
|
3682
4057
|
continue;
|
|
@@ -3796,14 +4171,14 @@ function supportsClaudeDataResidencyPricing(model) {
|
|
|
3796
4171
|
// src/ingest/codex.ts
|
|
3797
4172
|
init_database();
|
|
3798
4173
|
init_pricing();
|
|
3799
|
-
import { existsSync as
|
|
3800
|
-
import { homedir as
|
|
3801
|
-
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";
|
|
3802
4177
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
3803
|
-
var DEFAULT_CODEX_DB_PATH =
|
|
3804
|
-
var DEFAULT_CODEX_CONFIG_PATH =
|
|
3805
|
-
var DEFAULT_CODEWITH_DB_PATH =
|
|
3806
|
-
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");
|
|
3807
4182
|
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
3808
4183
|
function codexDbPath() {
|
|
3809
4184
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
@@ -3845,10 +4220,10 @@ function codexSources() {
|
|
|
3845
4220
|
});
|
|
3846
4221
|
}
|
|
3847
4222
|
function readCodexModel(configPath = codexConfigPath()) {
|
|
3848
|
-
if (!
|
|
4223
|
+
if (!existsSync4(configPath))
|
|
3849
4224
|
return "gpt-5-codex";
|
|
3850
4225
|
try {
|
|
3851
|
-
const content =
|
|
4226
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
3852
4227
|
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
3853
4228
|
return match?.[1] ?? "gpt-5-codex";
|
|
3854
4229
|
} catch {
|
|
@@ -3886,12 +4261,12 @@ function openCodexDb(dbPath, verbose) {
|
|
|
3886
4261
|
return null;
|
|
3887
4262
|
}
|
|
3888
4263
|
function readTokenEvents(rolloutPath) {
|
|
3889
|
-
if (!rolloutPath || !
|
|
4264
|
+
if (!rolloutPath || !existsSync4(rolloutPath))
|
|
3890
4265
|
return [];
|
|
3891
4266
|
const fallbackUsages = new Map;
|
|
3892
4267
|
let fallbackTimestamp;
|
|
3893
4268
|
let aggregate = null;
|
|
3894
|
-
for (const line of
|
|
4269
|
+
for (const line of readFileSync3(rolloutPath, "utf-8").split(`
|
|
3895
4270
|
`)) {
|
|
3896
4271
|
if (!line.trim())
|
|
3897
4272
|
continue;
|
|
@@ -3967,7 +4342,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
3967
4342
|
let requests = 0;
|
|
3968
4343
|
const account = await resolveAccountForAgent("codex");
|
|
3969
4344
|
for (const source of codexSources()) {
|
|
3970
|
-
if (!
|
|
4345
|
+
if (!existsSync4(source.dbPath)) {
|
|
3971
4346
|
if (verbose)
|
|
3972
4347
|
console.log(`${source.label} DB not found:`, source.dbPath);
|
|
3973
4348
|
continue;
|
|
@@ -4048,11 +4423,11 @@ async function ingestCodex(db, verbose = false) {
|
|
|
4048
4423
|
// src/ingest/gemini.ts
|
|
4049
4424
|
init_database();
|
|
4050
4425
|
init_pricing();
|
|
4051
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
4052
|
-
import { homedir as
|
|
4053
|
-
import { join as
|
|
4054
|
-
var DEFAULT_GEMINI_TMP_DIR =
|
|
4055
|
-
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");
|
|
4056
4431
|
function geminiTmpDir() {
|
|
4057
4432
|
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
4058
4433
|
}
|
|
@@ -4069,12 +4444,12 @@ function numberField(...values) {
|
|
|
4069
4444
|
function listProjectDirs(...roots) {
|
|
4070
4445
|
const dirs = new Set;
|
|
4071
4446
|
for (const root of roots) {
|
|
4072
|
-
if (!
|
|
4447
|
+
if (!existsSync5(root))
|
|
4073
4448
|
continue;
|
|
4074
4449
|
try {
|
|
4075
4450
|
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
4076
4451
|
if (entry.isDirectory())
|
|
4077
|
-
dirs.add(
|
|
4452
|
+
dirs.add(join5(root, entry.name));
|
|
4078
4453
|
}
|
|
4079
4454
|
} catch {}
|
|
4080
4455
|
}
|
|
@@ -4085,17 +4460,17 @@ function projectRoot(projectDir, chatData) {
|
|
|
4085
4460
|
return chatData.projectPath;
|
|
4086
4461
|
if (chatData.project_path)
|
|
4087
4462
|
return chatData.project_path;
|
|
4088
|
-
const rootFile =
|
|
4463
|
+
const rootFile = join5(projectDir, ".project_root");
|
|
4089
4464
|
try {
|
|
4090
|
-
if (
|
|
4091
|
-
return
|
|
4465
|
+
if (existsSync5(rootFile))
|
|
4466
|
+
return readFileSync4(rootFile, "utf-8").trim();
|
|
4092
4467
|
} catch {}
|
|
4093
4468
|
return "";
|
|
4094
4469
|
}
|
|
4095
4470
|
async function ingestGemini(db, verbose) {
|
|
4096
4471
|
const tmpDir = geminiTmpDir();
|
|
4097
4472
|
const historyDir = geminiHistoryDir();
|
|
4098
|
-
if (!
|
|
4473
|
+
if (!existsSync5(tmpDir) && !existsSync5(historyDir)) {
|
|
4099
4474
|
if (verbose)
|
|
4100
4475
|
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
4101
4476
|
return { sessions: 0, requests: 0 };
|
|
@@ -4107,17 +4482,17 @@ async function ingestGemini(db, verbose) {
|
|
|
4107
4482
|
const account = await resolveAccountForAgent("gemini");
|
|
4108
4483
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
4109
4484
|
for (const projectDir of projectDirs) {
|
|
4110
|
-
const chatsDir =
|
|
4111
|
-
if (!
|
|
4485
|
+
const chatsDir = join5(projectDir, "chats");
|
|
4486
|
+
if (!existsSync5(chatsDir))
|
|
4112
4487
|
continue;
|
|
4113
4488
|
let chatFiles = [];
|
|
4114
4489
|
try {
|
|
4115
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) =>
|
|
4490
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join5(chatsDir, f));
|
|
4116
4491
|
} catch {
|
|
4117
4492
|
continue;
|
|
4118
4493
|
}
|
|
4119
4494
|
for (const filePath of chatFiles) {
|
|
4120
|
-
const stateKey = filePath.replace(
|
|
4495
|
+
const stateKey = filePath.replace(homedir5(), "~");
|
|
4121
4496
|
let fileMtime = "0";
|
|
4122
4497
|
try {
|
|
4123
4498
|
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
@@ -4129,7 +4504,7 @@ async function ingestGemini(db, verbose) {
|
|
|
4129
4504
|
continue;
|
|
4130
4505
|
let chatData;
|
|
4131
4506
|
try {
|
|
4132
|
-
chatData = JSON.parse(
|
|
4507
|
+
chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
4133
4508
|
} catch {
|
|
4134
4509
|
continue;
|
|
4135
4510
|
}
|
|
@@ -4207,15 +4582,15 @@ async function ingestGemini(db, verbose) {
|
|
|
4207
4582
|
// src/ingest/opencode.ts
|
|
4208
4583
|
init_database();
|
|
4209
4584
|
init_pricing();
|
|
4210
|
-
import { existsSync as
|
|
4211
|
-
import { homedir as
|
|
4212
|
-
import { join as
|
|
4213
|
-
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");
|
|
4214
4589
|
function walkJsonFiles(dir, acc = []) {
|
|
4215
|
-
if (!
|
|
4590
|
+
if (!existsSync6(dir))
|
|
4216
4591
|
return acc;
|
|
4217
4592
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
4218
|
-
const p =
|
|
4593
|
+
const p = join6(dir, entry.name);
|
|
4219
4594
|
if (entry.isDirectory())
|
|
4220
4595
|
walkJsonFiles(p, acc);
|
|
4221
4596
|
else if (entry.name.endsWith(".json"))
|
|
@@ -4235,7 +4610,7 @@ function parseSessionIdFromPath(filePath) {
|
|
|
4235
4610
|
return null;
|
|
4236
4611
|
}
|
|
4237
4612
|
async function ingestOpenCode(db, verbose = false) {
|
|
4238
|
-
const messageDir =
|
|
4613
|
+
const messageDir = join6(OPENCODE_STORAGE, "message");
|
|
4239
4614
|
const files = walkJsonFiles(messageDir);
|
|
4240
4615
|
let requests = 0;
|
|
4241
4616
|
const touched = new Set;
|
|
@@ -4250,7 +4625,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
4250
4625
|
continue;
|
|
4251
4626
|
let parsed;
|
|
4252
4627
|
try {
|
|
4253
|
-
parsed = JSON.parse(
|
|
4628
|
+
parsed = JSON.parse(readFileSync5(file, "utf-8"));
|
|
4254
4629
|
} catch {
|
|
4255
4630
|
continue;
|
|
4256
4631
|
}
|
|
@@ -4437,15 +4812,15 @@ async function ingestCursor(db, verbose = false) {
|
|
|
4437
4812
|
|
|
4438
4813
|
// src/ingest/pi.ts
|
|
4439
4814
|
init_database();
|
|
4440
|
-
import { existsSync as
|
|
4441
|
-
import { homedir as
|
|
4442
|
-
import { join as
|
|
4443
|
-
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");
|
|
4444
4819
|
function walkSessions(dir, acc = []) {
|
|
4445
|
-
if (!
|
|
4820
|
+
if (!existsSync7(dir))
|
|
4446
4821
|
return acc;
|
|
4447
4822
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
4448
|
-
const p =
|
|
4823
|
+
const p = join7(dir, entry.name);
|
|
4449
4824
|
if (entry.isDirectory())
|
|
4450
4825
|
walkSessions(p, acc);
|
|
4451
4826
|
else if (entry.name.endsWith(".json"))
|
|
@@ -4467,7 +4842,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
4467
4842
|
continue;
|
|
4468
4843
|
let data;
|
|
4469
4844
|
try {
|
|
4470
|
-
data = JSON.parse(
|
|
4845
|
+
data = JSON.parse(readFileSync6(file, "utf-8"));
|
|
4471
4846
|
} catch {
|
|
4472
4847
|
continue;
|
|
4473
4848
|
}
|
|
@@ -4531,10 +4906,10 @@ async function ingestPi(db, verbose = false) {
|
|
|
4531
4906
|
|
|
4532
4907
|
// src/ingest/hermes.ts
|
|
4533
4908
|
init_database();
|
|
4534
|
-
import { existsSync as
|
|
4535
|
-
import { homedir as
|
|
4536
|
-
import { join as
|
|
4537
|
-
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");
|
|
4538
4913
|
function mapCostBasis(billingMode) {
|
|
4539
4914
|
if (billingMode === "subscription")
|
|
4540
4915
|
return "subscription_included";
|
|
@@ -4543,7 +4918,7 @@ function mapCostBasis(billingMode) {
|
|
|
4543
4918
|
return defaultCostBasisForAgent("hermes");
|
|
4544
4919
|
}
|
|
4545
4920
|
async function ingestHermes(db, verbose = false) {
|
|
4546
|
-
if (!
|
|
4921
|
+
if (!existsSync8(HERMES_DB)) {
|
|
4547
4922
|
return { sessions: 0, requests: 0 };
|
|
4548
4923
|
}
|
|
4549
4924
|
const { Database: Sqlite } = await import("bun:sqlite");
|
|
@@ -4623,19 +4998,78 @@ function statSyncSafe(path) {
|
|
|
4623
4998
|
// src/ingest/loops.ts
|
|
4624
4999
|
init_database();
|
|
4625
5000
|
init_pricing();
|
|
4626
|
-
import { existsSync as
|
|
4627
|
-
import { homedir as
|
|
4628
|
-
import { join as
|
|
5001
|
+
import { existsSync as existsSync9 } from "fs";
|
|
5002
|
+
import { homedir as homedir9 } from "os";
|
|
5003
|
+
import { join as join9 } from "path";
|
|
4629
5004
|
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
4630
|
-
import {
|
|
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
|
+
}
|
|
4631
5065
|
var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
|
|
4632
5066
|
function defaultLoopsDbPath() {
|
|
4633
5067
|
const resolved = dataDir2({
|
|
4634
5068
|
app: "loops",
|
|
4635
5069
|
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
4636
5070
|
});
|
|
4637
|
-
const legacy =
|
|
4638
|
-
return
|
|
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;
|
|
4639
5073
|
}
|
|
4640
5074
|
function loopsDbPath() {
|
|
4641
5075
|
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
|
|
@@ -4727,7 +5161,7 @@ function readLoopGoalRuns(loopsDb) {
|
|
|
4727
5161
|
}
|
|
4728
5162
|
async function ingestLoops(db, verbose = false) {
|
|
4729
5163
|
const dbPath = loopsDbPath();
|
|
4730
|
-
if (!
|
|
5164
|
+
if (!existsSync9(dbPath)) {
|
|
4731
5165
|
if (verbose)
|
|
4732
5166
|
console.log("OpenLoops DB not found:", dbPath);
|
|
4733
5167
|
return { sessions: 0, requests: 0 };
|
|
@@ -4817,30 +5251,30 @@ async function ingestLoops(db, verbose = false) {
|
|
|
4817
5251
|
|
|
4818
5252
|
// src/ingest/claude-quota.ts
|
|
4819
5253
|
init_database();
|
|
4820
|
-
import { existsSync as
|
|
5254
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
|
|
4821
5255
|
|
|
4822
5256
|
// src/lib/paths.ts
|
|
4823
|
-
import { homedir as
|
|
4824
|
-
import { join as
|
|
5257
|
+
import { homedir as homedir10 } from "os";
|
|
5258
|
+
import { join as join10 } from "path";
|
|
4825
5259
|
function getHomeDir2() {
|
|
4826
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ??
|
|
5260
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir10();
|
|
4827
5261
|
}
|
|
4828
5262
|
function agentPaths() {
|
|
4829
5263
|
const home = getHomeDir2();
|
|
4830
5264
|
return {
|
|
4831
|
-
claudeProjects:
|
|
4832
|
-
claudeCredentials:
|
|
4833
|
-
takumiProjects:
|
|
4834
|
-
codexDir:
|
|
4835
|
-
codexDb:
|
|
4836
|
-
codexAuth:
|
|
4837
|
-
codexConfig:
|
|
4838
|
-
geminiTmp:
|
|
4839
|
-
geminiHistory:
|
|
4840
|
-
opencodeMessages:
|
|
4841
|
-
piSessions:
|
|
4842
|
-
hermesDir:
|
|
4843
|
-
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")
|
|
4844
5278
|
};
|
|
4845
5279
|
}
|
|
4846
5280
|
|
|
@@ -4852,10 +5286,10 @@ function readClaudeToken() {
|
|
|
4852
5286
|
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
4853
5287
|
if (fromEnv)
|
|
4854
5288
|
return { token: fromEnv };
|
|
4855
|
-
if (!
|
|
5289
|
+
if (!existsSync10(CREDENTIALS_PATH))
|
|
4856
5290
|
return null;
|
|
4857
5291
|
try {
|
|
4858
|
-
const creds = JSON.parse(
|
|
5292
|
+
const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
|
|
4859
5293
|
const oauth = creds.claudeAiOauth;
|
|
4860
5294
|
if (!oauth?.accessToken)
|
|
4861
5295
|
return null;
|
|
@@ -4991,17 +5425,17 @@ async function ingestClaudeQuota(db, verbose = false) {
|
|
|
4991
5425
|
|
|
4992
5426
|
// src/ingest/codex-quota.ts
|
|
4993
5427
|
init_database();
|
|
4994
|
-
import { existsSync as
|
|
5428
|
+
import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
|
|
4995
5429
|
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
4996
5430
|
function readCodexAuth() {
|
|
4997
5431
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
4998
5432
|
if (fromEnv)
|
|
4999
5433
|
return { token: fromEnv, authMode: "chatgpt" };
|
|
5000
5434
|
const authPath = agentPaths().codexAuth;
|
|
5001
|
-
if (!
|
|
5435
|
+
if (!existsSync11(authPath))
|
|
5002
5436
|
return null;
|
|
5003
5437
|
try {
|
|
5004
|
-
const auth = JSON.parse(
|
|
5438
|
+
const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
|
|
5005
5439
|
const token = auth.tokens?.access_token;
|
|
5006
5440
|
if (!token)
|
|
5007
5441
|
return null;
|
|
@@ -5986,7 +6420,7 @@ init_database();
|
|
|
5986
6420
|
|
|
5987
6421
|
// src/lib/serve-auth.ts
|
|
5988
6422
|
function getServeApiToken() {
|
|
5989
|
-
return process.env["
|
|
6423
|
+
return process.env["HASNA_ECONOMY_API_TOKEN"];
|
|
5990
6424
|
}
|
|
5991
6425
|
function getServeBindHost() {
|
|
5992
6426
|
const explicit = process.env["ECONOMY_BIND"] ?? process.env["ECONOMY_HOST"];
|
|
@@ -6010,13 +6444,11 @@ function isAuthorizedRequest(req, path) {
|
|
|
6010
6444
|
|
|
6011
6445
|
// src/server/serve.ts
|
|
6012
6446
|
import { randomUUID } from "crypto";
|
|
6013
|
-
import { existsSync as existsSync13 } from "fs";
|
|
6014
|
-
import { resolve as resolve2, sep } from "path";
|
|
6015
6447
|
// package.json
|
|
6016
6448
|
var package_default = {
|
|
6017
6449
|
name: "@hasna/economy",
|
|
6018
|
-
version: "0.
|
|
6019
|
-
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",
|
|
6020
6452
|
repository: {
|
|
6021
6453
|
type: "git",
|
|
6022
6454
|
url: "git+https://github.com/hasna/economy.git"
|
|
@@ -6047,7 +6479,6 @@ var package_default = {
|
|
|
6047
6479
|
files: [
|
|
6048
6480
|
"dist",
|
|
6049
6481
|
"docs",
|
|
6050
|
-
"dashboard/dist",
|
|
6051
6482
|
"postinstall.js",
|
|
6052
6483
|
"LICENSE",
|
|
6053
6484
|
"README.md",
|
|
@@ -6057,12 +6488,11 @@ var package_default = {
|
|
|
6057
6488
|
"CODE_OF_CONDUCT.md"
|
|
6058
6489
|
],
|
|
6059
6490
|
scripts: {
|
|
6060
|
-
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",
|
|
6061
6492
|
"build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
|
|
6062
6493
|
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
|
|
6063
6494
|
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
6064
6495
|
"build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
6065
|
-
"build:dashboard": "cd dashboard && bun run build",
|
|
6066
6496
|
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
6067
6497
|
prepack: "bun run build && bun run artifact-scan",
|
|
6068
6498
|
typecheck: "tsc --noEmit",
|
|
@@ -6092,12 +6522,8 @@ var package_default = {
|
|
|
6092
6522
|
access: "public"
|
|
6093
6523
|
},
|
|
6094
6524
|
dependencies: {
|
|
6095
|
-
"@hasna/
|
|
6096
|
-
"@hasna/agent-registry": "^0.1.0",
|
|
6097
|
-
"@hasna/contracts": "0.13.4",
|
|
6525
|
+
"@hasna/contracts": "1.0.2",
|
|
6098
6526
|
"@hasna/events": "^0.1.16",
|
|
6099
|
-
"@hasna/mcp-harness": "^0.1.0",
|
|
6100
|
-
"@hasna/paths": "0.1.0",
|
|
6101
6527
|
"@hasna/projects": "1.0.0",
|
|
6102
6528
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
6103
6529
|
chalk: "^5.4.1",
|
|
@@ -6106,7 +6532,7 @@ var package_default = {
|
|
|
6106
6532
|
zod: "^3.24.2"
|
|
6107
6533
|
},
|
|
6108
6534
|
devDependencies: {
|
|
6109
|
-
"@types/bun": "
|
|
6535
|
+
"@types/bun": "1.3.14",
|
|
6110
6536
|
"@types/pg": "^8.20.0",
|
|
6111
6537
|
"bun-types": "latest",
|
|
6112
6538
|
typescript: "^5.7.2"
|
|
@@ -7784,7 +8210,6 @@ var APP = "economy";
|
|
|
7784
8210
|
var ECONOMY_WRITE_SCOPE = `${APP}:write`;
|
|
7785
8211
|
var AGENT_ERROR = `agent must be one of: ${AGENTS.join(", ")}`;
|
|
7786
8212
|
var SYNC_SOURCES = ["all", ...AGENTS, "loops"];
|
|
7787
|
-
var DEFAULT_DASHBOARD_DIR = new URL("../../dashboard/dist", import.meta.url).pathname;
|
|
7788
8213
|
function json(data, status = 200) {
|
|
7789
8214
|
return new Response(JSON.stringify(data), {
|
|
7790
8215
|
status,
|
|
@@ -7837,38 +8262,6 @@ function requiredScopesForRequest(method, path) {
|
|
|
7837
8262
|
function stringArray(value) {
|
|
7838
8263
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
7839
8264
|
}
|
|
7840
|
-
function dashboardPath(root, pathname) {
|
|
7841
|
-
let decoded;
|
|
7842
|
-
try {
|
|
7843
|
-
decoded = decodeURIComponent(pathname);
|
|
7844
|
-
} catch {
|
|
7845
|
-
return null;
|
|
7846
|
-
}
|
|
7847
|
-
const relativePath = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
|
|
7848
|
-
const rootPath = resolve2(root);
|
|
7849
|
-
const filePath = resolve2(rootPath, relativePath);
|
|
7850
|
-
return filePath === rootPath || filePath.startsWith(rootPath + sep) ? filePath : null;
|
|
7851
|
-
}
|
|
7852
|
-
var FOUNDATION_PATHS = new Set(["/health", "/healthz", "/ready", "/readyz", "/version", "/openapi.json"]);
|
|
7853
|
-
function createServerFetch(apiHandler, dashboardDir = DEFAULT_DASHBOARD_DIR) {
|
|
7854
|
-
return async function fetch2(req) {
|
|
7855
|
-
const url = new URL(req.url);
|
|
7856
|
-
if (url.pathname.startsWith("/api") || url.pathname.startsWith("/v1") || FOUNDATION_PATHS.has(url.pathname)) {
|
|
7857
|
-
return apiHandler(req);
|
|
7858
|
-
}
|
|
7859
|
-
if (existsSync13(dashboardDir)) {
|
|
7860
|
-
const filePath = dashboardPath(dashboardDir, url.pathname);
|
|
7861
|
-
if (filePath && existsSync13(filePath)) {
|
|
7862
|
-
return new Response(Bun.file(filePath));
|
|
7863
|
-
}
|
|
7864
|
-
const indexPath = dashboardPath(dashboardDir, "/");
|
|
7865
|
-
if (indexPath && existsSync13(indexPath)) {
|
|
7866
|
-
return new Response(Bun.file(indexPath));
|
|
7867
|
-
}
|
|
7868
|
-
}
|
|
7869
|
-
return apiHandler(req);
|
|
7870
|
-
};
|
|
7871
|
-
}
|
|
7872
8265
|
function applyFields(obj, fields) {
|
|
7873
8266
|
if (!fields || fields.length === 0)
|
|
7874
8267
|
return obj;
|
|
@@ -8392,7 +8785,7 @@ function startServer(port = 3456, options = {}) {
|
|
|
8392
8785
|
const server = Bun.serve({
|
|
8393
8786
|
port,
|
|
8394
8787
|
hostname: hostname2,
|
|
8395
|
-
fetch:
|
|
8788
|
+
fetch: apiHandler
|
|
8396
8789
|
});
|
|
8397
8790
|
const address = `http://${hostname2 === "0.0.0.0" ? "localhost" : hostname2}:${server.port}`;
|
|
8398
8791
|
log(`economy-serve listening on ${address}`);
|