Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

agent-metry

Package Overview
Dependencies
Maintainers
1
Versions
3
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

agent-metry - npm Package Compare versions

Comparing version
0.1.2
to
0.1.3
+1
-1
package.json
{
"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",

#!/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":"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"}
+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