agent-metry
Advanced tools
+1
-1
| { | ||
| "name": "agent-metry", | ||
| "type": "module", | ||
| "version": "0.1.2", | ||
| "version": "0.1.3", | ||
| "description": "Agent Metry CLI with bundled frontend and backend", | ||
@@ -6,0 +6,0 @@ "packageManager": "bun@1.3.11", |
+172
-2
| #!/usr/bin/env bun | ||
| import { configManager, modelsManager, authManager } from "./core/index.js"; | ||
| import { configManager, modelsManager, isUnknownModel, calculateModelCost, authManager, resolveAgentTelemetryDir } from "./core/index.js"; | ||
| import { planDefaultCodexSessionUploads, markCodexSessionsUploaded } from "./core/codex.js"; | ||
| import { Database } from "bun:sqlite"; | ||
| import { Command } from "commander"; | ||
| const VERSION = "0.1.2"; | ||
| const VERSION = "0.1.3"; | ||
| async function runCommand(command, args, options = {}) { | ||
@@ -24,2 +25,5 @@ const child = Bun.spawn([command, ...args], { | ||
| } | ||
| function resolveDatabasePath() { | ||
| return `${resolveAgentTelemetryDir()}/data.db`; | ||
| } | ||
| function trimTrailingSlash(value) { | ||
@@ -31,2 +35,151 @@ return value.replace(/\/+$/, ""); | ||
| } | ||
| function parseModelProvider(value) { | ||
| const provider = value.trim(); | ||
| if (provider === "openai" || provider === "anthropic") | ||
| return provider; | ||
| throw new Error("Provider must be openai or anthropic"); | ||
| } | ||
| function parsePriceOption(value, key) { | ||
| const rawValue = value.trim(); | ||
| if (!rawValue) | ||
| throw new Error(`${key} must be a non-negative finite number`); | ||
| const price = Number(rawValue); | ||
| if (!Number.isFinite(price) || price < 0) | ||
| throw new Error(`${key} must be a non-negative finite number`); | ||
| return price; | ||
| } | ||
| function hasSessionMetricsTable(sqlite) { | ||
| return Boolean(sqlite.query(` | ||
| SELECT name | ||
| FROM sqlite_master | ||
| WHERE type = 'table' AND name = ? | ||
| `).get("session_metrics")); | ||
| } | ||
| async function openExistingMetricsDatabase() { | ||
| const databasePath = resolveDatabasePath(); | ||
| if (!await pathExists(databasePath)) | ||
| return void 0; | ||
| const sqlite = new Database(databasePath); | ||
| if (!hasSessionMetricsTable(sqlite)) { | ||
| sqlite.close(); | ||
| return void 0; | ||
| } | ||
| return sqlite; | ||
| } | ||
| async function listMissingModels() { | ||
| const state = await modelsManager.ensureDefaultModels(); | ||
| const knownModelIds = new Set(state.models.map((model) => model.model)); | ||
| const sqlite = await openExistingMetricsDatabase(); | ||
| if (!sqlite) | ||
| return []; | ||
| try { | ||
| const rows = sqlite.query(` | ||
| SELECT DISTINCT model | ||
| FROM session_metrics | ||
| ORDER BY model ASC | ||
| `).all(); | ||
| return rows.map((row) => row.model).filter((model) => !isUnknownModel(model) && !knownModelIds.has(model)); | ||
| } finally { | ||
| sqlite.close(); | ||
| } | ||
| } | ||
| async function repriceSessions(model) { | ||
| const modelId = model?.trim(); | ||
| if (model !== void 0 && !modelId) | ||
| throw new Error("Model id is required"); | ||
| const state = await modelsManager.ensureDefaultModels(); | ||
| const prices = new Map(state.models.map((price) => [price.model, price])); | ||
| const sqlite = await openExistingMetricsDatabase(); | ||
| if (!sqlite) | ||
| return 0; | ||
| try { | ||
| const rows = modelId ? sqlite.query(` | ||
| SELECT | ||
| id, | ||
| model, | ||
| input_tokens, | ||
| output_tokens, | ||
| cached_input_tokens, | ||
| reasoning_output_tokens | ||
| FROM session_metrics | ||
| WHERE model = ? | ||
| `).all(modelId) : sqlite.query(` | ||
| SELECT | ||
| id, | ||
| model, | ||
| input_tokens, | ||
| output_tokens, | ||
| cached_input_tokens, | ||
| reasoning_output_tokens | ||
| FROM session_metrics | ||
| `).all(); | ||
| sqlite.transaction(() => { | ||
| const updateQuery = sqlite.query(` | ||
| UPDATE session_metrics | ||
| SET | ||
| input_cost = ?, | ||
| output_cost = ?, | ||
| cached_input_cost = ?, | ||
| reasoning_output_cost = ?, | ||
| total_cost = ? | ||
| WHERE id = ? | ||
| `); | ||
| for (const row of rows) { | ||
| const cost = calculateModelCost(row, prices.get(row.model)); | ||
| updateQuery.run( | ||
| cost.input_cost, | ||
| cost.output_cost, | ||
| cost.cached_input_cost, | ||
| cost.reasoning_output_cost, | ||
| cost.total_cost, | ||
| row.id | ||
| ); | ||
| } | ||
| })(); | ||
| return rows.length; | ||
| } finally { | ||
| sqlite.close(); | ||
| } | ||
| } | ||
| async function clearModelCosts(model) { | ||
| const sqlite = await openExistingMetricsDatabase(); | ||
| if (!sqlite) | ||
| return 0; | ||
| try { | ||
| const count = sqlite.query(` | ||
| SELECT COUNT(*) AS count | ||
| FROM session_metrics | ||
| WHERE model = ? | ||
| `).get(model)?.count ?? 0; | ||
| sqlite.query(` | ||
| UPDATE session_metrics | ||
| SET | ||
| input_cost = 0, | ||
| output_cost = 0, | ||
| cached_input_cost = 0, | ||
| reasoning_output_cost = 0, | ||
| total_cost = 0 | ||
| WHERE model = ? | ||
| `).run(model); | ||
| return count; | ||
| } finally { | ||
| sqlite.close(); | ||
| } | ||
| } | ||
| async function setModel(model, options) { | ||
| const result = await modelsManager.set({ | ||
| model, | ||
| provider: parseModelProvider(options.provider), | ||
| input_usd_per_1m_tokens: parsePriceOption(options.input, "input"), | ||
| cached_input_usd_per_1m_tokens: parsePriceOption(options.cachedInput, "cached-input"), | ||
| output_usd_per_1m_tokens: parsePriceOption(options.output, "output") | ||
| }); | ||
| const repricedSessions = await repriceSessions(result.model.model); | ||
| console.log(`action=${result.action} repriced_sessions=${repricedSessions}`); | ||
| } | ||
| async function removeModel(model) { | ||
| const result = await modelsManager.remove(model); | ||
| const clearedSessions = await clearModelCosts(result.model); | ||
| console.log(`removed=true cleared_sessions=${clearedSessions}`); | ||
| } | ||
| async function requestJson(path, options) { | ||
@@ -187,2 +340,19 @@ const baseUrl = await readBaseUrl(); | ||
| }); | ||
| const modelsCommand = program.command("models").description("Manage local model prices"); | ||
| modelsCommand.command("list").description("List local model prices").action(async () => { | ||
| console.log(JSON.stringify(await modelsManager.list(), null, 2)); | ||
| }); | ||
| modelsCommand.command("missing").description("List models present in uploaded sessions but missing prices").action(async () => { | ||
| console.log(JSON.stringify(await listMissingModels(), null, 2)); | ||
| }); | ||
| modelsCommand.command("set").argument("<model>").requiredOption("--provider <provider>", "Model provider: openai or anthropic").requiredOption("--input <usd>", "Input price in USD per 1M tokens").requiredOption("--cached-input <usd>", "Cached input price in USD per 1M tokens").requiredOption("--output <usd>", "Output price in USD per 1M tokens").description("Create or update a local model price and reprice its sessions").action(async (model, options) => { | ||
| await setModel(model, options); | ||
| }); | ||
| modelsCommand.command("remove").argument("<model>").description("Remove a local model price and clear its stored session costs").action(async (model) => { | ||
| await removeModel(model); | ||
| }); | ||
| modelsCommand.command("reprice").argument("[model]").description("Recalculate stored session costs from local model prices").action(async (model) => { | ||
| const repricedSessions = await repriceSessions(model); | ||
| console.log(`model=${model?.trim() || "all"} repriced_sessions=${repricedSessions}`); | ||
| }); | ||
| program.command("login").argument("<email>").argument("<password>").description("Login and save CLI auth credentials").action(async (email, password) => { | ||
@@ -189,0 +359,0 @@ await login(email, password); |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"cli.js","sources":["../src/cli.ts"],"sourcesContent":["import {\n authManager,\n configManager,\n modelsManager,\n type AuthState,\n type SessionMetricUpload,\n type UploadSessionsResponse,\n} from '@agent-metry/core'\nimport {\n markCodexSessionsUploaded,\n planDefaultCodexSessionUploads,\n type PendingCodexSessionUpload,\n} from '@agent-metry/core/codex'\nimport { Command } from 'commander'\n\nconst VERSION = __APP_VERSION__\n\ninterface RunOptions {\n cwd?: string\n env?: Record<string, string | undefined>\n}\n\ninterface ServerOptions {\n port?: string\n}\n\ninterface HttpErrorPayload {\n error?: string\n message?: string\n}\n\nasync function runCommand(command: string, args: string[], options: RunOptions = {}) {\n const child = Bun.spawn([command, ...args], {\n cwd: options.cwd,\n env: options.env,\n stdin: 'inherit',\n stdout: 'inherit',\n stderr: 'inherit',\n })\n\n const exitCode = await child.exited\n if (exitCode !== 0)\n throw new Error(`${command} ${args.join(' ')} exited with ${exitCode}`)\n}\n\nfunction resolveCliDist() {\n return import.meta.dirname\n}\n\nasync function pathExists(path: string) {\n return Bun.file(path).exists()\n}\n\nfunction trimTrailingSlash(value: string) {\n return value.replace(/\\/+$/, '')\n}\n\nasync function readBaseUrl() {\n return trimTrailingSlash(await configManager.getRequired('base_url'))\n}\n\nasync function requestJson<T>(\n path: string,\n options: {\n method: 'GET' | 'POST' | 'PATCH'\n body?: unknown\n token?: string\n },\n): Promise<T> {\n const baseUrl = await readBaseUrl()\n const headers = new Headers({ accept: 'application/json' })\n if (options.body !== undefined)\n headers.set('content-type', 'application/json')\n if (options.token)\n headers.set('authorization', `Bearer ${options.token}`)\n\n const response = await fetch(`${baseUrl}${path}`, {\n method: options.method,\n headers,\n body: options.body === undefined ? undefined : JSON.stringify(options.body),\n })\n\n const text = await response.text()\n const payload = text ? JSON.parse(text) as HttpErrorPayload : undefined\n if (!response.ok) {\n const message = payload?.error ?? payload?.message ?? `${response.status} ${response.statusText}`\n throw new Error(message)\n }\n\n return payload as T\n}\n\nasync function runServer(options: ServerOptions) {\n await modelsManager.ensureDefaultModels()\n\n const cliDist = resolveCliDist()\n const webRoot = `${cliDist}/web`\n const frontendDist = `${webRoot}/frontend`\n const backendEntry = `${webRoot}/backend/index.js`\n\n if (!(await pathExists(`${frontendDist}/index.html`))) {\n throw new Error('Frontend assets not found. Please rebuild the CLI package.')\n }\n\n if (!(await pathExists(backendEntry))) {\n throw new Error('Backend bundle not found. Please rebuild the CLI package.')\n }\n\n const env: Record<string, string | undefined> = {\n ...Bun.env,\n FRONTEND_DIST: frontendDist,\n }\n\n if (options.port) {\n env.PORT = options.port\n }\n\n await runCommand('bun', [backendEntry], { env })\n}\n\nasync function createUser(email: string, password: string) {\n const result = await requestJson<{ user: { id: string, email: string, name: string } }>('/api/users', {\n method: 'POST',\n body: {\n email,\n password,\n name: email,\n },\n })\n\n console.log(`Created user ${result.user.email}`)\n}\n\nfunction extractAuthState(payload: unknown): AuthState {\n if (!payload || typeof payload !== 'object')\n throw new Error('Login response did not contain an auth token')\n\n const record = payload as Record<string, unknown>\n const token = typeof record.token === 'string' ? record.token : undefined\n const refreshToken = typeof record.refreshToken === 'string' ? record.refreshToken : ''\n if (!token)\n throw new Error('Login response did not contain an auth token')\n\n return {\n access_token: token,\n refresh_token: refreshToken,\n }\n}\n\nasync function login(email: string, password: string) {\n const payload = await requestJson<unknown>('/api/auth/sign-in/email', {\n method: 'POST',\n body: {\n email,\n password,\n },\n })\n\n await authManager.write(extractAuthState(payload))\n console.log(`Logged in as ${email}`)\n}\n\nasync function readAuthToken() {\n const auth = await authManager.read()\n if (!auth?.access_token) {\n throw new Error('Not logged in. Run `agent-metry login <email> <password>` first.')\n }\n return auth.access_token\n}\n\nfunction toUploadMetric(item: PendingCodexSessionUpload): SessionMetricUpload {\n const { metrics } = item\n\n return {\n provider: 'codex',\n session_id: metrics.session_id,\n started_at: metrics.started_at,\n ended_at: metrics.ended_at,\n model: metrics.model || 'unknown',\n model_provider: metrics.model_provider,\n reasoning_effort: metrics.reasoning_effort,\n cli_version: metrics.cli_version,\n model_context_window: metrics.model_context_window,\n input_tokens: metrics.input_tokens,\n output_tokens: metrics.output_tokens,\n cached_input_tokens: metrics.cached_input_tokens,\n reasoning_output_tokens: metrics.reasoning_output_tokens,\n total_tokens: metrics.total_tokens,\n api_call_count: metrics.api_call_count,\n conversation_turn_count: metrics.conversation_turn_count,\n user_message_count: metrics.user_message_count,\n tool_call_count: metrics.tool_call_count,\n }\n}\n\nasync function uploadCodexSessions() {\n const token = await readAuthToken()\n const plan = await planDefaultCodexSessionUploads()\n\n if (plan.pending.length === 0) {\n console.log(`No changed Codex sessions to upload. Skipped ${plan.skipped.length}.`)\n return\n }\n\n const response = await requestJson<UploadSessionsResponse>('/api/upload', {\n method: 'POST',\n token,\n body: {\n provider: 'codex',\n sessions: plan.pending.map(toUploadMetric),\n },\n })\n\n if (response.error_count > 0) {\n throw new Error(`Upload completed with ${response.error_count} rejected session(s); history was not updated.`)\n }\n\n await markCodexSessionsUploaded(plan.pending)\n console.log([\n `Uploaded ${plan.pending.length} Codex session(s).`,\n `inserted=${response.inserted_count}`,\n `updated=${response.updated_count}`,\n `skipped_local=${plan.skipped.length}`,\n ].join(' '))\n}\n\nconst program = new Command()\n\nawait configManager.ensureConfig()\n\nprogram\n .name('agent-metry')\n .description('Agent Metry CLI.')\n .version(VERSION)\n\nprogram\n .command('server')\n .description('Serve the bundled frontend with the backend API')\n .option('-p, --port <port>', 'Set backend port', '3000')\n .action(async (options: ServerOptions) => {\n await runServer(options)\n })\n\nconst configCommand = program\n .command('config')\n .description('Manage local CLI configuration')\n\nconfigCommand\n .command('set')\n .argument('<key>')\n .argument('<value>')\n .description('Set a local config value')\n .action(async (key: string, value: string) => {\n await configManager.set(key, value)\n })\n\nconfigCommand\n .command('get')\n .argument('<key>')\n .description('Get a required local config value')\n .action(async (key: string) => {\n console.log(await configManager.getRequired(key))\n })\n\nconfigCommand\n .command('list')\n .description('List local config values')\n .action(async () => {\n console.log(JSON.stringify(await configManager.list(), null, 2))\n })\n\nconfigCommand\n .command('remove')\n .argument('<key>')\n .description('Remove a local config value')\n .action(async (key: string) => {\n await configManager.remove(key)\n })\n\nconst userCommand = program\n .command('user')\n .description('Manage users')\n\nuserCommand\n .command('create')\n .argument('<email>')\n .argument('<password>')\n .description('Create a user with name defaulting to email')\n .action(async (email: string, password: string) => {\n await createUser(email, password)\n })\n\nprogram\n .command('login')\n .argument('<email>')\n .argument('<password>')\n .description('Login and save CLI auth credentials')\n .action(async (email: string, password: string) => {\n await login(email, password)\n })\n\nprogram\n .command('upload')\n .description('Upload changed Codex session aggregates')\n .action(async () => {\n await uploadCodexSessions()\n })\n\nawait program.parseAsync(Bun.argv).catch((error) => {\n console.error(error instanceof Error ? error.message : error)\n process.exitCode = 1\n})\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"cli.js","sources":["../src/cli.ts"],"sourcesContent":["import {\n authManager,\n calculateModelCost,\n configManager,\n isUnknownModel,\n modelsManager,\n resolveAgentTelemetryDir,\n type AuthState,\n type ModelProvider,\n type SessionMetricUpload,\n type UploadSessionsResponse,\n} from '@agent-metry/core'\nimport {\n markCodexSessionsUploaded,\n planDefaultCodexSessionUploads,\n type PendingCodexSessionUpload,\n} from '@agent-metry/core/codex'\nimport { Database } from 'bun:sqlite'\nimport { Command } from 'commander'\n\nconst VERSION = __APP_VERSION__\n\ninterface RunOptions {\n cwd?: string\n env?: Record<string, string | undefined>\n}\n\ninterface ServerOptions {\n port?: string\n}\n\ninterface HttpErrorPayload {\n error?: string\n message?: string\n}\n\ninterface ModelSetOptions {\n provider: string\n input: string\n cachedInput: string\n output: string\n}\n\ninterface SessionMetricCostRow {\n id: string\n model: string\n input_tokens: number\n output_tokens: number\n cached_input_tokens: number\n reasoning_output_tokens: number\n}\n\ninterface ModelRow {\n model: string\n}\n\ninterface CountRow {\n count: number\n}\n\nasync function runCommand(command: string, args: string[], options: RunOptions = {}) {\n const child = Bun.spawn([command, ...args], {\n cwd: options.cwd,\n env: options.env,\n stdin: 'inherit',\n stdout: 'inherit',\n stderr: 'inherit',\n })\n\n const exitCode = await child.exited\n if (exitCode !== 0)\n throw new Error(`${command} ${args.join(' ')} exited with ${exitCode}`)\n}\n\nfunction resolveCliDist() {\n return import.meta.dirname\n}\n\nasync function pathExists(path: string) {\n return Bun.file(path).exists()\n}\n\nfunction resolveDatabasePath() {\n return `${resolveAgentTelemetryDir()}/data.db`\n}\n\nfunction trimTrailingSlash(value: string) {\n return value.replace(/\\/+$/, '')\n}\n\nasync function readBaseUrl() {\n return trimTrailingSlash(await configManager.getRequired('base_url'))\n}\n\nfunction parseModelProvider(value: string): ModelProvider {\n const provider = value.trim()\n if (provider === 'openai' || provider === 'anthropic')\n return provider\n throw new Error('Provider must be openai or anthropic')\n}\n\nfunction parsePriceOption(value: string, key: string) {\n const rawValue = value.trim()\n if (!rawValue)\n throw new Error(`${key} must be a non-negative finite number`)\n const price = Number(rawValue)\n if (!Number.isFinite(price) || price < 0)\n throw new Error(`${key} must be a non-negative finite number`)\n return price\n}\n\nfunction hasSessionMetricsTable(sqlite: Database) {\n return Boolean(sqlite.query<{ name: string }, [string]>(`\n SELECT name\n FROM sqlite_master\n WHERE type = 'table' AND name = ?\n `).get('session_metrics'))\n}\n\nasync function openExistingMetricsDatabase() {\n const databasePath = resolveDatabasePath()\n if (!(await pathExists(databasePath)))\n return undefined\n\n const sqlite = new Database(databasePath)\n if (!hasSessionMetricsTable(sqlite)) {\n sqlite.close()\n return undefined\n }\n\n return sqlite\n}\n\nasync function listMissingModels() {\n const state = await modelsManager.ensureDefaultModels()\n const knownModelIds = new Set(state.models.map(model => model.model))\n const sqlite = await openExistingMetricsDatabase()\n if (!sqlite)\n return []\n\n try {\n const rows = sqlite.query<ModelRow, []>(`\n SELECT DISTINCT model\n FROM session_metrics\n ORDER BY model ASC\n `).all()\n\n return rows\n .map(row => row.model)\n .filter(model => !isUnknownModel(model) && !knownModelIds.has(model))\n }\n finally {\n sqlite.close()\n }\n}\n\nasync function repriceSessions(model?: string) {\n const modelId = model?.trim()\n if (model !== undefined && !modelId)\n throw new Error('Model id is required')\n\n const state = await modelsManager.ensureDefaultModels()\n const prices = new Map(state.models.map(price => [price.model, price]))\n const sqlite = await openExistingMetricsDatabase()\n if (!sqlite)\n return 0\n\n try {\n const rows = modelId\n ? sqlite.query<SessionMetricCostRow, [string]>(`\n SELECT\n id,\n model,\n input_tokens,\n output_tokens,\n cached_input_tokens,\n reasoning_output_tokens\n FROM session_metrics\n WHERE model = ?\n `).all(modelId)\n : sqlite.query<SessionMetricCostRow, []>(`\n SELECT\n id,\n model,\n input_tokens,\n output_tokens,\n cached_input_tokens,\n reasoning_output_tokens\n FROM session_metrics\n `).all()\n\n sqlite.transaction(() => {\n const updateQuery = sqlite.query(`\n UPDATE session_metrics\n SET\n input_cost = ?,\n output_cost = ?,\n cached_input_cost = ?,\n reasoning_output_cost = ?,\n total_cost = ?\n WHERE id = ?\n `)\n\n for (const row of rows) {\n const cost = calculateModelCost(row, prices.get(row.model))\n updateQuery.run(\n cost.input_cost,\n cost.output_cost,\n cost.cached_input_cost,\n cost.reasoning_output_cost,\n cost.total_cost,\n row.id,\n )\n }\n })()\n\n return rows.length\n }\n finally {\n sqlite.close()\n }\n}\n\nasync function clearModelCosts(model: string) {\n const sqlite = await openExistingMetricsDatabase()\n if (!sqlite)\n return 0\n\n try {\n const count = sqlite.query<CountRow, [string]>(`\n SELECT COUNT(*) AS count\n FROM session_metrics\n WHERE model = ?\n `).get(model)?.count ?? 0\n\n sqlite.query(`\n UPDATE session_metrics\n SET\n input_cost = 0,\n output_cost = 0,\n cached_input_cost = 0,\n reasoning_output_cost = 0,\n total_cost = 0\n WHERE model = ?\n `).run(model)\n\n return count\n }\n finally {\n sqlite.close()\n }\n}\n\nasync function setModel(model: string, options: ModelSetOptions) {\n const result = await modelsManager.set({\n model,\n provider: parseModelProvider(options.provider),\n input_usd_per_1m_tokens: parsePriceOption(options.input, 'input'),\n cached_input_usd_per_1m_tokens: parsePriceOption(options.cachedInput, 'cached-input'),\n output_usd_per_1m_tokens: parsePriceOption(options.output, 'output'),\n })\n const repricedSessions = await repriceSessions(result.model.model)\n console.log(`action=${result.action} repriced_sessions=${repricedSessions}`)\n}\n\nasync function removeModel(model: string) {\n const result = await modelsManager.remove(model)\n const clearedSessions = await clearModelCosts(result.model)\n console.log(`removed=true cleared_sessions=${clearedSessions}`)\n}\n\nasync function requestJson<T>(\n path: string,\n options: {\n method: 'GET' | 'POST' | 'PATCH'\n body?: unknown\n token?: string\n },\n): Promise<T> {\n const baseUrl = await readBaseUrl()\n const headers = new Headers({ accept: 'application/json' })\n if (options.body !== undefined)\n headers.set('content-type', 'application/json')\n if (options.token)\n headers.set('authorization', `Bearer ${options.token}`)\n\n const response = await fetch(`${baseUrl}${path}`, {\n method: options.method,\n headers,\n body: options.body === undefined ? undefined : JSON.stringify(options.body),\n })\n\n const text = await response.text()\n const payload = text ? JSON.parse(text) as HttpErrorPayload : undefined\n if (!response.ok) {\n const message = payload?.error ?? payload?.message ?? `${response.status} ${response.statusText}`\n throw new Error(message)\n }\n\n return payload as T\n}\n\nasync function runServer(options: ServerOptions) {\n await modelsManager.ensureDefaultModels()\n\n const cliDist = resolveCliDist()\n const webRoot = `${cliDist}/web`\n const frontendDist = `${webRoot}/frontend`\n const backendEntry = `${webRoot}/backend/index.js`\n\n if (!(await pathExists(`${frontendDist}/index.html`))) {\n throw new Error('Frontend assets not found. Please rebuild the CLI package.')\n }\n\n if (!(await pathExists(backendEntry))) {\n throw new Error('Backend bundle not found. Please rebuild the CLI package.')\n }\n\n const env: Record<string, string | undefined> = {\n ...Bun.env,\n FRONTEND_DIST: frontendDist,\n }\n\n if (options.port) {\n env.PORT = options.port\n }\n\n await runCommand('bun', [backendEntry], { env })\n}\n\nasync function createUser(email: string, password: string) {\n const result = await requestJson<{ user: { id: string, email: string, name: string } }>('/api/users', {\n method: 'POST',\n body: {\n email,\n password,\n name: email,\n },\n })\n\n console.log(`Created user ${result.user.email}`)\n}\n\nfunction extractAuthState(payload: unknown): AuthState {\n if (!payload || typeof payload !== 'object')\n throw new Error('Login response did not contain an auth token')\n\n const record = payload as Record<string, unknown>\n const token = typeof record.token === 'string' ? record.token : undefined\n const refreshToken = typeof record.refreshToken === 'string' ? record.refreshToken : ''\n if (!token)\n throw new Error('Login response did not contain an auth token')\n\n return {\n access_token: token,\n refresh_token: refreshToken,\n }\n}\n\nasync function login(email: string, password: string) {\n const payload = await requestJson<unknown>('/api/auth/sign-in/email', {\n method: 'POST',\n body: {\n email,\n password,\n },\n })\n\n await authManager.write(extractAuthState(payload))\n console.log(`Logged in as ${email}`)\n}\n\nasync function readAuthToken() {\n const auth = await authManager.read()\n if (!auth?.access_token) {\n throw new Error('Not logged in. Run `agent-metry login <email> <password>` first.')\n }\n return auth.access_token\n}\n\nfunction toUploadMetric(item: PendingCodexSessionUpload): SessionMetricUpload {\n const { metrics } = item\n\n return {\n provider: 'codex',\n session_id: metrics.session_id,\n started_at: metrics.started_at,\n ended_at: metrics.ended_at,\n model: metrics.model || 'unknown',\n model_provider: metrics.model_provider,\n reasoning_effort: metrics.reasoning_effort,\n cli_version: metrics.cli_version,\n model_context_window: metrics.model_context_window,\n input_tokens: metrics.input_tokens,\n output_tokens: metrics.output_tokens,\n cached_input_tokens: metrics.cached_input_tokens,\n reasoning_output_tokens: metrics.reasoning_output_tokens,\n total_tokens: metrics.total_tokens,\n api_call_count: metrics.api_call_count,\n conversation_turn_count: metrics.conversation_turn_count,\n user_message_count: metrics.user_message_count,\n tool_call_count: metrics.tool_call_count,\n }\n}\n\nasync function uploadCodexSessions() {\n const token = await readAuthToken()\n const plan = await planDefaultCodexSessionUploads()\n\n if (plan.pending.length === 0) {\n console.log(`No changed Codex sessions to upload. Skipped ${plan.skipped.length}.`)\n return\n }\n\n const response = await requestJson<UploadSessionsResponse>('/api/upload', {\n method: 'POST',\n token,\n body: {\n provider: 'codex',\n sessions: plan.pending.map(toUploadMetric),\n },\n })\n\n if (response.error_count > 0) {\n throw new Error(`Upload completed with ${response.error_count} rejected session(s); history was not updated.`)\n }\n\n await markCodexSessionsUploaded(plan.pending)\n console.log([\n `Uploaded ${plan.pending.length} Codex session(s).`,\n `inserted=${response.inserted_count}`,\n `updated=${response.updated_count}`,\n `skipped_local=${plan.skipped.length}`,\n ].join(' '))\n}\n\nconst program = new Command()\n\nawait configManager.ensureConfig()\n\nprogram\n .name('agent-metry')\n .description('Agent Metry CLI.')\n .version(VERSION)\n\nprogram\n .command('server')\n .description('Serve the bundled frontend with the backend API')\n .option('-p, --port <port>', 'Set backend port', '3000')\n .action(async (options: ServerOptions) => {\n await runServer(options)\n })\n\nconst configCommand = program\n .command('config')\n .description('Manage local CLI configuration')\n\nconfigCommand\n .command('set')\n .argument('<key>')\n .argument('<value>')\n .description('Set a local config value')\n .action(async (key: string, value: string) => {\n await configManager.set(key, value)\n })\n\nconfigCommand\n .command('get')\n .argument('<key>')\n .description('Get a required local config value')\n .action(async (key: string) => {\n console.log(await configManager.getRequired(key))\n })\n\nconfigCommand\n .command('list')\n .description('List local config values')\n .action(async () => {\n console.log(JSON.stringify(await configManager.list(), null, 2))\n })\n\nconfigCommand\n .command('remove')\n .argument('<key>')\n .description('Remove a local config value')\n .action(async (key: string) => {\n await configManager.remove(key)\n })\n\nconst userCommand = program\n .command('user')\n .description('Manage users')\n\nuserCommand\n .command('create')\n .argument('<email>')\n .argument('<password>')\n .description('Create a user with name defaulting to email')\n .action(async (email: string, password: string) => {\n await createUser(email, password)\n })\n\nconst modelsCommand = program\n .command('models')\n .description('Manage local model prices')\n\nmodelsCommand\n .command('list')\n .description('List local model prices')\n .action(async () => {\n console.log(JSON.stringify(await modelsManager.list(), null, 2))\n })\n\nmodelsCommand\n .command('missing')\n .description('List models present in uploaded sessions but missing prices')\n .action(async () => {\n console.log(JSON.stringify(await listMissingModels(), null, 2))\n })\n\nmodelsCommand\n .command('set')\n .argument('<model>')\n .requiredOption('--provider <provider>', 'Model provider: openai or anthropic')\n .requiredOption('--input <usd>', 'Input price in USD per 1M tokens')\n .requiredOption('--cached-input <usd>', 'Cached input price in USD per 1M tokens')\n .requiredOption('--output <usd>', 'Output price in USD per 1M tokens')\n .description('Create or update a local model price and reprice its sessions')\n .action(async (model: string, options: ModelSetOptions) => {\n await setModel(model, options)\n })\n\nmodelsCommand\n .command('remove')\n .argument('<model>')\n .description('Remove a local model price and clear its stored session costs')\n .action(async (model: string) => {\n await removeModel(model)\n })\n\nmodelsCommand\n .command('reprice')\n .argument('[model]')\n .description('Recalculate stored session costs from local model prices')\n .action(async (model?: string) => {\n const repricedSessions = await repriceSessions(model)\n console.log(`model=${model?.trim() || 'all'} repriced_sessions=${repricedSessions}`)\n })\n\nprogram\n .command('login')\n .argument('<email>')\n .argument('<password>')\n .description('Login and save CLI auth credentials')\n .action(async (email: string, password: string) => {\n await login(email, password)\n })\n\nprogram\n .command('upload')\n .description('Upload changed Codex session aggregates')\n .action(async () => {\n await uploadCodexSessions()\n })\n\nawait program.parseAsync(Bun.argv).catch((error) => {\n console.error(error instanceof Error ? error.message : error)\n process.exitCode = 1\n})\n"],"names":[],"mappings":"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"} |
@@ -24,5 +24,6 @@ export interface HealthStatusPayload { | ||
| } | ||
| export type ModelProvider = 'openai' | 'anthropic'; | ||
| export interface ModelPrice { | ||
| model: string; | ||
| provider: 'openai' | 'anthropic'; | ||
| provider: ModelProvider; | ||
| input_usd_per_1m_tokens: number; | ||
@@ -35,2 +36,17 @@ cached_input_usd_per_1m_tokens: number; | ||
| } | ||
| export interface ModelTokenUsage { | ||
| model: string; | ||
| input_tokens: number; | ||
| output_tokens: number; | ||
| cached_input_tokens: number; | ||
| reasoning_output_tokens: number; | ||
| } | ||
| export interface ModelCost { | ||
| input_cost: number; | ||
| output_cost: number; | ||
| cached_input_cost: number; | ||
| reasoning_output_cost: number; | ||
| total_cost: number; | ||
| missing_model_id?: string; | ||
| } | ||
| export interface UploadHistoryItem { | ||
@@ -82,2 +98,5 @@ provider: 'codex'; | ||
| export declare const DEFAULT_MODEL_PRICES: ModelPrice[]; | ||
| export declare function isUnknownModel(model: string): boolean; | ||
| export declare function validateModelPrice(price: ModelPrice): ModelPrice; | ||
| export declare function calculateModelCost(usage: ModelTokenUsage, price?: ModelPrice): ModelCost; | ||
| export declare function resolveAgentTelemetryDir(options?: LocalStateOptions): string; | ||
@@ -110,2 +129,17 @@ export declare const configManager: { | ||
| read(options?: LocalStateOptions): Promise<ModelsState | undefined>; | ||
| write(state: ModelsState, options?: LocalStateOptions): Promise<void>; | ||
| list(options?: LocalStateOptions): Promise<ModelsState>; | ||
| set(price: ModelPrice, options?: LocalStateOptions): Promise<{ | ||
| action: string; | ||
| model: ModelPrice; | ||
| state: { | ||
| models: ModelPrice[]; | ||
| }; | ||
| }>; | ||
| remove(model: string, options?: LocalStateOptions): Promise<{ | ||
| model: string; | ||
| state: { | ||
| models: ModelPrice[]; | ||
| }; | ||
| }>; | ||
| }; | ||
@@ -112,0 +146,0 @@ export declare const historyManager: { |
@@ -90,2 +90,74 @@ function buildHealthStatusMessage() { | ||
| ]; | ||
| const MICRO_USD_PER_CENT = 1e4; | ||
| function isModelProvider(value) { | ||
| return value === "openai" || value === "anthropic"; | ||
| } | ||
| function costForTokens(tokens, usdPer1mTokens) { | ||
| return Math.round(tokens * usdPer1mTokens / MICRO_USD_PER_CENT) * MICRO_USD_PER_CENT; | ||
| } | ||
| function zeroCost(missingModelId) { | ||
| return { | ||
| input_cost: 0, | ||
| output_cost: 0, | ||
| cached_input_cost: 0, | ||
| reasoning_output_cost: 0, | ||
| total_cost: 0, | ||
| missing_model_id: missingModelId | ||
| }; | ||
| } | ||
| function assertModelId(model) { | ||
| const normalized = model.trim(); | ||
| if (!normalized) | ||
| throw new Error("Model id is required"); | ||
| return normalized; | ||
| } | ||
| function assertModelProvider(provider) { | ||
| if (!isModelProvider(provider)) | ||
| throw new Error("Provider must be openai or anthropic"); | ||
| return provider; | ||
| } | ||
| function assertNonNegativeFinitePrice(value, key) { | ||
| if (typeof value !== "number" || !Number.isFinite(value) || value < 0) | ||
| throw new Error(`${key} must be a non-negative finite number`); | ||
| return value; | ||
| } | ||
| function isUnknownModel(model) { | ||
| return model.toLowerCase() === "unknown"; | ||
| } | ||
| function validateModelPrice(price) { | ||
| return { | ||
| model: assertModelId(price.model), | ||
| provider: assertModelProvider(price.provider), | ||
| input_usd_per_1m_tokens: assertNonNegativeFinitePrice( | ||
| price.input_usd_per_1m_tokens, | ||
| "input_usd_per_1m_tokens" | ||
| ), | ||
| cached_input_usd_per_1m_tokens: assertNonNegativeFinitePrice( | ||
| price.cached_input_usd_per_1m_tokens, | ||
| "cached_input_usd_per_1m_tokens" | ||
| ), | ||
| output_usd_per_1m_tokens: assertNonNegativeFinitePrice( | ||
| price.output_usd_per_1m_tokens, | ||
| "output_usd_per_1m_tokens" | ||
| ) | ||
| }; | ||
| } | ||
| function calculateModelCost(usage, price) { | ||
| if (isUnknownModel(usage.model)) | ||
| return zeroCost(); | ||
| if (!price) | ||
| return zeroCost(usage.model); | ||
| const uncachedInputTokens = Math.max(usage.input_tokens - usage.cached_input_tokens, 0); | ||
| const inputCost = costForTokens(uncachedInputTokens, price.input_usd_per_1m_tokens); | ||
| const outputCost = costForTokens(usage.output_tokens, price.output_usd_per_1m_tokens); | ||
| const cachedInputCost = costForTokens(usage.cached_input_tokens, price.cached_input_usd_per_1m_tokens); | ||
| const reasoningOutputCost = costForTokens(usage.reasoning_output_tokens, price.output_usd_per_1m_tokens); | ||
| return { | ||
| input_cost: inputCost, | ||
| output_cost: outputCost, | ||
| cached_input_cost: cachedInputCost, | ||
| reasoning_output_cost: reasoningOutputCost, | ||
| total_cost: inputCost + outputCost + cachedInputCost + reasoningOutputCost | ||
| }; | ||
| } | ||
| function resolveHomeDir(options = {}) { | ||
@@ -195,2 +267,35 @@ const homeDir = options.homeDir ?? globalThis.Bun?.env.HOME; | ||
| return await readJsonFile(this.path(options)); | ||
| }, | ||
| async write(state, options) { | ||
| await ensureStateDir(options); | ||
| await writeJsonFile(this.path(options), { | ||
| models: state.models.map(validateModelPrice) | ||
| }); | ||
| }, | ||
| async list(options) { | ||
| return await this.ensureDefaultModels(options); | ||
| }, | ||
| async set(price, options) { | ||
| const normalized = validateModelPrice(price); | ||
| const state = await this.ensureDefaultModels(options); | ||
| const existingIndex = state.models.findIndex((model) => model.model === normalized.model); | ||
| const action = existingIndex === -1 ? "created" : "updated"; | ||
| const nextModels = [...state.models]; | ||
| if (existingIndex === -1) | ||
| nextModels.push(normalized); | ||
| else | ||
| nextModels[existingIndex] = normalized; | ||
| const next = { models: nextModels }; | ||
| await this.write(next, options); | ||
| return { action, model: normalized, state: next }; | ||
| }, | ||
| async remove(model, options) { | ||
| const modelId = assertModelId(model); | ||
| const state = await this.ensureDefaultModels(options); | ||
| const nextModels = state.models.filter((price) => price.model !== modelId); | ||
| if (nextModels.length === state.models.length) | ||
| throw new Error(`Model not found: ${modelId}`); | ||
| const next = { models: nextModels }; | ||
| await this.write(next, options); | ||
| return { model: modelId, state: next }; | ||
| } | ||
@@ -216,8 +321,11 @@ }; | ||
| buildHealthStatusMessage, | ||
| calculateModelCost, | ||
| configManager, | ||
| formatHealthStatus, | ||
| historyManager, | ||
| isUnknownModel, | ||
| modelsManager, | ||
| resolveAgentTelemetryDir | ||
| resolveAgentTelemetryDir, | ||
| validateModelPrice | ||
| }; | ||
| //# sourceMappingURL=index.js.map |
@@ -1,1 +0,1 @@ | ||
| {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["export interface HealthStatusPayload {\n message: string\n time: string\n}\n\nexport function buildHealthStatusMessage() {\n return 'Agent Metry backend'\n}\n\nexport function formatHealthStatus(payload?: HealthStatusPayload) {\n if (!payload) {\n return {\n title: 'Waiting',\n subtitle: 'Waiting for server response',\n }\n }\n\n return {\n title: payload.message,\n subtitle: `Updated at ${payload.time}`,\n }\n}\n\nexport const AGENT_METRY_DIR_NAME = '.agent-metry'\nexport const DEFAULT_BASE_URL = 'http://localhost:3000'\n\nexport interface LocalStateOptions {\n homeDir?: string\n stateDir?: string\n}\n\nexport interface AgentTelemetryConfig {\n base_url?: string\n [key: string]: string | undefined\n}\n\nexport interface AuthState {\n access_token: string\n refresh_token: string\n}\n\nexport interface ModelPrice {\n model: string\n provider: 'openai' | 'anthropic'\n input_usd_per_1m_tokens: number\n cached_input_usd_per_1m_tokens: number\n output_usd_per_1m_tokens: number\n}\n\nexport interface ModelsState {\n models: ModelPrice[]\n}\n\nexport interface UploadHistoryItem {\n provider: 'codex'\n session_id: string\n source_path: string\n file_size: number\n file_mtime_ms: number\n last_uploaded_at: string\n}\n\nexport interface UploadHistoryState {\n items: UploadHistoryItem[]\n}\n\nexport type UploadProvider = 'codex'\n\nexport interface SessionMetricUpload {\n provider?: UploadProvider\n session_id: string\n started_at: string\n ended_at: string\n model: string\n model_provider?: string | null\n reasoning_effort?: string | null\n cli_version?: string | null\n model_context_window?: number | null\n input_tokens?: number\n output_tokens?: number\n cached_input_tokens?: number\n reasoning_output_tokens?: number\n total_tokens?: number\n api_call_count?: number\n conversation_turn_count?: number\n user_message_count?: number\n tool_call_count?: number\n}\n\nexport interface UploadSessionsRequest {\n provider?: UploadProvider\n sessions: SessionMetricUpload[]\n}\n\nexport interface UploadSessionsResponse {\n batch_id: string\n received_count: number\n inserted_count: number\n updated_count: number\n skipped_count: number\n error_count: number\n missing_price_model_ids: string[]\n}\n\nexport const DEFAULT_MODEL_PRICES: ModelPrice[] = [\n {\n model: 'gpt-5.5',\n provider: 'openai',\n input_usd_per_1m_tokens: 5,\n cached_input_usd_per_1m_tokens: 0.5,\n output_usd_per_1m_tokens: 30,\n },\n {\n model: 'gpt-5.4',\n provider: 'openai',\n input_usd_per_1m_tokens: 2.5,\n cached_input_usd_per_1m_tokens: 0.25,\n output_usd_per_1m_tokens: 15,\n },\n {\n model: 'gpt-5.4-mini',\n provider: 'openai',\n input_usd_per_1m_tokens: 0.75,\n cached_input_usd_per_1m_tokens: 0.075,\n output_usd_per_1m_tokens: 4.5,\n },\n {\n model: 'gpt-5.4-nano',\n provider: 'openai',\n input_usd_per_1m_tokens: 0.2,\n cached_input_usd_per_1m_tokens: 0.02,\n output_usd_per_1m_tokens: 1.25,\n },\n {\n model: 'gpt-5.3-codex',\n provider: 'openai',\n input_usd_per_1m_tokens: 1.75,\n cached_input_usd_per_1m_tokens: 0.175,\n output_usd_per_1m_tokens: 14,\n },\n {\n model: 'claude-opus-4-7',\n provider: 'anthropic',\n input_usd_per_1m_tokens: 5,\n cached_input_usd_per_1m_tokens: 0.5,\n output_usd_per_1m_tokens: 25,\n },\n {\n model: 'claude-opus-4-6',\n provider: 'anthropic',\n input_usd_per_1m_tokens: 5,\n cached_input_usd_per_1m_tokens: 0.5,\n output_usd_per_1m_tokens: 25,\n },\n {\n model: 'claude-sonnet-4-6',\n provider: 'anthropic',\n input_usd_per_1m_tokens: 3,\n cached_input_usd_per_1m_tokens: 0.3,\n output_usd_per_1m_tokens: 15,\n },\n {\n model: 'claude-sonnet-4-5',\n provider: 'anthropic',\n input_usd_per_1m_tokens: 3,\n cached_input_usd_per_1m_tokens: 0.3,\n output_usd_per_1m_tokens: 15,\n },\n {\n model: 'claude-haiku-4-5',\n provider: 'anthropic',\n input_usd_per_1m_tokens: 1,\n cached_input_usd_per_1m_tokens: 0.1,\n output_usd_per_1m_tokens: 5,\n },\n]\n\nfunction resolveHomeDir(options: LocalStateOptions = {}) {\n const homeDir = options.homeDir ?? globalThis.Bun?.env.HOME\n if (!homeDir)\n throw new Error('HOME is not set; cannot resolve ~/.agent-metry')\n return homeDir\n}\n\nexport function resolveAgentTelemetryDir(options: LocalStateOptions = {}) {\n return options.stateDir\n ?? globalThis.Bun?.env.AGENT_METRY_HOME\n ?? `${resolveHomeDir(options)}/${AGENT_METRY_DIR_NAME}`\n}\n\nfunction resolveStateFile(fileName: string, options: LocalStateOptions = {}) {\n return `${resolveAgentTelemetryDir(options)}/${fileName}`\n}\n\nasync function ensureStateDir(options: LocalStateOptions = {}) {\n await globalThis.Bun.$`mkdir -p ${resolveAgentTelemetryDir(options)}`.quiet()\n}\n\nasync function readJsonFile<T>(path: string): Promise<T | undefined> {\n const file = globalThis.Bun.file(path)\n if (!(await file.exists()))\n return undefined\n return await file.json() as T\n}\n\nasync function writeJsonFile(path: string, value: unknown) {\n await globalThis.Bun.write(path, `${JSON.stringify(value, null, 2)}\\n`)\n}\n\nfunction assertStringField(value: string | undefined, key: string) {\n if (!value)\n throw new Error(`Missing required config field: ${key}`)\n return value\n}\n\nexport const configManager = {\n path(options?: LocalStateOptions) {\n return resolveStateFile('config.json', options)\n },\n\n async ensureConfig(options?: LocalStateOptions) {\n await ensureStateDir(options)\n const path = this.path(options)\n const existing = await readJsonFile<AgentTelemetryConfig>(path)\n if (existing)\n return existing\n const initial: AgentTelemetryConfig = { base_url: DEFAULT_BASE_URL }\n await writeJsonFile(path, initial)\n return initial\n },\n\n async read(options?: LocalStateOptions) {\n return await readJsonFile<AgentTelemetryConfig>(this.path(options)) ?? {}\n },\n\n async write(config: AgentTelemetryConfig, options?: LocalStateOptions) {\n await ensureStateDir(options)\n await writeJsonFile(this.path(options), config)\n },\n\n async get(key: string, options?: LocalStateOptions) {\n const config = await this.read(options)\n return config[key]\n },\n\n async getRequired(key: string, options?: LocalStateOptions) {\n return assertStringField(await this.get(key, options), key)\n },\n\n async set(key: string, value: string, options?: LocalStateOptions) {\n const config = await this.ensureConfig(options)\n const next = { ...config, [key]: value }\n await this.write(next, options)\n return next\n },\n\n async list(options?: LocalStateOptions) {\n await this.ensureConfig(options)\n return await this.read(options)\n },\n\n async remove(key: string, options?: LocalStateOptions) {\n const config = await this.ensureConfig(options)\n const next = { ...config }\n delete next[key]\n await this.write(next, options)\n return next\n },\n}\n\nexport const authManager = {\n path(options?: LocalStateOptions) {\n return resolveStateFile('auth.json', options)\n },\n\n async read(options?: LocalStateOptions) {\n return await readJsonFile<AuthState>(this.path(options))\n },\n\n async write(auth: AuthState, options?: LocalStateOptions) {\n await ensureStateDir(options)\n await writeJsonFile(this.path(options), auth)\n },\n}\n\nexport const modelsManager = {\n path(options?: LocalStateOptions) {\n return resolveStateFile('models.json', options)\n },\n\n async ensureDefaultModels(options?: LocalStateOptions) {\n await ensureStateDir(options)\n const path = this.path(options)\n const existing = await readJsonFile<ModelsState>(path)\n if (existing)\n return existing\n const initial: ModelsState = { models: DEFAULT_MODEL_PRICES }\n await writeJsonFile(path, initial)\n return initial\n },\n\n async read(options?: LocalStateOptions) {\n return await readJsonFile<ModelsState>(this.path(options))\n },\n}\n\nexport const historyManager = {\n path(options?: LocalStateOptions) {\n return resolveStateFile('history.json', options)\n },\n\n async read(options?: LocalStateOptions) {\n return await readJsonFile<UploadHistoryState>(this.path(options)) ?? { items: [] }\n },\n\n async write(history: UploadHistoryState, options?: LocalStateOptions) {\n await ensureStateDir(options)\n await writeJsonFile(this.path(options), history)\n },\n}\n"],"names":[],"mappings":"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"} | ||
| {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["export interface HealthStatusPayload {\n message: string\n time: string\n}\n\nexport function buildHealthStatusMessage() {\n return 'Agent Metry backend'\n}\n\nexport function formatHealthStatus(payload?: HealthStatusPayload) {\n if (!payload) {\n return {\n title: 'Waiting',\n subtitle: 'Waiting for server response',\n }\n }\n\n return {\n title: payload.message,\n subtitle: `Updated at ${payload.time}`,\n }\n}\n\nexport const AGENT_METRY_DIR_NAME = '.agent-metry'\nexport const DEFAULT_BASE_URL = 'http://localhost:3000'\n\nexport interface LocalStateOptions {\n homeDir?: string\n stateDir?: string\n}\n\nexport interface AgentTelemetryConfig {\n base_url?: string\n [key: string]: string | undefined\n}\n\nexport interface AuthState {\n access_token: string\n refresh_token: string\n}\n\nexport type ModelProvider = 'openai' | 'anthropic'\n\nexport interface ModelPrice {\n model: string\n provider: ModelProvider\n input_usd_per_1m_tokens: number\n cached_input_usd_per_1m_tokens: number\n output_usd_per_1m_tokens: number\n}\n\nexport interface ModelsState {\n models: ModelPrice[]\n}\n\nexport interface ModelTokenUsage {\n model: string\n input_tokens: number\n output_tokens: number\n cached_input_tokens: number\n reasoning_output_tokens: number\n}\n\nexport interface ModelCost {\n input_cost: number\n output_cost: number\n cached_input_cost: number\n reasoning_output_cost: number\n total_cost: number\n missing_model_id?: string\n}\n\nexport interface UploadHistoryItem {\n provider: 'codex'\n session_id: string\n source_path: string\n file_size: number\n file_mtime_ms: number\n last_uploaded_at: string\n}\n\nexport interface UploadHistoryState {\n items: UploadHistoryItem[]\n}\n\nexport type UploadProvider = 'codex'\n\nexport interface SessionMetricUpload {\n provider?: UploadProvider\n session_id: string\n started_at: string\n ended_at: string\n model: string\n model_provider?: string | null\n reasoning_effort?: string | null\n cli_version?: string | null\n model_context_window?: number | null\n input_tokens?: number\n output_tokens?: number\n cached_input_tokens?: number\n reasoning_output_tokens?: number\n total_tokens?: number\n api_call_count?: number\n conversation_turn_count?: number\n user_message_count?: number\n tool_call_count?: number\n}\n\nexport interface UploadSessionsRequest {\n provider?: UploadProvider\n sessions: SessionMetricUpload[]\n}\n\nexport interface UploadSessionsResponse {\n batch_id: string\n received_count: number\n inserted_count: number\n updated_count: number\n skipped_count: number\n error_count: number\n missing_price_model_ids: string[]\n}\n\nexport const DEFAULT_MODEL_PRICES: ModelPrice[] = [\n {\n model: 'gpt-5.5',\n provider: 'openai',\n input_usd_per_1m_tokens: 5,\n cached_input_usd_per_1m_tokens: 0.5,\n output_usd_per_1m_tokens: 30,\n },\n {\n model: 'gpt-5.4',\n provider: 'openai',\n input_usd_per_1m_tokens: 2.5,\n cached_input_usd_per_1m_tokens: 0.25,\n output_usd_per_1m_tokens: 15,\n },\n {\n model: 'gpt-5.4-mini',\n provider: 'openai',\n input_usd_per_1m_tokens: 0.75,\n cached_input_usd_per_1m_tokens: 0.075,\n output_usd_per_1m_tokens: 4.5,\n },\n {\n model: 'gpt-5.4-nano',\n provider: 'openai',\n input_usd_per_1m_tokens: 0.2,\n cached_input_usd_per_1m_tokens: 0.02,\n output_usd_per_1m_tokens: 1.25,\n },\n {\n model: 'gpt-5.3-codex',\n provider: 'openai',\n input_usd_per_1m_tokens: 1.75,\n cached_input_usd_per_1m_tokens: 0.175,\n output_usd_per_1m_tokens: 14,\n },\n {\n model: 'claude-opus-4-7',\n provider: 'anthropic',\n input_usd_per_1m_tokens: 5,\n cached_input_usd_per_1m_tokens: 0.5,\n output_usd_per_1m_tokens: 25,\n },\n {\n model: 'claude-opus-4-6',\n provider: 'anthropic',\n input_usd_per_1m_tokens: 5,\n cached_input_usd_per_1m_tokens: 0.5,\n output_usd_per_1m_tokens: 25,\n },\n {\n model: 'claude-sonnet-4-6',\n provider: 'anthropic',\n input_usd_per_1m_tokens: 3,\n cached_input_usd_per_1m_tokens: 0.3,\n output_usd_per_1m_tokens: 15,\n },\n {\n model: 'claude-sonnet-4-5',\n provider: 'anthropic',\n input_usd_per_1m_tokens: 3,\n cached_input_usd_per_1m_tokens: 0.3,\n output_usd_per_1m_tokens: 15,\n },\n {\n model: 'claude-haiku-4-5',\n provider: 'anthropic',\n input_usd_per_1m_tokens: 1,\n cached_input_usd_per_1m_tokens: 0.1,\n output_usd_per_1m_tokens: 5,\n },\n]\n\nconst MICRO_USD_PER_CENT = 10_000\n\nfunction isModelProvider(value: string): value is ModelProvider {\n return value === 'openai' || value === 'anthropic'\n}\n\nfunction costForTokens(tokens: number, usdPer1mTokens: number) {\n return Math.round((tokens * usdPer1mTokens) / MICRO_USD_PER_CENT) * MICRO_USD_PER_CENT\n}\n\nfunction zeroCost(missingModelId?: string): ModelCost {\n return {\n input_cost: 0,\n output_cost: 0,\n cached_input_cost: 0,\n reasoning_output_cost: 0,\n total_cost: 0,\n missing_model_id: missingModelId,\n }\n}\n\nfunction assertModelId(model: string) {\n const normalized = model.trim()\n if (!normalized)\n throw new Error('Model id is required')\n return normalized\n}\n\nfunction assertModelProvider(provider: string) {\n if (!isModelProvider(provider))\n throw new Error('Provider must be openai or anthropic')\n return provider\n}\n\nfunction assertNonNegativeFinitePrice(value: number, key: string) {\n if (typeof value !== 'number' || !Number.isFinite(value) || value < 0)\n throw new Error(`${key} must be a non-negative finite number`)\n return value\n}\n\nexport function isUnknownModel(model: string) {\n return model.toLowerCase() === 'unknown'\n}\n\nexport function validateModelPrice(price: ModelPrice): ModelPrice {\n return {\n model: assertModelId(price.model),\n provider: assertModelProvider(price.provider),\n input_usd_per_1m_tokens: assertNonNegativeFinitePrice(\n price.input_usd_per_1m_tokens,\n 'input_usd_per_1m_tokens',\n ),\n cached_input_usd_per_1m_tokens: assertNonNegativeFinitePrice(\n price.cached_input_usd_per_1m_tokens,\n 'cached_input_usd_per_1m_tokens',\n ),\n output_usd_per_1m_tokens: assertNonNegativeFinitePrice(\n price.output_usd_per_1m_tokens,\n 'output_usd_per_1m_tokens',\n ),\n }\n}\n\nexport function calculateModelCost(usage: ModelTokenUsage, price?: ModelPrice): ModelCost {\n if (isUnknownModel(usage.model))\n return zeroCost()\n\n if (!price)\n return zeroCost(usage.model)\n\n const uncachedInputTokens = Math.max(usage.input_tokens - usage.cached_input_tokens, 0)\n const inputCost = costForTokens(uncachedInputTokens, price.input_usd_per_1m_tokens)\n const outputCost = costForTokens(usage.output_tokens, price.output_usd_per_1m_tokens)\n const cachedInputCost = costForTokens(usage.cached_input_tokens, price.cached_input_usd_per_1m_tokens)\n const reasoningOutputCost = costForTokens(usage.reasoning_output_tokens, price.output_usd_per_1m_tokens)\n\n return {\n input_cost: inputCost,\n output_cost: outputCost,\n cached_input_cost: cachedInputCost,\n reasoning_output_cost: reasoningOutputCost,\n total_cost: inputCost + outputCost + cachedInputCost + reasoningOutputCost,\n }\n}\n\nfunction resolveHomeDir(options: LocalStateOptions = {}) {\n const homeDir = options.homeDir ?? globalThis.Bun?.env.HOME\n if (!homeDir)\n throw new Error('HOME is not set; cannot resolve ~/.agent-metry')\n return homeDir\n}\n\nexport function resolveAgentTelemetryDir(options: LocalStateOptions = {}) {\n return options.stateDir\n ?? globalThis.Bun?.env.AGENT_METRY_HOME\n ?? `${resolveHomeDir(options)}/${AGENT_METRY_DIR_NAME}`\n}\n\nfunction resolveStateFile(fileName: string, options: LocalStateOptions = {}) {\n return `${resolveAgentTelemetryDir(options)}/${fileName}`\n}\n\nasync function ensureStateDir(options: LocalStateOptions = {}) {\n await globalThis.Bun.$`mkdir -p ${resolveAgentTelemetryDir(options)}`.quiet()\n}\n\nasync function readJsonFile<T>(path: string): Promise<T | undefined> {\n const file = globalThis.Bun.file(path)\n if (!(await file.exists()))\n return undefined\n return await file.json() as T\n}\n\nasync function writeJsonFile(path: string, value: unknown) {\n await globalThis.Bun.write(path, `${JSON.stringify(value, null, 2)}\\n`)\n}\n\nfunction assertStringField(value: string | undefined, key: string) {\n if (!value)\n throw new Error(`Missing required config field: ${key}`)\n return value\n}\n\nexport const configManager = {\n path(options?: LocalStateOptions) {\n return resolveStateFile('config.json', options)\n },\n\n async ensureConfig(options?: LocalStateOptions) {\n await ensureStateDir(options)\n const path = this.path(options)\n const existing = await readJsonFile<AgentTelemetryConfig>(path)\n if (existing)\n return existing\n const initial: AgentTelemetryConfig = { base_url: DEFAULT_BASE_URL }\n await writeJsonFile(path, initial)\n return initial\n },\n\n async read(options?: LocalStateOptions) {\n return await readJsonFile<AgentTelemetryConfig>(this.path(options)) ?? {}\n },\n\n async write(config: AgentTelemetryConfig, options?: LocalStateOptions) {\n await ensureStateDir(options)\n await writeJsonFile(this.path(options), config)\n },\n\n async get(key: string, options?: LocalStateOptions) {\n const config = await this.read(options)\n return config[key]\n },\n\n async getRequired(key: string, options?: LocalStateOptions) {\n return assertStringField(await this.get(key, options), key)\n },\n\n async set(key: string, value: string, options?: LocalStateOptions) {\n const config = await this.ensureConfig(options)\n const next = { ...config, [key]: value }\n await this.write(next, options)\n return next\n },\n\n async list(options?: LocalStateOptions) {\n await this.ensureConfig(options)\n return await this.read(options)\n },\n\n async remove(key: string, options?: LocalStateOptions) {\n const config = await this.ensureConfig(options)\n const next = { ...config }\n delete next[key]\n await this.write(next, options)\n return next\n },\n}\n\nexport const authManager = {\n path(options?: LocalStateOptions) {\n return resolveStateFile('auth.json', options)\n },\n\n async read(options?: LocalStateOptions) {\n return await readJsonFile<AuthState>(this.path(options))\n },\n\n async write(auth: AuthState, options?: LocalStateOptions) {\n await ensureStateDir(options)\n await writeJsonFile(this.path(options), auth)\n },\n}\n\nexport const modelsManager = {\n path(options?: LocalStateOptions) {\n return resolveStateFile('models.json', options)\n },\n\n async ensureDefaultModels(options?: LocalStateOptions) {\n await ensureStateDir(options)\n const path = this.path(options)\n const existing = await readJsonFile<ModelsState>(path)\n if (existing)\n return existing\n const initial: ModelsState = { models: DEFAULT_MODEL_PRICES }\n await writeJsonFile(path, initial)\n return initial\n },\n\n async read(options?: LocalStateOptions) {\n return await readJsonFile<ModelsState>(this.path(options))\n },\n\n async write(state: ModelsState, options?: LocalStateOptions) {\n await ensureStateDir(options)\n await writeJsonFile(this.path(options), {\n models: state.models.map(validateModelPrice),\n })\n },\n\n async list(options?: LocalStateOptions) {\n return await this.ensureDefaultModels(options)\n },\n\n async set(price: ModelPrice, options?: LocalStateOptions) {\n const normalized = validateModelPrice(price)\n const state = await this.ensureDefaultModels(options)\n const existingIndex = state.models.findIndex(model => model.model === normalized.model)\n const action = existingIndex === -1 ? 'created' : 'updated'\n const nextModels = [...state.models]\n\n if (existingIndex === -1)\n nextModels.push(normalized)\n else\n nextModels[existingIndex] = normalized\n\n const next = { models: nextModels }\n await this.write(next, options)\n return { action, model: normalized, state: next }\n },\n\n async remove(model: string, options?: LocalStateOptions) {\n const modelId = assertModelId(model)\n const state = await this.ensureDefaultModels(options)\n const nextModels = state.models.filter(price => price.model !== modelId)\n if (nextModels.length === state.models.length)\n throw new Error(`Model not found: ${modelId}`)\n\n const next = { models: nextModels }\n await this.write(next, options)\n return { model: modelId, state: next }\n },\n}\n\nexport const historyManager = {\n path(options?: LocalStateOptions) {\n return resolveStateFile('history.json', options)\n },\n\n async read(options?: LocalStateOptions) {\n return await readJsonFile<UploadHistoryState>(this.path(options)) ?? { items: [] }\n },\n\n async write(history: UploadHistoryState, options?: LocalStateOptions) {\n await ensureStateDir(options)\n await writeJsonFile(this.path(options), history)\n },\n}\n"],"names":[],"mappings":"AAKO,SAAS,2BAA2B;AACzC,SAAO;AACT;AAEO,SAAS,mBAAmB,SAA+B;AAChE,MAAI,CAAC,SAAS;AACZ,WAAO;AAAA,MACL,OAAO;AAAA,MACP,UAAU;AAAA,IAAA;AAAA,EAEd;AAEA,SAAO;AAAA,IACL,OAAO,QAAQ;AAAA,IACf,UAAU,cAAc,QAAQ,IAAI;AAAA,EAAA;AAExC;AAEO,MAAM,uBAAuB;AAC7B,MAAM,mBAAmB;AAmGzB,MAAM,uBAAqC;AAAA,EAChD;AAAA,IACE,OAAO;AAAA,IACP,UAAU;AAAA,IACV,yBAAyB;AAAA,IACzB,gCAAgC;AAAA,IAChC,0BAA0B;AAAA,EAAA;AAAA,EAE5B;AAAA,IACE,OAAO;AAAA,IACP,UAAU;AAAA,IACV,yBAAyB;AAAA,IACzB,gCAAgC;AAAA,IAChC,0BAA0B;AAAA,EAAA;AAAA,EAE5B;AAAA,IACE,OAAO;AAAA,IACP,UAAU;AAAA,IACV,yBAAyB;AAAA,IACzB,gCAAgC;AAAA,IAChC,0BAA0B;AAAA,EAAA;AAAA,EAE5B;AAAA,IACE,OAAO;AAAA,IACP,UAAU;AAAA,IACV,yBAAyB;AAAA,IACzB,gCAAgC;AAAA,IAChC,0BAA0B;AAAA,EAAA;AAAA,EAE5B;AAAA,IACE,OAAO;AAAA,IACP,UAAU;AAAA,IACV,yBAAyB;AAAA,IACzB,gCAAgC;AAAA,IAChC,0BAA0B;AAAA,EAAA;AAAA,EAE5B;AAAA,IACE,OAAO;AAAA,IACP,UAAU;AAAA,IACV,yBAAyB;AAAA,IACzB,gCAAgC;AAAA,IAChC,0BAA0B;AAAA,EAAA;AAAA,EAE5B;AAAA,IACE,OAAO;AAAA,IACP,UAAU;AAAA,IACV,yBAAyB;AAAA,IACzB,gCAAgC;AAAA,IAChC,0BAA0B;AAAA,EAAA;AAAA,EAE5B;AAAA,IACE,OAAO;AAAA,IACP,UAAU;AAAA,IACV,yBAAyB;AAAA,IACzB,gCAAgC;AAAA,IAChC,0BAA0B;AAAA,EAAA;AAAA,EAE5B;AAAA,IACE,OAAO;AAAA,IACP,UAAU;AAAA,IACV,yBAAyB;AAAA,IACzB,gCAAgC;AAAA,IAChC,0BAA0B;AAAA,EAAA;AAAA,EAE5B;AAAA,IACE,OAAO;AAAA,IACP,UAAU;AAAA,IACV,yBAAyB;AAAA,IACzB,gCAAgC;AAAA,IAChC,0BAA0B;AAAA,EAAA;AAE9B;AAEA,MAAM,qBAAqB;AAE3B,SAAS,gBAAgB,OAAuC;AAC9D,SAAO,UAAU,YAAY,UAAU;AACzC;AAEA,SAAS,cAAc,QAAgB,gBAAwB;AAC7D,SAAO,KAAK,MAAO,SAAS,iBAAkB,kBAAkB,IAAI;AACtE;AAEA,SAAS,SAAS,gBAAoC;AACpD,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,mBAAmB;AAAA,IACnB,uBAAuB;AAAA,IACvB,YAAY;AAAA,IACZ,kBAAkB;AAAA,EAAA;AAEtB;AAEA,SAAS,cAAc,OAAe;AACpC,QAAM,aAAa,MAAM,KAAA;AACzB,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,sBAAsB;AACxC,SAAO;AACT;AAEA,SAAS,oBAAoB,UAAkB;AAC7C,MAAI,CAAC,gBAAgB,QAAQ;AAC3B,UAAM,IAAI,MAAM,sCAAsC;AACxD,SAAO;AACT;AAEA,SAAS,6BAA6B,OAAe,KAAa;AAChE,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ;AAClE,UAAM,IAAI,MAAM,GAAG,GAAG,uCAAuC;AAC/D,SAAO;AACT;AAEO,SAAS,eAAe,OAAe;AAC5C,SAAO,MAAM,kBAAkB;AACjC;AAEO,SAAS,mBAAmB,OAA+B;AAChE,SAAO;AAAA,IACL,OAAO,cAAc,MAAM,KAAK;AAAA,IAChC,UAAU,oBAAoB,MAAM,QAAQ;AAAA,IAC5C,yBAAyB;AAAA,MACvB,MAAM;AAAA,MACN;AAAA,IAAA;AAAA,IAEF,gCAAgC;AAAA,MAC9B,MAAM;AAAA,MACN;AAAA,IAAA;AAAA,IAEF,0BAA0B;AAAA,MACxB,MAAM;AAAA,MACN;AAAA,IAAA;AAAA,EACF;AAEJ;AAEO,SAAS,mBAAmB,OAAwB,OAA+B;AACxF,MAAI,eAAe,MAAM,KAAK;AAC5B,WAAO,SAAA;AAET,MAAI,CAAC;AACH,WAAO,SAAS,MAAM,KAAK;AAE7B,QAAM,sBAAsB,KAAK,IAAI,MAAM,eAAe,MAAM,qBAAqB,CAAC;AACtF,QAAM,YAAY,cAAc,qBAAqB,MAAM,uBAAuB;AAClF,QAAM,aAAa,cAAc,MAAM,eAAe,MAAM,wBAAwB;AACpF,QAAM,kBAAkB,cAAc,MAAM,qBAAqB,MAAM,8BAA8B;AACrG,QAAM,sBAAsB,cAAc,MAAM,yBAAyB,MAAM,wBAAwB;AAEvG,SAAO;AAAA,IACL,YAAY;AAAA,IACZ,aAAa;AAAA,IACb,mBAAmB;AAAA,IACnB,uBAAuB;AAAA,IACvB,YAAY,YAAY,aAAa,kBAAkB;AAAA,EAAA;AAE3D;AAEA,SAAS,eAAe,UAA6B,IAAI;AACvD,QAAM,UAAU,QAAQ,WAAW,WAAW,KAAK,IAAI;AACvD,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,gDAAgD;AAClE,SAAO;AACT;AAEO,SAAS,yBAAyB,UAA6B,IAAI;AACxE,SAAO,QAAQ,YACV,WAAW,KAAK,IAAI,oBACpB,GAAG,eAAe,OAAO,CAAC,IAAI,oBAAoB;AACzD;AAEA,SAAS,iBAAiB,UAAkB,UAA6B,IAAI;AAC3E,SAAO,GAAG,yBAAyB,OAAO,CAAC,IAAI,QAAQ;AACzD;AAEA,eAAe,eAAe,UAA6B,IAAI;AAC7D,QAAM,WAAW,IAAI,aAAa,yBAAyB,OAAO,CAAC,GAAG,MAAA;AACxE;AAEA,eAAe,aAAgB,MAAsC;AACnE,QAAM,OAAO,WAAW,IAAI,KAAK,IAAI;AACrC,MAAI,CAAE,MAAM,KAAK,OAAA;AACf,WAAO;AACT,SAAO,MAAM,KAAK,KAAA;AACpB;AAEA,eAAe,cAAc,MAAc,OAAgB;AACzD,QAAM,WAAW,IAAI,MAAM,MAAM,GAAG,KAAK,UAAU,OAAO,MAAM,CAAC,CAAC;AAAA,CAAI;AACxE;AAEA,SAAS,kBAAkB,OAA2B,KAAa;AACjE,MAAI,CAAC;AACH,UAAM,IAAI,MAAM,kCAAkC,GAAG,EAAE;AACzD,SAAO;AACT;AAEO,MAAM,gBAAgB;AAAA,EAC3B,KAAK,SAA6B;AAChC,WAAO,iBAAiB,eAAe,OAAO;AAAA,EAChD;AAAA,EAEA,MAAM,aAAa,SAA6B;AAC9C,UAAM,eAAe,OAAO;AAC5B,UAAM,OAAO,KAAK,KAAK,OAAO;AAC9B,UAAM,WAAW,MAAM,aAAmC,IAAI;AAC9D,QAAI;AACF,aAAO;AACT,UAAM,UAAgC,EAAE,UAAU,iBAAA;AAClD,UAAM,cAAc,MAAM,OAAO;AACjC,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,KAAK,SAA6B;AACtC,WAAO,MAAM,aAAmC,KAAK,KAAK,OAAO,CAAC,KAAK,CAAA;AAAA,EACzE;AAAA,EAEA,MAAM,MAAM,QAA8B,SAA6B;AACrE,UAAM,eAAe,OAAO;AAC5B,UAAM,cAAc,KAAK,KAAK,OAAO,GAAG,MAAM;AAAA,EAChD;AAAA,EAEA,MAAM,IAAI,KAAa,SAA6B;AAClD,UAAM,SAAS,MAAM,KAAK,KAAK,OAAO;AACtC,WAAO,OAAO,GAAG;AAAA,EACnB;AAAA,EAEA,MAAM,YAAY,KAAa,SAA6B;AAC1D,WAAO,kBAAkB,MAAM,KAAK,IAAI,KAAK,OAAO,GAAG,GAAG;AAAA,EAC5D;AAAA,EAEA,MAAM,IAAI,KAAa,OAAe,SAA6B;AACjE,UAAM,SAAS,MAAM,KAAK,aAAa,OAAO;AAC9C,UAAM,OAAO,EAAE,GAAG,QAAQ,CAAC,GAAG,GAAG,MAAA;AACjC,UAAM,KAAK,MAAM,MAAM,OAAO;AAC9B,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,KAAK,SAA6B;AACtC,UAAM,KAAK,aAAa,OAAO;AAC/B,WAAO,MAAM,KAAK,KAAK,OAAO;AAAA,EAChC;AAAA,EAEA,MAAM,OAAO,KAAa,SAA6B;AACrD,UAAM,SAAS,MAAM,KAAK,aAAa,OAAO;AAC9C,UAAM,OAAO,EAAE,GAAG,OAAA;AAClB,WAAO,KAAK,GAAG;AACf,UAAM,KAAK,MAAM,MAAM,OAAO;AAC9B,WAAO;AAAA,EACT;AACF;AAEO,MAAM,cAAc;AAAA,EACzB,KAAK,SAA6B;AAChC,WAAO,iBAAiB,aAAa,OAAO;AAAA,EAC9C;AAAA,EAEA,MAAM,KAAK,SAA6B;AACtC,WAAO,MAAM,aAAwB,KAAK,KAAK,OAAO,CAAC;AAAA,EACzD;AAAA,EAEA,MAAM,MAAM,MAAiB,SAA6B;AACxD,UAAM,eAAe,OAAO;AAC5B,UAAM,cAAc,KAAK,KAAK,OAAO,GAAG,IAAI;AAAA,EAC9C;AACF;AAEO,MAAM,gBAAgB;AAAA,EAC3B,KAAK,SAA6B;AAChC,WAAO,iBAAiB,eAAe,OAAO;AAAA,EAChD;AAAA,EAEA,MAAM,oBAAoB,SAA6B;AACrD,UAAM,eAAe,OAAO;AAC5B,UAAM,OAAO,KAAK,KAAK,OAAO;AAC9B,UAAM,WAAW,MAAM,aAA0B,IAAI;AACrD,QAAI;AACF,aAAO;AACT,UAAM,UAAuB,EAAE,QAAQ,qBAAA;AACvC,UAAM,cAAc,MAAM,OAAO;AACjC,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,KAAK,SAA6B;AACtC,WAAO,MAAM,aAA0B,KAAK,KAAK,OAAO,CAAC;AAAA,EAC3D;AAAA,EAEA,MAAM,MAAM,OAAoB,SAA6B;AAC3D,UAAM,eAAe,OAAO;AAC5B,UAAM,cAAc,KAAK,KAAK,OAAO,GAAG;AAAA,MACtC,QAAQ,MAAM,OAAO,IAAI,kBAAkB;AAAA,IAAA,CAC5C;AAAA,EACH;AAAA,EAEA,MAAM,KAAK,SAA6B;AACtC,WAAO,MAAM,KAAK,oBAAoB,OAAO;AAAA,EAC/C;AAAA,EAEA,MAAM,IAAI,OAAmB,SAA6B;AACxD,UAAM,aAAa,mBAAmB,KAAK;AAC3C,UAAM,QAAQ,MAAM,KAAK,oBAAoB,OAAO;AACpD,UAAM,gBAAgB,MAAM,OAAO,UAAU,WAAS,MAAM,UAAU,WAAW,KAAK;AACtF,UAAM,SAAS,kBAAkB,KAAK,YAAY;AAClD,UAAM,aAAa,CAAC,GAAG,MAAM,MAAM;AAEnC,QAAI,kBAAkB;AACpB,iBAAW,KAAK,UAAU;AAAA;AAE1B,iBAAW,aAAa,IAAI;AAE9B,UAAM,OAAO,EAAE,QAAQ,WAAA;AACvB,UAAM,KAAK,MAAM,MAAM,OAAO;AAC9B,WAAO,EAAE,QAAQ,OAAO,YAAY,OAAO,KAAA;AAAA,EAC7C;AAAA,EAEA,MAAM,OAAO,OAAe,SAA6B;AACvD,UAAM,UAAU,cAAc,KAAK;AACnC,UAAM,QAAQ,MAAM,KAAK,oBAAoB,OAAO;AACpD,UAAM,aAAa,MAAM,OAAO,OAAO,CAAA,UAAS,MAAM,UAAU,OAAO;AACvE,QAAI,WAAW,WAAW,MAAM,OAAO;AACrC,YAAM,IAAI,MAAM,oBAAoB,OAAO,EAAE;AAE/C,UAAM,OAAO,EAAE,QAAQ,WAAA;AACvB,UAAM,KAAK,MAAM,MAAM,OAAO;AAC9B,WAAO,EAAE,OAAO,SAAS,OAAO,KAAA;AAAA,EAClC;AACF;AAEO,MAAM,iBAAiB;AAAA,EAC5B,KAAK,SAA6B;AAChC,WAAO,iBAAiB,gBAAgB,OAAO;AAAA,EACjD;AAAA,EAEA,MAAM,KAAK,SAA6B;AACtC,WAAO,MAAM,aAAiC,KAAK,KAAK,OAAO,CAAC,KAAK,EAAE,OAAO,GAAC;AAAA,EACjF;AAAA,EAEA,MAAM,MAAM,SAA6B,SAA6B;AACpE,UAAM,eAAe,OAAO;AAC5B,UAAM,cAAc,KAAK,KAAK,OAAO,GAAG,OAAO;AAAA,EACjD;AACF;"} |
+145
-17
| # Agent Metry | ||
| This repo contains a CLI, frontend, and backend integrated in a Bun workspace. | ||
| Agent Metry 是一个自部署的 agent 消耗记录应用。用户通过 CLI 主动上传本机 Codex session 的非内容型聚合数据,并在 Web 控制台查看个人使用概览、用户排行榜和个人资料。 | ||
| ## Packages | ||
| V1 只支持 Codex。上传粒度是 session 级聚合记录,包含模型、时间、token、API 调用次数、对话轮数、用户消息数、工具调用数和后端计算出的费用。系统不上传 prompt、response、reasoning、命令输出、项目路径、文件路径、git 信息或原始 JSONL。 | ||
| - `packages/cli`: `@agent-metry/cli` (private, internal) | ||
| - `packages/frontend`: `@agent-metry/frontend` (private, internal) | ||
| - `packages/backend`: `@agent-metry/backend` (private, internal) | ||
| - `packages/core`: `@agent-metry/core` (private, internal) | ||
| ## 产品组成 | ||
| Published package: `agent-metry` | ||
| - `agent-metry` CLI:启动自部署服务、管理本地配置、创建用户、登录并上传 Codex session 聚合数据。 | ||
| - Hono 后端:提供鉴权、用户、上传、统计、排行榜和个人资料接口。 | ||
| - React Web 控制台:提供登录页、信息页、排行榜和个人页。 | ||
| - SQLite 本地数据库:V1 使用 `~/.agent-metry/data.db` 保存用户和聚合指标。 | ||
| ## Quick start | ||
| 核心本地目录是 `~/.agent-metry`: | ||
| - Install deps: `bun install` | ||
| - Build all: `bun run build` | ||
| - Start bundled server: `bun run --filter @agent-metry/cli start -- server --port 3000` | ||
| - `config.json`:CLI 配置,默认 `base_url` 为 `http://localhost:3000`。 | ||
| - `auth.json`:CLI 登录后保存的访问凭据。 | ||
| - `data.db`:服务端 SQLite 数据库,首次运行 `server` 时创建。 | ||
| - `history.json`:已成功上传的 Codex session 文件状态,用于增量上传。 | ||
| - `models.json`:模型价格表,首次运行 `server` 时写入默认值。 | ||
| `bun run build` builds frontend and backend and bundles them into the CLI package output. | ||
| ## 用法与核心链路 | ||
| ## Release | ||
| ### 1. 安装依赖并构建 | ||
| - Publish current package: `npm publish` | ||
| - Bump, publish, tag, and push: `bun run release` | ||
| - Non-interactive patch release: `bun run release:patch` | ||
| ```bash | ||
| bun install | ||
| bun run build | ||
| ``` | ||
| The release workflow uses `bumpp` to keep the published package version and CLI `--version` in sync. | ||
| `bun run build` 会构建前端、后端和 CLI,并把 Web 资源打包进 CLI 输出目录。 | ||
| ### 2. 启动自部署服务 | ||
| 源码仓库内运行: | ||
| ```bash | ||
| bun run --filter @agent-metry/cli start -- server --port 3000 | ||
| ``` | ||
| 构建后或安装发布包后,正式 CLI 命令为: | ||
| ```bash | ||
| agent-metry server --port 3000 | ||
| ``` | ||
| 服务启动后访问 `http://localhost:3000` 打开 Web 控制台。后端健康检查接口为: | ||
| ```bash | ||
| curl http://localhost:3000/api/health | ||
| ``` | ||
| ### 3. 配置 CLI 服务地址 | ||
| 默认服务地址是 `http://localhost:3000`。如果服务运行在其他地址,先更新 `base_url`: | ||
| ```bash | ||
| agent-metry config set base_url http://localhost:3000 | ||
| agent-metry config get base_url | ||
| agent-metry config list | ||
| agent-metry config remove base_url | ||
| ``` | ||
| 源码仓库内调试时,把 `agent-metry` 替换为: | ||
| ```bash | ||
| bun run --filter @agent-metry/cli start -- | ||
| ``` | ||
| 例如: | ||
| ```bash | ||
| bun run --filter @agent-metry/cli start -- config list | ||
| ``` | ||
| ### 4. 创建用户并登录 CLI | ||
| CLI 创建用户: | ||
| ```bash | ||
| agent-metry user create user@example.com your-password | ||
| ``` | ||
| 登录并保存后续上传使用的 token: | ||
| ```bash | ||
| agent-metry login user@example.com your-password | ||
| ``` | ||
| Web 控制台也支持邮箱密码登录和注册。注册成功后会自动登录,默认昵称等于邮箱。 | ||
| ### 5. 上传 Codex session 聚合数据 | ||
| ```bash | ||
| agent-metry upload | ||
| ``` | ||
| 上传默认扫描: | ||
| - `~/.codex/sessions/**/*.jsonl` | ||
| - `~/.codex/archived_sessions/*.jsonl` | ||
| CLI 会根据 `history.json` 做增量上传:新 session 会上传;文件大小或修改时间变化的 session 会重新解析并上传;未变化的 session 会跳过。本地删除历史 session 不会删除服务端已有数据。 | ||
| ### 6. 查看 Web 控制台 | ||
| - `/login`:登录和注册。 | ||
| - `/`:信息页,支持 `1d / 7d / 30d / total` 时间范围。 | ||
| - `/leaderboard`:排行榜,支持费用和 Token 两类排行。 | ||
| - `/profile`:个人页,支持查看个人信息和编辑昵称。 | ||
| 如果上传数据中的模型没有价格配置,上传和统计不会失败;前端会提示管理员补充 `~/.agent-metry/models.json` 中对应模型 id 的价格。 | ||
| ## 贡献与仓库开发引导 | ||
| 本仓库使用 Bun workspace,统一 TypeScript + ESM。 | ||
| ```text | ||
| packages/cli CLI 入口、命令实现和 Web 打包脚本 | ||
| packages/backend Hono API、鉴权、SQLite 初始化和统计逻辑 | ||
| packages/frontend Vite + React + TanStack Router Web 控制台 | ||
| packages/core 共享类型、本地状态管理、Codex session 解析和上传规划 | ||
| docs/PRD.md 产品需求文档 | ||
| docs/TECH.md 技术设计文档 | ||
| ``` | ||
| 开发常用命令: | ||
| ```bash | ||
| bun install | ||
| bun run build | ||
| bun run typecheck | ||
| bun run --filter @agent-metry/backend dev | ||
| bun run --filter @agent-metry/frontend dev | ||
| bun run --filter @agent-metry/cli start -- server --port 3000 | ||
| ``` | ||
| 涉及产品边界再读 `docs/PRD.md`;涉及 Codex 数据解析、数据库、费用计算、鉴权或隐私约束再读 `docs/TECH.md`。 | ||
| 开发约定: | ||
| - 只实现当前任务需要的最小改动,避免保留无用兼容层或新增未要求的抽象。 | ||
| - 后端接口改动使用 `curl` 验收,至少覆盖健康检查、用户创建/登录、上传、统计、排行榜和昵称更新中受影响的链路。 | ||
| - 前端页面改动使用 agent-browser 做视觉验收,截图保存到系统 `/tmp` 目录。 | ||
| - 前端遵循现有 ESLint / Prettier 配置;组件使用 PascalCase,hooks 使用 `useX`,`@/` 指向 `packages/frontend/src`。 | ||
| - 提交信息延续 Conventional Commits,例如 `feat:`、`fix:`、`chore:`。 | ||
| 发布相关命令: | ||
| ```bash | ||
| npm publish | ||
| bun run release | ||
| bun run release:patch | ||
| ``` | ||
| 发布流程使用 `bumpp` 保持包版本和 CLI `--version` 同步。 |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
7988938
0.38%54670
0.52%157
441.38%