opencode-usage-stats 0.1.0 → 0.3.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 +23 -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/dist/tui.d.ts +22 -0
- package/dist/tui.d.ts.map +1 -0
- package/dist/tui.js +143 -0
- package/dist/tui.js.map +7 -0
- package/package.json +27 -7
package/README.md
CHANGED
|
@@ -17,13 +17,34 @@ OpenCode will install the npm package automatically on startup.
|
|
|
17
17
|
|
|
18
18
|
## Use
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
The plugin provides both a TUI sidebar panel and a slash command.
|
|
21
|
+
|
|
22
|
+
### TUI sidebar
|
|
23
|
+
|
|
24
|
+
When OpenCode starts, a small **Usage** panel appears in the right sidebar showing the current session's live stats:
|
|
25
|
+
|
|
26
|
+
- Messages
|
|
27
|
+
- Cost
|
|
28
|
+
- Tokens
|
|
29
|
+
- Top model
|
|
30
|
+
|
|
31
|
+
It updates automatically as new assistant messages arrive.
|
|
32
|
+
|
|
33
|
+
### Slash command
|
|
34
|
+
|
|
35
|
+
Run the slash command in any session:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
/usage
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or ask the agent:
|
|
21
42
|
|
|
22
43
|
```
|
|
23
44
|
Show my OpenCode usage
|
|
24
45
|
```
|
|
25
46
|
|
|
26
|
-
|
|
47
|
+
This returns a markdown summary of all sessions, messages, tokens, cost, model/tool breakdown.
|
|
27
48
|
|
|
28
49
|
## Configuration
|
|
29
50
|
|
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": "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",
|
|
6
|
+
"names": ["tool", "config"]
|
|
7
|
+
}
|
package/dist/tui.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { TuiPluginModule } from "@opencode-ai/plugin/tui";
|
|
2
|
+
import type { Message } from "@opencode-ai/sdk/v2";
|
|
3
|
+
export interface CurrentSessionStats {
|
|
4
|
+
messages: number;
|
|
5
|
+
cost: number;
|
|
6
|
+
inputTokens: number;
|
|
7
|
+
outputTokens: number;
|
|
8
|
+
reasoningTokens: number;
|
|
9
|
+
cacheRead: number;
|
|
10
|
+
cacheWrite: number;
|
|
11
|
+
models: Array<{
|
|
12
|
+
key: string;
|
|
13
|
+
messages: number;
|
|
14
|
+
cost: number;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare function aggregateCurrentSessionMessages(messages: ReadonlyArray<Message>): CurrentSessionStats;
|
|
18
|
+
declare const plugin: TuiPluginModule & {
|
|
19
|
+
id: string;
|
|
20
|
+
};
|
|
21
|
+
export default plugin;
|
|
22
|
+
//# sourceMappingURL=tui.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAgB,eAAe,EAAE,MAAM,yBAAyB,CAAA;AAC5E,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAKlD,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,eAAe,EAAE,MAAM,CAAA;IACvB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/D;AAmCD,wBAAgB,+BAA+B,CAC7C,QAAQ,EAAE,aAAa,CAAC,OAAO,CAAC,GAC/B,mBAAmB,CA0CrB;AAwCD,QAAA,MAAM,MAAM,EAAE,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAqD3C,CAAA;eAEc,MAAM"}
|
package/dist/tui.js
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// src/tui.ts
|
|
2
|
+
import { createElement, insert, setProp } from "@opentui/solid";
|
|
3
|
+
import { createSignal } from "solid-js";
|
|
4
|
+
function element(tag, props, children = []) {
|
|
5
|
+
const node = createElement(tag);
|
|
6
|
+
for (const [key, value] of Object.entries(props)) {
|
|
7
|
+
if (value !== void 0) setProp(node, key, value);
|
|
8
|
+
}
|
|
9
|
+
for (const child of children) {
|
|
10
|
+
if (child !== null && child !== void 0 && child !== false) {
|
|
11
|
+
insert(node, child);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return node;
|
|
15
|
+
}
|
|
16
|
+
function box(props, children = []) {
|
|
17
|
+
return element("box", props, children);
|
|
18
|
+
}
|
|
19
|
+
function text(props, children = []) {
|
|
20
|
+
return element("text", props, children);
|
|
21
|
+
}
|
|
22
|
+
function formatNumber(num) {
|
|
23
|
+
if (num >= 1e6) return `${(num / 1e6).toFixed(1)}M`;
|
|
24
|
+
if (num >= 1e3) return `${(num / 1e3).toFixed(1)}K`;
|
|
25
|
+
return num.toLocaleString();
|
|
26
|
+
}
|
|
27
|
+
function aggregateCurrentSessionMessages(messages) {
|
|
28
|
+
const stats = {
|
|
29
|
+
messages: 0,
|
|
30
|
+
cost: 0,
|
|
31
|
+
inputTokens: 0,
|
|
32
|
+
outputTokens: 0,
|
|
33
|
+
reasoningTokens: 0,
|
|
34
|
+
cacheRead: 0,
|
|
35
|
+
cacheWrite: 0,
|
|
36
|
+
models: []
|
|
37
|
+
};
|
|
38
|
+
const modelMap = {};
|
|
39
|
+
for (const message of messages) {
|
|
40
|
+
if (message.role !== "assistant") continue;
|
|
41
|
+
stats.messages += 1;
|
|
42
|
+
stats.cost += message.cost ?? 0;
|
|
43
|
+
const tokens = message.tokens;
|
|
44
|
+
if (tokens) {
|
|
45
|
+
stats.inputTokens += tokens.input ?? 0;
|
|
46
|
+
stats.outputTokens += tokens.output ?? 0;
|
|
47
|
+
stats.reasoningTokens += tokens.reasoning ?? 0;
|
|
48
|
+
stats.cacheRead += tokens.cache?.read ?? 0;
|
|
49
|
+
stats.cacheWrite += tokens.cache?.write ?? 0;
|
|
50
|
+
}
|
|
51
|
+
const key = `${message.providerID}/${message.modelID}`;
|
|
52
|
+
if (!modelMap[key]) {
|
|
53
|
+
modelMap[key] = { messages: 0, cost: 0 };
|
|
54
|
+
}
|
|
55
|
+
modelMap[key].messages += 1;
|
|
56
|
+
modelMap[key].cost += message.cost ?? 0;
|
|
57
|
+
}
|
|
58
|
+
stats.models = Object.entries(modelMap).sort(([, a], [, b]) => b.messages - a.messages).map(([key, usage]) => ({ key, ...usage }));
|
|
59
|
+
return stats;
|
|
60
|
+
}
|
|
61
|
+
function renderStatsPanel(stats, theme) {
|
|
62
|
+
const totalTokens = stats.inputTokens + stats.outputTokens + stats.reasoningTokens + stats.cacheRead + stats.cacheWrite;
|
|
63
|
+
const lines = [
|
|
64
|
+
{ label: "Messages", value: stats.messages.toLocaleString() },
|
|
65
|
+
{ label: "Cost", value: `$${stats.cost.toFixed(4)}` },
|
|
66
|
+
{ label: "Tokens", value: formatNumber(totalTokens) }
|
|
67
|
+
];
|
|
68
|
+
if (stats.models.length > 0) {
|
|
69
|
+
lines.push({ label: "Model", value: stats.models[0].key.split("/").pop() ?? stats.models[0].key });
|
|
70
|
+
}
|
|
71
|
+
return box(
|
|
72
|
+
{
|
|
73
|
+
width: "100%",
|
|
74
|
+
flexDirection: "column",
|
|
75
|
+
paddingTop: 1,
|
|
76
|
+
paddingBottom: 1,
|
|
77
|
+
paddingLeft: 1,
|
|
78
|
+
paddingRight: 1
|
|
79
|
+
},
|
|
80
|
+
[
|
|
81
|
+
text({ fg: theme.text, bold: true }, ["Usage"]),
|
|
82
|
+
...lines.map(
|
|
83
|
+
(line) => box({ width: "100%", flexDirection: "row", justifyContent: "space-between" }, [
|
|
84
|
+
text({ fg: theme.textMuted }, [`${line.label}:`]),
|
|
85
|
+
text({ fg: line.accent ? theme.accent : theme.text }, [line.value])
|
|
86
|
+
])
|
|
87
|
+
)
|
|
88
|
+
]
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
var plugin = {
|
|
92
|
+
id: "opencode-usage-stats:tui",
|
|
93
|
+
tui: async (api) => {
|
|
94
|
+
const [getStats, setStats] = createSignal({
|
|
95
|
+
messages: 0,
|
|
96
|
+
cost: 0,
|
|
97
|
+
inputTokens: 0,
|
|
98
|
+
outputTokens: 0,
|
|
99
|
+
reasoningTokens: 0,
|
|
100
|
+
cacheRead: 0,
|
|
101
|
+
cacheWrite: 0,
|
|
102
|
+
models: []
|
|
103
|
+
});
|
|
104
|
+
const [getSessionID, setSessionID] = createSignal("");
|
|
105
|
+
const refresh = () => {
|
|
106
|
+
const sessionID = getSessionID();
|
|
107
|
+
if (!sessionID) return;
|
|
108
|
+
try {
|
|
109
|
+
const messages = api.state.session.messages(sessionID);
|
|
110
|
+
const stats = aggregateCurrentSessionMessages(messages);
|
|
111
|
+
setStats(stats);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
api.event.on("message.updated", refresh);
|
|
116
|
+
api.event.on("message.part.updated", refresh);
|
|
117
|
+
api.event.on("session.status", refresh);
|
|
118
|
+
api.slots.register({
|
|
119
|
+
order: 100,
|
|
120
|
+
slots: {
|
|
121
|
+
sidebar_content(_ctx, data) {
|
|
122
|
+
if (data.session_id !== getSessionID()) {
|
|
123
|
+
setSessionID(data.session_id);
|
|
124
|
+
refresh();
|
|
125
|
+
}
|
|
126
|
+
const theme = api.theme.current;
|
|
127
|
+
const stats = getStats();
|
|
128
|
+
return renderStatsPanel(stats, {
|
|
129
|
+
text: theme.text,
|
|
130
|
+
textMuted: theme.textMuted,
|
|
131
|
+
accent: theme.accent
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
var tui_default = plugin;
|
|
139
|
+
export {
|
|
140
|
+
aggregateCurrentSessionMessages,
|
|
141
|
+
tui_default as default
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=tui.js.map
|
package/dist/tui.js.map
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/tui.ts"],
|
|
4
|
+
"sourcesContent": ["import type { TuiPluginApi, TuiPluginModule } from \"@opencode-ai/plugin/tui\"\nimport type { Message } from \"@opencode-ai/sdk/v2\"\nimport type { JSX } from \"@opentui/solid\"\nimport { createElement, insert, setProp } from \"@opentui/solid\"\nimport { createSignal } from \"solid-js\"\n\nexport interface CurrentSessionStats {\n messages: number\n cost: number\n inputTokens: number\n outputTokens: number\n reasoningTokens: number\n cacheRead: number\n cacheWrite: number\n models: Array<{ key: string; messages: number; cost: number }>\n}\n\ntype Child = JSX.Element | string | number | null | undefined | false\n\nfunction element(\n tag: string,\n props: Record<string, unknown>,\n children: Child[] = [],\n): JSX.Element {\n const node = createElement(tag)\n for (const [key, value] of Object.entries(props)) {\n if (value !== undefined) setProp(node, key, value)\n }\n for (const child of children) {\n if (child !== null && child !== undefined && child !== false) {\n insert(node, child)\n }\n }\n return node as JSX.Element\n}\n\nfunction box(props: Record<string, unknown>, children: Child[] = []): JSX.Element {\n return element(\"box\", props, children)\n}\n\nfunction text(props: Record<string, unknown>, children: Child[] = []): JSX.Element {\n return element(\"text\", props, children)\n}\n\nfunction formatNumber(num: number): string {\n if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`\n if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`\n return num.toLocaleString()\n}\n\nexport function aggregateCurrentSessionMessages(\n messages: ReadonlyArray<Message>,\n): CurrentSessionStats {\n const stats: CurrentSessionStats = {\n messages: 0,\n cost: 0,\n inputTokens: 0,\n outputTokens: 0,\n reasoningTokens: 0,\n cacheRead: 0,\n cacheWrite: 0,\n models: [],\n }\n\n const modelMap: Record<string, { messages: number; cost: number }> = {}\n\n for (const message of messages) {\n if (message.role !== \"assistant\") continue\n\n stats.messages += 1\n stats.cost += message.cost ?? 0\n\n const tokens = message.tokens\n if (tokens) {\n stats.inputTokens += tokens.input ?? 0\n stats.outputTokens += tokens.output ?? 0\n stats.reasoningTokens += tokens.reasoning ?? 0\n stats.cacheRead += tokens.cache?.read ?? 0\n stats.cacheWrite += tokens.cache?.write ?? 0\n }\n\n const key = `${message.providerID}/${message.modelID}`\n if (!modelMap[key]) {\n modelMap[key] = { messages: 0, cost: 0 }\n }\n modelMap[key].messages += 1\n modelMap[key].cost += message.cost ?? 0\n }\n\n stats.models = Object.entries(modelMap)\n .sort(([, a], [, b]) => b.messages - a.messages)\n .map(([key, usage]) => ({ key, ...usage }))\n\n return stats\n}\n\nfunction renderStatsPanel(\n stats: CurrentSessionStats,\n theme: { text: unknown; textMuted: unknown; accent: unknown },\n): JSX.Element {\n const totalTokens =\n stats.inputTokens + stats.outputTokens + stats.reasoningTokens + stats.cacheRead + stats.cacheWrite\n\n const lines: Array<{ label: string; value: string; accent?: boolean }> = [\n { label: \"Messages\", value: stats.messages.toLocaleString() },\n { label: \"Cost\", value: `$${stats.cost.toFixed(4)}` },\n { label: \"Tokens\", value: formatNumber(totalTokens) },\n ]\n\n if (stats.models.length > 0) {\n lines.push({ label: \"Model\", value: stats.models[0].key.split(\"/\").pop() ?? stats.models[0].key })\n }\n\n return box(\n {\n width: \"100%\",\n flexDirection: \"column\",\n paddingTop: 1,\n paddingBottom: 1,\n paddingLeft: 1,\n paddingRight: 1,\n },\n [\n text({ fg: theme.text, bold: true }, [\"Usage\"]),\n ...lines.map((line) =>\n box({ width: \"100%\", flexDirection: \"row\", justifyContent: \"space-between\" }, [\n text({ fg: theme.textMuted }, [`${line.label}:`]),\n text({ fg: line.accent ? theme.accent : theme.text }, [line.value]),\n ]),\n ),\n ],\n )\n}\n\nconst plugin: TuiPluginModule & { id: string } = {\n id: \"opencode-usage-stats:tui\",\n tui: async (api) => {\n const [getStats, setStats] = createSignal<CurrentSessionStats>({\n messages: 0,\n cost: 0,\n inputTokens: 0,\n outputTokens: 0,\n reasoningTokens: 0,\n cacheRead: 0,\n cacheWrite: 0,\n models: [],\n })\n\n const [getSessionID, setSessionID] = createSignal<string>(\"\")\n\n const refresh = (): void => {\n const sessionID = getSessionID()\n if (!sessionID) return\n try {\n const messages = api.state.session.messages(sessionID)\n const stats = aggregateCurrentSessionMessages(messages)\n setStats(stats)\n } catch (error) {\n // Ignore refresh errors; the session may not be ready yet.\n }\n }\n\n api.event.on(\"message.updated\", refresh)\n api.event.on(\"message.part.updated\", refresh)\n api.event.on(\"session.status\", refresh)\n\n api.slots.register({\n order: 100,\n slots: {\n sidebar_content(_ctx, data: { session_id: string }) {\n if (data.session_id !== getSessionID()) {\n setSessionID(data.session_id)\n refresh()\n }\n\n const theme = api.theme.current\n const stats = getStats()\n\n return renderStatsPanel(stats, {\n text: theme.text,\n textMuted: theme.textMuted,\n accent: theme.accent,\n })\n },\n },\n })\n },\n}\n\nexport default plugin\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-usage-stats",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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
|
+
"./tui": {
|
|
14
|
+
"import": "./dist/tui.js"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
8
17
|
"files": [
|
|
9
18
|
"dist",
|
|
10
19
|
"README.md",
|
|
11
20
|
"LICENSE"
|
|
12
21
|
],
|
|
22
|
+
"oc-plugin": [
|
|
23
|
+
"server",
|
|
24
|
+
"tui"
|
|
25
|
+
],
|
|
13
26
|
"scripts": {
|
|
14
|
-
"build": "
|
|
27
|
+
"build": "npm run build:server && npm run build:tui && npm run build:types",
|
|
28
|
+
"build:server": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@opencode-ai/plugin --external:@opencode-ai/sdk --external:node:* --sourcemap",
|
|
29
|
+
"build:tui": "esbuild src/tui.ts --bundle --platform=node --format=esm --outfile=dist/tui.js --external:@opencode-ai/plugin --external:@opentui/solid --external:solid-js --sourcemap",
|
|
30
|
+
"build:types": "tsc --emitDeclarationOnly",
|
|
15
31
|
"test": "vitest run",
|
|
16
32
|
"prepublishOnly": "npm run build"
|
|
17
33
|
},
|
|
@@ -39,9 +55,13 @@
|
|
|
39
55
|
"devDependencies": {
|
|
40
56
|
"@opencode-ai/plugin": "^1.18.11",
|
|
41
57
|
"@opencode-ai/sdk": "^1.18.11",
|
|
42
|
-
"@
|
|
43
|
-
"
|
|
44
|
-
"
|
|
58
|
+
"@opentui/keymap": "^0.4.5",
|
|
59
|
+
"@opentui/solid": "^0.4.5",
|
|
60
|
+
"@types/node": "^22.0.0",
|
|
61
|
+
"esbuild": "^0.25.0",
|
|
62
|
+
"solid-js": "^1.9.14",
|
|
63
|
+
"typescript": "^7.0.0",
|
|
64
|
+
"vitest": "^4.0.0"
|
|
45
65
|
},
|
|
46
66
|
"engines": {
|
|
47
67
|
"node": ">=18"
|