Skill: imgolye/agent-usage-tracker (version 0.1.0)

--- name: agent-usage-tracker description: Track AI agent token usage, model costs, and budget thresholds with a TypeScript and SQLite workflow. Use when the user wants to instrument agent runs, calculate token spend, enforce per-session or time-window budgets, or analyze usage by model, session, or time range. --- # Agent Usage Tracker Use this skill when you need local token accounting for AI agents. ## What it provides - Real-time token usage persistence in SQLite - Cost calculation based on per-model pricing - Budget thresholds with persisted warning and blocking states - Usage statistics grouped by time window, session, or model ## Files to use - `src/UsageTracker.ts`: ingestion, storage, and reporting - `src/CostCalculator.ts`: pricing catalog and cost math - `src/BudgetManager.ts`: budget policy evaluation - `examples/basic-usage.ts`: end-to-end usage example - `tests/`: reference behavior for tracking, budgeting, and aggregation ## Recommended workflow 1. Instantiate `UsageTracker` with a SQLite path or `:memory:` for tests. 2. Register model pricing with `CostCalculator`. 3. Record each agent interaction with prompt tokens, completion tokens, session id, and timestamp. 4. Save reusable budgets with `BudgetManager.setBudget`, then call `evaluateBudget` before or after new work to warn or stop when a limit is exceeded. 5. Use `UsageTracker.getUsageSummary` or `getTimeSeries` for reporting. ## Integration notes - Store token counts at the moment the provider returns usage metadata. - Keep model ids normalized. Pricing lookup is exact by model id. - Use `metadata` for provider-specific fields such as request id or tool name. - For sliding-window budgets, query usage by `startTime` and `endTime` before dispatching new work. ## Output expectations This skill ships as a local Node.js package with tests and examples. Extend `CostCalculator` if your provider pricing changes.

path: README.md
content: # agent-usage-tracker

`agent-usage-tracker` is an OpenClaw skill package for tracking AI agent token usage, computing spend, enforcing budgets, and generating usage analytics from SQLite-backed history.

## Features

- Persist prompt, completion, and total token usage
- Calculate per-request and aggregate cost from model pricing
- Persist budget policies with warning and blocking thresholds
- Summarize usage by model, session, and time range
- Generate daily time-series data for reporting

## Project layout

- `SKILL.md`: skill trigger metadata and operating guidance
- `src/`: implementation
- `tests/`: Vitest coverage
- `examples/`: runnable sample

## Install

```bash
npm install
```

## Build

```bash
npm run build
```

## Test

```bash
npm test
```

## Example

```bash
npm run example
```

## Usage

```ts
import { BudgetManager, CostCalculator, UsageTracker } from "./src/index.js";

const pricing = new CostCalculator({
  "gpt-4.1-mini": { inputCostPerMillion: 0.4, outputCostPerMillion: 1.6 }
});

const tracker = new UsageTracker({
  dbPath: "usage.db",
  costCalculator: pricing
});

await tracker.recordUsage({
  sessionId: "session-1",
  model: "gpt-4.1-mini",
  promptTokens: 1200,
  completionTokens: 300
});

const budgetManager = new BudgetManager(tracker);
await budgetManager.setBudget({
  name: "daily-dev-budget",
  limitUsd: 5,
  warningThreshold: 0.8
});
const result = await budgetManager.evaluateBudget("daily-dev-budget");

console.log(result.status);
```


path: examples/basic-usage.ts
content: import { BudgetManager, CostCalculator, UsageTracker } from "../src/index.js";

async function main() {
  const tracker = new UsageTracker({
    dbPath: ":memory:",
    costCalculator: new CostCalculator({
      "gpt-4.1-mini": {
        inputCostPerMillion: 0.4,
        outputCostPerMillion: 1.6
      },
      "gpt-4.1": {
        inputCostPerMillion: 2,
        outputCostPerMillion: 8
      }
    })
  });

  await tracker.recordUsage({
    sessionId: "demo-session",
    model: "gpt-4.1-mini",
    promptTokens: 3000,
    completionTokens: 900,
    metadata: { tool: "summarize" }
  });

  await tracker.recordUsage({
    sessionId: "demo-session",
    model: "gpt-4.1",
    promptTokens: 1200,
    completionTokens: 600,
    metadata: { tool: "analyze" }
  });

  const summary = await tracker.getUsageSummary({ sessionId: "demo-session" });
  const budgetManager = new BudgetManager(tracker);
  await budgetManager.setBudget({
    name: "demo-budget",
    limitUsd: 0.05,
    warningThreshold: 0.6,
    sessionId: "demo-session"
  });
  const budget = await budgetManager.evaluateBudget("demo-budget");

  console.log("Usage summary:", JSON.stringify(summary, null, 2));
  console.log("Budget status:", JSON.stringify(budget, null, 2));

  tracker.close();
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});


