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.
- package/.github/workflows/ci.yml +1 -1
- package/.github/workflows/publish.yml +10 -7
- package/README.md +10 -1
- package/package.json +9 -1
- package/tui.tsx +105 -33
package/.github/workflows/ci.yml
CHANGED
|
@@ -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@
|
|
17
|
+
uses: actions/checkout@v6
|
|
14
18
|
|
|
15
|
-
- name: Setup Node.js
|
|
16
|
-
uses: actions/setup-node@
|
|
19
|
+
- name: Setup Node.js
|
|
20
|
+
uses: actions/setup-node@v6
|
|
17
21
|
with:
|
|
18
|
-
node-version:
|
|
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 --
|
|
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
|
+
[](https://www.npmjs.com/package/opencode-total-session-cost)
|
|
4
|
+
[](https://www.npmjs.com/package/opencode-total-session-cost)
|
|
5
|
+
[](https://www.npmjs.com/package/opencode-total-session-cost)
|
|
6
|
+
[](https://github.com/StayPirate/opencode-total-session-cost/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/StayPirate/opencode-total-session-cost)
|
|
8
|
+
[](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.
|
|
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
|
|
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
|
-
|
|
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
|
]);
|