@hasna/economy 0.5.3 → 0.5.4

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.4",
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";
@@ -1563,34 +1563,141 @@ import { join as join6, basename } from "path";
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)
@@ -5332,17 +5458,17 @@ function buildServer() {
5332
5458
  function textError(message) {
5333
5459
  return { content: [{ type: "text", text: message }], isError: true };
5334
5460
  }
5335
- server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z6.string().optional() }, async ({ query }) => {
5461
+ server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z7.string().optional() }, async ({ query }) => {
5336
5462
  const q2 = query?.toLowerCase();
5337
5463
  const matches = q2 ? TOOL_NAMES.filter((name) => name.includes(q2)) : [...TOOL_NAMES];
5338
5464
  return text(matches.join(", "));
5339
5465
  });
5340
- server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z6.array(z6.string()) }, async ({ names }) => {
5466
+ server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z7.array(z7.string()) }, async ({ names }) => {
5341
5467
  const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
5342
5468
  `);
5343
5469
  return text(result);
5344
5470
  });
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 }) => {
5471
+ 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
5472
  const resolved = period ?? "today";
5347
5473
  const s = await store.summary(resolved, machine);
5348
5474
  const machineLabel = machine ? ` on ${machine}` : "";
@@ -5358,11 +5484,11 @@ function buildServer() {
5358
5484
  `));
5359
5485
  });
5360
5486
  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()
5487
+ agent: z7.enum(AGENTS).optional(),
5488
+ project: z7.string().optional(),
5489
+ account: z7.string().optional(),
5490
+ machine: z7.string().optional(),
5491
+ limit: z7.number().int().positive().max(100).optional()
5366
5492
  }, async ({ agent, project, account, machine, limit }) => {
5367
5493
  const sessions = await store.sessions({ agent, project, account, machine, limit: limit ?? 20 });
5368
5494
  const lines = ["id agent cost tokens project"];
@@ -5372,8 +5498,8 @@ function buildServer() {
5372
5498
  `));
5373
5499
  });
5374
5500
  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()
5501
+ n: z7.number().int().positive().max(100).optional(),
5502
+ agent: z7.enum(AGENTS).optional()
5377
5503
  }, async ({ n, agent }) => {
5378
5504
  const sessions = await store.topSessions(n ?? 10, agent);
5379
5505
  const lines = ["rank id agent cost tokens project"];
@@ -5381,7 +5507,7 @@ function buildServer() {
5381
5507
  return text(lines.join(`
5382
5508
  `));
5383
5509
  });
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 }) => {
5510
+ 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
5511
  const rows = await store.modelBreakdown();
5386
5512
  if (json)
5387
5513
  return text(JSON.stringify(rows, null, 2));
@@ -5396,7 +5522,7 @@ function buildServer() {
5396
5522
  return text(lines.join(`
5397
5523
  `));
5398
5524
  });
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 }) => {
5525
+ 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
5526
  const rows = await store.projectBreakdown({ period: period ?? "all" });
5401
5527
  if (json)
5402
5528
  return text(JSON.stringify(rows, null, 2));
@@ -5412,7 +5538,7 @@ function buildServer() {
5412
5538
  return text(lines.join(`
5413
5539
  `));
5414
5540
  });
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 }) => {
5541
+ 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
5542
  const rows = await store.agentBreakdown({ period: period ?? "all" });
5417
5543
  if (rows.length === 0)
5418
5544
  return text("No agent usage yet.");
@@ -5423,7 +5549,7 @@ function buildServer() {
5423
5549
  return text(lines.join(`
5424
5550
  `));
5425
5551
  });
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 }) => {
5552
+ 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
5553
  const rows = await store.accountBreakdown({ period: period ?? "all" });
5428
5554
  if (json)
5429
5555
  return text(JSON.stringify(rows, null, 2));
@@ -5442,12 +5568,12 @@ function buildServer() {
5442
5568
  `));
5443
5569
  });
5444
5570
  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()
