@hasna/economy 0.5.3 → 0.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mcp/index.js CHANGED
@@ -85,7 +85,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
85
85
  // package.json
86
86
  var package_default = {
87
87
  name: "@hasna/economy",
88
- version: "0.5.3",
88
+ version: "0.5.5",
89
89
  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",
90
90
  repository: {
91
91
  type: "git",
@@ -1507,7 +1507,7 @@ function registerIngestJournalTool(server) {
1507
1507
  }
1508
1508
 
1509
1509
  // src/mcp/server.ts
1510
- import { z as z6 } from "zod";
1510
+ import { z as z7 } from "zod";
1511
1511
 
1512
1512
  // src/ingest/collection-outcome.ts
1513
1513
  import { AsyncLocalStorage } from "async_hooks";
@@ -1556,41 +1556,148 @@ function sourceTimeMillis(value) {
1556
1556
 
1557
1557
  // src/ingest/claude.ts
1558
1558
  import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync3, statSync } from "fs";
1559
- import { homedir as homedir2 } from "os";
1560
- import { join as join6, basename } from "path";
1559
+ import { homedir as homedir3 } from "os";
1560
+ import { join as join7, basename } from "path";
1561
1561
 
1562
1562
  // src/lib/accounts-store.ts
1563
1563
  import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
1564
1564
  import { homedir } from "os";
1565
1565
  import { join as join5 } from "path";
1566
- import { z as z5 } from "zod";
1566
+ import { z as z6 } from "zod";
1567
1567
  import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
1568
- var ACCOUNTS_APP = "accounts";
1569
- var profileNameSchema = z5.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
1568
+
1569
+ // src/lib/subscriptions-registry.ts
1570
+ import { execFile } from "child_process";
1571
+ import { promisify } from "util";
1572
+ import { isAbsolute } from "path";
1573
+ import { z as z5 } from "zod";
1574
+ var execute = promisify(execFile);
1575
+ var readSubscriptionsMetadata = async (args, env) => {
1576
+ let stdout;
1577
+ try {
1578
+ const result = await execute("subscriptions", args, {
1579
+ env,
1580
+ encoding: "utf8",
1581
+ timeout: 30000,
1582
+ maxBuffer: 16 * 1024 * 1024,
1583
+ windowsHide: true
1584
+ });
1585
+ stdout = result.stdout;
1586
+ } catch (error) {
1587
+ const code = error?.code;
1588
+ if (code === "ENOENT")
1589
+ throw new Error("subscriptions: CLI not found on PATH; install the Subscriptions CLI to resolve station account metadata");
1590
+ const status = typeof code === "number" && Number.isInteger(code) ? `exit ${code}` : "execution limit or transport failure";
1591
+ throw new Error(`subscriptions: read-only metadata command failed (${status}); verify the installed CLI and its hosted credentials`);
1592
+ }
1593
+ try {
1594
+ return JSON.parse(stdout);
1595
+ } catch {
1596
+ throw new Error("subscriptions: metadata command returned invalid JSON");
1597
+ }
1598
+ };
1599
+ var nonempty = z5.string().min(1);
1570
1600
  var profileSchema = z5.object({
1571
- name: z5.string(),
1572
- tool: z5.string(),
1601
+ name: nonempty,
1602
+ tool: nonempty,
1573
1603
  email: z5.string().optional(),
1574
1604
  displayName: z5.string().optional(),
1575
- dir: z5.string(),
1605
+ dir: nonempty.refine(isAbsolute).refine((value) => !/[\0\r\n]/.test(value)),
1606
+ createdAt: nonempty,
1607
+ lastUsedAt: z5.string().optional(),
1576
1608
  description: z5.string().optional(),
1577
- createdAt: z5.string(),
1578
- lastUsedAt: z5.string().optional()
1609
+ active: z5.boolean(),
1610
+ applied: z5.boolean(),
1611
+ switchedAway: z5.object({ profile: nonempty, email: z5.string().optional() }).optional()
1612
+ });
1613
+ var toolsSchema = z5.array(z5.object({
1614
+ id: nonempty,
1615
+ label: nonempty,
1616
+ envVar: nonempty,
1617
+ defaultDir: nonempty,
1618
+ bin: nonempty
1619
+ }));
1620
+ function unique(rows, identity) {
1621
+ if (new Set(rows.map(identity)).size !== rows.length) {
1622
+ throw new Error("subscriptions: duplicate identity in metadata response");
1623
+ }
1624
+ return rows;
1625
+ }
1626
+
1627
+ class SubscriptionsRegistry {
1628
+ read;
1629
+ transport = "api";
1630
+ registry = "subscriptions";
1631
+ env;
1632
+ profiles;
1633
+ tools;
1634
+ constructor(baseUrl, env, read = readSubscriptionsMetadata) {
1635
+ this.read = read;
1636
+ this.env = { ...env, HASNA_SUBSCRIPTIONS_API_URL: baseUrl.replace(/\/v1\/?$/, "") };
1637
+ }
1638
+ allProfiles() {
1639
+ return this.profiles ??= this.read(["list", "--json"], this.env).then((raw) => {
1640
+ const parsed = z5.array(profileSchema).safeParse(raw);
1641
+ if (!parsed.success)
1642
+ throw new Error("subscriptions: invalid profile metadata response");
1643
+ return unique(parsed.data, (row) => JSON.stringify([row.tool, row.name]));
1644
+ });
1645
+ }
1646
+ async listProfiles(tool) {
1647
+ const rows = await this.allProfiles();
1648
+ return rows.filter((row) => !tool || row.tool === tool);
1649
+ }
1650
+ async findProfile(name, tool) {
1651
+ const matches = (await this.listProfiles(tool)).filter((row) => row.name === name);
1652
+ return matches.length === 1 ? matches[0] : undefined;
1653
+ }
1654
+ async appliedProfile(tool) {
1655
+ const matches = (await this.allProfiles()).filter((row) => row.tool === tool && row.applied);
1656
+ if (matches.length > 1)
1657
+ throw new Error("subscriptions: ambiguous station applied-profile binding");
1658
+ return matches[0];
1659
+ }
1660
+ async currentProfile(_tool) {
1661
+ return;
1662
+ }
1663
+ async listTools() {
1664
+ return this.tools ??= this.read(["tools", "list", "--json"], this.env).then((raw) => {
1665
+ const parsed = toolsSchema.safeParse(raw);
1666
+ if (!parsed.success)
1667
+ throw new Error("subscriptions: invalid tool metadata response");
1668
+ return unique(parsed.data, (row) => row.id);
1669
+ });
1670
+ }
1671
+ }
1672
+
1673
+ // src/lib/accounts-store.ts
1674
+ var ACCOUNTS_APP = "accounts";
1675
+ var profileNameSchema = z6.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
1676
+ var profileSchema2 = z6.object({
1677
+ name: z6.string(),
1678
+ tool: z6.string(),
1679
+ email: z6.string().optional(),
1680
+ displayName: z6.string().optional(),
1681
+ dir: z6.string(),
1682
+ description: z6.string().optional(),
1683
+ createdAt: z6.string(),
1684
+ lastUsedAt: z6.string().optional(),
1685
+ switchedAway: z6.object({ profile: z6.string().min(1), email: z6.string().optional() }).optional()
1579
1686
  });
1580
- var toolDefSchema = z5.object({
1581
- id: z5.string(),
1582
- label: z5.string(),
1583
- envVar: z5.string(),
1584
- defaultDir: z5.string(),
1585
- bin: z5.string()
1687
+ var toolDefSchema = z6.object({
1688
+ id: z6.string(),
1689
+ label: z6.string(),
1690
+ envVar: z6.string(),
1691
+ defaultDir: z6.string(),
1692
+ bin: z6.string()
1586
1693
  });
1587
- var storeSchema = z5.object({
1588
- version: z5.literal(1),
1589
- current: z5.record(z5.string(), z5.string()).default({}),
1590
- applied: z5.record(z5.string(), z5.string()).default({}),
1591
- toolLocks: z5.record(z5.string(), z5.string()).default({}),
1592
- profiles: z5.array(profileSchema).default([]),
1593
- tools: z5.array(toolDefSchema).default([])
1694
+ var storeSchema = z6.object({
1695
+ version: z6.literal(1),
1696
+ current: z6.record(z6.string(), z6.string()).default({}),
1697
+ applied: z6.record(z6.string(), z6.string()).default({}),
1698
+ toolLocks: z6.record(z6.string(), z6.string()).default({}),
1699
+ profiles: z6.array(profileSchema2).default([]),
1700
+ tools: z6.array(toolDefSchema).default([])
1594
1701
  });
1595
1702
  function localRegistryAllowed(env = process.env) {
1596
1703
  return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => {
@@ -1872,25 +1979,34 @@ class ApiStore {
1872
1979
  }
1873
1980
  }
1874
1981
  function resolveStore(env = process.env, options = {}) {
1982
+ const legacy = resolveRegistryTransport(ACCOUNTS_APP, env, options.credentials);
1983
+ if (legacy)
1984
+ return new ApiStore(legacy.baseUrl, legacy.apiKey);
1985
+ const current = resolveRegistryTransport("subscriptions", env, options.subscriptionsCredentials);
1986
+ if (current)
1987
+ return new SubscriptionsRegistry(current.baseUrl, env, options.readSubscriptionsMetadata);
1988
+ return unconfiguredStore(env);
1989
+ }
1990
+ function resolveRegistryTransport(app, env, credentials) {
1875
1991
  const clientEnv = env;
1876
- const credential = resolveCredential(ACCOUNTS_APP, clientEnv, options.credentials);
1992
+ const credential = resolveCredential(app, clientEnv, credentials);
1877
1993
  if (credential === null) {
1878
1994
  try {
1879
- resolveClientTransport(ACCOUNTS_APP, clientEnv, options.credentials ? { credentials: options.credentials } : {});
1995
+ resolveClientTransport(app, clientEnv, credentials ? { credentials } : {});
1880
1996
  } catch (err) {
1881
1997
  if (isNoAccountsCredentialConfigurationError(err))
1882
- return unconfiguredStore(env);
1998
+ return null;
1883
1999
  throw err;
1884
2000
  }
1885
- return unconfiguredStore(env);
2001
+ return null;
1886
2002
  }
1887
2003
  if (credential.tier === "pointer") {
1888
- throw new Error(`accounts: ${credential.source} names a secrets-vault item, and the local accounts API store resolves ` + "credentials synchronously, so it cannot complete the pointer per request. It is a deliberate " + "selection and is not resolved around. Use a literal tier instead \u2014 the Keychain item " + "hasna.credentials.accounts.api-key, ~/.hasna/accounts/config/credentials, or HASNA_ACCOUNTS_API_KEY.");
2004
+ throw new Error(`${app}: ${credential.source} names a secrets-vault item, and the registry adapter resolves ` + "credentials synchronously, so it cannot complete the pointer per request. It is a deliberate " + "selection and is not resolved around. Use a literal tier instead \u2014 the Keychain item " + `hasna.credentials.${app}.api-key, ~/.hasna/${app}/config/credentials, or HASNA_${app.toUpperCase()}_API_KEY.`);
1889
2005
  }
1890
- const resolution = resolveClientTransport(ACCOUNTS_APP, clientEnv, {
1891
- credentials: { ...options.credentials, apiKey: credential.apiKey }
2006
+ const resolution = resolveClientTransport(app, clientEnv, {
2007
+ credentials: { ...credentials, apiKey: credential.apiKey }
1892
2008
  });
1893
- return new ApiStore(resolution.baseUrl, credential.apiKey);
2009
+ return { baseUrl: resolution.baseUrl, apiKey: credential.apiKey };
1894
2010
  }
1895
2011
  function isNoAccountsCredentialConfigurationError(error) {
1896
2012
  if (typeof error !== "object" || error === null)
@@ -1907,10 +2023,10 @@ function unconfiguredStore(env) {
1907
2023
  return new NoRegistryStore;
1908
2024
  }
1909
2025
  function accountsLocalModeNotice() {
1910
- return `accounts: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 no Hasna Accounts credential resolved, so ` + "attribution reads the on-box JSON registry (~/.hasna/accounts/accounts.json) instead of accounts-serve. " + "To go hosted, put the fleet key in the Keychain item hasna.credentials.accounts.api-key, write " + "~/.hasna/accounts/config/credentials, or set HASNA_ACCOUNTS_API_KEY.";
2026
+ return `accounts: LOCAL mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 neither Accounts nor Subscriptions credentials resolved, so ` + "attribution reads the on-box JSON registry (~/.hasna/accounts/accounts.json) instead of accounts-serve. " + "For hosted attribution, configure the Subscriptions CLI through its normal sign-in or credential resolver.";
1911
2027
  }
1912
2028
  function accountsRefusalNotice() {
1913
- return "accounts: no Hasna Accounts credential resolved (Keychain item hasna.credentials.accounts.api-key, " + "~/.hasna/accounts/config/credentials, HASNA_ACCOUNTS_API_KEY), so spend attribution is omitted. The " + `on-box registry (~/.hasna/accounts/accounts.json) is NOT read without ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 \u2014 ` + "economy fails closed instead of attributing hosted spend from an on-box file.";
2029
+ return "accounts: neither Accounts nor Subscriptions credentials resolved (legacy hasna.credentials.accounts.api-key, " + "~/.hasna/accounts/config/credentials, HASNA_ACCOUNTS_API_KEY; current hasna.credentials.subscriptions.api-key, " + "~/.hasna/subscriptions/config/credentials, HASNA_SUBSCRIPTIONS_API_KEY), so spend attribution is omitted. The " + `on-box registry (~/.hasna/accounts/accounts.json) is NOT read without ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 \u2014 ` + "economy fails closed instead of attributing hosted spend from an on-box file.";
1914
2030
  }
1915
2031
  var accountsLocalNoticePrinted = false;
1916
2032
  function announceAccountsLocalMode() {
@@ -1998,7 +2114,10 @@ async function profileForEnvDir(store, tool, env) {
1998
2114
  if (!configuredDir)
1999
2115
  return null;
2000
2116
  const normalized = normalizeDir(configuredDir);
2001
- return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
2117
+ const matches = (await store.listProfiles(tool.id)).filter((profile) => normalizeDir(profile.dir) === normalized);
2118
+ if (matches.length > 1)
2119
+ throw new Error("account attribution: multiple profiles match the configured directory");
2120
+ return matches[0] ?? null;
2002
2121
  }
2003
2122
  async function resolveAccountForAgent(agent, env = process.env, options = {}) {
2004
2123
  const override = envOverride(agent, env);
@@ -2012,8 +2131,15 @@ async function resolveAccountForAgent(agent, env = process.env, options = {}) {
2012
2131
  continue;
2013
2132
  const envProfile = await profileForEnvDir(store, tool, env);
2014
2133
  if (envProfile)
2015
- return fromProfile(envProfile, "env");
2016
- const appliedName = appliedProfileName(toolId);
2134
+ return envProfile.switchedAway ? null : fromProfile(envProfile, "env");
2135
+ if (store.registry === "subscriptions" && env[tool.envVar])
2136
+ return null;
2137
+ if (store.appliedProfile) {
2138
+ const applied = await store.appliedProfile(toolId);
2139
+ if (applied)
2140
+ return applied.switchedAway ? null : fromProfile(applied, "applied");
2141
+ }
2142
+ const appliedName = store.registry === "subscriptions" ? undefined : appliedProfileName(toolId);
2017
2143
  if (appliedName) {
2018
2144
  const applied = await store.findProfile(appliedName, toolId);
2019
2145
  if (applied)
@@ -2038,12 +2164,41 @@ function withAccount(record, account) {
2038
2164
  };
2039
2165
  }
2040
2166
 
2167
+ // src/lib/paths.ts
2168
+ import { homedir as homedir2 } from "os";
2169
+ import { join as join6 } from "path";
2170
+ function getHomeDir() {
2171
+ return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir2();
2172
+ }
2173
+ function agentPaths() {
2174
+ const home = getHomeDir();
2175
+ const claudeHome = process.env["CLAUDE_CONFIG_DIR"] || join6(home, ".claude");
2176
+ const codexHome = process.env["CODEX_HOME"] || join6(home, ".codex");
2177
+ return {
2178
+ claudeProjects: join6(claudeHome, "projects"),
2179
+ claudeCredentials: join6(claudeHome, ".credentials.json"),
2180
+ takumiProjects: join6(home, ".takumi", "projects"),
2181
+ codexDir: codexHome,
2182
+ codexDb: join6(codexHome, "state_5.sqlite"),
2183
+ codexAuth: join6(codexHome, "auth.json"),
2184
+ codexConfig: join6(codexHome, "config.toml"),
2185
+ geminiTmp: join6(home, ".gemini", "tmp"),
2186
+ geminiHistory: join6(home, ".gemini", "history"),
2187
+ opencodeMessages: join6(home, ".local", "share", "opencode", "storage", "message"),
2188
+ piSessions: join6(home, ".pi", "agent", "sessions"),
2189
+ hermesDir: join6(home, ".hermes"),
2190
+ hermesDb: join6(home, ".hermes", "state.db")
2191
+ };
2192
+ }
2193
+
2041
2194
  // src/ingest/claude.ts
2042
2195
  function autoDetectProject(cwd, projects) {
2043
2196
  return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
2044
2197
  }
2045
- var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join6(homedir2(), ".claude", "projects");
2046
- var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join6(homedir2(), ".takumi", "projects");
2198
+ var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join7(homedir3(), ".takumi", "projects");
2199
+ function claudeProjectsDir() {
2200
+ return process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? agentPaths().claudeProjects;
2201
+ }
2047
2202
  function dirNameToPath(dirName) {
2048
2203
  return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
2049
2204
  }
@@ -2053,9 +2208,9 @@ function collectJsonlFiles(projectDir) {
2053
2208
  try {
2054
2209
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
2055
2210
  if (entry.isDirectory())
2056
- walk(join6(dir, entry.name));
2211
+ walk(join7(dir, entry.name));
2057
2212
  else if (entry.name.endsWith(".jsonl"))
2058
- files.push(join6(dir, entry.name));
2213
+ files.push(join7(dir, entry.name));
2059
2214
  }
2060
2215
  } catch {
2061
2216
  collectionProblem("failed", "source_unreadable");
@@ -2064,7 +2219,7 @@ function collectJsonlFiles(projectDir) {
2064
2219
  walk(projectDir);
2065
2220
  return files;
2066
2221
  }
2067
- async function ingestClaude(db, verbose = false, projectsDir = CLAUDE_PROJECTS_DIR) {
2222
+ async function ingestClaude(db, verbose = false, projectsDir = claudeProjectsDir()) {
2068
2223
  return ingestJsonlProjects(db, projectsDir, "claude", verbose);
2069
2224
  }
2070
2225
  async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_DIR) {
@@ -2086,7 +2241,7 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
2086
2241
  const ingestState = loadIngestState(db, agentName);
2087
2242
  const projectDirs = readdirSync3(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
2088
2243
  for (const projectDirEntry of projectDirs) {
2089
- const projectDirPath = join6(projectsDir, projectDirEntry.name);
2244
+ const projectDirPath = join7(projectsDir, projectDirEntry.name);
2090
2245
  const projectPath = dirNameToPath(projectDirEntry.name);
2091
2246
  const jsonlFiles = collectJsonlFiles(projectDirPath);
2092
2247
  for (const filePath of jsonlFiles) {
@@ -2235,24 +2390,19 @@ function supportsClaudeDataResidencyPricing(model) {
2235
2390
  // src/ingest/codex.ts
2236
2391
  import { existsSync as existsSync4, readFileSync as readFileSync5, statSync as statSync2 } from "fs";
2237
2392
  import { createHash as createHash3 } from "crypto";
2238
- import { homedir as homedir3 } from "os";
2239
- import { join as join7, basename as basename2 } from "path";
2240
- var DEFAULT_CODEX_DB_PATH = join7(homedir3(), ".codex", "state_5.sqlite");
2241
- var DEFAULT_CODEX_CONFIG_PATH = join7(homedir3(), ".codex", "config.toml");
2242
- var DEFAULT_CODEWITH_DB_PATH = join7(homedir3(), ".codewith", "state_5.sqlite");
2243
- var DEFAULT_CODEWITH_CONFIG_PATH = join7(homedir3(), ".codewith", "config.toml");
2393
+ import { join as join8, basename as basename2 } from "path";
2244
2394
  var CODEX_INGEST_VERSION = "rollout-events-v4";
2245
2395
  function codexDbPath() {
2246
- return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
2396
+ return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? agentPaths().codexDb;
2247
2397
  }
2248
2398
  function codexConfigPath() {
2249
- return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? DEFAULT_CODEX_CONFIG_PATH;
2399
+ return process.env["HASNA_ECONOMY_CODEX_CONFIG_PATH"] ?? agentPaths().codexConfig;
2250
2400
  }
2251
2401
  function codewithDbPath() {
2252
- return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? DEFAULT_CODEWITH_DB_PATH;
2402
+ return process.env["HASNA_ECONOMY_CODEWITH_DB_PATH"] ?? join8(getHomeDir(), ".codewith", "state_5.sqlite");
2253
2403
  }
2254
2404
  function codewithConfigPath() {
2255
- return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? DEFAULT_CODEWITH_CONFIG_PATH;
2405
+ return process.env["HASNA_ECONOMY_CODEWITH_CONFIG_PATH"] ?? join8(getHomeDir(), ".codewith", "config.toml");
2256
2406
  }
2257
2407
  function codexSources() {
2258
2408
  const explicitCodexPath = process.env["HASNA_ECONOMY_CODEX_DB_PATH"];
@@ -2264,7 +2414,7 @@ function codexSources() {
2264
2414
  sessionPrefix: "codex",
2265
2415
  stateSource: "codex"
2266
2416
  }];
2267
- if (!explicitCodexPath || explicitCodewithPath) {
2417
+ if (!explicitCodexPath && !process.env["CODEX_HOME"] || explicitCodewithPath) {
2268
2418
  sources.push({
2269
2419
  label: "Codewith",
2270
2420
  dbPath: codewithDbPath(),
@@ -2502,9 +2652,9 @@ async function ingestCodex(db, verbose = false) {
2502
2652
  // src/ingest/gemini.ts
2503
2653
  import { readdirSync as readdirSync4, readFileSync as readFileSync6, existsSync as existsSync5, statSync as statSync3 } from "fs";
2504
2654
  import { homedir as homedir4 } from "os";
2505
- import { join as join8, basename as basename3 } from "path";
2506
- var DEFAULT_GEMINI_TMP_DIR = join8(homedir4(), ".gemini", "tmp");
2507
- var DEFAULT_GEMINI_HISTORY_DIR = join8(homedir4(), ".gemini", "history");
2655
+ import { join as join9, basename as basename3 } from "path";
2656
+ var DEFAULT_GEMINI_TMP_DIR = join9(homedir4(), ".gemini", "tmp");
2657
+ var DEFAULT_GEMINI_HISTORY_DIR = join9(homedir4(), ".gemini", "history");
2508
2658
  function geminiTmpDir() {
2509
2659
  return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
2510
2660
  }
@@ -2526,7 +2676,7 @@ function listProjectDirs(...roots) {
2526
2676
  try {
2527
2677
  for (const entry of readdirSync4(root, { withFileTypes: true })) {
2528
2678
  if (entry.isDirectory())
2529
- dirs.add(join8(root, entry.name));
2679
+ dirs.add(join9(root, entry.name));
2530
2680
  }
2531
2681
  } catch {
2532
2682
  collectionProblem("failed", "source_unreadable");
@@ -2539,7 +2689,7 @@ function projectRoot(projectDir, chatData) {
2539
2689
  return chatData.projectPath;
2540
2690
  if (chatData.project_path)
2541
2691
  return chatData.project_path;
2542
- const rootFile = join8(projectDir, ".project_root");
2692
+ const rootFile = join9(projectDir, ".project_root");
2543
2693
  try {
2544
2694
  if (existsSync5(rootFile))
2545
2695
  return readFileSync6(rootFile, "utf-8").trim();
@@ -2564,12 +2714,12 @@ async function ingestGemini(db, verbose) {
2564
2714
  const account = await resolveAccountForAgent("gemini");
2565
2715
  const projectDirs = listProjectDirs(tmpDir, historyDir);
2566
2716
  for (const projectDir of projectDirs) {
2567
- const chatsDir = join8(projectDir, "chats");
2717
+ const chatsDir = join9(projectDir, "chats");
2568
2718
  if (!existsSync5(chatsDir))
2569
2719
  continue;
2570
2720
  let chatFiles = [];
2571
2721
  try {
2572
- chatFiles = readdirSync4(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join8(chatsDir, f));
2722
+ chatFiles = readdirSync4(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join9(chatsDir, f));
2573
2723
  } catch {
2574
2724
  collectionProblem("failed", "source_unreadable");
2575
2725
  continue;
@@ -2679,13 +2829,13 @@ async function ingestGemini(db, verbose) {
2679
2829
  // src/ingest/opencode.ts
2680
2830
  import { existsSync as existsSync6, readFileSync as readFileSync7, readdirSync as readdirSync5, statSync as statSync4 } from "fs";
2681
2831
  import { homedir as homedir5 } from "os";
2682
- import { join as join9 } from "path";
2683
- var OPENCODE_STORAGE = join9(homedir5(), ".local", "share", "opencode", "storage");
2832
+ import { join as join10 } from "path";
2833
+ var OPENCODE_STORAGE = join10(homedir5(), ".local", "share", "opencode", "storage");
2684
2834
  function walkJsonFiles(dir, acc = []) {
2685
2835
  if (!existsSync6(dir))
2686
2836
  return acc;
2687
2837
  for (const entry of readdirSync5(dir, { withFileTypes: true })) {
2688
- const p = join9(dir, entry.name);
2838
+ const p = join10(dir, entry.name);
2689
2839
  if (entry.isDirectory())
2690
2840
  walkJsonFiles(p, acc);
2691
2841
  else if (entry.name.endsWith(".json"))
@@ -2705,7 +2855,7 @@ function parseSessionIdFromPath(filePath) {
2705
2855
  return null;
2706
2856
  }
2707
2857
  async function ingestOpenCode(db, verbose = false, sourceDir = OPENCODE_STORAGE) {
2708
- const messageDir = join9(sourceDir, "message");
2858
+ const messageDir = join10(sourceDir, "message");
2709
2859
  if (!existsSync6(messageDir))
2710
2860
  collectionProblem("missing", "source_missing");
2711
2861
  const files = walkJsonFiles(messageDir);
@@ -2897,13 +3047,13 @@ async function ingestCursor(db, verbose = false) {
2897
3047
  // src/ingest/pi.ts
2898
3048
  import { existsSync as existsSync7, readFileSync as readFileSync8, readdirSync as readdirSync6, statSync as statSync5 } from "fs";
2899
3049
  import { homedir as homedir6 } from "os";
2900
- import { join as join10 } from "path";
2901
- var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join10(homedir6(), ".pi", "agent", "sessions");
3050
+ import { join as join11 } from "path";
3051
+ var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join11(homedir6(), ".pi", "agent", "sessions");
2902
3052
  function walkSessions(dir, acc = []) {
2903
3053
  if (!existsSync7(dir))
2904
3054
  return acc;
2905
3055
  for (const entry of readdirSync6(dir, { withFileTypes: true })) {
2906
- const p = join10(dir, entry.name);
3056
+ const p = join11(dir, entry.name);
2907
3057
  if (entry.isDirectory())
2908
3058
  walkSessions(p, acc);
2909
3059
  else if (entry.name.endsWith(".json"))
@@ -3003,8 +3153,8 @@ async function ingestPi(db, verbose = false, sourceDir = PI_SESSION_DIR) {
3003
3153
  // src/ingest/hermes.ts
3004
3154
  import { existsSync as existsSync8, statSync as statSync6 } from "fs";
3005
3155
  import { homedir as homedir7 } from "os";
3006
- import { join as join11 } from "path";
3007
- var HERMES_DB = join11(homedir7(), ".hermes", "state.db");
3156
+ import { join as join12 } from "path";
3157
+ var HERMES_DB = join12(homedir7(), ".hermes", "state.db");
3008
3158
  async function ingestHermes(db, verbose = false, sourcePath = HERMES_DB) {
3009
3159
  if (!existsSync8(sourcePath)) {
3010
3160
  collectionProblem("missing", "source_missing");
@@ -3072,7 +3222,7 @@ function statSyncSafe(path) {
3072
3222
  // src/ingest/loops.ts
3073
3223
  import { existsSync as existsSync9 } from "fs";
3074
3224
  import { homedir as homedir8 } from "os";
3075
- import { join as join12 } from "path";
3225
+ import { join as join13 } from "path";
3076
3226
  import { homedir as pathsResolverHomedir } from "os";
3077
3227
  import { join as pathsResolverJoin } from "path";
3078
3228
  var PATHS_RESOLVER_KIND_ENV = {
@@ -3139,8 +3289,8 @@ function defaultLoopsDbPath() {
3139
3289
  app: "loops",
3140
3290
  home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
3141
3291
  });
3142
- const legacy = join12(process.env["HOME"] || process.env["USERPROFILE"] || homedir8(), ".hasna", "loops", "loops.db");
3143
- return existsSync9(join12(resolved, "loops.db")) ? join12(resolved, "loops.db") : legacy;
3292
+ const legacy = join13(process.env["HOME"] || process.env["USERPROFILE"] || homedir8(), ".hasna", "loops", "loops.db");
3293
+ return existsSync9(join13(resolved, "loops.db")) ? join13(resolved, "loops.db") : legacy;
3144
3294
  }
3145
3295
  function loopsDbPath() {
3146
3296
  return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
@@ -3324,33 +3474,6 @@ async function ingestLoops(db, verbose = false) {
3324
3474
 
3325
3475
  // src/ingest/claude-quota.ts
3326
3476
  import { existsSync as existsSync10, readFileSync as readFileSync9 } from "fs";
3327
-
3328
- // src/lib/paths.ts
3329
- import { homedir as homedir9 } from "os";
3330
- import { join as join13 } from "path";
3331
- function getHomeDir() {
3332
- return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir9();
3333
- }
3334
- function agentPaths() {
3335
- const home = getHomeDir();
3336
- return {
3337
- claudeProjects: join13(home, ".claude", "projects"),
3338
- claudeCredentials: join13(home, ".claude", ".credentials.json"),
3339
- takumiProjects: join13(home, ".takumi", "projects"),
3340
- codexDir: join13(home, ".codex"),
3341
- codexDb: join13(home, ".codex", "state_5.sqlite"),
3342
- codexAuth: join13(home, ".codex", "auth.json"),
3343
- codexConfig: join13(home, ".codex", "config.toml"),
3344
- geminiTmp: join13(home, ".gemini", "tmp"),
3345
- geminiHistory: join13(home, ".gemini", "history"),
3346
- opencodeMessages: join13(home, ".local", "share", "opencode", "storage", "message"),
3347
- piSessions: join13(home, ".pi", "agent", "sessions"),
3348
- hermesDir: join13(home, ".hermes"),
3349
- hermesDb: join13(home, ".hermes", "state.db")
3350
- };
3351
- }
3352
-
3353
- // src/ingest/claude-quota.ts
3354
3477
  var USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
3355
3478
  var OAUTH_BETA = "oauth-2025-04-20";
3356
3479
  function readClaudeToken() {
@@ -5332,17 +5455,17 @@ function buildServer() {
5332
5455
  function textError(message) {
5333
5456
  return { content: [{ type: "text", text: message }], isError: true };
5334
5457
  }
5335
- server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z6.string().optional() }, async ({ query }) => {
5458
+ server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z7.string().optional() }, async ({ query }) => {
5336
5459
  const q2 = query?.toLowerCase();
5337
5460
  const matches = q2 ? TOOL_NAMES.filter((name) => name.includes(q2)) : [...TOOL_NAMES];
5338
5461
  return text(matches.join(", "));
5339
5462
  });
5340
- server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z6.array(z6.string()) }, async ({ names }) => {
5463
+ server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z7.array(z7.string()) }, async ({ names }) => {
5341
5464
  const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
5342
5465
  `);
5343
5466
  return text(result);
5344
5467
  });
5345
- server.tool("get_cost_summary", "Stored usage-value summary with pricing coverage; never a paid-spend or complete-source claim. period: today|week|month|year|all. machine: filter by hostname.", { period: z6.enum(["today", "week", "month", "year", "all"]).optional(), machine: z6.string().optional() }, async ({ period, machine }) => {
5468
+ server.tool("get_cost_summary", "Stored usage-value summary with pricing coverage; never a paid-spend or complete-source claim. period: today|week|month|year|all. machine: filter by hostname.", { period: z7.enum(["today", "week", "month", "year", "all"]).optional(), machine: z7.string().optional() }, async ({ period, machine }) => {
5346
5469
  const resolved = period ?? "today";
5347
5470
  const s = await store.summary(resolved, machine);
5348
5471
  const machineLabel = machine ? ` on ${machine}` : "";
@@ -5358,11 +5481,11 @@ function buildServer() {
5358
5481
  `));
5359
5482
  });
5360
5483
  server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, account, machine, limit(20)", {
5361
- agent: z6.enum(AGENTS).optional(),
5362
- project: z6.string().optional(),
5363
- account: z6.string().optional(),
5364
- machine: z6.string().optional(),
5365
- limit: z6.number().int().positive().max(100).optional()
5484
+ agent: z7.enum(AGENTS).optional(),
5485
+ project: z7.string().optional(),
5486
+ account: z7.string().optional(),
5487
+ machine: z7.string().optional(),
5488
+ limit: z7.number().int().positive().max(100).optional()
5366
5489
  }, async ({ agent, project, account, machine, limit }) => {
5367
5490
  const sessions = await store.sessions({ agent, project, account, machine, limit: limit ?? 20 });
5368
5491
  const lines = ["id agent cost tokens project"];
@@ -5372,8 +5495,8 @@ function buildServer() {
5372
5495
  `));
5373
5496
  });
5374
5497
  server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
5375
- n: z6.number().int().positive().max(100).optional(),
5376
- agent: z6.enum(AGENTS).optional()
5498
+ n: z7.number().int().positive().max(100).optional(),
5499
+ agent: z7.enum(AGENTS).optional()
5377
5500
  }, async ({ n, agent }) => {
5378
5501
  const sessions = await store.topSessions(n ?? 10, agent);
5379
5502
  const lines = ["rank id agent cost tokens project"];
@@ -5381,7 +5504,7 @@ function buildServer() {
5381
5504
  return text(lines.join(`
5382
5505
  `));
5383
5506
  });
5384
- server.tool("get_model_breakdown", "Cost per model. Params: limit(20), verbose, json.", { limit: z6.number().int().positive().max(100).optional(), verbose: z6.boolean().optional(), json: z6.boolean().optional() }, async ({ limit, verbose, json }) => {
5507
+ server.tool("get_model_breakdown", "Cost per model. Params: limit(20), verbose, json.", { limit: z7.number().int().positive().max(100).optional(), verbose: z7.boolean().optional(), json: z7.boolean().optional() }, async ({ limit, verbose, json }) => {
5385
5508
  const rows = await store.modelBreakdown();
5386
5509
  if (json)
5387
5510
  return text(JSON.stringify(rows, null, 2));
@@ -5396,7 +5519,7 @@ function buildServer() {
5396
5519
  return text(lines.join(`
5397
5520
  `));
5398
5521
  });
5399
- server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all), limit(20), verbose, json.", { period: z6.enum(["today", "week", "month", "year", "all"]).optional(), limit: z6.number().int().positive().max(100).optional(), verbose: z6.boolean().optional(), json: z6.boolean().optional() }, async ({ period, limit, verbose, json }) => {
5522
+ server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all), limit(20), verbose, json.", { period: z7.enum(["today", "week", "month", "year", "all"]).optional(), limit: z7.number().int().positive().max(100).optional(), verbose: z7.boolean().optional(), json: z7.boolean().optional() }, async ({ period, limit, verbose, json }) => {
5400
5523
  const rows = await store.projectBreakdown({ period: period ?? "all" });
5401
5524
  if (json)
5402
5525
  return text(JSON.stringify(rows, null, 2));
@@ -5412,7 +5535,7 @@ function buildServer() {
5412
5535
  return text(lines.join(`
5413
5536
  `));
5414
5537
  });
5415
- 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: z6.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
5538
+ 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: z7.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
5416
5539
  const rows = await store.agentBreakdown({ period: period ?? "all" });
5417
5540
  if (rows.length === 0)
5418
5541
  return text("No agent usage yet.");
@@ -5423,7 +5546,7 @@ function buildServer() {
5423
5546
  return text(lines.join(`
5424
5547
  `));
5425
5548
  });
5426
- 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: z6.enum(["today", "week", "month", "year", "all"]).optional(), limit: z6.number().int().positive().max(100).optional(), verbose: z6.boolean().optional(), json: z6.boolean().optional() }, async ({ period, limit, verbose, json }) => {
5549
+ 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: z7.enum(["today", "week", "month", "year", "all"]).optional(), limit: z7.number().int().positive().max(100).optional(), verbose: z7.boolean().optional(), json: z7.boolean().optional() }, async ({ period, limit, verbose, json }) => {
5427
5550
  const rows = await store.accountBreakdown({ period: period ?? "all" });
5428
5551
  if (json)
5429
5552
  return text(JSON.stringify(rows, null, 2));
@@ -5442,12 +5565,12 @@ function buildServer() {
5442
5565
  `));
5443
5566
  });
5444
5567
  server.tool("get_cost_center_breakdown", "Cost per cost center. Params: period, kind, cursor(0), limit(20), verbose, json. json=true preserves the complete legacy array.", {
5445
- period: z6.enum(["today", "week", "month", "year", "all"]).optional(),
5446
- kind: z6.enum(["loop", "app", "repo", "service", "team"]).optional(),
5447
- cursor: z6.number().int().nonnegative().optional(),
5448
- limit: z6.number().int().positive().max(100).optional(),
5449
- verbose: z6.boolean().optional(),
5450
- json: z6.boolean().optional()
5568
+ period: z7.enum(["today", "week", "month", "year", "all"]).optional(),
5569
+ kind: z7.enum(["loop", "app", "repo", "service", "team"]).optional(),
5570
+ cursor: z7.number().int().nonnegative().optional(),
5571
+ limit: z7.number().int().positive().max(100).optional(),
5572
+ verbose: z7.boolean().optional(),
5573
+ json: z7.boolean().optional()
5451
5574
  }, async ({ period, kind, cursor, limit, verbose, json }) => {
5452
5575
  const rows = await store.costCenterBreakdown({ period: period ?? "all", kind });
5453
5576
  if (json)
@@ -5470,7 +5593,7 @@ function buildServer() {
5470
5593
  return text(lines.join(`
5471
5594
  `));
5472
5595
  });
5473
- server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. Params: limit(20), verbose, json.", { limit: z6.number().int().positive().max(100).optional(), verbose: z6.boolean().optional(), json: z6.boolean().optional() }, async ({ limit, verbose, json }) => {
5596
+ server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. Params: limit(20), verbose, json.", { limit: z7.number().int().positive().max(100).optional(), verbose: z7.boolean().optional(), json: z7.boolean().optional() }, async ({ limit, verbose, json }) => {
5474
5597
  const budgets = await store.listBudgets();
5475
5598
  if (json)
5476
5599
  return text(JSON.stringify(budgets, null, 2));
@@ -5491,12 +5614,12 @@ function buildServer() {
5491
5614
  `));
5492
5615
  });
5493
5616
  server.tool("set_budget", "Create a spending budget. period: daily|weekly|monthly. limit_usd must be positive. alert_at_percent defaults to 80.", {
5494
- period: z6.enum(["daily", "weekly", "monthly"]),
5495
- limit_usd: z6.number().positive(),
5496
- project_path: z6.string().optional(),
5497
- agent: z6.enum(AGENTS).optional(),
5498
- cost_center_id: z6.string().optional(),
5499
- alert_at_percent: z6.number().positive().max(100).optional()
5617
+ period: z7.enum(["daily", "weekly", "monthly"]),
5618
+ limit_usd: z7.number().positive(),
5619
+ project_path: z7.string().optional(),
5620
+ agent: z7.enum(AGENTS).optional(),
5621
+ cost_center_id: z7.string().optional(),
5622
+ alert_at_percent: z7.number().positive().max(100).optional()
5500
5623
  }, async ({ period, limit_usd, project_path, agent, cost_center_id, alert_at_percent }) => {
5501
5624
  const id = await store.setBudget({
5502
5625
  project_path: project_path ?? null,
@@ -5508,11 +5631,11 @@ function buildServer() {
5508
5631
  });
5509
5632
  return text(`Budget set: ${id}`);
5510
5633
  });
5511
- server.tool("remove_budget", "Delete a budget by id.", { id: z6.string() }, async ({ id }) => {
5634
+ server.tool("remove_budget", "Delete a budget by id.", { id: z7.string() }, async ({ id }) => {
5512
5635
  await store.removeBudget(id);
5513
5636
  return text("Budget removed.");
5514
5637
  });
5515
- server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage. Params: limit(20), verbose, json.", { limit: z6.number().int().positive().max(100).optional(), verbose: z6.boolean().optional(), json: z6.boolean().optional() }, async ({ limit, verbose, json }) => {
5638
+ server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage. Params: limit(20), verbose, json.", { limit: z7.number().int().positive().max(100).optional(), verbose: z7.boolean().optional(), json: z7.boolean().optional() }, async ({ limit, verbose, json }) => {
5516
5639
  const rows = await store.listPricing();
5517
5640
  if (json)
5518
5641
  return text(JSON.stringify(rows, null, 2));
@@ -5528,13 +5651,13 @@ function buildServer() {
5528
5651
  `));
5529
5652
  });
5530
5653
  server.tool("set_pricing", "Create or update a model pricing row. Values are USD per 1M tokens except cache_storage_per_1m_hour.", {
5531
- model: z6.string().min(1),
5532
- input_per_1m: z6.number().nonnegative(),
5533
- output_per_1m: z6.number().nonnegative(),
5534
- cache_read_per_1m: z6.number().nonnegative().optional(),
5535
- cache_write_per_1m: z6.number().nonnegative().optional(),
5536
- cache_write_1h_per_1m: z6.number().nonnegative().optional(),
5537
- cache_storage_per_1m_hour: z6.number().nonnegative().optional()
5654
+ model: z7.string().min(1),
5655
+ input_per_1m: z7.number().nonnegative(),
5656
+ output_per_1m: z7.number().nonnegative(),
5657
+ cache_read_per_1m: z7.number().nonnegative().optional(),
5658
+ cache_write_per_1m: z7.number().nonnegative().optional(),
5659
+ cache_write_1h_per_1m: z7.number().nonnegative().optional(),
5660
+ cache_storage_per_1m_hour: z7.number().nonnegative().optional()
5538
5661
  }, async (input) => {
5539
5662
  const model = input.model.trim();
5540
5663
  if (!model)
@@ -5550,11 +5673,11 @@ function buildServer() {
5550
5673
  });
5551
5674
  return text(`Pricing set: ${model}`);
5552
5675
  });
5553
- server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z6.string() }, async ({ model }) => {
5676
+ server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z7.string() }, async ({ model }) => {
5554
5677
  await store.removePricing(model);
5555
5678
  return text("Pricing removed.");
5556
5679
  });
5557
- server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z6.number().int().positive().max(365).optional() }, async ({ days }) => {
5680
+ server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z7.number().int().positive().max(365).optional() }, async ({ days }) => {
5558
5681
  const rows = await store.daily(days ?? 30);
5559
5682
  const byDate = new Map;
5560
5683
  for (const row of rows) {
@@ -5578,7 +5701,7 @@ function buildServer() {
5578
5701
  return text(lines.join(`
5579
5702
  `));
5580
5703
  });
5581
- server.tool("get_billing_summary", "Recorded provider billing-row subtotals; complete account/source coverage is unverified. Params: period(today|yesterday|week|month|year|all)", { period: z6.enum(["today", "yesterday", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
5704
+ server.tool("get_billing_summary", "Recorded provider billing-row subtotals; complete account/source coverage is unverified. Params: period(today|yesterday|week|month|year|all)", { period: z7.enum(["today", "yesterday", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
5582
5705
  const summary = await store.billingSummary(period ?? "month");
5583
5706
  const lines = ["provider recorded subtotal"];
5584
5707
  for (const [provider, cost] of Object.entries(summary.by_provider)) {
@@ -5589,7 +5712,7 @@ function buildServer() {
5589
5712
  return text(lines.join(`
5590
5713
  `));
5591
5714
  });
5592
- server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok), limit(20), verbose.", { session_id: z6.string(), limit: z6.number().int().positive().max(100).optional(), verbose: z6.boolean().optional() }, async ({ session_id, limit, verbose }) => {
5715
+ server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok), limit(20), verbose.", { session_id: z7.string(), limit: z7.number().int().positive().max(100).optional(), verbose: z7.boolean().optional() }, async ({ session_id, limit, verbose }) => {
5593
5716
  const detail = await store.sessionDetail(session_id);
5594
5717
  if (!detail)
5595
5718
  return textError(`Session not found: ${session_id}`);
@@ -5613,7 +5736,7 @@ function buildServer() {
5613
5736
  return text(lines.join(`
5614
5737
  `));
5615
5738
  });
5616
- server.tool("sync", `Ingest new cost data. sources: ${SYNC_SOURCES.join("|")} ('loops' reads another app's on-box store and is refused for a hosted client). Set json=true for the full result object.`, { sources: z6.enum([...SYNC_SOURCES]).optional(), json: z6.boolean().optional() }, async ({ sources, json }) => {
5739
+ server.tool("sync", `Ingest new cost data. sources: ${SYNC_SOURCES.join("|")} ('loops' reads another app's on-box store and is refused for a hosted client). Set json=true for the full result object.`, { sources: z7.enum([...SYNC_SOURCES]).optional(), json: z7.boolean().optional() }, async ({ sources, json }) => {
5617
5740
  const selected = sources ?? "all";
5618
5741
  const opts = selected === "all" ? {} : { [selected]: true };
5619
5742
  if (isCloudStore()) {
@@ -5670,7 +5793,7 @@ function buildServer() {
5670
5793
  return { ...text(lines.join(`
5671
5794
  `)), isError: failed };
5672
5795
  });
5673
- 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: z6.enum(["today", "week", "month", "year", "all"]).optional(), agent: z6.enum(AGENTS).optional(), limit: z6.number().int().positive().max(100).optional(), json: z6.boolean().optional() }, async ({ period, agent, limit, json }) => {
5796
+ 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: z7.enum(["today", "week", "month", "year", "all"]).optional(), agent: z7.enum(AGENTS).optional(), limit: z7.number().int().positive().max(100).optional(), json: z7.boolean().optional() }, async ({ period, agent, limit, json }) => {
5674
5797
  const p = period ?? "month";
5675
5798
  const { snapshots: snaps, summary } = await store.usage(p, agent);
5676
5799
  if (json)
@@ -5693,7 +5816,7 @@ function buildServer() {
5693
5816
  return text(lines.join(`
5694
5817
  `));
5695
5818
  });
5696
- server.tool("get_savings", "Estimated subscription-included usage benefit versus mapped current plan fees; unavailable when evidence is missing. Paid API cost is not savings. Params: period, agent, limit(20), json.", { period: z6.enum(["today", "week", "month", "year", "all"]).optional(), agent: z6.enum(AGENTS).optional(), limit: z6.number().int().positive().max(100).optional(), json: z6.boolean().optional() }, async ({ period, agent, limit, json }) => {
5819
+ server.tool("get_savings", "Estimated subscription-included usage benefit versus mapped current plan fees; unavailable when evidence is missing. Paid API cost is not savings. Params: period, agent, limit(20), json.", { period: z7.enum(["today", "week", "month", "year", "all"]).optional(), agent: z7.enum(AGENTS).optional(), limit: z7.number().int().positive().max(100).optional(), json: z7.boolean().optional() }, async ({ period, agent, limit, json }) => {
5697
5820
  const savings = await store.savings(period ?? "month", agent);
5698
5821
  if (json)
5699
5822
  return text(JSON.stringify(savings, null, 2));
@@ -5734,17 +5857,17 @@ function buildServer() {
5734
5857
  `));
5735
5858
  });
5736
5859
  server.tool("set_subscription", "Configure an account subscription plan. Explicit amounts are configured observations; omitted amounts remain unknown.", {
5737
- id: z6.string().optional(),
5738
- provider: z6.string().min(1),
5739
- plan: z6.string().min(1),
5740
- account_key: z6.string().min(1).max(256),
5741
- agent: z6.enum(AGENTS).optional(),
5742
- monthly_fee_usd: z6.number().nonnegative().optional(),
5743
- included_usage_usd: z6.number().nonnegative().optional(),
5744
- billing_cycle_start: z6.string().optional(),
5745
- reset_policy: z6.string().optional(),
5746
- active: z6.boolean().optional(),
5747
- json: z6.boolean().optional()
5860
+ id: z7.string().optional(),
5861
+ provider: z7.string().min(1),
5862
+ plan: z7.string().min(1),
5863
+ account_key: z7.string().min(1).max(256),
5864
+ agent: z7.enum(AGENTS).optional(),
5865
+ monthly_fee_usd: z7.number().nonnegative().optional(),
5866
+ included_usage_usd: z7.number().nonnegative().optional(),
5867
+ billing_cycle_start: z7.string().optional(),
5868
+ reset_policy: z7.string().optional(),
5869
+ active: z7.boolean().optional(),
5870
+ json: z7.boolean().optional()
5748
5871
  }, async (input) => {
5749
5872
  const row = await store.setSubscription({
5750
5873
  id: input.id,
@@ -5768,15 +5891,15 @@ agent: ${row.agent ?? "unallocated"}
5768
5891
  fee: ${money.fee}
5769
5892
  included: ${money.included}`);
5770
5893
  });
5771
- server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z6.string() }, async ({ id }) => {
5894
+ server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z7.string() }, async ({ id }) => {
5772
5895
  await store.removeSubscription(id);
5773
5896
  return text("Subscription removed.");
5774
5897
  });
5775
- server.tool("estimate_cost", "Pre-flight usage-value estimate for token counts; missing model pricing returns MODEL_PRICING_UNAVAILABLE, never a free estimate.", { model: z6.string().min(1), input_tokens: z6.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(), output_tokens: z6.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional() }, async ({ model, input_tokens, output_tokens }) => {
5898
+ server.tool("estimate_cost", "Pre-flight usage-value estimate for token counts; missing model pricing returns MODEL_PRICING_UNAVAILABLE, never a free estimate.", { model: z7.string().min(1), input_tokens: z7.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional(), output_tokens: z7.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).optional() }, async ({ model, input_tokens, output_tokens }) => {
5776
5899
  const cost = await store.estimate({ model, inputTokens: input_tokens ?? 0, outputTokens: output_tokens ?? 0 });
5777
5900
  return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
5778
5901
  });
5779
- server.tool("get_goals", "All spending goals with current progress. Params: limit(20), verbose, json.", { limit: z6.number().int().positive().max(100).optional(), verbose: z6.boolean().optional(), json: z6.boolean().optional() }, async ({ limit, verbose, json }) => {
5902
+ server.tool("get_goals", "All spending goals with current progress. Params: limit(20), verbose, json.", { limit: z7.number().int().positive().max(100).optional(), verbose: z7.boolean().optional(), json: z7.boolean().optional() }, async ({ limit, verbose, json }) => {
5780
5903
  const goals = await store.listGoals();
5781
5904
  if (json)
5782
5905
  return text(JSON.stringify(goals, null, 2));
@@ -5797,10 +5920,10 @@ included: ${money.included}`);
5797
5920
  `));
5798
5921
  });
5799
5922
  server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
5800
- period: z6.enum(["day", "week", "month", "year"]),
5801
- limit_usd: z6.number().positive(),
5802
- project_path: z6.string().optional(),
5803
- agent: z6.string().optional()
5923
+ period: z7.enum(["day", "week", "month", "year"]),
5924
+ limit_usd: z7.number().positive(),
5925
+ project_path: z7.string().optional(),
5926
+ agent: z7.string().optional()
5804
5927
  }, async ({ period, limit_usd, project_path, agent }) => {
5805
5928
  await store.setGoal({
5806
5929
  period,
@@ -5810,11 +5933,11 @@ included: ${money.included}`);
5810
5933
  });
5811
5934
  return text(`Goal set: ${period} $${limit_usd}`);
5812
5935
  });
5813
- server.tool("remove_goal", "Delete a goal by id.", { id: z6.string() }, async ({ id }) => {
5936
+ server.tool("remove_goal", "Delete a goal by id.", { id: z7.string() }, async ({ id }) => {
5814
5937
  await store.removeGoal(id);
5815
5938
  return text("Goal removed.");
5816
5939
  });
5817
- server.tool("list_machines", "List all machines that have synced data. Params: limit(20), verbose, json.", { limit: z6.number().int().positive().max(100).optional(), verbose: z6.boolean().optional(), json: z6.boolean().optional() }, async ({ limit, verbose, json }) => {
5940
+ server.tool("list_machines", "List all machines that have synced data. Params: limit(20), verbose, json.", { limit: z7.number().int().positive().max(100).optional(), verbose: z7.boolean().optional(), json: z7.boolean().optional() }, async ({ limit, verbose, json }) => {
5818
5941
  const machines = await store.machines();
5819
5942
  if (json)
5820
5943
  return text(JSON.stringify(machines, null, 2));
@@ -5836,9 +5959,9 @@ current machine: ${getMachineId()}`);
5836
5959
  registerAgentTools(server, { service: "economy" });
5837
5960
  registerIngestJournalTool(server);
5838
5961
  server.tool("send_feedback", "Send feedback about this service.", {
5839
- message: z6.string(),
5840
- email: z6.string().optional(),
5841
- category: z6.enum(["bug", "feature", "general"]).optional()
5962
+ message: z7.string(),
5963
+ email: z7.string().optional(),
5964
+ category: z7.enum(["bug", "feature", "general"]).optional()
5842
5965
  }, async ({ message, email, category }) => {
5843
5966
  try {
5844
5967
  await store.sendFeedback({ message, email, category });