opencode-total-session-cost 1.0.0 → 1.1.1

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.
@@ -1,4 +1,4 @@
1
- name: Continuous Integration
1
+ name: CI
2
2
 
3
3
  on:
4
4
  push:
@@ -3,25 +3,28 @@ name: Publish to NPM
3
3
  on:
4
4
  release:
5
5
  types: [published]
6
+ workflow_dispatch:
6
7
 
7
8
  jobs:
8
9
  publish:
9
10
  name: Publish Package
10
11
  runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ id-token: write
11
15
  steps:
12
16
  - name: Checkout Code
13
- uses: actions/checkout@v4
17
+ uses: actions/checkout@v6
14
18
 
15
- - name: Setup Node.js (for NPM Publish)
16
- uses: actions/setup-node@v4
19
+ - name: Setup Node.js
20
+ uses: actions/setup-node@v6
17
21
  with:
18
- node-version: 20
22
+ node-version: 24
19
23
  registry-url: 'https://registry.npmjs.org'
24
+ package-manager-cache: false
20
25
 
21
26
  - name: Install Dependencies
22
27
  run: npm ci
23
28
 
24
29
  - name: Publish Package
25
- run: npm publish --provenance --access public
26
- env:
27
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
30
+ run: npm publish --access public
package/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Opencode Total Session Cost Tracker 💰
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/opencode-total-session-cost.svg?logo=npm&color=CB3837)](https://www.npmjs.com/package/opencode-total-session-cost)
4
+ [![npm downloads](https://img.shields.io/npm/dm/opencode-total-session-cost.svg?logo=npm&color=51a822)](https://www.npmjs.com/package/opencode-total-session-cost)
5
+ [![npm provenance](https://img.shields.io/badge/provenance-signed-blue?logo=sigstore&color=007ec6)](https://www.npmjs.com/package/opencode-total-session-cost)
6
+ [![CI Status](https://github.com/StayPirate/opencode-total-session-cost/actions/workflows/ci.yml/badge.svg)](https://github.com/StayPirate/opencode-total-session-cost/actions/workflows/ci.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/StayPirate/opencode-total-session-cost)
8
+ [![GitHub stars](https://img.shields.io/github/stars/StayPirate/opencode-total-session-cost.svg?style=flat&logo=github&color=007ec6)](https://github.com/StayPirate/opencode-total-session-cost/stargazers)
9
+
3
10
  A real-time, lightweight plugin for [Opencode](https://opencode.im) that tracks and displays the **actual cumulative cost** of your active session, including all background tasks, sub-agents, and child sessions.
4
11
 
5
12
  ## Why this plugin?
@@ -12,7 +19,9 @@ Without tracking these sub-sessions, you might see a main session cost of a few
12
19
  ## Features
13
20
  - **Sidebar-Independent**: The cost indicator stays visible even when the Opencode sidebar is closed.
14
21
  - **Recursive Sub-Agent Tracking**: Automatically detects and sums up the costs of all child tasks spawned during your session.
15
- - **Detailed Cost Breakdown**: Offers a `/total_cost` slash command to show a detailed popup separating your active session cost from child sub-agent costs.
22
+ - **Detailed Cost Breakdown**: Offers a `/total_cost` slash command to show a detailed popup separating your active session cost from child task and sub-agent costs.
23
+ - **Provider & Model Breakdown**: Displays the exact costs accumulated per model across all session hierarchy levels.
24
+ - **Mouse Click Interaction**: Left-clicking on the cost bar in the prompt header right panel triggers the same detailed breakdown popup.
16
25
 
17
26
  ## Installation
18
27
  Once published, you can install it globally with:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-total-session-cost",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Real-time cumulative cost tracker for Opencode sessions, aggregating costs from parent sessions, archived messages, and all child sub-agent tasks.",
5
5
  "type": "module",
6
6
  "main": "./index.ts",
@@ -19,6 +19,14 @@
19
19
  ],
20
20
  "author": "crazybyte",
21
21
  "license": "MIT",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/StayPirate/opencode-total-session-cost.git"
25
+ },
26
+ "bugs": {
27
+ "url": "https://github.com/StayPirate/opencode-total-session-cost/issues"
28
+ },
29
+ "homepage": "https://github.com/StayPirate/opencode-total-session-cost#readme",
22
30
  "dependencies": {
23
31
  "@opencode-ai/plugin": "^1.18.30",
24
32
  "@opencode-ai/sdk": "^1.18.30"
package/tui.tsx CHANGED
@@ -4,6 +4,102 @@ import { createSignal, onCleanup } from "solid-js";
4
4
  export const id = "opencode-cost-bar";
5
5
 
6
6
  export const SessionCostPlugin: TuiPlugin = async (api) => {
7
+ const showCostBreakdown = async (sessionID: string) => {
8
+ let parentCost = 0;
9
+ let taskCost = 0;
10
+ let subagentCost = 0;
11
+
12
+ const modelCosts: Record<string, number> = {};
13
+
14
+ const addModelCost = (providerID: string, modelID: string, cost: number) => {
15
+ const key = `${providerID}/${modelID}`;
16
+ modelCosts[key] = (modelCosts[key] || 0) + cost;
17
+ };
18
+
19
+ const calculateCostRecursive = async (currID: string, fallbackSession?: any): Promise<void> => {
20
+ const sessionObj = api.state.session.get(currID) || fallbackSession;
21
+ let sessionCost = 0;
22
+ if (sessionObj && typeof sessionObj.cost === "number") {
23
+ sessionCost = sessionObj.cost;
24
+ if (currID === sessionID) {
25
+ parentCost += sessionCost;
26
+ } else if (sessionObj.agent === "explore" || sessionObj.agent === "general") {
27
+ taskCost += sessionCost;
28
+ } else {
29
+ subagentCost += sessionCost;
30
+ }
31
+ }
32
+
33
+ let messagesAttributedCost = 0;
34
+ try {
35
+ const msgRes = await api.client.session.messages({ sessionID: currID });
36
+ if (msgRes.data) {
37
+ for (const item of msgRes.data) {
38
+ const msg = item.info as any;
39
+ if (msg && msg.role === "assistant" && typeof msg.cost === "number" && msg.cost > 0) {
40
+ const provider = msg.providerID || (sessionObj?.model?.providerID) || "unknown";
41
+ const model = msg.modelID || (sessionObj?.model?.id) || "unknown";
42
+ addModelCost(provider, model, msg.cost);
43
+ messagesAttributedCost += msg.cost;
44
+ }
45
+ }
46
+ }
47
+ } catch (err) {}
48
+
49
+ const remainder = sessionCost - messagesAttributedCost;
50
+ if (remainder > 0.0001) {
51
+ const provider = (sessionObj?.model?.providerID) || "unknown";
52
+ const model = (sessionObj?.model?.id) || "unknown";
53
+ addModelCost(provider, model, remainder);
54
+ }
55
+
56
+ try {
57
+ const childrenRes = await api.client.session.children({ sessionID: currID });
58
+ if (childrenRes.data) {
59
+ for (const child of childrenRes.data) {
60
+ await calculateCostRecursive(child.id, child);
61
+ }
62
+ }
63
+ } catch (err) {}
64
+ };
65
+
66
+ await calculateCostRecursive(sessionID);
67
+ const total = parentCost + taskCost + subagentCost;
68
+
69
+ // Format Session breakdown
70
+ const sessionSection = `By session\nSession: $${parentCost.toFixed(2)}\nTask: $${taskCost.toFixed(2)}\nSub-agent: $${subagentCost.toFixed(2)}\n---------------\nTotal: $${total.toFixed(2)}`;
71
+
72
+ // Format Model breakdown
73
+ let maxLabelLength = 6; // Length of "Total:" is 6
74
+ for (const key of Object.keys(modelCosts)) {
75
+ const label = `${key}:`;
76
+ if (label.length > maxLabelLength) {
77
+ maxLabelLength = label.length;
78
+ }
79
+ }
80
+
81
+ const sortedModels = Object.entries(modelCosts).sort((a, b) => b[1] - a[1]);
82
+ const modelLines: string[] = [];
83
+ for (const [key, cost] of sortedModels) {
84
+ const label = `${key}:`;
85
+ const paddedLabel = label.padEnd(maxLabelLength + 1, " ");
86
+ modelLines.push(`${paddedLabel}$${cost.toFixed(2)}`);
87
+ }
88
+
89
+ const modelSeparator = "-".repeat(maxLabelLength + 7);
90
+ const paddedTotalLabel = "Total:".padEnd(maxLabelLength + 1, " ");
91
+ const modelTotalLine = `${paddedTotalLabel}$${total.toFixed(2)}`;
92
+
93
+ const modelSection = `By provider/model\n${modelLines.join("\n")}\n${modelSeparator}\n${modelTotalLine}`;
94
+
95
+ api.ui.toast({
96
+ title: "Session Costs Breakdown",
97
+ message: `${sessionSection}\n\n${modelSection}`,
98
+ variant: "success",
99
+ duration: 10000
100
+ });
101
+ };
102
+
7
103
  // Register TUI slots with reactive signals defined inside the renderers
8
104
  api.slots?.register({
9
105
  slots: {
@@ -55,7 +151,14 @@ export const SessionCostPlugin: TuiPlugin = async (api) => {
55
151
  update();
56
152
 
57
153
  return (
58
- <text fg="gray">
154
+ <text
155
+ fg="gray"
156
+ onMouseUp={(e: any) => {
157
+ if (props?.session_id && e.button === 0) {
158
+ showCostBreakdown(props.session_id);
159
+ }
160
+ }}
161
+ >
59
162
  {" "}[ ${total().toFixed(2)} ]
60
163
  </text>
61
164
  );
@@ -89,38 +192,7 @@ export const SessionCostPlugin: TuiPlugin = async (api) => {
89
192
  return;
90
193
  }
91
194
 
92
- let parentCost = 0;
93
- let childrenCost = 0;
94
-
95
- const calculateCostRecursive = async (sessionID: string): Promise<void> => {
96
- const sessionObj = api.state.session.get(sessionID);
97
- if (sessionObj && typeof sessionObj.cost === "number") {
98
- if (sessionID === currentSessionID) {
99
- parentCost += sessionObj.cost;
100
- } else {
101
- childrenCost += sessionObj.cost;
102
- }
103
- }
104
-
105
- try {
106
- const childrenRes = await api.client.session.children({ sessionID });
107
- if (childrenRes.data) {
108
- for (const child of childrenRes.data) {
109
- await calculateCostRecursive(child.id);
110
- }
111
- }
112
- } catch (err) {}
113
- };
114
-
115
- await calculateCostRecursive(currentSessionID);
116
- const total = parentCost + childrenCost;
117
-
118
- api.ui.toast({
119
- title: "Session Costs Breakdown",
120
- message: `Active: $${parentCost.toFixed(2)} | Children: $${childrenCost.toFixed(2)} | Total: $${total.toFixed(2)}`,
121
- variant: "success",
122
- duration: 6000
123
- });
195
+ await showCostBreakdown(currentSessionID);
124
196
  }
125
197
  }
126
198
  ]);