path: package.json
content: {
  "name": "agent-usage-tracker",
  "version": "0.1.0",
  "description": "OpenClaw skill for tracking AI agent token usage, costs, and budgets.",
  "type": "module",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "scripts": {
    "build": "tsc -p tsconfig.json",
    "test": "vitest run",
    "test:watch": "vitest",
    "example": "tsx examples/basic-usage.ts"
  },
  "keywords": [
    "openclaw",
    "skill",
    "token",
    "usage",
    "sqlite"
  ],
  "license": "MIT",
  "dependencies": {
    "better-sqlite3": "^11.8.1"
  },
  "devDependencies": {
    "@types/better-sqlite3": "^7.6.13",
    "@types/node": "^24.0.0",
    "tsx": "^4.19.3",
    "typescript": "^5.8.2",
    "vitest": "^3.0.8"
  }
}


path: src/BudgetManager.ts
content: import { BudgetEvaluation, BudgetPolicy, StoredBudgetPolicy } from "./types.js";
import { UsageTracker } from "./UsageTracker.js";

export class BudgetManager {
  constructor(private readonly usageTracker: UsageTracker) {
    this.initialize();
  }

  private initialize(): void {
    this.usageTracker.getDatabase().exec(`
      CREATE TABLE IF NOT EXISTS budget_policies (
        name TEXT PRIMARY KEY,
        limit_usd REAL NOT NULL,
        warning_threshold REAL NOT NULL,
        session_id TEXT,
        model TEXT,
        start_time TEXT,
        end_time TEXT,
        created_at TEXT NOT NULL,
        updated_at TEXT NOT NULL
      );
    `);
  }

  async setBudget(policy: BudgetPolicy): Promise<StoredBudgetPolicy> {
    this.validatePolicy(policy);

    const now = new Date().toISOString();
    const existing = await this.getBudget(policy.name);
    const createdAt = existing?.createdAt ?? now;
    const warningThreshold = policy.warningThreshold ?? 0.8;

    this.usageTracker.getDatabase().prepare(`
      INSERT INTO budget_policies (
        name,
        limit_usd,
        warning_threshold,
        session_id,
        model,
        start_time,
        end_time,
        created_at,
        updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
      ON CONFLICT(name) DO UPDATE SET
        limit_usd = excluded.limit_usd,
        warning_threshold = excluded.warning_threshold,
        session_id = excluded.session_id,
        model = excluded.model,
        start_time = excluded.start_time,
        end_time = excluded.end_time,
        updated_at = excluded.updated_at
    `).run(
      policy.name,
      policy.limitUsd,
      warningThreshold,
      policy.sessionId ?? null,
      policy.model ?? null,
      policy.startTime ?? null,
      policy.endTime ?? null,
      createdAt,
      now
    );

    return {
      ...policy,
      warningThreshold,
      createdAt,
      updatedAt: now
    };
  }

  async getBudget(name: string): Promise<StoredBudgetPolicy | null> {
    const row = this.usageTracker.getDatabase().prepare(`
      SELECT
        name,
        limit_usd AS limitUsd,
        warning_threshold AS warningThreshold,
        session_id AS sessionId,
        model,
        start_time AS startTime,
        end_time AS endTime,
        created_at AS createdAt,
        updated_at AS updatedAt
      FROM budget_policies
      WHERE name = ?
    `).get(name) as StoredBudgetPolicy | undefined;

    return row ?? null;
  }