5571
+ period: z7.enum(["today", "week", "month", "year", "all"]).optional(),
5572
+ kind: z7.enum(["loop", "app", "repo", "service", "team"]).optional(),
5573
+ cursor: z7.number().int().nonnegative().optional(),
5574
+ limit: z7.number().int().positive().max(100).optional(),
5575
+ verbose: z7.boolean().optional(),
5576
+ json: z7.boolean().optional()
5451
5577
  }, async ({ period, kind, cursor, limit, verbose, json }) => {
5452
5578
  const rows = await store.costCenterBreakdown({ period: period ?? "all", kind });
5453
5579
  if (json)
@@ -5470,7 +5596,7 @@ function buildServer() {
5470
5596
  return text(lines.join(`
5471
5597
  `));
5472
5598
  });
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 }) => {
5599
+ 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
5600
  const budgets = await store.listBudgets();
5475
5601
  if (json)
5476
5602
  return text(JSON.stringify(budgets, null, 2));
@@ -5491,12 +5617,12 @@ function buildServer() {
5491
5617
  `));
5492
5618
  });
5493
5619
  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()
5620
+ period: z7.enum(["daily", "weekly", "monthly"]),
5621
+ limit_usd: z7.number().positive(),
5622
+ project_path: z7.string().optional(),
5623
+ agent: z7.enum(AGENTS).optional(),
5624
+ cost_center_id: z7.string().optional(),
5625
+ alert_at_percent: z7.number().positive().max(100).optional()
5500
5626
  }, async ({ period, limit_usd, project_path, agent, cost_center_id, alert_at_percent }) => {
5501
5627
  const id = await store.setBudget({
5502
5628
  project_path: project_path ?? null,
@@ -5508,11 +5634,11 @@ function buildServer() {
5508
5634
  });
5509
5635
  return text(`Budget set: ${id}`);
5510
5636
  });
5511
- server.tool("remove_budget", "Delete a budget by id.", { id: z6.string() }, async ({ id }) => {
5637
+ server.tool("remove_budget", "Delete a budget by id.", { id: z7.string() }, async ({ id }) => {
5512
5638
  await store.removeBudget(id);
5513
5639
  return text("Budget removed.");
5514
5640
  });
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 }) => {
5641
+ 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
5642
  const rows = await store.listPricing();
5517
5643
  if (json)
5518
5644
  return text(JSON.stringify(rows, null, 2));
@@ -5528,13 +5654,13 @@ function buildServer() {
5528
5654
  `));
5529
5655
  });
5530
5656
  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()
5657
+ model: z7.string().min(1),
5658
+ input_per_1m: z7.number().nonnegative(),
5659
+ output_per_1m: z7.number().nonnegative(),
5660
+ cache_read_per_1m: z7.number().nonnegative().optional(),
5661
+ cache_write_per_1m: z7.number().nonnegative().optional(),
5662
+ cache_write_1h_per_1m: z7.number().nonnegative().optional(),
5663
+ cache_storage_per_1m_hour: z7.number().nonnegative().optional()
5538
5664
  }, async (input) => {
5539
5665
  const model = input.model.trim();
5540
5666
  if (!model)
@@ -5550,11 +5676,11 @@ function buildServer() {
5550
5676
  });
5551
5677
  return text(`Pricing set: ${model}`);
5552
5678
  });
5553
- server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z6.string() }, async ({ model }) => {
5679
+ server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z7.string() }, async ({ model }) => {
5554
5680
  await store.removePricing(model);
5555
5681
  return text("Pricing removed.");
5556
5682
  });
