@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +101 -0
  2. package/CONTRIBUTING.md +1 -1
  3. package/README.md +9 -16
  4. package/SECURITY.md +1 -1
  5. package/dist/cli/commands/extras.d.ts.map +1 -1
  6. package/dist/cli/index.js +1182 -862
  7. package/dist/db/cloud.d.ts +16 -8
  8. package/dist/db/cloud.d.ts.map +1 -1
  9. package/dist/db/database.d.ts +58 -0
  10. package/dist/db/database.d.ts.map +1 -1
  11. package/dist/index.js +236 -27
  12. package/dist/ingest/claude.d.ts.map +1 -1
  13. package/dist/ingest/loops.d.ts +19 -0
  14. package/dist/ingest/loops.d.ts.map +1 -1
  15. package/dist/lib/accounts-store.d.ts +83 -0
  16. package/dist/lib/accounts-store.d.ts.map +1 -0
  17. package/dist/lib/accounts.d.ts +2 -1
  18. package/dist/lib/accounts.d.ts.map +1 -1
  19. package/dist/lib/api-display-url.d.ts +19 -0
  20. package/dist/lib/api-display-url.d.ts.map +1 -0
  21. package/dist/lib/autosync-gate.d.ts +15 -0
  22. package/dist/lib/autosync-gate.d.ts.map +1 -0
  23. package/dist/lib/cloud-ingest.d.ts +15 -1
  24. package/dist/lib/cloud-ingest.d.ts.map +1 -1
  25. package/dist/lib/cloud-storage.d.ts +166 -10
  26. package/dist/lib/cloud-storage.d.ts.map +1 -1
  27. package/dist/lib/gatherer.d.ts.map +1 -1
  28. package/dist/lib/model-config.d.ts +1 -1
  29. package/dist/lib/model-config.d.ts.map +1 -1
  30. package/dist/lib/serve-auth.d.ts.map +1 -1
  31. package/dist/lib/store/index.d.ts +6 -3
  32. package/dist/lib/store/index.d.ts.map +1 -1
  33. package/dist/lib/test-hermetic-accounts.d.ts +3 -2
  34. package/dist/lib/test-hermetic-accounts.d.ts.map +1 -1
  35. package/dist/lib/test-hermetic-fleet-env.d.ts +4 -0
  36. package/dist/lib/test-hermetic-fleet-env.d.ts.map +1 -0
  37. package/dist/mcp/agent-registry.d.ts +85 -0
  38. package/dist/mcp/agent-registry.d.ts.map +1 -0
  39. package/dist/mcp/harness.d.ts +26 -0
  40. package/dist/mcp/harness.d.ts.map +1 -0
  41. package/dist/mcp/index.js +1387 -311
  42. package/dist/mcp/server.d.ts.map +1 -1
  43. package/dist/otel/index.js +468 -24
  44. package/dist/server/index.js +595 -164
  45. package/dist/server/serve.d.ts +0 -2
  46. package/dist/server/serve.d.ts.map +1 -1
  47. package/docs/cli.md +2 -3
  48. package/docs/configuration.md +23 -10
  49. package/docs/ingestion.md +1 -1
  50. package/docs/mcp.md +4 -2
  51. package/docs/otel.md +8 -2
  52. package/docs/rest-api.md +2 -2
  53. package/package.json +7 -11
  54. package/postinstall.js +82 -0
  55. package/dashboard/README.md +0 -26
  56. package/dist/cli/brains.d.ts +0 -3
  57. package/dist/cli/brains.d.ts.map +0 -1
package/dist/mcp/index.js CHANGED
@@ -595,7 +595,63 @@ import { execFileSync } from "child_process";
595
595
  import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
596
596
  import { hostname, platform } from "os";
597
597
  import { homedir } from "os";