  async listBudgets(): Promise<StoredBudgetPolicy[]> {
    return this.usageTracker.getDatabase().prepare(`
      SELECT
        name,
        limit_usd AS limitUsd,
        warning_threshold AS warningThreshold,
        session_id AS sessionId,
        model,
        start_time AS startTime,
        end_time AS endTime,
        created_at AS createdAt,
        updated_at AS updatedAt
      FROM budget_policies
      ORDER BY name ASC
    `).all() as StoredBudgetPolicy[];
  }

  async evaluateBudget(policyOrName: BudgetPolicy | string): Promise<BudgetEvaluation> {
    const policy = typeof policyOrName === "string"
      ? await this.getBudgetOrThrow(policyOrName)
      : policyOrName;
    this.validatePolicy(policy);

    const warningThreshold = policy.warningThreshold ?? 0.8;

    const summary = await this.usageTracker.getUsageSummary({
      sessionId: policy.sessionId,
      model: policy.model,
      startTime: policy.startTime,
      endTime: policy.endTime
    });

    const spentUsd = summary.totalCostUsd;
    const remainingUsd = Math.max(policy.limitUsd - spentUsd, 0);
    const usageRatio = spentUsd / policy.limitUsd;

    let status: BudgetEvaluation["status"] = "ok";
    if (usageRatio >= 1) {
      status = "exceeded";
    } else if (usageRatio >= warningThreshold) {
      status = "warning";
    }

    return {
      budgetName: policy.name,
      limitUsd: policy.limitUsd,
      spentUsd,
      remainingUsd,
      usageRatio,
      status
    };
  }

  private async getBudgetOrThrow(name: string): Promise<StoredBudgetPolicy> {
    const policy = await this.getBudget(name);
    if (!policy) {
      throw new Error(`Budget policy not found: ${name}`);
    }
    return policy;
  }

  private validatePolicy(policy: BudgetPolicy): void {
    if (!policy.name.trim()) {
      throw new Error("Budget name is required.");
    }

    if (policy.limitUsd <= 0) {
      throw new Error("Budget limitUsd must be greater than zero.");
    }

    const warningThreshold = policy.warningThreshold ?? 0.8;
    if (warningThreshold <= 0 || warningThreshold > 1) {
      throw new Error("warningThreshold must be within (0, 1].");
    }
  }
}


path: src/CostCalculator.ts
content: import { ModelPricing } from "./types.js";

export class CostCalculator {
  private readonly pricing = new Map<string, ModelPricing>();

  constructor(initialPricing: Record<string, ModelPricing> = {}) {
    for (const [model, price] of Object.entries(initialPricing)) {
      this.setPricing(model, price);
    }
  }

  setPricing(model: string, pricing: ModelPricing): void {
    if (pricing.inputCostPerMillion < 0 || pricing.outputCostPerMillion < 0) {
      throw new Error("Pricing values must be non-negative.");
    }

    this.pricing.set(model, pricing);
  }

  getPricing(model: string): ModelPricing | undefined {
    return this.pricing.get(model);
  }

  calculateCost(model: string, promptTokens: number, completionTokens: number) {
    const pricing = this.pricing.get(model);

    if (!pricing) {
      return {
        inputCostUsd: 0,
        outputCostUsd: 0,
        totalCostUsd: 0
      };
    }

    const inputCostUsd = (promptTokens / 1_000_000) * pricing.inputCostPerMillion;
    const outputCostUsd = (completionTokens / 1_000_000) * pricing.outputCostPerMillion;

    return {
      inputCostUsd,
      outputCostUsd,
      totalCostUsd: inputCostUsd + outputCostUsd
    };
  }
}


path: src/UsageTracker.ts
content: import Database from "better-sqlite3";

import {
  TimeSeriesPoint,
  UsageEventInput,
  UsageQuery,
  UsageRecord,
  UsageSummary,
  UsageTrackerOptions
} from "./types.js";

interface UsageRow {
  id: number;
  session_id: string;
  model: string;
  prompt_tokens: number;
  completion_tokens: number;
  total_tokens: number;
  input_cost_usd: number;
  output_cost_usd: number;
  total_cost_usd: number;
  timestamp: string;
  metadata_json: string | null;
}

export class UsageTracker {
  private readonly db: Database.Database;
  private readonly costCalculator?: UsageTrackerOptions["costCalculator"];