5557
- server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z6.number().int().positive().max(365).optional() }, async ({ days }) => {
5683
+ server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z7.number().int().positive().max(365).optional() }, async ({ days }) => {
5558
5684
  const rows = await store.daily(days ?? 30);
5559
5685
  const byDate = new Map;
5560
5686
  for (const row of rows) {
@@ -5578,7 +5704,7 @@ function buildServer() {
5578
5704
  return text(lines.join(`
5579
5705
  `));
5580
5706
  });
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 }) => {
5707
+ 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
5708
  const summary = await store.billingSummary(period ?? "month");
5583
5709
  const lines = ["provider recorded subtotal"];
5584
5710
  for (const [provider, cost] of Object.entries(summary.by_provider)) {
@@ -5589,7 +5715,7 @@ function buildServer() {
5589
5715
  return text(lines.join(`
5590
5716
  `));
5591
5717
  });
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 }) => {
5718
+ 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
5719
  const detail = await store.sessionDetail(session_id);
5594
5720
  if (!detail)
5595
5721
  return textError(`Session not found: ${session_id}`);
@@ -5613,7 +5739,7 @@ function buildServer() {
5613
5739
  return text(lines.join(`
5614
5740
  `));
5615
5741
  });
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 }) => {
5742
+ 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
5743
  const selected = sources ?? "all";
5618
5744
  const opts = selected === "all" ? {} : { [selected]: true };
5619
5745
  if (isCloudStore()) {
@@ -5670,7 +5796,7 @@ function buildServer() {
5670
5796
  return { ...text(lines.join(`
5671
5797
  `)), isError: failed };
5672
5798
  });
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 }) => {
5799
+ 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
5800
  const p = period ?? "month";
5675
5801
  const { snapshots: snaps, summary } = await store.usage(p, agent);
5676
5802
  if (json)
@@ -5693,7 +5819,7 @@ function buildServer() {
5693
5819
  return text(lines.join(`
5694
5820
  `));
5695
5821
  });
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 }) => {
5822
+ 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
5823
  const savings = await store.savings(period ?? "month", agent);
5698
5824
  if (json)
5699
5825
  return text(JSON.stringify(savings, null, 2));
@@ -5734,17 +5860,17 @@ function buildServer() {
5734
5860
  `));
5735
5861
  });
5736
5862
  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()
5863
+ id: z7.string().optional(),
5864
+ provider: z7.string().min(1),
5865
+ plan: z7.string().min(1),
5866
+ account_key: z7.string().min(1).max(256),
5867
+ agent: z7.enum(AGENTS).optional(),
5868
+ monthly_fee_usd: z7.number().nonnegative().optional(),
5869
+ included_usage_usd: z7.number().nonnegative().optional(),
5870
+ billing_cycle_start: z7.string().optional(),
5871
+ reset_policy: z7.string().optional(),
5872
+ active: z7.boolean().optional(),
5873
+ json: z7.boolean().optional()
5748
5874
  }, async (input) => {
5749
5875
  const row = await store.setSubscription({
5750
5876
  id: input.id,
@@ -5768,15 +5894,15 @@ agent: ${row.agent ?? "unallocated"}
5768
5894
  fee: ${money.fee}
5769
5895
  included: ${money.included}`);
5770
5896
  });
5771
- server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z6.string() }, async ({ id }) => {
5897
+ server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z7.string() }, async ({ id }) => {
5772
5898
  await store.removeSubscription(id);
5773
5899
  return text("Subscription removed.");
5774
5900
  });
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 }) => {
5901
+ 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
5902
  const cost = await store.estimate({ model, inputTokens: input_tokens ?? 0, outputTokens: output_tokens ?? 0 });
5777
5903
  return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
5778
5904
  });
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 }) => {
5905
+ 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
5906
  const goals = await store.listGoals();
5781
5907
  if (json)
5782
5908
  return text(JSON.stringify(goals, null, 2));
@@ -5797,10 +5923,10 @@ included: ${money.included}`);
5797
5923
  `));
5798
5924
  });
5799
5925
  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()
5926
+ period: z7.enum(["day", "week", "month", "year"]),
5927
+ limit_usd: z7.number().positive(),
5928
+ project_path: z7.string().optional(),
5929
+ agent: z7.string().optional()
5804
5930
  }, async ({ period, limit_usd, project_path, agent }) => {
5805
5931
  await store.setGoal({
5806
5932
  period,
@@ -5810,11 +5936,11 @@ included: ${money.included}`);
5810
5936
  });
5811
5937
  return text(`Goal set: ${period} $${limit_usd}`);
5812
5938
  });
5813
- server.tool("remove_goal", "Delete a goal by id.", { id: z6.string() }, async ({ id }) => {
5939
+ server.tool("remove_goal", "Delete a goal by id.", { id: z7.string() }, async ({ id }) => {
5814
5940
  await store.removeGoal(id);
5815
5941
  return text("Goal removed.");
5816
5942
  });
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 }) => {
5943
+ 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
5944
  const machines = await store.machines();
5819
5945
  if (json)
5820
5946
  return text(JSON.stringify(machines, null, 2));
@@ -5836,9 +5962,9 @@ current machine: ${getMachineId()}`);
5836
5962
  registerAgentTools(server, { service: "economy" });
5837
5963
  registerIngestJournalTool(server);
5838
5964
  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()
5965
+ message: z7.string(),
5966
+ email: z7.string().optional(),
5967
+ category: z7.enum(["bug", "feature", "general"]).optional()
5842
5968
  }, async ({ message, email, category }) => {
5843
5969
  try {
5844
5970
  await store.sendFeedback({ message, email, category });