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