  constructor(options: UsageTrackerOptions = {}) {
    this.db = new Database(options.dbPath ?? "usage-tracker.db");
    this.costCalculator = options.costCalculator;
    this.initialize();
  }

  private initialize(): void {
    this.db.pragma("journal_mode = WAL");
    this.db.exec(`
      CREATE TABLE IF NOT EXISTS usage_records (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        session_id TEXT NOT NULL,
        model TEXT NOT NULL,
        prompt_tokens INTEGER NOT NULL,
        completion_tokens INTEGER NOT NULL,
        total_tokens INTEGER NOT NULL,
        input_cost_usd REAL NOT NULL,
        output_cost_usd REAL NOT NULL,
        total_cost_usd REAL NOT NULL,
        timestamp TEXT NOT NULL,
        metadata_json TEXT
      );

      CREATE INDEX IF NOT EXISTS idx_usage_records_timestamp
        ON usage_records(timestamp);
      CREATE INDEX IF NOT EXISTS idx_usage_records_session_id
        ON usage_records(session_id);
      CREATE INDEX IF NOT EXISTS idx_usage_records_model
        ON usage_records(model);
    `);
  }

  async recordUsage(input: UsageEventInput): Promise<UsageRecord> {
    this.validateUsage(input);

    const timestamp = input.timestamp ?? new Date().toISOString();
    const totalTokens = input.promptTokens + input.completionTokens;
    const costs = this.costCalculator?.calculateCost(
      input.model,
      input.promptTokens,
      input.completionTokens
    ) ?? {
      inputCostUsd: 0,
      outputCostUsd: 0,
      totalCostUsd: 0
    };

    const statement = this.db.prepare(`
      INSERT INTO usage_records (
        session_id,
        model,
        prompt_tokens,
        completion_tokens,
        total_tokens,
        input_cost_usd,
        output_cost_usd,
        total_cost_usd,
        timestamp,
        metadata_json
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
    `);

    const result = statement.run(
      input.sessionId,
      input.model,
      input.promptTokens,
      input.completionTokens,
      totalTokens,
      costs.inputCostUsd,
      costs.outputCostUsd,
      costs.totalCostUsd,
      timestamp,
      input.metadata ? JSON.stringify(input.metadata) : null
    );

    return {
      id: Number(result.lastInsertRowid),
      sessionId: input.sessionId,
      model: input.model,
      promptTokens: input.promptTokens,
      completionTokens: input.completionTokens,
      totalTokens,
      inputCostUsd: costs.inputCostUsd,
      outputCostUsd: costs.outputCostUsd,
      totalCostUsd: costs.totalCostUsd,
      timestamp,
      metadata: input.metadata
    };
  }

  async listUsage(query: UsageQuery = {}): Promise<UsageRecord[]> {
    const { whereClause, parameters } = this.buildWhereClause(query);
    const statement = this.db.prepare(`
      SELECT *
      FROM usage_records
      ${whereClause}
      ORDER BY timestamp ASC, id ASC
    `);

    const rows = statement.all(...parameters) as UsageRow[];
    return rows.map((row) => this.mapRow(row));
  }