598
- import { join } from "path";
598
+ import { join, resolve } from "path";
599
+ import { homedir as pathsResolverHomedir } from "os";
600
+ import { join as pathsResolverJoin } from "path";
601
+ function pathsResolverAssertApp(app) {
602
+ if (typeof app !== "string" || app.length === 0) {
603
+ throw new TypeError("paths: app must be a non-empty string");
604
+ }
605
+ if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
606
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
607
+ }
608
+ }
609
+ function pathsResolverAssertKind(kind) {
610
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
611
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
612
+ }
613
+ }
614
+ function pathsResolverBaseDir(kind, options) {
615
+ pathsResolverAssertKind(kind);
616
+ const env = options.env ?? process.env;
617
+ const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
618
+ if (typeof override === "string" && override.length > 0)
619
+ return override;
620
+ const home = options.home ?? pathsResolverHomedir();
621
+ const platform2 = options.platform ?? process.platform;
622
+ if (platform2 === "darwin") {
623
+ switch (kind) {
624
+ case "config":
625
+ case "data":
626
+ return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
627
+ case "cache":
628
+ return pathsResolverJoin(home, "Library", "Caches", "Hasna");
629
+ case "state":
630
+ return pathsResolverJoin(home, "Library", "Logs", "Hasna");
631
+ }
632
+ }
633
+ switch (kind) {
634
+ case "config":
635
+ return pathsResolverJoin(home, ".config", "hasna");
636
+ case "data":
637
+ return pathsResolverJoin(home, ".local", "share", "hasna");
638
+ case "state":
639
+ return pathsResolverJoin(home, ".local", "state", "hasna");
640
+ case "cache":
641
+ return pathsResolverJoin(home, ".cache", "hasna");
642
+ }
643
+ }
644
+ function pathsResolverResolve(kind, options) {
645
+ pathsResolverAssertApp(options.app);
646
+ const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
647
+ return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
648
+ }
649
+ function dataDir(options) {
650
+ return pathsResolverResolve("data", options);
651
+ }
652
+ function cacheDir(options) {
653
+ return pathsResolverResolve("cache", options);
654
+ }
599
655
  function normalizeMachineId(value) {
600
656
  const id = value?.trim().toLowerCase().split(".")[0];
601
657
  return id && id.length > 0 ? id : null;
@@ -618,7 +674,7 @@ function macHostMachineId() {
618
674
  return null;
619
675
  }
620
676
  function getMachineId() {
621
- const envMachine = normalizeMachineId(process.env["ECONOMY_MACHINE_ID"]);
677
+ const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
622
678
  if (envMachine)
623
679
  return envMachine;
624
680
  const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
@@ -626,21 +682,48 @@ function getMachineId() {
626
682
  return macHostMachineId() ?? hostMachine;
627
683
  return hostMachine;
628
684
  }
629
- function getDataDir() {
685
+ function getHomeDir() {
630
686
  const home = process.env["HOME"] || process.env["USERPROFILE"] || homedir();
631
- const newDir = join(home, ".hasna", "economy");
632
- const oldDir = join(home, ".economy");
633
- if (existsSync(oldDir) && !existsSync(newDir)) {
634
- mkdirSync(newDir, { recursive: true });
687
+ if (!home)
688
+ throw new Error("Could not resolve the user home directory");
689
+ return home;
690
+ }
691
+ function getResolverDataRoot() {
692
+ return dataDir({
693
+ app: "economy",
694
+ home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
695
+ });
696
+ }
697
+ function getLegacyDataRoot() {
698
+ return join(getHomeDir(), ".hasna", "economy");
699
+ }
700
+ function adoptResolverDataRoot(resolved, env = process.env) {
701
+ const dataOverride = env.HASNA_DATA_HOME;
702
+ if (typeof dataOverride === "string" && dataOverride.trim().length > 0)
703
+ return true;
704
+ return existsSync(join(resolved, "economy.db"));
705
+ }
706
+ function getExactDataRoot() {
707
+ const dir = process.env["HASNA_ECONOMY_HOME"] ?? process.env["ECONOMY_HOME"];
708
+ if (dir && dir.trim())
709
+ return dir.trim();
710
+ return;
711
+ }
712
+ function getDataDir() {
713
+ const exact = getExactDataRoot();
714
+ const effective = exact ? resolve(exact) : adoptResolverDataRoot(getResolverDataRoot()) ? resolve(getResolverDataRoot()) : getLegacyDataRoot();
715
+ const oldDir = join(getHomeDir(), ".economy");
716
+ if (existsSync(oldDir) && !existsSync(effective)) {
717
+ mkdirSync(effective, { recursive: true });
635
718
  for (const file of readdirSync(oldDir)) {
636
719
  const oldPath = join(oldDir, file);
637
720
  if (statSync(oldPath).isFile()) {
638
- copyFileSync(oldPath, join(newDir, file));
721
+ copyFileSync(oldPath, join(effective, file));
639
722
  }
640
723
  }
641
724
  }
642
- mkdirSync(newDir, { recursive: true });
643
- return newDir;
725
+ mkdirSync(effective, { recursive: true });
726
+ return effective;
644
727
  }
645
728
  function getDbPath() {
646
729
  if (process.env["HASNA_ECONOMY_DB_PATH"])
@@ -1901,6 +1984,10 @@ function getIngestState(db, source, key) {
1901
1984
  const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
1902
1985
  return row?.value ?? null;
1903
1986
  }
1987
+ function loadIngestState(db, source) {
1988
+ const rows = db.prepare(`SELECT key, value FROM ingest_state WHERE source = ?`).all(source);
1989
+ return new Map(rows.map((row) => [row.key, row.value]));
1990
+ }
1904
1991
  function setIngestState(db, source, key, value) {
1905
1992
  db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
1906
1993
  }
@@ -2076,8 +2163,16 @@ function dedupeRequests(db) {
2076
2163
  }
2077
2164
  return removed;
2078
2165
  }
2166
+ var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
2079
2167
  var init_database = __esm(() => {
2080
2168
  init_sqlite_adapter();
2169
+ PATHS_RESOLVER_KIND_ENV = {
2170
+ config: "HASNA_CONFIG_HOME",
2171
+ data: "HASNA_DATA_HOME",
2172
+ state: "HASNA_STATE_HOME",
2173
+ cache: "HASNA_CACHE_HOME"
2174
+ };
2175
+ PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
2081
2176
  });
2082
2177
 
2083
2178
  // src/mcp/index.ts
@@ -2085,8 +2180,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
2085
2180
  // package.json
2086
2181
  var package_default = {
2087
2182
  name: "@hasna/economy",
2088
- version: "0.3.27",
2089
- description: "AI coding cost tracker \u2014 CLI + MCP server + REST API + web dashboard for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
2183
+ version: "0.4.0",
2184
+ 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",
2090
2185
  repository: {
2091
2186
  type: "git",
2092
2187
  url: "git+https://github.com/hasna/economy.git"
@@ -2117,7 +2212,7 @@ var package_default = {
2117
2212
  files: [
2118
2213
  "dist",
2119
2214
  "docs",
2120
- "dashboard/dist",
2215
+ "postinstall.js",
2121
2216
  "LICENSE",
2122
2217
  "README.md",
2123
2218
  "CHANGELOG.md",
@@ -2126,12 +2221,11 @@ var package_default = {
2126
2221
  "CODE_OF_CONDUCT.md"
2127
2222
  ],
2128
2223
  scripts: {
2129
- build: "cd dashboard && bun install --frozen-lockfile && bun run build && cd .. && 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",
2224
+ 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",
2130
2225
  "build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
2131
2226
  "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
2132
2227
  "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
2133
2228
  "build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
2134
- "build:dashboard": "cd dashboard && bun run build",
2135
2229
  "artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
2136
2230
  prepack: "bun run build && bun run artifact-scan",
2137
2231
  typecheck: "tsc --noEmit",
@@ -2140,7 +2234,7 @@ var package_default = {
2140
2234
  "dev:mcp": "bun run src/mcp/index.ts",
2141
2235
  "dev:serve": "bun run src/server/index.ts",
2142
2236
  "dev:otel": "bun run src/otel/index.ts",
2143
- postinstall: "mkdir -p $HOME/.hasna/economy/training 2>/dev/null || true"
2237
+ postinstall: "node postinstall.js"
2144
2238
  },
2145
2239
  keywords: [
2146
2240
  "economy",
@@ -2161,11 +2255,8 @@ var package_default = {
2161
2255
  access: "public"
2162
2256
  },
2163
2257
  dependencies: {
2164
- "@hasna/accounts": "0.2.23",
2165
- "@hasna/agent-registry": "^0.1.0",
2166
- "@hasna/contracts": "0.13.4",
2258
+ "@hasna/contracts": "1.0.2",
2167
2259
  "@hasna/events": "^0.1.16",
2168
- "@hasna/mcp-harness": "^0.1.0",
2169
2260
  "@hasna/projects": "1.0.0",
2170
2261
  "@modelcontextprotocol/sdk": "^1.12.1",
2171
2262
  chalk: "^5.4.1",
@@ -2174,7 +2265,7 @@ var package_default = {
2174
2265
  zod: "^3.24.2"
2175
2266
  },
2176
2267
  devDependencies: {
2177
- "@types/bun": "latest",
2268
+ "@types/bun": "1.3.14",
2178
2269
  "@types/pg": "^8.20.0",
2179
2270
  "bun-types": "latest",
2180
2271
  typescript: "^5.7.2"
@@ -2198,17 +2289,572 @@ function getPackageMetadata() {
2198
2289
  var packageMetadata = getPackageMetadata();
2199
2290
 
2200
2291
  // src/mcp/server.ts
2201
- init_database();
2202
2292
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2203
- import { registerAgentTools } from "@hasna/agent-registry";
2293
+
2294
+ // src/mcp/agent-registry.ts
2295
+ init_database();
2296
+ import { Database as Database2 } from "bun:sqlite";
2297
+ import { dirname, join as join2 } from "path";
2298
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
2204
2299
  import { z } from "zod";
2205
2300
 
2301
+ // src/lib/cloud-storage.ts
2302
+ import {
2303
+ appConfigDiskValue,
2304
+ ClientTransportConfigurationError,
2305
+ clientTransportEnvKeys,
2306
+ createClientTransport,
2307
+ credentialDiskSources,
2308
+ credentialOverrideEnvKey,
2309
+ credentialPointerEnvKey,
2310
+ CREDENTIAL_PROFILE_ENV_KEY,
2311
+ keychainConfigValue
2312
+ } from "@hasna/contracts/client";
2313
+ import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
2314
+ var ECONOMY_APP = "economy";
2315
+ var RETIRED_STORAGE_MODE_KEYS = [
2316
+ "HASNA_ECONOMY_STORAGE_MODE",
2317
+ "HASNA_ECONOMY_MODE",
2318
+ "ECONOMY_STORAGE_MODE",
2319
+ "ECONOMY_MODE"
2320
+ ];
2321
+ function assertNoRetiredStorageMode(env) {
2322
+ const legacyKey = RETIRED_STORAGE_MODE_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
2323
+ if (!legacyKey)
2324
+ return;
2325
+ 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.`);
2326
+ }
2327
+ var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
2328
+ function envFlagSet(env, key) {
2329
+ const raw = env[key];
2330
+ if (raw === undefined)
2331
+ return false;
2332
+ const value = raw.trim().toLowerCase();
2333
+ return value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
2334
+ }
2335
+ function localStorageExplicitlyOptedIn(env) {
2336
+ return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => envFlagSet(env, key));
2337
+ }
2338
+ function economyAuthorityEnvKeys() {
2339
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
2340
+ return [
2341
+ ...keys.apiUrlKeys,
2342
+ ...keys.apiKeyKeys,
2343
+ credentialOverrideEnvKey(ECONOMY_APP),
2344
+ credentialPointerEnvKey(ECONOMY_APP),
2345
+ CREDENTIAL_PROFILE_ENV_KEY
2346
+ ];
2347
+ }
2348
+ function hasEconomyEnvAuthorityIntent(env) {
2349
+ return economyAuthorityEnvKeys().some((key) => (env[key] ?? "").trim().length > 0);
2350
+ }
2351
+ function hostedAuthorityConfigured(env, options = {}) {
2352
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
2353
+ if (keys.apiUrlKeys.some((key) => (env[key] ?? "").trim().length > 0))
2354
+ return true;
2355
+ if (keychainConfigValue(ECONOMY_APP, env, options.credentials?.keychain) !== null)
2356
+ return true;
2357
+ return appConfigDiskValue(ECONOMY_APP, env, keys.apiUrlKeys) !== null;
2358
+ }
2359
+ function economyResolverEnv(env) {
2360
+ const blanks = economyAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
2361
+ if (blanks.length === 0)
2362
+ return env;
2363
+ const next = { ...env };
2364
+ for (const key of blanks)
2365
+ delete next[key];
2366
+ return next;
2367
+ }
2368
+ var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
2369
+ function isAmbientEconomyEnv(env) {
2370
+ if (typeof process !== "undefined" && env === process.env)
2371
+ return true;
2372
+ return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
2373
+ }
2374
+ function economyResolverInputs(env, credentials = {}) {
2375
+ const normalised = economyResolverEnv(env);
2376
+ if (normalised === env)
2377
+ return { env: normalised, credentials };
2378
+ const keychain = { ...credentials.keychain };
2379
+ if (keychain.enabled === undefined && keychain.run === undefined) {
2380
+ keychain.enabled = isAmbientEconomyEnv(env);
2381
+ }
2382
+ return { env: normalised, credentials: { ...credentials, keychain } };
2383
+ }
2384
+ function resolveEconomyStorageClient(name, env, overrides = {}) {
2385
+ const { env: resolverEnv, credentials } = economyResolverInputs(env, overrides.credentials);
2386
+ const wired = createClientTransport(name, resolverEnv, {
2387
+ credentials,
2388
+ ...overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
2389
+ ...overrides.headers ? { headers: overrides.headers } : {},
2390
+ ...overrides.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
2391
+ ...overrides.retry !== undefined ? { retry: overrides.retry } : {},
2392
+ ...overrides.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
2393
+ });
2394
+ return {
2395
+ transport: "http",
2396
+ client: createHasnaStorageClient(name, wired.client),
2397
+ resolution: wired.resolution
2398
+ };
2399
+ }
2400
+ function localEconomyNotice() {
2401
+ 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.`;
2402
+ }
2403
+ function credentialRequiredError(env, cause) {
2404
+ const keys = clientTransportEnvKeys(ECONOMY_APP);
2405
+ const clientEnv = env;
2406
+ 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.`;
2407
+ 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.`;
2408
+ 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);
2409
+ }
2410
+ var cache;
2411
+ var localNoticePrinted = false;
2412
+ function resolveEconomyCloudStorage(env = process.env, overrides) {
2413
+ assertNoRetiredStorageMode(env);
2414
+ const clientEnv = env;
2415
+ if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
2416
+ return { active: false, client: null };
2417
+ }
2418
+ let resolved;
2419
+ try {
2420
+ resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
2421
+ } catch (error) {
2422
+ if (error instanceof ClientTransportConfigurationError) {
2423
+ throw credentialRequiredError(env, error);
2424
+ }
2425
+ throw error;
2426
+ }
2427
+ return { active: true, client: resolved.client };
2428
+ }
2429
+ function economyCloudStorage(env = process.env) {
2430
+ if (cache === undefined) {
2431
+ cache = resolveEconomyCloudStorage(env);
2432
+ if (!cache.active && !localNoticePrinted) {
2433
+ localNoticePrinted = true;
2434
+ if (typeof process !== "undefined")
2435
+ process.stderr.write(`${localEconomyNotice()}
2436
+ `);
2437
+ }
2438
+ }
2439
+ return cache;
2440
+ }
2441
+ function cleanQuery(query) {
2442
+ if (!query)
2443
+ return;
2444
+ const out = {};
2445
+ for (const [k, v] of Object.entries(query)) {
2446
+ if (v !== undefined && v !== null && v !== "")
2447
+ out[k] = v;
2448
+ }
2449
+ return Object.keys(out).length > 0 ? out : undefined;
2450
+ }
2451
+ async function cloudListItems(storage, resource, query) {
2452
+ const cleaned = cleanQuery(query);
2453
+ const res = await storage.client.list(resource, cleaned ? { query: cleaned } : {});
2454
+ return res.items;
2455
+ }
2456
+ async function cloudObject(storage, path, query) {
2457
+ const cleaned = cleanQuery(query);
2458
+ const raw = await storage.client.transport.get(path, cleaned ? { query: cleaned } : {});
2459
+ if (raw && typeof raw === "object" && "data" in raw) {
2460
+ return raw.data;
2461
+ }
2462
+ return raw;
2463
+ }
2464
+
2465
+ // src/mcp/agent-registry.ts
2466
+ var SERVICE_NAME = "agent-registry";
2467
+ var DEFAULT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
2468
+ function envVar(suffix) {
2469
+ return process.env[`HASNA_AGENT_REGISTRY_${suffix}`] ?? process.env[`HASNA_AGENT_${suffix}`];
2470
+ }
2471
+ function getActiveWindowMs() {
2472
+ const env = envVar("TIMEOUT_MS");
2473
+ if (env) {
2474
+ const parsed = Number.parseInt(env, 10);
2475
+ if (Number.isFinite(parsed) && parsed > 0)
2476
+ return parsed;
2477
+ }
2478
+ return DEFAULT_ACTIVE_WINDOW_MS;
2479
+ }
2480
+ function envSessionId() {
2481
+ return envVar("SESSION_ID") ?? null;
2482
+ }
2483
+ var MEMORY_REGISTRY_PATH = ":memory:";
2484
+ function resolveRegistryDbPath() {
2485
+ const explicit = process.env.HASNA_AGENT_REGISTRY_DB_PATH;
2486
+ if (explicit && explicit.trim())
2487
+ return explicit;
2488
+ if (economyCloudStorage().active)
2489
+ return MEMORY_REGISTRY_PATH;
2490
+ return join2(getDataDir(), "agent-registry.db");
2491
+ }
2492
+ var defaultDb = null;
2493
+
2494
+ class RegistryDb {
2495
+ db;
2496
+ constructor(path) {
2497
+ if (path !== MEMORY_REGISTRY_PATH && dirname(path) && !existsSync2(dirname(path))) {
2498
+ mkdirSync2(dirname(path), { recursive: true, mode: 448 });
2499
+ }
2500
+ this.db = new Database2(path);
2501
+ this.db.exec("PRAGMA journal_mode = WAL");
2502
+ this.db.exec("PRAGMA busy_timeout = 10000");
2503
+ }
2504
+ get(sql, ...params) {
2505
+ return this.db.prepare(sql).get(...params);
2506
+ }
2507
+ all(sql, ...params) {
2508
+ return this.db.prepare(sql).all(...params);
2509
+ }
2510
+ run(sql, ...params) {
2511
+ return this.db.prepare(sql).run(...params);
2512
+ }
2513
+ exec(sql) {
2514
+ this.db.exec(sql);
2515
+ }
2516
+ close() {
2517
+ this.db.close();
2518
+ }
2519
+ }
2520
+ function getDefaultStore() {
2521
+ if (!defaultDb) {
2522
+ defaultDb = new RegistryDb(resolveRegistryDbPath());
2523
+ ensureAgentsTable(defaultDb);
2524
+ }
2525
+ return defaultDb;
2526
+ }
2527
+ var AGENTS_TABLE_SQL = `
2528
+ CREATE TABLE IF NOT EXISTS agents (
2529
+ id TEXT PRIMARY KEY,
2530
+ name TEXT NOT NULL,
2531
+ session_id TEXT,
2532
+ role TEXT NOT NULL DEFAULT 'agent',
2533
+ status TEXT NOT NULL DEFAULT 'active',
2534
+ active_project_id TEXT,
2535
+ working_dir TEXT,
2536
+ machine_id TEXT,
2537
+ capabilities TEXT NOT NULL DEFAULT '[]',
2538
+ metadata TEXT NOT NULL DEFAULT '{}',
2539
+ last_seen_at TEXT NOT NULL,
2540
+ created_at TEXT NOT NULL
2541
+ )`;
2542
+ var AGENTS_NAME_INDEX_SQL = `CREATE INDEX IF NOT EXISTS agents_name_idx ON agents(name)`;
2543
+ function ensureAgentsTable(db) {
2544
+ db.exec(AGENTS_TABLE_SQL);
2545
+ db.exec(AGENTS_NAME_INDEX_SQL);
2546
+ }
2547
+ function newAgentId() {
2548
+ return `ag_${crypto.randomUUID().slice(0, 8)}`;
2549
+ }
2550
+ function now() {
2551
+ return new Date().toISOString();
2552
+ }
2553
+ function normalizeName(name) {
2554
+ return name.trim().toLowerCase();
2555
+ }
2556
+ function rowToAgent(row) {
2557
+ return {
2558
+ id: String(row.id),
2559
+ name: String(row.name),
2560
+ session_id: row.session_id ?? null,
2561
+ role: row.role ?? "agent",
2562
+ status: row.status ?? "active",
2563
+ active_project_id: row.active_project_id ?? null,
2564
+ working_dir: row.working_dir ?? null,
2565
+ machine_id: row.machine_id ?? null,
2566
+ capabilities: safeParseArray(row.capabilities),
2567
+ metadata: safeParseObject(row.metadata),
2568
+ last_seen_at: String(row.last_seen_at),
2569
+ created_at: String(row.created_at)
2570
+ };
2571
+ }
2572
+ function safeParseArray(value) {
2573
+ if (!value)
2574
+ return [];
2575
+ try {
2576
+ const parsed = JSON.parse(String(value));
2577
+ return Array.isArray(parsed) ? parsed : [];
2578
+ } catch {
2579
+ return [];
2580
+ }
2581
+ }
2582
+ function safeParseObject(value) {
2583
+ if (!value)
2584
+ return {};
2585
+ try {
2586
+ const parsed = JSON.parse(String(value));
2587
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
2588
+ } catch {
2589
+ return {};
2590
+ }
2591
+ }
2592
+ function isActiveAt(lastSeenAt, windowMs) {
2593
+ return Date.now() - new Date(lastSeenAt).getTime() < windowMs;
2594
+ }
2595
+ function isAgentConflict(result) {
2596
+ return Boolean(result) && result.conflict === true;
2597
+ }
2598
+ function getAgent(id, db) {
2599
+ const row = db.get("SELECT * FROM agents WHERE id = ?", id);
2600
+ return row ? rowToAgent(row) : null;
2601
+ }
2602
+ function getAgentByName(name, db) {
2603
+ const row = db.get("SELECT * FROM agents WHERE name = ? AND status = 'active' ORDER BY last_seen_at DESC LIMIT 1", normalizeName(name));
2604
+ return row ? rowToAgent(row) : null;
2605
+ }
2606
+ function resolveAgent(idOrName, db) {
2607
+ const key = idOrName ?? envSessionId();
2608
+ if (!key)
2609
+ return null;
2610
+ return getAgentByName(key, db) ?? getAgent(key, db);
2611
+ }
2612
+ function listAgents(opts = {}, db) {
2613
+ const includeArchived = opts.include_archived ?? false;
2614
+ const where = includeArchived ? "" : "WHERE status = 'active'";
2615
+ const rows = db.all(`SELECT * FROM agents ${where} ORDER BY name`);
2616
+ let agents = rows.map(rowToAgent);
2617
+ if (opts.online_only) {
2618
+ const windowMs = getActiveWindowMs();
2619
+ agents = agents.filter((a) => a.status === "active" && isActiveAt(a.last_seen_at, windowMs));
2620
+ }
2621
+ return agents;
2622
+ }
2623
+ function registerAgent(input, db) {
2624
+ const name = normalizeName(input.name);
2625
+ if (!name) {
2626
+ throw new Error("register_agent: name is required");
2627
+ }
2628
+ const sessionId = input.session_id ?? envSessionId() ?? null;
2629
+ const windowMs = getActiveWindowMs();
2630
+ const existing = getAgentByName(name, db);
2631
+ if (existing) {
2632
+ const active = isActiveAt(existing.last_seen_at, windowMs);
2633
+ const sameSession = Boolean(sessionId) && Boolean(existing.session_id) && sessionId === existing.session_id;
2634
+ const differentSession = Boolean(sessionId) && Boolean(existing.session_id) && sessionId !== existing.session_id;
2635
+ const callerHasNoSession = !sessionId;
2636
+ const existingHasActiveSession = Boolean(existing.session_id) && active;
2637
+ if (!input.force) {
2638
+ if (active && differentSession)
2639
+ return buildConflict(existing);
2640
+ if (callerHasNoSession && existingHasActiveSession)
2641
+ return buildConflict(existing);
2642
+ }
2643
+ const sets = ["last_seen_at = ?"];
2644
+ const params = [now()];
2645
+ if (sessionId && !sameSession) {
2646
+ sets.push("session_id = ?");
2647
+ params.push(sessionId);
2648
+ }
2649
+ if (input.role !== undefined) {
2650
+ sets.push("role = ?");
2651
+ params.push(input.role);
2652
+ }
2653
+ if (input.working_dir !== undefined) {
2654
+ sets.push("working_dir = ?");
2655
+ params.push(input.working_dir);
2656
+ }
2657
+ if (input.machine_id !== undefined) {
2658
+ sets.push("machine_id = ?");
2659
+ params.push(input.machine_id);
2660
+ }
2661
+ if (input.project_id !== undefined) {
2662
+ sets.push("active_project_id = ?");
2663
+ params.push(input.project_id);
2664
+ }
2665
+ if (input.capabilities !== undefined) {
2666
+ sets.push("capabilities = ?");
2667
+ params.push(JSON.stringify(input.capabilities));
2668
+ }
2669
+ if (input.metadata !== undefined) {
2670
+ sets.push("metadata = ?");
2671
+ params.push(JSON.stringify(input.metadata));
2672
+ }
2673
+ sets.push("status = 'active'");
2674
+ params.push(existing.id);
2675
+ db.run(`UPDATE agents SET ${sets.join(", ")} WHERE id = ?`, ...params);
2676
+ return getAgent(existing.id, db);
2677
+ }
2678
+ const id = newAgentId();
2679
+ const ts = now();
2680
+ db.run(`INSERT INTO agents
2681
+ (id, name, session_id, role, status, active_project_id, working_dir, machine_id, capabilities, metadata, last_seen_at, created_at)
2682
+ VALUES (?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)`, id, name, sessionId, input.role ?? "agent", input.project_id ?? null, input.working_dir ?? null, input.machine_id ?? null, JSON.stringify(input.capabilities ?? []), JSON.stringify(input.metadata ?? {}), ts, ts);
2683
+ return getAgent(id, db);
2684
+ }
2685
+ function buildConflict(existing) {
2686
+ const minutesAgo = Math.round((Date.now() - new Date(existing.last_seen_at).getTime()) / 60000);
2687
+ return {
2688
+ conflict: true,
2689
+ existing_id: existing.id,
2690
+ existing_name: existing.name,
2691
+ last_seen_at: existing.last_seen_at,
2692
+ session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
2693
+ working_dir: existing.working_dir,
2694
+ message: `Agent "${existing.name}" is already active (last seen ${minutesAgo}m ago). ` + (existing.session_id ? `Pass session_id="${existing.session_id}" to reclaim it, ` : "") + "use force:true to take over, or choose a different name."
2695
+ };
2696
+ }
2697
+ function heartbeat(idOrName, db) {
2698
+ const agent = resolveAgent(idOrName, db);
2699
+ if (!agent)
2700
+ return null;
2701
+ db.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", now(), agent.id);
2702
+ return getAgent(agent.id, db);
2703
+ }
2704
+ function setFocus(idOrName, projectId, db) {
2705
+ const agent = resolveAgent(idOrName, db);
2706
+ if (!agent)
2707
+ return null;
2708
+ db.run("UPDATE agents SET active_project_id = ?, last_seen_at = ? WHERE id = ?", projectId, now(), agent.id);
2709
+ return getAgent(agent.id, db);
2710
+ }
2711
+ function agentData(agent) {
2712
+ return {
2713
+ agent_id: agent.id,
2714
+ session_id: agent.session_id,
2715
+ project_id: agent.active_project_id,
2716
+ machine_id: agent.machine_id,
2717
+ role: agent.role
2718
+ };
2719
+ }
2720
+ async function emitAgentEvent(client, type, agent, extra = {}) {
2721
+ if (!client)
2722
+ return;
2723
+ try {
2724
+ await client.emit({
2725
+ source: SERVICE_NAME,
2726
+ type,
2727
+ subject: agent.name,
2728
+ severity: type === "agent.heartbeat" ? "debug" : "info",
2729
+ data: { ...agentData(agent), ...extra }
2730
+ });
2731
+ } catch {}
2732
+ }
2733
+ async function emitConflictEvent(client, conflict) {
2734
+ if (!client)
2735
+ return;
2736
+ try {
2737
+ await client.emit({
2738
+ source: SERVICE_NAME,
2739
+ type: "agent.conflict",
2740
+ subject: conflict.existing_name,
2741
+ severity: "warning",
2742
+ data: {
2743
+ existing_id: conflict.existing_id,
2744
+ last_seen_at: conflict.last_seen_at,
2745
+ session_hint: conflict.session_hint
2746
+ },
2747
+ message: conflict.message
2748
+ });
2749
+ } catch {}
2750
+ }
2751
+ function jsonText(data) {
2752
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
2753
+ }
2754
+ function errorText(message) {
2755
+ return { content: [{ type: "text", text: message }], isError: true };
2756
+ }
2757
+ function registerAgentTools(server, opts = {}) {
2758
+ let resolved;
2759
+ const db = () => resolved ??= opts.db ?? getDefaultStore();
2760
+ const events = opts.events;
2761
+ const focus = opts.agentFocus;
2762
+ const includeExtended = opts.includeExtendedTools ?? false;
2763
+ const allow = (name) => opts.toolFilter ? opts.toolFilter(name) : true;
2764
+ const tool = (name, description, schema, handler) => {
2765
+ if (!allow(name))
2766
+ return;
2767
+ server.tool(name, description, schema, handler);
2768
+ };
2769
+ tool("register_agent", "Register or heartbeat an agent by name. Returns the agent, or a structured conflict descriptor if the name is actively held by another session.", {
2770
+ name: z.string().describe("Agent name (normalized to lowercase; unique among active agents)"),
2771
+ session_id: z.string().optional().describe("Stable session id used to reclaim the name"),
2772
+ role: z.string().optional().describe("Agent role (default: agent)"),
2773
+ working_dir: z.string().optional().describe("Working directory of the agent"),
2774
+ project_id: z.string().optional().describe("Set active project focus on register"),
2775
+ capabilities: z.array(z.string()).optional().describe("Declared capabilities"),
2776
+ force: z.boolean().optional().describe("Take over the name even if actively held")
2777
+ }, async (args) => {
2778
+ const result = registerAgent({
2779
+ name: args.name,
2780
+ session_id: args.session_id,
2781
+ role: args.role,
2782
+ working_dir: args.working_dir,
2783
+ project_id: args.project_id,
2784
+ capabilities: args.capabilities,
2785
+ force: args.force
2786
+ }, db());
2787
+ if (isAgentConflict(result)) {
2788
+ await emitConflictEvent(events, result);
2789
+ return { ...jsonText(result), isError: true };
2790
+ }
2791
+ await emitAgentEvent(events, "agent.registered", result);
2792
+ return jsonText(result);
2793
+ });
2794
+ tool("heartbeat", "Update an agent's last_seen_at to signal it is still active.", {
2795
+ agent_id: z.string().optional().describe("Agent id"),
2796
+ name: z.string().optional().describe("Agent name (alternative to agent_id)")
2797
+ }, async (args) => {
2798
+ const agent = heartbeat(args.agent_id ?? args.name, db());
2799
+ if (!agent)
2800
+ return errorText(`Agent not found: ${args.agent_id ?? args.name ?? "(none)"}`);
2801
+ await emitAgentEvent(events, "agent.heartbeat", agent);
2802
+ return jsonText(agent);
2803
+ });
2804
+ tool("set_focus", "Set (or clear) the active project context for an agent. Omit project_id to clear.", {
2805
+ agent_id: z.string().optional().describe("Agent id"),
2806
+ name: z.string().optional().describe("Agent name (alternative to agent_id)"),
2807
+ project_id: z.string().optional().describe("Project id to focus on; omit to clear")
2808
+ }, async (args) => {
2809
+ const projectId = args.project_id ?? null;
2810
+ const agent = setFocus(args.agent_id ?? args.name, projectId, db());
2811
+ if (!agent)
2812
+ return errorText(`Agent not found: ${args.agent_id ?? args.name ?? "(none)"}`);
2813
+ focus?.set(agent.id, { project_id: projectId });
2814
+ await emitAgentEvent(events, "agent.focus_changed", agent, { project_id: projectId });
2815
+ return jsonText(agent);
2816
+ });
2817
+ tool("list_agents", "List registered agents.", {
2818
+ online_only: z.boolean().optional().describe("Only agents seen within the active window"),
2819
+ include_archived: z.boolean().optional().describe("Include archived agents")
2820
+ }, async (args) => {
2821
+ const agents = listAgents({ online_only: args.online_only, include_archived: args.include_archived }, db());
2822
+ return jsonText(agents);
2823
+ });
2824
+ if (includeExtended) {
2825
+ tool("get_focus", "Get the active project context for an agent.", {
2826
+ agent_id: z.string().optional(),
2827
+ name: z.string().optional()
2828
+ }, async (args) => {
2829
+ const agent = resolveAgent(args.agent_id ?? args.name, db());
2830
+ if (!agent)
2831
+ return errorText(`Agent not found: ${args.agent_id ?? args.name ?? "(none)"}`);
2832
+ return jsonText({ agent_id: agent.id, project_id: agent.active_project_id });
2833
+ });
2834
+ tool("unfocus", "Clear the active project context for an agent.", {
2835
+ agent_id: z.string().optional(),
2836
+ name: z.string().optional()
2837
+ }, async (args) => {
2838
+ const agent = setFocus(args.agent_id ?? args.name, null, db());
2839
+ if (!agent)
2840
+ return errorText(`Agent not found: ${args.agent_id ?? args.name ?? "(none)"}`);
2841
+ focus?.set(agent.id, { project_id: null });
2842
+ await emitAgentEvent(events, "agent.focus_changed", agent, { project_id: null });
2843
+ return jsonText(agent);
2844
+ });
2845
+ }
2846
+ }
2847
+
2848
+ // src/mcp/server.ts
2849
+ init_database();
2850
+ import { z as z3 } from "zod";
2851
+
2206
2852
  // src/ingest/claude.ts
2207
2853
  init_database();
2208
2854
  init_pricing();
2209
- import { readdirSync as readdirSync2, readFileSync, existsSync as existsSync2, statSync as statSync2 } from "fs";
2210
- import { homedir as homedir2 } from "os";
2211
- import { join as join2, basename } from "path";
2855
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync4, statSync as statSync2 } from "fs";
2856
+ import { homedir as homedir3 } from "os";
2857
+ import { join as join4, basename } from "path";
2212
2858
 
2213
2859
  // src/lib/savings.ts
2214
2860
  function periodWhere2(period, column) {
@@ -2228,8 +2874,8 @@ function periodWhere2(period, column) {
2228
2874
  }
2229
2875
  }
2230
2876
  function prorateMonthlyFee(monthlyFee, period) {
2231
- const now = new Date;
2232
- const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
2877
+ const now2 = new Date;
2878
+ const daysInMonth = new Date(now2.getFullYear(), now2.getMonth() + 1, 0).getDate();
2233
2879
  switch (period) {
2234
2880
  case "today":
2235
2881
  case "yesterday":
@@ -2364,8 +3010,319 @@ function defaultCostBasisForAgent(agent) {
2364
3010
  return "estimated";
2365
3011
  }
2366
3012
 
3013
+ // src/lib/accounts-store.ts
3014
+ import { existsSync as existsSync3, readFileSync } from "fs";
3015
+ import { homedir as homedir2 } from "os";
3016
+ import { join as join3 } from "path";
3017
+ import { z as z2 } from "zod";
3018
+ import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
3019
+ var ACCOUNTS_APP = "accounts";
3020
+ var profileNameSchema = z2.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
3021
+ var profileSchema = z2.object({
3022
+ name: z2.string(),
3023
+ tool: z2.string(),
3024
+ email: z2.string().optional(),
3025
+ displayName: z2.string().optional(),
3026
+ dir: z2.string(),
3027
+ description: z2.string().optional(),
3028
+ createdAt: z2.string(),
3029
+ lastUsedAt: z2.string().optional()
3030
+ });
3031
+ var toolDefSchema = z2.object({
3032
+ id: z2.string(),
3033
+ label: z2.string(),
3034
+ envVar: z2.string(),
3035
+ defaultDir: z2.string(),
3036
+ bin: z2.string()
3037
+ });
3038
+ var storeSchema = z2.object({
3039
+ version: z2.literal(1),
3040
+ current: z2.record(z2.string(), z2.string()).default({}),
3041
+ applied: z2.record(z2.string(), z2.string()).default({}),
3042
+ toolLocks: z2.record(z2.string(), z2.string()).default({}),
3043
+ profiles: z2.array(profileSchema).default([]),
3044
+ tools: z2.array(toolDefSchema).default([])
3045
+ });
3046
+ function validateEnvPath(value, label) {
3047
+ const trimmed = value.trim();
3048
+ if (!trimmed || trimmed.includes("\x00") || /[\r\n]/.test(trimmed))
3049
+ throw new Error(`invalid ${label}`);
3050
+ return trimmed;
3051
+ }
3052
+ function accountsHome() {
3053
+ const override = process.env.ACCOUNTS_HOME;
3054
+ if (override && override.trim())
3055
+ return validateEnvPath(override, "ACCOUNTS_HOME");
3056
+ return join3(homedir2(), ".hasna", "accounts");
3057
+ }
3058
+ function storePath() {
3059
+ const override = process.env.ACCOUNTS_STORE_PATH;
3060
+ if (override && override.trim())
3061
+ return validateEnvPath(override, "ACCOUNTS_STORE_PATH");
3062
+ return join3(accountsHome(), "accounts.json");
3063
+ }
3064
+ var EMPTY_STORE = {
3065
+ version: 1,
3066
+ current: {},
3067
+ applied: {},
3068
+ toolLocks: {},
3069
+ profiles: [],
3070
+ tools: []
3071
+ };
3072
+ function parseStoreFile() {
3073
+ const path = storePath();
3074
+ if (!existsSync3(path))
3075
+ return structuredClone(EMPTY_STORE);
3076
+ let raw;
3077
+ try {
3078
+ raw = JSON.parse(readFileSync(path, "utf8"));
3079
+ } catch (err) {
3080
+ throw new Error(`could not parse store at ${path}: ${err instanceof Error ? err.message : String(err)}`);
3081
+ }
3082
+ const parsed = storeSchema.safeParse(raw);
3083
+ if (!parsed.success) {
3084
+ throw new Error(`invalid store at ${path}: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
3085
+ }
3086
+ return parsed.data;
3087
+ }
3088
+ function loadAppliedMap() {
3089
+ const applied = {};
3090
+ for (const [toolId, name] of Object.entries(parseStoreFile().applied)) {
3091
+ if (name && profileNameSchema.safeParse(name).success)
3092
+ applied[toolId] = name;
3093
+ }
3094
+ return applied;
3095
+ }
3096
+ function appliedProfileName(toolId) {
3097
+ return loadAppliedMap()[toolId];
3098
+ }
3099
+ var BUILTIN_TOOLS = [
3100
+ { id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join3(homedir2(), ".claude"), bin: "claude" },
3101
+ { id: "codex-app", label: "Codex App", envVar: "CODEX_HOME", defaultDir: join3(homedir2(), ".codex"), bin: "/Applications/Codex.app/Contents/MacOS/Codex" },
3102
+ { id: "codex", label: "Codex CLI", envVar: "CODEX_HOME", defaultDir: join3(homedir2(), ".codex"), bin: "codex" },
3103
+ { id: "codewith", label: "Codewith", envVar: "CODEWITH_HOME", defaultDir: join3(homedir2(), ".codewith"), bin: "codewith" },
3104
+ { id: "takumi", label: "Takumi", envVar: "TAKUMI_CONFIG_DIR", defaultDir: join3(homedir2(), ".takumi"), bin: "takumi" },
3105
+ { id: "gemini", label: "Gemini CLI", envVar: "GEMINI_CONFIG_DIR", defaultDir: join3(homedir2(), ".gemini"), bin: "gemini" },
3106
+ { id: "opencode", label: "opencode", envVar: "OPENCODE_CONFIG_DIR", defaultDir: join3(homedir2(), ".config", "opencode"), bin: "opencode" },
3107
+ { id: "cursor", label: "Cursor Agent", envVar: "CURSOR_CONFIG_DIR", defaultDir: join3(homedir2(), ".cursor"), bin: "cursor-agent" },
3108
+ { id: "pi", label: "Pi Coding Agent", envVar: "PI_CODING_AGENT_HOME", defaultDir: join3(homedir2(), ".pi"), bin: "pi" },
3109
+ { id: "hermes", label: "Hermes", envVar: "HERMES_HOME", defaultDir: join3(homedir2(), ".hermes"), bin: "hermes" },
3110
+ { id: "kimi", label: "Kimi Code", envVar: "KIMI_CODE_HOME", defaultDir: join3(homedir2(), ".kimi-code"), bin: "kimi" },
3111
+ { id: "grok", label: "Grok Build", envVar: "HOME", defaultDir: join3(homedir2(), ".grok"), bin: "grok" }
3112
+ ];
3113
+
3114
+ class LocalStore {
3115
+ transport = "local";
3116
+ async listProfiles(tool) {
3117
+ const profiles = parseStoreFile().profiles;
3118
+ const filtered = tool ? profiles.filter((profile) => profile.tool === tool) : profiles;
3119
+ return filtered.slice().sort((a, b) => a.tool.localeCompare(b.tool) || a.name.localeCompare(b.name));
3120
+ }
3121
+ async findProfile(name, tool) {
3122
+ const matches = parseStoreFile().profiles.filter((profile) => profile.name === name && (!tool || profile.tool === tool));
3123
+ return matches.length === 1 ? matches[0] : undefined;
3124
+ }
3125
+ async currentProfile(tool) {
3126
+ const store = parseStoreFile();
3127
+ const name = store.current[tool];
3128
+ if (!name)
3129
+ return;
3130
+ return store.profiles.find((profile) => profile.name === name && profile.tool === tool);
3131
+ }
3132
+ async listTools() {
3133
+ const custom = parseStoreFile().tools;
3134
+ const byId = new Map;
3135
+ for (const tool of BUILTIN_TOOLS)
3136
+ byId.set(tool.id, tool);
3137
+ for (const tool of custom)
3138
+ byId.set(tool.id, tool);
3139
+ return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
3140
+ }
3141
+ }
3142
+ var RETIRED_STORAGE_MODE_KEYS2 = [
3143
+ "HASNA_ACCOUNTS_STORAGE_MODE",
3144
+ "ACCOUNTS_STORAGE_MODE",
3145
+ "HASNA_ACCOUNTS_MODE",
3146
+ "ACCOUNTS_MODE"
3147
+ ];
3148
+ function assertNoRetiredStorageMode2(env) {
3149
+ const legacyKey = RETIRED_STORAGE_MODE_KEYS2.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
3150
+ if (!legacyKey)
3151
+ return;
3152
+ 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.`);
3153
+ }
3154
+ var RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
3155
+
3156
+ class AccountsApiError extends Error {
3157
+ status;
3158
+ body;
3159
+ constructor(method, path, status, body) {
3160
+ super(`accounts cloud request failed: ${method} ${path} -> ${status}`);
3161
+ this.name = "AccountsApiError";
3162
+ this.status = status;
3163
+ this.body = body;
3164
+ }
3165
+ }
3166
+ function isNotFound(err) {
3167
+ return Boolean(err && typeof err === "object" && err.status === 404);
3168
+ }
3169
+ function sleep(ms) {
3170
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
3171
+ }
3172
+ async function cloudRequest(baseUrl, apiKey, path, query) {
3173
+ const rel = path.startsWith("/") ? path : `/${path}`;
3174
+ const url = query && Object.keys(query).length > 0 ? `${baseUrl}${rel}?${new URLSearchParams(query).toString()}` : `${baseUrl}${rel}`;
3175
+ let lastError;
3176
+ for (let attempt = 0;attempt < 3; attempt += 1) {
3177
+ const controller = new AbortController;
3178
+ const timer = setTimeout(() => controller.abort(), 30000);
3179
+ let res;
3180
+ try {
3181
+ res = await fetch(url, {
3182
+ method: "GET",
3183
+ headers: {
3184
+ "x-api-key": apiKey,
3185
+ Authorization: `Bearer ${apiKey}`,
3186
+ Accept: "application/json"
3187
+ },
3188
+ signal: controller.signal
3189
+ });
3190
+ } catch (error) {
3191
+ lastError = error instanceof Error ? error : new Error(String(error));
3192
+ clearTimeout(timer);
3193
+ if (attempt < 2) {
3194
+ const backoff = Math.min(2000, 200 * 2 ** attempt);
3195
+ const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
3196
+ await sleep(backoff + jitter);
3197
+ }
3198
+ continue;
3199
+ } finally {
3200
+ clearTimeout(timer);
3201
+ }
3202
+ const text = await res.text();
3203
+ let parsed;
3204
+ if (text.length > 0) {
3205
+ try {
3206
+ parsed = JSON.parse(text);
3207
+ } catch {
3208
+ parsed = text;
3209
+ }
3210
+ }
3211
+ if (!res.ok) {
3212
+ lastError = new AccountsApiError("GET", rel, res.status, parsed);
3213
+ if (attempt < 2 && RETRY_STATUSES.includes(res.status)) {
3214
+ const backoff = Math.min(2000, 200 * 2 ** attempt);
3215
+ const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
3216
+ await sleep(backoff + jitter);
3217
+ continue;
3218
+ }
3219
+ break;
3220
+ }
3221
+ return parsed;
3222
+ }
3223
+ throw lastError;
3224
+ }
3225
+ function toProfile(account) {
3226
+ const profile = {
3227
+ name: String(account.name),
3228
+ tool: String(account.tool),
3229
+ dir: typeof account.dir === "string" ? account.dir : "",
3230
+ createdAt: String(account.createdAt)
3231
+ };
3232
+ if (typeof account.email === "string")
3233
+ profile.email = account.email;
3234
+ if (typeof account.displayName === "string")
3235
+ profile.displayName = account.displayName;
3236
+ if (typeof account.description === "string")
3237
+ profile.description = account.description;
3238
+ if (typeof account.lastUsedAt === "string")
3239
+ profile.lastUsedAt = account.lastUsedAt;
3240
+ return profile;
3241
+ }
3242
+
3243
+ class ApiStore {
3244
+ baseUrl;
3245
+ apiKey;
3246
+ transport = "api";
3247
+ constructor(baseUrl, apiKey) {
3248
+ this.baseUrl = baseUrl;
3249
+ this.apiKey = apiKey;
3250
+ }
3251
+ async listProfiles(tool) {
3252
+ const raw = await cloudRequest(this.baseUrl, this.apiKey, "/accounts", tool ? { tool } : undefined);
3253
+ const accounts = Array.isArray(raw?.accounts) ? raw.accounts : [];
3254
+ return accounts.map((account) => toProfile(account)).sort((a, b) => a.tool.localeCompare(b.tool) || a.name.localeCompare(b.name));
3255
+ }
3256
+ async findProfile(name, tool) {
3257
+ if (tool) {
3258
+ try {
3259
+ const account = await cloudRequest(this.baseUrl, this.apiKey, `/accounts/${encodeURIComponent(tool)}/${encodeURIComponent(name)}`);
3260
+ return account ? toProfile(account) : undefined;
3261
+ } catch (err) {
3262
+ if (isNotFound(err))
3263
+ return;
3264
+ throw err;
3265
+ }
3266
+ }
3267
+ const matches = (await this.listProfiles()).filter((profile) => profile.name === name);
3268
+ return matches.length === 1 ? matches[0] : undefined;
3269
+ }
3270
+ async currentProfile(tool) {
3271
+ let current;
3272
+ try {
3273
+ const raw = await cloudRequest(this.baseUrl, this.apiKey, `/current/${encodeURIComponent(tool)}`);
3274
+ current = raw && typeof raw === "object" ? raw : null;
3275
+ } catch (err) {
3276
+ if (isNotFound(err))
3277
+ return;
3278
+ throw err;
3279
+ }
3280
+ if (!current || typeof current.name !== "string")
3281
+ return;
3282
+ return this.findProfile(current.name, tool);
3283
+ }
3284
+ async listTools() {
3285
+ const raw = await cloudRequest(this.baseUrl, this.apiKey, "/tools");
3286
+ const tools = Array.isArray(raw?.tools) ? raw.tools : [];
3287
+ const custom = [];
3288
+ for (const item of tools) {
3289
+ if (item === null || typeof item !== "object")
3290
+ continue;
3291
+ const record = item;
3292
+ if (record.builtin !== false)
3293
+ continue;
3294
+ const parsed = toolDefSchema.safeParse(item);
3295
+ if (!parsed.success) {
3296
+ throw new Error(`invalid custom tool "${String(record.id)}" returned by accounts-serve: ` + parsed.error.issues.map((issue) => issue.message).join("; "));
3297
+ }
3298
+ custom.push(parsed.data);
3299
+ }
3300
+ const byId = new Map;
3301
+ for (const tool of BUILTIN_TOOLS)
3302
+ byId.set(tool.id, tool);
3303
+ for (const tool of custom)
3304
+ byId.set(tool.id, tool);
3305
+ return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
3306
+ }
3307
+ }
3308
+ function resolveStore(env = process.env, options = {}) {
3309
+ assertNoRetiredStorageMode2(env);
3310
+ const clientEnv = env;
3311
+ try {
3312
+ const credential = resolveCredential(ACCOUNTS_APP, clientEnv, options.credentials);
3313
+ if (credential === null)
3314
+ return new LocalStore;
3315
+ const resolution = resolveClientTransport(ACCOUNTS_APP, clientEnv, {
3316
+ credentials: credential ? { ...options.credentials, apiKey: credential.apiKey } : options.credentials
3317
+ });
3318
+ return new ApiStore(resolution.baseUrl, credential.apiKey);
3319
+ } catch (err) {
3320
+ console.warn(`accounts: API configuration unusable (${err instanceof Error ? err.message : String(err)}); using local store`);
3321
+ return new LocalStore;
3322
+ }
3323
+ }
3324
+
2367
3325
  // src/lib/accounts.ts
2368
- import { appliedProfileName, resolveStore } from "@hasna/accounts";
2369
3326
  var AGENT_ACCOUNT_TOOLS = {
2370
3327
  claude: ["claude"],
2371
3328
  takumi: ["takumi", "claude"],
@@ -2437,11 +3394,11 @@ async function profileForEnvDir(store, tool, env) {
2437
3394
  const normalized = normalizeDir(configuredDir);
2438
3395
  return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
2439
3396
  }
2440
- async function resolveAccountForAgent(agent, env = process.env) {
3397
+ async function resolveAccountForAgent(agent, env = process.env, options = {}) {
2441
3398
  const override = envOverride(agent, env);
2442
3399
  if (override)
2443
3400
  return override;
2444
- const store = resolveStore(env);
3401
+ const store = resolveStore(env, options);
2445
3402
  const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
2446
3403
  for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
2447
3404
  const tool = tools.get(toolId);
@@ -2479,8 +3436,8 @@ function withAccount(record, account) {
2479
3436
  function autoDetectProject(cwd, projects) {
2480
3437
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
2481
3438
  }
2482
- var CLAUDE_PROJECTS_DIR = join2(homedir2(), ".claude", "projects");
2483
- var TAKUMI_PROJECTS_DIR = join2(homedir2(), ".takumi", "projects");
3439
+ var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join4(homedir3(), ".claude", "projects");
3440
+ var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join4(homedir3(), ".takumi", "projects");
2484
3441
  function dirNameToPath(dirName) {
2485
3442
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
2486
3443
  }
@@ -2490,9 +3447,9 @@ function collectJsonlFiles(projectDir) {
2490
3447
  try {
2491
3448
  for (const entry of readdirSync2(dir, { withFileTypes: true })) {
2492
3449
  if (entry.isDirectory())
2493
- walk(join2(dir, entry.name));
3450
+ walk(join4(dir, entry.name));
2494
3451
  else if (entry.name.endsWith(".jsonl"))
2495
- files.push(join2(dir, entry.name));
3452
+ files.push(join4(dir, entry.name));
2496
3453
  }
2497
3454
  } catch {}
2498
3455
  }
@@ -2506,7 +3463,7 @@ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_D
2506
3463
  return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
2507
3464
  }
2508
3465
  async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
2509
- if (!existsSync2(projectsDir)) {
3466
+ if (!existsSync4(projectsDir)) {
2510
3467
  if (verbose)
2511
3468
  console.log(`${agentName} projects dir not found:`, projectsDir);
2512
3469
  return { files: 0, requests: 0, sessions: 0 };
@@ -2517,9 +3474,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2517
3474
  const touchedSessions = new Set;
2518
3475
  const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
2519
3476
  const account = await resolveAccountForAgent(agentName);
3477
+ const ingestState = loadIngestState(db, agentName);
2520
3478
  const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
2521
3479
  for (const projectDirEntry of projectDirs) {
2522
- const projectDirPath = join2(projectsDir, projectDirEntry.name);
3480
+ const projectDirPath = join4(projectsDir, projectDirEntry.name);
2523
3481
  const projectPath = dirNameToPath(projectDirEntry.name);
2524
3482
  const jsonlFiles = collectJsonlFiles(projectDirPath);
2525
3483
  for (const filePath of jsonlFiles) {
@@ -2530,12 +3488,12 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2530
3488
  } catch {
2531
3489
  continue;
2532
3490
  }
2533
- const processed = getIngestState(db, agentName, stateKey);
3491
+ const processed = ingestState.get(stateKey) ?? null;
2534
3492
  if (processed === fileMtime)
2535
3493
  continue;
2536
3494
  let lines;
2537
3495
  try {
2538
- lines = readFileSync(filePath, "utf-8").split(`
3496
+ lines = readFileSync2(filePath, "utf-8").split(`
2539
3497
  `).filter((l) => l.trim());
2540
3498
  } catch {
2541
3499
  continue;
@@ -2655,14 +3613,14 @@ function supportsClaudeDataResidencyPricing(model) {
2655
3613
  // src/ingest/codex.ts
2656
3614
  init_database();
2657
3615
  init_pricing();
2658
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
2659
- import { homedir as homedir3 } from "os";
2660
- import { join as join3, basename as basename2 } from "path";
3616
+ import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
3617
+ import { homedir as homedir4 } from "os";
3618
+ import { join as join5, basename as basename2 } from "path";
2661
3619
  import { Database as BunDatabase } from "bun:sqlite";
2662
- var DEFAULT_CODEX_DB_PATH = join3(homedir3(), ".codex", "state_5.sqlite");
2663
- var DEFAULT_CODEX_CONFIG_PATH = join3(homedir3(), ".codex", "config.toml");
2664
- var DEFAULT_CODEWITH_DB_PATH = join3(homedir3(), ".codewith", "state_5.sqlite");
2665
- var DEFAULT_CODEWITH_CONFIG_PATH = join3(homedir3(), ".codewith", "config.toml");
3620
+ var DEFAULT_CODEX_DB_PATH = join5(homedir4(), ".codex", "state_5.sqlite");
3621
+ var DEFAULT_CODEX_CONFIG_PATH = join5(homedir4(), ".codex", "config.toml");
3622
+ var DEFAULT_CODEWITH_DB_PATH = join5(homedir4(), ".codewith", "state_5.sqlite");
3623
+ var DEFAULT_CODEWITH_CONFIG_PATH = join5(homedir4(), ".codewith", "config.toml");
2666
3624
  var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
2667
3625
  function codexDbPath() {
2668
3626
  return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
@@ -2704,10 +3662,10 @@ function codexSources() {
2704
3662
  });
2705
3663
  }
2706
3664
  function readCodexModel(configPath = codexConfigPath()) {
2707
- if (!existsSync3(configPath))
3665
+ if (!existsSync5(configPath))
2708
3666
  return "gpt-5-codex";
2709
3667
  try {
2710
- const content = readFileSync2(configPath, "utf-8");
3668
+ const content = readFileSync3(configPath, "utf-8");
2711
3669
  const match = content.match(/^model\s*=\s*"([^"]+)"/m);
2712
3670
  return match?.[1] ?? "gpt-5-codex";
2713
3671
  } catch {
@@ -2745,12 +3703,12 @@ function openCodexDb(dbPath, verbose) {
2745
3703
  return null;
2746
3704
  }
2747
3705
  function readTokenEvents(rolloutPath) {
2748
- if (!rolloutPath || !existsSync3(rolloutPath))
3706
+ if (!rolloutPath || !existsSync5(rolloutPath))
2749
3707
  return [];
2750
3708
  const fallbackUsages = new Map;
2751
3709
  let fallbackTimestamp;
2752
3710
  let aggregate = null;
2753
- for (const line of readFileSync2(rolloutPath, "utf-8").split(`
3711
+ for (const line of readFileSync3(rolloutPath, "utf-8").split(`
2754
3712
  `)) {
2755
3713
  if (!line.trim())
2756
3714
  continue;
@@ -2826,7 +3784,7 @@ async function ingestCodex(db, verbose = false) {
2826
3784
  let requests = 0;
2827
3785
  const account = await resolveAccountForAgent("codex");
2828
3786
  for (const source of codexSources()) {
2829
- if (!existsSync3(source.dbPath)) {
3787
+ if (!existsSync5(source.dbPath)) {
2830
3788
  if (verbose)
2831
3789
  console.log(`${source.label} DB not found:`, source.dbPath);
2832
3790
  continue;
@@ -2907,11 +3865,11 @@ async function ingestCodex(db, verbose = false) {
2907
3865
  // src/ingest/gemini.ts
2908
3866
  init_database();
2909
3867
  init_pricing();
2910
- import { readdirSync as readdirSync3, readFileSync as readFileSync3, existsSync as existsSync4, statSync as statSync3 } from "fs";
2911
- import { homedir as homedir4 } from "os";
2912
- import { join as join4, basename as basename3 } from "path";
2913
- var DEFAULT_GEMINI_TMP_DIR = join4(homedir4(), ".gemini", "tmp");
2914
- var DEFAULT_GEMINI_HISTORY_DIR = join4(homedir4(), ".gemini", "history");
3868
+ import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync6, statSync as statSync3 } from "fs";
3869
+ import { homedir as homedir5 } from "os";
3870
+ import { join as join6, basename as basename3 } from "path";
3871
+ var DEFAULT_GEMINI_TMP_DIR = join6(homedir5(), ".gemini", "tmp");
3872
+ var DEFAULT_GEMINI_HISTORY_DIR = join6(homedir5(), ".gemini", "history");
2915
3873
  function geminiTmpDir() {
2916
3874
  return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
2917
3875
  }
@@ -2928,12 +3886,12 @@ function numberField(...values) {
2928
3886
  function listProjectDirs(...roots) {
2929
3887
  const dirs = new Set;
2930
3888
  for (const root of roots) {
2931
- if (!existsSync4(root))
3889
+ if (!existsSync6(root))
2932
3890
  continue;
2933
3891
  try {
2934
3892
  for (const entry of readdirSync3(root, { withFileTypes: true })) {
2935
3893
  if (entry.isDirectory())
2936
- dirs.add(join4(root, entry.name));
3894
+ dirs.add(join6(root, entry.name));
2937
3895
  }
2938
3896
  } catch {}
2939
3897
  }
@@ -2944,17 +3902,17 @@ function projectRoot(projectDir, chatData) {
2944
3902
  return chatData.projectPath;
2945
3903
  if (chatData.project_path)
2946
3904
  return chatData.project_path;
2947
- const rootFile = join4(projectDir, ".project_root");
3905
+ const rootFile = join6(projectDir, ".project_root");
2948
3906
  try {
2949
- if (existsSync4(rootFile))
2950
- return readFileSync3(rootFile, "utf-8").trim();
3907
+ if (existsSync6(rootFile))
3908
+ return readFileSync4(rootFile, "utf-8").trim();
2951
3909
  } catch {}
2952
3910
  return "";
2953
3911
  }
2954
3912
  async function ingestGemini(db, verbose) {
2955
3913
  const tmpDir = geminiTmpDir();
2956
3914
  const historyDir = geminiHistoryDir();
2957
- if (!existsSync4(tmpDir) && !existsSync4(historyDir)) {
3915
+ if (!existsSync6(tmpDir) && !existsSync6(historyDir)) {
2958
3916
  if (verbose)
2959
3917
  console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
2960
3918
  return { sessions: 0, requests: 0 };
@@ -2966,17 +3924,17 @@ async function ingestGemini(db, verbose) {
2966
3924
  const account = await resolveAccountForAgent("gemini");
2967
3925
  const projectDirs = listProjectDirs(tmpDir, historyDir);
2968
3926
  for (const projectDir of projectDirs) {
2969
- const chatsDir = join4(projectDir, "chats");
2970
- if (!existsSync4(chatsDir))
3927
+ const chatsDir = join6(projectDir, "chats");
3928
+ if (!existsSync6(chatsDir))
2971
3929
  continue;
2972
3930
  let chatFiles = [];
2973
3931
  try {
2974
- chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join4(chatsDir, f));
3932
+ chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
2975
3933
  } catch {
2976
3934
  continue;
2977
3935
  }
2978
3936
  for (const filePath of chatFiles) {
2979
- const stateKey = filePath.replace(homedir4(), "~");
3937
+ const stateKey = filePath.replace(homedir5(), "~");
2980
3938
  let fileMtime = "0";
2981
3939
  try {
2982
3940
  fileMtime = statSync3(filePath).mtimeMs.toString();
@@ -2988,7 +3946,7 @@ async function ingestGemini(db, verbose) {
2988
3946
  continue;
2989
3947
  let chatData;
2990
3948
  try {
2991
- chatData = JSON.parse(readFileSync3(filePath, "utf-8"));
3949
+ chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
2992
3950
  } catch {
2993
3951
  continue;
2994
3952
  }
@@ -3066,15 +4024,15 @@ async function ingestGemini(db, verbose) {
3066
4024
  // src/ingest/opencode.ts
3067
4025
  init_database();
3068
4026
  init_pricing();
3069
- import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
3070
- import { homedir as homedir5 } from "os";
3071
- import { join as join5 } from "path";
3072
- var OPENCODE_STORAGE = join5(homedir5(), ".local", "share", "opencode", "storage");
4027
+ import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
4028
+ import { homedir as homedir6 } from "os";
4029
+ import { join as join7 } from "path";
4030
+ var OPENCODE_STORAGE = join7(homedir6(), ".local", "share", "opencode", "storage");
3073
4031
  function walkJsonFiles(dir, acc = []) {
3074
- if (!existsSync5(dir))
4032
+ if (!existsSync7(dir))
3075
4033
  return acc;
3076
4034
  for (const entry of readdirSync4(dir, { withFileTypes: true })) {
3077
- const p = join5(dir, entry.name);
4035
+ const p = join7(dir, entry.name);
3078
4036
  if (entry.isDirectory())
3079
4037
  walkJsonFiles(p, acc);
3080
4038
  else if (entry.name.endsWith(".json"))
@@ -3094,12 +4052,12 @@ function parseSessionIdFromPath(filePath) {
3094
4052
  return null;
3095
4053
  }
3096
4054
  async function ingestOpenCode(db, verbose = false) {
3097
- const messageDir = join5(OPENCODE_STORAGE, "message");
4055
+ const messageDir = join7(OPENCODE_STORAGE, "message");
3098
4056
  const files = walkJsonFiles(messageDir);
3099
4057
  let requests = 0;
3100
4058
  const touched = new Set;
3101
4059
  const machineId = getMachineId();
3102
- const now = new Date().toISOString();
4060
+ const now2 = new Date().toISOString();
3103
4061
  const account = await resolveAccountForAgent("opencode");
3104
4062
  for (const file of files) {
3105
4063
  const mtime = statSync4(file).mtimeMs;
@@ -3109,7 +4067,7 @@ async function ingestOpenCode(db, verbose = false) {
3109
4067
  continue;
3110
4068
  let parsed;
3111
4069
  try {
3112
- parsed = JSON.parse(readFileSync4(file, "utf-8"));
4070
+ parsed = JSON.parse(readFileSync5(file, "utf-8"));
3113
4071
  } catch {
3114
4072
  continue;
3115
4073
  }
@@ -3145,7 +4103,7 @@ async function ingestOpenCode(db, verbose = false) {
3145
4103
  timestamp,
3146
4104
  source_request_id: sourceId,
3147
4105
  machine_id: machineId,
3148
- updated_at: now
4106
+ updated_at: now2
3149
4107
  }, account));
3150
4108
  requests++;
3151
4109
  if (!touched.has(sessionId)) {
@@ -3160,7 +4118,7 @@ async function ingestOpenCode(db, verbose = false) {
3160
4118
  total_tokens: 0,
3161
4119
  request_count: 0,
3162
4120
  machine_id: machineId,
3163
- updated_at: now
4121
+ updated_at: now2
3164
4122
  }, account));
3165
4123
  touched.add(sessionId);
3166
4124
  }
@@ -3206,7 +4164,7 @@ async function ingestCursor(db, verbose = false) {
3206
4164
  if (prev)
3207
4165
  return { requests: 0, snapshots: 0 };
3208
4166
  const machineId = getMachineId();
3209
- const now = new Date().toISOString();
4167
+ const now2 = new Date().toISOString();
3210
4168
  let snapshots = 0;
3211
4169
  const account = await resolveAccountForAgent("cursor");
3212
4170
  const usage = await cursorFetch("/api/usage", token);
@@ -3262,12 +4220,12 @@ async function ingestCursor(db, verbose = false) {
3262
4220
  project_path: "",
3263
4221
  project_name: "Cursor subscription",
3264
4222
  started_at: `${today}T00:00:00.000Z`,
3265
- ended_at: now,
4223
+ ended_at: now2,
3266
4224
  total_cost_usd: onDemand + included,
3267
4225
  total_tokens: 0,
3268
4226
  request_count: 1,
3269
4227
  machine_id: machineId,
3270
- updated_at: now
4228
+ updated_at: now2
3271
4229
  }, account));
3272
4230
  upsertRequest(db, withAccount({
3273
4231
  id: `cursor-${today}-${machineId}-usage`,
@@ -3281,14 +4239,14 @@ async function ingestCursor(db, verbose = false) {
3281
4239
  cost_usd: onDemand + included,
3282
4240
  cost_basis: "subscription_included",
3283
4241
  duration_ms: 0,
3284
- timestamp: now,
4242
+ timestamp: now2,
3285
4243
  source_request_id: today,
3286
4244
  machine_id: machineId,
3287
- updated_at: now
4245
+ updated_at: now2
3288
4246
  }, account));
3289
4247
  rollupSession(db, sessionId);
3290
4248
  }
3291
- setIngestState(db, "cursor", `sync-${today}`, now);
4249
+ setIngestState(db, "cursor", `sync-${today}`, now2);
3292
4250
  if (verbose)
3293
4251
  console.log(` cursor: on-demand $${onDemand.toFixed(2)}, included $${included.toFixed(2)}`);
3294
4252
  return { requests: onDemand + included > 0 ? 1 : 0, snapshots };
@@ -3296,15 +4254,15 @@ async function ingestCursor(db, verbose = false) {
3296
4254
 
3297
4255
  // src/ingest/pi.ts
3298
4256
  init_database();
3299
- import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
3300
- import { homedir as homedir6 } from "os";
3301
- import { join as join6 } from "path";
3302
- var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join6(homedir6(), ".pi", "agent", "sessions");
4257
+ import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
4258
+ import { homedir as homedir7 } from "os";
4259
+ import { join as join8 } from "path";
4260
+ var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join8(homedir7(), ".pi", "agent", "sessions");
3303
4261
  function walkSessions(dir, acc = []) {
3304
- if (!existsSync6(dir))
4262
+ if (!existsSync8(dir))
3305
4263
  return acc;
3306
4264
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
3307
- const p = join6(dir, entry.name);
4265
+ const p = join8(dir, entry.name);
3308
4266
  if (entry.isDirectory())
3309
4267
  walkSessions(p, acc);
3310
4268
  else if (entry.name.endsWith(".json"))
@@ -3317,7 +4275,7 @@ async function ingestPi(db, verbose = false) {
3317
4275
  let requests = 0;
3318
4276
  const touched = new Set;
3319
4277
  const machineId = getMachineId();
3320
- const now = new Date().toISOString();
4278
+ const now2 = new Date().toISOString();
3321
4279
  const account = await resolveAccountForAgent("pi");
3322
4280
  for (const file of files) {
3323
4281
  const mtime = statSync5(file).mtimeMs;
@@ -3326,7 +4284,7 @@ async function ingestPi(db, verbose = false) {
3326
4284
  continue;
3327
4285
  let data;
3328
4286
  try {
3329
- data = JSON.parse(readFileSync5(file, "utf-8"));
4287
+ data = JSON.parse(readFileSync6(file, "utf-8"));
3330
4288
  } catch {
3331
4289
  continue;
3332
4290
  }
@@ -3359,7 +4317,7 @@ async function ingestPi(db, verbose = false) {
3359
4317
  timestamp,
3360
4318
  source_request_id: `${sessionId}-${i}`,
3361
4319
  machine_id: machineId,
3362
- updated_at: now
4320
+ updated_at: now2
3363
4321
  }, account));
3364
4322
  requests++;
3365
4323
  }
@@ -3369,13 +4327,13 @@ async function ingestPi(db, verbose = false) {
3369
4327
  agent: "pi",
3370
4328
  project_path: "",
3371
4329
  project_name: "",
3372
- started_at: turns[0]?.timestamp ?? now,
4330
+ started_at: turns[0]?.timestamp ?? now2,
3373
4331
  ended_at: null,
3374
4332
  total_cost_usd: 0,
3375
4333
  total_tokens: 0,
3376
4334
  request_count: 0,
3377
4335
  machine_id: machineId,
3378
- updated_at: now
4336
+ updated_at: now2
3379
4337
  }, account));
3380
4338
  touched.add(sessionId);
3381
4339
  }
@@ -3390,10 +4348,10 @@ async function ingestPi(db, verbose = false) {
3390
4348
 
3391
4349
  // src/ingest/hermes.ts
3392
4350
  init_database();
3393
- import { existsSync as existsSync7, statSync as statSync6 } from "fs";
3394
- import { homedir as homedir7 } from "os";
3395
- import { join as join7 } from "path";
3396
- var HERMES_DB = join7(homedir7(), ".hermes", "state.db");
4351
+ import { existsSync as existsSync9, statSync as statSync6 } from "fs";
4352
+ import { homedir as homedir8 } from "os";
4353
+ import { join as join9 } from "path";
4354
+ var HERMES_DB = join9(homedir8(), ".hermes", "state.db");
3397
4355
  function mapCostBasis(billingMode) {
3398
4356
  if (billingMode === "subscription")
3399
4357
  return "subscription_included";
@@ -3402,7 +4360,7 @@ function mapCostBasis(billingMode) {
3402
4360
  return defaultCostBasisForAgent("hermes");
3403
4361
  }
3404
4362
  async function ingestHermes(db, verbose = false) {
3405
- if (!existsSync7(HERMES_DB)) {
4363
+ if (!existsSync9(HERMES_DB)) {
3406
4364
  return { sessions: 0, requests: 0 };
3407
4365
  }
3408
4366
  const { Database: Sqlite } = await import("bun:sqlite");
@@ -3422,7 +4380,7 @@ async function ingestHermes(db, verbose = false) {
3422
4380
  return { sessions: 0, requests: 0 };
3423
4381
  }
3424
4382
  const machineId = getMachineId();
3425
- const now = new Date().toISOString();
4383
+ const now2 = new Date().toISOString();
3426
4384
  let requests = 0;
3427
4385
  const account = await resolveAccountForAgent("hermes");
3428
4386
  for (const row of rows) {
@@ -3442,7 +4400,7 @@ async function ingestHermes(db, verbose = false) {
3442
4400
  total_tokens: tokens,
3443
4401
  request_count: 1,
3444
4402
  machine_id: machineId,
3445
- updated_at: now
4403
+ updated_at: now2
3446
4404
  }, account));
3447
4405
  const reqId = `hermes-${row.id}-rollup`;
3448
4406
  upsertRequest(db, withAccount({
@@ -3460,7 +4418,7 @@ async function ingestHermes(db, verbose = false) {
3460
4418
  timestamp: endedAt ?? startedAt,
3461
4419
  source_request_id: row.id,
3462
4420
  machine_id: machineId,
3463
- updated_at: now
4421
+ updated_at: now2
3464
4422
  }, account));
3465
4423
  requests++;
3466
4424
  rollupSession(db, sessionId);
@@ -3482,14 +4440,81 @@ function statSyncSafe(path) {
3482
4440
  // src/ingest/loops.ts
3483
4441
  init_database();
3484
4442
  init_pricing();
3485
- import { existsSync as existsSync8 } from "fs";
3486
- import { homedir as homedir8 } from "os";
3487
- import { join as join8 } from "path";
4443
+ import { existsSync as existsSync10 } from "fs";
4444
+ import { homedir as homedir9 } from "os";
4445
+ import { join as join10 } from "path";
3488
4446
  import { Database as BunDatabase2 } from "bun:sqlite";
3489
- var DEFAULT_LOOPS_DB_PATH = join8(homedir8(), ".hasna", "loops", "loops.db");
4447
+ import { homedir as pathsResolverHomedir2 } from "os";
4448
+ import { join as pathsResolverJoin2 } from "path";
4449
+ var PATHS_RESOLVER_KIND_ENV2 = {
4450
+ config: "HASNA_CONFIG_HOME",
4451
+ data: "HASNA_DATA_HOME",
4452
+ state: "HASNA_STATE_HOME",
4453
+ cache: "HASNA_CACHE_HOME"
4454
+ };
4455
+ var PATHS_RESOLVER_APP_SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4456
+ function pathsResolverAssertApp2(app) {
4457
+ if (typeof app !== "string" || app.length === 0) {
4458
+ throw new TypeError("paths: app must be a non-empty string");
4459
+ }
4460
+ if (!PATHS_RESOLVER_APP_SLUG_RE2.test(app)) {
4461
+ throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
4462
+ }
4463
+ }
4464
+ function pathsResolverAssertKind2(kind) {
4465
+ if (!Object.keys(PATHS_RESOLVER_KIND_ENV2).includes(kind)) {
4466
+ throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV2).join(", ")}`);
4467
+ }
4468
+ }
4469
+ function pathsResolverBaseDir2(kind, options) {
4470
+ pathsResolverAssertKind2(kind);
4471
+ const env = options.env ?? process.env;
4472
+ const override = env[PATHS_RESOLVER_KIND_ENV2[kind]];
4473
+ if (typeof override === "string" && override.length > 0)
4474
+ return override;
4475
+ const home = options.home ?? pathsResolverHomedir2();
4476
+ const platform2 = options.platform ?? process.platform;
4477
+ if (platform2 === "darwin") {
4478
+ switch (kind) {
4479
+ case "config":
4480
+ case "data":
4481
+ return pathsResolverJoin2(home, "Library", "Application Support", "Hasna");
4482
+ case "cache":
4483
+ return pathsResolverJoin2(home, "Library", "Caches", "Hasna");
4484
+ case "state":
4485
+ return pathsResolverJoin2(home, "Library", "Logs", "Hasna");
4486
+ }
4487
+ }
4488
+ switch (kind) {
4489
+ case "config":
4490
+ return pathsResolverJoin2(home, ".config", "hasna");
4491
+ case "data":
4492
+ return pathsResolverJoin2(home, ".local", "share", "hasna");
4493
+ case "state":
4494
+ return pathsResolverJoin2(home, ".local", "state", "hasna");
4495
+ case "cache":
4496
+ return pathsResolverJoin2(home, ".cache", "hasna");
4497
+ }
4498
+ }
4499
+ function pathsResolverResolve2(kind, options) {
4500
+ pathsResolverAssertApp2(options.app);
4501
+ const appSegment = options.internal === true ? pathsResolverJoin2("internal", options.app) : options.app;
4502
+ return pathsResolverJoin2(pathsResolverBaseDir2(kind, options), appSegment);
4503
+ }
4504
+ function dataDir2(options) {
4505
+ return pathsResolverResolve2("data", options);
4506
+ }
3490
4507
  var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
4508
+ function defaultLoopsDbPath() {
4509
+ const resolved = dataDir2({
4510
+ app: "loops",
4511
+ home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
4512
+ });
4513
+ const legacy = join10(process.env["HOME"] || process.env["USERPROFILE"] || homedir9(), ".hasna", "loops", "loops.db");
4514
+ return existsSync10(join10(resolved, "loops.db")) ? join10(resolved, "loops.db") : legacy;
4515
+ }
3491
4516
  function loopsDbPath() {
3492
- return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? DEFAULT_LOOPS_DB_PATH;
4517
+ return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
3493
4518
  }
3494
4519
  function loopsModel() {
3495
4520
  return process.env["HASNA_ECONOMY_LOOPS_MODEL"] ?? process.env["ECONOMY_LOOPS_MODEL"] ?? "gpt-5.3-codex";
@@ -3578,7 +4603,7 @@ function readLoopGoalRuns(loopsDb) {
3578
4603
  }
3579
4604
  async function ingestLoops(db, verbose = false) {
3580
4605
  const dbPath = loopsDbPath();
3581
- if (!existsSync8(dbPath)) {
4606
+ if (!existsSync10(dbPath)) {
3582
4607
  if (verbose)
3583
4608
  console.log("OpenLoops DB not found:", dbPath);
3584
4609
  return { sessions: 0, requests: 0 };
@@ -3668,30 +4693,30 @@ async function ingestLoops(db, verbose = false) {
3668
4693
 
3669
4694
  // src/ingest/claude-quota.ts
3670
4695
  init_database();
3671
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
4696
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
3672
4697
 
3673
4698
  // src/lib/paths.ts
3674
- import { homedir as homedir9 } from "os";
3675
- import { join as join9 } from "path";
3676
- function getHomeDir() {
3677
- return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
4699
+ import { homedir as homedir10 } from "os";
4700
+ import { join as join11 } from "path";
4701
+ function getHomeDir2() {
4702
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir10();
3678
4703
  }
3679
4704
  function agentPaths() {
3680
- const home = getHomeDir();
4705
+ const home = getHomeDir2();
3681
4706
  return {
3682
- claudeProjects: join9(home, ".claude", "projects"),
3683
- claudeCredentials: join9(home, ".claude", ".credentials.json"),
3684
- takumiProjects: join9(home, ".takumi", "projects"),
3685
- codexDir: join9(home, ".codex"),
3686
- codexDb: join9(home, ".codex", "state_5.sqlite"),
3687
- codexAuth: join9(home, ".codex", "auth.json"),
3688
- codexConfig: join9(home, ".codex", "config.toml"),
3689
- geminiTmp: join9(home, ".gemini", "tmp"),
3690
- geminiHistory: join9(home, ".gemini", "history"),
3691
- opencodeMessages: join9(home, ".local", "share", "opencode", "storage", "message"),
3692
- piSessions: join9(home, ".pi", "agent", "sessions"),
3693
- hermesDir: join9(home, ".hermes"),
3694
- hermesDb: join9(home, ".hermes", "state.db")
4707
+ claudeProjects: join11(home, ".claude", "projects"),
4708
+ claudeCredentials: join11(home, ".claude", ".credentials.json"),
4709
+ takumiProjects: join11(home, ".takumi", "projects"),
4710
+ codexDir: join11(home, ".codex"),
4711
+ codexDb: join11(home, ".codex", "state_5.sqlite"),
4712
+ codexAuth: join11(home, ".codex", "auth.json"),
4713
+ codexConfig: join11(home, ".codex", "config.toml"),
4714
+ geminiTmp: join11(home, ".gemini", "tmp"),
4715
+ geminiHistory: join11(home, ".gemini", "history"),
4716
+ opencodeMessages: join11(home, ".local", "share", "opencode", "storage", "message"),
4717
+ piSessions: join11(home, ".pi", "agent", "sessions"),
4718
+ hermesDir: join11(home, ".hermes"),
4719
+ hermesDb: join11(home, ".hermes", "state.db")
3695
4720
  };
3696
4721
  }
3697
4722
 
@@ -3703,10 +4728,10 @@ function readClaudeToken() {
3703
4728
  const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
3704
4729
  if (fromEnv)
3705
4730
  return { token: fromEnv };
3706
- if (!existsSync9(CREDENTIALS_PATH))
4731
+ if (!existsSync11(CREDENTIALS_PATH))
3707
4732
  return null;
3708
4733
  try {
3709
- const creds = JSON.parse(readFileSync6(CREDENTIALS_PATH, "utf-8"));
4734
+ const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
3710
4735
  const oauth = creds.claudeAiOauth;
3711
4736
  if (!oauth?.accessToken)
3712
4737
  return null;
@@ -3820,7 +4845,7 @@ async function ingestClaudeQuota(db, verbose = false) {
3820
4845
  snapshots++;
3821
4846
  }
3822
4847
  const monthlyFee = inferMonthlyFee(auth.subscriptionType, auth.rateLimitTier);
3823
- const now = new Date().toISOString();
4848
+ const now2 = new Date().toISOString();
3824
4849
  upsertSubscription(db, {
3825
4850
  id: "anthropic-claude-oauth",
3826
4851
  provider: "anthropic",
@@ -3831,8 +4856,8 @@ async function ingestClaudeQuota(db, verbose = false) {
3831
4856
  billing_cycle_start: null,
3832
4857
  reset_policy: "monthly",
3833
4858
  active: 1,
3834
- created_at: now,
3835
- updated_at: now
4859
+ created_at: now2,
4860
+ updated_at: now2
3836
4861
  });
3837
4862
  setIngestState(db, "claude", `quota-${today}`, new Date().toISOString());
3838
4863
  if (verbose)
@@ -3842,17 +4867,17 @@ async function ingestClaudeQuota(db, verbose = false) {
3842
4867
 
3843
4868
  // src/ingest/codex-quota.ts
3844
4869
  init_database();
3845
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
4870
+ import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
3846
4871
  var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
3847
4872
  function readCodexAuth() {
3848
4873
  const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
3849
4874
  if (fromEnv)
3850
4875
  return { token: fromEnv, authMode: "chatgpt" };
3851
4876
  const authPath = agentPaths().codexAuth;
3852
- if (!existsSync10(authPath))
4877
+ if (!existsSync12(authPath))
3853
4878
  return null;
3854
4879
  try {
3855
- const auth = JSON.parse(readFileSync7(authPath, "utf-8"));
4880
+ const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
3856
4881
  const token = auth.tokens?.access_token;
3857
4882
  if (!token)
3858
4883
  return null;
@@ -3919,7 +4944,7 @@ async function ingestCodexQuota(db, verbose = false) {
3919
4944
  }
3920
4945
  const machineId = getMachineId();
3921
4946
  let snapshots = 0;
3922
- const now = new Date().toISOString();
4947
+ const now2 = new Date().toISOString();
3923
4948
  const windows = [
3924
4949
  ["five_hour_utilization", usage.rate_limit?.primary_window],
3925
4950
  ["seven_day_utilization", usage.rate_limit?.secondary_window]
@@ -3970,10 +4995,10 @@ async function ingestCodexQuota(db, verbose = false) {
3970
4995
  billing_cycle_start: null,
3971
4996
  reset_policy: "monthly",
3972
4997
  active: 1,
3973
- created_at: now,
3974
- updated_at: now
4998
+ created_at: now2,
4999
+ updated_at: now2
3975
5000
  });
3976
- setIngestState(db, "codex", `quota-${today}`, now);
5001
+ setIngestState(db, "codex", `quota-${today}`, now2);
3977
5002
  if (verbose)
3978
5003
  console.log(` codex quota: ${snapshots} snapshots (${usage.plan_type ?? "unknown plan"})`);
3979
5004
  return { snapshots };
@@ -4015,7 +5040,8 @@ async function syncAll(db, opts = {}) {
4015
5040
  // src/lib/cloud-ingest.ts
4016
5041
  init_database();
4017
5042
  init_pricing();
4018
- import { join as join10 } from "path";
5043
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, writeFileSync } from "fs";
5044
+ import { dirname as dirname2, join as join12 } from "path";
4019
5045
 
4020
5046
  // src/ingest/billing.ts
4021
5047
  init_database();
@@ -4059,15 +5085,39 @@ var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snaps
4059
5085
  function getIngestCachePath() {
4060
5086
  if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
4061
5087
  return process.env["HASNA_ECONOMY_INGEST_CACHE"];
4062
- return join10(getDataDir(), "ingest-cache.db");
5088
+ const home = process.env["HOME"] || process.env["USERPROFILE"] || undefined;
5089
+ return join12(cacheDir({ app: "economy", home }), "ingest-cache.json");
4063
5090
  }
4064
- function copyIngestState(from, to) {
4065
- const rows = from.prepare(`SELECT source, key, value FROM ingest_state`).all();
4066
- to.prepare(`DELETE FROM ingest_state`).run();
4067
- const insert = to.prepare(`INSERT INTO ingest_state (source, key, value) VALUES (?, ?, ?)`);
5091
+ function readIngestCache(path) {
5092
+ let text;
5093
+ try {
5094
+ text = readFileSync9(path, "utf8");
5095
+ } catch {
5096
+ return [];
5097
+ }
5098
+ try {
5099
+ const parsed = JSON.parse(text);
5100
+ const rows = Array.isArray(parsed?.ingest_state) ? parsed.ingest_state : [];
5101
+ return rows.filter((row) => typeof row === "object" && row !== null && typeof row.source === "string" && typeof row.key === "string" && typeof row.value === "string");
5102
+ } catch {
5103
+ return [];
5104
+ }
5105
+ }
5106
+ function writeIngestCache(path, rows) {
5107
+ mkdirSync3(dirname2(path), { recursive: true });
5108
+ const tmp = `${path}.${process.pid}.tmp`;
5109
+ writeFileSync(tmp, JSON.stringify({ version: 1, ingest_state: rows }), { mode: 384 });
5110
+ renameSync(tmp, path);
5111
+ }
5112
+ function loadIngestState2(scratch, rows) {
5113
+ scratch.prepare(`DELETE FROM ingest_state`).run();
5114
+ const insert = scratch.prepare(`INSERT INTO ingest_state (source, key, value) VALUES (?, ?, ?)`);
4068
5115
  for (const row of rows)
4069
5116
  insert.run(row.source, row.key, row.value);
4070
5117
  }
5118
+ function dumpIngestState(scratch) {
5119
+ return scratch.prepare(`SELECT source, key, value FROM ingest_state`).all();
5120
+ }
4071
5121
  function exportIngestRows(db) {
4072
5122
  const body = {};
4073
5123
  for (const table of CLOUD_INGEST_TABLES) {
@@ -4095,11 +5145,9 @@ async function postIngest(cloud, body) {
4095
5145
  async function syncAllToCloud(cloud, opts = {}) {
4096
5146
  const scratch = openDatabase(":memory:", true);
4097
5147
  ensurePricingSeeded(scratch);
4098
- const cache = openDatabase(opts.cachePath ?? getIngestCachePath(), true);
5148
+ const cachePath = opts.cachePath ?? getIngestCachePath();
4099
5149
  try {
4100
- if (opts.force)
4101
- cache.prepare(`DELETE FROM ingest_state`).run();
4102
- copyIngestState(cache, scratch);
5150
+ loadIngestState2(scratch, opts.force ? [] : readIngestCache(cachePath));
4103
5151
  const result = await syncAll(scratch, opts);
4104
5152
  if (opts.backfillMachine)
4105
5153
  backfillMachineId(scratch);
@@ -4107,59 +5155,17 @@ async function syncAllToCloud(cloud, opts = {}) {
4107
5155
  await recalculateZeroCostRequests(scratch);
4108
5156
  const body = exportIngestRows(scratch);
4109
5157
  if (Object.keys(body).length === 0) {
4110
- copyIngestState(scratch, cache);
5158
+ writeIngestCache(cachePath, dumpIngestState(scratch));
4111
5159
  return { ...result, posted: false };
4112
5160
  }
4113
5161
  const response = await postIngest(cloud, body);
4114
- copyIngestState(scratch, cache);
5162
+ writeIngestCache(cachePath, dumpIngestState(scratch));
4115
5163
  return { ...result, posted: true, ingested: response.ingested, total: response.total };
4116
5164
  } finally {
4117
5165
  try {
4118
5166
  scratch.close();
4119
5167
  } catch {}
4120
- try {
4121
- cache.close();
4122
- } catch {}
4123
- }
4124
- }
4125
-
4126
- // src/lib/contracts-client/storage.ts
4127
- import { resolveStorageClient } from "@hasna/contracts/client/storage";
4128
-
4129
- // src/lib/cloud-storage.ts
4130
- var ECONOMY_APP = "economy";
4131
- var cache;
4132
- function resolveEconomyCloudStorage(env = process.env, overrides) {
4133
- const resolved = resolveStorageClient(ECONOMY_APP, env, overrides);
4134
- return resolved.transport === "http" ? { active: true, client: resolved.client } : { active: false, client: null };
4135
- }
4136
- function economyCloudStorage(env = process.env) {
4137
- if (cache === undefined)
4138
- cache = resolveEconomyCloudStorage(env);
4139
- return cache;
4140
- }
4141
- function cleanQuery(query) {
4142
- if (!query)
4143
- return;
4144
- const out = {};
4145
- for (const [k, v] of Object.entries(query)) {
4146
- if (v !== undefined && v !== null && v !== "")
4147
- out[k] = v;
4148
5168
  }
4149
- return Object.keys(out).length > 0 ? out : undefined;
4150
- }
4151
- async function cloudListItems(storage, resource, query) {
4152
- const cleaned = cleanQuery(query);
4153
- const res = await storage.client.list(resource, cleaned ? { query: cleaned } : {});
4154
- return res.items;
4155
- }
4156
- async function cloudObject(storage, path, query) {
4157
- const cleaned = cleanQuery(query);
4158
- const raw = await storage.client.transport.get(path, cleaned ? { query: cleaned } : {});
4159
- if (raw && typeof raw === "object" && "data" in raw) {
4160
- return raw.data;
4161
- }
4162
- return raw;
4163
5169
  }
4164
5170
 
4165
5171
  // src/lib/agents.ts
@@ -4259,24 +5265,24 @@ function ymd(date) {
4259
5265
  return date.toISOString().substring(0, 10);
4260
5266
  }
4261
5267
  function usageSnapshotFilterForPeriod(period) {
4262
- const now = new Date;
5268
+ const now2 = new Date;
4263
5269
  switch (period) {
4264
5270
  case "today":
4265
- return { date: ymd(now) };
5271
+ return { date: ymd(now2) };
4266
5272
  case "yesterday": {
4267
- const yesterday = new Date(now);
5273
+ const yesterday = new Date(now2);
4268
5274
  yesterday.setUTCDate(yesterday.getUTCDate() - 1);
4269
5275
  return { date: ymd(yesterday) };
4270
5276
  }
4271
5277
  case "week": {
4272
- const weekAgo = new Date(now);
5278
+ const weekAgo = new Date(now2);
4273
5279
  weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
4274
5280
  return { since: ymd(weekAgo) };
4275
5281
  }
4276
5282
  case "month":
4277
- return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1))) };
5283
+ return { since: ymd(new Date(Date.UTC(now2.getUTCFullYear(), now2.getUTCMonth(), 1))) };
4278
5284
  case "year":
4279
- return { since: ymd(new Date(Date.UTC(now.getUTCFullYear(), 0, 1))) };
5285
+ return { since: ymd(new Date(Date.UTC(now2.getUTCFullYear(), 0, 1))) };
4280
5286
  case "all":
4281
5287
  return {};
4282
5288
  }
@@ -4305,13 +5311,13 @@ var ZERO_SESSION_TOTALS = {
4305
5311
  cost_usd: 0,
4306
5312
  last_active: null
4307
5313
  };
4308
- function formatAge(timestamp, now) {
5314
+ function formatAge(timestamp, now2) {
4309
5315
  if (!timestamp)
4310
5316
  return "never";
4311
5317
  const t = new Date(timestamp).getTime();
4312
5318
  if (!Number.isFinite(t))
4313
5319
  return "unknown";
4314
- const ageMs = Math.max(0, now.getTime() - t);
5320
+ const ageMs = Math.max(0, now2.getTime() - t);
4315
5321
  const minutes = Math.floor(ageMs / 60000);
4316
5322
  if (minutes < 1)
4317
5323
  return "<1m";
@@ -4460,7 +5466,7 @@ function querySinceTotals(db, since, machine) {
4460
5466
  const sessions = querySessionOnlyTotals(db, "started_at >= ?", [since], machine);
4461
5467
  return combineTotals(requests, sessions);
4462
5468
  }
4463
- function parseSinceDate(input, now) {
5469
+ function parseSinceDate(input, now2) {
4464
5470
  const value = input.trim();
4465
5471
  const match = value.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/i);
4466
5472
  if (match) {
@@ -4468,7 +5474,7 @@ function parseSinceDate(input, now) {
4468
5474
  const unit = match[2].toLowerCase();
4469
5475
  const multiplier = unit === "m" ? 60000 : unit === "h" ? 60 * 60000 : unit === "d" ? 24 * 60 * 60000 : 7 * 24 * 60 * 60000;
4470
5476
  return {
4471
- timestamp: new Date(now.getTime() - amount * multiplier).toISOString(),
5477
+ timestamp: new Date(now2.getTime() - amount * multiplier).toISOString(),
4472
5478
  label: value
4473
5479
  };
4474
5480
  }
@@ -4809,7 +5815,7 @@ function accountBreakdownToBriefRow(row) {
4809
5815
  last_active: row.last_active
4810
5816
  };
4811
5817
  }
4812
- function queryFreshness(db, now, machine, localSync) {
5818
+ function queryFreshness(db, now2, machine, localSync) {
4813
5819
  const requestFilter = machine ? " AND machine_id = ?" : "";
4814
5820
  const requestRows = db.prepare(`
4815
5821
  SELECT machine_id, MAX(timestamp) as max_request_at
@@ -4823,7 +5829,7 @@ function queryFreshness(db, now, machine, localSync) {
4823
5829
  machines.set(row.machine_id, {
4824
5830
  machine_id: row.machine_id,
4825
5831
  max_request_at: row.max_request_at,
4826
- request_age: formatAge(row.max_request_at, now),
5832
+ request_age: formatAge(row.max_request_at, now2),
4827
5833
  last_merge_sync_at: null,
4828
5834
  merge_sync_age: "never"
4829
5835
  });
@@ -4838,7 +5844,7 @@ function queryFreshness(db, now, machine, localSync) {
4838
5844
  };
4839
5845
  const syncAt = latestTimestamp(row.last_pull_at, row.last_push_at, row.updated_at);
4840
5846
  current.last_merge_sync_at = syncAt;
4841
- current.merge_sync_age = formatAge(syncAt, now);
5847
+ current.merge_sync_age = formatAge(syncAt, now2);
4842
5848
  machines.set(row.machine_id, current);
4843
5849
  }
4844
5850
  if (localSync && (!machine || machine === localSync.machineId)) {
@@ -4850,7 +5856,7 @@ function queryFreshness(db, now, machine, localSync) {
4850
5856
  merge_sync_age: "never"
4851
5857
  };
4852
5858
  current.last_merge_sync_at = latestTimestamp(current.last_merge_sync_at, localSync.syncedAt);
4853
- current.merge_sync_age = formatAge(current.last_merge_sync_at, now);
5859
+ current.merge_sync_age = formatAge(current.last_merge_sync_at, now2);
4854
5860
  machines.set(localSync.machineId, current);
4855
5861
  }
4856
5862
  const rows = [...machines.values()].sort((a, b) => a.machine_id.localeCompare(b.machine_id));
@@ -4861,10 +5867,10 @@ function queryFreshness(db, now, machine, localSync) {
4861
5867
  };
4862
5868
  }
4863
5869
  function buildBrief(db, opts = {}) {
4864
- const now = opts.now ?? new Date;
5870
+ const now2 = opts.now ?? new Date;
4865
5871
  const machine = machineFilter(opts.machine);
4866
5872
  const sinceInput = opts.since ?? "24h";
4867
- const since = parseSinceDate(sinceInput, now);
5873
+ const since = parseSinceDate(sinceInput, now2);
4868
5874
  const summaries = [
4869
5875
  {
4870
5876
  label: "Today",
@@ -4885,10 +5891,10 @@ function buildBrief(db, opts = {}) {
4885
5891
  ];
4886
5892
  const machines = queryMachineSinceRows(db, since.timestamp, machine);
4887
5893
  for (const row of machines) {
4888
- row.last_data_age = formatAge(row.last_data_at, now);
5894
+ row.last_data_age = formatAge(row.last_data_at, now2);
4889
5895
  }
4890
5896
  return {
4891
- generated_at: now.toISOString(),
5897
+ generated_at: now2.toISOString(),
4892
5898
  machine: machine ?? "all",
4893
5899
  since: {
4894
5900
  input: sinceInput,
@@ -4899,7 +5905,7 @@ function buildBrief(db, opts = {}) {
4899
5905
  machines,
4900
5906
  agents: queryAgentSinceRows(db, since.timestamp, machine),
4901
5907
  accounts: queryAccountSinceRows(db, since.timestamp, machine),
4902
- freshness: queryFreshness(db, now, machine, opts.currentMachineId && opts.localSyncAt ? { machineId: opts.currentMachineId, syncedAt: opts.localSyncAt.toISOString() } : undefined)
5908
+ freshness: queryFreshness(db, now2, machine, opts.currentMachineId && opts.localSyncAt ? { machineId: opts.currentMachineId, syncedAt: opts.localSyncAt.toISOString() } : undefined)
4903
5909
  };
4904
5910
  }
4905
5911
 
@@ -4963,14 +5969,14 @@ function queryRangeStats(db, from, to) {
4963
5969
  const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
4964
5970
  return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
4965
5971
  }
4966
- function queryForecast(db, now = new Date) {
4967
- const daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
4968
- const dayOfMonth = now.getDate();
4969
- const monthStart = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`;
5972
+ function queryForecast(db, now2 = new Date) {
5973
+ const daysInMonth = new Date(now2.getFullYear(), now2.getMonth() + 1, 0).getDate();
5974
+ const dayOfMonth = now2.getDate();
5975
+ const monthStart = `${now2.getFullYear()}-${String(now2.getMonth() + 1).padStart(2, "0")}-01`;
4970
5976
  const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(monthStart);
4971
5977
  const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
4972
5978
  const projected = dailyAvg * daysInMonth;
4973
- const sevenDaysAgo = new Date(now);
5979
+ const sevenDaysAgo = new Date(now2);
4974
5980
  sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
4975
5981
  const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
4976
5982
  const last7DailyAvg = last7.cost / 7;
@@ -5000,7 +6006,7 @@ function queryModelEfficiency(db) {
5000
6006
  }
5001
6007
 
5002
6008
  // src/lib/store/index.ts
5003
- class LocalStore {
6009
+ class LocalStore2 {
5004
6010
  transport = "local";
5005
6011
  _db;
5006
6012
  db() {
@@ -5116,8 +6122,8 @@ class LocalStore {
5116
6122
  return estimateCostFromRows(listModelPricing(this.db()), input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
5117
6123
  }
5118
6124
  async setBudget(input) {
5119
- const now = new Date().toISOString();
5120
- const budget = { id: randomUUID(), ...input, created_at: now, updated_at: now };
6125
+ const now2 = new Date().toISOString();
6126
+ const budget = { id: randomUUID(), ...input, created_at: now2, updated_at: now2 };
5121
6127
  upsertBudget(this.db(), budget);
5122
6128
  return budget.id;
5123
6129
  }
@@ -5125,8 +6131,8 @@ class LocalStore {
5125
6131
  deleteBudget(this.db(), id);
5126
6132
  }
5127
6133
  async setGoal(input) {
5128
- const now = new Date().toISOString();
5129
- upsertGoal(this.db(), { id: randomUUID(), ...input, created_at: now, updated_at: now });
6134
+ const now2 = new Date().toISOString();
6135
+ upsertGoal(this.db(), { id: randomUUID(), ...input, created_at: now2, updated_at: now2 });
5130
6136
  }
5131
6137
  async removeGoal(id) {
5132
6138
  deleteGoal(this.db(), id);
@@ -5138,7 +6144,7 @@ class LocalStore {
5138
6144
  deleteModelPricing(this.db(), model);
5139
6145
  }
5140
6146
  async setSubscription(input) {
5141
- const now = new Date().toISOString();
6147
+ const now2 = new Date().toISOString();
5142
6148
  const row = {
5143
6149
  id: input.id ?? randomUUID(),
5144
6150
  provider: input.provider,
@@ -5149,8 +6155,8 @@ class LocalStore {
5149
6155
  billing_cycle_start: input.billing_cycle_start,
5150
6156
  reset_policy: input.reset_policy,
5151
6157
  active: input.active,
5152
- created_at: now,
5153
- updated_at: now
6158
+ created_at: now2,
6159
+ updated_at: now2
5154
6160
  };
5155
6161
  upsertSubscription(this.db(), row);
5156
6162
  return row;
@@ -5195,7 +6201,7 @@ function q(params) {
5195
6201
  return out;
5196
6202
  }
5197
6203
 
5198
- class ApiStore {
6204
+ class ApiStore2 {
5199
6205
  cloud;
5200
6206
  transport = "cloud-http";
5201
6207
  constructor(cloud) {
@@ -5339,7 +6345,7 @@ class ApiStore {
5339
6345
  await this.cloud.client.delete("pricing", model);
5340
6346
  }
5341
6347
  async setSubscription(input) {
5342
- const now = new Date().toISOString();
6348
+ const now2 = new Date().toISOString();
5343
6349
  const row = {
5344
6350
  id: input.id ?? randomUUID(),
5345
6351
  provider: input.provider,
@@ -5350,8 +6356,8 @@ class ApiStore {
5350
6356
  billing_cycle_start: input.billing_cycle_start,
5351
6357
  reset_policy: input.reset_policy,
5352
6358
  active: input.active,
5353
- created_at: now,
5354
- updated_at: now
6359
+ created_at: now2,
6360
+ updated_at: now2
5355
6361
  };
5356
6362
  await this.cloud.client.create("subscriptions", row);
5357
6363
  return row;
@@ -5379,7 +6385,7 @@ class ApiStore {
5379
6385
  }
5380
6386
  function getStore(env = process.env) {
5381
6387
  const cloud = economyCloudStorage(env);
5382
- return cloud.active ? new ApiStore(cloud) : new LocalStore;
6388
+ return cloud.active ? new ApiStore2(cloud) : new LocalStore2;
5383
6389
  }
5384
6390
  function isCloudStore(env = process.env) {
5385
6391
  return economyCloudStorage(env).active;
@@ -5527,17 +6533,17 @@ function buildServer() {
5527
6533
  function textError(message) {
5528
6534
  return { content: [{ type: "text", text: message }], isError: true };
5529
6535
  }
5530
- server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z.string().optional() }, async ({ query }) => {
6536
+ server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z3.string().optional() }, async ({ query }) => {
5531
6537
  const q2 = query?.toLowerCase();
5532
6538
  const matches = q2 ? TOOL_NAMES.filter((name) => name.includes(q2)) : [...TOOL_NAMES];
5533
6539
  return text(matches.join(", "));
5534
6540
  });
5535
- server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z.array(z.string()) }, async ({ names }) => {
6541
+ server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z3.array(z3.string()) }, async ({ names }) => {
5536
6542
  const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
5537
6543
  `);
5538
6544
  return text(result);
5539
6545
  });
5540
- server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), machine: z.string().optional() }, async ({ period, machine }) => {
6546
+ server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional(), machine: z3.string().optional() }, async ({ period, machine }) => {
5541
6547
  const resolved = period ?? "today";
5542
6548
  const s = await store.summary(resolved, machine);
5543
6549
  const machineLabel = machine ? ` on ${machine}` : "";
@@ -5552,11 +6558,11 @@ function buildServer() {
5552
6558
  `));
5553
6559
  });
5554
6560
  server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, account, machine, limit(20)", {
5555
- agent: z.enum(AGENTS).optional(),
5556
- project: z.string().optional(),
5557
- account: z.string().optional(),
5558
- machine: z.string().optional(),
5559
- limit: z.number().int().positive().max(100).optional()
6561
+ agent: z3.enum(AGENTS).optional(),
6562
+ project: z3.string().optional(),
6563
+ account: z3.string().optional(),
6564
+ machine: z3.string().optional(),
6565
+ limit: z3.number().int().positive().max(100).optional()
5560
6566
  }, async ({ agent, project, account, machine, limit }) => {
5561
6567
  const sessions = await store.sessions({ agent, project, account, machine, limit: limit ?? 20 });
5562
6568
  const lines = ["id agent cost tokens project"];
@@ -5566,8 +6572,8 @@ function buildServer() {
5566
6572
  `));
5567
6573
  });
5568
6574
  server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
5569
- n: z.number().int().positive().max(100).optional(),
5570
- agent: z.enum(AGENTS).optional()
6575
+ n: z3.number().int().positive().max(100).optional(),
6576
+ agent: z3.enum(AGENTS).optional()
5571
6577
  }, async ({ n, agent }) => {
5572
6578
  const sessions = await store.topSessions(n ?? 10, agent);
5573
6579
  const lines = ["rank id agent cost tokens project"];
@@ -5575,7 +6581,7 @@ function buildServer() {
5575
6581
  return text(lines.join(`
5576
6582
  `));
5577
6583
  });
5578
- server.tool("get_model_breakdown", "Cost per model. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
6584
+ server.tool("get_model_breakdown", "Cost per model. Params: limit(20), verbose, json.", { limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ limit, verbose, json }) => {
5579
6585
  const rows = await store.modelBreakdown();
5580
6586
  if (json)
5581
6587
  return text(JSON.stringify(rows, null, 2));
@@ -5590,7 +6596,7 @@ function buildServer() {
5590
6596
  return text(lines.join(`
5591
6597
  `));
5592
6598
  });
5593
- server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all), limit(20), verbose, json.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ period, limit, verbose, json }) => {
6599
+ server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all), limit(20), verbose, json.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional(), limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ period, limit, verbose, json }) => {
5594
6600
  const rows = await store.projectBreakdown({ period: period ?? "all" });
5595
6601
  if (json)
5596
6602
  return text(JSON.stringify(rows, null, 2));
@@ -5606,7 +6612,7 @@ function buildServer() {
5606
6612
  return text(lines.join(`
5607
6613
  `));
5608
6614
  });
5609
- server.tool("get_agent_breakdown", "Cost per coding agent. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
6615
+ server.tool("get_agent_breakdown", "Cost per coding agent. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
5610
6616
  const rows = await store.agentBreakdown({ period: period ?? "all" });
5611
6617
  if (rows.length === 0)
5612
6618
  return text("No agent usage yet.");
@@ -5617,7 +6623,7 @@ function buildServer() {
5617
6623
  return text(lines.join(`
5618
6624
  `));
5619
6625
  });
5620
- server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all), limit(20), verbose, json. Shows API-equivalent, billable API, and subscription-included usage.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ period, limit, verbose, json }) => {
6626
+ server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all), limit(20), verbose, json. Shows API-equivalent, billable API, and subscription-included usage.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional(), limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ period, limit, verbose, json }) => {
5621
6627
  const rows = await store.accountBreakdown({ period: period ?? "all" });
5622
6628
  if (json)
5623
6629
  return text(JSON.stringify(rows, null, 2));
@@ -5636,8 +6642,8 @@ function buildServer() {
5636
6642
  `));
5637
6643
  });
5638
6644
  server.tool("get_cost_center_breakdown", "Cost per cost center. Params: period(today|week|month|year|all), kind(loop|app|repo|service|team).", {
5639
- period: z.enum(["today", "week", "month", "year", "all"]).optional(),
5640
- kind: z.enum(["loop", "app", "repo", "service", "team"]).optional()
6645
+ period: z3.enum(["today", "week", "month", "year", "all"]).optional(),
6646
+ kind: z3.enum(["loop", "app", "repo", "service", "team"]).optional()
5641
6647
  }, async ({ period, kind }) => {
5642
6648
  const rows = await store.costCenterBreakdown({ period: period ?? "all", kind });
5643
6649
  if (rows.length === 0)
@@ -5649,7 +6655,7 @@ function buildServer() {
5649
6655
  return text(lines.join(`
5650
6656
  `));
5651
6657
  });
5652
- server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
6658
+ server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. Params: limit(20), verbose, json.", { limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ limit, verbose, json }) => {
5653
6659
  const budgets = await store.listBudgets();
5654
6660
  if (json)
5655
6661
  return text(JSON.stringify(budgets, null, 2));
@@ -5670,12 +6676,12 @@ function buildServer() {
5670
6676
  `));
5671
6677
  });
5672
6678
  server.tool("set_budget", "Create a spending budget. period: daily|weekly|monthly. limit_usd must be positive. alert_at_percent defaults to 80.", {
5673
- period: z.enum(["daily", "weekly", "monthly"]),
5674
- limit_usd: z.number().positive(),
5675
- project_path: z.string().optional(),
5676
- agent: z.enum(AGENTS).optional(),
5677
- cost_center_id: z.string().optional(),
5678
- alert_at_percent: z.number().positive().max(100).optional()
6679
+ period: z3.enum(["daily", "weekly", "monthly"]),
6680
+ limit_usd: z3.number().positive(),
6681
+ project_path: z3.string().optional(),
6682
+ agent: z3.enum(AGENTS).optional(),
6683
+ cost_center_id: z3.string().optional(),
6684
+ alert_at_percent: z3.number().positive().max(100).optional()
5679
6685
  }, async ({ period, limit_usd, project_path, agent, cost_center_id, alert_at_percent }) => {
5680
6686
  const id = await store.setBudget({
5681
6687
  project_path: project_path ?? null,
@@ -5687,11 +6693,11 @@ function buildServer() {
5687
6693
  });
5688
6694
  return text(`Budget set: ${id}`);
5689
6695
  });
5690
- server.tool("remove_budget", "Delete a budget by id.", { id: z.string() }, async ({ id }) => {
6696
+ server.tool("remove_budget", "Delete a budget by id.", { id: z3.string() }, async ({ id }) => {
5691
6697
  await store.removeBudget(id);
5692
6698
  return text("Budget removed.");
5693
6699
  });
5694
- server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
6700
+ server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage. Params: limit(20), verbose, json.", { limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ limit, verbose, json }) => {
5695
6701
  const rows = await store.listPricing();
5696
6702
  if (json)
5697
6703
  return text(JSON.stringify(rows, null, 2));
@@ -5707,13 +6713,13 @@ function buildServer() {
5707
6713
  `));
5708
6714
  });
5709
6715
  server.tool("set_pricing", "Create or update a model pricing row. Values are USD per 1M tokens except cache_storage_per_1m_hour.", {
5710
- model: z.string().min(1),
5711
- input_per_1m: z.number().nonnegative(),
5712
- output_per_1m: z.number().nonnegative(),
5713
- cache_read_per_1m: z.number().nonnegative().optional(),
5714
- cache_write_per_1m: z.number().nonnegative().optional(),
5715
- cache_write_1h_per_1m: z.number().nonnegative().optional(),
5716
- cache_storage_per_1m_hour: z.number().nonnegative().optional()
6716
+ model: z3.string().min(1),
6717
+ input_per_1m: z3.number().nonnegative(),
6718
+ output_per_1m: z3.number().nonnegative(),
6719
+ cache_read_per_1m: z3.number().nonnegative().optional(),
6720
+ cache_write_per_1m: z3.number().nonnegative().optional(),
6721
+ cache_write_1h_per_1m: z3.number().nonnegative().optional(),
6722
+ cache_storage_per_1m_hour: z3.number().nonnegative().optional()
5717
6723
  }, async (input) => {
5718
6724
  const model = input.model.trim();
5719
6725
  if (!model)
@@ -5729,11 +6735,11 @@ function buildServer() {
5729
6735
  });
5730
6736
  return text(`Pricing set: ${model}`);
5731
6737
  });
5732
- server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z.string() }, async ({ model }) => {
6738
+ server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z3.string() }, async ({ model }) => {
5733
6739
  await store.removePricing(model);
5734
6740
  return text("Pricing removed.");
5735
6741
  });
5736
- server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z.number().int().positive().max(365).optional() }, async ({ days }) => {
6742
+ server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z3.number().int().positive().max(365).optional() }, async ({ days }) => {
5737
6743
  const rows = await store.daily(days ?? 30);
5738
6744
  const byDate = new Map;
5739
6745
  for (const row of rows) {
@@ -5757,7 +6763,7 @@ function buildServer() {
5757
6763
  return text(lines.join(`
5758
6764
  `));
5759
6765
  });
5760
- server.tool("get_billing_summary", "Actual provider billing totals from admin API sync. Params: period(today|yesterday|week|month|year|all)", { period: z.enum(["today", "yesterday", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
6766
+ server.tool("get_billing_summary", "Actual provider billing totals from admin API sync. Params: period(today|yesterday|week|month|year|all)", { period: z3.enum(["today", "yesterday", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
5761
6767
  const summary = await store.billingSummary(period ?? "month");
5762
6768
  const lines = ["provider billed"];
5763
6769
  for (const [provider, cost] of Object.entries(summary.by_provider)) {
@@ -5767,7 +6773,7 @@ function buildServer() {
5767
6773
  return text(lines.join(`
5768
6774
  `));
5769
6775
  });
5770
- server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok), limit(20), verbose.", { session_id: z.string(), limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional() }, async ({ session_id, limit, verbose }) => {
6776
+ server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok), limit(20), verbose.", { session_id: z3.string(), limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional() }, async ({ session_id, limit, verbose }) => {
5771
6777
  const detail = await store.sessionDetail(session_id);
5772
6778
  if (!detail)
5773
6779
  return textError(`Session not found: ${session_id}`);
@@ -5791,13 +6797,13 @@ function buildServer() {
5791
6797
  return text(lines.join(`
5792
6798
  `));
5793
6799
  });
5794
- server.tool("sync", `Ingest new cost data. sources: ${SYNC_SOURCES.join("|")}. Set json=true for the full result object.`, { sources: z.enum([...SYNC_SOURCES]).optional(), json: z.boolean().optional() }, async ({ sources, json }) => {
6800
+ server.tool("sync", `Ingest new cost data. sources: ${SYNC_SOURCES.join("|")}. Set json=true for the full result object.`, { sources: z3.enum([...SYNC_SOURCES]).optional(), json: z3.boolean().optional() }, async ({ sources, json }) => {
5795
6801
  const selected = sources ?? "all";
5796
6802
  const opts = selected === "all" ? {} : { [selected]: true };
5797
6803
  if (isCloudStore()) {
5798
6804
  const cloud = economyCloudStorage();
5799
6805
  if (!cloud.active) {
5800
- return text("cloud mode: sync transport unavailable (set HASNA_ECONOMY_API_URL and HASNA_ECONOMY_API_KEY)");
6806
+ return text("cloud mode: sync transport unavailable (no credential resolved; set HASNA_ECONOMY_API_KEY or write ~/.hasna/economy/config/credentials)");
5801
6807
  }
5802
6808
  const result2 = await syncAllToCloud(cloud, opts);
5803
6809
  if (json)
@@ -5839,7 +6845,7 @@ function buildServer() {
5839
6845
  return text(lines.join(`
5840
6846
  `));
5841
6847
  });
5842
- server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all, agent, limit(20). Set json=true for full data.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional(), limit: z.number().int().positive().max(100).optional(), json: z.boolean().optional() }, async ({ period, agent, limit, json }) => {
6848
+ server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all, agent, limit(20). Set json=true for full data.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional(), agent: z3.enum(AGENTS).optional(), limit: z3.number().int().positive().max(100).optional(), json: z3.boolean().optional() }, async ({ period, agent, limit, json }) => {
5843
6849
  const p = period ?? "month";
5844
6850
  const { snapshots: snaps, summary } = await store.usage(p, agent);
5845
6851
  if (json)
@@ -5861,7 +6867,7 @@ function buildServer() {
5861
6867
  return text(lines.join(`
5862
6868
  `));
5863
6869
  });
5864
- server.tool("get_savings", "Subscription vs API savings summary. Params: period, agent, limit(20). Set json=true for full data.", { period: z.enum(["today", "week", "month", "year", "all"]).optional(), agent: z.enum(AGENTS).optional(), limit: z.number().int().positive().max(100).optional(), json: z.boolean().optional() }, async ({ period, agent, limit, json }) => {
6870
+ server.tool("get_savings", "Subscription vs API savings summary. Params: period, agent, limit(20). Set json=true for full data.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional(), agent: z3.enum(AGENTS).optional(), limit: z3.number().int().positive().max(100).optional(), json: z3.boolean().optional() }, async ({ period, agent, limit, json }) => {
5865
6871
  const savings = await store.savings(period ?? "month", agent);
5866
6872
  if (json)
5867
6873
  return text(JSON.stringify(savings, null, 2));
@@ -5898,16 +6904,16 @@ function buildServer() {
5898
6904
  `));
5899
6905
  });
5900
6906
  server.tool("set_subscription", "Create or update a subscription plan used by subscription-vs-API savings calculations.", {
5901
- id: z.string().optional(),
5902
- provider: z.string().min(1),
5903
- plan: z.string().min(1),
5904
- agent: z.enum(AGENTS).optional(),
5905
- monthly_fee_usd: z.number().nonnegative().optional(),
5906
- included_usage_usd: z.number().nonnegative().optional(),
5907
- billing_cycle_start: z.string().optional(),
5908
- reset_policy: z.string().optional(),
5909
- active: z.boolean().optional(),
5910
- json: z.boolean().optional()
6907
+ id: z3.string().optional(),
6908
+ provider: z3.string().min(1),
6909
+ plan: z3.string().min(1),
6910
+ agent: z3.enum(AGENTS).optional(),
6911
+ monthly_fee_usd: z3.number().nonnegative().optional(),
6912
+ included_usage_usd: z3.number().nonnegative().optional(),
6913
+ billing_cycle_start: z3.string().optional(),
6914
+ reset_policy: z3.string().optional(),
6915
+ active: z3.boolean().optional(),
6916
+ json: z3.boolean().optional()
5911
6917
  }, async (input) => {
5912
6918
  const row = await store.setSubscription({
5913
6919
  id: input.id,
@@ -5929,15 +6935,15 @@ agent: ${row.agent ?? "all"}
5929
6935
  fee: ${fmtUsd(row.monthly_fee_usd)}
5930
6936
  included: ${fmtUsd(row.included_usage_usd)}`);
5931
6937
  });
5932
- server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z.string() }, async ({ id }) => {
6938
+ server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z3.string() }, async ({ id }) => {
5933
6939
  await store.removeSubscription(id);
5934
6940
  return text("Subscription removed.");
5935
6941
  });
5936
- server.tool("estimate_cost", "Pre-flight cost estimate for token counts", { model: z.string(), input_tokens: z.number().optional(), output_tokens: z.number().optional() }, async ({ model, input_tokens, output_tokens }) => {
6942
+ server.tool("estimate_cost", "Pre-flight cost estimate for token counts", { model: z3.string(), input_tokens: z3.number().optional(), output_tokens: z3.number().optional() }, async ({ model, input_tokens, output_tokens }) => {
5937
6943
  const cost = await store.estimate({ model, inputTokens: input_tokens ?? 0, outputTokens: output_tokens ?? 0 });
5938
6944
  return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
5939
6945
  });
5940
- server.tool("get_goals", "All spending goals with current progress. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
6946
+ server.tool("get_goals", "All spending goals with current progress. Params: limit(20), verbose, json.", { limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ limit, verbose, json }) => {
5941
6947
  const goals = await store.listGoals();
5942
6948
  if (json)
5943
6949
  return text(JSON.stringify(goals, null, 2));
@@ -5958,10 +6964,10 @@ included: ${fmtUsd(row.included_usage_usd)}`);
5958
6964
  `));
5959
6965
  });
5960
6966
  server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
5961
- period: z.enum(["day", "week", "month", "year"]),
5962
- limit_usd: z.number().positive(),
5963
- project_path: z.string().optional(),
5964
- agent: z.string().optional()
6967
+ period: z3.enum(["day", "week", "month", "year"]),
6968
+ limit_usd: z3.number().positive(),
6969
+ project_path: z3.string().optional(),
6970
+ agent: z3.string().optional()
5965
6971
  }, async ({ period, limit_usd, project_path, agent }) => {
5966
6972
  await store.setGoal({
5967
6973
  period,
@@ -5971,11 +6977,11 @@ included: ${fmtUsd(row.included_usage_usd)}`);
5971
6977
  });
5972
6978
  return text(`Goal set: ${period} $${limit_usd}`);
5973
6979
  });
5974
- server.tool("remove_goal", "Delete a goal by id.", { id: z.string() }, async ({ id }) => {
6980
+ server.tool("remove_goal", "Delete a goal by id.", { id: z3.string() }, async ({ id }) => {
5975
6981
  await store.removeGoal(id);
5976
6982
  return text("Goal removed.");
5977
6983
  });
5978
- server.tool("list_machines", "List all machines that have synced data. Params: limit(20), verbose, json.", { limit: z.number().int().positive().max(100).optional(), verbose: z.boolean().optional(), json: z.boolean().optional() }, async ({ limit, verbose, json }) => {
6984
+ server.tool("list_machines", "List all machines that have synced data. Params: limit(20), verbose, json.", { limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ limit, verbose, json }) => {
5979
6985
  const machines = await store.machines();
5980
6986
  if (json)
5981
6987
  return text(JSON.stringify(machines, null, 2));
@@ -5994,11 +7000,11 @@ current machine: ${getMachineId()}`);
5994
7000
  return text(lines.join(`
5995
7001
  `));
5996
7002
  });
5997
- registerAgentTools(server, { includeFeedback: false });
7003
+ registerAgentTools(server, { service: "economy" });
5998
7004
  server.tool("send_feedback", "Send feedback about this service.", {
5999
- message: z.string(),
6000
- email: z.string().optional(),
6001
- category: z.enum(["bug", "feature", "general"]).optional()
7005
+ message: z3.string(),
7006
+ email: z3.string().optional(),
7007
+ category: z3.enum(["bug", "feature", "general"]).optional()
6002
7008
  }, async ({ message, email, category }) => {
6003
7009
  try {
6004
7010
  await store.sendFeedback({ message, email, category });
@@ -6010,32 +7016,102 @@ current machine: ${getMachineId()}`);
6010
7016
  return server;
6011
7017
  }
6012
7018
 
7019
+ // src/mcp/harness.ts
7020
+ import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
7021
+ var PORT_MIN = 0;
7022
+ var PORT_MAX = 65535;
7023
+ var DEFAULT_MCP_HTTP_PORT2 = 8899;
7024
+ function invalidPortMessage(source, value) {
7025
+ return `Invalid ${source} "${value}". Expected an integer between ${PORT_MIN} and ${PORT_MAX}.`;
7026
+ }
7027
+ function validatePort(port, source) {
7028
+ if (!Number.isInteger(port) || port < PORT_MIN || port > PORT_MAX) {
7029
+ throw new Error(invalidPortMessage(source, String(port)));
7030
+ }
7031
+ return port;
7032
+ }
7033
+ function parsePortValue(value, source) {
7034
+ const trimmed = value.trim();
7035
+ if (!/^\d+$/.test(trimmed)) {
7036
+ throw new Error(invalidPortMessage(source, value));
7037
+ }
7038
+ return validatePort(Number(trimmed), source);
7039
+ }
7040
+ function isStdioMode(argv = process.argv, env = process.env) {
7041
+ return argv.includes("--stdio") || env.MCP_STDIO === "1";
7042
+ }
7043
+ function isHttpMode(argv = process.argv, env = process.env) {
7044
+ return argv.includes("--http") || env.MCP_HTTP === "1";
7045
+ }
7046
+ function parseHttpArgv(argv = process.argv) {
7047
+ const http = isHttpMode(argv);
7048
+ let port;
7049
+ const portEqualsArg = argv.find((arg) => arg.startsWith("--port="));
7050
+ if (portEqualsArg) {
7051
+ port = parsePortValue(portEqualsArg.slice("--port=".length), "--port");
7052
+ }
7053
+ const portIdx = argv.indexOf("--port");
7054
+ if (portIdx !== -1) {
7055
+ const value = argv[portIdx + 1];
7056
+ if (value === undefined) {
7057
+ throw new Error(invalidPortMessage("--port", ""));
7058
+ }
7059
+ port = parsePortValue(value, "--port");
7060
+ }
7061
+ return { http, port };
7062
+ }
7063
+ function resolveMcpHttpPort(opts) {
7064
+ const argv = opts?.argv ?? process.argv;
7065
+ const env = opts?.env ?? process.env;
7066
+ if (opts?.explicit != null) {
7067
+ return validatePort(opts.explicit, "--port");
7068
+ }
7069
+ const { port } = parseHttpArgv(argv);
7070
+ if (port != null)
7071
+ return port;
7072
+ const envPort = env.MCP_HTTP_PORT;
7073
+ if (envPort && envPort.trim() !== "") {
7074
+ return parsePortValue(envPort, "MCP_HTTP_PORT");
7075
+ }
7076
+ return opts?.default ?? DEFAULT_MCP_HTTP_PORT2;
7077
+ }
7078
+ function healthPayload(name) {
7079
+ return { status: "ok", name };
7080
+ }
7081
+ async function handleMcpHttpRequest(req, buildServer2, options) {
7082
+ try {
7083
+ const server = await buildServer2();
7084
+ const transport = new WebStandardStreamableHTTPServerTransport({
7085
+ sessionIdGenerator: undefined,
7086
+ enableJsonResponse: options?.enableJsonResponse
7087
+ });
7088
+ await server.connect(transport);
7089
+ return await transport.handleRequest(req);
7090
+ } catch (error) {
7091
+ console.error("[mcp] HTTP error:", error);
7092
+ return Response.json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }, { status: 500 });
7093
+ }
7094
+ }
7095
+
6013
7096
  // src/mcp/http.ts
6014
- import {
6015
- healthPayload,
6016
- isHttpMode as harnessIsHttpMode,
6017
- isStdioMode as harnessIsStdioMode,
6018
- resolveMcpHttpPort as harnessResolveMcpHttpPort
6019
- } from "@hasna/mcp-harness";
6020
- import { handleMcpHttpRequest as harnessHandleMcpHttpRequest } from "@hasna/mcp-harness/bun";
6021
7097
  var MCP_HTTP_IDLE_TIMEOUT_SECONDS = 0;
6022
- function isHttpMode(argv = process.argv.slice(2)) {
6023
- return harnessIsHttpMode(argv);
7098
+ function isHttpMode2(argv = process.argv.slice(2)) {
7099
+ return isHttpMode(argv);
6024
7100
  }
6025
- function isStdioMode(argv = process.argv.slice(2)) {
6026
- return harnessIsStdioMode(argv);
7101
+ function isStdioMode2(argv = process.argv.slice(2)) {
7102
+ return isStdioMode(argv);
6027
7103
  }
6028
7104
  function resolveHttpPort(argv = process.argv.slice(2)) {
6029
7105
  const normalized = argv.map((arg) => arg === "-p" ? "--port" : arg);
6030
- return harnessResolveMcpHttpPort({ argv: normalized, default: DEFAULT_MCP_HTTP_PORT });
7106
+ return resolveMcpHttpPort({ argv: normalized, default: DEFAULT_MCP_HTTP_PORT });
6031
7107
  }
6032
- async function handleMcpHttpRequest(req) {
7108
+ async function handleMcpHttpRequest2(req) {
6033
7109
  const url = new URL(req.url);
6034
7110
  if (url.pathname === "/health" && req.method === "GET") {
6035
7111
  return Response.json(healthPayload(MCP_NAME));
6036
7112
  }
6037
7113
  if (url.pathname === "/mcp") {
6038
- return harnessHandleMcpHttpRequest(req, buildServer);
7114
+ return handleMcpHttpRequest(req, buildServer);
6039
7115
  }
6040
7116
  return new Response("Not Found", { status: 404 });
6041
7117
  }
@@ -6047,7 +7123,7 @@ function startHttpServer(options = {}) {
6047
7123
  port,
6048
7124
  hostname: hostname2,
6049
7125
  idleTimeout: MCP_HTTP_IDLE_TIMEOUT_SECONDS,
6050
- fetch: handleMcpHttpRequest
7126
+ fetch: handleMcpHttpRequest2
6051
7127
  });
6052
7128
  const address = `http://${hostname2}:${server.port}`;
6053
7129
  log(`${MCP_NAME}-mcp HTTP listening on ${address}/mcp (health: ${address}/health)`);
@@ -6080,7 +7156,7 @@ if (args.includes("--version") || args.includes("-V")) {
6080
7156
  process.exit(0);
6081
7157
  }
6082
7158
  async function main() {
6083
- if (isStdioMode(args) || !isHttpMode(args)) {
7159
+ if (isStdioMode2(args) || !isHttpMode2(args)) {
6084
7160
  const server = buildServer();
6085
7161
  const transport = new StdioServerTransport;
6086
7162
  await server.connect(transport);
@@ -6090,6 +7166,6 @@ async function main() {
6090
7166
  await new Promise(() => {});
6091
7167
  }
6092
7168
  main().catch((error) => {
6093
- console.error("MCP server error:", error);
7169
+ console.error("MCP server error:", error instanceof Error ? error.message : String(error));
6094
7170
  process.exit(1);
6095
7171
  });