opencode-usage-coach 0.12.1 → 0.13.1
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 +12 -5
- package/dist/cli.js +564 -0
- package/dist/index.js +115 -69
- package/dist/tui.js +4 -4
- package/package.json +8 -1
package/README.md
CHANGED
|
@@ -33,16 +33,23 @@ See **[docs/architecture.md](docs/architecture.md)** for the full design.
|
|
|
33
33
|
{ "$schema": "https://opencode.ai/tui.json", "plugin": ["opencode-usage-coach/tui"] }
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
Then
|
|
36
|
+
Then run setup:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
#
|
|
40
|
-
|
|
39
|
+
usage-coach setup # auto-generates harness.config.json + copies agent file
|
|
40
|
+
usage-coach setup --json # machine-readable output (for scripts/CI)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This creates `~/.config/opencode-usage-coach/harness.config.json` (edit `generator`/`grader` or use `/coach-config` at runtime) and copies `agents/usage-coach-harness.md` to `~/.config/opencode/agents/`.
|
|
44
|
+
|
|
45
|
+
If you use `codexbar` for quota sensing:
|
|
41
46
|
|
|
42
|
-
|
|
43
|
-
|
|
47
|
+
```bash
|
|
48
|
+
printf '%s' "$YOUR_PROVIDER_API_KEY" | codexbar config set-api-key --provider <id> --stdin
|
|
44
49
|
```
|
|
45
50
|
|
|
51
|
+
Without codexbar, the plugin runs in GO-only mode (no quota sensing).
|
|
52
|
+
|
|
46
53
|
For local dev without npm: `bun install && bun run build`, then point both configs at the `dist/` files.
|
|
47
54
|
|
|
48
55
|
## Configuration
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import {
|
|
5
|
+
readFileSync,
|
|
6
|
+
writeFileSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
statSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
copyFileSync
|
|
12
|
+
} from "fs";
|
|
13
|
+
import { createHash } from "crypto";
|
|
14
|
+
import { homedir } from "os";
|
|
15
|
+
import { join, resolve, dirname } from "path";
|
|
16
|
+
import { fileURLToPath } from "url";
|
|
17
|
+
import { spawnSync } from "child_process";
|
|
18
|
+
function projectStateDir(dir) {
|
|
19
|
+
const abs = resolve(dir || ".");
|
|
20
|
+
const h = createHash("sha1").update(abs).digest("hex").slice(0, 12);
|
|
21
|
+
return join(homedir(), ".cache", "opencode-usage-coach", "projects", h);
|
|
22
|
+
}
|
|
23
|
+
function resolveStateDir(dir) {
|
|
24
|
+
return process.env.UC_STATE_DIR ?? projectStateDir(dir ?? process.cwd());
|
|
25
|
+
}
|
|
26
|
+
var CACHE_ROOT = join(homedir(), ".cache", "opencode-usage-coach");
|
|
27
|
+
function readJson(path) {
|
|
28
|
+
try {
|
|
29
|
+
if (!existsSync(path)) return null;
|
|
30
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function readLines(path) {
|
|
36
|
+
try {
|
|
37
|
+
if (!existsSync(path)) return [];
|
|
38
|
+
return readFileSync(path, "utf8").split("\n").filter(Boolean);
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function readNdjson(path) {
|
|
44
|
+
return readLines(path).map((l) => {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(l);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}).filter((x) => x !== null);
|
|
51
|
+
}
|
|
52
|
+
function readText(path) {
|
|
53
|
+
try {
|
|
54
|
+
if (!existsSync(path)) return "";
|
|
55
|
+
return readFileSync(path, "utf8");
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function countFileLines(path) {
|
|
61
|
+
return readLines(path).length;
|
|
62
|
+
}
|
|
63
|
+
function findHarness(stateDir) {
|
|
64
|
+
let best = null;
|
|
65
|
+
let entries = [];
|
|
66
|
+
try {
|
|
67
|
+
entries = readdirSync(stateDir);
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
for (const d of entries) {
|
|
71
|
+
const sub = join(stateDir, d);
|
|
72
|
+
let isDir = false;
|
|
73
|
+
try {
|
|
74
|
+
isDir = statSync(sub).isDirectory();
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
if (!isDir) continue;
|
|
78
|
+
const f = join(sub, "harness.json");
|
|
79
|
+
if (!existsSync(f)) continue;
|
|
80
|
+
let st;
|
|
81
|
+
try {
|
|
82
|
+
st = statSync(f);
|
|
83
|
+
} catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
let active = false;
|
|
87
|
+
try {
|
|
88
|
+
active = !!JSON.parse(readFileSync(f, "utf8")).active;
|
|
89
|
+
} catch {
|
|
90
|
+
}
|
|
91
|
+
if (!best || active && !best.active || active === best.active && st.mtimeMs > best.mtime) {
|
|
92
|
+
best = { file: f, mtime: st.mtimeMs, active };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (best) return readJson(best.file);
|
|
96
|
+
return readJson(join(stateDir, "harness.json"));
|
|
97
|
+
}
|
|
98
|
+
function readStatus(dir) {
|
|
99
|
+
const stateDir = resolveStateDir(dir);
|
|
100
|
+
const s = readJson(join(stateDir, "state.json"));
|
|
101
|
+
const h = findHarness(stateDir);
|
|
102
|
+
const rulesCount = parseRules(readText(join(stateDir, "rules.md"))).length;
|
|
103
|
+
const failuresCount = countFileLines(join(stateDir, "failures.ndjson"));
|
|
104
|
+
const domainNodes = countFileLines(join(stateDir, "nodes.ndjson"));
|
|
105
|
+
const domainEdges = countFileLines(join(stateDir, "edges.ndjson"));
|
|
106
|
+
return {
|
|
107
|
+
directory: resolve(dir ?? process.cwd()),
|
|
108
|
+
stateDir,
|
|
109
|
+
quota: s ? {
|
|
110
|
+
decision: s.decision,
|
|
111
|
+
fiveHour: s.fiveHour,
|
|
112
|
+
weekly: s.weekly,
|
|
113
|
+
monthly: s.monthly,
|
|
114
|
+
model: s.model,
|
|
115
|
+
provider: s.provider,
|
|
116
|
+
isFree: s.isFree,
|
|
117
|
+
advice: s.advice
|
|
118
|
+
} : null,
|
|
119
|
+
providers: s?.providers ?? null,
|
|
120
|
+
harness: h ? {
|
|
121
|
+
active: h.active ?? false,
|
|
122
|
+
name: h.name,
|
|
123
|
+
total: h.total,
|
|
124
|
+
current: h.current,
|
|
125
|
+
tasks: h.tasks.map((t) => ({
|
|
126
|
+
id: t.id,
|
|
127
|
+
title: t.title,
|
|
128
|
+
status: t.status,
|
|
129
|
+
score: t.score ?? void 0,
|
|
130
|
+
model: t.model,
|
|
131
|
+
steps: t.subStep
|
|
132
|
+
}))
|
|
133
|
+
} : null,
|
|
134
|
+
learning: { rulesCount, failuresCount, domainNodes, domainEdges },
|
|
135
|
+
updatedAt: s?.updatedAt
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function readAggregateStatus() {
|
|
139
|
+
const projectsDir = join(CACHE_ROOT, "projects");
|
|
140
|
+
let dirs = [];
|
|
141
|
+
try {
|
|
142
|
+
dirs = readdirSync(projectsDir).map((d) => join(projectsDir, d));
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
const instances = [];
|
|
146
|
+
const decCount = {};
|
|
147
|
+
let max5h = 0, maxWk = 0, maxMo = 0, activeHarnesses = 0, totalTasks = 0, totalRules = 0, totalFailures = 0, totalDomainNodes = 0, totalDomainEdges = 0;
|
|
148
|
+
for (const d of dirs) {
|
|
149
|
+
let isDir = false;
|
|
150
|
+
try {
|
|
151
|
+
isDir = statSync(d).isDirectory();
|
|
152
|
+
} catch {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (!isDir) continue;
|
|
156
|
+
const s = readJson(join(d, "state.json"));
|
|
157
|
+
const decision = s?.decision ?? "unknown";
|
|
158
|
+
const fiveHour = s?.fiveHour ?? 0;
|
|
159
|
+
const weekly = s?.weekly ?? 0;
|
|
160
|
+
instances.push({
|
|
161
|
+
directory: d,
|
|
162
|
+
stateDir: d,
|
|
163
|
+
decision,
|
|
164
|
+
fiveHour,
|
|
165
|
+
weekly,
|
|
166
|
+
model: s?.model
|
|
167
|
+
});
|
|
168
|
+
decCount[decision] = (decCount[decision] ?? 0) + 1;
|
|
169
|
+
max5h = Math.max(max5h, fiveHour);
|
|
170
|
+
maxWk = Math.max(maxWk, weekly);
|
|
171
|
+
maxMo = Math.max(maxMo, s?.monthly ?? 0);
|
|
172
|
+
const h = findHarness(d);
|
|
173
|
+
if (h?.active) {
|
|
174
|
+
activeHarnesses++;
|
|
175
|
+
totalTasks += h.tasks.length;
|
|
176
|
+
}
|
|
177
|
+
totalRules += parseRules(readText(join(d, "rules.md"))).length;
|
|
178
|
+
totalFailures += countFileLines(join(d, "failures.ndjson"));
|
|
179
|
+
totalDomainNodes += countFileLines(join(d, "nodes.ndjson"));
|
|
180
|
+
totalDomainEdges += countFileLines(join(d, "edges.ndjson"));
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
instanceCount: instances.length,
|
|
184
|
+
instances,
|
|
185
|
+
aggregate: {
|
|
186
|
+
maxFiveHour: max5h,
|
|
187
|
+
maxWeekly: maxWk,
|
|
188
|
+
maxMonthly: maxMo,
|
|
189
|
+
decisions: decCount,
|
|
190
|
+
activeHarnesses,
|
|
191
|
+
totalTasks,
|
|
192
|
+
totalRules,
|
|
193
|
+
totalFailures,
|
|
194
|
+
totalDomainNodes,
|
|
195
|
+
totalDomainEdges
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function readRules(dir) {
|
|
200
|
+
const stateDir = resolveStateDir(dir);
|
|
201
|
+
const content = readText(join(stateDir, "rules.md"));
|
|
202
|
+
const rules = parseRules(content);
|
|
203
|
+
return { count: rules.length, rules };
|
|
204
|
+
}
|
|
205
|
+
function parseRules(content) {
|
|
206
|
+
if (!content.trim()) return [];
|
|
207
|
+
const blocks = content.split(/^## /m).filter((b) => b.startsWith("Rule"));
|
|
208
|
+
return blocks.map((block) => {
|
|
209
|
+
const headerMatch = block.match(
|
|
210
|
+
/^Rule\s+(\d+)\s*\(([^,]+),\s*category:\s*([^)]+)\)/
|
|
211
|
+
);
|
|
212
|
+
const number = headerMatch ? parseInt(headerMatch[1], 10) : 0;
|
|
213
|
+
const date = headerMatch ? headerMatch[2].trim() : "";
|
|
214
|
+
const category = headerMatch ? headerMatch[3].trim() : "";
|
|
215
|
+
const body = block.slice(headerMatch?.[0]?.length ?? 0).trim();
|
|
216
|
+
const originMatch = body.match(/^Origin:\s*(.+)$/m);
|
|
217
|
+
const text = body.split("\n").filter((l) => !l.startsWith("Origin:")).join(" ").trim();
|
|
218
|
+
return {
|
|
219
|
+
number,
|
|
220
|
+
category,
|
|
221
|
+
date,
|
|
222
|
+
text,
|
|
223
|
+
origin: originMatch ? originMatch[1].trim() : ""
|
|
224
|
+
};
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function readDecisions(dir, limit = 20) {
|
|
228
|
+
const stateDir = resolveStateDir(dir);
|
|
229
|
+
const lines = readLines(join(stateDir, "coach.log"));
|
|
230
|
+
const decisions = [];
|
|
231
|
+
for (let i = lines.length - 1; i >= 0 && decisions.length < limit; i--) {
|
|
232
|
+
const line = lines[i];
|
|
233
|
+
const m = line.match(
|
|
234
|
+
/^(\S+)\s+DECIDE\s+(GO|THROTTLE|STOP)\s+(.*)$/
|
|
235
|
+
);
|
|
236
|
+
if (m) {
|
|
237
|
+
decisions.push({
|
|
238
|
+
ts: m[1],
|
|
239
|
+
decision: m[2],
|
|
240
|
+
detail: m[3]
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return { count: decisions.length, decisions };
|
|
245
|
+
}
|
|
246
|
+
function readDomainStats(dir) {
|
|
247
|
+
const stateDir = resolveStateDir(dir);
|
|
248
|
+
const nodes = readNdjson(join(stateDir, "nodes.ndjson"));
|
|
249
|
+
const edges = readNdjson(join(stateDir, "edges.ndjson"));
|
|
250
|
+
const nodeTypes = {};
|
|
251
|
+
for (const n of nodes) {
|
|
252
|
+
const t = n.type ?? "unknown";
|
|
253
|
+
nodeTypes[t] = (nodeTypes[t] ?? 0) + 1;
|
|
254
|
+
}
|
|
255
|
+
const edgeTypes = {};
|
|
256
|
+
for (const e of edges) {
|
|
257
|
+
const r = e.rel ?? "unknown";
|
|
258
|
+
edgeTypes[r] = (edgeTypes[r] ?? 0) + 1;
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
nodes: nodes.length,
|
|
262
|
+
edges: edges.length,
|
|
263
|
+
nodeTypes,
|
|
264
|
+
edgeTypes
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function bar(pct) {
|
|
268
|
+
const n = pct <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(pct / 10)));
|
|
269
|
+
return "\u2588".repeat(n) + "\u2591".repeat(10 - n);
|
|
270
|
+
}
|
|
271
|
+
function formatStatus(r) {
|
|
272
|
+
const lines = [];
|
|
273
|
+
if (!r.quota) {
|
|
274
|
+
lines.push("usage-coach: no state (is the plugin running?)");
|
|
275
|
+
return lines.join("\n");
|
|
276
|
+
}
|
|
277
|
+
const q = r.quota;
|
|
278
|
+
const tag = q.isFree ? "free" : q.decision;
|
|
279
|
+
const model = q.model ? ` ${q.model.split("/").pop()}` : "";
|
|
280
|
+
lines.push(`usage-coach [${tag}]${model}`);
|
|
281
|
+
if (!q.isFree) {
|
|
282
|
+
if (q.fiveHour >= 0) lines.push(` 5h ${bar(q.fiveHour)} ${q.fiveHour}%`);
|
|
283
|
+
if (q.weekly >= 0) lines.push(` 1w ${bar(q.weekly)} ${q.weekly}%`);
|
|
284
|
+
}
|
|
285
|
+
if (q.advice) lines.push(` ${q.advice}`);
|
|
286
|
+
if (r.harness?.active) {
|
|
287
|
+
lines.push("");
|
|
288
|
+
lines.push(`harness: ${r.harness.name} ${r.harness.current}/${r.harness.total}`);
|
|
289
|
+
for (const t of r.harness.tasks) {
|
|
290
|
+
const score = t.score ? ` [${t.score}]` : "";
|
|
291
|
+
lines.push(` ${t.id} [${t.status}]${score} ${t.title}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (r.learning.rulesCount > 0) {
|
|
295
|
+
lines.push("");
|
|
296
|
+
lines.push(
|
|
297
|
+
`learning: ${r.learning.rulesCount} rules, ${r.learning.failuresCount} failures, ${r.learning.domainNodes} domain nodes`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return lines.join("\n");
|
|
301
|
+
}
|
|
302
|
+
function formatAggregate(r) {
|
|
303
|
+
const lines = [];
|
|
304
|
+
const a = r.aggregate;
|
|
305
|
+
lines.push(`usage-coach aggregate \u2014 ${r.instanceCount} instances`);
|
|
306
|
+
lines.push(` max 5h ${bar(a.maxFiveHour)} ${a.maxFiveHour}%`);
|
|
307
|
+
lines.push(` max 1w ${bar(a.maxWeekly)} ${a.maxWeekly}%`);
|
|
308
|
+
const decs = Object.entries(a.decisions).map(([k, v]) => `${k}:${v}`).join(" ");
|
|
309
|
+
lines.push(` decisions: ${decs}`);
|
|
310
|
+
lines.push(
|
|
311
|
+
` harnesses: ${a.activeHarnesses} active, ${a.totalTasks} tasks`
|
|
312
|
+
);
|
|
313
|
+
lines.push(
|
|
314
|
+
` learning: ${a.totalRules} rules, ${a.totalFailures} failures, ${a.totalDomainNodes} domain nodes`
|
|
315
|
+
);
|
|
316
|
+
return lines.join("\n");
|
|
317
|
+
}
|
|
318
|
+
function formatRules(r) {
|
|
319
|
+
if (r.count === 0) return "No rules accumulated yet.";
|
|
320
|
+
const lines = [`${r.count} rules:`];
|
|
321
|
+
for (const rule of r.rules) {
|
|
322
|
+
lines.push(
|
|
323
|
+
` #${rule.number} (${rule.date}, ${rule.category}): ${rule.text.slice(0, 80)}${rule.text.length > 80 ? "..." : ""}`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
return lines.join("\n");
|
|
327
|
+
}
|
|
328
|
+
function formatDecisions(r) {
|
|
329
|
+
if (r.count === 0) return "No decisions logged.";
|
|
330
|
+
const lines = [`${r.count} recent decisions:`];
|
|
331
|
+
for (const d of r.decisions) {
|
|
332
|
+
lines.push(` ${d.ts} ${d.decision} ${d.detail}`);
|
|
333
|
+
}
|
|
334
|
+
return lines.join("\n");
|
|
335
|
+
}
|
|
336
|
+
function formatDomain(r) {
|
|
337
|
+
if (r.nodes === 0 && r.edges === 0) return "No domain knowledge stored.";
|
|
338
|
+
const nt = Object.entries(r.nodeTypes).map(([k, v]) => `${k}:${v}`).join(" ");
|
|
339
|
+
const et = Object.entries(r.edgeTypes).map(([k, v]) => `${k}:${v}`).join(" ");
|
|
340
|
+
return [
|
|
341
|
+
`domain: ${r.nodes} nodes, ${r.edges} edges`,
|
|
342
|
+
` node types: ${nt}`,
|
|
343
|
+
` edge types: ${et}`
|
|
344
|
+
].join("\n");
|
|
345
|
+
}
|
|
346
|
+
var GLOBAL_CONFIG_DIR = join(homedir(), ".config", "opencode-usage-coach");
|
|
347
|
+
var OPENCODE_AGENTS_DIR = join(homedir(), ".config", "opencode", "agents");
|
|
348
|
+
var DEFAULT_HARNESS_CONFIG = {
|
|
349
|
+
generator: "opencode/deepseek-v4-flash-free",
|
|
350
|
+
grader: "opencode/mimo-v2.5-free",
|
|
351
|
+
provider: "",
|
|
352
|
+
lighterModel: ""
|
|
353
|
+
};
|
|
354
|
+
function resolveAgentSourceFile() {
|
|
355
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
356
|
+
return join(scriptDir, "..", "agents", "usage-coach-harness.md");
|
|
357
|
+
}
|
|
358
|
+
function detectCodexbar() {
|
|
359
|
+
try {
|
|
360
|
+
const r = spawnSync("codexbar", ["--version"], { timeout: 5e3 });
|
|
361
|
+
if (r.status === 0 || r.stdout && r.stdout.toString().trim().length > 0) {
|
|
362
|
+
return { found: true, version: (r.stdout?.toString().trim() ?? "") || "unknown" };
|
|
363
|
+
}
|
|
364
|
+
return { found: false, version: "" };
|
|
365
|
+
} catch {
|
|
366
|
+
return { found: false, version: "" };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function doSetup(opts = {}) {
|
|
370
|
+
const configDir = opts.configDir ?? GLOBAL_CONFIG_DIR;
|
|
371
|
+
const agentsDir = opts.agentsDir ?? OPENCODE_AGENTS_DIR;
|
|
372
|
+
const agentSource = opts.agentSourceFile ?? resolveAgentSourceFile();
|
|
373
|
+
const configPath = join(configDir, "harness.config.json");
|
|
374
|
+
let configAction;
|
|
375
|
+
if (existsSync(configPath)) {
|
|
376
|
+
configAction = "exists";
|
|
377
|
+
} else {
|
|
378
|
+
mkdirSync(configDir, { recursive: true });
|
|
379
|
+
writeFileSync(
|
|
380
|
+
configPath,
|
|
381
|
+
JSON.stringify(DEFAULT_HARNESS_CONFIG, null, 2) + "\n"
|
|
382
|
+
);
|
|
383
|
+
configAction = "created";
|
|
384
|
+
}
|
|
385
|
+
const codexbar = detectCodexbar();
|
|
386
|
+
const agentDestPath = join(agentsDir, "usage-coach-harness.md");
|
|
387
|
+
let agentAction;
|
|
388
|
+
if (!existsSync(agentSource)) {
|
|
389
|
+
agentAction = "source-not-found";
|
|
390
|
+
} else if (existsSync(agentDestPath)) {
|
|
391
|
+
agentAction = "exists";
|
|
392
|
+
} else {
|
|
393
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
394
|
+
copyFileSync(agentSource, agentDestPath);
|
|
395
|
+
agentAction = "copied";
|
|
396
|
+
}
|
|
397
|
+
return {
|
|
398
|
+
harnessConfig: { action: configAction, path: configPath },
|
|
399
|
+
codexbar,
|
|
400
|
+
agentFile: { action: agentAction, path: agentDestPath }
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
function formatSetup(r) {
|
|
404
|
+
const lines = ["usage-coach setup", ""];
|
|
405
|
+
const configCheck = r.harnessConfig.action === "created" ? "\u2705" : "\u2705";
|
|
406
|
+
lines.push(
|
|
407
|
+
` ${configCheck} harness.config.json ${r.harnessConfig.action === "created" ? "created" : "already exists"}`
|
|
408
|
+
);
|
|
409
|
+
lines.push(` ${r.harnessConfig.path}`);
|
|
410
|
+
if (r.harnessConfig.action === "created") {
|
|
411
|
+
lines.push(
|
|
412
|
+
` generator: ${DEFAULT_HARNESS_CONFIG.generator} (edit or use /coach-config)`
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
if (r.codexbar.found) {
|
|
416
|
+
lines.push("");
|
|
417
|
+
lines.push(` \u2705 codexbar found`);
|
|
418
|
+
lines.push(` ${r.codexbar.version}`);
|
|
419
|
+
} else {
|
|
420
|
+
lines.push("");
|
|
421
|
+
lines.push(` \u26A0\uFE0F codexbar not found`);
|
|
422
|
+
lines.push(` Plugin runs in GO-only mode (no quota sensing)`);
|
|
423
|
+
}
|
|
424
|
+
lines.push("");
|
|
425
|
+
if (r.agentFile.action === "copied") {
|
|
426
|
+
lines.push(` \u2705 Agent file copied`);
|
|
427
|
+
lines.push(` ${r.agentFile.path}`);
|
|
428
|
+
} else if (r.agentFile.action === "exists") {
|
|
429
|
+
lines.push(` \u2705 Agent file already exists`);
|
|
430
|
+
lines.push(` ${r.agentFile.path}`);
|
|
431
|
+
} else {
|
|
432
|
+
lines.push(` \u26A0\uFE0F Agent source not found`);
|
|
433
|
+
lines.push(` Expected: ${r.agentFile.path}`);
|
|
434
|
+
}
|
|
435
|
+
lines.push("");
|
|
436
|
+
lines.push("Setup complete. Restart opencode to apply changes.");
|
|
437
|
+
return lines.join("\n");
|
|
438
|
+
}
|
|
439
|
+
function parseArgs(argv) {
|
|
440
|
+
const args = argv.slice(2);
|
|
441
|
+
let command = "status";
|
|
442
|
+
let json = false;
|
|
443
|
+
let dir;
|
|
444
|
+
let aggregate = false;
|
|
445
|
+
let limit = 20;
|
|
446
|
+
for (let i = 0; i < args.length; i++) {
|
|
447
|
+
const a = args[i];
|
|
448
|
+
if (a === "--json" || a === "-j") json = true;
|
|
449
|
+
else if (a === "--aggregate" || a === "-a") aggregate = true;
|
|
450
|
+
else if (a === "--dir" || a === "-d") {
|
|
451
|
+
dir = args[++i];
|
|
452
|
+
} else if (a === "--limit" || a === "-l") {
|
|
453
|
+
limit = parseInt(args[++i], 10) || 20;
|
|
454
|
+
} else if (a === "--help" || a === "-h") {
|
|
455
|
+
command = "help";
|
|
456
|
+
} else if (a === "--version" || a === "-v") {
|
|
457
|
+
command = "version";
|
|
458
|
+
} else if (!a.startsWith("-")) {
|
|
459
|
+
command = a;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return { command, json, dir, aggregate, limit };
|
|
463
|
+
}
|
|
464
|
+
var HELP = `usage-coach \u2014 quota intelligence CLI for opencode
|
|
465
|
+
|
|
466
|
+
Commands:
|
|
467
|
+
status Show quota + harness + learning state (default)
|
|
468
|
+
rules List accumulated learning rules
|
|
469
|
+
decisions Show recent GO/THROTTLE/STOP decision history
|
|
470
|
+
domain Show domain knowledge graph stats
|
|
471
|
+
setup Auto-generate harness.config.json, detect codexbar, copy agent file
|
|
472
|
+
|
|
473
|
+
Flags:
|
|
474
|
+
--json, -j Output as JSON (default: human-readable)
|
|
475
|
+
--dir <path> Project directory to query (default: cwd)
|
|
476
|
+
--aggregate, -a Scan all project instances (status only)
|
|
477
|
+
--limit <n> Number of decisions to show (default: 20)
|
|
478
|
+
--help, -h Show this help
|
|
479
|
+
--version, -v Show version
|
|
480
|
+
|
|
481
|
+
Examples:
|
|
482
|
+
usage-coach status --json
|
|
483
|
+
usage-coach status --aggregate --json
|
|
484
|
+
usage-coach rules --json --dir /path/to/project`;
|
|
485
|
+
function getVersion() {
|
|
486
|
+
try {
|
|
487
|
+
const pkgPath = join(
|
|
488
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
489
|
+
"..",
|
|
490
|
+
"package.json"
|
|
491
|
+
);
|
|
492
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
493
|
+
return pkg.version ?? "unknown";
|
|
494
|
+
} catch {
|
|
495
|
+
return "unknown";
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
function main() {
|
|
499
|
+
const args = parseArgs(process.argv);
|
|
500
|
+
switch (args.command) {
|
|
501
|
+
case "help": {
|
|
502
|
+
console.log(HELP);
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
case "version": {
|
|
506
|
+
console.log(getVersion());
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
case "status": {
|
|
510
|
+
if (args.aggregate) {
|
|
511
|
+
const r = readAggregateStatus();
|
|
512
|
+
console.log(args.json ? JSON.stringify(r, null, 2) : formatAggregate(r));
|
|
513
|
+
} else {
|
|
514
|
+
const r = readStatus(args.dir);
|
|
515
|
+
console.log(args.json ? JSON.stringify(r, null, 2) : formatStatus(r));
|
|
516
|
+
}
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
case "rules": {
|
|
520
|
+
const r = readRules(args.dir);
|
|
521
|
+
console.log(args.json ? JSON.stringify(r, null, 2) : formatRules(r));
|
|
522
|
+
break;
|
|
523
|
+
}
|
|
524
|
+
case "decisions": {
|
|
525
|
+
const r = readDecisions(args.dir, args.limit);
|
|
526
|
+
console.log(
|
|
527
|
+
args.json ? JSON.stringify(r, null, 2) : formatDecisions(r)
|
|
528
|
+
);
|
|
529
|
+
break;
|
|
530
|
+
}
|
|
531
|
+
case "domain": {
|
|
532
|
+
const r = readDomainStats(args.dir);
|
|
533
|
+
console.log(args.json ? JSON.stringify(r, null, 2) : formatDomain(r));
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case "setup": {
|
|
537
|
+
const r = doSetup();
|
|
538
|
+
console.log(args.json ? JSON.stringify(r, null, 2) : formatSetup(r));
|
|
539
|
+
break;
|
|
540
|
+
}
|
|
541
|
+
default: {
|
|
542
|
+
console.error(`Unknown command: ${args.command}
|
|
543
|
+
|
|
544
|
+
${HELP}`);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
var isDirectRun = process.argv[1] && (process.argv[1].endsWith("cli.js") || process.argv[1].endsWith("cli.ts") || process.argv[1].endsWith("usage-coach"));
|
|
550
|
+
if (isDirectRun) {
|
|
551
|
+
main();
|
|
552
|
+
}
|
|
553
|
+
export {
|
|
554
|
+
doSetup,
|
|
555
|
+
parseArgs,
|
|
556
|
+
projectStateDir,
|
|
557
|
+
readAggregateStatus,
|
|
558
|
+
readDecisions,
|
|
559
|
+
readDomainStats,
|
|
560
|
+
readRules,
|
|
561
|
+
readStatus,
|
|
562
|
+
resolveAgentSourceFile,
|
|
563
|
+
resolveStateDir
|
|
564
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -1349,6 +1349,7 @@ async function runModel(client, model, prompt, directory, track, maxSteps = DEFA
|
|
|
1349
1349
|
const providerID = slash >= 0 ? model.slice(0, slash) : model;
|
|
1350
1350
|
const modelID = slash >= 0 ? model.slice(slash + 1) : "";
|
|
1351
1351
|
const s = await client.session.create({ body: { title: "uc-harness-sub" }, query: { directory } });
|
|
1352
|
+
log(`runModel(${model}): session.create ${Date.now() - t0}ms`);
|
|
1352
1353
|
const id = s?.data?.info?.id ?? s?.data?.id ?? s?.id;
|
|
1353
1354
|
if (!id) return `ERROR: session.create returned no id (response: ${JSON.stringify(s?.data ?? s).slice(0, 200)})`;
|
|
1354
1355
|
subId = id;
|
|
@@ -1417,6 +1418,7 @@ async function runModel(client, model, prompt, directory, track, maxSteps = DEFA
|
|
|
1417
1418
|
);
|
|
1418
1419
|
const resp = await Promise.race([promptP, timeoutSignal.then(() => null)]);
|
|
1419
1420
|
const elapsed = Math.round((Date.now() - t0) / 1e3);
|
|
1421
|
+
log(`runModel(${model}): prompt resolved ${elapsed}s, timedOut=${timedOut}`);
|
|
1420
1422
|
if (timedOut) {
|
|
1421
1423
|
try {
|
|
1422
1424
|
const summary = await client.session.summarize?.({ path: { id } });
|
|
@@ -1424,7 +1426,7 @@ async function runModel(client, model, prompt, directory, track, maxSteps = DEFA
|
|
|
1424
1426
|
} catch {
|
|
1425
1427
|
}
|
|
1426
1428
|
try {
|
|
1427
|
-
|
|
1429
|
+
client.session.delete?.({ path: { id } });
|
|
1428
1430
|
} catch {
|
|
1429
1431
|
}
|
|
1430
1432
|
subId = null;
|
|
@@ -1434,17 +1436,19 @@ async function runModel(client, model, prompt, directory, track, maxSteps = DEFA
|
|
|
1434
1436
|
}
|
|
1435
1437
|
const parts = resp?.data?.parts ?? resp?.parts ?? [];
|
|
1436
1438
|
const text = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("");
|
|
1439
|
+
const cleanupStart = Date.now();
|
|
1437
1440
|
try {
|
|
1438
|
-
|
|
1439
|
-
|
|
1441
|
+
client.session.summarize?.({ path: { id } }).then((summary) => log(`runModel(${model}): summary ${Date.now() - cleanupStart}ms: ${JSON.stringify(summary?.data ?? summary).slice(0, 300)}`)).catch(() => {
|
|
1442
|
+
});
|
|
1440
1443
|
} catch {
|
|
1441
1444
|
}
|
|
1442
1445
|
try {
|
|
1443
|
-
|
|
1446
|
+
client.session.delete?.({ path: { id } }).catch(() => {
|
|
1447
|
+
});
|
|
1444
1448
|
} catch {
|
|
1445
1449
|
}
|
|
1446
1450
|
subId = null;
|
|
1447
|
-
log(`runModel(${model}): done ${elapsed}s, ${text.length} chars`);
|
|
1451
|
+
log(`runModel(${model}): done ${elapsed}s, ${text.length} chars (cleanup dispatched)`);
|
|
1448
1452
|
return text.trim() || `ERROR: no assistant text in prompt response after ${elapsed}s (parts: ${parts.length}, types: ${parts.map((p) => p?.type).join(",")})`;
|
|
1449
1453
|
} catch (e) {
|
|
1450
1454
|
const elapsed = Math.round((Date.now() - t0) / 1e3);
|
|
@@ -1462,7 +1466,8 @@ async function runModel(client, model, prompt, directory, track, maxSteps = DEFA
|
|
|
1462
1466
|
}
|
|
1463
1467
|
if (subId) {
|
|
1464
1468
|
try {
|
|
1465
|
-
|
|
1469
|
+
client.session.delete?.({ path: { id: subId } }).catch(() => {
|
|
1470
|
+
});
|
|
1466
1471
|
} catch {
|
|
1467
1472
|
}
|
|
1468
1473
|
}
|
|
@@ -1510,11 +1515,15 @@ function captureStdout(args) {
|
|
|
1510
1515
|
p.stdout?.on("data", (d) => {
|
|
1511
1516
|
out += d.toString();
|
|
1512
1517
|
});
|
|
1513
|
-
p.on("error", () =>
|
|
1518
|
+
p.on("error", (err) => {
|
|
1519
|
+
if (err.code === "ENOENT") codexbarMissing = true;
|
|
1520
|
+
resolve2("");
|
|
1521
|
+
});
|
|
1514
1522
|
p.on("close", () => resolve2(out));
|
|
1515
1523
|
});
|
|
1516
1524
|
}
|
|
1517
1525
|
async function fetchEnabledProviders() {
|
|
1526
|
+
if (codexbarMissing) return [];
|
|
1518
1527
|
const out = await captureStdout(["config", "providers"]);
|
|
1519
1528
|
const ids = [];
|
|
1520
1529
|
for (const line of out.split("\n")) {
|
|
@@ -1525,11 +1534,11 @@ async function fetchEnabledProviders() {
|
|
|
1525
1534
|
}
|
|
1526
1535
|
function providerAdvice(h5, wk) {
|
|
1527
1536
|
const S5H = STOP_5H, SWK = STOP_WK, T5H = THR_5H, TWK = THR_WK;
|
|
1528
|
-
if (h5 >= S5H || wk >= SWK) return "STOP \u2014 finish current only";
|
|
1529
|
-
if (h5 >= T5H && wk >= TWK) return "small tasks only \u2014 big ones will hit both limits";
|
|
1530
|
-
if (h5 >= T5H) return "small tasks only \u2014 5h window nearly full, big tasks after reset";
|
|
1531
|
-
if (wk >= TWK) return "small tasks only \u2014 big ones will strain late-week";
|
|
1532
|
-
if (h5 >= 50 || wk >= 50) return "moderate tasks OK \u2014 save big ones for headroom";
|
|
1537
|
+
if (h5 !== null && h5 >= S5H || wk !== null && wk >= SWK) return "STOP \u2014 finish current only";
|
|
1538
|
+
if (h5 !== null && wk !== null && h5 >= T5H && wk >= TWK) return "small tasks only \u2014 big ones will hit both limits";
|
|
1539
|
+
if (h5 !== null && h5 >= T5H) return "small tasks only \u2014 5h window nearly full, big tasks after reset";
|
|
1540
|
+
if (wk !== null && wk >= TWK) return "small tasks only \u2014 big ones will strain late-week";
|
|
1541
|
+
if (h5 !== null && h5 >= 50 || wk !== null && wk >= 50) return "moderate tasks OK \u2014 save big ones for headroom";
|
|
1533
1542
|
return "big tasks OK \u2014 short & long limits comfortable";
|
|
1534
1543
|
}
|
|
1535
1544
|
async function fetchProvidersCoach() {
|
|
@@ -1537,17 +1546,17 @@ async function fetchProvidersCoach() {
|
|
|
1537
1546
|
const results = await Promise.all(ids.map(async (id) => {
|
|
1538
1547
|
try {
|
|
1539
1548
|
const out = await captureStdout(["usage", "--provider", id, "--json"]);
|
|
1540
|
-
const
|
|
1541
|
-
if (!
|
|
1542
|
-
const h5 = Math.round(
|
|
1543
|
-
const wk = Math.round(
|
|
1549
|
+
const q = parseQuotaResponse(out);
|
|
1550
|
+
if (!q) return null;
|
|
1551
|
+
const h5 = q.fiveHour ? Math.round(q.fiveHour.usedPercent ?? 0) : null;
|
|
1552
|
+
const wk = q.weekly ? Math.round(q.weekly.usedPercent ?? 0) : null;
|
|
1544
1553
|
return {
|
|
1545
1554
|
id,
|
|
1546
1555
|
name: id,
|
|
1547
|
-
fiveHour: h5,
|
|
1548
|
-
weekly: wk,
|
|
1549
|
-
fiveHourReset: humanRemaining(
|
|
1550
|
-
weeklyReset: humanRemaining(
|
|
1556
|
+
fiveHour: h5 ?? -1,
|
|
1557
|
+
weekly: wk ?? -1,
|
|
1558
|
+
fiveHourReset: q.fiveHour ? humanRemaining(q.fiveHour.resetsAt) : "",
|
|
1559
|
+
weeklyReset: q.weekly ? humanRemaining(q.weekly.resetsAt) : "",
|
|
1551
1560
|
advice: providerAdvice(h5, wk)
|
|
1552
1561
|
};
|
|
1553
1562
|
} catch {
|
|
@@ -1562,11 +1571,36 @@ function parseQuotaResponse(rawText) {
|
|
|
1562
1571
|
if (!text || text === "[]") return null;
|
|
1563
1572
|
const u = JSON.parse(text)[0]?.usage;
|
|
1564
1573
|
if (!u) return null;
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1574
|
+
const result = { weekly: null, monthly: null, fiveHour: null };
|
|
1575
|
+
const entries = [
|
|
1576
|
+
{ key: "primary", slot: u.primary },
|
|
1577
|
+
{ key: "secondary", slot: u.secondary },
|
|
1578
|
+
{ key: "tertiary", slot: u.tertiary }
|
|
1579
|
+
].filter((e) => e.slot != null);
|
|
1580
|
+
for (const { key, slot } of entries) {
|
|
1581
|
+
const mins = slot.windowMinutes ?? 0;
|
|
1582
|
+
const desc = String(slot.resetDescription ?? "").toLowerCase();
|
|
1583
|
+
if (mins === 300 || desc.includes("5 hour") || desc.includes("5h")) {
|
|
1584
|
+
result.fiveHour = slot;
|
|
1585
|
+
} else if (mins === 10080 || desc.includes("week")) {
|
|
1586
|
+
result.weekly = slot;
|
|
1587
|
+
} else if (desc.includes("month")) {
|
|
1588
|
+
result.monthly = slot;
|
|
1589
|
+
} else if (mins > 0 && mins <= 360) {
|
|
1590
|
+
result.fiveHour = slot;
|
|
1591
|
+
} else if (mins > 360 && mins <= 14400) {
|
|
1592
|
+
result.weekly = slot;
|
|
1593
|
+
} else if (mins > 14400) {
|
|
1594
|
+
result.monthly = slot;
|
|
1595
|
+
} else if (key === "primary") {
|
|
1596
|
+
result.weekly = slot;
|
|
1597
|
+
} else if (key === "secondary") {
|
|
1598
|
+
result.monthly = slot;
|
|
1599
|
+
} else if (key === "tertiary") {
|
|
1600
|
+
result.fiveHour = slot;
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
return result;
|
|
1570
1604
|
} catch {
|
|
1571
1605
|
return null;
|
|
1572
1606
|
}
|
|
@@ -1586,13 +1620,17 @@ function fetchQuota(provider) {
|
|
|
1586
1620
|
p.stdout?.on("data", (d) => {
|
|
1587
1621
|
out += d.toString();
|
|
1588
1622
|
});
|
|
1589
|
-
p.on("error", () =>
|
|
1623
|
+
p.on("error", (err) => {
|
|
1624
|
+
if (err.code === "ENOENT") codexbarMissing = true;
|
|
1625
|
+
resolve2(null);
|
|
1626
|
+
});
|
|
1590
1627
|
p.on("close", () => {
|
|
1591
1628
|
resolve2(parseQuotaResponse(out));
|
|
1592
1629
|
});
|
|
1593
1630
|
});
|
|
1594
1631
|
}
|
|
1595
1632
|
async function fetchQuotaWithRetry(provider, maxRetries = 3) {
|
|
1633
|
+
if (codexbarMissing) return null;
|
|
1596
1634
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
1597
1635
|
const q = await fetchQuota(provider);
|
|
1598
1636
|
if (q) return q;
|
|
@@ -1604,23 +1642,44 @@ async function fetchQuotaWithRetry(provider, maxRetries = 3) {
|
|
|
1604
1642
|
}
|
|
1605
1643
|
function coach(q, lighter) {
|
|
1606
1644
|
if (!q) return { decision: "GO", advice: "quota unavailable \u2014 retrying. proceeding cautiously.", weekly: -2, monthly: -2, fiveHour: -2 };
|
|
1607
|
-
const wk =
|
|
1608
|
-
|
|
1609
|
-
const
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1645
|
+
const wk = q.weekly ? Math.round(q.weekly.usedPercent ?? 0) : null;
|
|
1646
|
+
const mo = q.monthly ? Math.round(q.monthly.usedPercent ?? 0) : null;
|
|
1647
|
+
const h5 = q.fiveHour ? Math.round(q.fiveHour.usedPercent ?? 0) : null;
|
|
1648
|
+
if (wk !== null && Number.isNaN(wk) || mo !== null && Number.isNaN(mo) || h5 !== null && Number.isNaN(h5)) {
|
|
1649
|
+
const sWk = wk !== null && !Number.isNaN(wk) ? wk : 0;
|
|
1650
|
+
const sMo = mo !== null && !Number.isNaN(mo) ? mo : 0;
|
|
1651
|
+
const sH5 = h5 !== null && !Number.isNaN(h5) ? h5 : 0;
|
|
1652
|
+
return { decision: "THROTTLE", advice: "invalid quota data \u2014 proceeding with caution. switch to lighter model if available.", weekly: sWk, monthly: sMo, fiveHour: sH5 };
|
|
1653
|
+
}
|
|
1654
|
+
const wkR = q.weekly ? humanRemaining(q.weekly.resetsAt) : "";
|
|
1655
|
+
const h5R = q.fiveHour ? humanRemaining(q.fiveHour.resetsAt) : "";
|
|
1656
|
+
const stop = (r) => ({ decision: "STOP", advice: `STOP recommend \u2014 ${r}. window nearly exhausted. stop now or it will be force-blocked.`, weekly: wk ?? -1, monthly: mo ?? -1, fiveHour: h5 ?? -1 });
|
|
1657
|
+
const thr = (r) => ({ decision: "THROTTLE", advice: `Throttle recommend \u2014 ${r}. switch to lighter model (${lighter}) or wait for window reset.`, weekly: wk ?? -1, monthly: mo ?? -1, fiveHour: h5 ?? -1 });
|
|
1658
|
+
if (h5 !== null && h5 >= STOP_5H) return stop(`5h window ${h5}% (${h5R})`);
|
|
1659
|
+
if (wk !== null && wk >= STOP_WK) return stop(`weekly ${wk}% (${wkR})`);
|
|
1660
|
+
if (mo !== null && mo >= STOP_MO) return stop(`monthly ${mo}%`);
|
|
1661
|
+
if (h5 !== null && h5 >= THR_5H) return thr(`5h window ${h5}% (${h5R})`);
|
|
1662
|
+
if (wk !== null && wk >= THR_WK) return thr(`weekly ${wk}% (${wkR})`);
|
|
1663
|
+
const parts = [];
|
|
1664
|
+
if (wk !== null) parts.push(`weekly ${wk}%`);
|
|
1665
|
+
if (h5 !== null) parts.push(`5h ${h5}%`);
|
|
1666
|
+
if (mo !== null) parts.push(`monthly ${mo}%`);
|
|
1667
|
+
const summary = parts.length > 0 ? parts.join(" \xB7 ") : "no quota limits detected";
|
|
1668
|
+
const resetInfo = h5R ? ` 5h window ${h5R}.` : wkR ? ` weekly ${wkR}.` : "";
|
|
1669
|
+
return { decision: "GO", advice: `Comfortable \u2014 ${summary}. proceed.${resetInfo}`, weekly: wk ?? -1, monthly: mo ?? -1, fiveHour: h5 ?? -1 };
|
|
1618
1670
|
}
|
|
1619
1671
|
var agentCache = /* @__PURE__ */ new Map();
|
|
1620
1672
|
var currentModel = "";
|
|
1621
1673
|
var currentProvider = "";
|
|
1622
1674
|
var currentAgent = "";
|
|
1623
1675
|
var modelChanged = false;
|
|
1676
|
+
var codexbarMissing = false;
|
|
1677
|
+
function isCodexbarMissing() {
|
|
1678
|
+
return codexbarMissing;
|
|
1679
|
+
}
|
|
1680
|
+
function __resetCodexbarMissing() {
|
|
1681
|
+
codexbarMissing = false;
|
|
1682
|
+
}
|
|
1624
1683
|
function isFreeModel(model, provider) {
|
|
1625
1684
|
if (!model && !provider) return false;
|
|
1626
1685
|
if (provider === "opencode") return true;
|
|
@@ -1682,6 +1741,12 @@ async function UsageCoachPlugin(input) {
|
|
|
1682
1741
|
const refreshBackground = () => {
|
|
1683
1742
|
try {
|
|
1684
1743
|
if (refreshing) return;
|
|
1744
|
+
if (codexbarMissing) {
|
|
1745
|
+
last = { decision: "GO", advice: "codexbar not installed \u2014 running in GO-only mode (no quota sensing).", weekly: -3, monthly: -3, fiveHour: -3 };
|
|
1746
|
+
lastFetchedAt = Date.now();
|
|
1747
|
+
refreshing = false;
|
|
1748
|
+
return;
|
|
1749
|
+
}
|
|
1685
1750
|
if (last && !modelChanged && Date.now() - lastFetchedAt < TTL_MS) return;
|
|
1686
1751
|
refreshing = true;
|
|
1687
1752
|
modelChanged = false;
|
|
@@ -1705,9 +1770,12 @@ async function UsageCoachPlugin(input) {
|
|
|
1705
1770
|
providers = await fetchProvidersCoach();
|
|
1706
1771
|
} catch {
|
|
1707
1772
|
}
|
|
1708
|
-
if (providers.length > 0 && last.weekly < 0) {
|
|
1773
|
+
if (providers.length > 0 && last.weekly < 0 && last.fiveHour < 0) {
|
|
1709
1774
|
const p0 = providers[0];
|
|
1710
|
-
|
|
1775
|
+
const newWeekly = p0.weekly >= 0 ? p0.weekly : last.weekly;
|
|
1776
|
+
const newFiveHour = p0.fiveHour >= 0 ? p0.fiveHour : last.fiveHour;
|
|
1777
|
+
const newDecision = newWeekly >= STOP_WK || newFiveHour >= STOP_5H ? "STOP" : newWeekly >= THR_WK || newFiveHour >= THR_5H ? "THROTTLE" : "GO";
|
|
1778
|
+
last = { ...last, weekly: newWeekly, fiveHour: newFiveHour, advice: p0.advice, decision: newDecision };
|
|
1711
1779
|
}
|
|
1712
1780
|
writeState({ ...last, providers, model: currentModel, provider: currentProvider, isFree: false, agent: currentAgent, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1713
1781
|
log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
|
|
@@ -1781,7 +1849,13 @@ async function UsageCoachPlugin(input) {
|
|
|
1781
1849
|
let instruction = "";
|
|
1782
1850
|
if (c.decision === "STOP") instruction = `[${PLUGIN_NAME}] QUOTA limit exceeded. ${c.advice} Stop making further tool calls, finish the in-progress work, then report the quota status to the user.`;
|
|
1783
1851
|
else if (c.decision === "THROTTLE") instruction = `[${PLUGIN_NAME}] ${c.advice} Hold off on long/heavy tasks.`;
|
|
1784
|
-
else if (c.weekly >= 0
|
|
1852
|
+
else if (c.weekly >= 0 || c.fiveHour >= 0) {
|
|
1853
|
+
const parts = [];
|
|
1854
|
+
if (c.weekly >= 0) parts.push(`weekly ${c.weekly}%`);
|
|
1855
|
+
if (c.fiveHour >= 0) parts.push(`5h ${c.fiveHour}%`);
|
|
1856
|
+
if (c.monthly >= 0) parts.push(`monthly ${c.monthly}%`);
|
|
1857
|
+
instruction = `[${PLUGIN_NAME}] quota ok \u2014 ${parts.join(" \xB7 ")}.`;
|
|
1858
|
+
}
|
|
1785
1859
|
if (instruction) output.system.push(instruction);
|
|
1786
1860
|
} catch (e) {
|
|
1787
1861
|
log(`system.transform err: ${String(e)}`);
|
|
@@ -2645,32 +2719,4 @@ Note: Changes take effect immediately for new generate/grade calls.`,
|
|
|
2645
2719
|
return NOOP_HOOKS;
|
|
2646
2720
|
}
|
|
2647
2721
|
}
|
|
2648
|
-
export {
|
|
2649
|
-
buildGapPrompt,
|
|
2650
|
-
buildScanSummary,
|
|
2651
|
-
checkScanGate,
|
|
2652
|
-
clearSubSession,
|
|
2653
|
-
coach,
|
|
2654
|
-
UsageCoachPlugin as default,
|
|
2655
|
-
detectLanguage,
|
|
2656
|
-
extractImplNotes,
|
|
2657
|
-
extractKeywords,
|
|
2658
|
-
findActiveTaskId,
|
|
2659
|
-
formatReport,
|
|
2660
|
-
humanRemaining,
|
|
2661
|
-
isFreeModel,
|
|
2662
|
-
isHarnessAgent,
|
|
2663
|
-
parseFileList,
|
|
2664
|
-
parseGapAnalysis,
|
|
2665
|
-
parseQuotaResponse,
|
|
2666
|
-
providerAdvice,
|
|
2667
|
-
providerToCodexbar,
|
|
2668
|
-
readHarness,
|
|
2669
|
-
readHarnessCfg,
|
|
2670
|
-
readRules,
|
|
2671
|
-
UsageCoachPlugin as server,
|
|
2672
|
-
setStateDir,
|
|
2673
|
-
updateSubSession,
|
|
2674
|
-
writeHarness,
|
|
2675
|
-
writeHarnessCfg
|
|
2676
|
-
};
|
|
2722
|
+
export { UsageCoachPlugin as server, UsageCoachPlugin as default };
|
package/dist/tui.js
CHANGED
|
@@ -324,7 +324,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
324
324
|
}
|
|
325
325
|
if (s.providers && s.providers.length > 0) {
|
|
326
326
|
for (const p of s.providers) {
|
|
327
|
-
nodes.push((() => {
|
|
327
|
+
if (p.fiveHour >= 0) nodes.push((() => {
|
|
328
328
|
var _el$16 = _$createElement("box"), _el$17 = _$createElement("text"), _el$19 = _$createElement("text"), _el$20 = _$createElement("text"), _el$21 = _$createElement("text"), _el$22 = _$createTextNode(` `), _el$23 = _$createTextNode(`% `);
|
|
329
329
|
_$insertNode(_el$16, _el$17);
|
|
330
330
|
_$insertNode(_el$16, _el$19);
|
|
@@ -349,7 +349,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
349
349
|
});
|
|
350
350
|
return _el$16;
|
|
351
351
|
})());
|
|
352
|
-
nodes.push((() => {
|
|
352
|
+
if (p.weekly >= 0) nodes.push((() => {
|
|
353
353
|
var _el$24 = _$createElement("box"), _el$25 = _$createElement("text"), _el$27 = _$createElement("text"), _el$28 = _$createElement("text"), _el$29 = _$createElement("text"), _el$30 = _$createTextNode(` `), _el$31 = _$createTextNode(`% `);
|
|
354
354
|
_$insertNode(_el$24, _el$25);
|
|
355
355
|
_$insertNode(_el$24, _el$27);
|
|
@@ -376,7 +376,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
376
376
|
})());
|
|
377
377
|
}
|
|
378
378
|
} else {
|
|
379
|
-
nodes.push((() => {
|
|
379
|
+
if (s.fiveHour >= 0) nodes.push((() => {
|
|
380
380
|
var _el$32 = _$createElement("box"), _el$33 = _$createElement("text"), _el$35 = _$createElement("text"), _el$36 = _$createElement("text"), _el$37 = _$createElement("text"), _el$38 = _$createTextNode(` `), _el$39 = _$createTextNode(`%`);
|
|
381
381
|
_$insertNode(_el$32, _el$33);
|
|
382
382
|
_$insertNode(_el$32, _el$35);
|
|
@@ -400,7 +400,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
400
400
|
});
|
|
401
401
|
return _el$32;
|
|
402
402
|
})());
|
|
403
|
-
nodes.push((() => {
|
|
403
|
+
if (s.weekly >= 0) nodes.push((() => {
|
|
404
404
|
var _el$40 = _$createElement("box"), _el$41 = _$createElement("text"), _el$43 = _$createElement("text"), _el$44 = _$createElement("text"), _el$45 = _$createElement("text"), _el$46 = _$createTextNode(` `), _el$47 = _$createTextNode(`%`);
|
|
405
405
|
_$insertNode(_el$40, _el$41);
|
|
406
406
|
_$insertNode(_el$40, _el$43);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-usage-coach",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "opencode closed-loop usage coach — quota SENSE -> coaching DECIDE -> loop ACT + TUI integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,8 +13,15 @@
|
|
|
13
13
|
"./tui": {
|
|
14
14
|
"types": "./dist/tui.d.ts",
|
|
15
15
|
"import": "./dist/tui.js"
|
|
16
|
+
},
|
|
17
|
+
"./cli": {
|
|
18
|
+
"types": "./dist/cli.d.ts",
|
|
19
|
+
"import": "./dist/cli.js"
|
|
16
20
|
}
|
|
17
21
|
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"usage-coach": "./dist/cli.js"
|
|
24
|
+
},
|
|
18
25
|
"scripts": {
|
|
19
26
|
"build": "tsup",
|
|
20
27
|
"typecheck": "tsc --noEmit",
|