  async getUsageSummary(query: UsageQuery = {}): Promise<UsageSummary> {
    const { whereClause, parameters } = this.buildWhereClause(query);

    const totals = this.db.prepare(`
      SELECT
        COUNT(*) AS totalRequests,
        COALESCE(SUM(prompt_tokens), 0) AS promptTokens,
        COALESCE(SUM(completion_tokens), 0) AS completionTokens,
        COALESCE(SUM(total_tokens), 0) AS totalTokens,
        COALESCE(SUM(total_cost_usd), 0) AS totalCostUsd
      FROM usage_records
      ${whereClause}
    `).get(...parameters) as {
      totalRequests: number;
      promptTokens: number;
      completionTokens: number;
      totalTokens: number;
      totalCostUsd: number;
    };

    const byModel = this.db.prepare(`
      SELECT
        model,
        COUNT(*) AS requests,
        COALESCE(SUM(prompt_tokens), 0) AS promptTokens,
        COALESCE(SUM(completion_tokens), 0) AS completionTokens,
        COALESCE(SUM(total_tokens), 0) AS totalTokens,
        COALESCE(SUM(total_cost_usd), 0) AS totalCostUsd
      FROM usage_records
      ${whereClause}
      GROUP BY model
      ORDER BY totalCostUsd DESC, model ASC
    `).all(...parameters) as UsageSummary["byModel"];

    const bySession = this.db.prepare(`
      SELECT
        session_id AS sessionId,
        COUNT(*) AS requests,
        COALESCE(SUM(prompt_tokens), 0) AS promptTokens,
        COALESCE(SUM(completion_tokens), 0) AS completionTokens,
        COALESCE(SUM(total_tokens), 0) AS totalTokens,
        COALESCE(SUM(total_cost_usd), 0) AS totalCostUsd
      FROM usage_records
      ${whereClause}
      GROUP BY session_id
      ORDER BY totalCostUsd DESC, sessionId ASC
    `).all(...parameters) as UsageSummary["bySession"];

    return {
      totalRequests: totals.totalRequests,
      promptTokens: totals.promptTokens,
      completionTokens: totals.completionTokens,
      totalTokens: totals.totalTokens,
      totalCostUsd: totals.totalCostUsd,
      byModel,
      bySession
    };
  }

  async getTimeSeries(query: UsageQuery = {}): Promise<TimeSeriesPoint[]> {
    const { whereClause, parameters } = this.buildWhereClause(query);
    const rows = this.db.prepare(`
      SELECT
        substr(timestamp, 1, 10) AS bucket,
        COUNT(*) AS requests,
        COALESCE(SUM(prompt_tokens), 0) AS promptTokens,
        COALESCE(SUM(completion_tokens), 0) AS completionTokens,
        COALESCE(SUM(total_tokens), 0) AS totalTokens,
        COALESCE(SUM(total_cost_usd), 0) AS totalCostUsd
      FROM usage_records
      ${whereClause}
      GROUP BY bucket
      ORDER BY bucket ASC
    `).all(...parameters) as TimeSeriesPoint[];

    return rows;
  }

  close(): void {
    this.db.close();
  }

  getDatabase(): Database.Database {
    return this.db;
  }

  private buildWhereClause(query: UsageQuery) {
    const clauses: string[] = [];
    const parameters: Array<string> = [];

    if (query.sessionId) {
      clauses.push("session_id = ?");
      parameters.push(query.sessionId);
    }

    if (query.model) {
      clauses.push("model = ?");
      parameters.push(query.model);
    }

    if (query.startTime) {
      clauses.push("timestamp >= ?");
      parameters.push(query.startTime);
    }

    if (query.endTime) {
      clauses.push("timestamp <= ?");
      parameters.push(query.endTime);
    }

    return {
      whereClause: clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "",
      parameters
    };
  }

  private mapRow(row: UsageRow): UsageRecord {
    return {
      id: row.id,
      sessionId: row.session_id,
      model: row.model,
      promptTokens: row.prompt_tokens,
      completionTokens: row.completion_tokens,
      totalTokens: row.total_tokens,
      inputCostUsd: row.input_cost_usd,
      outputCostUsd: row.output_cost_usd,
      totalCostUsd: row.total_cost_usd,
      timestamp: row.timestamp,
      metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined
    };
  }

  private validateUsage(input: UsageEventInput): void {
    if (!input.sessionId.trim()) {
      throw new Error("sessionId is required.");
    }

    if (!input.model.trim()) {
      throw new Error("model is required.");
    }

    if (input.promptTokens < 0 || input.completionTokens < 0) {
      throw new Error("Token counts must be non-negative.");
    }
  }
}


path: src/index.ts
content: export { BudgetManager } from "./BudgetManager.js";
export { CostCalculator } from "./CostCalculator.js";
export { UsageTracker } from "./UsageTracker.js";
export type {
  BudgetEvaluation,
  BudgetPolicy,
  ModelPricing,
  StoredBudgetPolicy,
  TimeSeriesPoint,
  UsageEventInput,
  UsageQuery,
  UsageRecord,
  UsageSummary,
  UsageTrackerOptions
} from "./types.js";


path: src/types.ts
content: export interface ModelPricing {
  inputCostPerMillion: number;
  outputCostPerMillion: number;
}

