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.
- package/.github/workflows/ci.yml +27 -0
- package/.github/workflows/publish.yml +27 -0
- package/README.md +30 -0
- package/index.ts +14 -0
- package/opencode.jsonc +6 -0
- package/package.json +36 -0
- package/tsconfig.json +15 -0
- package/tui.tsx +138 -0
|
@@ -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
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;
|