opencode-usage-coach 0.2.3 → 0.2.4
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 +26 -29
- package/dist/index.js +17 -14
- package/dist/tui.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# opencode-usage-coach
|
|
2
2
|
|
|
3
3
|
A closed-loop usage coach and harness for [OpenCode](https://opencode.ai). Built for
|
|
4
|
-
flat-rate coding plans (e.g. z.ai / GLM) where USD cost
|
|
5
|
-
**quota windows (5h / weekly / monthly)** and turns them
|
|
4
|
+
flat-rate coding plans (e.g. z.ai / GLM, or any quota-metered provider) where USD cost
|
|
5
|
+
is meaningless, so it tracks **quota windows (5h / weekly / monthly)** and turns them
|
|
6
|
+
into coaching + loop control. Provider-agnostic — configure via `harness.config.json`.
|
|
6
7
|
|
|
7
8
|
Existing plugins only *display* usage. This one **senses quota → coaches → stops/advances
|
|
8
|
-
the loop** — and ships a harness agent mode
|
|
9
|
+
the loop** — and ships a harness agent mode.
|
|
9
10
|
|
|
10
11
|
## What it does
|
|
11
12
|
|
|
12
13
|
**Always-on guardian (plugin):**
|
|
13
|
-
- Senses
|
|
14
|
+
- Senses provider quota windows via the `codexbar` CLI (config-driven — any provider codexbar knows).
|
|
14
15
|
- On STOP threshold: blocks tool calls (`tool.execute.before` throws) → the agent self-stops.
|
|
15
16
|
- Injects coaching (how to use right now) into the system prompt (double defense).
|
|
16
17
|
- Surfaces a sidebar panel: per-provider quota meters + harness task states + coaching.
|
|
@@ -21,12 +22,11 @@ the loop** — and ships a harness agent mode + a deterministic orchestrator.
|
|
|
21
22
|
unclear → clarify; substantive → generate→grade→revise→advance.
|
|
22
23
|
- Multi-model via plugin tools `generate`/`grade`: run the configured generator/grader model
|
|
23
24
|
(from `harness.config.json`) in a new session on the same server — no second terminal.
|
|
24
|
-
e.g. GLM generation + free mimo grading in one opencode session.
|
|
25
25
|
- Reports progress via `task_update` → the sidebar panel shows live task states.
|
|
26
26
|
|
|
27
27
|
## Requirements
|
|
28
|
-
- opencode (tested on 1.17.13) with a
|
|
29
|
-
- `codexbar` CLI with
|
|
28
|
+
- opencode (tested on 1.17.13) with a quota-metered provider configured.
|
|
29
|
+
- `codexbar` CLI with your provider key wired (e.g. `codexbar config set-api-key --provider zai --stdin`).
|
|
30
30
|
|
|
31
31
|
## Install (from npm)
|
|
32
32
|
```jsonc
|
|
@@ -69,29 +69,29 @@ This plugin has **four config surfaces**. Only the first two are required to run
|
|
|
69
69
|
|
|
70
70
|
### 2. codexbar (quota data source)
|
|
71
71
|
```bash
|
|
72
|
-
printf '%s' "$
|
|
72
|
+
printf '%s' "$YOUR_PROVIDER_API_KEY" | codexbar config set-api-key --provider <id> --stdin
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
### 3. Harness config — `harness.config.json` (role → model) ★ the main one
|
|
76
|
-
Place in the **work directory
|
|
77
|
-
|
|
76
|
+
Place in the **work directory**. Each role runs on its model, so per-model quota is tracked
|
|
77
|
+
(works for local LLMs — they show 0%).
|
|
78
78
|
```jsonc
|
|
79
79
|
{
|
|
80
|
-
"generator": "
|
|
81
|
-
"grader": "
|
|
82
|
-
"
|
|
83
|
-
"
|
|
80
|
+
"generator": "opencode/deepseek-v4-flash-free", // model that produces the work
|
|
81
|
+
"grader": "opencode/mimo-v2.5-free", // model that grades it
|
|
82
|
+
"provider": "", // codexbar quota provider ("" = default)
|
|
83
|
+
"lighterModel": "" // suggested when throttling
|
|
84
84
|
}
|
|
85
85
|
```
|
|
86
86
|
| Field | Default | Notes |
|
|
87
87
|
|---|---|---|
|
|
88
|
-
| `generator` |
|
|
89
|
-
| `grader` | `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
88
|
+
| `generator` | **required** | any `provider/model` opencode knows |
|
|
89
|
+
| `grader` | falls back to `generator` | can differ (multi-model) |
|
|
90
|
+
| `provider` | `""` (codexbar default) | which provider's quota the guardian watches |
|
|
91
|
+
| `lighterModel` | `""` | shown in throttle advice; env `UC_LIGHTER_MODEL` overrides |
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
`generator` is **required** — the tools return a clear error if missing. Copy
|
|
94
|
+
`harness.config.example.json` to get started (both example models are free → never gated).
|
|
95
95
|
|
|
96
96
|
### 4. Env vars (thresholds / tuning)
|
|
97
97
|
| Var | Default | Meaning |
|
|
@@ -101,12 +101,9 @@ Missing roles fall back to `UC_MODEL`. Provider for quota is derived from the mo
|
|
|
101
101
|
| `UC_STOP_WEEKLY` | 95 | weekly STOP % |
|
|
102
102
|
| `UC_THROTTLE_WEEKLY` | 85 | weekly throttle % |
|
|
103
103
|
| `UC_STOP_MONTHLY` | 98 | monthly STOP % |
|
|
104
|
-
| `UC_LIGHTER_MODEL` |
|
|
105
|
-
| `UC_PROVIDER` |
|
|
104
|
+
| `UC_LIGHTER_MODEL` | (config `lighterModel`) | suggested model when throttling |
|
|
105
|
+
| `UC_PROVIDER` | (config `provider`) | codexbar provider for the guardian |
|
|
106
106
|
| `UC_TTL_MS` | 60000 | quota cache TTL (ms) |
|
|
107
|
-
| `UC_MODEL` | zai-coding-plan/glm-5.1 | harness fallback model |
|
|
108
|
-
| `UC_TASK_TIMEOUT_MS` | 1800000 | harness per-task timeout (overridden by config) |
|
|
109
|
-
| `UC_MAX_REVISIONS` | 2 | harness revise attempts (overridden by config) |
|
|
110
107
|
| `UC_DEBUG` | 0 | set to `1` for a diagnostic log at `~/.cache/opencode-usage-coach/coach.log` |
|
|
111
108
|
|
|
112
109
|
## Architecture
|
|
@@ -114,13 +111,13 @@ Missing roles fall back to `UC_MODEL`. Provider for quota is derived from the mo
|
|
|
114
111
|
- **TUI module** (`src/tui.tsx`) — SolidJS, reads a state file, renders into `sidebar_footer`/`home_footer`. Loaded via `tui.json`. Bundled with `tsup` + `esbuild-plugin-solid`, solid kept **external** (resolves to opencode's bundle — avoids the duplicate-instance crash). Exports `{ tui }` (a bare function is misread as a server plugin).
|
|
115
112
|
- server↔TUI communicate via a state file (`~/.cache/opencode-usage-coach/*.json`) — they are separate processes.
|
|
116
113
|
|
|
117
|
-
Key lessons (
|
|
114
|
+
**Key lessons (hard-won):** never install `solid-js` in the config dir (conflicts with
|
|
118
115
|
opencode's bundled solid); TUI plugins must be compiled + loaded via `tui.json` file path;
|
|
119
116
|
`codexbar` must be called via `spawn` (the `$` BunShell leaks output to the TUI).
|
|
120
117
|
|
|
121
118
|
## Status
|
|
122
|
-
- ✅
|
|
123
|
-
- ✅
|
|
124
|
-
- ✅
|
|
119
|
+
- ✅ Quota guardian + TUI panel (per-provider coach view, colors, collapsible Alt+H)
|
|
120
|
+
- ✅ Harness: agent mode (triage) with generate/grade model-specific tools (1 terminal, multi-model)
|
|
121
|
+
- ✅ npm packaging (`opencode plugin install opencode-usage-coach`)
|
|
125
122
|
|
|
126
123
|
License: MIT.
|
package/dist/index.js
CHANGED
|
@@ -106,7 +106,6 @@ async function runModel(client, model, prompt, directory) {
|
|
|
106
106
|
return null;
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
|
-
var PROVIDER = process.env.UC_PROVIDER ?? "zai";
|
|
110
109
|
var num = (e, d) => {
|
|
111
110
|
try {
|
|
112
111
|
const v = Number(process.env[e]);
|
|
@@ -120,7 +119,6 @@ var THR_5H = num("UC_THROTTLE_5H", 70);
|
|
|
120
119
|
var STOP_WK = num("UC_STOP_WEEKLY", 95);
|
|
121
120
|
var THR_WK = num("UC_THROTTLE_WEEKLY", 85);
|
|
122
121
|
var STOP_MO = num("UC_STOP_MONTHLY", 98);
|
|
123
|
-
var LIGHTER = process.env.UC_LIGHTER_MODEL ?? "glm-4.5-air";
|
|
124
122
|
function humanRemaining(iso) {
|
|
125
123
|
try {
|
|
126
124
|
if (!iso) return "";
|
|
@@ -191,12 +189,13 @@ async function fetchProvidersCoach() {
|
|
|
191
189
|
}));
|
|
192
190
|
return results.filter(Boolean);
|
|
193
191
|
}
|
|
194
|
-
function fetchQuota() {
|
|
192
|
+
function fetchQuota(provider) {
|
|
195
193
|
return new Promise((resolve2) => {
|
|
196
194
|
let out = "";
|
|
197
195
|
let p;
|
|
198
196
|
try {
|
|
199
|
-
|
|
197
|
+
const args = provider ? ["usage", "--provider", provider, "--json"] : ["usage", "--json"];
|
|
198
|
+
p = spawn("codexbar", args, {
|
|
200
199
|
stdio: ["ignore", "pipe", "ignore"]
|
|
201
200
|
});
|
|
202
201
|
} catch {
|
|
@@ -219,12 +218,12 @@ function fetchQuota() {
|
|
|
219
218
|
});
|
|
220
219
|
});
|
|
221
220
|
}
|
|
222
|
-
function coach(q) {
|
|
221
|
+
function coach(q, lighter) {
|
|
223
222
|
if (!q) return { decision: "GO", advice: "quota unavailable \u2014 proceeding cautiously.", weekly: -1, monthly: -1, fiveHour: -1 };
|
|
224
223
|
const wk = Math.round(q.weekly?.usedPercent ?? 0), mo = Math.round(q.monthly?.usedPercent ?? 0), h5 = Math.round(q.fiveHour?.usedPercent ?? 0);
|
|
225
224
|
const wkR = humanRemaining(q.weekly?.resetsAt), h5R = humanRemaining(q.fiveHour?.resetsAt);
|
|
226
225
|
const stop = (r) => ({ decision: "STOP", advice: `STOP recommend \u2014 ${r}. window nearly exhausted. stop now or it will be force-blocked.`, weekly: wk, monthly: mo, fiveHour: h5 });
|
|
227
|
-
const thr = (r) => ({ decision: "THROTTLE", advice: `Throttle recommend \u2014 ${r}. switch to lighter model (${
|
|
226
|
+
const thr = (r) => ({ decision: "THROTTLE", advice: `Throttle recommend \u2014 ${r}. switch to lighter model (${lighter}) or wait for window reset.`, weekly: wk, monthly: mo, fiveHour: h5 });
|
|
228
227
|
if (h5 >= STOP_5H) return stop(`5h window ${h5}% (${h5R})`);
|
|
229
228
|
if (wk >= STOP_WK) return stop(`weekly ${wk}% (${wkR})`);
|
|
230
229
|
if (mo >= STOP_MO) return stop(`monthly ${mo}%`);
|
|
@@ -236,6 +235,9 @@ var LOADING = { decision: "GO", advice: "quota loading\u2026", weekly: -1, month
|
|
|
236
235
|
async function UsageCoachPlugin(input) {
|
|
237
236
|
try {
|
|
238
237
|
setStateDir(input.directory);
|
|
238
|
+
const cfg0 = readHarnessCfg(input.directory);
|
|
239
|
+
const PROVIDER = process.env.UC_PROVIDER ?? cfg0.provider ?? "";
|
|
240
|
+
const LIGHTER = process.env.UC_LIGHTER_MODEL ?? cfg0.lighterModel ?? "a lighter model";
|
|
239
241
|
let last = null;
|
|
240
242
|
let lastFetchedAt = 0;
|
|
241
243
|
let refreshing = false;
|
|
@@ -244,9 +246,9 @@ async function UsageCoachPlugin(input) {
|
|
|
244
246
|
if (refreshing) return;
|
|
245
247
|
if (last && Date.now() - lastFetchedAt < TTL_MS) return;
|
|
246
248
|
refreshing = true;
|
|
247
|
-
fetchQuota().then(async (q) => {
|
|
249
|
+
fetchQuota(PROVIDER).then(async (q) => {
|
|
248
250
|
try {
|
|
249
|
-
last = coach(q);
|
|
251
|
+
last = coach(q, LIGHTER);
|
|
250
252
|
lastFetchedAt = Date.now();
|
|
251
253
|
let providers = [];
|
|
252
254
|
try {
|
|
@@ -356,9 +358,9 @@ async function UsageCoachPlugin(input) {
|
|
|
356
358
|
args: { prompt: tool.schema.string() },
|
|
357
359
|
async execute(args, ctx) {
|
|
358
360
|
const cfg = readHarnessCfg(ctx.directory);
|
|
359
|
-
|
|
360
|
-
const out = await runModel(input.client,
|
|
361
|
-
return out ?? `(generator ${
|
|
361
|
+
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
362
|
+
const out = await runModel(input.client, cfg.generator, args.prompt, ctx.directory);
|
|
363
|
+
return out ?? `(generator ${cfg.generator} produced no text)`;
|
|
362
364
|
}
|
|
363
365
|
}),
|
|
364
366
|
generate_batch: tool({
|
|
@@ -366,9 +368,9 @@ async function UsageCoachPlugin(input) {
|
|
|
366
368
|
args: { tasks: tool.schema.array(tool.schema.object({ id: tool.schema.number(), prompt: tool.schema.string() })) },
|
|
367
369
|
async execute(args, ctx) {
|
|
368
370
|
const cfg = readHarnessCfg(ctx.directory);
|
|
369
|
-
|
|
371
|
+
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
370
372
|
const results = await Promise.all(args.tasks.map(async (t) => {
|
|
371
|
-
const out = await runModel(input.client,
|
|
373
|
+
const out = await runModel(input.client, cfg.generator, t.prompt, ctx.directory);
|
|
372
374
|
return `[task ${t.id}] ${out ?? "(no output)"}`;
|
|
373
375
|
}));
|
|
374
376
|
return results.join("\n\n");
|
|
@@ -379,7 +381,8 @@ async function UsageCoachPlugin(input) {
|
|
|
379
381
|
args: { prompt: tool.schema.string() },
|
|
380
382
|
async execute(args, ctx) {
|
|
381
383
|
const cfg = readHarnessCfg(ctx.directory);
|
|
382
|
-
const model = cfg.grader ?? cfg.generator
|
|
384
|
+
const model = cfg.grader ?? cfg.generator;
|
|
385
|
+
if (!model) return 'ERROR: no grader/generator model configured. Set "grader" (and "generator") in harness.config.json (see harness.config.example.json).';
|
|
383
386
|
const out = await runModel(input.client, model, args.prompt, ctx.directory);
|
|
384
387
|
return out ?? `(grader ${model} produced no text)`;
|
|
385
388
|
}
|
package/dist/tui.js
CHANGED
|
@@ -334,7 +334,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
334
334
|
_$effect((_$p) => _$setProp(_el$59, "style", st(sKey), _$p));
|
|
335
335
|
return _el$59;
|
|
336
336
|
})());
|
|
337
|
-
const pv = t.model ?
|
|
337
|
+
const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
|
|
338
338
|
const q = pv && h.quotas?.[pv] ? h.quotas[pv] : null;
|
|
339
339
|
const pct = q ? q.fiveHour : 0;
|
|
340
340
|
nodes.push((() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-usage-coach",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "opencode closed-loop usage coach \u2014 quota SENSE -> coaching DECIDE -> loop ACT + TUI integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"usage",
|
|
34
34
|
"coach",
|
|
35
35
|
"loop",
|
|
36
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"llm",
|
|
37
|
+
"harness"
|
|
38
38
|
],
|
|
39
39
|
"license": "MIT",
|
|
40
40
|
"peerDependencies": {
|