opencode-token-monitor 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,54 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.3.0] - 2026-02-08
9
+
10
+ ### Added
11
+ - **Project-Aware Analytics**:
12
+ - Automatic session tagging with project IDs.
13
+ - New `scope` parameter for `token_stats` and `token_history` to filter trends and history by project.
14
+ - New `history_scope` parameter for `token_export` to filter range exports by project.
15
+ - Project ID inclusion in JSON, CSV, and Markdown exports.
16
+ - Backward-compatible history loading (legacy records included in global scope, excluded from project-specific scope).
17
+
18
+ ## [0.2.0] - 2026-02-08
19
+
20
+ ### Added
21
+ - **Ecosystem Analytics**:
22
+ - Token and cost attribution by Execution Agent and Initiator Agent.
23
+ - Agent×Model cross-breakdown table.
24
+ - Tool×Command attribution with safe command summaries.
25
+ - **History & Trends**:
26
+ - Persistent session history storage.
27
+ - `token_history` tool for querying past usage.
28
+ - Trend analysis with ASCII bar charts for daily costs.
29
+ - Week-over-week change detection and cost spike alerts.
30
+ - **Budgeting & Quotas**:
31
+ - Configurable daily, weekly, and monthly budget thresholds via `token-monitor.json`.
32
+ - Antigravity quota monitoring.
33
+ - Session-level and budget-level toast notifications.
34
+ - **Enhanced `token_stats`**:
35
+ - `include_children` parameter for recursive session aggregation.
36
+ - `compact` mode for reducing output size.
37
+ - `agent_view`, `agent_sort`, and `agent_top_n` parameters for fine-grained control.
38
+ - **Data Export**:
39
+ - `token_export` tool supporting JSON, CSV, and Markdown.
40
+ - **Stability**:
41
+ - Automatic output truncation and Antigravity auto-degradation.
42
+
43
+ ### Changed
44
+ - Refactored rendering logic into specialized `lib/renderer.ts`.
45
+ - Improved model detection and pricing coverage (including Antigravity variants).
46
+ - Updated `package.json` for NPM readiness.
47
+
48
+ ## [0.1.0] - 2026-02-01
49
+
50
+ ### Added
51
+ - Initial release.
52
+ - Basic `token_stats` tool with model breakdown.
53
+ - Cost calculation with custom `pricing.json` support.
54
+ - Core aggregation logic for input, output, reasoning, and cache tokens.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ainsley0917
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # opencode-token-monitor
2
+
3
+ [![CI](https://github.com/Ainsley0917/opencode-token-monitor/actions/workflows/ci.yml/badge.svg)](https://github.com/Ainsley0917/opencode-token-monitor/actions/workflows/ci.yml)
4
+
5
+ OpenCode plugin for monitoring token usage, estimating costs, and tracking ecosystem analytics across AI coding sessions.
6
+
7
+ ## Features
8
+
9
+ - **Real-time Monitoring**: Track token usage (input/output/reasoning/cache) for all assistant messages.
10
+ - **Ecosystem Analytics**:
11
+ - **Agent Breakdown**: Detailed cost and token attribution by execution agent and initiator agent.
12
+ - **Agent×Model Cross-breakdown**: See which agents are using which models.
13
+ - **Tool×Command Attribution**: Identify high-cost tools and commands (safe summaries only).
14
+ - **History & Trends**:
15
+ - **Persistent History**: Automatically saves session records for long-term tracking.
16
+ - **Trend Analysis**: Daily cost trends, Week-over-week changes, and cost spike detection.
17
+ - **Visual Charts**: ASCII bar charts for cost trends directly in the terminal.
18
+ - **Budgeting & Quotas**:
19
+ - **Budget Thresholds**: Set daily, weekly, and monthly budget limits.
20
+ - **Quota Integration**: Monitor Antigravity quota remaining fractions.
21
+ - **Live Notifications**: Toast notifications for session costs and budget warnings.
22
+ - **Advanced Export**: Export data to JSON, CSV, or Markdown formats for external analysis.
23
+ - **Stability Features**: Automatic output truncation and "compact mode" for heavy sessions or Antigravity models.
24
+
25
+ ## Installation
26
+
27
+ ### Manual Installation
28
+
29
+ 1. Clone the repository:
30
+ ```bash
31
+ git clone https://github.com/Ainsley0917/opencode-token-monitor.git
32
+ cd opencode-token-monitor
33
+ ```
34
+ 2. Install dependencies and build:
35
+ ```bash
36
+ bun install
37
+ bun run build
38
+ ```
39
+ 3. Copy the plugin to your OpenCode plugins directory:
40
+ ```bash
41
+ cp dist/plugin.js ~/.opencode/plugins/token-monitor.js
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ The plugin registers three tools: `token_stats`, `token_history`, and `token_export`.
47
+
48
+ ### `token_stats`
49
+
50
+ Show detailed token usage for the current or specified session.
51
+
52
+ **Parameters:**
53
+ - `session_id` (string, optional): Session ID to inspect. Defaults to current.
54
+ - `include_children` (boolean, optional): Include child sessions in aggregation.
55
+ - `agent_view` (string, optional): "execution", "initiator", or "both" (default).
56
+ - `agent_sort` (string, optional): Sort tables by "cost" (default) or "tokens".
57
+ - `agent_top_n` (number, optional): Show top N agents (default: 10). Use 0 to show all.
58
+ - `trend_days` (number, optional): Number of days for trend analysis (default: 7).
59
+ - `scope` (string, optional): Filter historical trends to "project" or "all" (default).
60
+ - `compact` (boolean, optional): Skip heavy tables (auto-enabled for Antigravity models).
61
+ - `debug` (boolean, optional): Include debug information.
62
+
63
+ ### `token_history`
64
+
65
+ Query historical token usage over a date range.
66
+
67
+ **Parameters:**
68
+ - `from` (string, optional): Start date (ISO format, e.g., "2026-01-01").
69
+ - `to` (string, optional): End date (ISO format, e.g., "2026-02-07").
70
+ - `scope` (string, optional): Filter history to "project" or "all" (default).
71
+
72
+ ### `token_export`
73
+
74
+ Export token data for external use.
75
+
76
+ **Parameters:**
77
+ - `format` (string, required): "json", "csv", or "markdown".
78
+ - `scope` (string, optional): "session" (default) or "range".
79
+ - `session_id` (string, optional): For session scope.
80
+ - `from`/`to` (string, optional): For range scope.
81
+ - `history_scope` (string, optional): Filter range data to "project" or "all" (default).
82
+ - `file_path` (string, optional): Save to a specific file.
83
+
84
+ ## Project-Aware Analytics
85
+
86
+ The plugin automatically tracks usage on a per-project basis using the project ID provided by OpenCode.
87
+
88
+ - **Automatic Tagging**: Every session record is tagged with the current project ID.
89
+ - **Scope Selection**: Use `scope: "project"` (in `token_stats`/`token_history`) or `history_scope: "project"` (in `token_export`) to filter analytics to the current project.
90
+ - **Backward Compatibility**: History recorded before this feature was added is preserved. These "legacy" records are included when using `scope: "all"` (default) but are excluded when filtering by a specific project.
91
+ - **No Manual Migration**: The system handles mixed history gracefully without requiring any manual data updates.
92
+
93
+ ## Configuration
94
+
95
+ ### Pricing (`pricing.json`)
96
+
97
+ Customize pricing in `pricing.json` (searched in current dir, `~/.opencode/`, or `~/.config/opencode/`):
98
+
99
+ ```json
100
+ {
101
+ "anthropic/claude-sonnet-4": {
102
+ "input_per_million": 3.0,
103
+ "output_per_million": 15.0,
104
+ "cache_read_per_million": 0.30,
105
+ "cache_write_per_million": 3.75
106
+ }
107
+ }
108
+ ```
109
+
110
+ ### Budgets (`token-monitor.json`)
111
+
112
+ Set budget limits in `token-monitor.json`:
113
+
114
+ ```json
115
+ {
116
+ "budget": {
117
+ "daily": 5.00,
118
+ "weekly": 25.00,
119
+ "monthly": 100.00
120
+ }
121
+ }
122
+ ```
123
+
124
+ ## Development
125
+
126
+ ```bash
127
+ bun test # Run 340+ tests
128
+ bun run typecheck # Verify types
129
+ bun run build # Bundle plugin
130
+ ```
131
+
132
+ Created by [@Ainsley0917](https://github.com/Ainsley0917)
133
+
134
+ ## License
135
+
136
+ MIT
@@ -0,0 +1,20 @@
1
+ import type { AssistantMessage, TokenStats, TokenStatsByModel, TokenStatsByAgent, TokenStatsByAgentModel, ToolAttributionResult, UserMessage } from "./types";
2
+ export declare function aggregateTokens(messages: AssistantMessage[]): TokenStats;
3
+ export declare function aggregateTokensByModel(messages: AssistantMessage[]): TokenStatsByModel;
4
+ export declare function aggregateTokensByAgent(messages: AssistantMessage[]): TokenStatsByAgent;
5
+ export declare function aggregateTokensByAgentModel(messages: AssistantMessage[]): TokenStatsByAgentModel;
6
+ export declare function aggregateToolAttribution(parts: any[]): ToolAttributionResult;
7
+ export declare function aggregateTokensByInitiator(messages: AssistantMessage[], userMessages: UserMessage[]): TokenStatsByAgent;
8
+ export declare function topNAgents(stats: TokenStatsByAgent, costMap: Record<string, number>, n: number, sortBy: "cost" | "tokens"): {
9
+ rows: Array<{
10
+ agent: string;
11
+ stats: TokenStatsByAgent[string];
12
+ cost: number;
13
+ }>;
14
+ others?: {
15
+ agent: string;
16
+ stats: TokenStatsByAgent[string];
17
+ cost: number;
18
+ count: number;
19
+ };
20
+ };
@@ -0,0 +1,7 @@
1
+ export type ChartOptions = {
2
+ maxPoints?: number;
3
+ width?: number;
4
+ height?: number;
5
+ };
6
+ export declare function renderBarChart(values: number[], labels: string[], options?: ChartOptions): string;
7
+ export declare function renderSparkline(values: number[], options?: ChartOptions): string;
@@ -0,0 +1,23 @@
1
+ import type { SessionRecord } from "./types";
2
+ import type { Severity } from "./quota";
3
+ export type BudgetConfig = {
4
+ daily?: number;
5
+ weekly?: number;
6
+ monthly?: number;
7
+ thresholds?: {
8
+ warning?: number;
9
+ error?: number;
10
+ };
11
+ };
12
+ export type BudgetStatus = {
13
+ period: "daily" | "weekly" | "monthly";
14
+ limit: number;
15
+ spent: number;
16
+ remaining: number;
17
+ percentage: number;
18
+ severity: Severity;
19
+ };
20
+ export declare function loadBudgetConfig(basePath?: string): BudgetConfig;
21
+ export declare function computeSpend(records: SessionRecord[], period: "daily" | "weekly" | "monthly"): number;
22
+ export declare function getBudgetStatus(records: SessionRecord[], config: BudgetConfig): BudgetStatus[];
23
+ export declare function formatBudgetSection(statuses: BudgetStatus[]): string;
@@ -0,0 +1,3 @@
1
+ import type { TokenStatsByModel, PriceConfig, CostResult } from "./types";
2
+ export declare function loadPricingConfig(configPath?: string): PriceConfig;
3
+ export declare function calculateCost(stats: TokenStatsByModel, customPricing?: PriceConfig): CostResult;
@@ -0,0 +1,15 @@
1
+ import type { Message, Part } from "@opencode-ai/sdk";
2
+ import type { PreparedMessage, UserMessage } from "./types";
3
+ type RawMessage = {
4
+ info: Message;
5
+ parts: Part[];
6
+ };
7
+ export declare function prepareMessages(rawMessages: RawMessage[]): {
8
+ assistant: PreparedMessage[];
9
+ user: UserMessage[];
10
+ };
11
+ export declare function prepareMessagesWithChildren(rootMessages: RawMessage[], childMessages: RawMessage[][]): {
12
+ assistant: PreparedMessage[];
13
+ user: UserMessage[];
14
+ };
15
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { SessionRecord } from "./types";
2
+ export type ExportFormat = "json" | "csv" | "markdown";
3
+ export type ExportScope = "session" | "range";
4
+ export interface ExportOptions {
5
+ format: ExportFormat;
6
+ scope: ExportScope;
7
+ sessionID?: string;
8
+ from?: Date;
9
+ to?: Date;
10
+ includeChildren?: boolean;
11
+ }
12
+ export declare function exportToJSON(records: SessionRecord[]): string;
13
+ export declare function exportToCSV(records: SessionRecord[]): string;
14
+ export declare function exportToMarkdown(records: SessionRecord[]): string;
15
+ export declare function exportData(records: SessionRecord[], format: ExportFormat): string;
@@ -0,0 +1,4 @@
1
+ import type { SessionRecord } from "./types";
2
+ export declare function getShardPath(date: Date, baseDir?: string): string;
3
+ export declare function saveSessionRecord(record: SessionRecord, baseDir?: string): Promise<void>;
4
+ export declare function loadHistoryForRange(from: Date, to: Date, baseDir?: string, projectID?: string): Promise<SessionRecord[]>;
@@ -0,0 +1,14 @@
1
+ import type { QuotaStatus, Severity } from "./quota";
2
+ export type NotificationState = {
3
+ lastCost: number;
4
+ lastToastAt: number;
5
+ previousQuotaSeverities: Map<string, Severity>;
6
+ };
7
+ export declare function shouldShowToast(currentCost: number, quotaStatuses: QuotaStatus[], sessionID: string, nowOverride?: number): {
8
+ show: boolean;
9
+ message?: string;
10
+ };
11
+ export declare function updateState(sessionID: string, cost: number, quotaStatuses: QuotaStatus[]): void;
12
+ export declare function resetState(sessionID: string): void;
13
+ export declare function formatCostToast(cost: number, delta?: number): string;
14
+ export declare function formatQuotaAlertToast(status: QuotaStatus): string;
@@ -0,0 +1,12 @@
1
+ import type { TokenStats, TokenStatsByModel, PriceConfig } from "./types";
2
+ export type Suggestion = {
3
+ id: string;
4
+ message: string;
5
+ metric: string;
6
+ severity: "info" | "warning";
7
+ };
8
+ export declare function analyzeModelCosts(byModel: TokenStatsByModel, pricing: PriceConfig): Suggestion[];
9
+ export declare function analyzeCacheEfficiency(stats: TokenStats): Suggestion[];
10
+ export declare function analyzeReasoningUsage(byModel: TokenStatsByModel): Suggestion[];
11
+ export declare function generateOptimizationSuggestions(stats: TokenStats, byModel: TokenStatsByModel, pricing: PriceConfig): Suggestion[];
12
+ export declare function formatOptimizationSection(suggestions: Suggestion[]): string;
@@ -0,0 +1,19 @@
1
+ export type StabilityConfig = {
2
+ maxChars: number;
3
+ maxTableRows: number;
4
+ maxChartPoints: number;
5
+ };
6
+ export declare const DEFAULT_STABILITY_CONFIG: StabilityConfig;
7
+ export type TruncatedResult = {
8
+ content: string;
9
+ truncated: boolean;
10
+ originalLength: number;
11
+ message?: string;
12
+ };
13
+ export declare function truncateOutput(content: string, config?: Partial<StabilityConfig>): TruncatedResult;
14
+ export declare function limitTableRows<T>(rows: T[], config?: Partial<StabilityConfig>): {
15
+ rows: T[];
16
+ truncated: boolean;
17
+ totalCount: number;
18
+ };
19
+ export declare function getDebugInfo(sections: string[]): string;
@@ -0,0 +1,13 @@
1
+ export type QuotaSource = "antigravity" | "codex";
2
+ export type Severity = "info" | "warning" | "error";
3
+ export type QuotaStatus = {
4
+ source: QuotaSource;
5
+ scope: string;
6
+ remainingFraction: number;
7
+ resetsAt?: string;
8
+ severity: Severity;
9
+ };
10
+ export declare function getSeverity(remainingFraction: number): Severity;
11
+ export declare function loadAntigravityQuota(basePath?: string): QuotaStatus[];
12
+ export declare function loadCodexQuota(basePath?: string): QuotaStatus[];
13
+ export declare function loadAllQuota(): QuotaStatus[];
@@ -0,0 +1,12 @@
1
+ import type { topNAgents } from "./aggregation";
2
+ import type { TokenStats, TokenStatsByAgentModel, TokenStatsByModel, ToolAttributionResult } from "./types";
3
+ type TopNAgentsResult = ReturnType<typeof topNAgents>;
4
+ export declare function renderHeader(sessionID: string, models: string[], isAntigravity: boolean, isCompact: boolean): string;
5
+ export declare function renderTotals(totalStats: TokenStats): string;
6
+ export declare function renderEstimatedCost(totalCost: number): string;
7
+ export declare function renderModelTable(statsByModel: TokenStatsByModel, costByModel: Record<string, number>): string;
8
+ export declare function renderAgentTable(title: string, rows: TopNAgentsResult, totalCost: number): string;
9
+ export declare function renderAgentModelTable(statsByAgentModel: TokenStatsByAgentModel, totalCost: number): string;
10
+ export declare function renderToolCommandTable(attribution: ToolAttributionResult, totalCost: number): string;
11
+ export declare function renderWarnings(warnings: string[]): string;
12
+ export {};
@@ -0,0 +1,22 @@
1
+ import type { OpencodeClient } from "@opencode-ai/sdk";
2
+ import type { AssistantMessage, TokenStats, TokenStatsByModel, PriceConfig } from "./types";
3
+ export type SessionTreeOptions = {
4
+ maxDepth?: number;
5
+ };
6
+ export type SessionNode = {
7
+ sessionID: string;
8
+ messages: AssistantMessage[];
9
+ children: SessionNode[];
10
+ };
11
+ export type ChildSessionSummary = {
12
+ sessionID: string;
13
+ tokens: TokenStats;
14
+ cost: number;
15
+ };
16
+ export type SessionTreeStats = {
17
+ totals: TokenStats;
18
+ byModel: TokenStatsByModel;
19
+ childSummaries: ChildSessionSummary[];
20
+ };
21
+ export declare function listSessionTree(rootID: string, client: OpencodeClient, options?: SessionTreeOptions): Promise<SessionNode>;
22
+ export declare function aggregateSessionTree(node: SessionNode, pricing: PriceConfig): SessionTreeStats;
@@ -0,0 +1,16 @@
1
+ import type { SessionRecord } from "./types";
2
+ export type DailyBucket = {
3
+ date: string;
4
+ cost: number;
5
+ tokens: number;
6
+ sessions: number;
7
+ };
8
+ export type TrendStats = {
9
+ buckets: DailyBucket[];
10
+ weekOverWeekDelta: number;
11
+ spikes: string[];
12
+ };
13
+ export declare function bucketByDay(records: SessionRecord[]): DailyBucket[];
14
+ export declare function computeWeekOverWeek(buckets: DailyBucket[]): number;
15
+ export declare function detectSpikes(buckets: DailyBucket[], threshold?: number): string[];
16
+ export declare function analyzeTrends(records: SessionRecord[]): TrendStats;
@@ -0,0 +1,107 @@
1
+ import type { Part } from "@opencode-ai/sdk";
2
+ export type AssistantMessage = {
3
+ id: string;
4
+ sessionID: string;
5
+ role: "assistant";
6
+ time: {
7
+ created: number;
8
+ completed?: number;
9
+ };
10
+ parentID: string;
11
+ modelID: string;
12
+ providerID: string;
13
+ mode: string;
14
+ path: {
15
+ cwd: string;
16
+ root: string;
17
+ };
18
+ summary?: boolean;
19
+ cost: number;
20
+ tokens: {
21
+ input: number;
22
+ output: number;
23
+ reasoning: number;
24
+ cache: {
25
+ read: number;
26
+ write: number;
27
+ };
28
+ };
29
+ finish?: string;
30
+ };
31
+ export type TokenStats = {
32
+ input: number;
33
+ output: number;
34
+ total: number;
35
+ reasoning: number;
36
+ cache: {
37
+ read: number;
38
+ write: number;
39
+ };
40
+ };
41
+ export type AgentTokenStats = TokenStats & {
42
+ messageCount: number;
43
+ };
44
+ export type AgentModelTokenStats = AgentTokenStats & {
45
+ agent: string;
46
+ model: string;
47
+ };
48
+ export type TokenStatsByModel = {
49
+ [key: string]: TokenStats;
50
+ };
51
+ export type TokenStatsByAgent = {
52
+ [key: string]: AgentTokenStats;
53
+ };
54
+ export type TokenStatsByAgentModel = {
55
+ [key: string]: AgentModelTokenStats;
56
+ };
57
+ export type ToolCallSummary = {
58
+ tool: string;
59
+ title: string;
60
+ callCount: number;
61
+ tokens: TokenStats;
62
+ cost: number;
63
+ };
64
+ export type ToolAttributionResult = {
65
+ byTool: Record<string, ToolCallSummary>;
66
+ };
67
+ export type UserMessage = {
68
+ id: string;
69
+ sessionID: string;
70
+ role: "user";
71
+ time: {
72
+ created: number;
73
+ };
74
+ agent: string;
75
+ model: {
76
+ providerID: string;
77
+ modelID: string;
78
+ };
79
+ };
80
+ export type PreparedMessage = {
81
+ info: AssistantMessage;
82
+ parts: Part[];
83
+ };
84
+ export type ModelPricing = {
85
+ input_per_million: number;
86
+ output_per_million: number;
87
+ cache_read_per_million?: number;
88
+ cache_write_per_million?: number;
89
+ };
90
+ export type PriceConfig = {
91
+ [modelKey: string]: ModelPricing;
92
+ };
93
+ export type CostResult = {
94
+ totalCost: number;
95
+ byModel: {
96
+ [modelKey: string]: number;
97
+ };
98
+ warnings: string[];
99
+ };
100
+ export type SessionRecord = {
101
+ sessionID: string;
102
+ projectID?: string;
103
+ timestamp: number;
104
+ totals: TokenStats;
105
+ byModel: TokenStatsByModel;
106
+ cost: number;
107
+ };
@@ -0,0 +1,3 @@
1
+ import { type Plugin, type PluginInput } from "@opencode-ai/plugin";
2
+ export declare function getInFlightCount(): number;
3
+ export default function (input: PluginInput): Promise<ReturnType<Plugin>>;