opencode-usage-report 0.4.1 → 0.5.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 +6 -0
- package/package.json +1 -1
- package/src/index.ts +172 -16
package/README.md
CHANGED
|
@@ -71,6 +71,12 @@ Or add the plugin manually to each file and restart opencode:
|
|
|
71
71
|
> `opencode plugin opencode-usage-report` prints `Detected server + tui targets`
|
|
72
72
|
> when the resolution works.
|
|
73
73
|
|
|
74
|
+
> **OpenCode 2.x (0.5.0).** The server entrypoint default-exports the V2
|
|
75
|
+
> `{ id, setup }` definition and still carries the V1 `server` hook, so
|
|
76
|
+
> OpenCode 1.18.29+ and 2.x both load it. Before 0.5.0 the module exported a
|
|
77
|
+
> bare V1 function, which OpenCode 2 rejects. The TUI entrypoint is unchanged
|
|
78
|
+
> in this release.
|
|
79
|
+
|
|
74
80
|
Options can be passed in the tuple form, independently per entrypoint:
|
|
75
81
|
|
|
76
82
|
```jsonc
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { Hooks, PluginInput } from "@opencode-ai/plugin";
|
|
2
2
|
import { tool } from "@opencode-ai/plugin";
|
|
3
3
|
import { DEFAULT_OPTIONS, collectReports } from "./report";
|
|
4
4
|
import { renderJson, renderText } from "./render";
|
|
@@ -39,12 +39,16 @@ export function coerceOptions(raw: Record<string, unknown> | undefined): PluginO
|
|
|
39
39
|
return opts;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Throttled warning pass shared by the V1 and V2 plugin APIs (spec §3.6/§3.7).
|
|
44
|
+
* Every failure is swallowed (spec §4): it must never throw into the event bus.
|
|
45
|
+
*/
|
|
46
|
+
function makeWarnRunner(
|
|
47
|
+
opts: PluginOptions,
|
|
48
|
+
notify: (message: string) => Promise<unknown> | unknown,
|
|
49
|
+
): () => Promise<void> {
|
|
44
50
|
let lastWarnCheck = 0;
|
|
45
|
-
|
|
46
|
-
/** Throttled warning pass (spec §3.7). Every failure is swallowed (spec §4). */
|
|
47
|
-
const runWarnCheck = async (): Promise<void> => {
|
|
51
|
+
return async (): Promise<void> => {
|
|
48
52
|
try {
|
|
49
53
|
if (Date.now() - lastWarnCheck < WARN_THROTTLE_MS) return;
|
|
50
54
|
lastWarnCheck = Date.now();
|
|
@@ -53,9 +57,7 @@ const plugin: Plugin = async ({ client }, options) => {
|
|
|
53
57
|
const hits = await checkWarnings(reports, opts.thresholdPercent);
|
|
54
58
|
for (const hit of hits) {
|
|
55
59
|
try {
|
|
56
|
-
await
|
|
57
|
-
body: { title: "Usage warning", message: hit.message, variant: "warning" },
|
|
58
|
-
});
|
|
60
|
+
await notify(hit.message);
|
|
59
61
|
} catch {
|
|
60
62
|
// Toast delivery is best-effort; it is unavailable in headless/server mode.
|
|
61
63
|
}
|
|
@@ -64,6 +66,159 @@ const plugin: Plugin = async ({ client }, options) => {
|
|
|
64
66
|
// Never throw into the event bus.
|
|
65
67
|
}
|
|
66
68
|
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function runUsageReport(
|
|
72
|
+
args: Record<string, unknown> | undefined,
|
|
73
|
+
opts: PluginOptions,
|
|
74
|
+
): Promise<{ content: string }> {
|
|
75
|
+
const reports = await collectReports({
|
|
76
|
+
providers: args?.provider ? [String(args.provider)] : undefined,
|
|
77
|
+
refresh: args?.refresh === true,
|
|
78
|
+
options: opts,
|
|
79
|
+
});
|
|
80
|
+
return { content: args?.json === true ? renderJson(reports) : renderText(reports) };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const TOOL_INPUT_SCHEMA: Record<string, unknown> = {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
provider: { type: "string" },
|
|
87
|
+
json: { type: "boolean" },
|
|
88
|
+
refresh: { type: "boolean" },
|
|
89
|
+
},
|
|
90
|
+
additionalProperties: false,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// OpenCode 2.x (V2) plugin API.
|
|
95
|
+
//
|
|
96
|
+
// The harness decodes the module's default export against `{ id, setup }` or
|
|
97
|
+
// `{ id, effect }` and otherwise fails with "Plugin must export a default
|
|
98
|
+
// definition with an id and an effect or setup function" (err_b890e30e). The
|
|
99
|
+
// context types are declared locally so the package needs no runtime dependency
|
|
100
|
+
// on the V2 SDK; opencode erases `import type` anyway.
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
interface V2ToolDefinition {
|
|
104
|
+
name: string;
|
|
105
|
+
description: string;
|
|
106
|
+
input: Record<string, unknown>;
|
|
107
|
+
execute: (args: Record<string, unknown> | undefined) => Promise<{ content: string }>;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface V2ToolEditor {
|
|
111
|
+
add(definition: V2ToolDefinition): void;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface V2ContextEvent {
|
|
115
|
+
sessionID?: string;
|
|
116
|
+
model?: { providerID?: string; id?: string };
|
|
117
|
+
system?: Array<{ type?: string; text?: string } | string>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
interface V2PluginContext {
|
|
121
|
+
options?: Record<string, unknown>;
|
|
122
|
+
tool: { transform: (callback: (editor: V2ToolEditor) => void) => Promise<unknown> };
|
|
123
|
+
session: {
|
|
124
|
+
hook: (name: "context", handler: (event: V2ContextEvent) => unknown) => Promise<unknown>;
|
|
125
|
+
};
|
|
126
|
+
event: { subscribe: (options: { signal: AbortSignal }) => AsyncIterable<{ type?: string }> };
|
|
127
|
+
/** Not part of the V2 server context; present only if the runtime exposes it. */
|
|
128
|
+
tui?: { showToast?: (input: unknown) => Promise<unknown> };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Joins V2 system parts (text parts plus plain strings) into one prompt string. */
|
|
132
|
+
function joinSystem(event: V2ContextEvent | undefined): string {
|
|
133
|
+
if (!Array.isArray(event?.system)) return "";
|
|
134
|
+
return event.system
|
|
135
|
+
.map((part) => (typeof part === "string" ? part : (part?.text ?? "")))
|
|
136
|
+
.join("\n");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Best-effort capture of the assembled system prompt size (spec §9.1). */
|
|
140
|
+
async function captureSystemFromContext(event: V2ContextEvent | undefined): Promise<void> {
|
|
141
|
+
try {
|
|
142
|
+
const sessionID = event?.sessionID;
|
|
143
|
+
const model = event?.model;
|
|
144
|
+
if (typeof sessionID !== "string" || sessionID === "") return;
|
|
145
|
+
if (
|
|
146
|
+
model === undefined ||
|
|
147
|
+
typeof model.providerID !== "string" ||
|
|
148
|
+
typeof model.id !== "string"
|
|
149
|
+
) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const system = joinSystem(event);
|
|
154
|
+
if (system === "") return;
|
|
155
|
+
|
|
156
|
+
await writeCapture({
|
|
157
|
+
version: 1,
|
|
158
|
+
sessionID,
|
|
159
|
+
providerID: model.providerID,
|
|
160
|
+
modelID: model.id,
|
|
161
|
+
systemChars: system.length,
|
|
162
|
+
systemTokens: estimateTokens(system),
|
|
163
|
+
capturedAt: Date.now(),
|
|
164
|
+
});
|
|
165
|
+
} catch {
|
|
166
|
+
// Best-effort: an experimental hook must never be able to break a request.
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function setup(ctx: V2PluginContext): Promise<() => void> {
|
|
171
|
+
const opts = coerceOptions(ctx?.options);
|
|
172
|
+
|
|
173
|
+
await ctx.tool.transform((editor) => {
|
|
174
|
+
editor.add({
|
|
175
|
+
name: "usage_report",
|
|
176
|
+
description: TOOL_DESCRIPTION,
|
|
177
|
+
input: TOOL_INPUT_SCHEMA,
|
|
178
|
+
execute: (args) => runUsageReport(args, opts),
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
await ctx.session.hook("context", (event) => captureSystemFromContext(event));
|
|
183
|
+
|
|
184
|
+
const runWarnCheck = makeWarnRunner(opts, (message) => {
|
|
185
|
+
const showToast = ctx.tui?.showToast;
|
|
186
|
+
if (typeof showToast !== "function") return;
|
|
187
|
+
return showToast({ body: { title: "Usage warning", message, variant: "warning" } });
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// Spec §3.6: one throttled warning check at startup, fire-and-forget.
|
|
191
|
+
setTimeout(() => {
|
|
192
|
+
void runWarnCheck();
|
|
193
|
+
}, 0);
|
|
194
|
+
|
|
195
|
+
const controller = new AbortController();
|
|
196
|
+
void (async () => {
|
|
197
|
+
try {
|
|
198
|
+
for await (const event of ctx.event.subscribe({ signal: controller.signal })) {
|
|
199
|
+
if (event?.type === "session.idle") await runWarnCheck();
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
// Subscription aborted on unload, or the event transport failed.
|
|
203
|
+
}
|
|
204
|
+
})();
|
|
205
|
+
|
|
206
|
+
return () => controller.abort();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
// OpenCode 1.x (V1) plugin API. Kept from the previous release so packages can
|
|
211
|
+
// serve both APIs during the V2 migration; V2 calls `setup`, V1 calls `server`.
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
async function server({ client }: PluginInput, options?: Record<string, unknown>): Promise<Hooks> {
|
|
215
|
+
const opts = coerceOptions(options);
|
|
216
|
+
|
|
217
|
+
const runWarnCheck = makeWarnRunner(opts, (message) =>
|
|
218
|
+
client.tui.showToast({
|
|
219
|
+
body: { title: "Usage warning", message, variant: "warning" },
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
67
222
|
|
|
68
223
|
// Spec §3.6: one throttled warning check at startup, fire-and-forget.
|
|
69
224
|
setTimeout(() => {
|
|
@@ -80,12 +235,7 @@ const plugin: Plugin = async ({ client }, options) => {
|
|
|
80
235
|
refresh: tool.schema.boolean().optional(),
|
|
81
236
|
},
|
|
82
237
|
async execute(args) {
|
|
83
|
-
|
|
84
|
-
providers: args.provider ? [args.provider] : undefined,
|
|
85
|
-
refresh: args.refresh === true,
|
|
86
|
-
options: opts,
|
|
87
|
-
});
|
|
88
|
-
return args.json ? renderJson(reports) : renderText(reports);
|
|
238
|
+
return (await runUsageReport(args, opts)).content;
|
|
89
239
|
},
|
|
90
240
|
}),
|
|
91
241
|
},
|
|
@@ -126,6 +276,12 @@ const plugin: Plugin = async ({ client }, options) => {
|
|
|
126
276
|
}
|
|
127
277
|
},
|
|
128
278
|
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const definition = {
|
|
282
|
+
id: "opencode-usage-report",
|
|
283
|
+
setup,
|
|
284
|
+
server,
|
|
129
285
|
};
|
|
130
286
|
|
|
131
|
-
export default
|
|
287
|
+
export default definition;
|