@hasna/economy 0.3.28 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +6 -13
- package/SECURITY.md +1 -1
- package/dist/cli/commands/extras.d.ts.map +1 -1
- package/dist/cli/index.js +1135 -853
- package/dist/db/cloud.d.ts +16 -8
- package/dist/db/cloud.d.ts.map +1 -1
- package/dist/db/database.d.ts +19 -0
- package/dist/db/database.d.ts.map +1 -1
- package/dist/index.js +198 -19
- package/dist/ingest/claude.d.ts.map +1 -1
- package/dist/ingest/loops.d.ts +9 -0
- package/dist/ingest/loops.d.ts.map +1 -1
- package/dist/lib/accounts-store.d.ts +83 -0
- package/dist/lib/accounts-store.d.ts.map +1 -0
- package/dist/lib/accounts.d.ts +2 -1
- package/dist/lib/accounts.d.ts.map +1 -1
- package/dist/lib/api-display-url.d.ts +19 -0
- package/dist/lib/api-display-url.d.ts.map +1 -0
- package/dist/lib/autosync-gate.d.ts +15 -0
- package/dist/lib/autosync-gate.d.ts.map +1 -0
- package/dist/lib/cloud-ingest.d.ts +15 -1
- package/dist/lib/cloud-ingest.d.ts.map +1 -1
- package/dist/lib/cloud-storage.d.ts +166 -10
- package/dist/lib/cloud-storage.d.ts.map +1 -1
- package/dist/lib/gatherer.d.ts.map +1 -1
- package/dist/lib/serve-auth.d.ts.map +1 -1
- package/dist/lib/store/index.d.ts +6 -3
- package/dist/lib/store/index.d.ts.map +1 -1
- package/dist/lib/test-hermetic-accounts.d.ts +3 -2
- package/dist/lib/test-hermetic-accounts.d.ts.map +1 -1
- package/dist/lib/test-hermetic-fleet-env.d.ts +4 -0
- package/dist/lib/test-hermetic-fleet-env.d.ts.map +1 -0
- package/dist/mcp/agent-registry.d.ts +85 -0
- package/dist/mcp/agent-registry.d.ts.map +1 -0
- package/dist/mcp/harness.d.ts +26 -0
- package/dist/mcp/harness.d.ts.map +1 -0
- package/dist/mcp/index.js +1340 -302
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/otel/index.js +430 -16
- package/dist/server/index.js +548 -155
- package/dist/server/serve.d.ts +0 -2
- package/dist/server/serve.d.ts.map +1 -1
- package/docs/cli.md +2 -3
- package/docs/configuration.md +23 -10
- package/docs/ingestion.md +1 -1
- package/docs/mcp.md +4 -2
- package/docs/otel.md +8 -2
- package/docs/rest-api.md +2 -2
- package/package.json +5 -11
- package/postinstall.js +52 -1
- package/dashboard/README.md +0 -26
- package/dist/cli/brains.d.ts +0 -3
- package/dist/cli/brains.d.ts.map +0 -1
package/dist/mcp/index.js
CHANGED
|
@@ -596,7 +596,62 @@ import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
|
596
596
|
import { hostname, platform } from "os";
|
|
597
597
|
import { homedir } from "os";
|
|
598
598
|
import { join, resolve } from "path";
|
|
599
|
-
import {
|
|
599
|
+
import { homedir as pathsResolverHomedir } from "os";
|
|
600
|
+
import { join as pathsResolverJoin } from "path";
|
|
601
|
+
function pathsResolverAssertApp(app) {
|
|
602
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
603
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
604
|
+
}
|
|
605
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE.test(app)) {
|
|
606
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
function pathsResolverAssertKind(kind) {
|
|
610
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV).includes(kind)) {
|
|
611
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV).join(", ")}`);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
function pathsResolverBaseDir(kind, options) {
|
|
615
|
+
pathsResolverAssertKind(kind);
|
|
616
|
+
const env = options.env ?? process.env;
|
|
617
|
+
const override = env[PATHS_RESOLVER_KIND_ENV[kind]];
|
|
618
|
+
if (typeof override === "string" && override.length > 0)
|
|
619
|
+
return override;
|
|
620
|
+
const home = options.home ?? pathsResolverHomedir();
|
|
621
|
+
const platform2 = options.platform ?? process.platform;
|
|
622
|
+
if (platform2 === "darwin") {
|
|
623
|
+
switch (kind) {
|
|
624
|
+
case "config":
|
|
625
|
+
case "data":
|
|
626
|
+
return pathsResolverJoin(home, "Library", "Application Support", "Hasna");
|
|
627
|
+
case "cache":
|
|
628
|
+
return pathsResolverJoin(home, "Library", "Caches", "Hasna");
|
|
629
|
+
case "state":
|
|
630
|
+
return pathsResolverJoin(home, "Library", "Logs", "Hasna");
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
switch (kind) {
|
|
634
|
+
case "config":
|
|
635
|
+
return pathsResolverJoin(home, ".config", "hasna");
|
|
636
|
+
case "data":
|
|
637
|
+
return pathsResolverJoin(home, ".local", "share", "hasna");
|
|
638
|
+
case "state":
|
|
639
|
+
return pathsResolverJoin(home, ".local", "state", "hasna");
|
|
640
|
+
case "cache":
|
|
641
|
+
return pathsResolverJoin(home, ".cache", "hasna");
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
function pathsResolverResolve(kind, options) {
|
|
645
|
+
pathsResolverAssertApp(options.app);
|
|
646
|
+
const appSegment = options.internal === true ? pathsResolverJoin("internal", options.app) : options.app;
|
|
647
|
+
return pathsResolverJoin(pathsResolverBaseDir(kind, options), appSegment);
|
|
648
|
+
}
|
|
649
|
+
function dataDir(options) {
|
|
650
|
+
return pathsResolverResolve("data", options);
|
|
651
|
+
}
|
|
652
|
+
function cacheDir(options) {
|
|
653
|
+
return pathsResolverResolve("cache", options);
|
|
654
|
+
}
|
|
600
655
|
function normalizeMachineId(value) {
|
|
601
656
|
const id = value?.trim().toLowerCase().split(".")[0];
|
|
602
657
|
return id && id.length > 0 ? id : null;
|
|
@@ -619,7 +674,7 @@ function macHostMachineId() {
|
|
|
619
674
|
return null;
|
|
620
675
|
}
|
|
621
676
|
function getMachineId() {
|
|
622
|
-
const envMachine = normalizeMachineId(process.env["
|
|
677
|
+
const envMachine = normalizeMachineId(process.env["HASNA_ECONOMY_MACHINE_ID"]);
|
|
623
678
|
if (envMachine)
|
|
624
679
|
return envMachine;
|
|
625
680
|
const hostMachine = normalizeMachineId(hostname()) ?? "unknown";
|
|
@@ -1929,6 +1984,10 @@ function getIngestState(db, source, key) {
|
|
|
1929
1984
|
const row = db.prepare(`SELECT value FROM ingest_state WHERE source = ? AND key = ?`).get(source, key);
|
|
1930
1985
|
return row?.value ?? null;
|
|
1931
1986
|
}
|
|
1987
|
+
function loadIngestState(db, source) {
|
|
1988
|
+
const rows = db.prepare(`SELECT key, value FROM ingest_state WHERE source = ?`).all(source);
|
|
1989
|
+
return new Map(rows.map((row) => [row.key, row.value]));
|
|
1990
|
+
}
|
|
1932
1991
|
function setIngestState(db, source, key, value) {
|
|
1933
1992
|
db.prepare(`INSERT OR REPLACE INTO ingest_state (source, key, value) VALUES (?, ?, ?)`).run(source, key, value);
|
|
1934
1993
|
}
|
|
@@ -2104,8 +2163,16 @@ function dedupeRequests(db) {
|
|
|
2104
2163
|
}
|
|
2105
2164
|
return removed;
|
|
2106
2165
|
}
|
|
2166
|
+
var PATHS_RESOLVER_KIND_ENV, PATHS_RESOLVER_APP_SLUG_RE;
|
|
2107
2167
|
var init_database = __esm(() => {
|
|
2108
2168
|
init_sqlite_adapter();
|
|
2169
|
+
PATHS_RESOLVER_KIND_ENV = {
|
|
2170
|
+
config: "HASNA_CONFIG_HOME",
|
|
2171
|
+
data: "HASNA_DATA_HOME",
|
|
2172
|
+
state: "HASNA_STATE_HOME",
|
|
2173
|
+
cache: "HASNA_CACHE_HOME"
|
|
2174
|
+
};
|
|
2175
|
+
PATHS_RESOLVER_APP_SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
2109
2176
|
});
|
|
2110
2177
|
|
|
2111
2178
|
// src/mcp/index.ts
|
|
@@ -2113,8 +2180,8 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
2113
2180
|
// package.json
|
|
2114
2181
|
var package_default = {
|
|
2115
2182
|
name: "@hasna/economy",
|
|
2116
|
-
version: "0.
|
|
2117
|
-
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API
|
|
2183
|
+
version: "0.4.0",
|
|
2184
|
+
description: "AI coding cost tracker \u2014 CLI + MCP server + REST API for Claude Code, Codex, OpenCode, Cursor, Pi, and Hermes, with legacy Gemini CLI session and Gemini API billing ingestion",
|
|
2118
2185
|
repository: {
|
|
2119
2186
|
type: "git",
|
|
2120
2187
|
url: "git+https://github.com/hasna/economy.git"
|
|
@@ -2145,7 +2212,6 @@ var package_default = {
|
|
|
2145
2212
|
files: [
|
|
2146
2213
|
"dist",
|
|
2147
2214
|
"docs",
|
|
2148
|
-
"dashboard/dist",
|
|
2149
2215
|
"postinstall.js",
|
|
2150
2216
|
"LICENSE",
|
|
2151
2217
|
"README.md",
|
|
@@ -2155,12 +2221,11 @@ var package_default = {
|
|
|
2155
2221
|
"CODE_OF_CONDUCT.md"
|
|
2156
2222
|
],
|
|
2157
2223
|
scripts: {
|
|
2158
|
-
build: "
|
|
2224
|
+
build: "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external && bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external && bun build src/server/index.ts --outdir dist/server --target bun --packages external && bun build src/db/pg-sync-worker.ts --outdir dist/server --target bun --packages external && bun build src/otel/index.ts --outdir dist/otel --target bun --packages external && bun build src/index.ts --outdir dist --target bun --packages external && tsc --emitDeclarationOnly --outDir dist",
|
|
2159
2225
|
"build:cli": "bun build src/cli/index.ts --outdir dist/cli --target bun --packages external",
|
|
2160
2226
|
"build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --packages external",
|
|
2161
2227
|
"build:server": "bun build src/server/index.ts --outdir dist/server --target bun --packages external",
|
|
2162
2228
|
"build:lib": "bun build src/index.ts --outdir dist --target bun --packages external",
|
|
2163
|
-
"build:dashboard": "cd dashboard && bun run build",
|
|
2164
2229
|
"artifact-scan": "bun scripts/contracts-cli.mjs artifact-scan dist",
|
|
2165
2230
|
prepack: "bun run build && bun run artifact-scan",
|
|
2166
2231
|
typecheck: "tsc --noEmit",
|
|
@@ -2190,12 +2255,8 @@ var package_default = {
|
|
|
2190
2255
|
access: "public"
|
|
2191
2256
|
},
|
|
2192
2257
|
dependencies: {
|
|
2193
|
-
"@hasna/
|
|
2194
|
-
"@hasna/agent-registry": "^0.1.0",
|
|
2195
|
-
"@hasna/contracts": "0.13.4",
|
|
2258
|
+
"@hasna/contracts": "1.0.2",
|
|
2196
2259
|
"@hasna/events": "^0.1.16",
|
|
2197
|
-
"@hasna/mcp-harness": "^0.1.0",
|
|
2198
|
-
"@hasna/paths": "0.1.0",
|
|
2199
2260
|
"@hasna/projects": "1.0.0",
|
|
2200
2261
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
2201
2262
|
chalk: "^5.4.1",
|
|
@@ -2204,7 +2265,7 @@ var package_default = {
|
|
|
2204
2265
|
zod: "^3.24.2"
|
|
2205
2266
|
},
|
|
2206
2267
|
devDependencies: {
|
|
2207
|
-
"@types/bun": "
|
|
2268
|
+
"@types/bun": "1.3.14",
|
|
2208
2269
|
"@types/pg": "^8.20.0",
|
|
2209
2270
|
"bun-types": "latest",
|
|
2210
2271
|
typescript: "^5.7.2"
|
|
@@ -2228,17 +2289,572 @@ function getPackageMetadata() {
|
|
|
2228
2289
|
var packageMetadata = getPackageMetadata();
|
|
2229
2290
|
|
|
2230
2291
|
// src/mcp/server.ts
|
|
2231
|
-
init_database();
|
|
2232
2292
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2233
|
-
|
|
2293
|
+
|
|
2294
|
+
// src/mcp/agent-registry.ts
|
|
2295
|
+
init_database();
|
|
2296
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
2297
|
+
import { dirname, join as join2 } from "path";
|
|
2298
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
2234
2299
|
import { z } from "zod";
|
|
2235
2300
|
|
|
2301
|
+
// src/lib/cloud-storage.ts
|
|
2302
|
+
import {
|
|
2303
|
+
appConfigDiskValue,
|
|
2304
|
+
ClientTransportConfigurationError,
|
|
2305
|
+
clientTransportEnvKeys,
|
|
2306
|
+
createClientTransport,
|
|
2307
|
+
credentialDiskSources,
|
|
2308
|
+
credentialOverrideEnvKey,
|
|
2309
|
+
credentialPointerEnvKey,
|
|
2310
|
+
CREDENTIAL_PROFILE_ENV_KEY,
|
|
2311
|
+
keychainConfigValue
|
|
2312
|
+
} from "@hasna/contracts/client";
|
|
2313
|
+
import { createHasnaStorageClient } from "@hasna/contracts/client/storage";
|
|
2314
|
+
var ECONOMY_APP = "economy";
|
|
2315
|
+
var RETIRED_STORAGE_MODE_KEYS = [
|
|
2316
|
+
"HASNA_ECONOMY_STORAGE_MODE",
|
|
2317
|
+
"HASNA_ECONOMY_MODE",
|
|
2318
|
+
"ECONOMY_STORAGE_MODE",
|
|
2319
|
+
"ECONOMY_MODE"
|
|
2320
|
+
];
|
|
2321
|
+
function assertNoRetiredStorageMode(env) {
|
|
2322
|
+
const legacyKey = RETIRED_STORAGE_MODE_KEYS.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
|
|
2323
|
+
if (!legacyKey)
|
|
2324
|
+
return;
|
|
2325
|
+
throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The client routes through the HTTP API resolved by @hasna/contracts ` + `(HASNA_ECONOMY_API_URL + HASNA_ECONOMY_API_KEY, the Keychain, or ` + `~/.hasna/economy/config/credentials), or serves the local SQLite store only when ` + `HASNA_ECONOMY_LOCAL=1 is set.`);
|
|
2326
|
+
}
|
|
2327
|
+
var LOCAL_STORAGE_OPT_IN_KEYS = ["HASNA_ECONOMY_LOCAL", "ECONOMY_LOCAL"];
|
|
2328
|
+
function envFlagSet(env, key) {
|
|
2329
|
+
const raw = env[key];
|
|
2330
|
+
if (raw === undefined)
|
|
2331
|
+
return false;
|
|
2332
|
+
const value = raw.trim().toLowerCase();
|
|
2333
|
+
return value !== "" && value !== "0" && value !== "false" && value !== "no" && value !== "off";
|
|
2334
|
+
}
|
|
2335
|
+
function localStorageExplicitlyOptedIn(env) {
|
|
2336
|
+
return LOCAL_STORAGE_OPT_IN_KEYS.some((key) => envFlagSet(env, key));
|
|
2337
|
+
}
|
|
2338
|
+
function economyAuthorityEnvKeys() {
|
|
2339
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
2340
|
+
return [
|
|
2341
|
+
...keys.apiUrlKeys,
|
|
2342
|
+
...keys.apiKeyKeys,
|
|
2343
|
+
credentialOverrideEnvKey(ECONOMY_APP),
|
|
2344
|
+
credentialPointerEnvKey(ECONOMY_APP),
|
|
2345
|
+
CREDENTIAL_PROFILE_ENV_KEY
|
|
2346
|
+
];
|
|
2347
|
+
}
|
|
2348
|
+
function hasEconomyEnvAuthorityIntent(env) {
|
|
2349
|
+
return economyAuthorityEnvKeys().some((key) => (env[key] ?? "").trim().length > 0);
|
|
2350
|
+
}
|
|
2351
|
+
function hostedAuthorityConfigured(env, options = {}) {
|
|
2352
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
2353
|
+
if (keys.apiUrlKeys.some((key) => (env[key] ?? "").trim().length > 0))
|
|
2354
|
+
return true;
|
|
2355
|
+
if (keychainConfigValue(ECONOMY_APP, env, options.credentials?.keychain) !== null)
|
|
2356
|
+
return true;
|
|
2357
|
+
return appConfigDiskValue(ECONOMY_APP, env, keys.apiUrlKeys) !== null;
|
|
2358
|
+
}
|
|
2359
|
+
function economyResolverEnv(env) {
|
|
2360
|
+
const blanks = economyAuthorityEnvKeys().filter((key) => (key in env) && (env[key] ?? "").trim() === "");
|
|
2361
|
+
if (blanks.length === 0)
|
|
2362
|
+
return env;
|
|
2363
|
+
const next = { ...env };
|
|
2364
|
+
for (const key of blanks)
|
|
2365
|
+
delete next[key];
|
|
2366
|
+
return next;
|
|
2367
|
+
}
|
|
2368
|
+
var CONTRACTS_AMBIENT_ENVIRONMENT = Symbol.for("hasna:contracts:ambientClientEnvironment");
|
|
2369
|
+
function isAmbientEconomyEnv(env) {
|
|
2370
|
+
if (typeof process !== "undefined" && env === process.env)
|
|
2371
|
+
return true;
|
|
2372
|
+
return env[CONTRACTS_AMBIENT_ENVIRONMENT] === true;
|
|
2373
|
+
}
|
|
2374
|
+
function economyResolverInputs(env, credentials = {}) {
|
|
2375
|
+
const normalised = economyResolverEnv(env);
|
|
2376
|
+
if (normalised === env)
|
|
2377
|
+
return { env: normalised, credentials };
|
|
2378
|
+
const keychain = { ...credentials.keychain };
|
|
2379
|
+
if (keychain.enabled === undefined && keychain.run === undefined) {
|
|
2380
|
+
keychain.enabled = isAmbientEconomyEnv(env);
|
|
2381
|
+
}
|
|
2382
|
+
return { env: normalised, credentials: { ...credentials, keychain } };
|
|
2383
|
+
}
|
|
2384
|
+
function resolveEconomyStorageClient(name, env, overrides = {}) {
|
|
2385
|
+
const { env: resolverEnv, credentials } = economyResolverInputs(env, overrides.credentials);
|
|
2386
|
+
const wired = createClientTransport(name, resolverEnv, {
|
|
2387
|
+
credentials,
|
|
2388
|
+
...overrides.fetchImpl ? { fetchImpl: overrides.fetchImpl } : {},
|
|
2389
|
+
...overrides.headers ? { headers: overrides.headers } : {},
|
|
2390
|
+
...overrides.timeoutMs ? { timeoutMs: overrides.timeoutMs } : {},
|
|
2391
|
+
...overrides.retry !== undefined ? { retry: overrides.retry } : {},
|
|
2392
|
+
...overrides.sleepImpl ? { sleepImpl: overrides.sleepImpl } : {}
|
|
2393
|
+
});
|
|
2394
|
+
return {
|
|
2395
|
+
transport: "http",
|
|
2396
|
+
client: createHasnaStorageClient(name, wired.client),
|
|
2397
|
+
resolution: wired.resolution
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
function localEconomyNotice() {
|
|
2401
|
+
return `economy: local mode (${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1) \u2014 reading and writing the on-box ` + `SQLite store, not the hosted fleet. Data in the shared economy API is NOT included.`;
|
|
2402
|
+
}
|
|
2403
|
+
function credentialRequiredError(env, cause) {
|
|
2404
|
+
const keys = clientTransportEnvKeys(ECONOMY_APP);
|
|
2405
|
+
const clientEnv = env;
|
|
2406
|
+
const detail = cause instanceof Error ? cause.message : `No API key could be resolved for '${ECONOMY_APP}'. Looked in the Keychain (macOS only), ` + `then for a credential file at ${credentialDiskSources(ECONOMY_APP, clientEnv).join(" or ") || "<no HOME set>"}, ` + `then for ${keys.apiKeyKeys[0]} in the environment.`;
|
|
2407
|
+
const wayOut = hostedAuthorityConfigured(clientEnv) ? `${keys.apiUrlKeys[0]} (or the Keychain hasna.credentials.${ECONOMY_APP}.api-url item, or ` + `~/.hasna/${ECONOMY_APP}/config/credentials) selects a hosted service, so ${LOCAL_STORAGE_OPT_IN_KEYS[0]} ` + `does not apply; unset the authority to use the local store.` : `Or set ${LOCAL_STORAGE_OPT_IN_KEYS[0]}=1 to explicitly opt into the local store.`;
|
|
2408
|
+
return new Error(`Economy fails closed instead of silently serving the local SQLite store: ${detail} ` + `Set ${keys.apiUrlKeys[0]} and ${keys.apiKeyKeys[0]} (or store the key in the Keychain item ` + `hasna.credentials.${ECONOMY_APP}.api-key, or in ~/.hasna/${ECONOMY_APP}/config/credentials) to use the ` + `hosted economy API at ${keys.apiUrlKeys[0]} or the default fleet gateway. ` + wayOut);
|
|
2409
|
+
}
|
|
2410
|
+
var cache;
|
|
2411
|
+
var localNoticePrinted = false;
|
|
2412
|
+
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
2413
|
+
assertNoRetiredStorageMode(env);
|
|
2414
|
+
const clientEnv = env;
|
|
2415
|
+
if (localStorageExplicitlyOptedIn(env) && !hasEconomyEnvAuthorityIntent(clientEnv)) {
|
|
2416
|
+
return { active: false, client: null };
|
|
2417
|
+
}
|
|
2418
|
+
let resolved;
|
|
2419
|
+
try {
|
|
2420
|
+
resolved = resolveEconomyStorageClient(ECONOMY_APP, clientEnv, overrides ?? {});
|
|
2421
|
+
} catch (error) {
|
|
2422
|
+
if (error instanceof ClientTransportConfigurationError) {
|
|
2423
|
+
throw credentialRequiredError(env, error);
|
|
2424
|
+
}
|
|
2425
|
+
throw error;
|
|
2426
|
+
}
|
|
2427
|
+
return { active: true, client: resolved.client };
|
|
2428
|
+
}
|
|
2429
|
+
function economyCloudStorage(env = process.env) {
|
|
2430
|
+
if (cache === undefined) {
|
|
2431
|
+
cache = resolveEconomyCloudStorage(env);
|
|
2432
|
+
if (!cache.active && !localNoticePrinted) {
|
|
2433
|
+
localNoticePrinted = true;
|
|
2434
|
+
if (typeof process !== "undefined")
|
|
2435
|
+
process.stderr.write(`${localEconomyNotice()}
|
|
2436
|
+
`);
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
return cache;
|
|
2440
|
+
}
|
|
2441
|
+
function cleanQuery(query) {
|
|
2442
|
+
if (!query)
|
|
2443
|
+
return;
|
|
2444
|
+
const out = {};
|
|
2445
|
+
for (const [k, v] of Object.entries(query)) {
|
|
2446
|
+
if (v !== undefined && v !== null && v !== "")
|
|
2447
|
+
out[k] = v;
|
|
2448
|
+
}
|
|
2449
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
2450
|
+
}
|
|
2451
|
+
async function cloudListItems(storage, resource, query) {
|
|
2452
|
+
const cleaned = cleanQuery(query);
|
|
2453
|
+
const res = await storage.client.list(resource, cleaned ? { query: cleaned } : {});
|
|
2454
|
+
return res.items;
|
|
2455
|
+
}
|
|
2456
|
+
async function cloudObject(storage, path, query) {
|
|
2457
|
+
const cleaned = cleanQuery(query);
|
|
2458
|
+
const raw = await storage.client.transport.get(path, cleaned ? { query: cleaned } : {});
|
|
2459
|
+
if (raw && typeof raw === "object" && "data" in raw) {
|
|
2460
|
+
return raw.data;
|
|
2461
|
+
}
|
|
2462
|
+
return raw;
|
|
2463
|
+
}
|
|
2464
|
+
|
|
2465
|
+
// src/mcp/agent-registry.ts
|
|
2466
|
+
var SERVICE_NAME = "agent-registry";
|
|
2467
|
+
var DEFAULT_ACTIVE_WINDOW_MS = 30 * 60 * 1000;
|
|
2468
|
+
function envVar(suffix) {
|
|
2469
|
+
return process.env[`HASNA_AGENT_REGISTRY_${suffix}`] ?? process.env[`HASNA_AGENT_${suffix}`];
|
|
2470
|
+
}
|
|
2471
|
+
function getActiveWindowMs() {
|
|
2472
|
+
const env = envVar("TIMEOUT_MS");
|
|
2473
|
+
if (env) {
|
|
2474
|
+
const parsed = Number.parseInt(env, 10);
|
|
2475
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
2476
|
+
return parsed;
|
|
2477
|
+
}
|
|
2478
|
+
return DEFAULT_ACTIVE_WINDOW_MS;
|
|
2479
|
+
}
|
|
2480
|
+
function envSessionId() {
|
|
2481
|
+
return envVar("SESSION_ID") ?? null;
|
|
2482
|
+
}
|
|
2483
|
+
var MEMORY_REGISTRY_PATH = ":memory:";
|
|
2484
|
+
function resolveRegistryDbPath() {
|
|
2485
|
+
const explicit = process.env.HASNA_AGENT_REGISTRY_DB_PATH;
|
|
2486
|
+
if (explicit && explicit.trim())
|
|
2487
|
+
return explicit;
|
|
2488
|
+
if (economyCloudStorage().active)
|
|
2489
|
+
return MEMORY_REGISTRY_PATH;
|
|
2490
|
+
return join2(getDataDir(), "agent-registry.db");
|
|
2491
|
+
}
|
|
2492
|
+
var defaultDb = null;
|
|
2493
|
+
|
|
2494
|
+
class RegistryDb {
|
|
2495
|
+
db;
|
|
2496
|
+
constructor(path) {
|
|
2497
|
+
if (path !== MEMORY_REGISTRY_PATH && dirname(path) && !existsSync2(dirname(path))) {
|
|
2498
|
+
mkdirSync2(dirname(path), { recursive: true, mode: 448 });
|
|
2499
|
+
}
|
|
2500
|
+
this.db = new Database2(path);
|
|
2501
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
2502
|
+
this.db.exec("PRAGMA busy_timeout = 10000");
|
|
2503
|
+
}
|
|
2504
|
+
get(sql, ...params) {
|
|
2505
|
+
return this.db.prepare(sql).get(...params);
|
|
2506
|
+
}
|
|
2507
|
+
all(sql, ...params) {
|
|
2508
|
+
return this.db.prepare(sql).all(...params);
|
|
2509
|
+
}
|
|
2510
|
+
run(sql, ...params) {
|
|
2511
|
+
return this.db.prepare(sql).run(...params);
|
|
2512
|
+
}
|
|
2513
|
+
exec(sql) {
|
|
2514
|
+
this.db.exec(sql);
|
|
2515
|
+
}
|
|
2516
|
+
close() {
|
|
2517
|
+
this.db.close();
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
function getDefaultStore() {
|
|
2521
|
+
if (!defaultDb) {
|
|
2522
|
+
defaultDb = new RegistryDb(resolveRegistryDbPath());
|
|
2523
|
+
ensureAgentsTable(defaultDb);
|
|
2524
|
+
}
|
|
2525
|
+
return defaultDb;
|
|
2526
|
+
}
|
|
2527
|
+
var AGENTS_TABLE_SQL = `
|
|
2528
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
2529
|
+
id TEXT PRIMARY KEY,
|
|
2530
|
+
name TEXT NOT NULL,
|
|
2531
|
+
session_id TEXT,
|
|
2532
|
+
role TEXT NOT NULL DEFAULT 'agent',
|
|
2533
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
2534
|
+
active_project_id TEXT,
|
|
2535
|
+
working_dir TEXT,
|
|
2536
|
+
machine_id TEXT,
|
|
2537
|
+
capabilities TEXT NOT NULL DEFAULT '[]',
|
|
2538
|
+
metadata TEXT NOT NULL DEFAULT '{}',
|
|
2539
|
+
last_seen_at TEXT NOT NULL,
|
|
2540
|
+
created_at TEXT NOT NULL
|
|
2541
|
+
)`;
|
|
2542
|
+
var AGENTS_NAME_INDEX_SQL = `CREATE INDEX IF NOT EXISTS agents_name_idx ON agents(name)`;
|
|
2543
|
+
function ensureAgentsTable(db) {
|
|
2544
|
+
db.exec(AGENTS_TABLE_SQL);
|
|
2545
|
+
db.exec(AGENTS_NAME_INDEX_SQL);
|
|
2546
|
+
}
|
|
2547
|
+
function newAgentId() {
|
|
2548
|
+
return `ag_${crypto.randomUUID().slice(0, 8)}`;
|
|
2549
|
+
}
|
|
2550
|
+
function now() {
|
|
2551
|
+
return new Date().toISOString();
|
|
2552
|
+
}
|
|
2553
|
+
function normalizeName(name) {
|
|
2554
|
+
return name.trim().toLowerCase();
|
|
2555
|
+
}
|
|
2556
|
+
function rowToAgent(row) {
|
|
2557
|
+
return {
|
|
2558
|
+
id: String(row.id),
|
|
2559
|
+
name: String(row.name),
|
|
2560
|
+
session_id: row.session_id ?? null,
|
|
2561
|
+
role: row.role ?? "agent",
|
|
2562
|
+
status: row.status ?? "active",
|
|
2563
|
+
active_project_id: row.active_project_id ?? null,
|
|
2564
|
+
working_dir: row.working_dir ?? null,
|
|
2565
|
+
machine_id: row.machine_id ?? null,
|
|
2566
|
+
capabilities: safeParseArray(row.capabilities),
|
|
2567
|
+
metadata: safeParseObject(row.metadata),
|
|
2568
|
+
last_seen_at: String(row.last_seen_at),
|
|
2569
|
+
created_at: String(row.created_at)
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
function safeParseArray(value) {
|
|
2573
|
+
if (!value)
|
|
2574
|
+
return [];
|
|
2575
|
+
try {
|
|
2576
|
+
const parsed = JSON.parse(String(value));
|
|
2577
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
2578
|
+
} catch {
|
|
2579
|
+
return [];
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
function safeParseObject(value) {
|
|
2583
|
+
if (!value)
|
|
2584
|
+
return {};
|
|
2585
|
+
try {
|
|
2586
|
+
const parsed = JSON.parse(String(value));
|
|
2587
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
2588
|
+
} catch {
|
|
2589
|
+
return {};
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
function isActiveAt(lastSeenAt, windowMs) {
|
|
2593
|
+
return Date.now() - new Date(lastSeenAt).getTime() < windowMs;
|
|
2594
|
+
}
|
|
2595
|
+
function isAgentConflict(result) {
|
|
2596
|
+
return Boolean(result) && result.conflict === true;
|
|
2597
|
+
}
|
|
2598
|
+
function getAgent(id, db) {
|
|
2599
|
+
const row = db.get("SELECT * FROM agents WHERE id = ?", id);
|
|
2600
|
+
return row ? rowToAgent(row) : null;
|
|
2601
|
+
}
|
|
2602
|
+
function getAgentByName(name, db) {
|
|
2603
|
+
const row = db.get("SELECT * FROM agents WHERE name = ? AND status = 'active' ORDER BY last_seen_at DESC LIMIT 1", normalizeName(name));
|
|
2604
|
+
return row ? rowToAgent(row) : null;
|
|
2605
|
+
}
|
|
2606
|
+
function resolveAgent(idOrName, db) {
|
|
2607
|
+
const key = idOrName ?? envSessionId();
|
|
2608
|
+
if (!key)
|
|
2609
|
+
return null;
|
|
2610
|
+
return getAgentByName(key, db) ?? getAgent(key, db);
|
|
2611
|
+
}
|
|
2612
|
+
function listAgents(opts = {}, db) {
|
|
2613
|
+
const includeArchived = opts.include_archived ?? false;
|
|
2614
|
+
const where = includeArchived ? "" : "WHERE status = 'active'";
|
|
2615
|
+
const rows = db.all(`SELECT * FROM agents ${where} ORDER BY name`);
|
|
2616
|
+
let agents = rows.map(rowToAgent);
|
|
2617
|
+
if (opts.online_only) {
|
|
2618
|
+
const windowMs = getActiveWindowMs();
|
|
2619
|
+
agents = agents.filter((a) => a.status === "active" && isActiveAt(a.last_seen_at, windowMs));
|
|
2620
|
+
}
|
|
2621
|
+
return agents;
|
|
2622
|
+
}
|
|
2623
|
+
function registerAgent(input, db) {
|
|
2624
|
+
const name = normalizeName(input.name);
|
|
2625
|
+
if (!name) {
|
|
2626
|
+
throw new Error("register_agent: name is required");
|
|
2627
|
+
}
|
|
2628
|
+
const sessionId = input.session_id ?? envSessionId() ?? null;
|
|
2629
|
+
const windowMs = getActiveWindowMs();
|
|
2630
|
+
const existing = getAgentByName(name, db);
|
|
2631
|
+
if (existing) {
|
|
2632
|
+
const active = isActiveAt(existing.last_seen_at, windowMs);
|
|
2633
|
+
const sameSession = Boolean(sessionId) && Boolean(existing.session_id) && sessionId === existing.session_id;
|
|
2634
|
+
const differentSession = Boolean(sessionId) && Boolean(existing.session_id) && sessionId !== existing.session_id;
|
|
2635
|
+
const callerHasNoSession = !sessionId;
|
|
2636
|
+
const existingHasActiveSession = Boolean(existing.session_id) && active;
|
|
2637
|
+
if (!input.force) {
|
|
2638
|
+
if (active && differentSession)
|
|
2639
|
+
return buildConflict(existing);
|
|
2640
|
+
if (callerHasNoSession && existingHasActiveSession)
|
|
2641
|
+
return buildConflict(existing);
|
|
2642
|
+
}
|
|
2643
|
+
const sets = ["last_seen_at = ?"];
|
|
2644
|
+
const params = [now()];
|
|
2645
|
+
if (sessionId && !sameSession) {
|
|
2646
|
+
sets.push("session_id = ?");
|
|
2647
|
+
params.push(sessionId);
|
|
2648
|
+
}
|
|
2649
|
+
if (input.role !== undefined) {
|
|
2650
|
+
sets.push("role = ?");
|
|
2651
|
+
params.push(input.role);
|
|
2652
|
+
}
|
|
2653
|
+
if (input.working_dir !== undefined) {
|
|
2654
|
+
sets.push("working_dir = ?");
|
|
2655
|
+
params.push(input.working_dir);
|
|
2656
|
+
}
|
|
2657
|
+
if (input.machine_id !== undefined) {
|
|
2658
|
+
sets.push("machine_id = ?");
|
|
2659
|
+
params.push(input.machine_id);
|
|
2660
|
+
}
|
|
2661
|
+
if (input.project_id !== undefined) {
|
|
2662
|
+
sets.push("active_project_id = ?");
|
|
2663
|
+
params.push(input.project_id);
|
|
2664
|
+
}
|
|
2665
|
+
if (input.capabilities !== undefined) {
|
|
2666
|
+
sets.push("capabilities = ?");
|
|
2667
|
+
params.push(JSON.stringify(input.capabilities));
|
|
2668
|
+
}
|
|
2669
|
+
if (input.metadata !== undefined) {
|
|
2670
|
+
sets.push("metadata = ?");
|
|
2671
|
+
params.push(JSON.stringify(input.metadata));
|
|
2672
|
+
}
|
|
2673
|
+
sets.push("status = 'active'");
|
|
2674
|
+
params.push(existing.id);
|
|
2675
|
+
db.run(`UPDATE agents SET ${sets.join(", ")} WHERE id = ?`, ...params);
|
|
2676
|
+
return getAgent(existing.id, db);
|
|
2677
|
+
}
|
|
2678
|
+
const id = newAgentId();
|
|
2679
|
+
const ts = now();
|
|
2680
|
+
db.run(`INSERT INTO agents
|
|
2681
|
+
(id, name, session_id, role, status, active_project_id, working_dir, machine_id, capabilities, metadata, last_seen_at, created_at)
|
|
2682
|
+
VALUES (?, ?, ?, ?, 'active', ?, ?, ?, ?, ?, ?, ?)`, id, name, sessionId, input.role ?? "agent", input.project_id ?? null, input.working_dir ?? null, input.machine_id ?? null, JSON.stringify(input.capabilities ?? []), JSON.stringify(input.metadata ?? {}), ts, ts);
|
|
2683
|
+
return getAgent(id, db);
|
|
2684
|
+
}
|
|
2685
|
+
function buildConflict(existing) {
|
|
2686
|
+
const minutesAgo = Math.round((Date.now() - new Date(existing.last_seen_at).getTime()) / 60000);
|
|
2687
|
+
return {
|
|
2688
|
+
conflict: true,
|
|
2689
|
+
existing_id: existing.id,
|
|
2690
|
+
existing_name: existing.name,
|
|
2691
|
+
last_seen_at: existing.last_seen_at,
|
|
2692
|
+
session_hint: existing.session_id ? existing.session_id.slice(0, 8) : null,
|
|
2693
|
+
working_dir: existing.working_dir,
|
|
2694
|
+
message: `Agent "${existing.name}" is already active (last seen ${minutesAgo}m ago). ` + (existing.session_id ? `Pass session_id="${existing.session_id}" to reclaim it, ` : "") + "use force:true to take over, or choose a different name."
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
function heartbeat(idOrName, db) {
|
|
2698
|
+
const agent = resolveAgent(idOrName, db);
|
|
2699
|
+
if (!agent)
|
|
2700
|
+
return null;
|
|
2701
|
+
db.run("UPDATE agents SET last_seen_at = ? WHERE id = ?", now(), agent.id);
|
|
2702
|
+
return getAgent(agent.id, db);
|
|
2703
|
+
}
|
|
2704
|
+
function setFocus(idOrName, projectId, db) {
|
|
2705
|
+
const agent = resolveAgent(idOrName, db);
|
|
2706
|
+
if (!agent)
|
|
2707
|
+
return null;
|
|
2708
|
+
db.run("UPDATE agents SET active_project_id = ?, last_seen_at = ? WHERE id = ?", projectId, now(), agent.id);
|
|
2709
|
+
return getAgent(agent.id, db);
|
|
2710
|
+
}
|
|
2711
|
+
function agentData(agent) {
|
|
2712
|
+
return {
|
|
2713
|
+
agent_id: agent.id,
|
|
2714
|
+
session_id: agent.session_id,
|
|
2715
|
+
project_id: agent.active_project_id,
|
|
2716
|
+
machine_id: agent.machine_id,
|
|
2717
|
+
role: agent.role
|
|
2718
|
+
};
|
|
2719
|
+
}
|
|
2720
|
+
async function emitAgentEvent(client, type, agent, extra = {}) {
|
|
2721
|
+
if (!client)
|
|
2722
|
+
return;
|
|
2723
|
+
try {
|
|
2724
|
+
await client.emit({
|
|
2725
|
+
source: SERVICE_NAME,
|
|
2726
|
+
type,
|
|
2727
|
+
subject: agent.name,
|
|
2728
|
+
severity: type === "agent.heartbeat" ? "debug" : "info",
|
|
2729
|
+
data: { ...agentData(agent), ...extra }
|
|
2730
|
+
});
|
|
2731
|
+
} catch {}
|
|
2732
|
+
}
|
|
2733
|
+
async function emitConflictEvent(client, conflict) {
|
|
2734
|
+
if (!client)
|
|
2735
|
+
return;
|
|
2736
|
+
try {
|
|
2737
|
+
await client.emit({
|
|
2738
|
+
source: SERVICE_NAME,
|
|
2739
|
+
type: "agent.conflict",
|
|
2740
|
+
subject: conflict.existing_name,
|
|
2741
|
+
severity: "warning",
|
|
2742
|
+
data: {
|
|
2743
|
+
existing_id: conflict.existing_id,
|
|
2744
|
+
last_seen_at: conflict.last_seen_at,
|
|
2745
|
+
session_hint: conflict.session_hint
|
|
2746
|
+
},
|
|
2747
|
+
message: conflict.message
|
|
2748
|
+
});
|
|
2749
|
+
} catch {}
|
|
2750
|
+
}
|
|
2751
|
+
function jsonText(data) {
|
|
2752
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
2753
|
+
}
|
|
2754
|
+
function errorText(message) {
|
|
2755
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
2756
|
+
}
|
|
2757
|
+
function registerAgentTools(server, opts = {}) {
|
|
2758
|
+
let resolved;
|
|
2759
|
+
const db = () => resolved ??= opts.db ?? getDefaultStore();
|
|
2760
|
+
const events = opts.events;
|
|
2761
|
+
const focus = opts.agentFocus;
|
|
2762
|
+
const includeExtended = opts.includeExtendedTools ?? false;
|
|
2763
|
+
const allow = (name) => opts.toolFilter ? opts.toolFilter(name) : true;
|
|
2764
|
+
const tool = (name, description, schema, handler) => {
|
|
2765
|
+
if (!allow(name))
|
|
2766
|
+
return;
|
|
2767
|
+
server.tool(name, description, schema, handler);
|
|
2768
|
+
};
|
|
2769
|
+
tool("register_agent", "Register or heartbeat an agent by name. Returns the agent, or a structured conflict descriptor if the name is actively held by another session.", {
|
|
2770
|
+
name: z.string().describe("Agent name (normalized to lowercase; unique among active agents)"),
|
|
2771
|
+
session_id: z.string().optional().describe("Stable session id used to reclaim the name"),
|
|
2772
|
+
role: z.string().optional().describe("Agent role (default: agent)"),
|
|
2773
|
+
working_dir: z.string().optional().describe("Working directory of the agent"),
|
|
2774
|
+
project_id: z.string().optional().describe("Set active project focus on register"),
|
|
2775
|
+
capabilities: z.array(z.string()).optional().describe("Declared capabilities"),
|
|
2776
|
+
force: z.boolean().optional().describe("Take over the name even if actively held")
|
|
2777
|
+
}, async (args) => {
|
|
2778
|
+
const result = registerAgent({
|
|
2779
|
+
name: args.name,
|
|
2780
|
+
session_id: args.session_id,
|
|
2781
|
+
role: args.role,
|
|
2782
|
+
working_dir: args.working_dir,
|
|
2783
|
+
project_id: args.project_id,
|
|
2784
|
+
capabilities: args.capabilities,
|
|
2785
|
+
force: args.force
|
|
2786
|
+
}, db());
|
|
2787
|
+
if (isAgentConflict(result)) {
|
|
2788
|
+
await emitConflictEvent(events, result);
|
|
2789
|
+
return { ...jsonText(result), isError: true };
|
|
2790
|
+
}
|
|
2791
|
+
await emitAgentEvent(events, "agent.registered", result);
|
|
2792
|
+
return jsonText(result);
|
|
2793
|
+
});
|
|
2794
|
+
tool("heartbeat", "Update an agent's last_seen_at to signal it is still active.", {
|
|
2795
|
+
agent_id: z.string().optional().describe("Agent id"),
|
|
2796
|
+
name: z.string().optional().describe("Agent name (alternative to agent_id)")
|
|
2797
|
+
}, async (args) => {
|
|
2798
|
+
const agent = heartbeat(args.agent_id ?? args.name, db());
|
|
2799
|
+
if (!agent)
|
|
2800
|
+
return errorText(`Agent not found: ${args.agent_id ?? args.name ?? "(none)"}`);
|
|
2801
|
+
await emitAgentEvent(events, "agent.heartbeat", agent);
|
|
2802
|
+
return jsonText(agent);
|
|
2803
|
+
});
|
|
2804
|
+
tool("set_focus", "Set (or clear) the active project context for an agent. Omit project_id to clear.", {
|
|
2805
|
+
agent_id: z.string().optional().describe("Agent id"),
|
|
2806
|
+
name: z.string().optional().describe("Agent name (alternative to agent_id)"),
|
|
2807
|
+
project_id: z.string().optional().describe("Project id to focus on; omit to clear")
|
|
2808
|
+
}, async (args) => {
|
|
2809
|
+
const projectId = args.project_id ?? null;
|
|
2810
|
+
const agent = setFocus(args.agent_id ?? args.name, projectId, db());
|
|
2811
|
+
if (!agent)
|
|
2812
|
+
return errorText(`Agent not found: ${args.agent_id ?? args.name ?? "(none)"}`);
|
|
2813
|
+
focus?.set(agent.id, { project_id: projectId });
|
|
2814
|
+
await emitAgentEvent(events, "agent.focus_changed", agent, { project_id: projectId });
|
|
2815
|
+
return jsonText(agent);
|
|
2816
|
+
});
|
|
2817
|
+
tool("list_agents", "List registered agents.", {
|
|
2818
|
+
online_only: z.boolean().optional().describe("Only agents seen within the active window"),
|
|
2819
|
+
include_archived: z.boolean().optional().describe("Include archived agents")
|
|
2820
|
+
}, async (args) => {
|
|
2821
|
+
const agents = listAgents({ online_only: args.online_only, include_archived: args.include_archived }, db());
|
|
2822
|
+
return jsonText(agents);
|
|
2823
|
+
});
|
|
2824
|
+
if (includeExtended) {
|
|
2825
|
+
tool("get_focus", "Get the active project context for an agent.", {
|
|
2826
|
+
agent_id: z.string().optional(),
|
|
2827
|
+
name: z.string().optional()
|
|
2828
|
+
}, async (args) => {
|
|
2829
|
+
const agent = resolveAgent(args.agent_id ?? args.name, db());
|
|
2830
|
+
if (!agent)
|
|
2831
|
+
return errorText(`Agent not found: ${args.agent_id ?? args.name ?? "(none)"}`);
|
|
2832
|
+
return jsonText({ agent_id: agent.id, project_id: agent.active_project_id });
|
|
2833
|
+
});
|
|
2834
|
+
tool("unfocus", "Clear the active project context for an agent.", {
|
|
2835
|
+
agent_id: z.string().optional(),
|
|
2836
|
+
name: z.string().optional()
|
|
2837
|
+
}, async (args) => {
|
|
2838
|
+
const agent = setFocus(args.agent_id ?? args.name, null, db());
|
|
2839
|
+
if (!agent)
|
|
2840
|
+
return errorText(`Agent not found: ${args.agent_id ?? args.name ?? "(none)"}`);
|
|
2841
|
+
focus?.set(agent.id, { project_id: null });
|
|
2842
|
+
await emitAgentEvent(events, "agent.focus_changed", agent, { project_id: null });
|
|
2843
|
+
return jsonText(agent);
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
// src/mcp/server.ts
|
|
2849
|
+
init_database();
|
|
2850
|
+
import { z as z3 } from "zod";
|
|
2851
|
+
|
|
2236
2852
|
// src/ingest/claude.ts
|
|
2237
2853
|
init_database();
|
|
2238
2854
|
init_pricing();
|
|
2239
|
-
import { readdirSync as readdirSync2, readFileSync, existsSync as
|
|
2240
|
-
import { homedir as
|
|
2241
|
-
import { join as
|
|
2855
|
+
import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync4, statSync as statSync2 } from "fs";
|
|
2856
|
+
import { homedir as homedir3 } from "os";
|
|
2857
|
+
import { join as join4, basename } from "path";
|
|
2242
2858
|
|
|
2243
2859
|
// src/lib/savings.ts
|
|
2244
2860
|
function periodWhere2(period, column) {
|
|
@@ -2258,8 +2874,8 @@ function periodWhere2(period, column) {
|
|
|
2258
2874
|
}
|
|
2259
2875
|
}
|
|
2260
2876
|
function prorateMonthlyFee(monthlyFee, period) {
|
|
2261
|
-
const
|
|
2262
|
-
const daysInMonth = new Date(
|
|
2877
|
+
const now2 = new Date;
|
|
2878
|
+
const daysInMonth = new Date(now2.getFullYear(), now2.getMonth() + 1, 0).getDate();
|
|
2263
2879
|
switch (period) {
|
|
2264
2880
|
case "today":
|
|
2265
2881
|
case "yesterday":
|
|
@@ -2394,8 +3010,319 @@ function defaultCostBasisForAgent(agent) {
|
|
|
2394
3010
|
return "estimated";
|
|
2395
3011
|
}
|
|
2396
3012
|
|
|
3013
|
+
// src/lib/accounts-store.ts
|
|
3014
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
3015
|
+
import { homedir as homedir2 } from "os";
|
|
3016
|
+
import { join as join3 } from "path";
|
|
3017
|
+
import { z as z2 } from "zod";
|
|
3018
|
+
import { resolveClientTransport, resolveCredential } from "@hasna/contracts/client";
|
|
3019
|
+
var ACCOUNTS_APP = "accounts";
|
|
3020
|
+
var profileNameSchema = z2.string().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*$/, "must be lowercase alphanumeric/hyphen and start with a letter or digit");
|
|
3021
|
+
var profileSchema = z2.object({
|
|
3022
|
+
name: z2.string(),
|
|
3023
|
+
tool: z2.string(),
|
|
3024
|
+
email: z2.string().optional(),
|
|
3025
|
+
displayName: z2.string().optional(),
|
|
3026
|
+
dir: z2.string(),
|
|
3027
|
+
description: z2.string().optional(),
|
|
3028
|
+
createdAt: z2.string(),
|
|
3029
|
+
lastUsedAt: z2.string().optional()
|
|
3030
|
+
});
|
|
3031
|
+
var toolDefSchema = z2.object({
|
|
3032
|
+
id: z2.string(),
|
|
3033
|
+
label: z2.string(),
|
|
3034
|
+
envVar: z2.string(),
|
|
3035
|
+
defaultDir: z2.string(),
|
|
3036
|
+
bin: z2.string()
|
|
3037
|
+
});
|
|
3038
|
+
var storeSchema = z2.object({
|
|
3039
|
+
version: z2.literal(1),
|
|
3040
|
+
current: z2.record(z2.string(), z2.string()).default({}),
|
|
3041
|
+
applied: z2.record(z2.string(), z2.string()).default({}),
|
|
3042
|
+
toolLocks: z2.record(z2.string(), z2.string()).default({}),
|
|
3043
|
+
profiles: z2.array(profileSchema).default([]),
|
|
3044
|
+
tools: z2.array(toolDefSchema).default([])
|
|
3045
|
+
});
|
|
3046
|
+
function validateEnvPath(value, label) {
|
|
3047
|
+
const trimmed = value.trim();
|
|
3048
|
+
if (!trimmed || trimmed.includes("\x00") || /[\r\n]/.test(trimmed))
|
|
3049
|
+
throw new Error(`invalid ${label}`);
|
|
3050
|
+
return trimmed;
|
|
3051
|
+
}
|
|
3052
|
+
function accountsHome() {
|
|
3053
|
+
const override = process.env.ACCOUNTS_HOME;
|
|
3054
|
+
if (override && override.trim())
|
|
3055
|
+
return validateEnvPath(override, "ACCOUNTS_HOME");
|
|
3056
|
+
return join3(homedir2(), ".hasna", "accounts");
|
|
3057
|
+
}
|
|
3058
|
+
function storePath() {
|
|
3059
|
+
const override = process.env.ACCOUNTS_STORE_PATH;
|
|
3060
|
+
if (override && override.trim())
|
|
3061
|
+
return validateEnvPath(override, "ACCOUNTS_STORE_PATH");
|
|
3062
|
+
return join3(accountsHome(), "accounts.json");
|
|
3063
|
+
}
|
|
3064
|
+
var EMPTY_STORE = {
|
|
3065
|
+
version: 1,
|
|
3066
|
+
current: {},
|
|
3067
|
+
applied: {},
|
|
3068
|
+
toolLocks: {},
|
|
3069
|
+
profiles: [],
|
|
3070
|
+
tools: []
|
|
3071
|
+
};
|
|
3072
|
+
function parseStoreFile() {
|
|
3073
|
+
const path = storePath();
|
|
3074
|
+
if (!existsSync3(path))
|
|
3075
|
+
return structuredClone(EMPTY_STORE);
|
|
3076
|
+
let raw;
|
|
3077
|
+
try {
|
|
3078
|
+
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
3079
|
+
} catch (err) {
|
|
3080
|
+
throw new Error(`could not parse store at ${path}: ${err instanceof Error ? err.message : String(err)}`);
|
|
3081
|
+
}
|
|
3082
|
+
const parsed = storeSchema.safeParse(raw);
|
|
3083
|
+
if (!parsed.success) {
|
|
3084
|
+
throw new Error(`invalid store at ${path}: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`);
|
|
3085
|
+
}
|
|
3086
|
+
return parsed.data;
|
|
3087
|
+
}
|
|
3088
|
+
function loadAppliedMap() {
|
|
3089
|
+
const applied = {};
|
|
3090
|
+
for (const [toolId, name] of Object.entries(parseStoreFile().applied)) {
|
|
3091
|
+
if (name && profileNameSchema.safeParse(name).success)
|
|
3092
|
+
applied[toolId] = name;
|
|
3093
|
+
}
|
|
3094
|
+
return applied;
|
|
3095
|
+
}
|
|
3096
|
+
function appliedProfileName(toolId) {
|
|
3097
|
+
return loadAppliedMap()[toolId];
|
|
3098
|
+
}
|
|
3099
|
+
var BUILTIN_TOOLS = [
|
|
3100
|
+
{ id: "claude", label: "Claude Code", envVar: "CLAUDE_CONFIG_DIR", defaultDir: join3(homedir2(), ".claude"), bin: "claude" },
|
|
3101
|
+
{ id: "codex-app", label: "Codex App", envVar: "CODEX_HOME", defaultDir: join3(homedir2(), ".codex"), bin: "/Applications/Codex.app/Contents/MacOS/Codex" },
|
|
3102
|
+
{ id: "codex", label: "Codex CLI", envVar: "CODEX_HOME", defaultDir: join3(homedir2(), ".codex"), bin: "codex" },
|
|
3103
|
+
{ id: "codewith", label: "Codewith", envVar: "CODEWITH_HOME", defaultDir: join3(homedir2(), ".codewith"), bin: "codewith" },
|
|
3104
|
+
{ id: "takumi", label: "Takumi", envVar: "TAKUMI_CONFIG_DIR", defaultDir: join3(homedir2(), ".takumi"), bin: "takumi" },
|
|
3105
|
+
{ id: "gemini", label: "Gemini CLI", envVar: "GEMINI_CONFIG_DIR", defaultDir: join3(homedir2(), ".gemini"), bin: "gemini" },
|
|
3106
|
+
{ id: "opencode", label: "opencode", envVar: "OPENCODE_CONFIG_DIR", defaultDir: join3(homedir2(), ".config", "opencode"), bin: "opencode" },
|
|
3107
|
+
{ id: "cursor", label: "Cursor Agent", envVar: "CURSOR_CONFIG_DIR", defaultDir: join3(homedir2(), ".cursor"), bin: "cursor-agent" },
|
|
3108
|
+
{ id: "pi", label: "Pi Coding Agent", envVar: "PI_CODING_AGENT_HOME", defaultDir: join3(homedir2(), ".pi"), bin: "pi" },
|
|
3109
|
+
{ id: "hermes", label: "Hermes", envVar: "HERMES_HOME", defaultDir: join3(homedir2(), ".hermes"), bin: "hermes" },
|
|
3110
|
+
{ id: "kimi", label: "Kimi Code", envVar: "KIMI_CODE_HOME", defaultDir: join3(homedir2(), ".kimi-code"), bin: "kimi" },
|
|
3111
|
+
{ id: "grok", label: "Grok Build", envVar: "HOME", defaultDir: join3(homedir2(), ".grok"), bin: "grok" }
|
|
3112
|
+
];
|
|
3113
|
+
|
|
3114
|
+
class LocalStore {
|
|
3115
|
+
transport = "local";
|
|
3116
|
+
async listProfiles(tool) {
|
|
3117
|
+
const profiles = parseStoreFile().profiles;
|
|
3118
|
+
const filtered = tool ? profiles.filter((profile) => profile.tool === tool) : profiles;
|
|
3119
|
+
return filtered.slice().sort((a, b) => a.tool.localeCompare(b.tool) || a.name.localeCompare(b.name));
|
|
3120
|
+
}
|
|
3121
|
+
async findProfile(name, tool) {
|
|
3122
|
+
const matches = parseStoreFile().profiles.filter((profile) => profile.name === name && (!tool || profile.tool === tool));
|
|
3123
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
3124
|
+
}
|
|
3125
|
+
async currentProfile(tool) {
|
|
3126
|
+
const store = parseStoreFile();
|
|
3127
|
+
const name = store.current[tool];
|
|
3128
|
+
if (!name)
|
|
3129
|
+
return;
|
|
3130
|
+
return store.profiles.find((profile) => profile.name === name && profile.tool === tool);
|
|
3131
|
+
}
|
|
3132
|
+
async listTools() {
|
|
3133
|
+
const custom = parseStoreFile().tools;
|
|
3134
|
+
const byId = new Map;
|
|
3135
|
+
for (const tool of BUILTIN_TOOLS)
|
|
3136
|
+
byId.set(tool.id, tool);
|
|
3137
|
+
for (const tool of custom)
|
|
3138
|
+
byId.set(tool.id, tool);
|
|
3139
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
var RETIRED_STORAGE_MODE_KEYS2 = [
|
|
3143
|
+
"HASNA_ACCOUNTS_STORAGE_MODE",
|
|
3144
|
+
"ACCOUNTS_STORAGE_MODE",
|
|
3145
|
+
"HASNA_ACCOUNTS_MODE",
|
|
3146
|
+
"ACCOUNTS_MODE"
|
|
3147
|
+
];
|
|
3148
|
+
function assertNoRetiredStorageMode2(env) {
|
|
3149
|
+
const legacyKey = RETIRED_STORAGE_MODE_KEYS2.find((key) => Object.hasOwn(env, key) && env[key] !== undefined);
|
|
3150
|
+
if (!legacyKey)
|
|
3151
|
+
return;
|
|
3152
|
+
throw new Error(`${legacyKey} was removed. Deployment modes no longer exist: delete the storage-mode variable. ` + `The accounts registry uses the local JSON store, or the HTTP API selected by ` + `HASNA_ACCOUNTS_API_URL + HASNA_ACCOUNTS_API_KEY.`);
|
|
3153
|
+
}
|
|
3154
|
+
var RETRY_STATUSES = [408, 425, 429, 500, 502, 503, 504];
|
|
3155
|
+
|
|
3156
|
+
class AccountsApiError extends Error {
|
|
3157
|
+
status;
|
|
3158
|
+
body;
|
|
3159
|
+
constructor(method, path, status, body) {
|
|
3160
|
+
super(`accounts cloud request failed: ${method} ${path} -> ${status}`);
|
|
3161
|
+
this.name = "AccountsApiError";
|
|
3162
|
+
this.status = status;
|
|
3163
|
+
this.body = body;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
function isNotFound(err) {
|
|
3167
|
+
return Boolean(err && typeof err === "object" && err.status === 404);
|
|
3168
|
+
}
|
|
3169
|
+
function sleep(ms) {
|
|
3170
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
3171
|
+
}
|
|
3172
|
+
async function cloudRequest(baseUrl, apiKey, path, query) {
|
|
3173
|
+
const rel = path.startsWith("/") ? path : `/${path}`;
|
|
3174
|
+
const url = query && Object.keys(query).length > 0 ? `${baseUrl}${rel}?${new URLSearchParams(query).toString()}` : `${baseUrl}${rel}`;
|
|
3175
|
+
let lastError;
|
|
3176
|
+
for (let attempt = 0;attempt < 3; attempt += 1) {
|
|
3177
|
+
const controller = new AbortController;
|
|
3178
|
+
const timer = setTimeout(() => controller.abort(), 30000);
|
|
3179
|
+
let res;
|
|
3180
|
+
try {
|
|
3181
|
+
res = await fetch(url, {
|
|
3182
|
+
method: "GET",
|
|
3183
|
+
headers: {
|
|
3184
|
+
"x-api-key": apiKey,
|
|
3185
|
+
Authorization: `Bearer ${apiKey}`,
|
|
3186
|
+
Accept: "application/json"
|
|
3187
|
+
},
|
|
3188
|
+
signal: controller.signal
|
|
3189
|
+
});
|
|
3190
|
+
} catch (error) {
|
|
3191
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
3192
|
+
clearTimeout(timer);
|
|
3193
|
+
if (attempt < 2) {
|
|
3194
|
+
const backoff = Math.min(2000, 200 * 2 ** attempt);
|
|
3195
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
3196
|
+
await sleep(backoff + jitter);
|
|
3197
|
+
}
|
|
3198
|
+
continue;
|
|
3199
|
+
} finally {
|
|
3200
|
+
clearTimeout(timer);
|
|
3201
|
+
}
|
|
3202
|
+
const text = await res.text();
|
|
3203
|
+
let parsed;
|
|
3204
|
+
if (text.length > 0) {
|
|
3205
|
+
try {
|
|
3206
|
+
parsed = JSON.parse(text);
|
|
3207
|
+
} catch {
|
|
3208
|
+
parsed = text;
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
if (!res.ok) {
|
|
3212
|
+
lastError = new AccountsApiError("GET", rel, res.status, parsed);
|
|
3213
|
+
if (attempt < 2 && RETRY_STATUSES.includes(res.status)) {
|
|
3214
|
+
const backoff = Math.min(2000, 200 * 2 ** attempt);
|
|
3215
|
+
const jitter = Math.floor(Math.random() * (backoff / 2 + 1));
|
|
3216
|
+
await sleep(backoff + jitter);
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
break;
|
|
3220
|
+
}
|
|
3221
|
+
return parsed;
|
|
3222
|
+
}
|
|
3223
|
+
throw lastError;
|
|
3224
|
+
}
|
|
3225
|
+
function toProfile(account) {
|
|
3226
|
+
const profile = {
|
|
3227
|
+
name: String(account.name),
|
|
3228
|
+
tool: String(account.tool),
|
|
3229
|
+
dir: typeof account.dir === "string" ? account.dir : "",
|
|
3230
|
+
createdAt: String(account.createdAt)
|
|
3231
|
+
};
|
|
3232
|
+
if (typeof account.email === "string")
|
|
3233
|
+
profile.email = account.email;
|
|
3234
|
+
if (typeof account.displayName === "string")
|
|
3235
|
+
profile.displayName = account.displayName;
|
|
3236
|
+
if (typeof account.description === "string")
|
|
3237
|
+
profile.description = account.description;
|
|
3238
|
+
if (typeof account.lastUsedAt === "string")
|
|
3239
|
+
profile.lastUsedAt = account.lastUsedAt;
|
|
3240
|
+
return profile;
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
class ApiStore {
|
|
3244
|
+
baseUrl;
|
|
3245
|
+
apiKey;
|
|
3246
|
+
transport = "api";
|
|
3247
|
+
constructor(baseUrl, apiKey) {
|
|
3248
|
+
this.baseUrl = baseUrl;
|
|
3249
|
+
this.apiKey = apiKey;
|
|
3250
|
+
}
|
|
3251
|
+
async listProfiles(tool) {
|
|
3252
|
+
const raw = await cloudRequest(this.baseUrl, this.apiKey, "/accounts", tool ? { tool } : undefined);
|
|
3253
|
+
const accounts = Array.isArray(raw?.accounts) ? raw.accounts : [];
|
|
3254
|
+
return accounts.map((account) => toProfile(account)).sort((a, b) => a.tool.localeCompare(b.tool) || a.name.localeCompare(b.name));
|
|
3255
|
+
}
|
|
3256
|
+
async findProfile(name, tool) {
|
|
3257
|
+
if (tool) {
|
|
3258
|
+
try {
|
|
3259
|
+
const account = await cloudRequest(this.baseUrl, this.apiKey, `/accounts/${encodeURIComponent(tool)}/${encodeURIComponent(name)}`);
|
|
3260
|
+
return account ? toProfile(account) : undefined;
|
|
3261
|
+
} catch (err) {
|
|
3262
|
+
if (isNotFound(err))
|
|
3263
|
+
return;
|
|
3264
|
+
throw err;
|
|
3265
|
+
}
|
|
3266
|
+
}
|
|
3267
|
+
const matches = (await this.listProfiles()).filter((profile) => profile.name === name);
|
|
3268
|
+
return matches.length === 1 ? matches[0] : undefined;
|
|
3269
|
+
}
|
|
3270
|
+
async currentProfile(tool) {
|
|
3271
|
+
let current;
|
|
3272
|
+
try {
|
|
3273
|
+
const raw = await cloudRequest(this.baseUrl, this.apiKey, `/current/${encodeURIComponent(tool)}`);
|
|
3274
|
+
current = raw && typeof raw === "object" ? raw : null;
|
|
3275
|
+
} catch (err) {
|
|
3276
|
+
if (isNotFound(err))
|
|
3277
|
+
return;
|
|
3278
|
+
throw err;
|
|
3279
|
+
}
|
|
3280
|
+
if (!current || typeof current.name !== "string")
|
|
3281
|
+
return;
|
|
3282
|
+
return this.findProfile(current.name, tool);
|
|
3283
|
+
}
|
|
3284
|
+
async listTools() {
|
|
3285
|
+
const raw = await cloudRequest(this.baseUrl, this.apiKey, "/tools");
|
|
3286
|
+
const tools = Array.isArray(raw?.tools) ? raw.tools : [];
|
|
3287
|
+
const custom = [];
|
|
3288
|
+
for (const item of tools) {
|
|
3289
|
+
if (item === null || typeof item !== "object")
|
|
3290
|
+
continue;
|
|
3291
|
+
const record = item;
|
|
3292
|
+
if (record.builtin !== false)
|
|
3293
|
+
continue;
|
|
3294
|
+
const parsed = toolDefSchema.safeParse(item);
|
|
3295
|
+
if (!parsed.success) {
|
|
3296
|
+
throw new Error(`invalid custom tool "${String(record.id)}" returned by accounts-serve: ` + parsed.error.issues.map((issue) => issue.message).join("; "));
|
|
3297
|
+
}
|
|
3298
|
+
custom.push(parsed.data);
|
|
3299
|
+
}
|
|
3300
|
+
const byId = new Map;
|
|
3301
|
+
for (const tool of BUILTIN_TOOLS)
|
|
3302
|
+
byId.set(tool.id, tool);
|
|
3303
|
+
for (const tool of custom)
|
|
3304
|
+
byId.set(tool.id, tool);
|
|
3305
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
function resolveStore(env = process.env, options = {}) {
|
|
3309
|
+
assertNoRetiredStorageMode2(env);
|
|
3310
|
+
const clientEnv = env;
|
|
3311
|
+
try {
|
|
3312
|
+
const credential = resolveCredential(ACCOUNTS_APP, clientEnv, options.credentials);
|
|
3313
|
+
if (credential === null)
|
|
3314
|
+
return new LocalStore;
|
|
3315
|
+
const resolution = resolveClientTransport(ACCOUNTS_APP, clientEnv, {
|
|
3316
|
+
credentials: credential ? { ...options.credentials, apiKey: credential.apiKey } : options.credentials
|
|
3317
|
+
});
|
|
3318
|
+
return new ApiStore(resolution.baseUrl, credential.apiKey);
|
|
3319
|
+
} catch (err) {
|
|
3320
|
+
console.warn(`accounts: API configuration unusable (${err instanceof Error ? err.message : String(err)}); using local store`);
|
|
3321
|
+
return new LocalStore;
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
|
|
2397
3325
|
// src/lib/accounts.ts
|
|
2398
|
-
import { appliedProfileName, resolveStore } from "@hasna/accounts";
|
|
2399
3326
|
var AGENT_ACCOUNT_TOOLS = {
|
|
2400
3327
|
claude: ["claude"],
|
|
2401
3328
|
takumi: ["takumi", "claude"],
|
|
@@ -2467,11 +3394,11 @@ async function profileForEnvDir(store, tool, env) {
|
|
|
2467
3394
|
const normalized = normalizeDir(configuredDir);
|
|
2468
3395
|
return (await store.listProfiles(tool.id)).find((profile) => normalizeDir(profile.dir) === normalized) ?? null;
|
|
2469
3396
|
}
|
|
2470
|
-
async function resolveAccountForAgent(agent, env = process.env) {
|
|
3397
|
+
async function resolveAccountForAgent(agent, env = process.env, options = {}) {
|
|
2471
3398
|
const override = envOverride(agent, env);
|
|
2472
3399
|
if (override)
|
|
2473
3400
|
return override;
|
|
2474
|
-
const store = resolveStore(env);
|
|
3401
|
+
const store = resolveStore(env, options);
|
|
2475
3402
|
const tools = new Map((await store.listTools()).map((tool) => [tool.id, tool]));
|
|
2476
3403
|
for (const toolId of AGENT_ACCOUNT_TOOLS[agent]) {
|
|
2477
3404
|
const tool = tools.get(toolId);
|
|
@@ -2509,8 +3436,8 @@ function withAccount(record, account) {
|
|
|
2509
3436
|
function autoDetectProject(cwd, projects) {
|
|
2510
3437
|
return projects.find((p) => cwd === p.path || cwd.startsWith(p.path + "/"));
|
|
2511
3438
|
}
|
|
2512
|
-
var CLAUDE_PROJECTS_DIR =
|
|
2513
|
-
var TAKUMI_PROJECTS_DIR =
|
|
3439
|
+
var CLAUDE_PROJECTS_DIR = process.env["HASNA_ECONOMY_CLAUDE_PROJECTS_DIR"] ?? join4(homedir3(), ".claude", "projects");
|
|
3440
|
+
var TAKUMI_PROJECTS_DIR = process.env["HASNA_ECONOMY_TAKUMI_PROJECTS_DIR"] ?? join4(homedir3(), ".takumi", "projects");
|
|
2514
3441
|
function dirNameToPath(dirName) {
|
|
2515
3442
|
return dirName.replace(/^-/, "/").replace(/-/g, "/").replace(/\/\//g, "/-");
|
|
2516
3443
|
}
|
|
@@ -2520,9 +3447,9 @@ function collectJsonlFiles(projectDir) {
|
|
|
2520
3447
|
try {
|
|
2521
3448
|
for (const entry of readdirSync2(dir, { withFileTypes: true })) {
|
|
2522
3449
|
if (entry.isDirectory())
|
|
2523
|
-
walk(
|
|
3450
|
+
walk(join4(dir, entry.name));
|
|
2524
3451
|
else if (entry.name.endsWith(".jsonl"))
|
|
2525
|
-
files.push(
|
|
3452
|
+
files.push(join4(dir, entry.name));
|
|
2526
3453
|
}
|
|
2527
3454
|
} catch {}
|
|
2528
3455
|
}
|
|
@@ -2536,7 +3463,7 @@ async function ingestTakumi(db, verbose = false, projectsDir = TAKUMI_PROJECTS_D
|
|
|
2536
3463
|
return ingestJsonlProjects(db, projectsDir, "takumi", verbose);
|
|
2537
3464
|
}
|
|
2538
3465
|
async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false) {
|
|
2539
|
-
if (!
|
|
3466
|
+
if (!existsSync4(projectsDir)) {
|
|
2540
3467
|
if (verbose)
|
|
2541
3468
|
console.log(`${agentName} projects dir not found:`, projectsDir);
|
|
2542
3469
|
return { files: 0, requests: 0, sessions: 0 };
|
|
@@ -2547,9 +3474,10 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2547
3474
|
const touchedSessions = new Set;
|
|
2548
3475
|
const registeredProjects = db.prepare(`SELECT path, name FROM projects ORDER BY LENGTH(path) DESC`).all();
|
|
2549
3476
|
const account = await resolveAccountForAgent(agentName);
|
|
3477
|
+
const ingestState = loadIngestState(db, agentName);
|
|
2550
3478
|
const projectDirs = readdirSync2(projectsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
2551
3479
|
for (const projectDirEntry of projectDirs) {
|
|
2552
|
-
const projectDirPath =
|
|
3480
|
+
const projectDirPath = join4(projectsDir, projectDirEntry.name);
|
|
2553
3481
|
const projectPath = dirNameToPath(projectDirEntry.name);
|
|
2554
3482
|
const jsonlFiles = collectJsonlFiles(projectDirPath);
|
|
2555
3483
|
for (const filePath of jsonlFiles) {
|
|
@@ -2560,12 +3488,12 @@ async function ingestJsonlProjects(db, projectsDir, agentName, verbose = false)
|
|
|
2560
3488
|
} catch {
|
|
2561
3489
|
continue;
|
|
2562
3490
|
}
|
|
2563
|
-
const processed =
|
|
3491
|
+
const processed = ingestState.get(stateKey) ?? null;
|
|
2564
3492
|
if (processed === fileMtime)
|
|
2565
3493
|
continue;
|
|
2566
3494
|
let lines;
|
|
2567
3495
|
try {
|
|
2568
|
-
lines =
|
|
3496
|
+
lines = readFileSync2(filePath, "utf-8").split(`
|
|
2569
3497
|
`).filter((l) => l.trim());
|
|
2570
3498
|
} catch {
|
|
2571
3499
|
continue;
|
|
@@ -2685,14 +3613,14 @@ function supportsClaudeDataResidencyPricing(model) {
|
|
|
2685
3613
|
// src/ingest/codex.ts
|
|
2686
3614
|
init_database();
|
|
2687
3615
|
init_pricing();
|
|
2688
|
-
import { existsSync as
|
|
2689
|
-
import { homedir as
|
|
2690
|
-
import { join as
|
|
3616
|
+
import { existsSync as existsSync5, readFileSync as readFileSync3 } from "fs";
|
|
3617
|
+
import { homedir as homedir4 } from "os";
|
|
3618
|
+
import { join as join5, basename as basename2 } from "path";
|
|
2691
3619
|
import { Database as BunDatabase } from "bun:sqlite";
|
|
2692
|
-
var DEFAULT_CODEX_DB_PATH =
|
|
2693
|
-
var DEFAULT_CODEX_CONFIG_PATH =
|
|
2694
|
-
var DEFAULT_CODEWITH_DB_PATH =
|
|
2695
|
-
var DEFAULT_CODEWITH_CONFIG_PATH =
|
|
3620
|
+
var DEFAULT_CODEX_DB_PATH = join5(homedir4(), ".codex", "state_5.sqlite");
|
|
3621
|
+
var DEFAULT_CODEX_CONFIG_PATH = join5(homedir4(), ".codex", "config.toml");
|
|
3622
|
+
var DEFAULT_CODEWITH_DB_PATH = join5(homedir4(), ".codewith", "state_5.sqlite");
|
|
3623
|
+
var DEFAULT_CODEWITH_CONFIG_PATH = join5(homedir4(), ".codewith", "config.toml");
|
|
2696
3624
|
var CODEX_INGEST_VERSION = "rollout-aggregate-v3";
|
|
2697
3625
|
function codexDbPath() {
|
|
2698
3626
|
return process.env["HASNA_ECONOMY_CODEX_DB_PATH"] ?? DEFAULT_CODEX_DB_PATH;
|
|
@@ -2734,10 +3662,10 @@ function codexSources() {
|
|
|
2734
3662
|
});
|
|
2735
3663
|
}
|
|
2736
3664
|
function readCodexModel(configPath = codexConfigPath()) {
|
|
2737
|
-
if (!
|
|
3665
|
+
if (!existsSync5(configPath))
|
|
2738
3666
|
return "gpt-5-codex";
|
|
2739
3667
|
try {
|
|
2740
|
-
const content =
|
|
3668
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
2741
3669
|
const match = content.match(/^model\s*=\s*"([^"]+)"/m);
|
|
2742
3670
|
return match?.[1] ?? "gpt-5-codex";
|
|
2743
3671
|
} catch {
|
|
@@ -2775,12 +3703,12 @@ function openCodexDb(dbPath, verbose) {
|
|
|
2775
3703
|
return null;
|
|
2776
3704
|
}
|
|
2777
3705
|
function readTokenEvents(rolloutPath) {
|
|
2778
|
-
if (!rolloutPath || !
|
|
3706
|
+
if (!rolloutPath || !existsSync5(rolloutPath))
|
|
2779
3707
|
return [];
|
|
2780
3708
|
const fallbackUsages = new Map;
|
|
2781
3709
|
let fallbackTimestamp;
|
|
2782
3710
|
let aggregate = null;
|
|
2783
|
-
for (const line of
|
|
3711
|
+
for (const line of readFileSync3(rolloutPath, "utf-8").split(`
|
|
2784
3712
|
`)) {
|
|
2785
3713
|
if (!line.trim())
|
|
2786
3714
|
continue;
|
|
@@ -2856,7 +3784,7 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2856
3784
|
let requests = 0;
|
|
2857
3785
|
const account = await resolveAccountForAgent("codex");
|
|
2858
3786
|
for (const source of codexSources()) {
|
|
2859
|
-
if (!
|
|
3787
|
+
if (!existsSync5(source.dbPath)) {
|
|
2860
3788
|
if (verbose)
|
|
2861
3789
|
console.log(`${source.label} DB not found:`, source.dbPath);
|
|
2862
3790
|
continue;
|
|
@@ -2937,11 +3865,11 @@ async function ingestCodex(db, verbose = false) {
|
|
|
2937
3865
|
// src/ingest/gemini.ts
|
|
2938
3866
|
init_database();
|
|
2939
3867
|
init_pricing();
|
|
2940
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
2941
|
-
import { homedir as
|
|
2942
|
-
import { join as
|
|
2943
|
-
var DEFAULT_GEMINI_TMP_DIR =
|
|
2944
|
-
var DEFAULT_GEMINI_HISTORY_DIR =
|
|
3868
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync4, existsSync as existsSync6, statSync as statSync3 } from "fs";
|
|
3869
|
+
import { homedir as homedir5 } from "os";
|
|
3870
|
+
import { join as join6, basename as basename3 } from "path";
|
|
3871
|
+
var DEFAULT_GEMINI_TMP_DIR = join6(homedir5(), ".gemini", "tmp");
|
|
3872
|
+
var DEFAULT_GEMINI_HISTORY_DIR = join6(homedir5(), ".gemini", "history");
|
|
2945
3873
|
function geminiTmpDir() {
|
|
2946
3874
|
return process.env["HASNA_ECONOMY_GEMINI_TMP_DIR"] ?? DEFAULT_GEMINI_TMP_DIR;
|
|
2947
3875
|
}
|
|
@@ -2958,12 +3886,12 @@ function numberField(...values) {
|
|
|
2958
3886
|
function listProjectDirs(...roots) {
|
|
2959
3887
|
const dirs = new Set;
|
|
2960
3888
|
for (const root of roots) {
|
|
2961
|
-
if (!
|
|
3889
|
+
if (!existsSync6(root))
|
|
2962
3890
|
continue;
|
|
2963
3891
|
try {
|
|
2964
3892
|
for (const entry of readdirSync3(root, { withFileTypes: true })) {
|
|
2965
3893
|
if (entry.isDirectory())
|
|
2966
|
-
dirs.add(
|
|
3894
|
+
dirs.add(join6(root, entry.name));
|
|
2967
3895
|
}
|
|
2968
3896
|
} catch {}
|
|
2969
3897
|
}
|
|
@@ -2974,17 +3902,17 @@ function projectRoot(projectDir, chatData) {
|
|
|
2974
3902
|
return chatData.projectPath;
|
|
2975
3903
|
if (chatData.project_path)
|
|
2976
3904
|
return chatData.project_path;
|
|
2977
|
-
const rootFile =
|
|
3905
|
+
const rootFile = join6(projectDir, ".project_root");
|
|
2978
3906
|
try {
|
|
2979
|
-
if (
|
|
2980
|
-
return
|
|
3907
|
+
if (existsSync6(rootFile))
|
|
3908
|
+
return readFileSync4(rootFile, "utf-8").trim();
|
|
2981
3909
|
} catch {}
|
|
2982
3910
|
return "";
|
|
2983
3911
|
}
|
|
2984
3912
|
async function ingestGemini(db, verbose) {
|
|
2985
3913
|
const tmpDir = geminiTmpDir();
|
|
2986
3914
|
const historyDir = geminiHistoryDir();
|
|
2987
|
-
if (!
|
|
3915
|
+
if (!existsSync6(tmpDir) && !existsSync6(historyDir)) {
|
|
2988
3916
|
if (verbose)
|
|
2989
3917
|
console.log("Gemini tmp/history dirs not found:", tmpDir, historyDir);
|
|
2990
3918
|
return { sessions: 0, requests: 0 };
|
|
@@ -2996,17 +3924,17 @@ async function ingestGemini(db, verbose) {
|
|
|
2996
3924
|
const account = await resolveAccountForAgent("gemini");
|
|
2997
3925
|
const projectDirs = listProjectDirs(tmpDir, historyDir);
|
|
2998
3926
|
for (const projectDir of projectDirs) {
|
|
2999
|
-
const chatsDir =
|
|
3000
|
-
if (!
|
|
3927
|
+
const chatsDir = join6(projectDir, "chats");
|
|
3928
|
+
if (!existsSync6(chatsDir))
|
|
3001
3929
|
continue;
|
|
3002
3930
|
let chatFiles = [];
|
|
3003
3931
|
try {
|
|
3004
|
-
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) =>
|
|
3932
|
+
chatFiles = readdirSync3(chatsDir).filter((f) => f.endsWith(".json")).map((f) => join6(chatsDir, f));
|
|
3005
3933
|
} catch {
|
|
3006
3934
|
continue;
|
|
3007
3935
|
}
|
|
3008
3936
|
for (const filePath of chatFiles) {
|
|
3009
|
-
const stateKey = filePath.replace(
|
|
3937
|
+
const stateKey = filePath.replace(homedir5(), "~");
|
|
3010
3938
|
let fileMtime = "0";
|
|
3011
3939
|
try {
|
|
3012
3940
|
fileMtime = statSync3(filePath).mtimeMs.toString();
|
|
@@ -3018,7 +3946,7 @@ async function ingestGemini(db, verbose) {
|
|
|
3018
3946
|
continue;
|
|
3019
3947
|
let chatData;
|
|
3020
3948
|
try {
|
|
3021
|
-
chatData = JSON.parse(
|
|
3949
|
+
chatData = JSON.parse(readFileSync4(filePath, "utf-8"));
|
|
3022
3950
|
} catch {
|
|
3023
3951
|
continue;
|
|
3024
3952
|
}
|
|
@@ -3096,15 +4024,15 @@ async function ingestGemini(db, verbose) {
|
|
|
3096
4024
|
// src/ingest/opencode.ts
|
|
3097
4025
|
init_database();
|
|
3098
4026
|
init_pricing();
|
|
3099
|
-
import { existsSync as
|
|
3100
|
-
import { homedir as
|
|
3101
|
-
import { join as
|
|
3102
|
-
var OPENCODE_STORAGE =
|
|
4027
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, readdirSync as readdirSync4, statSync as statSync4 } from "fs";
|
|
4028
|
+
import { homedir as homedir6 } from "os";
|
|
4029
|
+
import { join as join7 } from "path";
|
|
4030
|
+
var OPENCODE_STORAGE = join7(homedir6(), ".local", "share", "opencode", "storage");
|
|
3103
4031
|
function walkJsonFiles(dir, acc = []) {
|
|
3104
|
-
if (!
|
|
4032
|
+
if (!existsSync7(dir))
|
|
3105
4033
|
return acc;
|
|
3106
4034
|
for (const entry of readdirSync4(dir, { withFileTypes: true })) {
|
|
3107
|
-
const p =
|
|
4035
|
+
const p = join7(dir, entry.name);
|
|
3108
4036
|
if (entry.isDirectory())
|
|
3109
4037
|
walkJsonFiles(p, acc);
|
|
3110
4038
|
else if (entry.name.endsWith(".json"))
|
|
@@ -3124,12 +4052,12 @@ function parseSessionIdFromPath(filePath) {
|
|
|
3124
4052
|
return null;
|
|
3125
4053
|
}
|
|
3126
4054
|
async function ingestOpenCode(db, verbose = false) {
|
|
3127
|
-
const messageDir =
|
|
4055
|
+
const messageDir = join7(OPENCODE_STORAGE, "message");
|
|
3128
4056
|
const files = walkJsonFiles(messageDir);
|
|
3129
4057
|
let requests = 0;
|
|
3130
4058
|
const touched = new Set;
|
|
3131
4059
|
const machineId = getMachineId();
|
|
3132
|
-
const
|
|
4060
|
+
const now2 = new Date().toISOString();
|
|
3133
4061
|
const account = await resolveAccountForAgent("opencode");
|
|
3134
4062
|
for (const file of files) {
|
|
3135
4063
|
const mtime = statSync4(file).mtimeMs;
|
|
@@ -3139,7 +4067,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
3139
4067
|
continue;
|
|
3140
4068
|
let parsed;
|
|
3141
4069
|
try {
|
|
3142
|
-
parsed = JSON.parse(
|
|
4070
|
+
parsed = JSON.parse(readFileSync5(file, "utf-8"));
|
|
3143
4071
|
} catch {
|
|
3144
4072
|
continue;
|
|
3145
4073
|
}
|
|
@@ -3175,7 +4103,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
3175
4103
|
timestamp,
|
|
3176
4104
|
source_request_id: sourceId,
|
|
3177
4105
|
machine_id: machineId,
|
|
3178
|
-
updated_at:
|
|
4106
|
+
updated_at: now2
|
|
3179
4107
|
}, account));
|
|
3180
4108
|
requests++;
|
|
3181
4109
|
if (!touched.has(sessionId)) {
|
|
@@ -3190,7 +4118,7 @@ async function ingestOpenCode(db, verbose = false) {
|
|
|
3190
4118
|
total_tokens: 0,
|
|
3191
4119
|
request_count: 0,
|
|
3192
4120
|
machine_id: machineId,
|
|
3193
|
-
updated_at:
|
|
4121
|
+
updated_at: now2
|
|
3194
4122
|
}, account));
|
|
3195
4123
|
touched.add(sessionId);
|
|
3196
4124
|
}
|
|
@@ -3236,7 +4164,7 @@ async function ingestCursor(db, verbose = false) {
|
|
|
3236
4164
|
if (prev)
|
|
3237
4165
|
return { requests: 0, snapshots: 0 };
|
|
3238
4166
|
const machineId = getMachineId();
|
|
3239
|
-
const
|
|
4167
|
+
const now2 = new Date().toISOString();
|
|
3240
4168
|
let snapshots = 0;
|
|
3241
4169
|
const account = await resolveAccountForAgent("cursor");
|
|
3242
4170
|
const usage = await cursorFetch("/api/usage", token);
|
|
@@ -3292,12 +4220,12 @@ async function ingestCursor(db, verbose = false) {
|
|
|
3292
4220
|
project_path: "",
|
|
3293
4221
|
project_name: "Cursor subscription",
|
|
3294
4222
|
started_at: `${today}T00:00:00.000Z`,
|
|
3295
|
-
ended_at:
|
|
4223
|
+
ended_at: now2,
|
|
3296
4224
|
total_cost_usd: onDemand + included,
|
|
3297
4225
|
total_tokens: 0,
|
|
3298
4226
|
request_count: 1,
|
|
3299
4227
|
machine_id: machineId,
|
|
3300
|
-
updated_at:
|
|
4228
|
+
updated_at: now2
|
|
3301
4229
|
}, account));
|
|
3302
4230
|
upsertRequest(db, withAccount({
|
|
3303
4231
|
id: `cursor-${today}-${machineId}-usage`,
|
|
@@ -3311,14 +4239,14 @@ async function ingestCursor(db, verbose = false) {
|
|
|
3311
4239
|
cost_usd: onDemand + included,
|
|
3312
4240
|
cost_basis: "subscription_included",
|
|
3313
4241
|
duration_ms: 0,
|
|
3314
|
-
timestamp:
|
|
4242
|
+
timestamp: now2,
|
|
3315
4243
|
source_request_id: today,
|
|
3316
4244
|
machine_id: machineId,
|
|
3317
|
-
updated_at:
|
|
4245
|
+
updated_at: now2
|
|
3318
4246
|
}, account));
|
|
3319
4247
|
rollupSession(db, sessionId);
|
|
3320
4248
|
}
|
|
3321
|
-
setIngestState(db, "cursor", `sync-${today}`,
|
|
4249
|
+
setIngestState(db, "cursor", `sync-${today}`, now2);
|
|
3322
4250
|
if (verbose)
|
|
3323
4251
|
console.log(` cursor: on-demand $${onDemand.toFixed(2)}, included $${included.toFixed(2)}`);
|
|
3324
4252
|
return { requests: onDemand + included > 0 ? 1 : 0, snapshots };
|
|
@@ -3326,15 +4254,15 @@ async function ingestCursor(db, verbose = false) {
|
|
|
3326
4254
|
|
|
3327
4255
|
// src/ingest/pi.ts
|
|
3328
4256
|
init_database();
|
|
3329
|
-
import { existsSync as
|
|
3330
|
-
import { homedir as
|
|
3331
|
-
import { join as
|
|
3332
|
-
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ??
|
|
4257
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, readdirSync as readdirSync5, statSync as statSync5 } from "fs";
|
|
4258
|
+
import { homedir as homedir7 } from "os";
|
|
4259
|
+
import { join as join8 } from "path";
|
|
4260
|
+
var PI_SESSION_DIR = process.env["PI_CODING_AGENT_SESSION_DIR"] ?? join8(homedir7(), ".pi", "agent", "sessions");
|
|
3333
4261
|
function walkSessions(dir, acc = []) {
|
|
3334
|
-
if (!
|
|
4262
|
+
if (!existsSync8(dir))
|
|
3335
4263
|
return acc;
|
|
3336
4264
|
for (const entry of readdirSync5(dir, { withFileTypes: true })) {
|
|
3337
|
-
const p =
|
|
4265
|
+
const p = join8(dir, entry.name);
|
|
3338
4266
|
if (entry.isDirectory())
|
|
3339
4267
|
walkSessions(p, acc);
|
|
3340
4268
|
else if (entry.name.endsWith(".json"))
|
|
@@ -3347,7 +4275,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
3347
4275
|
let requests = 0;
|
|
3348
4276
|
const touched = new Set;
|
|
3349
4277
|
const machineId = getMachineId();
|
|
3350
|
-
const
|
|
4278
|
+
const now2 = new Date().toISOString();
|
|
3351
4279
|
const account = await resolveAccountForAgent("pi");
|
|
3352
4280
|
for (const file of files) {
|
|
3353
4281
|
const mtime = statSync5(file).mtimeMs;
|
|
@@ -3356,7 +4284,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
3356
4284
|
continue;
|
|
3357
4285
|
let data;
|
|
3358
4286
|
try {
|
|
3359
|
-
data = JSON.parse(
|
|
4287
|
+
data = JSON.parse(readFileSync6(file, "utf-8"));
|
|
3360
4288
|
} catch {
|
|
3361
4289
|
continue;
|
|
3362
4290
|
}
|
|
@@ -3389,7 +4317,7 @@ async function ingestPi(db, verbose = false) {
|
|
|
3389
4317
|
timestamp,
|
|
3390
4318
|
source_request_id: `${sessionId}-${i}`,
|
|
3391
4319
|
machine_id: machineId,
|
|
3392
|
-
updated_at:
|
|
4320
|
+
updated_at: now2
|
|
3393
4321
|
}, account));
|
|
3394
4322
|
requests++;
|
|
3395
4323
|
}
|
|
@@ -3399,13 +4327,13 @@ async function ingestPi(db, verbose = false) {
|
|
|
3399
4327
|
agent: "pi",
|
|
3400
4328
|
project_path: "",
|
|
3401
4329
|
project_name: "",
|
|
3402
|
-
started_at: turns[0]?.timestamp ??
|
|
4330
|
+
started_at: turns[0]?.timestamp ?? now2,
|
|
3403
4331
|
ended_at: null,
|
|
3404
4332
|
total_cost_usd: 0,
|
|
3405
4333
|
total_tokens: 0,
|
|
3406
4334
|
request_count: 0,
|
|
3407
4335
|
machine_id: machineId,
|
|
3408
|
-
updated_at:
|
|
4336
|
+
updated_at: now2
|
|
3409
4337
|
}, account));
|
|
3410
4338
|
touched.add(sessionId);
|
|
3411
4339
|
}
|
|
@@ -3420,10 +4348,10 @@ async function ingestPi(db, verbose = false) {
|
|
|
3420
4348
|
|
|
3421
4349
|
// src/ingest/hermes.ts
|
|
3422
4350
|
init_database();
|
|
3423
|
-
import { existsSync as
|
|
3424
|
-
import { homedir as
|
|
3425
|
-
import { join as
|
|
3426
|
-
var HERMES_DB =
|
|
4351
|
+
import { existsSync as existsSync9, statSync as statSync6 } from "fs";
|
|
4352
|
+
import { homedir as homedir8 } from "os";
|
|
4353
|
+
import { join as join9 } from "path";
|
|
4354
|
+
var HERMES_DB = join9(homedir8(), ".hermes", "state.db");
|
|
3427
4355
|
function mapCostBasis(billingMode) {
|
|
3428
4356
|
if (billingMode === "subscription")
|
|
3429
4357
|
return "subscription_included";
|
|
@@ -3432,7 +4360,7 @@ function mapCostBasis(billingMode) {
|
|
|
3432
4360
|
return defaultCostBasisForAgent("hermes");
|
|
3433
4361
|
}
|
|
3434
4362
|
async function ingestHermes(db, verbose = false) {
|
|
3435
|
-
if (!
|
|
4363
|
+
if (!existsSync9(HERMES_DB)) {
|
|
3436
4364
|
return { sessions: 0, requests: 0 };
|
|
3437
4365
|
}
|
|
3438
4366
|
const { Database: Sqlite } = await import("bun:sqlite");
|
|
@@ -3452,7 +4380,7 @@ async function ingestHermes(db, verbose = false) {
|
|
|
3452
4380
|
return { sessions: 0, requests: 0 };
|
|
3453
4381
|
}
|
|
3454
4382
|
const machineId = getMachineId();
|
|
3455
|
-
const
|
|
4383
|
+
const now2 = new Date().toISOString();
|
|
3456
4384
|
let requests = 0;
|
|
3457
4385
|
const account = await resolveAccountForAgent("hermes");
|
|
3458
4386
|
for (const row of rows) {
|
|
@@ -3472,7 +4400,7 @@ async function ingestHermes(db, verbose = false) {
|
|
|
3472
4400
|
total_tokens: tokens,
|
|
3473
4401
|
request_count: 1,
|
|
3474
4402
|
machine_id: machineId,
|
|
3475
|
-
updated_at:
|
|
4403
|
+
updated_at: now2
|
|
3476
4404
|
}, account));
|
|
3477
4405
|
const reqId = `hermes-${row.id}-rollup`;
|
|
3478
4406
|
upsertRequest(db, withAccount({
|
|
@@ -3490,7 +4418,7 @@ async function ingestHermes(db, verbose = false) {
|
|
|
3490
4418
|
timestamp: endedAt ?? startedAt,
|
|
3491
4419
|
source_request_id: row.id,
|
|
3492
4420
|
machine_id: machineId,
|
|
3493
|
-
updated_at:
|
|
4421
|
+
updated_at: now2
|
|
3494
4422
|
}, account));
|
|
3495
4423
|
requests++;
|
|
3496
4424
|
rollupSession(db, sessionId);
|
|
@@ -3512,19 +4440,78 @@ function statSyncSafe(path) {
|
|
|
3512
4440
|
// src/ingest/loops.ts
|
|
3513
4441
|
init_database();
|
|
3514
4442
|
init_pricing();
|
|
3515
|
-
import { existsSync as
|
|
3516
|
-
import { homedir as
|
|
3517
|
-
import { join as
|
|
4443
|
+
import { existsSync as existsSync10 } from "fs";
|
|
4444
|
+
import { homedir as homedir9 } from "os";
|
|
4445
|
+
import { join as join10 } from "path";
|
|
3518
4446
|
import { Database as BunDatabase2 } from "bun:sqlite";
|
|
3519
|
-
import {
|
|
4447
|
+
import { homedir as pathsResolverHomedir2 } from "os";
|
|
4448
|
+
import { join as pathsResolverJoin2 } from "path";
|
|
4449
|
+
var PATHS_RESOLVER_KIND_ENV2 = {
|
|
4450
|
+
config: "HASNA_CONFIG_HOME",
|
|
4451
|
+
data: "HASNA_DATA_HOME",
|
|
4452
|
+
state: "HASNA_STATE_HOME",
|
|
4453
|
+
cache: "HASNA_CACHE_HOME"
|
|
4454
|
+
};
|
|
4455
|
+
var PATHS_RESOLVER_APP_SLUG_RE2 = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
4456
|
+
function pathsResolverAssertApp2(app) {
|
|
4457
|
+
if (typeof app !== "string" || app.length === 0) {
|
|
4458
|
+
throw new TypeError("paths: app must be a non-empty string");
|
|
4459
|
+
}
|
|
4460
|
+
if (!PATHS_RESOLVER_APP_SLUG_RE2.test(app)) {
|
|
4461
|
+
throw new TypeError(`paths: invalid app slug "${app}" \u2014 expected lowercase kebab-case ([a-z0-9]+(-[a-z0-9]+)*)`);
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
function pathsResolverAssertKind2(kind) {
|
|
4465
|
+
if (!Object.keys(PATHS_RESOLVER_KIND_ENV2).includes(kind)) {
|
|
4466
|
+
throw new TypeError(`paths: invalid path kind "${kind}" \u2014 expected one of ${Object.keys(PATHS_RESOLVER_KIND_ENV2).join(", ")}`);
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
function pathsResolverBaseDir2(kind, options) {
|
|
4470
|
+
pathsResolverAssertKind2(kind);
|
|
4471
|
+
const env = options.env ?? process.env;
|
|
4472
|
+
const override = env[PATHS_RESOLVER_KIND_ENV2[kind]];
|
|
4473
|
+
if (typeof override === "string" && override.length > 0)
|
|
4474
|
+
return override;
|
|
4475
|
+
const home = options.home ?? pathsResolverHomedir2();
|
|
4476
|
+
const platform2 = options.platform ?? process.platform;
|
|
4477
|
+
if (platform2 === "darwin") {
|
|
4478
|
+
switch (kind) {
|
|
4479
|
+
case "config":
|
|
4480
|
+
case "data":
|
|
4481
|
+
return pathsResolverJoin2(home, "Library", "Application Support", "Hasna");
|
|
4482
|
+
case "cache":
|
|
4483
|
+
return pathsResolverJoin2(home, "Library", "Caches", "Hasna");
|
|
4484
|
+
case "state":
|
|
4485
|
+
return pathsResolverJoin2(home, "Library", "Logs", "Hasna");
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
switch (kind) {
|
|
4489
|
+
case "config":
|
|
4490
|
+
return pathsResolverJoin2(home, ".config", "hasna");
|
|
4491
|
+
case "data":
|
|
4492
|
+
return pathsResolverJoin2(home, ".local", "share", "hasna");
|
|
4493
|
+
case "state":
|
|
4494
|
+
return pathsResolverJoin2(home, ".local", "state", "hasna");
|
|
4495
|
+
case "cache":
|
|
4496
|
+
return pathsResolverJoin2(home, ".cache", "hasna");
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
function pathsResolverResolve2(kind, options) {
|
|
4500
|
+
pathsResolverAssertApp2(options.app);
|
|
4501
|
+
const appSegment = options.internal === true ? pathsResolverJoin2("internal", options.app) : options.app;
|
|
4502
|
+
return pathsResolverJoin2(pathsResolverBaseDir2(kind, options), appSegment);
|
|
4503
|
+
}
|
|
4504
|
+
function dataDir2(options) {
|
|
4505
|
+
return pathsResolverResolve2("data", options);
|
|
4506
|
+
}
|
|
3520
4507
|
var LOOPS_INGEST_VERSION = "goal-runs-orchestration-v1";
|
|
3521
4508
|
function defaultLoopsDbPath() {
|
|
3522
4509
|
const resolved = dataDir2({
|
|
3523
4510
|
app: "loops",
|
|
3524
4511
|
home: process.env["HOME"] || process.env["USERPROFILE"] || undefined
|
|
3525
4512
|
});
|
|
3526
|
-
const legacy =
|
|
3527
|
-
return
|
|
4513
|
+
const legacy = join10(process.env["HOME"] || process.env["USERPROFILE"] || homedir9(), ".hasna", "loops", "loops.db");
|
|
4514
|
+
return existsSync10(join10(resolved, "loops.db")) ? join10(resolved, "loops.db") : legacy;
|
|
3528
4515
|
}
|
|
3529
4516
|
function loopsDbPath() {
|
|
3530
4517
|
return process.env["HASNA_ECONOMY_LOOPS_DB_PATH"] ?? defaultLoopsDbPath();
|
|
@@ -3616,7 +4603,7 @@ function readLoopGoalRuns(loopsDb) {
|
|
|
3616
4603
|
}
|
|
3617
4604
|
async function ingestLoops(db, verbose = false) {
|
|
3618
4605
|
const dbPath = loopsDbPath();
|
|
3619
|
-
if (!
|
|
4606
|
+
if (!existsSync10(dbPath)) {
|
|
3620
4607
|
if (verbose)
|
|
3621
4608
|
console.log("OpenLoops DB not found:", dbPath);
|
|
3622
4609
|
return { sessions: 0, requests: 0 };
|
|
@@ -3706,30 +4693,30 @@ async function ingestLoops(db, verbose = false) {
|
|
|
3706
4693
|
|
|
3707
4694
|
// src/ingest/claude-quota.ts
|
|
3708
4695
|
init_database();
|
|
3709
|
-
import { existsSync as
|
|
4696
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7 } from "fs";
|
|
3710
4697
|
|
|
3711
4698
|
// src/lib/paths.ts
|
|
3712
|
-
import { homedir as
|
|
3713
|
-
import { join as
|
|
4699
|
+
import { homedir as homedir10 } from "os";
|
|
4700
|
+
import { join as join11 } from "path";
|
|
3714
4701
|
function getHomeDir2() {
|
|
3715
|
-
return process.env["USERPROFILE"] ?? process.env["HOME"] ??
|
|
4702
|
+
return process.env["USERPROFILE"] ?? process.env["HOME"] ?? homedir10();
|
|
3716
4703
|
}
|
|
3717
4704
|
function agentPaths() {
|
|
3718
4705
|
const home = getHomeDir2();
|
|
3719
4706
|
return {
|
|
3720
|
-
claudeProjects:
|
|
3721
|
-
claudeCredentials:
|
|
3722
|
-
takumiProjects:
|
|
3723
|
-
codexDir:
|
|
3724
|
-
codexDb:
|
|
3725
|
-
codexAuth:
|
|
3726
|
-
codexConfig:
|
|
3727
|
-
geminiTmp:
|
|
3728
|
-
geminiHistory:
|
|
3729
|
-
opencodeMessages:
|
|
3730
|
-
piSessions:
|
|
3731
|
-
hermesDir:
|
|
3732
|
-
hermesDb:
|
|
4707
|
+
claudeProjects: join11(home, ".claude", "projects"),
|
|
4708
|
+
claudeCredentials: join11(home, ".claude", ".credentials.json"),
|
|
4709
|
+
takumiProjects: join11(home, ".takumi", "projects"),
|
|
4710
|
+
codexDir: join11(home, ".codex"),
|
|
4711
|
+
codexDb: join11(home, ".codex", "state_5.sqlite"),
|
|
4712
|
+
codexAuth: join11(home, ".codex", "auth.json"),
|
|
4713
|
+
codexConfig: join11(home, ".codex", "config.toml"),
|
|
4714
|
+
geminiTmp: join11(home, ".gemini", "tmp"),
|
|
4715
|
+
geminiHistory: join11(home, ".gemini", "history"),
|
|
4716
|
+
opencodeMessages: join11(home, ".local", "share", "opencode", "storage", "message"),
|
|
4717
|
+
piSessions: join11(home, ".pi", "agent", "sessions"),
|
|
4718
|
+
hermesDir: join11(home, ".hermes"),
|
|
4719
|
+
hermesDb: join11(home, ".hermes", "state.db")
|
|
3733
4720
|
};
|
|
3734
4721
|
}
|
|
3735
4722
|
|
|
@@ -3741,10 +4728,10 @@ function readClaudeToken() {
|
|
|
3741
4728
|
const fromEnv = process.env["CLAUDE_OAUTH_TOKEN"] ?? process.env["ANTHROPIC_OAUTH_TOKEN"];
|
|
3742
4729
|
if (fromEnv)
|
|
3743
4730
|
return { token: fromEnv };
|
|
3744
|
-
if (!
|
|
4731
|
+
if (!existsSync11(CREDENTIALS_PATH))
|
|
3745
4732
|
return null;
|
|
3746
4733
|
try {
|
|
3747
|
-
const creds = JSON.parse(
|
|
4734
|
+
const creds = JSON.parse(readFileSync7(CREDENTIALS_PATH, "utf-8"));
|
|
3748
4735
|
const oauth = creds.claudeAiOauth;
|
|
3749
4736
|
if (!oauth?.accessToken)
|
|
3750
4737
|
return null;
|
|
@@ -3858,7 +4845,7 @@ async function ingestClaudeQuota(db, verbose = false) {
|
|
|
3858
4845
|
snapshots++;
|
|
3859
4846
|
}
|
|
3860
4847
|
const monthlyFee = inferMonthlyFee(auth.subscriptionType, auth.rateLimitTier);
|
|
3861
|
-
const
|
|
4848
|
+
const now2 = new Date().toISOString();
|
|
3862
4849
|
upsertSubscription(db, {
|
|
3863
4850
|
id: "anthropic-claude-oauth",
|
|
3864
4851
|
provider: "anthropic",
|
|
@@ -3869,8 +4856,8 @@ async function ingestClaudeQuota(db, verbose = false) {
|
|
|
3869
4856
|
billing_cycle_start: null,
|
|
3870
4857
|
reset_policy: "monthly",
|
|
3871
4858
|
active: 1,
|
|
3872
|
-
created_at:
|
|
3873
|
-
updated_at:
|
|
4859
|
+
created_at: now2,
|
|
4860
|
+
updated_at: now2
|
|
3874
4861
|
});
|
|
3875
4862
|
setIngestState(db, "claude", `quota-${today}`, new Date().toISOString());
|
|
3876
4863
|
if (verbose)
|
|
@@ -3880,17 +4867,17 @@ async function ingestClaudeQuota(db, verbose = false) {
|
|
|
3880
4867
|
|
|
3881
4868
|
// src/ingest/codex-quota.ts
|
|
3882
4869
|
init_database();
|
|
3883
|
-
import { existsSync as
|
|
4870
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
3884
4871
|
var WHAM_USAGE_URL = process.env["CODEX_USAGE_URL"] ?? "https://chatgpt.com/backend-api/wham/usage";
|
|
3885
4872
|
function readCodexAuth() {
|
|
3886
4873
|
const fromEnv = process.env["CODEX_OAUTH_TOKEN"];
|
|
3887
4874
|
if (fromEnv)
|
|
3888
4875
|
return { token: fromEnv, authMode: "chatgpt" };
|
|
3889
4876
|
const authPath = agentPaths().codexAuth;
|
|
3890
|
-
if (!
|
|
4877
|
+
if (!existsSync12(authPath))
|
|
3891
4878
|
return null;
|
|
3892
4879
|
try {
|
|
3893
|
-
const auth = JSON.parse(
|
|
4880
|
+
const auth = JSON.parse(readFileSync8(authPath, "utf-8"));
|
|
3894
4881
|
const token = auth.tokens?.access_token;
|
|
3895
4882
|
if (!token)
|
|
3896
4883
|
return null;
|
|
@@ -3957,7 +4944,7 @@ async function ingestCodexQuota(db, verbose = false) {
|
|
|
3957
4944
|
}
|
|
3958
4945
|
const machineId = getMachineId();
|
|
3959
4946
|
let snapshots = 0;
|
|
3960
|
-
const
|
|
4947
|
+
const now2 = new Date().toISOString();
|
|
3961
4948
|
const windows = [
|
|
3962
4949
|
["five_hour_utilization", usage.rate_limit?.primary_window],
|
|
3963
4950
|
["seven_day_utilization", usage.rate_limit?.secondary_window]
|
|
@@ -4008,10 +4995,10 @@ async function ingestCodexQuota(db, verbose = false) {
|
|
|
4008
4995
|
billing_cycle_start: null,
|
|
4009
4996
|
reset_policy: "monthly",
|
|
4010
4997
|
active: 1,
|
|
4011
|
-
created_at:
|
|
4012
|
-
updated_at:
|
|
4998
|
+
created_at: now2,
|
|
4999
|
+
updated_at: now2
|
|
4013
5000
|
});
|
|
4014
|
-
setIngestState(db, "codex", `quota-${today}`,
|
|
5001
|
+
setIngestState(db, "codex", `quota-${today}`, now2);
|
|
4015
5002
|
if (verbose)
|
|
4016
5003
|
console.log(` codex quota: ${snapshots} snapshots (${usage.plan_type ?? "unknown plan"})`);
|
|
4017
5004
|
return { snapshots };
|
|
@@ -4053,7 +5040,8 @@ async function syncAll(db, opts = {}) {
|
|
|
4053
5040
|
// src/lib/cloud-ingest.ts
|
|
4054
5041
|
init_database();
|
|
4055
5042
|
init_pricing();
|
|
4056
|
-
import {
|
|
5043
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, writeFileSync } from "fs";
|
|
5044
|
+
import { dirname as dirname2, join as join12 } from "path";
|
|
4057
5045
|
|
|
4058
5046
|
// src/ingest/billing.ts
|
|
4059
5047
|
init_database();
|
|
@@ -4097,15 +5085,39 @@ var CLOUD_INGEST_TABLES = ["requests", "sessions", "billing_daily", "usage_snaps
|
|
|
4097
5085
|
function getIngestCachePath() {
|
|
4098
5086
|
if (process.env["HASNA_ECONOMY_INGEST_CACHE"])
|
|
4099
5087
|
return process.env["HASNA_ECONOMY_INGEST_CACHE"];
|
|
4100
|
-
|
|
5088
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || undefined;
|
|
5089
|
+
return join12(cacheDir({ app: "economy", home }), "ingest-cache.json");
|
|
5090
|
+
}
|
|
5091
|
+
function readIngestCache(path) {
|
|
5092
|
+
let text;
|
|
5093
|
+
try {
|
|
5094
|
+
text = readFileSync9(path, "utf8");
|
|
5095
|
+
} catch {
|
|
5096
|
+
return [];
|
|
5097
|
+
}
|
|
5098
|
+
try {
|
|
5099
|
+
const parsed = JSON.parse(text);
|
|
5100
|
+
const rows = Array.isArray(parsed?.ingest_state) ? parsed.ingest_state : [];
|
|
5101
|
+
return rows.filter((row) => typeof row === "object" && row !== null && typeof row.source === "string" && typeof row.key === "string" && typeof row.value === "string");
|
|
5102
|
+
} catch {
|
|
5103
|
+
return [];
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
function writeIngestCache(path, rows) {
|
|
5107
|
+
mkdirSync3(dirname2(path), { recursive: true });
|
|
5108
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
5109
|
+
writeFileSync(tmp, JSON.stringify({ version: 1, ingest_state: rows }), { mode: 384 });
|
|
5110
|
+
renameSync(tmp, path);
|
|
4101
5111
|
}
|
|
4102
|
-
function
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
const insert = to.prepare(`INSERT INTO ingest_state (source, key, value) VALUES (?, ?, ?)`);
|
|
5112
|
+
function loadIngestState2(scratch, rows) {
|
|
5113
|
+
scratch.prepare(`DELETE FROM ingest_state`).run();
|
|
5114
|
+
const insert = scratch.prepare(`INSERT INTO ingest_state (source, key, value) VALUES (?, ?, ?)`);
|
|
4106
5115
|
for (const row of rows)
|
|
4107
5116
|
insert.run(row.source, row.key, row.value);
|
|
4108
5117
|
}
|
|
5118
|
+
function dumpIngestState(scratch) {
|
|
5119
|
+
return scratch.prepare(`SELECT source, key, value FROM ingest_state`).all();
|
|
5120
|
+
}
|
|
4109
5121
|
function exportIngestRows(db) {
|
|
4110
5122
|
const body = {};
|
|
4111
5123
|
for (const table of CLOUD_INGEST_TABLES) {
|
|
@@ -4133,11 +5145,9 @@ async function postIngest(cloud, body) {
|
|
|
4133
5145
|
async function syncAllToCloud(cloud, opts = {}) {
|
|
4134
5146
|
const scratch = openDatabase(":memory:", true);
|
|
4135
5147
|
ensurePricingSeeded(scratch);
|
|
4136
|
-
const
|
|
5148
|
+
const cachePath = opts.cachePath ?? getIngestCachePath();
|
|
4137
5149
|
try {
|
|
4138
|
-
|
|
4139
|
-
cache.prepare(`DELETE FROM ingest_state`).run();
|
|
4140
|
-
copyIngestState(cache, scratch);
|
|
5150
|
+
loadIngestState2(scratch, opts.force ? [] : readIngestCache(cachePath));
|
|
4141
5151
|
const result = await syncAll(scratch, opts);
|
|
4142
5152
|
if (opts.backfillMachine)
|
|
4143
5153
|
backfillMachineId(scratch);
|
|
@@ -4145,61 +5155,19 @@ async function syncAllToCloud(cloud, opts = {}) {
|
|
|
4145
5155
|
await recalculateZeroCostRequests(scratch);
|
|
4146
5156
|
const body = exportIngestRows(scratch);
|
|
4147
5157
|
if (Object.keys(body).length === 0) {
|
|
4148
|
-
|
|
5158
|
+
writeIngestCache(cachePath, dumpIngestState(scratch));
|
|
4149
5159
|
return { ...result, posted: false };
|
|
4150
5160
|
}
|
|
4151
5161
|
const response = await postIngest(cloud, body);
|
|
4152
|
-
|
|
5162
|
+
writeIngestCache(cachePath, dumpIngestState(scratch));
|
|
4153
5163
|
return { ...result, posted: true, ingested: response.ingested, total: response.total };
|
|
4154
5164
|
} finally {
|
|
4155
5165
|
try {
|
|
4156
5166
|
scratch.close();
|
|
4157
5167
|
} catch {}
|
|
4158
|
-
try {
|
|
4159
|
-
cache.close();
|
|
4160
|
-
} catch {}
|
|
4161
5168
|
}
|
|
4162
5169
|
}
|
|
4163
5170
|
|
|
4164
|
-
// src/lib/contracts-client/storage.ts
|
|
4165
|
-
import { resolveStorageClient } from "@hasna/contracts/client/storage";
|
|
4166
|
-
|
|
4167
|
-
// src/lib/cloud-storage.ts
|
|
4168
|
-
var ECONOMY_APP = "economy";
|
|
4169
|
-
var cache;
|
|
4170
|
-
function resolveEconomyCloudStorage(env = process.env, overrides) {
|
|
4171
|
-
const resolved = resolveStorageClient(ECONOMY_APP, env, overrides);
|
|
4172
|
-
return resolved.transport === "http" ? { active: true, client: resolved.client } : { active: false, client: null };
|
|
4173
|
-
}
|
|
4174
|
-
function economyCloudStorage(env = process.env) {
|
|
4175
|
-
if (cache === undefined)
|
|
4176
|
-
cache = resolveEconomyCloudStorage(env);
|
|
4177
|
-
return cache;
|
|
4178
|
-
}
|
|
4179
|
-
function cleanQuery(query) {
|
|
4180
|
-
if (!query)
|
|
4181
|
-
return;
|
|
4182
|
-
const out = {};
|
|
4183
|
-
for (const [k, v] of Object.entries(query)) {
|
|
4184
|
-
if (v !== undefined && v !== null && v !== "")
|
|
4185
|
-
out[k] = v;
|
|
4186
|
-
}
|
|
4187
|
-
return Object.keys(out).length > 0 ? out : undefined;
|
|
4188
|
-
}
|
|
4189
|
-
async function cloudListItems(storage, resource, query) {
|
|
4190
|
-
const cleaned = cleanQuery(query);
|
|
4191
|
-
const res = await storage.client.list(resource, cleaned ? { query: cleaned } : {});
|
|
4192
|
-
return res.items;
|
|
4193
|
-
}
|
|
4194
|
-
async function cloudObject(storage, path, query) {
|
|
4195
|
-
const cleaned = cleanQuery(query);
|
|
4196
|
-
const raw = await storage.client.transport.get(path, cleaned ? { query: cleaned } : {});
|
|
4197
|
-
if (raw && typeof raw === "object" && "data" in raw) {
|
|
4198
|
-
return raw.data;
|
|
4199
|
-
}
|
|
4200
|
-
return raw;
|
|
4201
|
-
}
|
|
4202
|
-
|
|
4203
5171
|
// src/lib/agents.ts
|
|
4204
5172
|
var AGENTS = [
|
|
4205
5173
|
"claude",
|
|
@@ -4297,24 +5265,24 @@ function ymd(date) {
|
|
|
4297
5265
|
return date.toISOString().substring(0, 10);
|
|
4298
5266
|
}
|
|
4299
5267
|
function usageSnapshotFilterForPeriod(period) {
|
|
4300
|
-
const
|
|
5268
|
+
const now2 = new Date;
|
|
4301
5269
|
switch (period) {
|
|
4302
5270
|
case "today":
|
|
4303
|
-
return { date: ymd(
|
|
5271
|
+
return { date: ymd(now2) };
|
|
4304
5272
|
case "yesterday": {
|
|
4305
|
-
const yesterday = new Date(
|
|
5273
|
+
const yesterday = new Date(now2);
|
|
4306
5274
|
yesterday.setUTCDate(yesterday.getUTCDate() - 1);
|
|
4307
5275
|
return { date: ymd(yesterday) };
|
|
4308
5276
|
}
|
|
4309
5277
|
case "week": {
|
|
4310
|
-
const weekAgo = new Date(
|
|
5278
|
+
const weekAgo = new Date(now2);
|
|
4311
5279
|
weekAgo.setUTCDate(weekAgo.getUTCDate() - 7);
|
|
4312
5280
|
return { since: ymd(weekAgo) };
|
|
4313
5281
|
}
|
|
4314
5282
|
case "month":
|
|
4315
|
-
return { since: ymd(new Date(Date.UTC(
|
|
5283
|
+
return { since: ymd(new Date(Date.UTC(now2.getUTCFullYear(), now2.getUTCMonth(), 1))) };
|
|
4316
5284
|
case "year":
|
|
4317
|
-
return { since: ymd(new Date(Date.UTC(
|
|
5285
|
+
return { since: ymd(new Date(Date.UTC(now2.getUTCFullYear(), 0, 1))) };
|
|
4318
5286
|
case "all":
|
|
4319
5287
|
return {};
|
|
4320
5288
|
}
|
|
@@ -4343,13 +5311,13 @@ var ZERO_SESSION_TOTALS = {
|
|
|
4343
5311
|
cost_usd: 0,
|
|
4344
5312
|
last_active: null
|
|
4345
5313
|
};
|
|
4346
|
-
function formatAge(timestamp,
|
|
5314
|
+
function formatAge(timestamp, now2) {
|
|
4347
5315
|
if (!timestamp)
|
|
4348
5316
|
return "never";
|
|
4349
5317
|
const t = new Date(timestamp).getTime();
|
|
4350
5318
|
if (!Number.isFinite(t))
|
|
4351
5319
|
return "unknown";
|
|
4352
|
-
const ageMs = Math.max(0,
|
|
5320
|
+
const ageMs = Math.max(0, now2.getTime() - t);
|
|
4353
5321
|
const minutes = Math.floor(ageMs / 60000);
|
|
4354
5322
|
if (minutes < 1)
|
|
4355
5323
|
return "<1m";
|
|
@@ -4498,7 +5466,7 @@ function querySinceTotals(db, since, machine) {
|
|
|
4498
5466
|
const sessions = querySessionOnlyTotals(db, "started_at >= ?", [since], machine);
|
|
4499
5467
|
return combineTotals(requests, sessions);
|
|
4500
5468
|
}
|
|
4501
|
-
function parseSinceDate(input,
|
|
5469
|
+
function parseSinceDate(input, now2) {
|
|
4502
5470
|
const value = input.trim();
|
|
4503
5471
|
const match = value.match(/^(\d+(?:\.\d+)?)(m|h|d|w)$/i);
|
|
4504
5472
|
if (match) {
|
|
@@ -4506,7 +5474,7 @@ function parseSinceDate(input, now) {
|
|
|
4506
5474
|
const unit = match[2].toLowerCase();
|
|
4507
5475
|
const multiplier = unit === "m" ? 60000 : unit === "h" ? 60 * 60000 : unit === "d" ? 24 * 60 * 60000 : 7 * 24 * 60 * 60000;
|
|
4508
5476
|
return {
|
|
4509
|
-
timestamp: new Date(
|
|
5477
|
+
timestamp: new Date(now2.getTime() - amount * multiplier).toISOString(),
|
|
4510
5478
|
label: value
|
|
4511
5479
|
};
|
|
4512
5480
|
}
|
|
@@ -4847,7 +5815,7 @@ function accountBreakdownToBriefRow(row) {
|
|
|
4847
5815
|
last_active: row.last_active
|
|
4848
5816
|
};
|
|
4849
5817
|
}
|
|
4850
|
-
function queryFreshness(db,
|
|
5818
|
+
function queryFreshness(db, now2, machine, localSync) {
|
|
4851
5819
|
const requestFilter = machine ? " AND machine_id = ?" : "";
|
|
4852
5820
|
const requestRows = db.prepare(`
|
|
4853
5821
|
SELECT machine_id, MAX(timestamp) as max_request_at
|
|
@@ -4861,7 +5829,7 @@ function queryFreshness(db, now, machine, localSync) {
|
|
|
4861
5829
|
machines.set(row.machine_id, {
|
|
4862
5830
|
machine_id: row.machine_id,
|
|
4863
5831
|
max_request_at: row.max_request_at,
|
|
4864
|
-
request_age: formatAge(row.max_request_at,
|
|
5832
|
+
request_age: formatAge(row.max_request_at, now2),
|
|
4865
5833
|
last_merge_sync_at: null,
|
|
4866
5834
|
merge_sync_age: "never"
|
|
4867
5835
|
});
|
|
@@ -4876,7 +5844,7 @@ function queryFreshness(db, now, machine, localSync) {
|
|
|
4876
5844
|
};
|
|
4877
5845
|
const syncAt = latestTimestamp(row.last_pull_at, row.last_push_at, row.updated_at);
|
|
4878
5846
|
current.last_merge_sync_at = syncAt;
|
|
4879
|
-
current.merge_sync_age = formatAge(syncAt,
|
|
5847
|
+
current.merge_sync_age = formatAge(syncAt, now2);
|
|
4880
5848
|
machines.set(row.machine_id, current);
|
|
4881
5849
|
}
|
|
4882
5850
|
if (localSync && (!machine || machine === localSync.machineId)) {
|
|
@@ -4888,7 +5856,7 @@ function queryFreshness(db, now, machine, localSync) {
|
|
|
4888
5856
|
merge_sync_age: "never"
|
|
4889
5857
|
};
|
|
4890
5858
|
current.last_merge_sync_at = latestTimestamp(current.last_merge_sync_at, localSync.syncedAt);
|
|
4891
|
-
current.merge_sync_age = formatAge(current.last_merge_sync_at,
|
|
5859
|
+
current.merge_sync_age = formatAge(current.last_merge_sync_at, now2);
|
|
4892
5860
|
machines.set(localSync.machineId, current);
|
|
4893
5861
|
}
|
|
4894
5862
|
const rows = [...machines.values()].sort((a, b) => a.machine_id.localeCompare(b.machine_id));
|
|
@@ -4899,10 +5867,10 @@ function queryFreshness(db, now, machine, localSync) {
|
|
|
4899
5867
|
};
|
|
4900
5868
|
}
|
|
4901
5869
|
function buildBrief(db, opts = {}) {
|
|
4902
|
-
const
|
|
5870
|
+
const now2 = opts.now ?? new Date;
|
|
4903
5871
|
const machine = machineFilter(opts.machine);
|
|
4904
5872
|
const sinceInput = opts.since ?? "24h";
|
|
4905
|
-
const since = parseSinceDate(sinceInput,
|
|
5873
|
+
const since = parseSinceDate(sinceInput, now2);
|
|
4906
5874
|
const summaries = [
|
|
4907
5875
|
{
|
|
4908
5876
|
label: "Today",
|
|
@@ -4923,10 +5891,10 @@ function buildBrief(db, opts = {}) {
|
|
|
4923
5891
|
];
|
|
4924
5892
|
const machines = queryMachineSinceRows(db, since.timestamp, machine);
|
|
4925
5893
|
for (const row of machines) {
|
|
4926
|
-
row.last_data_age = formatAge(row.last_data_at,
|
|
5894
|
+
row.last_data_age = formatAge(row.last_data_at, now2);
|
|
4927
5895
|
}
|
|
4928
5896
|
return {
|
|
4929
|
-
generated_at:
|
|
5897
|
+
generated_at: now2.toISOString(),
|
|
4930
5898
|
machine: machine ?? "all",
|
|
4931
5899
|
since: {
|
|
4932
5900
|
input: sinceInput,
|
|
@@ -4937,7 +5905,7 @@ function buildBrief(db, opts = {}) {
|
|
|
4937
5905
|
machines,
|
|
4938
5906
|
agents: queryAgentSinceRows(db, since.timestamp, machine),
|
|
4939
5907
|
accounts: queryAccountSinceRows(db, since.timestamp, machine),
|
|
4940
|
-
freshness: queryFreshness(db,
|
|
5908
|
+
freshness: queryFreshness(db, now2, machine, opts.currentMachineId && opts.localSyncAt ? { machineId: opts.currentMachineId, syncedAt: opts.localSyncAt.toISOString() } : undefined)
|
|
4941
5909
|
};
|
|
4942
5910
|
}
|
|
4943
5911
|
|
|
@@ -5001,14 +5969,14 @@ function queryRangeStats(db, from, to) {
|
|
|
5001
5969
|
const s = db.prepare(`SELECT COUNT(*) as sessions FROM sessions WHERE DATE(started_at) BETWEEN ? AND ?`).get(from, to);
|
|
5002
5970
|
return { cost: r.cost, requests: r.requests, tokens: r.tokens, sessions: s.sessions };
|
|
5003
5971
|
}
|
|
5004
|
-
function queryForecast(db,
|
|
5005
|
-
const daysInMonth = new Date(
|
|
5006
|
-
const dayOfMonth =
|
|
5007
|
-
const monthStart = `${
|
|
5972
|
+
function queryForecast(db, now2 = new Date) {
|
|
5973
|
+
const daysInMonth = new Date(now2.getFullYear(), now2.getMonth() + 1, 0).getDate();
|
|
5974
|
+
const dayOfMonth = now2.getDate();
|
|
5975
|
+
const monthStart = `${now2.getFullYear()}-${String(now2.getMonth() + 1).padStart(2, "0")}-01`;
|
|
5008
5976
|
const monthSoFar = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(monthStart);
|
|
5009
5977
|
const dailyAvg = dayOfMonth > 0 ? monthSoFar.cost / dayOfMonth : 0;
|
|
5010
5978
|
const projected = dailyAvg * daysInMonth;
|
|
5011
|
-
const sevenDaysAgo = new Date(
|
|
5979
|
+
const sevenDaysAgo = new Date(now2);
|
|
5012
5980
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
5013
5981
|
const last7 = db.prepare(`SELECT COALESCE(SUM(cost_usd),0) as cost FROM requests WHERE DATE(timestamp) >= ?`).get(sevenDaysAgo.toISOString().substring(0, 10));
|
|
5014
5982
|
const last7DailyAvg = last7.cost / 7;
|
|
@@ -5038,7 +6006,7 @@ function queryModelEfficiency(db) {
|
|
|
5038
6006
|
}
|
|
5039
6007
|
|
|
5040
6008
|
// src/lib/store/index.ts
|
|
5041
|
-
class
|
|
6009
|
+
class LocalStore2 {
|
|
5042
6010
|
transport = "local";
|
|
5043
6011
|
_db;
|
|
5044
6012
|
db() {
|
|
@@ -5154,8 +6122,8 @@ class LocalStore {
|
|
|
5154
6122
|
return estimateCostFromRows(listModelPricing(this.db()), input.model, input.inputTokens, input.outputTokens, input.cacheReadTokens ?? 0, input.cacheWriteTokens ?? 0, input.cacheWrite1hTokens ?? 0, input.cacheStorageTokenHours ?? 0);
|
|
5155
6123
|
}
|
|
5156
6124
|
async setBudget(input) {
|
|
5157
|
-
const
|
|
5158
|
-
const budget = { id: randomUUID(), ...input, created_at:
|
|
6125
|
+
const now2 = new Date().toISOString();
|
|
6126
|
+
const budget = { id: randomUUID(), ...input, created_at: now2, updated_at: now2 };
|
|
5159
6127
|
upsertBudget(this.db(), budget);
|
|
5160
6128
|
return budget.id;
|
|
5161
6129
|
}
|
|
@@ -5163,8 +6131,8 @@ class LocalStore {
|
|
|
5163
6131
|
deleteBudget(this.db(), id);
|
|
5164
6132
|
}
|
|
5165
6133
|
async setGoal(input) {
|
|
5166
|
-
const
|
|
5167
|
-
upsertGoal(this.db(), { id: randomUUID(), ...input, created_at:
|
|
6134
|
+
const now2 = new Date().toISOString();
|
|
6135
|
+
upsertGoal(this.db(), { id: randomUUID(), ...input, created_at: now2, updated_at: now2 });
|
|
5168
6136
|
}
|
|
5169
6137
|
async removeGoal(id) {
|
|
5170
6138
|
deleteGoal(this.db(), id);
|
|
@@ -5176,7 +6144,7 @@ class LocalStore {
|
|
|
5176
6144
|
deleteModelPricing(this.db(), model);
|
|
5177
6145
|
}
|
|
5178
6146
|
async setSubscription(input) {
|
|
5179
|
-
const
|
|
6147
|
+
const now2 = new Date().toISOString();
|
|
5180
6148
|
const row = {
|
|
5181
6149
|
id: input.id ?? randomUUID(),
|
|
5182
6150
|
provider: input.provider,
|
|
@@ -5187,8 +6155,8 @@ class LocalStore {
|
|
|
5187
6155
|
billing_cycle_start: input.billing_cycle_start,
|
|
5188
6156
|
reset_policy: input.reset_policy,
|
|
5189
6157
|
active: input.active,
|
|
5190
|
-
created_at:
|
|
5191
|
-
updated_at:
|
|
6158
|
+
created_at: now2,
|
|
6159
|
+
updated_at: now2
|
|
5192
6160
|
};
|
|
5193
6161
|
upsertSubscription(this.db(), row);
|
|
5194
6162
|
return row;
|
|
@@ -5233,7 +6201,7 @@ function q(params) {
|
|
|
5233
6201
|
return out;
|
|
5234
6202
|
}
|
|
5235
6203
|
|
|
5236
|
-
class
|
|
6204
|
+
class ApiStore2 {
|
|
5237
6205
|
cloud;
|
|
5238
6206
|
transport = "cloud-http";
|
|
5239
6207
|
constructor(cloud) {
|
|
@@ -5377,7 +6345,7 @@ class ApiStore {
|
|
|
5377
6345
|
await this.cloud.client.delete("pricing", model);
|
|
5378
6346
|
}
|
|
5379
6347
|
async setSubscription(input) {
|
|
5380
|
-
const
|
|
6348
|
+
const now2 = new Date().toISOString();
|
|
5381
6349
|
const row = {
|
|
5382
6350
|
id: input.id ?? randomUUID(),
|
|
5383
6351
|
provider: input.provider,
|
|
@@ -5388,8 +6356,8 @@ class ApiStore {
|
|
|
5388
6356
|
billing_cycle_start: input.billing_cycle_start,
|
|
5389
6357
|
reset_policy: input.reset_policy,
|
|
5390
6358
|
active: input.active,
|
|
5391
|
-
created_at:
|
|
5392
|
-
updated_at:
|
|
6359
|
+
created_at: now2,
|
|
6360
|
+
updated_at: now2
|
|
5393
6361
|
};
|
|
5394
6362
|
await this.cloud.client.create("subscriptions", row);
|
|
5395
6363
|
return row;
|
|
@@ -5417,7 +6385,7 @@ class ApiStore {
|
|
|
5417
6385
|
}
|
|
5418
6386
|
function getStore(env = process.env) {
|
|
5419
6387
|
const cloud = economyCloudStorage(env);
|
|
5420
|
-
return cloud.active ? new
|
|
6388
|
+
return cloud.active ? new ApiStore2(cloud) : new LocalStore2;
|
|
5421
6389
|
}
|
|
5422
6390
|
function isCloudStore(env = process.env) {
|
|
5423
6391
|
return economyCloudStorage(env).active;
|
|
@@ -5565,17 +6533,17 @@ function buildServer() {
|
|
|
5565
6533
|
function textError(message) {
|
|
5566
6534
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
5567
6535
|
}
|
|
5568
|
-
server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query:
|
|
6536
|
+
server.tool("search_tools", "List tool names matching query. Use first to find relevant tools.", { query: z3.string().optional() }, async ({ query }) => {
|
|
5569
6537
|
const q2 = query?.toLowerCase();
|
|
5570
6538
|
const matches = q2 ? TOOL_NAMES.filter((name) => name.includes(q2)) : [...TOOL_NAMES];
|
|
5571
6539
|
return text(matches.join(", "));
|
|
5572
6540
|
});
|
|
5573
|
-
server.tool("describe_tools", "Get param hints for specific tools by name.", { names:
|
|
6541
|
+
server.tool("describe_tools", "Get param hints for specific tools by name.", { names: z3.array(z3.string()) }, async ({ names }) => {
|
|
5574
6542
|
const result = names.map((name) => `${name}: ${TOOL_DESCRIPTIONS[name] ?? "see tool schema"}`).join(`
|
|
5575
6543
|
`);
|
|
5576
6544
|
return text(result);
|
|
5577
6545
|
});
|
|
5578
|
-
server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period:
|
|
6546
|
+
server.tool("get_cost_summary", "Cost summary (total_usd, sessions, requests, tokens, human summary). period: today|week|month|year|all. machine: filter by hostname.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional(), machine: z3.string().optional() }, async ({ period, machine }) => {
|
|
5579
6547
|
const resolved = period ?? "today";
|
|
5580
6548
|
const s = await store.summary(resolved, machine);
|
|
5581
6549
|
const machineLabel = machine ? ` on ${machine}` : "";
|
|
@@ -5590,11 +6558,11 @@ function buildServer() {
|
|
|
5590
6558
|
`));
|
|
5591
6559
|
});
|
|
5592
6560
|
server.tool("get_sessions", "List sessions. Returns compact table. Params: agent, project, account, machine, limit(20)", {
|
|
5593
|
-
agent:
|
|
5594
|
-
project:
|
|
5595
|
-
account:
|
|
5596
|
-
machine:
|
|
5597
|
-
limit:
|
|
6561
|
+
agent: z3.enum(AGENTS).optional(),
|
|
6562
|
+
project: z3.string().optional(),
|
|
6563
|
+
account: z3.string().optional(),
|
|
6564
|
+
machine: z3.string().optional(),
|
|
6565
|
+
limit: z3.number().int().positive().max(100).optional()
|
|
5598
6566
|
}, async ({ agent, project, account, machine, limit }) => {
|
|
5599
6567
|
const sessions = await store.sessions({ agent, project, account, machine, limit: limit ?? 20 });
|
|
5600
6568
|
const lines = ["id agent cost tokens project"];
|
|
@@ -5604,8 +6572,8 @@ function buildServer() {
|
|
|
5604
6572
|
`));
|
|
5605
6573
|
});
|
|
5606
6574
|
server.tool("get_top_sessions", "Top sessions by cost. Params: n(10), agent", {
|
|
5607
|
-
n:
|
|
5608
|
-
agent:
|
|
6575
|
+
n: z3.number().int().positive().max(100).optional(),
|
|
6576
|
+
agent: z3.enum(AGENTS).optional()
|
|
5609
6577
|
}, async ({ n, agent }) => {
|
|
5610
6578
|
const sessions = await store.topSessions(n ?? 10, agent);
|
|
5611
6579
|
const lines = ["rank id agent cost tokens project"];
|
|
@@ -5613,7 +6581,7 @@ function buildServer() {
|
|
|
5613
6581
|
return text(lines.join(`
|
|
5614
6582
|
`));
|
|
5615
6583
|
});
|
|
5616
|
-
server.tool("get_model_breakdown", "Cost per model. Params: limit(20), verbose, json.", { limit:
|
|
6584
|
+
server.tool("get_model_breakdown", "Cost per model. Params: limit(20), verbose, json.", { limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5617
6585
|
const rows = await store.modelBreakdown();
|
|
5618
6586
|
if (json)
|
|
5619
6587
|
return text(JSON.stringify(rows, null, 2));
|
|
@@ -5628,7 +6596,7 @@ function buildServer() {
|
|
|
5628
6596
|
return text(lines.join(`
|
|
5629
6597
|
`));
|
|
5630
6598
|
});
|
|
5631
|
-
server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all), limit(20), verbose, json.", { period:
|
|
6599
|
+
server.tool("get_project_breakdown", "Cost per project. Params: period(today|week|month|year|all), limit(20), verbose, json.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional(), limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ period, limit, verbose, json }) => {
|
|
5632
6600
|
const rows = await store.projectBreakdown({ period: period ?? "all" });
|
|
5633
6601
|
if (json)
|
|
5634
6602
|
return text(JSON.stringify(rows, null, 2));
|
|
@@ -5644,7 +6612,7 @@ function buildServer() {
|
|
|
5644
6612
|
return text(lines.join(`
|
|
5645
6613
|
`));
|
|
5646
6614
|
});
|
|
5647
|
-
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:
|
|
6615
|
+
server.tool("get_agent_breakdown", "Cost per coding agent. Params: period(today|week|month|year|all). Shows API-equivalent, billable API, and subscription-included usage.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
5648
6616
|
const rows = await store.agentBreakdown({ period: period ?? "all" });
|
|
5649
6617
|
if (rows.length === 0)
|
|
5650
6618
|
return text("No agent usage yet.");
|
|
@@ -5655,7 +6623,7 @@ function buildServer() {
|
|
|
5655
6623
|
return text(lines.join(`
|
|
5656
6624
|
`));
|
|
5657
6625
|
});
|
|
5658
|
-
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:
|
|
6626
|
+
server.tool("get_account_breakdown", "Cost per account/profile. Params: period(today|week|month|year|all), limit(20), verbose, json. Shows API-equivalent, billable API, and subscription-included usage.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional(), limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ period, limit, verbose, json }) => {
|
|
5659
6627
|
const rows = await store.accountBreakdown({ period: period ?? "all" });
|
|
5660
6628
|
if (json)
|
|
5661
6629
|
return text(JSON.stringify(rows, null, 2));
|
|
@@ -5674,8 +6642,8 @@ function buildServer() {
|
|
|
5674
6642
|
`));
|
|
5675
6643
|
});
|
|
5676
6644
|
server.tool("get_cost_center_breakdown", "Cost per cost center. Params: period(today|week|month|year|all), kind(loop|app|repo|service|team).", {
|
|
5677
|
-
period:
|
|
5678
|
-
kind:
|
|
6645
|
+
period: z3.enum(["today", "week", "month", "year", "all"]).optional(),
|
|
6646
|
+
kind: z3.enum(["loop", "app", "repo", "service", "team"]).optional()
|
|
5679
6647
|
}, async ({ period, kind }) => {
|
|
5680
6648
|
const rows = await store.costCenterBreakdown({ period: period ?? "all", kind });
|
|
5681
6649
|
if (rows.length === 0)
|
|
@@ -5687,7 +6655,7 @@ function buildServer() {
|
|
|
5687
6655
|
return text(lines.join(`
|
|
5688
6656
|
`));
|
|
5689
6657
|
});
|
|
5690
|
-
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. Params: limit(20), verbose, json.", { limit:
|
|
6658
|
+
server.tool("get_budget_status", "Budget limits vs spend, percent used, alert flags. Params: limit(20), verbose, json.", { limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5691
6659
|
const budgets = await store.listBudgets();
|
|
5692
6660
|
if (json)
|
|
5693
6661
|
return text(JSON.stringify(budgets, null, 2));
|
|
@@ -5708,12 +6676,12 @@ function buildServer() {
|
|
|
5708
6676
|
`));
|
|
5709
6677
|
});
|
|
5710
6678
|
server.tool("set_budget", "Create a spending budget. period: daily|weekly|monthly. limit_usd must be positive. alert_at_percent defaults to 80.", {
|
|
5711
|
-
period:
|
|
5712
|
-
limit_usd:
|
|
5713
|
-
project_path:
|
|
5714
|
-
agent:
|
|
5715
|
-
cost_center_id:
|
|
5716
|
-
alert_at_percent:
|
|
6679
|
+
period: z3.enum(["daily", "weekly", "monthly"]),
|
|
6680
|
+
limit_usd: z3.number().positive(),
|
|
6681
|
+
project_path: z3.string().optional(),
|
|
6682
|
+
agent: z3.enum(AGENTS).optional(),
|
|
6683
|
+
cost_center_id: z3.string().optional(),
|
|
6684
|
+
alert_at_percent: z3.number().positive().max(100).optional()
|
|
5717
6685
|
}, async ({ period, limit_usd, project_path, agent, cost_center_id, alert_at_percent }) => {
|
|
5718
6686
|
const id = await store.setBudget({
|
|
5719
6687
|
project_path: project_path ?? null,
|
|
@@ -5725,11 +6693,11 @@ function buildServer() {
|
|
|
5725
6693
|
});
|
|
5726
6694
|
return text(`Budget set: ${id}`);
|
|
5727
6695
|
});
|
|
5728
|
-
server.tool("remove_budget", "Delete a budget by id.", { id:
|
|
6696
|
+
server.tool("remove_budget", "Delete a budget by id.", { id: z3.string() }, async ({ id }) => {
|
|
5729
6697
|
await store.removeBudget(id);
|
|
5730
6698
|
return text("Budget removed.");
|
|
5731
6699
|
});
|
|
5732
|
-
server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage. Params: limit(20), verbose, json.", { limit:
|
|
6700
|
+
server.tool("get_pricing", "Editable model pricing rows. Includes input/output/cache rates and context-cache storage. Params: limit(20), verbose, json.", { limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5733
6701
|
const rows = await store.listPricing();
|
|
5734
6702
|
if (json)
|
|
5735
6703
|
return text(JSON.stringify(rows, null, 2));
|
|
@@ -5745,13 +6713,13 @@ function buildServer() {
|
|
|
5745
6713
|
`));
|
|
5746
6714
|
});
|
|
5747
6715
|
server.tool("set_pricing", "Create or update a model pricing row. Values are USD per 1M tokens except cache_storage_per_1m_hour.", {
|
|
5748
|
-
model:
|
|
5749
|
-
input_per_1m:
|
|
5750
|
-
output_per_1m:
|
|
5751
|
-
cache_read_per_1m:
|
|
5752
|
-
cache_write_per_1m:
|
|
5753
|
-
cache_write_1h_per_1m:
|
|
5754
|
-
cache_storage_per_1m_hour:
|
|
6716
|
+
model: z3.string().min(1),
|
|
6717
|
+
input_per_1m: z3.number().nonnegative(),
|
|
6718
|
+
output_per_1m: z3.number().nonnegative(),
|
|
6719
|
+
cache_read_per_1m: z3.number().nonnegative().optional(),
|
|
6720
|
+
cache_write_per_1m: z3.number().nonnegative().optional(),
|
|
6721
|
+
cache_write_1h_per_1m: z3.number().nonnegative().optional(),
|
|
6722
|
+
cache_storage_per_1m_hour: z3.number().nonnegative().optional()
|
|
5755
6723
|
}, async (input) => {
|
|
5756
6724
|
const model = input.model.trim();
|
|
5757
6725
|
if (!model)
|
|
@@ -5767,11 +6735,11 @@ function buildServer() {
|
|
|
5767
6735
|
});
|
|
5768
6736
|
return text(`Pricing set: ${model}`);
|
|
5769
6737
|
});
|
|
5770
|
-
server.tool("remove_pricing", "Delete a model pricing row by model id.", { model:
|
|
6738
|
+
server.tool("remove_pricing", "Delete a model pricing row by model id.", { model: z3.string() }, async ({ model }) => {
|
|
5771
6739
|
await store.removePricing(model);
|
|
5772
6740
|
return text("Pricing removed.");
|
|
5773
6741
|
});
|
|
5774
|
-
server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days:
|
|
6742
|
+
server.tool("get_daily", "Daily cost table by agent. Params: days(30)", { days: z3.number().int().positive().max(365).optional() }, async ({ days }) => {
|
|
5775
6743
|
const rows = await store.daily(days ?? 30);
|
|
5776
6744
|
const byDate = new Map;
|
|
5777
6745
|
for (const row of rows) {
|
|
@@ -5795,7 +6763,7 @@ function buildServer() {
|
|
|
5795
6763
|
return text(lines.join(`
|
|
5796
6764
|
`));
|
|
5797
6765
|
});
|
|
5798
|
-
server.tool("get_billing_summary", "Actual provider billing totals from admin API sync. Params: period(today|yesterday|week|month|year|all)", { period:
|
|
6766
|
+
server.tool("get_billing_summary", "Actual provider billing totals from admin API sync. Params: period(today|yesterday|week|month|year|all)", { period: z3.enum(["today", "yesterday", "week", "month", "year", "all"]).optional() }, async ({ period }) => {
|
|
5799
6767
|
const summary = await store.billingSummary(period ?? "month");
|
|
5800
6768
|
const lines = ["provider billed"];
|
|
5801
6769
|
for (const [provider, cost] of Object.entries(summary.by_provider)) {
|
|
@@ -5805,7 +6773,7 @@ function buildServer() {
|
|
|
5805
6773
|
return text(lines.join(`
|
|
5806
6774
|
`));
|
|
5807
6775
|
});
|
|
5808
|
-
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok), limit(20), verbose.", { session_id:
|
|
6776
|
+
server.tool("get_session_detail", "Per-request breakdown of a single session. Params: session_id (prefix ok), limit(20), verbose.", { session_id: z3.string(), limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional() }, async ({ session_id, limit, verbose }) => {
|
|
5809
6777
|
const detail = await store.sessionDetail(session_id);
|
|
5810
6778
|
if (!detail)
|
|
5811
6779
|
return textError(`Session not found: ${session_id}`);
|
|
@@ -5829,13 +6797,13 @@ function buildServer() {
|
|
|
5829
6797
|
return text(lines.join(`
|
|
5830
6798
|
`));
|
|
5831
6799
|
});
|
|
5832
|
-
server.tool("sync", `Ingest new cost data. sources: ${SYNC_SOURCES.join("|")}. Set json=true for the full result object.`, { sources:
|
|
6800
|
+
server.tool("sync", `Ingest new cost data. sources: ${SYNC_SOURCES.join("|")}. Set json=true for the full result object.`, { sources: z3.enum([...SYNC_SOURCES]).optional(), json: z3.boolean().optional() }, async ({ sources, json }) => {
|
|
5833
6801
|
const selected = sources ?? "all";
|
|
5834
6802
|
const opts = selected === "all" ? {} : { [selected]: true };
|
|
5835
6803
|
if (isCloudStore()) {
|
|
5836
6804
|
const cloud = economyCloudStorage();
|
|
5837
6805
|
if (!cloud.active) {
|
|
5838
|
-
return text("cloud mode: sync transport unavailable (set
|
|
6806
|
+
return text("cloud mode: sync transport unavailable (no credential resolved; set HASNA_ECONOMY_API_KEY or write ~/.hasna/economy/config/credentials)");
|
|
5839
6807
|
}
|
|
5840
6808
|
const result2 = await syncAllToCloud(cloud, opts);
|
|
5841
6809
|
if (json)
|
|
@@ -5877,7 +6845,7 @@ function buildServer() {
|
|
|
5877
6845
|
return text(lines.join(`
|
|
5878
6846
|
`));
|
|
5879
6847
|
});
|
|
5880
|
-
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:
|
|
6848
|
+
server.tool("get_usage", "Usage snapshots and fleet summary. period: today|week|month|year|all, agent, limit(20). Set json=true for full data.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional(), agent: z3.enum(AGENTS).optional(), limit: z3.number().int().positive().max(100).optional(), json: z3.boolean().optional() }, async ({ period, agent, limit, json }) => {
|
|
5881
6849
|
const p = period ?? "month";
|
|
5882
6850
|
const { snapshots: snaps, summary } = await store.usage(p, agent);
|
|
5883
6851
|
if (json)
|
|
@@ -5899,7 +6867,7 @@ function buildServer() {
|
|
|
5899
6867
|
return text(lines.join(`
|
|
5900
6868
|
`));
|
|
5901
6869
|
});
|
|
5902
|
-
server.tool("get_savings", "Subscription vs API savings summary. Params: period, agent, limit(20). Set json=true for full data.", { period:
|
|
6870
|
+
server.tool("get_savings", "Subscription vs API savings summary. Params: period, agent, limit(20). Set json=true for full data.", { period: z3.enum(["today", "week", "month", "year", "all"]).optional(), agent: z3.enum(AGENTS).optional(), limit: z3.number().int().positive().max(100).optional(), json: z3.boolean().optional() }, async ({ period, agent, limit, json }) => {
|
|
5903
6871
|
const savings = await store.savings(period ?? "month", agent);
|
|
5904
6872
|
if (json)
|
|
5905
6873
|
return text(JSON.stringify(savings, null, 2));
|
|
@@ -5936,16 +6904,16 @@ function buildServer() {
|
|
|
5936
6904
|
`));
|
|
5937
6905
|
});
|
|
5938
6906
|
server.tool("set_subscription", "Create or update a subscription plan used by subscription-vs-API savings calculations.", {
|
|
5939
|
-
id:
|
|
5940
|
-
provider:
|
|
5941
|
-
plan:
|
|
5942
|
-
agent:
|
|
5943
|
-
monthly_fee_usd:
|
|
5944
|
-
included_usage_usd:
|
|
5945
|
-
billing_cycle_start:
|
|
5946
|
-
reset_policy:
|
|
5947
|
-
active:
|
|
5948
|
-
json:
|
|
6907
|
+
id: z3.string().optional(),
|
|
6908
|
+
provider: z3.string().min(1),
|
|
6909
|
+
plan: z3.string().min(1),
|
|
6910
|
+
agent: z3.enum(AGENTS).optional(),
|
|
6911
|
+
monthly_fee_usd: z3.number().nonnegative().optional(),
|
|
6912
|
+
included_usage_usd: z3.number().nonnegative().optional(),
|
|
6913
|
+
billing_cycle_start: z3.string().optional(),
|
|
6914
|
+
reset_policy: z3.string().optional(),
|
|
6915
|
+
active: z3.boolean().optional(),
|
|
6916
|
+
json: z3.boolean().optional()
|
|
5949
6917
|
}, async (input) => {
|
|
5950
6918
|
const row = await store.setSubscription({
|
|
5951
6919
|
id: input.id,
|
|
@@ -5967,15 +6935,15 @@ agent: ${row.agent ?? "all"}
|
|
|
5967
6935
|
fee: ${fmtUsd(row.monthly_fee_usd)}
|
|
5968
6936
|
included: ${fmtUsd(row.included_usage_usd)}`);
|
|
5969
6937
|
});
|
|
5970
|
-
server.tool("remove_subscription", "Delete a subscription plan by id.", { id:
|
|
6938
|
+
server.tool("remove_subscription", "Delete a subscription plan by id.", { id: z3.string() }, async ({ id }) => {
|
|
5971
6939
|
await store.removeSubscription(id);
|
|
5972
6940
|
return text("Subscription removed.");
|
|
5973
6941
|
});
|
|
5974
|
-
server.tool("estimate_cost", "Pre-flight cost estimate for token counts", { model:
|
|
6942
|
+
server.tool("estimate_cost", "Pre-flight cost estimate for token counts", { model: z3.string(), input_tokens: z3.number().optional(), output_tokens: z3.number().optional() }, async ({ model, input_tokens, output_tokens }) => {
|
|
5975
6943
|
const cost = await store.estimate({ model, inputTokens: input_tokens ?? 0, outputTokens: output_tokens ?? 0 });
|
|
5976
6944
|
return text(`${model}: ${fmtUsd(cost)} (${input_tokens ?? 0} in / ${output_tokens ?? 0} out)`);
|
|
5977
6945
|
});
|
|
5978
|
-
server.tool("get_goals", "All spending goals with current progress. Params: limit(20), verbose, json.", { limit:
|
|
6946
|
+
server.tool("get_goals", "All spending goals with current progress. Params: limit(20), verbose, json.", { limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
5979
6947
|
const goals = await store.listGoals();
|
|
5980
6948
|
if (json)
|
|
5981
6949
|
return text(JSON.stringify(goals, null, 2));
|
|
@@ -5996,10 +6964,10 @@ included: ${fmtUsd(row.included_usage_usd)}`);
|
|
|
5996
6964
|
`));
|
|
5997
6965
|
});
|
|
5998
6966
|
server.tool("set_goal", "Create/update a spending goal. period(day|week|month|year), limit_usd, project_path?, agent?", {
|
|
5999
|
-
period:
|
|
6000
|
-
limit_usd:
|
|
6001
|
-
project_path:
|
|
6002
|
-
agent:
|
|
6967
|
+
period: z3.enum(["day", "week", "month", "year"]),
|
|
6968
|
+
limit_usd: z3.number().positive(),
|
|
6969
|
+
project_path: z3.string().optional(),
|
|
6970
|
+
agent: z3.string().optional()
|
|
6003
6971
|
}, async ({ period, limit_usd, project_path, agent }) => {
|
|
6004
6972
|
await store.setGoal({
|
|
6005
6973
|
period,
|
|
@@ -6009,11 +6977,11 @@ included: ${fmtUsd(row.included_usage_usd)}`);
|
|
|
6009
6977
|
});
|
|
6010
6978
|
return text(`Goal set: ${period} $${limit_usd}`);
|
|
6011
6979
|
});
|
|
6012
|
-
server.tool("remove_goal", "Delete a goal by id.", { id:
|
|
6980
|
+
server.tool("remove_goal", "Delete a goal by id.", { id: z3.string() }, async ({ id }) => {
|
|
6013
6981
|
await store.removeGoal(id);
|
|
6014
6982
|
return text("Goal removed.");
|
|
6015
6983
|
});
|
|
6016
|
-
server.tool("list_machines", "List all machines that have synced data. Params: limit(20), verbose, json.", { limit:
|
|
6984
|
+
server.tool("list_machines", "List all machines that have synced data. Params: limit(20), verbose, json.", { limit: z3.number().int().positive().max(100).optional(), verbose: z3.boolean().optional(), json: z3.boolean().optional() }, async ({ limit, verbose, json }) => {
|
|
6017
6985
|
const machines = await store.machines();
|
|
6018
6986
|
if (json)
|
|
6019
6987
|
return text(JSON.stringify(machines, null, 2));
|
|
@@ -6032,11 +7000,11 @@ current machine: ${getMachineId()}`);
|
|
|
6032
7000
|
return text(lines.join(`
|
|
6033
7001
|
`));
|
|
6034
7002
|
});
|
|
6035
|
-
registerAgentTools(server, {
|
|
7003
|
+
registerAgentTools(server, { service: "economy" });
|
|
6036
7004
|
server.tool("send_feedback", "Send feedback about this service.", {
|
|
6037
|
-
message:
|
|
6038
|
-
email:
|
|
6039
|
-
category:
|
|
7005
|
+
message: z3.string(),
|
|
7006
|
+
email: z3.string().optional(),
|
|
7007
|
+
category: z3.enum(["bug", "feature", "general"]).optional()
|
|
6040
7008
|
}, async ({ message, email, category }) => {
|
|
6041
7009
|
try {
|
|
6042
7010
|
await store.sendFeedback({ message, email, category });
|
|
@@ -6048,32 +7016,102 @@ current machine: ${getMachineId()}`);
|
|
|
6048
7016
|
return server;
|
|
6049
7017
|
}
|
|
6050
7018
|
|
|
7019
|
+
// src/mcp/harness.ts
|
|
7020
|
+
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
|
|
7021
|
+
var PORT_MIN = 0;
|
|
7022
|
+
var PORT_MAX = 65535;
|
|
7023
|
+
var DEFAULT_MCP_HTTP_PORT2 = 8899;
|
|
7024
|
+
function invalidPortMessage(source, value) {
|
|
7025
|
+
return `Invalid ${source} "${value}". Expected an integer between ${PORT_MIN} and ${PORT_MAX}.`;
|
|
7026
|
+
}
|
|
7027
|
+
function validatePort(port, source) {
|
|
7028
|
+
if (!Number.isInteger(port) || port < PORT_MIN || port > PORT_MAX) {
|
|
7029
|
+
throw new Error(invalidPortMessage(source, String(port)));
|
|
7030
|
+
}
|
|
7031
|
+
return port;
|
|
7032
|
+
}
|
|
7033
|
+
function parsePortValue(value, source) {
|
|
7034
|
+
const trimmed = value.trim();
|
|
7035
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
7036
|
+
throw new Error(invalidPortMessage(source, value));
|
|
7037
|
+
}
|
|
7038
|
+
return validatePort(Number(trimmed), source);
|
|
7039
|
+
}
|
|
7040
|
+
function isStdioMode(argv = process.argv, env = process.env) {
|
|
7041
|
+
return argv.includes("--stdio") || env.MCP_STDIO === "1";
|
|
7042
|
+
}
|
|
7043
|
+
function isHttpMode(argv = process.argv, env = process.env) {
|
|
7044
|
+
return argv.includes("--http") || env.MCP_HTTP === "1";
|
|
7045
|
+
}
|
|
7046
|
+
function parseHttpArgv(argv = process.argv) {
|
|
7047
|
+
const http = isHttpMode(argv);
|
|
7048
|
+
let port;
|
|
7049
|
+
const portEqualsArg = argv.find((arg) => arg.startsWith("--port="));
|
|
7050
|
+
if (portEqualsArg) {
|
|
7051
|
+
port = parsePortValue(portEqualsArg.slice("--port=".length), "--port");
|
|
7052
|
+
}
|
|
7053
|
+
const portIdx = argv.indexOf("--port");
|
|
7054
|
+
if (portIdx !== -1) {
|
|
7055
|
+
const value = argv[portIdx + 1];
|
|
7056
|
+
if (value === undefined) {
|
|
7057
|
+
throw new Error(invalidPortMessage("--port", ""));
|
|
7058
|
+
}
|
|
7059
|
+
port = parsePortValue(value, "--port");
|
|
7060
|
+
}
|
|
7061
|
+
return { http, port };
|
|
7062
|
+
}
|
|
7063
|
+
function resolveMcpHttpPort(opts) {
|
|
7064
|
+
const argv = opts?.argv ?? process.argv;
|
|
7065
|
+
const env = opts?.env ?? process.env;
|
|
7066
|
+
if (opts?.explicit != null) {
|
|
7067
|
+
return validatePort(opts.explicit, "--port");
|
|
7068
|
+
}
|
|
7069
|
+
const { port } = parseHttpArgv(argv);
|
|
7070
|
+
if (port != null)
|
|
7071
|
+
return port;
|
|
7072
|
+
const envPort = env.MCP_HTTP_PORT;
|
|
7073
|
+
if (envPort && envPort.trim() !== "") {
|
|
7074
|
+
return parsePortValue(envPort, "MCP_HTTP_PORT");
|
|
7075
|
+
}
|
|
7076
|
+
return opts?.default ?? DEFAULT_MCP_HTTP_PORT2;
|
|
7077
|
+
}
|
|
7078
|
+
function healthPayload(name) {
|
|
7079
|
+
return { status: "ok", name };
|
|
7080
|
+
}
|
|
7081
|
+
async function handleMcpHttpRequest(req, buildServer2, options) {
|
|
7082
|
+
try {
|
|
7083
|
+
const server = await buildServer2();
|
|
7084
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
7085
|
+
sessionIdGenerator: undefined,
|
|
7086
|
+
enableJsonResponse: options?.enableJsonResponse
|
|
7087
|
+
});
|
|
7088
|
+
await server.connect(transport);
|
|
7089
|
+
return await transport.handleRequest(req);
|
|
7090
|
+
} catch (error) {
|
|
7091
|
+
console.error("[mcp] HTTP error:", error);
|
|
7092
|
+
return Response.json({ jsonrpc: "2.0", error: { code: -32603, message: "Internal server error" }, id: null }, { status: 500 });
|
|
7093
|
+
}
|
|
7094
|
+
}
|
|
7095
|
+
|
|
6051
7096
|
// src/mcp/http.ts
|
|
6052
|
-
import {
|
|
6053
|
-
healthPayload,
|
|
6054
|
-
isHttpMode as harnessIsHttpMode,
|
|
6055
|
-
isStdioMode as harnessIsStdioMode,
|
|
6056
|
-
resolveMcpHttpPort as harnessResolveMcpHttpPort
|
|
6057
|
-
} from "@hasna/mcp-harness";
|
|
6058
|
-
import { handleMcpHttpRequest as harnessHandleMcpHttpRequest } from "@hasna/mcp-harness/bun";
|
|
6059
7097
|
var MCP_HTTP_IDLE_TIMEOUT_SECONDS = 0;
|
|
6060
|
-
function
|
|
6061
|
-
return
|
|
7098
|
+
function isHttpMode2(argv = process.argv.slice(2)) {
|
|
7099
|
+
return isHttpMode(argv);
|
|
6062
7100
|
}
|
|
6063
|
-
function
|
|
6064
|
-
return
|
|
7101
|
+
function isStdioMode2(argv = process.argv.slice(2)) {
|
|
7102
|
+
return isStdioMode(argv);
|
|
6065
7103
|
}
|
|
6066
7104
|
function resolveHttpPort(argv = process.argv.slice(2)) {
|
|
6067
7105
|
const normalized = argv.map((arg) => arg === "-p" ? "--port" : arg);
|
|
6068
|
-
return
|
|
7106
|
+
return resolveMcpHttpPort({ argv: normalized, default: DEFAULT_MCP_HTTP_PORT });
|
|
6069
7107
|
}
|
|
6070
|
-
async function
|
|
7108
|
+
async function handleMcpHttpRequest2(req) {
|
|
6071
7109
|
const url = new URL(req.url);
|
|
6072
7110
|
if (url.pathname === "/health" && req.method === "GET") {
|
|
6073
7111
|
return Response.json(healthPayload(MCP_NAME));
|
|
6074
7112
|
}
|
|
6075
7113
|
if (url.pathname === "/mcp") {
|
|
6076
|
-
return
|
|
7114
|
+
return handleMcpHttpRequest(req, buildServer);
|
|
6077
7115
|
}
|
|
6078
7116
|
return new Response("Not Found", { status: 404 });
|
|
6079
7117
|
}
|
|
@@ -6085,7 +7123,7 @@ function startHttpServer(options = {}) {
|
|
|
6085
7123
|
port,
|
|
6086
7124
|
hostname: hostname2,
|
|
6087
7125
|
idleTimeout: MCP_HTTP_IDLE_TIMEOUT_SECONDS,
|
|
6088
|
-
fetch:
|
|
7126
|
+
fetch: handleMcpHttpRequest2
|
|
6089
7127
|
});
|
|
6090
7128
|
const address = `http://${hostname2}:${server.port}`;
|
|
6091
7129
|
log(`${MCP_NAME}-mcp HTTP listening on ${address}/mcp (health: ${address}/health)`);
|
|
@@ -6118,7 +7156,7 @@ if (args.includes("--version") || args.includes("-V")) {
|
|
|
6118
7156
|
process.exit(0);
|
|
6119
7157
|
}
|
|
6120
7158
|
async function main() {
|
|
6121
|
-
if (
|
|
7159
|
+
if (isStdioMode2(args) || !isHttpMode2(args)) {
|
|
6122
7160
|
const server = buildServer();
|
|
6123
7161
|
const transport = new StdioServerTransport;
|
|
6124
7162
|
await server.connect(transport);
|
|
@@ -6128,6 +7166,6 @@ async function main() {
|
|
|
6128
7166
|
await new Promise(() => {});
|
|
6129
7167
|
}
|
|
6130
7168
|
main().catch((error) => {
|
|
6131
|
-
console.error("MCP server error:", error);
|
|
7169
|
+
console.error("MCP server error:", error instanceof Error ? error.message : String(error));
|
|
6132
7170
|
process.exit(1);
|
|
6133
7171
|
});
|