opencode-usage-stats 0.1.0 → 0.2.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/README.md +8 -2
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +231 -60
- package/dist/index.js.map +7 -1
- package/package.json +19 -7
package/README.md
CHANGED
|
@@ -17,13 +17,19 @@ OpenCode will install the npm package automatically on startup.
|
|
|
17
17
|
|
|
18
18
|
## Use
|
|
19
19
|
|
|
20
|
-
In any OpenCode session,
|
|
20
|
+
In any OpenCode session, run the slash command:
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
/usage
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Or ask the agent:
|
|
21
27
|
|
|
22
28
|
```
|
|
23
29
|
Show my OpenCode usage
|
|
24
30
|
```
|
|
25
31
|
|
|
26
|
-
The
|
|
32
|
+
The plugin will return a markdown summary of sessions, messages, tokens, cost, model/tool breakdown.
|
|
27
33
|
|
|
28
34
|
## Configuration
|
|
29
35
|
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
2
2
|
declare const OpenCodeUsage: Plugin;
|
|
3
|
-
|
|
3
|
+
declare const plugin: {
|
|
4
|
+
id: string;
|
|
5
|
+
server: Plugin;
|
|
6
|
+
};
|
|
7
|
+
export default plugin;
|
|
4
8
|
export { OpenCodeUsage };
|
|
5
9
|
export * from "./api.js";
|
|
6
10
|
export * from "./config.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAA8B,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAA8B,MAAM,qBAAqB,CAAA;AAmB7E,QAAA,MAAM,aAAa,EAAE,MA2EpB,CAAA;AAED,QAAA,MAAM,MAAM;IACV,EAAE;IACF,MAAM;CACP,CAAA;eAEc,MAAM;AACrB,OAAO,EAAE,aAAa,EAAE,CAAA;AACxB,cAAc,UAAU,CAAA;AACxB,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -1,63 +1,234 @@
|
|
|
1
|
+
// src/index.ts
|
|
1
2
|
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
3
|
+
|
|
4
|
+
// src/api.ts
|
|
5
|
+
async function fetchCloudUsage(_config) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// src/config.ts
|
|
10
|
+
function resolveConfig(options) {
|
|
11
|
+
const cfg = options ?? {};
|
|
12
|
+
return {
|
|
13
|
+
enableCloudApi: cfg.enableCloudApi ?? false,
|
|
14
|
+
cloudApiUrl: cfg.cloudApiUrl ?? "https://console.opencode.ai/api/usage",
|
|
15
|
+
apiKey: cfg.apiKey ?? ""
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/usage.ts
|
|
20
|
+
function isAssistantMessage(message) {
|
|
21
|
+
return "role" in message && message.role === "assistant";
|
|
22
|
+
}
|
|
23
|
+
function isToolPart(part) {
|
|
24
|
+
return part.type === "tool";
|
|
25
|
+
}
|
|
26
|
+
function aggregateMessages(messages) {
|
|
27
|
+
const stats = {
|
|
28
|
+
totalSessions: 0,
|
|
29
|
+
totalMessages: messages.length,
|
|
30
|
+
totalCost: 0,
|
|
31
|
+
totalInputTokens: 0,
|
|
32
|
+
totalOutputTokens: 0,
|
|
33
|
+
totalReasoningTokens: 0,
|
|
34
|
+
totalCacheRead: 0,
|
|
35
|
+
totalCacheWrite: 0,
|
|
36
|
+
modelUsage: {},
|
|
37
|
+
toolUsage: {}
|
|
38
|
+
};
|
|
39
|
+
for (const { info, parts } of messages) {
|
|
40
|
+
if (!isAssistantMessage(info)) continue;
|
|
41
|
+
stats.totalCost += info.cost ?? 0;
|
|
42
|
+
const tokens = info.tokens;
|
|
43
|
+
if (tokens) {
|
|
44
|
+
stats.totalInputTokens += tokens.input ?? 0;
|
|
45
|
+
stats.totalOutputTokens += tokens.output ?? 0;
|
|
46
|
+
stats.totalReasoningTokens += tokens.reasoning ?? 0;
|
|
47
|
+
stats.totalCacheRead += tokens.cache?.read ?? 0;
|
|
48
|
+
stats.totalCacheWrite += tokens.cache?.write ?? 0;
|
|
49
|
+
}
|
|
50
|
+
const modelKey = `${info.providerID}/${info.modelID}`;
|
|
51
|
+
if (!stats.modelUsage[modelKey]) {
|
|
52
|
+
stats.modelUsage[modelKey] = {
|
|
53
|
+
messages: 0,
|
|
54
|
+
inputTokens: 0,
|
|
55
|
+
outputTokens: 0,
|
|
56
|
+
cacheRead: 0,
|
|
57
|
+
cacheWrite: 0,
|
|
58
|
+
cost: 0
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
const model = stats.modelUsage[modelKey];
|
|
62
|
+
model.messages += 1;
|
|
63
|
+
if (tokens) {
|
|
64
|
+
model.inputTokens += tokens.input ?? 0;
|
|
65
|
+
model.outputTokens += tokens.output ?? 0;
|
|
66
|
+
model.cacheRead += tokens.cache?.read ?? 0;
|
|
67
|
+
model.cacheWrite += tokens.cache?.write ?? 0;
|
|
68
|
+
}
|
|
69
|
+
model.cost += info.cost ?? 0;
|
|
70
|
+
for (const part of parts) {
|
|
71
|
+
if (isToolPart(part)) {
|
|
72
|
+
stats.toolUsage[part.tool] = (stats.toolUsage[part.tool] ?? 0) + 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return stats;
|
|
77
|
+
}
|
|
78
|
+
async function aggregateUsage(client) {
|
|
79
|
+
const sessionsResponse = await client.session.list();
|
|
80
|
+
const sessions = Array.isArray(sessionsResponse.data) ? sessionsResponse.data : [];
|
|
81
|
+
const allMessages = [];
|
|
82
|
+
for (const session of sessions) {
|
|
83
|
+
try {
|
|
84
|
+
const response = await client.session.messages({ path: { id: session.id } });
|
|
85
|
+
const messages = Array.isArray(response.data) ? response.data : [];
|
|
86
|
+
allMessages.push(...messages);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const stats = aggregateMessages(allMessages);
|
|
92
|
+
stats.totalSessions = sessions.length;
|
|
93
|
+
return stats;
|
|
94
|
+
}
|
|
95
|
+
function formatUsage(stats) {
|
|
96
|
+
const totalTokens = stats.totalInputTokens + stats.totalOutputTokens + stats.totalReasoningTokens + stats.totalCacheRead + stats.totalCacheWrite;
|
|
97
|
+
const lines = [
|
|
98
|
+
"## OpenCode Usage",
|
|
99
|
+
"",
|
|
100
|
+
`- Sessions: ${stats.totalSessions.toLocaleString()}`,
|
|
101
|
+
`- Messages: ${stats.totalMessages.toLocaleString()}`,
|
|
102
|
+
`- Total Cost: $${stats.totalCost.toFixed(4)}`,
|
|
103
|
+
`- Total Tokens: ${totalTokens.toLocaleString()}`,
|
|
104
|
+
` - Input: ${stats.totalInputTokens.toLocaleString()}`,
|
|
105
|
+
` - Output: ${stats.totalOutputTokens.toLocaleString()}`,
|
|
106
|
+
` - Reasoning: ${stats.totalReasoningTokens.toLocaleString()}`,
|
|
107
|
+
` - Cache Read: ${stats.totalCacheRead.toLocaleString()}`,
|
|
108
|
+
` - Cache Write: ${stats.totalCacheWrite.toLocaleString()}`,
|
|
109
|
+
""
|
|
110
|
+
];
|
|
111
|
+
const models = Object.entries(stats.modelUsage).sort(([, a], [, b]) => b.messages - a.messages);
|
|
112
|
+
if (models.length > 0) {
|
|
113
|
+
lines.push("### Model Usage");
|
|
114
|
+
lines.push("");
|
|
115
|
+
for (const [model, usage] of models) {
|
|
116
|
+
lines.push(`- ${model}`);
|
|
117
|
+
lines.push(` - Messages: ${usage.messages.toLocaleString()}`);
|
|
118
|
+
lines.push(
|
|
119
|
+
` - Tokens: ${(usage.inputTokens + usage.outputTokens).toLocaleString()} (in ${usage.inputTokens.toLocaleString()} / out ${usage.outputTokens.toLocaleString()})`
|
|
120
|
+
);
|
|
121
|
+
lines.push(` - Cost: $${usage.cost.toFixed(4)}`);
|
|
122
|
+
}
|
|
123
|
+
lines.push("");
|
|
124
|
+
}
|
|
125
|
+
const tools = Object.entries(stats.toolUsage).sort(([, a], [, b]) => b - a);
|
|
126
|
+
if (tools.length > 0) {
|
|
127
|
+
lines.push("### Tool Usage");
|
|
128
|
+
lines.push("");
|
|
129
|
+
for (const [tool2, count] of tools) {
|
|
130
|
+
lines.push(`- ${tool2}: ${count.toLocaleString()}`);
|
|
131
|
+
}
|
|
132
|
+
lines.push("");
|
|
133
|
+
}
|
|
134
|
+
lines.push(
|
|
135
|
+
"_Cloud plan/quota information is not available because OpenCode does not expose a public usage API yet. Set `opencode-usage-stats.enableCloudApi: true` once it is supported._"
|
|
136
|
+
);
|
|
137
|
+
return lines.join("\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/index.ts
|
|
141
|
+
var createLogger = (client) => {
|
|
142
|
+
return {
|
|
143
|
+
debug: async (message, extra) => client.app.log({ body: { service: "opencode-usage-stats", level: "debug", message, extra } }),
|
|
144
|
+
info: async (message, extra) => client.app.log({ body: { service: "opencode-usage-stats", level: "info", message, extra } }),
|
|
145
|
+
warn: async (message, extra) => client.app.log({ body: { service: "opencode-usage-stats", level: "warn", message, extra } }),
|
|
146
|
+
error: async (message, extra) => client.app.log({ body: { service: "opencode-usage-stats", level: "error", message, extra } })
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
var OpenCodeUsage = async (input, options) => {
|
|
150
|
+
const config = resolveConfig(options);
|
|
151
|
+
const { client } = input;
|
|
152
|
+
const logger = createLogger(client);
|
|
153
|
+
await logger.info("Plugin initialized", { enableCloudApi: config.enableCloudApi });
|
|
154
|
+
return {
|
|
155
|
+
tool: {
|
|
156
|
+
usage: tool({
|
|
157
|
+
description: "Show OpenCode usage statistics (sessions, messages, tokens, cost, model/tool breakdown). When OpenCode exposes a cloud usage API, this can also show plan limits and remaining quota.",
|
|
158
|
+
args: {
|
|
159
|
+
cloud: tool.schema.boolean().optional().describe("Force fetch cloud usage information if available.")
|
|
55
160
|
},
|
|
56
|
-
|
|
161
|
+
async execute(args) {
|
|
162
|
+
const wantsCloud = args.cloud ?? config.enableCloudApi;
|
|
163
|
+
if (wantsCloud) {
|
|
164
|
+
await logger.info("Fetching cloud usage", { cloudApiUrl: config.cloudApiUrl });
|
|
165
|
+
const cloud = await fetchCloudUsage(config);
|
|
166
|
+
if (cloud) {
|
|
167
|
+
return {
|
|
168
|
+
title: "OpenCode Cloud Usage",
|
|
169
|
+
output: [
|
|
170
|
+
"## OpenCode Cloud Usage",
|
|
171
|
+
"",
|
|
172
|
+
`- Plan: ${cloud.plan ?? "unknown"}`,
|
|
173
|
+
`- Billing Period: ${cloud.billingPeriod ?? "unknown"}`,
|
|
174
|
+
`- Monthly Limit: ${cloud.monthlyLimit ?? "unknown"}`,
|
|
175
|
+
`- Monthly Used: ${cloud.monthlyUsed ?? "unknown"}`,
|
|
176
|
+
`- Remaining: ${cloud.remaining ?? "unknown"}`,
|
|
177
|
+
`- Estimated Cost: ${cloud.estimatedCost !== void 0 ? `$${cloud.estimatedCost.toFixed(4)}` : "unknown"}`
|
|
178
|
+
].join("\n")
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
await logger.warn("Cloud API returned no data, falling back to local usage");
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
await logger.info("Aggregating local usage");
|
|
185
|
+
const stats = await aggregateUsage(client);
|
|
186
|
+
await logger.info("Local usage aggregated", {
|
|
187
|
+
sessions: stats.totalSessions,
|
|
188
|
+
messages: stats.totalMessages,
|
|
189
|
+
cost: stats.totalCost
|
|
190
|
+
});
|
|
191
|
+
return {
|
|
192
|
+
title: "OpenCode Usage",
|
|
193
|
+
output: formatUsage(stats)
|
|
194
|
+
};
|
|
195
|
+
} catch (error) {
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
await logger.error("Failed to aggregate local usage", { error: message });
|
|
198
|
+
return {
|
|
199
|
+
title: "OpenCode Usage Error",
|
|
200
|
+
output: `Failed to gather OpenCode usage statistics.
|
|
201
|
+
|
|
202
|
+
${message}
|
|
203
|
+
|
|
204
|
+
Make sure OpenCode is running and the plugin has access to its local API.`
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
})
|
|
209
|
+
},
|
|
210
|
+
async config(config2) {
|
|
211
|
+
config2.command = config2.command ?? {};
|
|
212
|
+
config2.command.usage = {
|
|
213
|
+
template: "Call the usage tool and display the formatted usage summary.",
|
|
214
|
+
description: "Show OpenCode local usage statistics"
|
|
215
|
+
};
|
|
216
|
+
await logger.info("Registered /usage command");
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
var plugin = {
|
|
221
|
+
id: "opencode-usage-stats",
|
|
222
|
+
server: OpenCodeUsage
|
|
223
|
+
};
|
|
224
|
+
var index_default = plugin;
|
|
225
|
+
export {
|
|
226
|
+
OpenCodeUsage,
|
|
227
|
+
aggregateMessages,
|
|
228
|
+
aggregateUsage,
|
|
229
|
+
index_default as default,
|
|
230
|
+
fetchCloudUsage,
|
|
231
|
+
formatUsage,
|
|
232
|
+
resolveConfig
|
|
57
233
|
};
|
|
58
|
-
|
|
59
|
-
export { OpenCodeUsage };
|
|
60
|
-
export * from "./api.js";
|
|
61
|
-
export * from "./config.js";
|
|
62
|
-
export * from "./usage.js";
|
|
63
|
-
//# sourceMappingURL=index.js.map
|
|
234
|
+
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/index.ts", "../src/api.ts", "../src/config.ts", "../src/usage.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Plugin, PluginInput, PluginOptions } from \"@opencode-ai/plugin\"\nimport { tool } from \"@opencode-ai/plugin\"\nimport { fetchCloudUsage } from \"./api.js\"\nimport { resolveConfig } from \"./config.js\"\nimport { aggregateUsage, formatUsage } from \"./usage.js\"\n\nconst createLogger = (client: PluginInput[\"client\"]) => {\n return {\n debug: async (message: string, extra?: Record<string, unknown>) =>\n client.app.log({ body: { service: \"opencode-usage-stats\", level: \"debug\", message, extra } }),\n info: async (message: string, extra?: Record<string, unknown>) =>\n client.app.log({ body: { service: \"opencode-usage-stats\", level: \"info\", message, extra } }),\n warn: async (message: string, extra?: Record<string, unknown>) =>\n client.app.log({ body: { service: \"opencode-usage-stats\", level: \"warn\", message, extra } }),\n error: async (message: string, extra?: Record<string, unknown>) =>\n client.app.log({ body: { service: \"opencode-usage-stats\", level: \"error\", message, extra } }),\n }\n}\n\nconst OpenCodeUsage: Plugin = async (input: PluginInput, options?: PluginOptions) => {\n const config = resolveConfig(options)\n const { client } = input\n const logger = createLogger(client)\n\n await logger.info(\"Plugin initialized\", { enableCloudApi: config.enableCloudApi })\n\n return {\n tool: {\n usage: tool({\n description:\n \"Show OpenCode usage statistics (sessions, messages, tokens, cost, model/tool breakdown). \" +\n \"When OpenCode exposes a cloud usage API, this can also show plan limits and remaining quota.\",\n args: {\n cloud: tool.schema\n .boolean()\n .optional()\n .describe(\"Force fetch cloud usage information if available.\"),\n },\n async execute(args) {\n const wantsCloud = args.cloud ?? config.enableCloudApi\n\n if (wantsCloud) {\n await logger.info(\"Fetching cloud usage\", { cloudApiUrl: config.cloudApiUrl })\n const cloud = await fetchCloudUsage(config)\n if (cloud) {\n return {\n title: \"OpenCode Cloud Usage\",\n output: [\n \"## OpenCode Cloud Usage\",\n \"\",\n `- Plan: ${cloud.plan ?? \"unknown\"}`,\n `- Billing Period: ${cloud.billingPeriod ?? \"unknown\"}`,\n `- Monthly Limit: ${cloud.monthlyLimit ?? \"unknown\"}`,\n `- Monthly Used: ${cloud.monthlyUsed ?? \"unknown\"}`,\n `- Remaining: ${cloud.remaining ?? \"unknown\"}`,\n `- Estimated Cost: ${cloud.estimatedCost !== undefined ? `$${cloud.estimatedCost.toFixed(4)}` : \"unknown\"}`,\n ].join(\"\\n\"),\n }\n }\n await logger.warn(\"Cloud API returned no data, falling back to local usage\")\n }\n\n try {\n await logger.info(\"Aggregating local usage\")\n const stats = await aggregateUsage(client)\n await logger.info(\"Local usage aggregated\", {\n sessions: stats.totalSessions,\n messages: stats.totalMessages,\n cost: stats.totalCost,\n })\n return {\n title: \"OpenCode Usage\",\n output: formatUsage(stats),\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error)\n await logger.error(\"Failed to aggregate local usage\", { error: message })\n return {\n title: \"OpenCode Usage Error\",\n output: `Failed to gather OpenCode usage statistics.\\n\\n${message}\\n\\nMake sure OpenCode is running and the plugin has access to its local API.`,\n }\n }\n },\n }),\n },\n async config(config) {\n config.command = config.command ?? {}\n config.command.usage = {\n template: \"Call the usage tool and display the formatted usage summary.\",\n description: \"Show OpenCode local usage statistics\",\n }\n await logger.info(\"Registered /usage command\")\n },\n }\n}\n\nconst plugin = {\n id: \"opencode-usage-stats\",\n server: OpenCodeUsage,\n}\n\nexport default plugin\nexport { OpenCodeUsage }\nexport * from \"./api.js\"\nexport * from \"./config.js\"\nexport * from \"./usage.js\"\n", "import type { OpenCodeUsageConfig } from \"./config.js\"\n\n/**\n * Cloud usage response shape.\n *\n * This is intentionally generic because OpenCode does not currently expose a\n * public REST endpoint for plan/usage data. When such an API becomes available,\n * this interface can be tightened to match the real schema.\n */\nexport interface CloudUsageInfo {\n plan?: string\n monthlyLimit?: number\n monthlyUsed?: number\n remaining?: number\n billingPeriod?: string\n currency?: string\n estimatedCost?: number\n}\n\n/**\n * Fetch usage information from a cloud usage API.\n *\n * Currently this is a placeholder. The official OpenCode project does not\n * expose a public REST endpoint for billing/usage/remaining quota. When it does,\n * implement the real network call here and flip `enableCloudApi` to `true` in\n * your OpenCode config.\n *\n * @returns Cloud usage data, or `null` if not available.\n */\nexport async function fetchCloudUsage(\n _config: Required<OpenCodeUsageConfig>,\n): Promise<CloudUsageInfo | null> {\n // Placeholder: C-2 is reserved but not enabled by default.\n // Real implementation should:\n // 1. Validate config.apiKey and config.cloudApiUrl\n // 2. Make an authenticated request to the usage endpoint\n // 3. Parse and return the response\n return null\n}\n", "import type { PluginOptions } from \"@opencode-ai/plugin\"\n\n/**\n * Configuration shape for the OpenCodeUsage plugin.\n *\n * This is read from the OpenCode config under the `OpenCodeUsage` key,\n * e.g.:\n *\n * ```json\n * {\n * \"plugin\": [\"OpenCodeUsage\"],\n * \"OpenCodeUsage\": {\n * \"enableCloudApi\": false,\n * \"cloudApiUrl\": \"https://console.opencode.ai/api/usage\",\n * \"apiKey\": \"\"\n * }\n * }\n * ```\n */\nexport interface OpenCodeUsageConfig {\n /** When true, attempt to call the cloud usage API (C-2). Default false (C-1). */\n enableCloudApi?: boolean\n /** Base URL for the cloud usage API. Reserved for future OpenCode usage endpoints. */\n cloudApiUrl?: string\n /** API key for the cloud usage API. Reserved for future use. */\n apiKey?: string\n}\n\nexport function resolveConfig(options?: PluginOptions): Required<OpenCodeUsageConfig> {\n const cfg = (options ?? {}) as OpenCodeUsageConfig\n return {\n enableCloudApi: cfg.enableCloudApi ?? false,\n cloudApiUrl: cfg.cloudApiUrl ?? \"https://console.opencode.ai/api/usage\",\n apiKey: cfg.apiKey ?? \"\",\n }\n}\n", "import type { createOpencodeClient, Message, Part, Session } from \"@opencode-ai/sdk\"\n\nexport interface ModelUsage {\n messages: number\n inputTokens: number\n outputTokens: number\n cacheRead: number\n cacheWrite: number\n cost: number\n}\n\nexport interface ToolUsage {\n [tool: string]: number\n}\n\nexport interface UsageStats {\n totalSessions: number\n totalMessages: number\n totalCost: number\n totalInputTokens: number\n totalOutputTokens: number\n totalReasoningTokens: number\n totalCacheRead: number\n totalCacheWrite: number\n modelUsage: Record<string, ModelUsage>\n toolUsage: ToolUsage\n}\n\nexport type OpencodeClient = ReturnType<typeof createOpencodeClient>\n\nfunction isAssistantMessage(message: Message): message is Extract<Message, { role: \"assistant\" }> {\n return \"role\" in message && message.role === \"assistant\"\n}\n\nfunction isToolPart(part: Part): part is Extract<Part, { type: \"tool\" }> {\n return part.type === \"tool\"\n}\n\nexport function aggregateMessages(messages: { info: Message; parts: Part[] }[]): UsageStats {\n const stats: UsageStats = {\n totalSessions: 0,\n totalMessages: messages.length,\n totalCost: 0,\n totalInputTokens: 0,\n totalOutputTokens: 0,\n totalReasoningTokens: 0,\n totalCacheRead: 0,\n totalCacheWrite: 0,\n modelUsage: {},\n toolUsage: {},\n }\n\n for (const { info, parts } of messages) {\n if (!isAssistantMessage(info)) continue\n\n stats.totalCost += info.cost ?? 0\n\n const tokens = info.tokens\n if (tokens) {\n stats.totalInputTokens += tokens.input ?? 0\n stats.totalOutputTokens += tokens.output ?? 0\n stats.totalReasoningTokens += tokens.reasoning ?? 0\n stats.totalCacheRead += tokens.cache?.read ?? 0\n stats.totalCacheWrite += tokens.cache?.write ?? 0\n }\n\n const modelKey = `${info.providerID}/${info.modelID}`\n if (!stats.modelUsage[modelKey]) {\n stats.modelUsage[modelKey] = {\n messages: 0,\n inputTokens: 0,\n outputTokens: 0,\n cacheRead: 0,\n cacheWrite: 0,\n cost: 0,\n }\n }\n const model = stats.modelUsage[modelKey]\n model.messages += 1\n if (tokens) {\n model.inputTokens += tokens.input ?? 0\n model.outputTokens += tokens.output ?? 0\n model.cacheRead += tokens.cache?.read ?? 0\n model.cacheWrite += tokens.cache?.write ?? 0\n }\n model.cost += info.cost ?? 0\n\n for (const part of parts) {\n if (isToolPart(part)) {\n stats.toolUsage[part.tool] = (stats.toolUsage[part.tool] ?? 0) + 1\n }\n }\n }\n\n return stats\n}\n\nexport async function aggregateUsage(client: OpencodeClient): Promise<UsageStats> {\n const sessionsResponse = await client.session.list()\n const sessions = Array.isArray(sessionsResponse.data)\n ? (sessionsResponse.data as Session[])\n : []\n\n const allMessages: { info: Message; parts: Part[] }[] = []\n\n for (const session of sessions) {\n try {\n const response = await client.session.messages({ path: { id: session.id } })\n const messages = Array.isArray(response.data) ? response.data : []\n allMessages.push(...messages)\n } catch (error) {\n // Skip sessions we cannot read.\n continue\n }\n }\n\n const stats = aggregateMessages(allMessages)\n stats.totalSessions = sessions.length\n return stats\n}\n\nexport function formatUsage(stats: UsageStats): string {\n const totalTokens =\n stats.totalInputTokens +\n stats.totalOutputTokens +\n stats.totalReasoningTokens +\n stats.totalCacheRead +\n stats.totalCacheWrite\n\n const lines: string[] = [\n \"## OpenCode Usage\",\n \"\",\n `- Sessions: ${stats.totalSessions.toLocaleString()}`,\n `- Messages: ${stats.totalMessages.toLocaleString()}`,\n `- Total Cost: $${stats.totalCost.toFixed(4)}`,\n `- Total Tokens: ${totalTokens.toLocaleString()}`,\n ` - Input: ${stats.totalInputTokens.toLocaleString()}`,\n ` - Output: ${stats.totalOutputTokens.toLocaleString()}`,\n ` - Reasoning: ${stats.totalReasoningTokens.toLocaleString()}`,\n ` - Cache Read: ${stats.totalCacheRead.toLocaleString()}`,\n ` - Cache Write: ${stats.totalCacheWrite.toLocaleString()}`,\n \"\",\n ]\n\n const models = Object.entries(stats.modelUsage).sort(([, a], [, b]) => b.messages - a.messages)\n if (models.length > 0) {\n lines.push(\"### Model Usage\")\n lines.push(\"\")\n for (const [model, usage] of models) {\n lines.push(`- ${model}`)\n lines.push(` - Messages: ${usage.messages.toLocaleString()}`)\n lines.push(\n ` - Tokens: ${(usage.inputTokens + usage.outputTokens).toLocaleString()} (in ${usage.inputTokens.toLocaleString()} / out ${usage.outputTokens.toLocaleString()})`,\n )\n lines.push(` - Cost: $${usage.cost.toFixed(4)}`)\n }\n lines.push(\"\")\n }\n\n const tools = Object.entries(stats.toolUsage).sort(([, a], [, b]) => b - a)\n if (tools.length > 0) {\n lines.push(\"### Tool Usage\")\n lines.push(\"\")\n for (const [tool, count] of tools) {\n lines.push(`- ${tool}: ${count.toLocaleString()}`)\n }\n lines.push(\"\")\n }\n\n lines.push(\n \"_Cloud plan/quota information is not available because OpenCode does not expose a public usage API yet. Set `opencode-usage-stats.enableCloudApi: true` once it is supported._\",\n )\n\n return lines.join(\"\\n\")\n}\n"],
|
|
5
|
+
"mappings": ";AACA,SAAS,YAAY;;;AC4BrB,eAAsB,gBACpB,SACgC;AAMhC,SAAO;AACT;;;ACVO,SAAS,cAAc,SAAwD;AACpF,QAAM,MAAO,WAAW,CAAC;AACzB,SAAO;AAAA,IACL,gBAAgB,IAAI,kBAAkB;AAAA,IACtC,aAAa,IAAI,eAAe;AAAA,IAChC,QAAQ,IAAI,UAAU;AAAA,EACxB;AACF;;;ACLA,SAAS,mBAAmB,SAAsE;AAChG,SAAO,UAAU,WAAW,QAAQ,SAAS;AAC/C;AAEA,SAAS,WAAW,MAAqD;AACvE,SAAO,KAAK,SAAS;AACvB;AAEO,SAAS,kBAAkB,UAA0D;AAC1F,QAAM,QAAoB;AAAA,IACxB,eAAe;AAAA,IACf,eAAe,SAAS;AAAA,IACxB,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,mBAAmB;AAAA,IACnB,sBAAsB;AAAA,IACtB,gBAAgB;AAAA,IAChB,iBAAiB;AAAA,IACjB,YAAY,CAAC;AAAA,IACb,WAAW,CAAC;AAAA,EACd;AAEA,aAAW,EAAE,MAAM,MAAM,KAAK,UAAU;AACtC,QAAI,CAAC,mBAAmB,IAAI,EAAG;AAE/B,UAAM,aAAa,KAAK,QAAQ;AAEhC,UAAM,SAAS,KAAK;AACpB,QAAI,QAAQ;AACV,YAAM,oBAAoB,OAAO,SAAS;AAC1C,YAAM,qBAAqB,OAAO,UAAU;AAC5C,YAAM,wBAAwB,OAAO,aAAa;AAClD,YAAM,kBAAkB,OAAO,OAAO,QAAQ;AAC9C,YAAM,mBAAmB,OAAO,OAAO,SAAS;AAAA,IAClD;AAEA,UAAM,WAAW,GAAG,KAAK,UAAU,IAAI,KAAK,OAAO;AACnD,QAAI,CAAC,MAAM,WAAW,QAAQ,GAAG;AAC/B,YAAM,WAAW,QAAQ,IAAI;AAAA,QAC3B,UAAU;AAAA,QACV,aAAa;AAAA,QACb,cAAc;AAAA,QACd,WAAW;AAAA,QACX,YAAY;AAAA,QACZ,MAAM;AAAA,MACR;AAAA,IACF;AACA,UAAM,QAAQ,MAAM,WAAW,QAAQ;AACvC,UAAM,YAAY;AAClB,QAAI,QAAQ;AACV,YAAM,eAAe,OAAO,SAAS;AACrC,YAAM,gBAAgB,OAAO,UAAU;AACvC,YAAM,aAAa,OAAO,OAAO,QAAQ;AACzC,YAAM,cAAc,OAAO,OAAO,SAAS;AAAA,IAC7C;AACA,UAAM,QAAQ,KAAK,QAAQ;AAE3B,eAAW,QAAQ,OAAO;AACxB,UAAI,WAAW,IAAI,GAAG;AACpB,cAAM,UAAU,KAAK,IAAI,KAAK,MAAM,UAAU,KAAK,IAAI,KAAK,KAAK;AAAA,MACnE;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAEA,eAAsB,eAAe,QAA6C;AAChF,QAAM,mBAAmB,MAAM,OAAO,QAAQ,KAAK;AACnD,QAAM,WAAW,MAAM,QAAQ,iBAAiB,IAAI,IAC/C,iBAAiB,OAClB,CAAC;AAEL,QAAM,cAAkD,CAAC;AAEzD,aAAW,WAAW,UAAU;AAC9B,QAAI;AACF,YAAM,WAAW,MAAM,OAAO,QAAQ,SAAS,EAAE,MAAM,EAAE,IAAI,QAAQ,GAAG,EAAE,CAAC;AAC3E,YAAM,WAAW,MAAM,QAAQ,SAAS,IAAI,IAAI,SAAS,OAAO,CAAC;AACjE,kBAAY,KAAK,GAAG,QAAQ;AAAA,IAC9B,SAAS,OAAO;AAEd;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAQ,kBAAkB,WAAW;AAC3C,QAAM,gBAAgB,SAAS;AAC/B,SAAO;AACT;AAEO,SAAS,YAAY,OAA2B;AACrD,QAAM,cACJ,MAAM,mBACN,MAAM,oBACN,MAAM,uBACN,MAAM,iBACN,MAAM;AAER,QAAM,QAAkB;AAAA,IACtB;AAAA,IACA;AAAA,IACA,eAAe,MAAM,cAAc,eAAe,CAAC;AAAA,IACnD,eAAe,MAAM,cAAc,eAAe,CAAC;AAAA,IACnD,kBAAkB,MAAM,UAAU,QAAQ,CAAC,CAAC;AAAA,IAC5C,mBAAmB,YAAY,eAAe,CAAC;AAAA,IAC/C,cAAc,MAAM,iBAAiB,eAAe,CAAC;AAAA,IACrD,eAAe,MAAM,kBAAkB,eAAe,CAAC;AAAA,IACvD,kBAAkB,MAAM,qBAAqB,eAAe,CAAC;AAAA,IAC7D,mBAAmB,MAAM,eAAe,eAAe,CAAC;AAAA,IACxD,oBAAoB,MAAM,gBAAgB,eAAe,CAAC;AAAA,IAC1D;AAAA,EACF;AAEA,QAAM,SAAS,OAAO,QAAQ,MAAM,UAAU,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,EAAE,QAAQ;AAC9F,MAAI,OAAO,SAAS,GAAG;AACrB,UAAM,KAAK,iBAAiB;AAC5B,UAAM,KAAK,EAAE;AACb,eAAW,CAAC,OAAO,KAAK,KAAK,QAAQ;AACnC,YAAM,KAAK,KAAK,KAAK,EAAE;AACvB,YAAM,KAAK,iBAAiB,MAAM,SAAS,eAAe,CAAC,EAAE;AAC7D,YAAM;AAAA,QACJ,gBAAgB,MAAM,cAAc,MAAM,cAAc,eAAe,CAAC,QAAQ,MAAM,YAAY,eAAe,CAAC,UAAU,MAAM,aAAa,eAAe,CAAC;AAAA,MACjK;AACA,YAAM,KAAK,cAAc,MAAM,KAAK,QAAQ,CAAC,CAAC,EAAE;AAAA,IAClD;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,QAAM,QAAQ,OAAO,QAAQ,MAAM,SAAS,EAAE,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC;AAC1E,MAAI,MAAM,SAAS,GAAG;AACpB,UAAM,KAAK,gBAAgB;AAC3B,UAAM,KAAK,EAAE;AACb,eAAW,CAACA,OAAM,KAAK,KAAK,OAAO;AACjC,YAAM,KAAK,KAAKA,KAAI,KAAK,MAAM,eAAe,CAAC,EAAE;AAAA,IACnD;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,QAAM;AAAA,IACJ;AAAA,EACF;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;;;AHxKA,IAAM,eAAe,CAAC,WAAkC;AACtD,SAAO;AAAA,IACL,OAAO,OAAO,SAAiB,UAC7B,OAAO,IAAI,IAAI,EAAE,MAAM,EAAE,SAAS,wBAAwB,OAAO,SAAS,SAAS,MAAM,EAAE,CAAC;AAAA,IAC9F,MAAM,OAAO,SAAiB,UAC5B,OAAO,IAAI,IAAI,EAAE,MAAM,EAAE,SAAS,wBAAwB,OAAO,QAAQ,SAAS,MAAM,EAAE,CAAC;AAAA,IAC7F,MAAM,OAAO,SAAiB,UAC5B,OAAO,IAAI,IAAI,EAAE,MAAM,EAAE,SAAS,wBAAwB,OAAO,QAAQ,SAAS,MAAM,EAAE,CAAC;AAAA,IAC7F,OAAO,OAAO,SAAiB,UAC7B,OAAO,IAAI,IAAI,EAAE,MAAM,EAAE,SAAS,wBAAwB,OAAO,SAAS,SAAS,MAAM,EAAE,CAAC;AAAA,EAChG;AACF;AAEA,IAAM,gBAAwB,OAAO,OAAoB,YAA4B;AACnF,QAAM,SAAS,cAAc,OAAO;AACpC,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,SAAS,aAAa,MAAM;AAElC,QAAM,OAAO,KAAK,sBAAsB,EAAE,gBAAgB,OAAO,eAAe,CAAC;AAEjF,SAAO;AAAA,IACL,MAAM;AAAA,MACJ,OAAO,KAAK;AAAA,QACV,aACE;AAAA,QAEF,MAAM;AAAA,UACJ,OAAO,KAAK,OACT,QAAQ,EACR,SAAS,EACT,SAAS,mDAAmD;AAAA,QACjE;AAAA,QACA,MAAM,QAAQ,MAAM;AAClB,gBAAM,aAAa,KAAK,SAAS,OAAO;AAExC,cAAI,YAAY;AACd,kBAAM,OAAO,KAAK,wBAAwB,EAAE,aAAa,OAAO,YAAY,CAAC;AAC7E,kBAAM,QAAQ,MAAM,gBAAgB,MAAM;AAC1C,gBAAI,OAAO;AACT,qBAAO;AAAA,gBACL,OAAO;AAAA,gBACP,QAAQ;AAAA,kBACN;AAAA,kBACA;AAAA,kBACA,WAAW,MAAM,QAAQ,SAAS;AAAA,kBAClC,qBAAqB,MAAM,iBAAiB,SAAS;AAAA,kBACrD,oBAAoB,MAAM,gBAAgB,SAAS;AAAA,kBACnD,mBAAmB,MAAM,eAAe,SAAS;AAAA,kBACjD,gBAAgB,MAAM,aAAa,SAAS;AAAA,kBAC5C,qBAAqB,MAAM,kBAAkB,SAAY,IAAI,MAAM,cAAc,QAAQ,CAAC,CAAC,KAAK,SAAS;AAAA,gBAC3G,EAAE,KAAK,IAAI;AAAA,cACb;AAAA,YACF;AACA,kBAAM,OAAO,KAAK,yDAAyD;AAAA,UAC7E;AAEA,cAAI;AACF,kBAAM,OAAO,KAAK,yBAAyB;AAC3C,kBAAM,QAAQ,MAAM,eAAe,MAAM;AACzC,kBAAM,OAAO,KAAK,0BAA0B;AAAA,cAC1C,UAAU,MAAM;AAAA,cAChB,UAAU,MAAM;AAAA,cAChB,MAAM,MAAM;AAAA,YACd,CAAC;AACD,mBAAO;AAAA,cACL,OAAO;AAAA,cACP,QAAQ,YAAY,KAAK;AAAA,YAC3B;AAAA,UACF,SAAS,OAAO;AACd,kBAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,kBAAM,OAAO,MAAM,mCAAmC,EAAE,OAAO,QAAQ,CAAC;AACxE,mBAAO;AAAA,cACL,OAAO;AAAA,cACP,QAAQ;AAAA;AAAA,EAAkD,OAAO;AAAA;AAAA;AAAA,YACnE;AAAA,UACF;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,IACA,MAAM,OAAOC,SAAQ;AACnB,MAAAA,QAAO,UAAUA,QAAO,WAAW,CAAC;AACpC,MAAAA,QAAO,QAAQ,QAAQ;AAAA,QACrB,UAAU;AAAA,QACV,aAAa;AAAA,MACf;AACA,YAAM,OAAO,KAAK,2BAA2B;AAAA,IAC/C;AAAA,EACF;AACF;AAEA,IAAM,SAAS;AAAA,EACb,IAAI;AAAA,EACJ,QAAQ;AACV;AAEA,IAAO,gBAAQ;",
|
|
6
|
+
"names": ["tool", "config"]
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,28 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-usage-stats",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "An OpenCode plugin that surfaces local usage statistics and reserves a cloud usage API integration.",
|
|
5
|
-
"main": "dist/index.js",
|
|
6
|
-
"types": "dist/index.d.ts",
|
|
7
5
|
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
8
14
|
"files": [
|
|
9
15
|
"dist",
|
|
10
16
|
"README.md",
|
|
11
17
|
"LICENSE"
|
|
12
18
|
],
|
|
19
|
+
"oc-plugin": [
|
|
20
|
+
"server"
|
|
21
|
+
],
|
|
13
22
|
"scripts": {
|
|
14
|
-
"build": "
|
|
23
|
+
"build": "npm run build:bundle && npm run build:types",
|
|
24
|
+
"build:bundle": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@opencode-ai/plugin --external:@opencode-ai/sdk --external:node:* --sourcemap",
|
|
25
|
+
"build:types": "tsc --emitDeclarationOnly",
|
|
15
26
|
"test": "vitest run",
|
|
16
27
|
"prepublishOnly": "npm run build"
|
|
17
28
|
},
|
|
@@ -39,9 +50,10 @@
|
|
|
39
50
|
"devDependencies": {
|
|
40
51
|
"@opencode-ai/plugin": "^1.18.11",
|
|
41
52
|
"@opencode-ai/sdk": "^1.18.11",
|
|
42
|
-
"@types/node": "^
|
|
43
|
-
"
|
|
44
|
-
"
|
|
53
|
+
"@types/node": "^22.0.0",
|
|
54
|
+
"esbuild": "^0.25.0",
|
|
55
|
+
"typescript": "^7.0.0",
|
|
56
|
+
"vitest": "^4.0.0"
|
|
45
57
|
},
|
|
46
58
|
"engines": {
|
|
47
59
|
"node": ">=18"
|