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 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 wire `codexbar` and drop `agents/usage-coach-harness.md` into `~/.config/opencode/agents/` for agent mode:
36
+ Then run setup:
37
37
 
38
38
  ```bash
39
- # provider quota data source
40
- printf '%s' "$YOUR_PROVIDER_API_KEY" | codexbar config set-api-key --provider <id> --stdin
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
- # harness role → model mapping (place in your work directory)
43
- cp harness.config.example.json harness.config.json # edit generator/grader
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
- await client.session.delete?.({ path: { id } });
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
- const summary = await client.session.summarize?.({ path: { id } });
1439
- log(`runModel(${model}): sub-session summary: ${JSON.stringify(summary?.data ?? summary).slice(0, 300)}`);
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
- await client.session.delete?.({ path: { id } });
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
- await client.session.delete?.({ path: { id: subId } });
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", () => resolve2(""));
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 u = JSON.parse(out)[0]?.usage;
1541
- if (!u) return null;
1542
- const h5 = Math.round(u.tertiary?.usedPercent ?? 0);
1543
- const wk = Math.round(u.primary?.usedPercent ?? 0);
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(u.tertiary?.resetsAt),
1550
- weeklyReset: humanRemaining(u.primary?.resetsAt),
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
- return {
1566
- weekly: u.primary ?? { usedPercent: 0 },
1567
- monthly: u.secondary ?? { usedPercent: 0 },
1568
- fiveHour: u.tertiary ?? { usedPercent: 0 }
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", () => resolve2(null));
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 = Math.round(q.weekly?.usedPercent ?? 0), mo = Math.round(q.monthly?.usedPercent ?? 0), h5 = Math.round(q.fiveHour?.usedPercent ?? 0);
1608
- if (Number.isNaN(wk) || Number.isNaN(mo) || Number.isNaN(h5)) return { decision: "THROTTLE", advice: "invalid quota data \u2014 proceeding with caution. switch to lighter model if available.", weekly: Number.isNaN(wk) ? 0 : wk, monthly: Number.isNaN(mo) ? 0 : mo, fiveHour: Number.isNaN(h5) ? 0 : h5 };
1609
- const wkR = humanRemaining(q.weekly?.resetsAt), h5R = humanRemaining(q.fiveHour?.resetsAt);
1610
- 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 });
1611
- 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 });
1612
- if (h5 >= STOP_5H) return stop(`5h window ${h5}% (${h5R})`);
1613
- if (wk >= STOP_WK) return stop(`weekly ${wk}% (${wkR})`);
1614
- if (mo >= STOP_MO) return stop(`monthly ${mo}%`);
1615
- if (h5 >= THR_5H) return thr(`5h window ${h5}% (${h5R})`);
1616
- if (wk >= THR_WK) return thr(`weekly ${wk}% (${wkR})`);
1617
- return { decision: "GO", advice: `Comfortable \u2014 weekly ${wk}% \xB7 5h ${h5}% \xB7 monthly ${mo}%. proceed. 5h window ${h5R}.`, weekly: wk, monthly: mo, fiveHour: h5 };
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
- last = { ...last, weekly: p0.weekly, fiveHour: p0.fiveHour, monthly: p0.weekly >= 0 ? 0 : -1, advice: p0.advice, decision: p0.weekly >= STOP_WK ? "STOP" : p0.weekly >= THR_WK ? "THROTTLE" : "GO" };
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) instruction = `[${PLUGIN_NAME}] quota ok \u2014 weekly ${c.weekly}% \xB7 5h ${c.fiveHour}% \xB7 monthly ${c.monthly}%.`;
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.12.1",
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",