export interface UsageEventInput {
  sessionId: string;
  model: string;
  promptTokens: number;
  completionTokens: number;
  timestamp?: string;
  metadata?: Record<string, unknown>;
}

export interface UsageRecord extends UsageEventInput {
  id: number;
  totalTokens: number;
  inputCostUsd: number;
  outputCostUsd: number;
  totalCostUsd: number;
  timestamp: string;
}

export interface UsageQuery {
  sessionId?: string;
  model?: string;
  startTime?: string;
  endTime?: string;
}

export interface UsageSummary {
  totalRequests: number;
  promptTokens: number;
  completionTokens: number;
  totalTokens: number;
  totalCostUsd: number;
  byModel: Array<{
    model: string;
    requests: number;
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
    totalCostUsd: number;
  }>;
  bySession: Array<{
    sessionId: string;
    requests: number;
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
    totalCostUsd: number;
  }>;
}

export interface TimeSeriesPoint {
  bucket: string;
  requests: number;
  promptTokens: number;
  completionTokens: number;
  totalTokens: number;
  totalCostUsd: number;
}

export interface UsageTrackerOptions {
  dbPath?: string;
  costCalculator?: {
    calculateCost(model: string, promptTokens: number, completionTokens: number): {
      inputCostUsd: number;
      outputCostUsd: number;
      totalCostUsd: number;
    };
  };
}

export interface BudgetPolicy {
  name: string;
  limitUsd: number;
  warningThreshold?: number;
  sessionId?: string;
  model?: string;
  startTime?: string;
  endTime?: string;
}

export interface StoredBudgetPolicy extends BudgetPolicy {
  createdAt: string;
  updatedAt: string;
}

export interface BudgetEvaluation {
  budgetName: string;
  limitUsd: number;
  spentUsd: number;
  remainingUsd: number;
  usageRatio: number;
  status: "ok" | "warning" | "exceeded";
}


path: tests/BudgetManager.test.ts
content: import { afterEach, describe, expect, it } from "vitest";

import { BudgetManager } from "../src/BudgetManager.js";
import { CostCalculator } from "../src/CostCalculator.js";
import { UsageTracker } from "../src/UsageTracker.js";

describe("BudgetManager", () => {
  let tracker: UsageTracker | undefined;

  afterEach(() => {
    tracker?.close();
    tracker = undefined;
  });

  it("returns warning when spend crosses threshold", async () => {
    tracker = new UsageTracker({
      dbPath: ":memory:",
      costCalculator: new CostCalculator({
        expensive: { inputCostPerMillion: 10, outputCostPerMillion: 20 }
      })
    });

    await tracker.recordUsage({
      sessionId: "budget-session",
      model: "expensive",
      promptTokens: 40_000,
      completionTokens: 20_000
    });

    const manager = new BudgetManager(tracker);
    await manager.setBudget({
      name: "session-budget",
      limitUsd: 1,
      warningThreshold: 0.75
    });
    const evaluation = await manager.evaluateBudget("session-budget");

    expect(evaluation.status).toBe("warning");
    expect(evaluation.spentUsd).toBeCloseTo(0.8);
  });

  it("returns exceeded when spend is above limit", async () => {
    tracker = new UsageTracker({
      dbPath: ":memory:",
      costCalculator: new CostCalculator({
        expensive: { inputCostPerMillion: 20, outputCostPerMillion: 40 }
      })
    });

    await tracker.recordUsage({
      sessionId: "budget-session",
      model: "expensive",
      promptTokens: 60_000,
      completionTokens: 30_000
    });

    const manager = new BudgetManager(tracker);
    await manager.setBudget({
      name: "session-budget",
      limitUsd: 1
    });
    const evaluation = await manager.evaluateBudget("session-budget");

    expect(evaluation.status).toBe("exceeded");
    expect(evaluation.remainingUsd).toBe(0);
  });

  it("stores and lists budget policies", async () => {
    tracker = new UsageTracker({ dbPath: ":memory:" });
    const manager = new BudgetManager(tracker);

    const stored = await manager.setBudget({
      name: "daily-team-budget",
      limitUsd: 25,
      warningThreshold: 0.5,
      model: "gpt-4.1-mini"
    });

    expect(stored.name).toBe("daily-team-budget");

    const loaded = await manager.getBudget("daily-team-budget");
    const all = await manager.listBudgets();

    expect(loaded?.model).toBe("gpt-4.1-mini");
    expect(all).toHaveLength(1);
  });
});


