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 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 is meaningless, so it tracks
5
- **quota windows (5h / weekly / monthly)** and turns them into coaching + loop control.
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 + a deterministic orchestrator.
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 z.ai quota windows via the `codexbar` CLI.
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 z.ai coding-plan provider configured.
29
- - `codexbar` CLI with the z.ai key wired (`codexbar config set-api-key --provider zai --stdin`).
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' "$Z_AI_API_KEY" | codexbar config set-api-key --provider zai --stdin
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** beside `tasks.txt`/`rubric.md`. Each role runs on its model,
77
- so per-model quota is tracked (works for local LLMs — they show 0%).
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": "zai-coding-plan/glm-5.1", // model that produces the work
81
- "grader": "ollama/llama3", // model that grades it
82
- "taskTimeoutMs": 1800000, // per-task timeout (on exceed: auto-split)
83
- "maxRevisions": 2 // revise attempts before giving up
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` | `UC_MODEL` | any `provider/model` opencode knows |
89
- | `grader` | `UC_MODEL` | can differ from generator (multi-model) |
90
- | `taskTimeoutMs` | `1800000` (30m) | exceed task split into subtasks |
91
- | `maxRevisions` | `2` | FAIL revise re-grade, up to N times |
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
- Missing roles fall back to `UC_MODEL`. Provider for quota is derived from the model id
94
- (`zai-coding-plan/...` `zai`, `ollama/...``ollama`).
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` | glm-4.5-air | suggested model when throttling |
105
- | `UC_PROVIDER` | zai | codexbar provider for the guardian |
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 (see `PLAN.md`): never install `solid-js` in the config dir (conflicts with
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
- - ✅ M0–M2 quota guardian + TUI panel (per-provider coach view, colors, collapsible Alt+H)
123
- - ✅ M3 harness: agent mode (triage) with generate/grade model-specific tools (1 terminal, multi-model)
124
- - ✅ M4 npm packaging (`opencode plugin install opencode-usage-coach`)
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
- p = spawn("codexbar", ["usage", "--provider", PROVIDER, "--json"], {
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 (${LIGHTER}) or wait for window reset.`, weekly: wk, monthly: mo, fiveHour: h5 });
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
- const model = cfg.generator ?? "zai-coding-plan/glm-5.1";
360
- const out = await runModel(input.client, model, args.prompt, ctx.directory);
361
- return out ?? `(generator ${model} produced no text)`;
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
- const model = cfg.generator ?? "zai-coding-plan/glm-5.1";
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, model, t.prompt, ctx.directory);
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 ?? "opencode/mimo-v2.5-free";
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 ? t.model.startsWith("zai") ? "zai" : (t.model.split("/")[0] ?? "").split("-")[0] : "";
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",
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
- "zai",
37
- "glm"
36
+ "llm",
37
+ "harness"
38
38
  ],
39
39
  "license": "MIT",
40
40
  "peerDependencies": {