opencode-usage-coach 0.13.0 → 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/dist/cli.js CHANGED
@@ -279,8 +279,8 @@ function formatStatus(r) {
279
279
  const model = q.model ? ` ${q.model.split("/").pop()}` : "";
280
280
  lines.push(`usage-coach [${tag}]${model}`);
281
281
  if (!q.isFree) {
282
- lines.push(` 5h ${bar(q.fiveHour)} ${q.fiveHour}%`);
283
- lines.push(` 1w ${bar(q.weekly)} ${q.weekly}%`);
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
284
  }
285
285
  if (q.advice) lines.push(` ${q.advice}`);
286
286
  if (r.harness?.active) {
package/dist/index.js CHANGED
@@ -1534,11 +1534,11 @@ async function fetchEnabledProviders() {
1534
1534
  }
1535
1535
  function providerAdvice(h5, wk) {
1536
1536
  const S5H = STOP_5H, SWK = STOP_WK, T5H = THR_5H, TWK = THR_WK;
1537
- if (h5 >= S5H || wk >= SWK) return "STOP \u2014 finish current only";
1538
- if (h5 >= T5H && wk >= TWK) return "small tasks only \u2014 big ones will hit both limits";
1539
- if (h5 >= T5H) return "small tasks only \u2014 5h window nearly full, big tasks after reset";
1540
- if (wk >= TWK) return "small tasks only \u2014 big ones will strain late-week";
1541
- 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";
1542
1542
  return "big tasks OK \u2014 short & long limits comfortable";
1543
1543
  }
1544
1544
  async function fetchProvidersCoach() {
@@ -1546,17 +1546,17 @@ async function fetchProvidersCoach() {
1546
1546
  const results = await Promise.all(ids.map(async (id) => {
1547
1547
  try {
1548
1548
  const out = await captureStdout(["usage", "--provider", id, "--json"]);
1549
- const u = JSON.parse(out)[0]?.usage;
1550
- if (!u) return null;
1551
- const h5 = Math.round(u.tertiary?.usedPercent ?? 0);
1552
- 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;
1553
1553
  return {
1554
1554
  id,
1555
1555
  name: id,
1556
- fiveHour: h5,
1557
- weekly: wk,
1558
- fiveHourReset: humanRemaining(u.tertiary?.resetsAt),
1559
- 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) : "",
1560
1560
  advice: providerAdvice(h5, wk)
1561
1561
  };
1562
1562
  } catch {
@@ -1571,11 +1571,36 @@ function parseQuotaResponse(rawText) {
1571
1571
  if (!text || text === "[]") return null;
1572
1572
  const u = JSON.parse(text)[0]?.usage;
1573
1573
  if (!u) return null;
1574
- return {
1575
- weekly: u.primary ?? { usedPercent: 0 },
1576
- monthly: u.secondary ?? { usedPercent: 0 },
1577
- fiveHour: u.tertiary ?? { usedPercent: 0 }
1578
- };
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;
1579
1604
  } catch {
1580
1605
  return null;
1581
1606
  }
@@ -1617,17 +1642,31 @@ async function fetchQuotaWithRetry(provider, maxRetries = 3) {
1617
1642
  }
1618
1643
  function coach(q, lighter) {
1619
1644
  if (!q) return { decision: "GO", advice: "quota unavailable \u2014 retrying. proceeding cautiously.", weekly: -2, monthly: -2, fiveHour: -2 };
1620
- const wk = Math.round(q.weekly?.usedPercent ?? 0), mo = Math.round(q.monthly?.usedPercent ?? 0), h5 = Math.round(q.fiveHour?.usedPercent ?? 0);
1621
- 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 };
1622
- const wkR = humanRemaining(q.weekly?.resetsAt), h5R = humanRemaining(q.fiveHour?.resetsAt);
1623
- 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 });
1624
- 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 });
1625
- if (h5 >= STOP_5H) return stop(`5h window ${h5}% (${h5R})`);
1626
- if (wk >= STOP_WK) return stop(`weekly ${wk}% (${wkR})`);
1627
- if (mo >= STOP_MO) return stop(`monthly ${mo}%`);
1628
- if (h5 >= THR_5H) return thr(`5h window ${h5}% (${h5R})`);
1629
- if (wk >= THR_WK) return thr(`weekly ${wk}% (${wkR})`);
1630
- 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 };
1631
1670
  }
1632
1671
  var agentCache = /* @__PURE__ */ new Map();
1633
1672
  var currentModel = "";
@@ -1731,9 +1770,12 @@ async function UsageCoachPlugin(input) {
1731
1770
  providers = await fetchProvidersCoach();
1732
1771
  } catch {
1733
1772
  }
1734
- if (providers.length > 0 && last.weekly < 0) {
1773
+ if (providers.length > 0 && last.weekly < 0 && last.fiveHour < 0) {
1735
1774
  const p0 = providers[0];
1736
- 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 };
1737
1779
  }
1738
1780
  writeState({ ...last, providers, model: currentModel, provider: currentProvider, isFree: false, agent: currentAgent, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
1739
1781
  log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
@@ -1807,7 +1849,13 @@ async function UsageCoachPlugin(input) {
1807
1849
  let instruction = "";
1808
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.`;
1809
1851
  else if (c.decision === "THROTTLE") instruction = `[${PLUGIN_NAME}] ${c.advice} Hold off on long/heavy tasks.`;
1810
- 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
+ }
1811
1859
  if (instruction) output.system.push(instruction);
1812
1860
  } catch (e) {
1813
1861
  log(`system.transform err: ${String(e)}`);
@@ -2671,35 +2719,4 @@ Note: Changes take effect immediately for new generate/grade calls.`,
2671
2719
  return NOOP_HOOKS;
2672
2720
  }
2673
2721
  }
2674
- export {
2675
- __resetCodexbarMissing,
2676
- buildGapPrompt,
2677
- buildScanSummary,
2678
- checkScanGate,
2679
- clearSubSession,
2680
- coach,
2681
- UsageCoachPlugin as default,
2682
- detectLanguage,
2683
- extractImplNotes,
2684
- extractKeywords,
2685
- fetchQuotaWithRetry,
2686
- findActiveTaskId,
2687
- formatReport,
2688
- humanRemaining,
2689
- isCodexbarMissing,
2690
- isFreeModel,
2691
- isHarnessAgent,
2692
- parseFileList,
2693
- parseGapAnalysis,
2694
- parseQuotaResponse,
2695
- providerAdvice,
2696
- providerToCodexbar,
2697
- readHarness,
2698
- readHarnessCfg,
2699
- readRules,
2700
- UsageCoachPlugin as server,
2701
- setStateDir,
2702
- updateSubSession,
2703
- writeHarness,
2704
- writeHarnessCfg
2705
- };
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.13.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",