@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/CHANGELOG.md +12 -0
- package/README.md +7 -3
- package/dist/chunks/{index-s9mvven6.js → index-61c0vz9p.js} +223 -100
- package/dist/chunks/{index-bn04v378.js → index-7k1xt8mj.js} +1 -1
- package/dist/chunks/{index-1vz1arz5.js → index-pnnqdv3r.js} +1 -1
- package/dist/chunks/{index-mhrkwvtk.js → index-z9psed59.js} +1 -1
- package/dist/chunks/{serve-8yqe7nhs.js → serve-zp5a3qzw.js} +2 -2
- package/dist/chunks/{tui-fe94bvp9.js → tui-2yea642b.js} +3 -3
- package/dist/chunks/{watch-ct8dvrpr.js → watch-811vncjz.js} +3 -3
- package/dist/cli/index.js +10 -10
- package/dist/index.js +1 -1
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/codex.d.ts.map +1 -1
- package/dist/lib/accounts-store.d.ts +25 -1
- package/dist/lib/accounts-store.d.ts.map +1 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/paths.d.ts.map +1 -1
- package/dist/lib/subscriptions-registry.d.ts +26 -0
- package/dist/lib/subscriptions-registry.d.ts.map +1 -0
- package/dist/lib/test-keychain-fixture.d.ts +3 -2
- package/dist/lib/test-keychain-fixture.d.ts.map +1 -1
- package/dist/mcp/index.js +290 -167
- package/dist/otel/index.js +87 -65
- package/dist/server/index.js +231 -110
- package/dist/server/service-worker.js +222 -101
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
|
1560
|
-
import { join as
|
|
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
|
|
1566
|
+
import { z as z6 } from "zod";
|
|
1567
1567
|
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
1568
|
-
|
|
1569
|
-
|
|
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:
|
|
1572
|
-
tool:
|
|
1601
|
+
name: nonempty,
|
|
1602
|
+
tool: nonempty,
|
|
1573
1603
|
email: z5.string().optional(),
|
|
1574
1604
|
displayName: z5.string().optional(),
|
|
1575
|
-
dir:
|
|
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
|
-
|
|
1578
|
-
|
|
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 =
|
|
1581
|
-
id:
|
|
1582
|
-
label:
|
|
1583
|
-
envVar:
|
|
1584
|
-
defaultDir:
|
|
1585
|
-
bin:
|
|
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 =
|
|
1588
|
-
version:
|
|
1589
|
-
current:
|
|
1590
|
-
applied:
|
|
1591
|
-
toolLocks:
|
|
1592
|
-
profiles:
|
|
1593
|
-
tools:
|
|
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(
|
|
1992
|
+
const credential = resolveCredential(app, clientEnv, credentials);
|
|
1877
1993
|
if (credential === null) {
|
|
1878
1994
|
try {
|
|
1879
|
-
resolveClientTransport(
|
|
1995
|
+
resolveClientTransport(app, clientEnv, credentials ? { credentials } : {});
|
|
1880
1996
|
} catch (err) {
|
|
1881
1997
|
if (isNoAccountsCredentialConfigurationError(err))
|
|
1882
|
-
return
|
|
1998
|
+
return null;
|
|
1883
1999
|
throw err;
|
|
1884
2000
|
}
|
|
1885
|
-
return
|
|
2001
|
+
return null;
|
|
1886
2002
|
}
|
|
1887
2003
|
if (credential.tier === "pointer") {
|
|
1888
|
-
throw new Error(
|
|
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(
|
|
1891
|
-
credentials: { ...
|
|
2006
|
+
const resolution = resolveClientTransport(app, clientEnv, {
|
|
2007
|
+
credentials: { ...credentials, apiKey: credential.apiKey }
|
|
1892
2008
|
});
|
|
1893
|
-
return
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2046
|
-
|
|
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(
|
|
2211
|
+
walk(join7(dir, entry.name));
|
|
2057
2212
|
else if (entry.name.endsWith(".jsonl"))
|
|
2058
|
-
files.push(
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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"] ??
|
|
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"] ??
|
|
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"] ??
|
|
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"] ??
|
|
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
|
|
2506
|
-
var DEFAULT_GEMINI_TMP_DIR =
|
|
2507
|
-
var DEFAULT_GEMINI_HISTORY_DIR =
|
|
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(
|
|
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 =
|
|
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 =
|
|
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) =>
|
|
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
|
|
2683
|
-
var 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 =
|
|
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 =
|
|
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
|
|
2901
|
-
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ??
|
|
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 =
|
|
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
|
|
3007
|
-
var HERMES_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
|
|
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 =
|
|
3143
|
-
return existsSync9(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
5362
|
-
project:
|
|
5363
|
-
account:
|
|
5364
|
-
machine:
|
|
5365
|
-
limit:
|
|
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:
|
|
5376
|
-
agent:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
5446
|
-
kind:
|
|
5447
|
-
cursor:
|
|
5448
|
-
limit:
|
|
5449
|
-
verbose:
|
|
5450
|
-
json:
|
|
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:
|
|
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:
|
|
5495
|
-
limit_usd:
|
|
5496
|
-
project_path:
|
|
5497
|
-
agent:
|
|
5498
|
-
cost_center_id:
|
|
5499
|
-
alert_at_percent:
|
|
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:
|
|
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:
|
|
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:
|
|
5532
|
-
input_per_1m:
|
|
5533
|
-
output_per_1m:
|
|
5534
|
-
cache_read_per_1m:
|
|
5535
|
-
cache_write_per_1m:
|
|
5536
|
-
cache_write_1h_per_1m:
|
|
5537
|
-
cache_storage_per_1m_hour:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
5738
|
-
provider:
|
|
5739
|
-
plan:
|
|
5740
|
-
account_key:
|
|
5741
|
-
agent:
|
|
5742
|
-
monthly_fee_usd:
|
|
5743
|
-
included_usage_usd:
|
|
5744
|
-
billing_cycle_start:
|
|
5745
|
-
reset_policy:
|
|
5746
|
-
active:
|
|
5747
|
-
json:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
5801
|
-
limit_usd:
|
|
5802
|
-
project_path:
|
|
5803
|
-
agent:
|
|
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:
|
|
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:
|
|
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:
|
|
5840
|
-
email:
|
|
5841
|
-
category:
|
|
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 });
|