opencode-total-session-cost 1.0.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.
@@ -0,0 +1,27 @@
1
+ name: Continuous Integration
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+
9
+ jobs:
10
+ typecheck:
11
+ name: TypeScript Typecheck
12
+ runs-on: ubuntu-latest
13
+
14
+ steps:
15
+ - name: Checkout Code
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Setup Bun
19
+ uses: oven-sh/setup-bun@v2
20
+ with:
21
+ bun-version: latest
22
+
23
+ - name: Install Dependencies
24
+ run: bun install --frozen-lockfile
25
+
26
+ - name: Run Typecheck
27
+ run: bun x tsc --noEmit
@@ -0,0 +1,27 @@
1
+ name: Publish to NPM
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ publish:
9
+ name: Publish Package
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - name: Checkout Code
13
+ uses: actions/checkout@v4
14
+
15
+ - name: Setup Node.js (for NPM Publish)
16
+ uses: actions/setup-node@v4
17
+ with:
18
+ node-version: 20
19
+ registry-url: 'https://registry.npmjs.org'
20
+
21
+ - name: Install Dependencies
22
+ run: npm ci
23
+
24
+ - name: Publish Package
25
+ run: npm publish --provenance --access public
26
+ env:
27
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # Opencode Total Session Cost Tracker 💰
2
+
3
+ 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
+
5
+ ## Why this plugin?
6
+ By default, Opencode only displays the cost of your active main session. However, any background tasks or specialized sub-agents (like `explore`, `coder`, or custom test executors) invoked during your conversation run in their own child sub-sessions, keeping their costs hidden.
7
+
8
+ Without tracking these sub-sessions, you might see a main session cost of a few cents, while the background agents have spent significantly more.
9
+
10
+ **This plugin solves that.** It automatically aggregates the costs of your main session and all its nested child tasks, displaying your **real, total spending** in real-time directly inside the main session.
11
+
12
+ ## Features
13
+ - **Sidebar-Independent**: The cost indicator stays visible even when the Opencode sidebar is closed.
14
+ - **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.
16
+
17
+ ## Installation
18
+ Once published, you can install it globally with:
19
+ ```bash
20
+ opencode plugin opencode-total-session-cost -g
21
+ ```
22
+
23
+ ## How it works
24
+ The plugin runs inside Opencode's TUI framework, recursively fetching session data:
25
+ 1. It reads the core cost of your active session.
26
+ 2. It recursively queries all child sub-sessions spawned by sub-agents.
27
+ 3. It displays the combined sum dynamically in the TUI.
28
+
29
+ ## License
30
+ MIT
package/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { Plugin, PluginModule } from "@opencode-ai/plugin";
2
+
3
+ export const id = "opencode-cost-bar";
4
+
5
+ export const server: Plugin = async (ctx) => {
6
+ return {}; // No-op backend hooks
7
+ };
8
+
9
+ const pluginModule: PluginModule = {
10
+ id,
11
+ server
12
+ };
13
+
14
+ export default pluginModule;
package/opencode.jsonc ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "plugin": [
4
+ "./tui.tsx"
5
+ ]
6
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "opencode-total-session-cost",
3
+ "version": "1.0.0",
4
+ "description": "Real-time cumulative cost tracker for Opencode sessions, aggregating costs from parent sessions, archived messages, and all child sub-agent tasks.",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts",
9
+ "./tui": "./tui.tsx"
10
+ },
11
+ "keywords": [
12
+ "opencode-plugin",
13
+ "opencode",
14
+ "tui",
15
+ "cost",
16
+ "token",
17
+ "subagent",
18
+ "cumulative-cost"
19
+ ],
20
+ "author": "crazybyte",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@opencode-ai/plugin": "^1.18.30",
24
+ "@opencode-ai/sdk": "^1.18.30"
25
+ },
26
+ "peerDependencies": {
27
+ "@opentui/core": ">=0.1.97",
28
+ "@opentui/solid": ">=0.1.97",
29
+ "solid-js": "^1.9.12"
30
+ },
31
+ "devDependencies": {
32
+ "typescript": "^7.0.2",
33
+ "solid-js": "^1.9.12",
34
+ "@types/node": "^26.5.1"
35
+ }
36
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "jsx": "preserve",
7
+ "jsxImportSource": "@opentui/solid",
8
+ "strict": true,
9
+ "declaration": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["./index.ts", "./tui.tsx"]
15
+ }
package/tui.tsx ADDED
@@ -0,0 +1,138 @@
1
+ import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui";
2
+ import { createSignal, onCleanup } from "solid-js";
3
+
4
+ export const id = "opencode-cost-bar";
5
+
6
+ export const SessionCostPlugin: TuiPlugin = async (api) => {
7
+ // Register TUI slots with reactive signals defined inside the renderers
8
+ api.slots?.register({
9
+ slots: {
10
+ // session_prompt_right: Persistent cost tracker in the prompt header right panel next to active model info
11
+ session_prompt_right: (ctx: any, props: any) => {
12
+ const [total, setTotal] = createSignal<number>(0);
13
+
14
+ const calculateCost = async (sessionID: string): Promise<number> => {
15
+ let totalCost = 0;
16
+
17
+ const sessionObj = api.state.session.get(sessionID);
18
+ if (sessionObj && typeof sessionObj.cost === "number") {
19
+ totalCost += sessionObj.cost;
20
+ }
21
+
22
+ try {
23
+ const childrenRes = await api.client.session.children({ sessionID });
24
+ if (childrenRes.data) {
25
+ for (const child of childrenRes.data) {
26
+ totalCost += await calculateCost(child.id);
27
+ }
28
+ }
29
+ } catch (err) {}
30
+
31
+ return totalCost;
32
+ };
33
+
34
+ const update = async () => {
35
+ const sessionID = props?.session_id;
36
+ if (sessionID) {
37
+ const res = await calculateCost(sessionID);
38
+ setTotal(res);
39
+ } else {
40
+ setTotal(0);
41
+ }
42
+ };
43
+
44
+ const unsubMsgUpdated = api.event.on("message.updated", update);
45
+ const unsubMsgRemoved = api.event.on("message.removed", update);
46
+ const interval = setInterval(update, 3000);
47
+
48
+ onCleanup(() => {
49
+ unsubMsgUpdated();
50
+ unsubMsgRemoved();
51
+ clearInterval(interval);
52
+ });
53
+
54
+ // Trigger update immediately
55
+ update();
56
+
57
+ return (
58
+ <text fg="gray">
59
+ {" "}[ ${total().toFixed(2)} ]
60
+ </text>
61
+ );
62
+ }
63
+ }
64
+ });
65
+
66
+ // Register slash command /total_cost (and alias /costs)
67
+ api.command?.register(() => [
68
+ {
69
+ title: "Total Session Cost",
70
+ value: "total_cost",
71
+ description: "Display total cost breakdown of current session and child sessions",
72
+ category: "Cost Tracking",
73
+ slash: {
74
+ name: "total_cost",
75
+ aliases: ["costs"]
76
+ },
77
+ onSelect: async () => {
78
+ const currentSessionID = api.route.current && api.route.current.name === "session" && typeof api.route.current.params?.sessionID === "string"
79
+ ? api.route.current.params.sessionID
80
+ : null;
81
+
82
+ if (!currentSessionID) {
83
+ api.ui.toast({
84
+ title: "No Active Session",
85
+ message: "Please open a session to check costs.",
86
+ variant: "warning",
87
+ duration: 4000
88
+ });
89
+ return;
90
+ }
91
+
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
+ });
124
+ }
125
+ }
126
+ ]);
127
+ };
128
+
129
+ // Export named constant for loaders looking for `export const tui = ...`
130
+ export const tui = SessionCostPlugin;
131
+
132
+ // Export default module configuration
133
+ const pluginModule: TuiPluginModule = {
134
+ id,
135
+ tui: SessionCostPlugin
136
+ };
137
+
138
+ export default pluginModule;