path: tests/CostCalculator.test.ts
content: import { describe, expect, it } from "vitest";

import { CostCalculator } from "../src/CostCalculator.js";

describe("CostCalculator", () => {
  it("calculates prompt and completion cost from per-million rates", () => {
    const calculator = new CostCalculator({
      "gpt-4.1-mini": {
        inputCostPerMillion: 0.4,
        outputCostPerMillion: 1.6
      }
    });

    const result = calculator.calculateCost("gpt-4.1-mini", 250_000, 100_000);

    expect(result.inputCostUsd).toBeCloseTo(0.1);
    expect(result.outputCostUsd).toBeCloseTo(0.16);
    expect(result.totalCostUsd).toBeCloseTo(0.26);
  });

  it("returns zero for unknown model pricing", () => {
    const calculator = new CostCalculator();
    expect(calculator.calculateCost("unknown", 1000, 1000).totalCostUsd).toBe(0);
  });
});


path: tests/UsageTracker.test.ts
content: import { afterEach, describe, expect, it } from "vitest";

import { CostCalculator } from "../src/CostCalculator.js";
import { UsageTracker } from "../src/UsageTracker.js";

describe("UsageTracker", () => {
  let tracker: UsageTracker | undefined;

  afterEach(() => {
    tracker?.close();
    tracker = undefined;
  });

  it("records usage and includes computed cost", async () => {
    tracker = new UsageTracker({
      dbPath: ":memory:",
      costCalculator: new CostCalculator({
        "gpt-4.1-mini": {
          inputCostPerMillion: 0.4,
          outputCostPerMillion: 1.6
        }
      })
    });

    const record = await tracker.recordUsage({
      sessionId: "session-a",
      model: "gpt-4.1-mini",
      promptTokens: 10_000,
      completionTokens: 5_000,
      metadata: { requestId: "req_123" }
    });

    expect(record.totalTokens).toBe(15_000);
    expect(record.totalCostUsd).toBeCloseTo(0.012);

    const stored = await tracker.listUsage();
    expect(stored).toHaveLength(1);
    expect(stored[0]?.metadata).toEqual({ requestId: "req_123" });
  });

  it("aggregates by model, session, and day", async () => {
    tracker = new UsageTracker({
      dbPath: ":memory:",
      costCalculator: new CostCalculator({
        modelA: { inputCostPerMillion: 1, outputCostPerMillion: 2 },
        modelB: { inputCostPerMillion: 0.5, outputCostPerMillion: 1 }
      })
    });

    await tracker.recordUsage({
      sessionId: "s1",
      model: "modelA",
      promptTokens: 1000,
      completionTokens: 500,
      timestamp: "2026-03-07T10:00:00.000Z"
    });
    await tracker.recordUsage({
      sessionId: "s1",
      model: "modelB",
      promptTokens: 2000,
      completionTokens: 1000,
      timestamp: "2026-03-07T11:00:00.000Z"
    });
    await tracker.recordUsage({
      sessionId: "s2",
      model: "modelA",
      promptTokens: 4000,
      completionTokens: 2000,
      timestamp: "2026-03-08T09:00:00.000Z"
    });

    const summary = await tracker.getUsageSummary();
    expect(summary.totalRequests).toBe(3);
    expect(summary.totalTokens).toBe(10_500);
    expect(summary.byModel).toHaveLength(2);
    expect(summary.bySession).toHaveLength(2);

    const series = await tracker.getTimeSeries();
    expect(series).toEqual([
      expect.objectContaining({ bucket: "2026-03-07", requests: 2, totalTokens: 4500 }),
      expect.objectContaining({ bucket: "2026-03-08", requests: 1, totalTokens: 6000 })
    ]);
  });
});


path: tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "lib": ["ES2022"],
    "declaration": true,
    "outDir": "dist",
    "rootDir": ".",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts", "tests/**/*.ts", "examples/**/*.ts"]
}


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)