@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/mcp/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"AAcA,eAAO,MAAM,QAAQ,YAAY,CAAA;AACjC,eAAO,MAAM,qBAAqB,OAAO,CAAA;AAEzC,wBAAgB,WAAW,IAAI,GAAG,CA81BjC"}
|
package/dist/otel/index.js
CHANGED
|
@@ -594,7 +594,60 @@ import { execFileSync } from "child_process";
|
|
|
594
594
|
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
595
595
|
import { hostname, platform } from "os";
|
|
596
596
|
import { homedir } from "os";
|
|
597
|
-
import { join } from "path";
|
|
597
|
+
import { join, resolve } from "path";
|
|
598
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
599
|
+
import { join as pathsResolverJoin } from "path";
|
|
600
|
+
function pathsResolverAssertApp(app) {
|
|
601
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
602
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
603
|
+
}
|
|
604
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
605
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
function pathsResolverAssertKind(kind) {
|
|
609
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
610
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
function pathsResolverBaseDir(kind, options) {
|
|
614
|
+
pathsResolverAssertKind(kind);
|
|
615
|
+
const env = options.env ?? process.env;
|
|
616
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
617
|
+
if (typeof override === "string" && override.length > 0)
|
|
618
|
+
return override;
|
|
619
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
620
|
+
const platform2 = options.platform ?? process.platform;
|
|
621
|
+
if (platform2 === "darwin") {
|
|
622
|
+
switch (kind) {
|
|
623
|
+
case "config":
|
|
624
|
+
case "data":
|
|
625
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
626
|
+
case "cache":
|
|
627
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
628
|
+
case "state":
|
|
629
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
switch (kind) {
|
|
633
|
+
case "config":
|
|
634
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
635
|
+
case "data":
|
|
636
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
637
|
+
case "state":
|
|
638
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
639
|
+
case "cache":
|
|
640
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
function pathsResolverResolve(kind, options) {
|
|
644
|
+
pathsResolverAssertApp(options.app);
|
|
645
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
646
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
647
|
+
}
|
|
648
|
+
function dataDir(options) {
|
|
649
|
+
return pathsResolverResolve("data", options);
|
|
650
|
+
}
|
|
598
651
|
function normalizeMachineId(value) {
|
|
599
652
|
const id = value?.trim().toLowerCase().split(".")[0];
|
|
600
653
|
return id && id.length > 0 ? id : null;
|
|
@@ -617,7 +670,7 @@ function macHostMachineId() {
|
|
|
617
670
|
return null;
|
|
618
671
|
}
|
|
619
672
|
function getMachineId() {
|
|
620
|
-
const envMachine = normalizeMachineId(process.env["
|
|
673
|
+
const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
|
|
621
674
|
if (envMachine)
|
|
622
675
|
return envMachine;
|
|
623
676
|
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
@@ -625,21 +678,48 @@ function getMachineId() {
|
|
|
625
678
|
return macHostMachineId() ?? hostMachine;
|
|
626
679
|
return hostMachine;
|
|
627
680
|
}
|
|
628
|
-
function
|
|
681
|
+
function getHomeDir() {
|
|
629
682
|
const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
683
|
+
if (!home)
|
|
684
|
+
throw new Error("Could not resolve the user home directory");
|
|
685
|
+
return home;
|
|
686
|
+
}
|
|
687
|
+
function getResolverDataRoot() {
|
|
688
|
+
return dataDir({
|
|
689
|
+
app: "economy",
|
|
690
|
+
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
691
|
+
});
|
|
692
|
+
}
|
|
693
|
+
function getLegacyDataRoot() {
|
|
694
|
+
return join(getHomeDir(), ".hasna", "economy");
|
|
695
|
+
}
|
|
696
|
+
function adoptResolverDataRoot(resolved, env = process.env) {
|
|
697
|
+
const dataOverride = env.HASNA_DATA_HOME;
|
|
698
|
+
if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
|
|
699
|
+
return true;
|
|
700
|
+
return existsSync(join(resolved, "economy.db"));
|
|
701
|
+
}
|
|
702
|
+
function getExactDataRoot() {
|
|
703
|
+
const dir = process.env["HASNA_ECONOMY_HOME"] ?? process.env["ECONOMY_HOME"];
|
|
704
|
+
if (dir && dir.trim())
|
|
705
|
+
return dir.trim();
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
function getDataDir() {
|
|
709
|
+
const exact = getExactDataRoot();
|
|
710
|
+
const effective = exact ? resolve(exact) : adoptResolverDataRoot(getResolverDataRoot()) ? resolve(getResolverDataRoot()) : getLegacyDataRoot();
|
|
711
|
+
const oldDir = join(getHomeDir(), ".economy");
|
|
712
|
+
if (existsSync(oldDir) && !existsSync(effective)) {
|
|
713
|
+
mkdirSync(effective, { recursive: true });
|
|
634
714
|
for (const file of readdirSync(oldDir)) {
|
|
635
715
|
const oldPath = join(oldDir, file);
|
|
636
716
|
if (statSync(oldPath).isFile()) {
|
|
637
|
-
copyFileSync(oldPath, join(
|
|
717
|
+
copyFileSync(oldPath, join(effective, file));
|
|
638
718
|
}
|
|
639
719
|
}
|
|
640
720
|
}
|
|
641
|
-
mkdirSync(
|
|
642
|
-
return
|
|
721
|
+
mkdirSync(effective, { recursive: true });
|
|
722
|
+
return effective;
|
|
643
723
|
}
|
|
644
724
|
function getDbPath() {
|
|
645
725
|
if (process.env["HASNA_ECONOMY_DB_PATH"])
|
|
@@ -1039,8 +1119,16 @@ function seedModelPricing(db, defaults) {
|
|
|
1039
1119
|
});
|
|
1040
1120
|
}
|
|
1041
1121
|
}
|
|
1122
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
|
|
1042
1123
|
var init_database = __esm(() => {
|
|
1043
1124
|
init_sqlite_adapter();
|
|
1125
|
+
PATHS_RESOLVER_KIND_ENV = {
|
|
1126
|
+
config: "HASNA_CONFIG_HOME",
|
|
1127
|
+
data: "HASNA_DATA_HOME",
|
|
1128
|
+
state: "HASNA_STATE_HOME",
|
|
1129
|
+
cache: "HASNA_CACHE_HOME"
|
|
1130
|
+
};
|
|
1131
|
+
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
1044
1132
|
});
|
|
1045
1133
|
|
|
1046
1134
|
// src/otel/index.ts
|
|
@@ -1385,11 +1473,334 @@ async function ingestOtelRows(db, rows) {
|
|
|
1385
1473
|
rollupSession(db, sessionId);
|
|
1386
1474
|
return { requests, sessions: sessions.size };
|
|
1387
1475
|
}
|
|
1476
|
+
|
|
1477
|
+
// src/lib/cloud-ingest.ts
|
|
1478
|
+
init_database();
|
|
1479
|
+
init_pricing();
|
|
1480
|
+
|
|
1481
|
+
// src/ingest/claude.ts
|
|
1482
|
+
init_database();
|
|
1483
|
+
init_pricing();
|
|
1484
|
+
import { homedir as homedir3 } from "os";
|
|
1485
|
+
import { join as join3, basename } from "path";
|
|
1486
|
+
|
|
1487
|
+
// src/lib/accounts-store.ts
|
|
1488
|
+
import { homedir as homedir2 } from "os";
|
|
1489
|
+
import { join as join2 } from "path";
|
|
1490
|
+
import { z } from "zod";
|
|
1491
|
+
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
1492
|
+
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");
|
|
1493
|
+
var profileSchema = z.object({
|
|
1494
|
+
name: z.string(),
|
|
1495
|
+
tool: z.string(),
|
|
1496
|
+
email: z.string().optional(),
|
|
1497
|
+
displayName: z.string().optional(),
|
|
1498
|
+
dir: z.string(),
|
|
1499
|
+
description: z.string().optional(),
|
|
1500
|
+
createdAt: z.string(),
|
|
1501
|
+
lastUsedAt: z.string().optional()
|
|
1502
|
+
});
|
|
1503
|
+
var toolDefSchema = z.object({
|
|
1504
|
+
id: z.string(),
|
|
1505
|
+
label: z.string(),
|
|
1506
|
+
envVar: z.string(),
|
|
1507
|
+
defaultDir: z.string(),
|
|
1508
|
+
bin: z.string()
|
|
1509
|
+
});
|
|
1510
|
+
var storeSchema = z.object({
|
|
1511
|
+
version: z.literal(1),
|
|
1512
|
+
current: z.record(z.string(), z.string()).default({}),
|
|
1513
|
+
applied: z.record(z.string(), z.string()).default({}),
|
|
1514
|
+
toolLocks: z.record(z.string(), z.string()).default({}),
|
|
1515
|
+
profiles: z.array(profileSchema).default([]),
|
|
1516
|
+
tools: z.array(toolDefSchema).default([])
|
|
1517
|
+
});
|
|
1518
|
+
var BUILTIN_TOOLS = [
|
|
1519
|
+
{ id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join2(homedir2(), ".claude"), bin: "claude" },
|
|
1520
|
+
{ id: "codex-app", label: "Codex App", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "/Applications/Codex.app/Contents/MacOS/Codex" },
|
|
1521
|
+
{ id: "codex", label: "Codex CLI", envVar: "CODEX_HOME", defaultDir: join2(homedir2(), ".codex"), bin: "codex" },
|
|
1522
|
+
{ id: "codewith", label: "Codewith", envVar: "CODEWITH_HOME", defaultDir: join2(homedir2(), ".codewith"), bin: "codewith" },
|
|
1523
|
+
{ id: "takumi", label: "Takumi", envVar: "TAKUMI_CONFIG_DIR", defaultDir: join2(homedir2(), ".takumi"), bin: "takumi" },
|
|
1524
|
+
{ id: "gemini", label: "Gemini CLI", envVar: "GEMINI_CONFIG_DIR", defaultDir: join2(homedir2(), ".gemini"), bin: "gemini" },
|
|
1525
|
+
{ id: "opencode", label: "opencode", envVar: "OPENCODE_CONFIG_DIR", defaultDir: join2(homedir2(), ".config", "opencode"), bin: "opencode" },
|
|
1526
|
+
{ id: "cursor", label: "Cursor Agent", envVar: "CURSOR_CONFIG_DIR", defaultDir: join2(homedir2(), ".cursor"), bin: "cursor-agent" },
|
|
1527
|
+
{ id: "pi", label: "Pi Coding Agent", envVar: "PI_CODING_AGENT_HOME", defaultDir: join2(homedir2(), ".pi"), bin: "pi" },
|
|
1528
|
+
{ id: "hermes", label: "Hermes", envVar: "HERMES_HOME", defaultDir: join2(homedir2(), ".hermes"), bin: "hermes" },
|
|
1529
|
+
{ id: "kimi", label: "Kimi Code", envVar: "KIMI_CODE_HOME", defaultDir: join2(homedir2(), ".kimi-code"), bin: "kimi" },
|
|
1530
|
+
{ id: "grok", label: "Grok Build", envVar: "HOME", defaultDir: join2(homedir2(), ".grok"), bin: "grok" }
|
|
1531
|
+
];
|
|
1532
|
+
|
|
1533
|
+
// src/ingest/claude.ts
|
|
1534
|
+
var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join3(homedir3(), ".claude", "projects");
|
|
1535
|
+
var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join3(homedir3(), ".takumi", "projects");
|
|
1536
|
+
|
|
1537
|
+
// src/ingest/codex.ts
|
|
1538
|
+
import { homedir as homedir4 } from "os";
|
|
1539
|
+
import { join as join4, basename as basename2 } from "path";
|
|
1540
|
+
init_database();
|
|
1541
|
+
init_pricing();
|
|
1542
|
+
var DEFAULT_CODEX_DB_PATH = join4(homedir4(), ".codex", "state_5.sqlite");
|
|
1543
|
+
var DEFAULT_CODEX_CONFIG_PATH = join4(homedir4(), ".codex", "config.toml");
|
|
1544
|
+
var DEFAULT_CODEWITH_DB_PATH = join4(homedir4(), ".codewith", "state_5.sqlite");
|
|
1545
|
+
var DEFAULT_CODEWITH_CONFIG_PATH = join4(homedir4(), ".codewith", "config.toml");
|
|
1546
|
+
|
|
1547
|
+
// src/ingest/gemini.ts
|
|
1548
|
+
init_database();
|
|
1549
|
+
init_pricing();
|
|
1550
|
+
import { homedir as homedir5 } from "os";
|
|
1551
|
+
import { join as join5, basename as basename3 } from "path";
|
|
1552
|
+
var DEFAULT_GEMINI_TMP_DIR = join5(homedir5(), ".gemini", "tmp");
|
|
1553
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join5(homedir5(), ".gemini", "history");
|
|
1554
|
+
|
|
1555
|
+
// src/ingest/opencode.ts
|
|
1556
|
+
init_database();
|
|
1557
|
+
init_pricing();
|
|
1558
|
+
import { homedir as homedir6 } from "os";
|
|
1559
|
+
import { join as join6 } from "path";
|
|
1560
|
+
var OPENCODE_STORAGE = join6(homedir6(), ".local", "share", "opencode", "storage");
|
|
1561
|
+
|
|
1562
|
+
// src/ingest/cursor.ts
|
|
1563
|
+
init_database();
|
|
1564
|
+
|
|
1565
|
+
// src/ingest/pi.ts
|
|
1566
|
+
init_database();
|
|
1567
|
+
import { homedir as homedir7 } from "os";
|
|
1568
|
+
import { join as join7 } from "path";
|
|
1569
|
+
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join7(homedir7(), ".pi", "agent", "sessions");
|
|
1570
|
+
|
|
1571
|
+
// src/ingest/hermes.ts
|
|
1572
|
+
init_database();
|
|
1573
|
+
import { homedir as homedir8 } from "os";
|
|
1574
|
+
import { join as join8 } from "path";
|
|
1575
|
+
var HERMES_DB = join8(homedir8(), ".hermes", "state.db");
|
|
1576
|
+
|
|
1577
|
+
// src/ingest/loops.ts
|
|
1578
|
+
init_database();
|
|
1579
|
+
init_pricing();
|
|
1580
|
+
|
|
1581
|
+
// src/ingest/claude-quota.ts
|
|
1582
|
+
init_database();
|
|
1583
|
+
|
|
1584
|
+
// src/lib/paths.ts
|
|
1585
|
+
import { homedir as homedir9 } from "os";
|
|
1586
|
+
import { join as join9 } from "path";
|
|
1587
|
+
function getHomeDir2() {
|
|
1588
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
|
|
1589
|
+
}
|
|
1590
|
+
function agentPaths() {
|
|
1591
|
+
const home = getHomeDir2();
|
|
1592
|
+
return {
|
|
1593
|
+
claudeProjects: join9(home, ".claude", "projects"),
|
|
1594
|
+
claudeCredentials: join9(home, ".claude", ".credentials.json"),
|
|
1595
|
+
takumiProjects: join9(home, ".takumi", "projects"),
|
|
1596
|
+
codexDir: join9(home, ".codex"),
|
|
1597
|
+
codexDb: join9(home, ".codex", "state_5.sqlite"),
|
|
1598
|
+
codexAuth: join9(home, ".codex", "auth.json"),
|
|
1599
|
+
codexConfig: join9(home, ".codex", "config.toml"),
|
|
1600
|
+
geminiTmp: join9(home, ".gemini", "tmp"),
|
|
1601
|
+
geminiHistory: join9(home, ".gemini", "history"),
|
|
1602
|
+
opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
|
|
1603
|
+
piSessions: join9(home, ".pi", "agent", "sessions"),
|
|
1604
|
+
hermesDir: join9(home, ".hermes"),
|
|
1605
|
+
hermesDb: join9(home, ".hermes", "state.db")
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// src/ingest/claude-quota.ts
|
|
1610
|
+
var CREDENTIALS_PATH = agentPaths().claudeCredentials;
|
|
1611
|
+
|
|
1612
|
+
// src/ingest/codex-quota.ts
|
|
1613
|
+
init_database();
|
|
1614
|
+
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
1615
|
+
|
|
1616
|
+
// src/lib/sync-all.ts
|
|
1617
|
+
init_database();
|
|
1618
|
+
|
|
1619
|
+
// src/ingest/billing.ts
|
|
1620
|
+
init_database();
|
|
1621
|
+
|
|
1622
|
+
// src/lib/sync-maintenance.ts
|
|
1623
|
+
init_database();
|
|
1624
|
+
|
|
1625
|
+
// src/lib/cloud-ingest.ts
|
|
1626
|
+
var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snapshots", "subscriptions"];
|
|
1627
|
+
function exportIngestRows(db) {
|
|
1628
|
+
const body = {};
|
|
1629
|
+
for (const table of CLOUD_INGEST_TABLES) {
|
|
1630
|
+
const rows = db.prepare(`SELECT * FROM ${table}`).all();
|
|
1631
|
+
if (rows.length > 0)
|
|
1632
|
+
body[table] = rows;
|
|
1633
|
+
}
|
|
1634
|
+
return body;
|
|
1635
|
+
}
|
|
1636
|
+
function unwrapEnvelope(raw) {
|
|
1637
|
+
if (raw && typeof raw === "object" && "data" in raw) {
|
|
1638
|
+
const data = raw.data;
|
|
1639
|
+
return data && typeof data === "object" ? data : {};
|
|
1640
|
+
}
|
|
1641
|
+
return raw ?? {};
|
|
1642
|
+
}
|
|
1643
|
+
async function postIngest(cloud, body) {
|
|
1644
|
+
const raw = await cloud.client.transport.post("/ingest", body);
|
|
1645
|
+
const payload = unwrapEnvelope(raw);
|
|
1646
|
+
return {
|
|
1647
|
+
ingested: payload["ingested"] ?? {},
|
|
1648
|
+
total: Number(payload["total"] ?? 0)
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
async function pushIngestRows(cloud, scratch) {
|
|
1652
|
+
const body = exportIngestRows(scratch);
|
|
1653
|
+
if (Object.keys(body).length === 0)
|
|
1654
|
+
return { posted: false, ingested: {}, total: 0 };
|
|
1655
|
+
const response = await postIngest(cloud, body);
|
|
1656
|
+
return { posted: true, ingested: response.ingested, total: response.total };
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// src/lib/cloud-storage.ts
|
|
1660
|
+
import {
|
|
1661
|
+
appConfigDiskValue,
|
|
1662
|
+
ClientTransportConfigurationError,
|
|
1663
|
+
clientTransportEnvKeys,
|
|
1664
|
+
createClientTransport,
|
|
1665
|
+
credentialDiskSources,
|
|
1666
|
+
credentialOverrideEnvKey,
|
|
1667
|
+
credentialPointerEnvKey,
|
|
1668
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
1669
|
+
keychainConfigValue
|
|
1670
|
+
} from "@hasna/contracts/client";
|
|
1671
|
+
import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
|
|
1672
|
+
var ECONOMY_APP = "economy";
|
|
1673
|
+
var RETIRED_STORAGE_MODE_KEYS = [
|
|
1674
|
+
"HASNA_ECONOMY_STORAGE_MODE",
|
|
1675
|
+
"HASNA_ECONOMY_MODE",
|
|
1676
|
+
"ECONOMY_STORAGE_MODE",
|
|
1677
|
+
"ECONOMY_MODE"
|
|
1678
|
+
];
|
|
1679
|
+
function assertNoRetiredStorageMode(env) {
|
|
1680
|
+
const legacyKey = RETIRED_STORAGE_MODE_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
|
|
1681
|
+
if (!legacyKey)
|
|
1682
|
+
return;
|
|
1683
|
+
throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client routes through the HTTP API resolved by @hasna/contracts ` + `(HASNA_ECONOMY_API_URL + HASNA_ECONOMY_API_KEY, the Keychain, or ` + `~/.hasna/economy/config/credentials), or serves the local SQLite store only when ` + `HASNA_ECONOMY_LOCAL=1 is set.`);
|
|
1684
|
+
}
|
|
1685
|
+
var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
|
|
1686
|
+
function envFlagSet(env, key) {
|
|
1687
|
+
const raw = env[key];
|
|
1688
|
+
if (raw === undefined)
|
|
1689
|
+
return false;
|
|
1690
|
+
const value = raw.trim().toLowerCase();
|
|
1691
|
+
return value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
|
|
1692
|
+
}
|
|
1693
|
+
function localStorageExplicitlyOptedIn(env) {
|
|
1694
|
+
return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => envFlagSet(env, key));
|
|
1695
|
+
}
|
|
1696
|
+
function economyAuthorityEnvKeys() {
|
|
1697
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
1698
|
+
return [
|
|
1699
|
+
...keys.apiUrlKeys,
|
|
1700
|
+
...keys.apiKeyKeys,
|
|
1701
|
+
credentialOverrideEnvKey(ECONOMY_APP),
|
|
1702
|
+
credentialPointerEnvKey(ECONOMY_APP),
|
|
1703
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
1704
|
+
];
|
|
1705
|
+
}
|
|
1706
|
+
function hasEconomyEnvAuthorityIntent(env) {
|
|
1707
|
+
return economyAuthorityEnvKeys().some((key) => (env[key] ?? "").trim().length > 0);
|
|
1708
|
+
}
|
|
1709
|
+
function hostedAuthorityConfigured(env, options = {}) {
|
|
1710
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
1711
|
+
if (keys.apiUrlKeys.some((key) => (env[key] ?? "").trim().length > 0))
|
|
1712
|
+
return true;
|
|
1713
|
+
if (keychainConfigValue(ECONOMY_APP, env, options.credentials?.keychain) !== null)
|
|
1714
|
+
return true;
|
|
1715
|
+
return appConfigDiskValue(ECONOMY_APP, env, keys.apiUrlKeys) !== null;
|
|
1716
|
+
}
|
|
1717
|
+
function economyResolverEnv(env) {
|
|
1718
|
+
const blanks = economyAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
1719
|
+
if (blanks.length === 0)
|
|
1720
|
+
return env;
|
|
1721
|
+
const next = { ...env };
|
|
1722
|
+
for (const key of blanks)
|
|
1723
|
+
delete next[key];
|
|
1724
|
+
return next;
|
|
1725
|
+
}
|
|
1726
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
1727
|
+
function isAmbientEconomyEnv(env) {
|
|
1728
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
1729
|
+
return true;
|
|
1730
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
1731
|
+
}
|
|
1732
|
+
function economyResolverInputs(env, credentials = {}) {
|
|
1733
|
+
const normalised = economyResolverEnv(env);
|
|
1734
|
+
if (normalised === env)
|
|
1735
|
+
return { env: normalised, credentials };
|
|
1736
|
+
const keychain = { ...credentials.keychain };
|
|
1737
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
1738
|
+
keychain.enabled = isAmbientEconomyEnv(env);
|
|
1739
|
+
}
|
|
1740
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
1741
|
+
}
|
|
1742
|
+
function resolveEconomyStorageClient(name, env, overrides = {}) {
|
|
1743
|
+
const { env: resolverEnv, credentials } = economyResolverInputs(env, overrides.credentials);
|
|
1744
|
+
const wired = createClientTransport(name, resolverEnv, {
|
|
1745
|
+
credentials,
|
|
1746
|
+
...overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
1747
|
+
...overrides.headers ? { headers: overrides.headers } : {},
|
|
1748
|
+
...overrides.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
1749
|
+
...overrides.retry !== undefined ? { retry: overrides.retry } : {},
|
|
1750
|
+
...overrides.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
1751
|
+
});
|
|
1752
|
+
return {
|
|
1753
|
+
transport: "http",
|
|
1754
|
+
client: createHasnaStorageClient(name, wired.client),
|
|
1755
|
+
resolution: wired.resolution
|
|
1756
|
+
};
|
|
1757
|
+
}
|
|
1758
|
+
function localEconomyNotice() {
|
|
1759
|
+
return `economy: local mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
|
|
1760
|
+
}
|
|
1761
|
+
function credentialRequiredError(env, cause) {
|
|
1762
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
1763
|
+
const clientEnv = env;
|
|
1764
|
+
const detail = cause instanceof Error ? cause.message : `No API key could be resolved for '${ECONOMY_APP}'. Looked in the Keychain (macOS only), ` + `then for a credential file at ${credentialDiskSources(ECONOMY_APP, clientEnv).join(" or ") || "<no HOME set>"}, ` + `then for ${keys.apiKeyKeys[0]} in the environment.`;
|
|
1765
|
+
const wayOut = hostedAuthorityConfigured(clientEnv) ? `${keys.apiUrlKeys[0]} (or the Keychain hasna.credentials.${ECONOMY_APP}.api-url item, or ` + `~/.hasna/${ECONOMY_APP}/config/credentials) selects a hosted service, so ${LOCAL_STORAGE_OPT_IN_KEYS[0]} ` + `does not apply; unset the authority to use the local store.` : `Or set ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 to explicitly opt into the local store.`;
|
|
1766
|
+
return new Error(`Economy fails closed instead of silently serving the local SQLite store: ${detail} ` + `Set ${keys.apiUrlKeys[0]} and ${keys.apiKeyKeys[0]} (or store the key in the Keychain item ` + `hasna.credentials.${ECONOMY_APP}.api-key, or in ~/.hasna/${ECONOMY_APP}/config/credentials) to use the ` + `hosted economy API at ${keys.apiUrlKeys[0]} or the default fleet gateway. ` + wayOut);
|
|
1767
|
+
}
|
|
1768
|
+
var cache;
|
|
1769
|
+
var localNoticePrinted = false;
|
|
1770
|
+
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
1771
|
+
assertNoRetiredStorageMode(env);
|
|
1772
|
+
const clientEnv = env;
|
|
1773
|
+
if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
|
|
1774
|
+
return { active: false, client: null };
|
|
1775
|
+
}
|
|
1776
|
+
let resolved;
|
|
1777
|
+
try {
|
|
1778
|
+
resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
if (error instanceof ClientTransportConfigurationError) {
|
|
1781
|
+
throw credentialRequiredError(env, error);
|
|
1782
|
+
}
|
|
1783
|
+
throw error;
|
|
1784
|
+
}
|
|
1785
|
+
return { active: true, client: resolved.client };
|
|
1786
|
+
}
|
|
1787
|
+
function economyCloudStorage(env = process.env) {
|
|
1788
|
+
if (cache === undefined) {
|
|
1789
|
+
cache = resolveEconomyCloudStorage(env);
|
|
1790
|
+
if (!cache.active && !localNoticePrinted) {
|
|
1791
|
+
localNoticePrinted = true;
|
|
1792
|
+
if (typeof process !== "undefined")
|
|
1793
|
+
process.stderr.write(`${localEconomyNotice()}
|
|
1794
|
+
`);
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
return cache;
|
|
1798
|
+
}
|
|
1388
1799
|
// package.json
|
|
1389
1800
|
var package_default = {
|
|
1390
1801
|
name: "@hasna/economy",
|
|
1391
|
-
version: "0.
|
|
1392
|
-
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API
|
|
1802
|
+
version: "0.4.0",
|
|
1803
|
+
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",
|
|
1393
1804
|
repository: {
|
|
1394
1805
|
type: "git",
|
|
1395
1806
|
url: "git+https://github.com/hasna/economy.git"
|
|
@@ -1420,7 +1831,7 @@ var package_default = {
|
|
|
1420
1831
|
files: [
|
|
1421
1832
|
"dist",
|
|
1422
1833
|
"docs",
|
|
1423
|
-
"
|
|
1834
|
+
"postinstall.js",
|
|
1424
1835
|
"LICENSE",
|
|
1425
1836
|
"README.md",
|
|
1426
1837
|
"CHANGELOG.md",
|
|
@@ -1429,12 +1840,11 @@ var package_default = {
|
|
|
1429
1840
|
"CODE_OF_CONDUCT.md"
|
|
1430
1841
|
],
|
|
1431
1842
|
scripts: {
|
|
1432
|
-
build: "
|
|
1843
|
+
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",
|
|
1433
1844
|
"build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
|
|
1434
1845
|
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
|
|
1435
1846
|
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
1436
1847
|
"build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
1437
|
-
"build:dashboard": "cd dashboard && bun run build",
|
|
1438
1848
|
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
1439
1849
|
prepack: "bun run build && bun run artifact-scan",
|
|
1440
1850
|
typecheck: "tsc --noEmit",
|
|
@@ -1443,7 +1853,7 @@ var package_default = {
|
|
|
1443
1853
|
"dev:mcp": "bun run src/mcp/index.ts",
|
|
1444
1854
|
"dev:serve": "bun run src/server/index.ts",
|
|
1445
1855
|
"dev:otel": "bun run src/otel/index.ts",
|
|
1446
|
-
postinstall: "
|
|
1856
|
+
postinstall: "node postinstall.js"
|
|
1447
1857
|
},
|
|
1448
1858
|
keywords: [
|
|
1449
1859
|
"economy",
|
|
@@ -1464,11 +1874,8 @@ var package_default = {
|
|
|
1464
1874
|
access: "public"
|
|
1465
1875
|
},
|
|
1466
1876
|
dependencies: {
|
|
1467
|
-
"@hasna/
|
|
1468
|
-
"@hasna/agent-registry": "^0.1.0",
|
|
1469
|
-
"@hasna/contracts": "0.13.4",
|
|
1877
|
+
"@hasna/contracts": "1.0.2",
|
|
1470
1878
|
"@hasna/events": "^0.1.16",
|
|
1471
|
-
"@hasna/mcp-harness": "^0.1.0",
|
|
1472
1879
|
"@hasna/projects": "1.0.0",
|
|
1473
1880
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
1474
1881
|
chalk: "^5.4.1",
|
|
@@ -1477,7 +1884,7 @@ var package_default = {
|
|
|
1477
1884
|
zod: "^3.24.2"
|
|
1478
1885
|
},
|
|
1479
1886
|
devDependencies: {
|
|
1480
|
-
"@types/bun": "
|
|
1887
|
+
"@types/bun": "1.3.14",
|
|
1481
1888
|
"@types/pg": "^8.20.0",
|
|
1482
1889
|
"bun-types": "latest",
|
|
1483
1890
|
typescript: "^5.7.2"
|
|
@@ -1517,7 +1924,7 @@ if (args.includes("--version") || args.includes("-V")) {
|
|
|
1517
1924
|
if (args.includes("--help") || args.includes("-h")) {
|
|
1518
1925
|
console.log(`Usage: economy-otel [options]
|
|
1519
1926
|
|
|
1520
|
-
OTLP/HTTP metrics sidecar \u2014 ingests *.cost.* / *.token.* metrics into
|
|
1927
|
+
OTLP/HTTP metrics sidecar \u2014 ingests *.cost.* / *.token.* metrics into Economy
|
|
1521
1928
|
|
|
1522
1929
|
Options:
|
|
1523
1930
|
-p, --port <port> Port to bind (default: ECONOMY_OTEL_PORT or 4318)
|
|
@@ -1528,6 +1935,13 @@ Environment:
|
|
|
1528
1935
|
ECONOMY_OTEL_PORT Override the default port (4318)
|
|
1529
1936
|
ECONOMY_OTEL_BIND Bind address (default: 127.0.0.1)
|
|
1530
1937
|
|
|
1938
|
+
Storage (decided at startup, before the listener binds):
|
|
1939
|
+
A credential resolved by @hasna/contracts (Keychain item
|
|
1940
|
+
hasna.credentials.economy.api-key, ~/.hasna/economy/config/credentials,
|
|
1941
|
+
HASNA_ECONOMY_API_KEY) forwards every accepted payload to the hosted
|
|
1942
|
+
API's /v1/ingest. HASNA_ECONOMY_LOCAL=1 writes the on-box SQLite store
|
|
1943
|
+
instead. With neither the sidecar fails closed (exit 1, no listener).
|
|
1944
|
+
|
|
1531
1945
|
Endpoints:
|
|
1532
1946
|
POST /v1/metrics OTLP JSON metrics
|
|
1533
1947
|
POST /ingest Simplified single-event JSON
|
|
@@ -1535,7 +1949,28 @@ Endpoints:
|
|
|
1535
1949
|
process.exit(0);
|
|
1536
1950
|
}
|
|
1537
1951
|
var port = resolvePort(args);
|
|
1538
|
-
|
|
1952
|
+
function resolveStorage() {
|
|
1953
|
+
try {
|
|
1954
|
+
return economyCloudStorage();
|
|
1955
|
+
} catch (error) {
|
|
1956
|
+
console.error(`economy-otel: ${error instanceof Error ? error.message : String(error)}`);
|
|
1957
|
+
process.exit(1);
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
var storage = resolveStorage();
|
|
1961
|
+
var localDb = storage.active ? null : openDatabase();
|
|
1962
|
+
async function forwardRows(cloud, rows) {
|
|
1963
|
+
const scratch = openDatabase(":memory:", true);
|
|
1964
|
+
try {
|
|
1965
|
+
const result = await ingestOtelRows(scratch, rows);
|
|
1966
|
+
const pushed = await pushIngestRows(cloud, scratch);
|
|
1967
|
+
return { ...result, forwarded: pushed.total };
|
|
1968
|
+
} finally {
|
|
1969
|
+
try {
|
|
1970
|
+
scratch.close();
|
|
1971
|
+
} catch {}
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1539
1974
|
var server = Bun.serve({
|
|
1540
1975
|
port,
|
|
1541
1976
|
hostname: process.env["ECONOMY_OTEL_BIND"] ?? "127.0.0.1",
|
|
@@ -1560,7 +1995,16 @@ var server = Bun.serve({
|
|
|
1560
1995
|
if (rows.length === 0) {
|
|
1561
1996
|
return Response.json({ ingested: 0, message: "no matching metrics" });
|
|
1562
1997
|
}
|
|
1563
|
-
|
|
1998
|
+
if (storage.active) {
|
|
1999
|
+
try {
|
|
2000
|
+
const result2 = await forwardRows(storage, rows);
|
|
2001
|
+
return Response.json({ ingested: result2.requests, sessions: result2.sessions, forwarded: result2.forwarded });
|
|
2002
|
+
} catch (error) {
|
|
2003
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2004
|
+
return Response.json({ error: `forward to the economy API failed: ${reason}` }, { status: 502 });
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
const result = await ingestOtelRows(localDb, rows);
|
|
1564
2008
|
return Response.json({ ingested: result.requests, sessions: result.sessions });
|
|
1565
2009
|
}
|
|
1566
2010
|
});
|