@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/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
|
@@ -595,7 +595,59 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
|
595
595
|
import { hostname, platform } from "os";
|
|
596
596
|
import { homedir } from "os";
|
|
597
597
|
import { join, resolve } from "path";
|
|
598
|
-
import {
|
|
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
|
+
}
|
|
599
651
|
function normalizeMachineId(value) {
|
|
600
652
|
const id = value?.trim().toLowerCase().split(".")[0];
|
|
601
653
|
return id && id.length > 0 ? id : null;
|
|
@@ -618,7 +670,7 @@ function macHostMachineId() {
|
|
|
618
670
|
return null;
|
|
619
671
|
}
|
|
620
672
|
function getMachineId() {
|
|
621
|
-
const envMachine = normalizeMachineId(process.env["
|
|
673
|
+
const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
|
|
622
674
|
if (envMachine)
|
|
623
675
|
return envMachine;
|
|
624
676
|
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
@@ -1067,8 +1119,16 @@ function seedModelPricing(db, defaults) {
|
|
|
1067
1119
|
});
|
|
1068
1120
|
}
|
|
1069
1121
|
}
|
|
1122
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
|
|
1070
1123
|
var init_database = __esm(() => {
|
|
1071
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]+)*$/;
|
|
1072
1132
|
});
|
|
1073
1133
|
|
|
1074
1134
|
// src/otel/index.ts
|
|
@@ -1413,11 +1473,334 @@ async function ingestOtelRows(db, rows) {
|
|
|
1413
1473
|
rollupSession(db, sessionId);
|
|
1414
1474
|
return { requests, sessions: sessions.size };
|
|
1415
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
|
+
}
|
|
1416
1799
|
// package.json
|
|
1417
1800
|
var package_default = {
|
|
1418
1801
|
name: "@hasna/economy",
|
|
1419
|
-
version: "0.
|
|
1420
|
-
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",
|
|
1421
1804
|
repository: {
|
|
1422
1805
|
type: "git",
|
|
1423
1806
|
url: "git+https://github.com/hasna/economy.git"
|
|
@@ -1448,7 +1831,6 @@ var package_default = {
|
|
|
1448
1831
|
files: [
|
|
1449
1832
|
"dist",
|
|
1450
1833
|
"docs",
|
|
1451
|
-
"dashboard/dist",
|
|
1452
1834
|
"postinstall.js",
|
|
1453
1835
|
"LICENSE",
|
|
1454
1836
|
"README.md",
|
|
@@ -1458,12 +1840,11 @@ var package_default = {
|
|
|
1458
1840
|
"CODE_OF_CONDUCT.md"
|
|
1459
1841
|
],
|
|
1460
1842
|
scripts: {
|
|
1461
|
-
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",
|
|
1462
1844
|
"build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
|
|
1463
1845
|
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
|
|
1464
1846
|
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
1465
1847
|
"build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
1466
|
-
"build:dashboard": "cd dashboard && bun run build",
|
|
1467
1848
|
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
1468
1849
|
prepack: "bun run build && bun run artifact-scan",
|
|
1469
1850
|
typecheck: "tsc --noEmit",
|
|
@@ -1493,12 +1874,8 @@ var package_default = {
|
|
|
1493
1874
|
access: "public"
|
|
1494
1875
|
},
|
|
1495
1876
|
dependencies: {
|
|
1496
|
-
"@hasna/
|
|
1497
|
-
"@hasna/agent-registry": "^0.1.0",
|
|
1498
|
-
"@hasna/contracts": "0.13.4",
|
|
1877
|
+
"@hasna/contracts": "1.0.2",
|
|
1499
1878
|
"@hasna/events": "^0.1.16",
|
|
1500
|
-
"@hasna/mcp-harness": "^0.1.0",
|
|
1501
|
-
"@hasna/paths": "0.1.0",
|
|
1502
1879
|
"@hasna/projects": "1.0.0",
|
|
1503
1880
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
1504
1881
|
chalk: "^5.4.1",
|
|
@@ -1507,7 +1884,7 @@ var package_default = {
|
|
|
1507
1884
|
zod: "^3.24.2"
|
|
1508
1885
|
},
|
|
1509
1886
|
devDependencies: {
|
|
1510
|
-
"@types/bun": "
|
|
1887
|
+
"@types/bun": "1.3.14",
|
|
1511
1888
|
"@types/pg": "^8.20.0",
|
|
1512
1889
|
"bun-types": "latest",
|
|
1513
1890
|
typescript: "^5.7.2"
|
|
@@ -1547,7 +1924,7 @@ if (args.includes("--version") || args.includes("-V")) {
|
|
|
1547
1924
|
if (args.includes("--help") || args.includes("-h")) {
|
|
1548
1925
|
console.log(`Usage: economy-otel [options]
|
|
1549
1926
|
|
|
1550
|
-
OTLP/HTTP metrics sidecar \u2014 ingests *.cost.* / *.token.* metrics into
|
|
1927
|
+
OTLP/HTTP metrics sidecar \u2014 ingests *.cost.* / *.token.* metrics into Economy
|
|
1551
1928
|
|
|
1552
1929
|
Options:
|
|
1553
1930
|
-p, --port <port> Port to bind (default: ECONOMY_OTEL_PORT or 4318)
|
|
@@ -1558,6 +1935,13 @@ Environment:
|
|
|
1558
1935
|
ECONOMY_OTEL_PORT Override the default port (4318)
|
|
1559
1936
|
ECONOMY_OTEL_BIND Bind address (default: 127.0.0.1)
|
|
1560
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
|
+
|
|
1561
1945
|
Endpoints:
|
|
1562
1946
|
POST /v1/metrics OTLP JSON metrics
|
|
1563
1947
|
POST /ingest Simplified single-event JSON
|
|
@@ -1565,7 +1949,28 @@ Endpoints:
|
|
|
1565
1949
|
process.exit(0);
|
|
1566
1950
|
}
|
|
1567
1951
|
var port = resolvePort(args);
|
|
1568
|
-
|
|
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
|
+
}
|
|
1569
1974
|
var server = Bun.serve({
|
|
1570
1975
|
port,
|
|
1571
1976
|
hostname: process.env["ECONOMY_OTEL_BIND"] ?? "127.0.0.1",
|
|
@@ -1590,7 +1995,16 @@ var server = Bun.serve({
|
|
|
1590
1995
|
if (rows.length === 0) {
|
|
1591
1996
|
return Response.json({ ingested: 0, message: "no matching metrics" });
|
|
1592
1997
|
}
|
|
1593
|
-
|
|
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);
|
|
1594
2008
|
return Response.json({ ingested: result.requests, sessions: result.sessions });
|
|
1595
2009
|
}
|
|
1596
2010
|
});
|