opencode-usage-coach 0.5.0 → 0.6.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
@@ -155,6 +155,7 @@ Place in the **work directory**. Each role runs on its model, so per-model quota
155
155
  | `UC_HARNESS_AGENT` | `Usage-Coach-Harness` | comma-separated agent modes allowed to use harness tools + receive quota coaching (case-insensitive; must match the agent id, e.g. `usage-coach-harness` from `agents/usage-coach-harness.md`) |
156
156
  | `UC_WORM_MAX_AGE_DAYS` | 180 | domain DB worm (GC): drop nodes not accessed in N days (~6 months) |
157
157
  | `UC_WORM_MAX_NODES` | 100000 | domain DB worm (GC): cap node count, evict oldest-accessed beyond this |
158
+ | `UC_DOMAIN_TIMEOUT_MS` | 5000 | domain DB query timeout (ms) — a slow/hung query resolves to a safe fallback instead of blocking the plugin |
158
159
 
159
160
  ## Agent-mode scoping
160
161
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, appendFileSync as appendFileSync2, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
2
+ import { mkdirSync, writeFileSync, appendFileSync, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
3
3
  import { spawn } from "child_process";
4
4
  import { createHash } from "crypto";
5
5
  import { homedir } from "os";
@@ -7,108 +7,188 @@ import { join as join2, resolve, dirname } from "path";
7
7
  import { tool } from "@opencode-ai/plugin";
8
8
 
9
9
  // src/domain.ts
10
- import { mkdirSync, appendFileSync, readFileSync, existsSync, writeFileSync } from "fs";
10
+ import { Database, Connection } from "@ladybugdb/core";
11
11
  import { join } from "path";
12
- var BASE_DIR = "";
13
- function initDomain(stateDir) {
14
- BASE_DIR = stateDir;
15
- }
16
- var nodesFile = () => join(BASE_DIR, "nodes.ndjson");
17
- var edgesFile = () => join(BASE_DIR, "edges.ndjson");
18
- function readNdjson(path) {
12
+ import { existsSync, readFileSync } from "fs";
13
+ var QUERY_TIMEOUT_MS = (() => {
19
14
  try {
20
- if (!existsSync(path)) return [];
21
- return readFileSync(path, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
15
+ const v = Number(process.env.UC_DOMAIN_TIMEOUT_MS);
16
+ return Number.isFinite(v) && v > 0 ? v : 5e3;
22
17
  } catch {
23
- return [];
18
+ return 5e3;
24
19
  }
25
- }
26
- function readNodes() {
27
- return readNdjson(nodesFile());
28
- }
29
- function readEdges() {
30
- return readNdjson(edgesFile());
20
+ })();
21
+ var BASE_DIR = "";
22
+ var DB_PATH = "";
23
+ var db = null;
24
+ var conn = null;
25
+ var schemaReady = false;
26
+ var migrated = false;
27
+ function initDomain(stateDir) {
28
+ BASE_DIR = stateDir;
29
+ DB_PATH = join(BASE_DIR, "domain.ladybug");
30
+ db = null;
31
+ conn = null;
32
+ schemaReady = false;
33
+ migrated = false;
31
34
  }
32
35
  function uid(prefix) {
33
36
  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
34
37
  }
35
- function addDomainNode(node) {
36
- const full = { ...node, id: uid("node"), ts: (/* @__PURE__ */ new Date()).toISOString() };
38
+ function esc(s) {
39
+ return String(s ?? "").replace(/\\/g, "\\\\").replace(/'/g, "\\'");
40
+ }
41
+ function timed(op, fallback) {
42
+ let timer;
43
+ const timeout = new Promise((resolve2) => {
44
+ timer = setTimeout(() => resolve2(fallback), QUERY_TIMEOUT_MS);
45
+ });
46
+ return Promise.race([op().catch(() => fallback), timeout]).finally(() => {
47
+ if (timer) clearTimeout(timer);
48
+ });
49
+ }
50
+ function parseNode(r) {
51
+ let props = {};
37
52
  try {
38
- mkdirSync(BASE_DIR, { recursive: true });
39
- appendFileSync(nodesFile(), JSON.stringify(full) + "\n");
53
+ if (r.props) props = JSON.parse(r.props);
40
54
  } catch {
41
55
  }
42
- return full.id;
56
+ return {
57
+ id: r.id,
58
+ type: r.type,
59
+ name: r.name,
60
+ props,
61
+ source: r.source ?? "",
62
+ confidence: r.confidence ?? 0,
63
+ ts: r.ts,
64
+ lastAccessed: r.lastAccessed || void 0,
65
+ accessCount: r.accessCount ?? void 0
66
+ };
67
+ }
68
+ var NODE_COLS = "n.id AS id, n.type AS type, n.name AS name, n.props AS props, n.source AS source, n.confidence AS confidence, n.ts AS ts, n.lastAccessed AS lastAccessed, n.accessCount AS accessCount";
69
+ async function ready() {
70
+ if (!conn) {
71
+ db = new Database(DB_PATH);
72
+ conn = new Connection(db);
73
+ }
74
+ if (!schemaReady) {
75
+ try {
76
+ await conn.query("CREATE NODE TABLE DomainNode(id STRING, type STRING, name STRING, props STRING, source STRING, confidence DOUBLE, lastAccessed STRING, accessCount INT64 DEFAULT 0, ts STRING, PRIMARY KEY(id))");
77
+ } catch {
78
+ }
79
+ try {
80
+ await conn.query("CREATE REL TABLE Related(FROM DomainNode TO DomainNode, rel STRING, note STRING, ts STRING)");
81
+ } catch {
82
+ }
83
+ schemaReady = true;
84
+ }
85
+ if (!migrated) {
86
+ migrated = true;
87
+ await migrateFromNdjson();
88
+ }
89
+ return conn;
43
90
  }
44
- function writeNodes(nodes) {
91
+ async function migrateFromNdjson() {
92
+ if (!conn) return;
93
+ const nf = join(BASE_DIR, "nodes.ndjson");
94
+ const ef = join(BASE_DIR, "edges.ndjson");
95
+ if (!existsSync(nf)) return;
45
96
  try {
46
- mkdirSync(BASE_DIR, { recursive: true });
47
- const lines = nodes.map((n) => JSON.stringify(n));
48
- writeFileSync(nodesFile(), lines.length ? lines.join("\n") + "\n" : "");
97
+ const existing = await conn.query("MATCH (n:DomainNode) RETURN count(n) AS c");
98
+ const cnt = (await existing.getAll())[0]?.c ?? 0;
99
+ if (cnt > 0) return;
100
+ const readNdjson = (p) => {
101
+ try {
102
+ return readFileSync(p, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
103
+ } catch {
104
+ return [];
105
+ }
106
+ };
107
+ for (const n of readNdjson(nf)) {
108
+ await conn.query(`CREATE (n:DomainNode {id:'${esc(n.id)}',type:'${esc(n.type)}',name:'${esc(n.name)}',props:'${esc(JSON.stringify(n.props ?? {}))}',source:'${esc(n.source ?? "")}',confidence:${Number(n.confidence ?? 0)},ts:'${esc(n.ts ?? "")}',lastAccessed:'${esc(n.lastAccessed ?? "")}',accessCount:${Number(n.accessCount ?? 0)}})`);
109
+ }
110
+ if (existsSync(ef)) {
111
+ for (const e of readNdjson(ef)) {
112
+ await conn.query(`MATCH (a:DomainNode {id:'${esc(e.from)}'}), (b:DomainNode {id:'${esc(e.to)}'}) CREATE (a)-[:Related {rel:'${esc(e.rel)}',note:'${esc(e.note ?? "")}',ts:'${esc(e.ts ?? "")}'}]->(b)`);
113
+ }
114
+ }
49
115
  } catch {
50
116
  }
51
117
  }
118
+ async function _readNodes() {
119
+ const c = await ready();
120
+ const r = await c.query(`MATCH (n:DomainNode) RETURN ${NODE_COLS}`);
121
+ return (await r.getAll()).map(parseNode);
122
+ }
123
+ async function _addDomainNode(node) {
124
+ const c = await ready();
125
+ const id = uid("node");
126
+ const now = (/* @__PURE__ */ new Date()).toISOString();
127
+ await c.query(`CREATE (n:DomainNode {id:'${esc(id)}',type:'${esc(node.type)}',name:'${esc(node.name)}',props:'${esc(JSON.stringify(node.props ?? {}))}',source:'${esc(node.source ?? "")}',confidence:${Number(node.confidence ?? 0)},ts:'${esc(now)}',lastAccessed:'',accessCount:0})`);
128
+ return id;
129
+ }
52
130
  function queryDomain(keywords) {
53
- const lc = keywords.map((k) => k.toLowerCase());
54
- const nodes = readNodes();
55
- const matched = nodes.filter((n) => {
56
- const hay = (n.name + " " + JSON.stringify(n.props)).toLowerCase();
57
- return lc.some((k) => k && hay.includes(k));
58
- });
59
- if (matched.length) touchNodes(new Set(matched.map((n) => n.id)));
60
- const ids = new Set(matched.map((n) => n.id));
61
- const edges = readEdges().filter((e) => ids.has(e.from) || ids.has(e.to));
131
+ return timed(() => _queryDomain(keywords), { nodes: [], edges: [] });
132
+ }
133
+ async function _queryDomain(keywords) {
134
+ const lc = keywords.map((k) => k.toLowerCase()).filter(Boolean);
135
+ if (lc.length === 0) return { nodes: [], edges: [] };
136
+ const c = await ready();
137
+ const conds = lc.map((kw) => `(lower(n.name) CONTAINS '${esc(kw)}' OR lower(n.props) CONTAINS '${esc(kw)}')`).join(" OR ");
138
+ const r = await c.query(`MATCH (n:DomainNode) WHERE ${conds} RETURN ${NODE_COLS}`);
139
+ const matched = (await r.getAll()).map(parseNode);
140
+ if (matched.length) await _touchNodes(new Set(matched.map((n) => n.id)));
141
+ let edges = [];
142
+ if (matched.length) {
143
+ const ids = matched.map((n) => `'${esc(n.id)}'`).join(",");
144
+ const er = await c.query(`MATCH (a:DomainNode)-[r:Related]->(b:DomainNode) WHERE a.id IN [${ids}] OR b.id IN [${ids}] RETURN a.id AS from, b.id AS to, r.rel AS rel, r.note AS note, r.ts AS ts`);
145
+ edges = await er.getAll();
146
+ }
62
147
  return { nodes: matched, edges };
63
148
  }
64
- function touchNodes(ids) {
149
+ async function _touchNodes(ids) {
65
150
  if (ids.size === 0) return;
66
- try {
67
- const nodes = readNodes();
68
- let changed = false;
69
- const now = (/* @__PURE__ */ new Date()).toISOString();
70
- for (const n of nodes) {
71
- if (ids.has(n.id)) {
72
- n.lastAccessed = now;
73
- n.accessCount = (n.accessCount ?? 0) + 1;
74
- changed = true;
75
- }
76
- }
77
- if (changed) writeNodes(nodes);
78
- } catch {
151
+ const c = await ready();
152
+ const now = (/* @__PURE__ */ new Date()).toISOString();
153
+ const idList = [...ids].map((id) => `'${esc(id)}'`).join(",");
154
+ await c.query(`MATCH (n:DomainNode) WHERE n.id IN [${idList}] SET n.lastAccessed = '${esc(now)}', n.accessCount = COALESCE(n.accessCount, 0) + 1`);
155
+ }
156
+ function evictStale(maxAgeDays = 30, maxNodes = 1e5) {
157
+ return timed(() => _evictStale(maxAgeDays, maxNodes), { removed: 0, kept: 0 });
158
+ }
159
+ async function _evictStale(maxAgeDays, maxNodes) {
160
+ const c = await ready();
161
+ const all = await _readNodes();
162
+ if (all.length === 0) return { removed: 0, kept: 0 };
163
+ const now = Date.now();
164
+ const ageMs = maxAgeDays * 864e5;
165
+ const lastTs = (n) => {
166
+ const la = n.lastAccessed ? new Date(n.lastAccessed).getTime() : 0;
167
+ return la || new Date(n.ts).getTime();
168
+ };
169
+ let kept = all.filter((n) => now - lastTs(n) < ageMs);
170
+ if (kept.length > maxNodes) {
171
+ kept.sort((a, b) => lastTs(b) - lastTs(a));
172
+ kept = kept.slice(0, maxNodes);
79
173
  }
80
- }
81
- function evictStale(maxAgeDays = 30, maxNodes = 1e3) {
82
- try {
83
- const nodes = readNodes();
84
- if (nodes.length === 0) return { removed: 0, kept: 0 };
85
- const now = Date.now();
86
- const ageMs = maxAgeDays * 864e5;
87
- const lastTs = (n) => new Date(n.lastAccessed ?? n.ts).getTime();
88
- let kept = nodes.filter((n) => now - lastTs(n) < ageMs);
89
- if (kept.length > maxNodes) {
90
- kept.sort((a, b) => lastTs(b) - lastTs(a));
91
- kept = kept.slice(0, maxNodes);
92
- }
93
- const removed = nodes.length - kept.length;
94
- if (removed > 0) writeNodes(kept);
95
- return { removed, kept: kept.length };
96
- } catch {
97
- return { removed: 0, kept: 0 };
174
+ const keepIds = new Set(kept.map((n) => n.id));
175
+ const toRemove = all.filter((n) => !keepIds.has(n.id));
176
+ for (const n of toRemove) {
177
+ await c.query(`MATCH (n:DomainNode {id:'${esc(n.id)}'}) DETACH DELETE n`);
98
178
  }
179
+ return { removed: toRemove.length, kept: kept.length };
99
180
  }
100
181
  function saveInvestigationResult(keywords, result, source) {
101
- try {
102
- return addDomainNode({
103
- type: "fact",
104
- name: keywords.join(" "),
105
- props: { result },
106
- source: source || "investigation",
107
- confidence: 0.7
108
- });
109
- } catch {
110
- return "";
111
- }
182
+ return timed(() => _saveInvestigationResult(keywords, result, source), "");
183
+ }
184
+ async function _saveInvestigationResult(keywords, result, source) {
185
+ return _addDomainNode({
186
+ type: "fact",
187
+ name: keywords.join(" "),
188
+ props: { result },
189
+ source: source || "investigation",
190
+ confidence: 0.7
191
+ });
112
192
  }
113
193
 
114
194
  // src/index.ts
@@ -130,15 +210,15 @@ function setStateDir(dir) {
130
210
  var NOOP_HOOKS = {};
131
211
  function log(msg) {
132
212
  try {
133
- appendFileSync2(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
213
+ appendFileSync(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
134
214
  `);
135
215
  } catch {
136
216
  }
137
217
  }
138
218
  function writeState(c) {
139
219
  try {
140
- mkdirSync2(STATE_DIR, { recursive: true });
141
- writeFileSync2(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
220
+ mkdirSync(STATE_DIR, { recursive: true });
221
+ writeFileSync(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
142
222
  } catch {
143
223
  }
144
224
  }
@@ -189,9 +269,9 @@ function readHarness(sessionID) {
189
269
  function writeHarness(sessionID, h) {
190
270
  try {
191
271
  const f = harnessFile(sessionID);
192
- mkdirSync2(dirname(f), { recursive: true });
272
+ mkdirSync(dirname(f), { recursive: true });
193
273
  h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
194
- writeFileSync2(f, JSON.stringify(h, null, 2));
274
+ writeFileSync(f, JSON.stringify(h, null, 2));
195
275
  } catch {
196
276
  }
197
277
  }
@@ -371,6 +451,7 @@ function coach(q, lighter) {
371
451
  return { decision: "GO", advice: `Comfortable \u2014 weekly ${wk}% \xB7 5h ${h5}% \xB7 monthly ${mo}%. proceed. 5h window ${h5R}.`, weekly: wk, monthly: mo, fiveHour: h5 };
372
452
  }
373
453
  var agentCache = /* @__PURE__ */ new Map();
454
+ var lastResolvedAgent = "";
374
455
  async function resolveAgent(client, sessionID) {
375
456
  if (!sessionID) return "";
376
457
  const hit = agentCache.get(sessionID);
@@ -379,6 +460,7 @@ async function resolveAgent(client, sessionID) {
379
460
  const s = await client.session.get({ path: { id: sessionID } });
380
461
  const agent = String(s?.data?.info?.agent ?? s?.data?.agent ?? s?.info?.agent ?? "");
381
462
  agentCache.set(sessionID, { agent, ts: Date.now() });
463
+ lastResolvedAgent = agent;
382
464
  return agent;
383
465
  } catch (e) {
384
466
  log(`resolveAgent err: ${String(e)}`);
@@ -418,7 +500,7 @@ async function UsageCoachPlugin(input) {
418
500
  const p0 = providers[0];
419
501
  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" };
420
502
  }
421
- writeState({ ...last, providers, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
503
+ writeState({ ...last, agent: lastResolvedAgent, providers, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
422
504
  log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
423
505
  } catch (e) {
424
506
  log(`refresh-in-then err: ${String(e)}`);
@@ -446,7 +528,7 @@ async function UsageCoachPlugin(input) {
446
528
  if (event.type === "session.created" || event.type === "session.idle") refreshBackground();
447
529
  if (event.type === "session.idle") {
448
530
  try {
449
- const r = evictStale(WORM_MAX_AGE_DAYS, WORM_MAX_NODES);
531
+ const r = await evictStale(WORM_MAX_AGE_DAYS, WORM_MAX_NODES);
450
532
  if (r.removed) log(`evictStale: removed ${r.removed}, kept ${r.kept} (maxAge=${WORM_MAX_AGE_DAYS}d, maxNodes=${WORM_MAX_NODES})`);
451
533
  } catch (e) {
452
534
  log(`evictStale err: ${String(e)}`);
@@ -571,8 +653,8 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
571
653
  async execute(args, _ctx) {
572
654
  const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), task: args.task, prompt: args.prompt, gradeResult: args.gradeResult, model: args.model, revisions: args.revisions };
573
655
  try {
574
- mkdirSync2(STATE_DIR, { recursive: true });
575
- appendFileSync2(failuresFile(), JSON.stringify(rec) + "\n");
656
+ mkdirSync(STATE_DIR, { recursive: true });
657
+ appendFileSync(failuresFile(), JSON.stringify(rec) + "\n");
576
658
  } catch (e) {
577
659
  log(`record_failure err: ${String(e)}`);
578
660
  }
@@ -596,7 +678,7 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
596
678
  try {
597
679
  keywords = extractKeywords(`${args.task} ${args.gradeResult}`);
598
680
  if (keywords.length) {
599
- const { nodes, edges } = queryDomain(keywords);
681
+ const { nodes, edges } = await queryDomain(keywords);
600
682
  if (nodes && nodes.length || edges && edges.length) {
601
683
  domainEmpty = false;
602
684
  domainPrefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
@@ -620,7 +702,7 @@ evidence: <file/line or specific quote>`;
620
702
  const out = await runModel(input.client, cfg.generator, domainPrefix + rcaPrompt, ctx.directory);
621
703
  if (domainEmpty && keywords.length) {
622
704
  try {
623
- saveInvestigationResult(keywords, out, "investigate");
705
+ await saveInvestigationResult(keywords, out, "investigate");
624
706
  } catch (e) {
625
707
  log(`investigate save err: ${String(e)}`);
626
708
  }
@@ -676,8 +758,8 @@ Keep it concrete and actionable.`;
676
758
  const rule = out;
677
759
  try {
678
760
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
679
- mkdirSync2(STATE_DIR, { recursive: true });
680
- appendFileSync2(rulesFile(), `## Rule (${date})
761
+ mkdirSync(STATE_DIR, { recursive: true });
762
+ appendFileSync(rulesFile(), `## Rule (${date})
681
763
  ${rule}
682
764
  Origin: ${args.task}
683
765
 
@@ -716,7 +798,7 @@ ${rules}
716
798
  try {
717
799
  keywords = extractKeywords(args.prompt);
718
800
  if (keywords.length) {
719
- const { nodes, edges } = queryDomain(keywords);
801
+ const { nodes, edges } = await queryDomain(keywords);
720
802
  if (nodes && nodes.length || edges && edges.length) {
721
803
  domainEmpty = false;
722
804
  prefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
@@ -732,7 +814,7 @@ ${rules}
732
814
  const out = await runModel(input.client, model, prefix + args.prompt, ctx.directory);
733
815
  if (domainEmpty && keywords.length) {
734
816
  try {
735
- saveInvestigationResult(keywords, out, "generate");
817
+ await saveInvestigationResult(keywords, out, "generate");
736
818
  } catch (e) {
737
819
  log(`generate save err: ${String(e)}`);
738
820
  }
package/dist/tui.js CHANGED
@@ -19,6 +19,7 @@ var STATE_DIR = join(homedir(), ".cache", "opencode-usage-coach");
19
19
  var STATE_FILE = join(STATE_DIR, "state.json");
20
20
  var HARNESS_FILE = join(STATE_DIR, "harness.json");
21
21
  var MARKER = join(STATE_DIR, "tui-loaded.txt");
22
+ var HARNESS_AGENT_IDS = (process.env.UC_HARNESS_AGENT ?? "Usage-Coach-Harness").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
22
23
  function readState() {
23
24
  try {
24
25
  if (!existsSync(STATE_FILE)) return null;
@@ -188,6 +189,16 @@ function initializeTui(api, disposeRoot) {
188
189
  } catch {
189
190
  s = null;
190
191
  }
192
+ const isHarness = s?.agent ? HARNESS_AGENT_IDS.includes(s.agent.toLowerCase()) : false;
193
+ if (s?.agent && !isHarness) {
194
+ return (() => {
195
+ var _el$4 = _$createElement("box"), _el$5 = _$createElement("text");
196
+ _$insertNode(_el$4, _el$5);
197
+ _$insertNode(_el$5, _$createTextNode(`usage-coach`));
198
+ _$effect((_$p) => _$setProp(_el$5, "style", st("textMuted"), _$p));
199
+ return _el$4;
200
+ })();
201
+ }
191
202
  let h = null;
192
203
  try {
193
204
  const routeSid = api.route?.current?.params?.sessionID ?? "";
@@ -205,149 +216,149 @@ function initializeTui(api, disposeRoot) {
205
216
  if (s) {
206
217
  const dKey = s.decision === "GO" ? "success" : s.decision === "THROTTLE" ? "warning" : "error";
207
218
  nodes.push((() => {
208
- var _el$4 = _$createElement("text"), _el$5 = _$createTextNode(`usage-coach [`), _el$6 = _$createTextNode(`]`);
209
- _$insertNode(_el$4, _el$5);
210
- _$insertNode(_el$4, _el$6);
211
- _$insert(_el$4, () => TAG[s.decision], _el$6);
212
- _$effect((_$p) => _$setProp(_el$4, "style", st(dKey), _$p));
213
- return _el$4;
219
+ var _el$7 = _$createElement("text"), _el$8 = _$createTextNode(`usage-coach [`), _el$9 = _$createTextNode(`]`);
220
+ _$insertNode(_el$7, _el$8);
221
+ _$insertNode(_el$7, _el$9);
222
+ _$insert(_el$7, () => TAG[s.decision], _el$9);
223
+ _$effect((_$p) => _$setProp(_el$7, "style", st(dKey), _$p));
224
+ return _el$7;
214
225
  })());
215
226
  if (s.providers && s.providers.length > 0) {
216
227
  for (const p of s.providers) {
217
228
  nodes.push((() => {
218
- var _el$7 = _$createElement("text"), _el$8 = _$createTextNode(` `);
219
- _$insertNode(_el$7, _el$8);
220
- _$insert(_el$7, () => p.name, null);
221
- _$effect((_$p) => _$setProp(_el$7, "style", st("textMuted"), _$p));
222
- return _el$7;
229
+ var _el$0 = _$createElement("text"), _el$1 = _$createTextNode(` `);
230
+ _$insertNode(_el$0, _el$1);
231
+ _$insert(_el$0, () => p.name, null);
232
+ _$effect((_$p) => _$setProp(_el$0, "style", st("textMuted"), _$p));
233
+ return _el$0;
223
234
  })());
224
235
  nodes.push((() => {
225
- var _el$9 = _$createElement("box"), _el$0 = _$createElement("text"), _el$10 = _$createElement("text"), _el$11 = _$createElement("text"), _el$12 = _$createElement("text"), _el$13 = _$createTextNode(` `), _el$14 = _$createTextNode(`% `);
226
- _$insertNode(_el$9, _el$0);
227
- _$insertNode(_el$9, _el$10);
228
- _$insertNode(_el$9, _el$11);
229
- _$insertNode(_el$9, _el$12);
230
- _$setProp(_el$9, "flexDirection", "row");
231
- _$insertNode(_el$0, _$createTextNode(` 5h `));
232
- _$insert(_el$10, () => barFill(p.fiveHour));
233
- _$insert(_el$11, () => barEmpty(p.fiveHour));
234
- _$insertNode(_el$12, _el$13);
235
- _$insertNode(_el$12, _el$14);
236
- _$insert(_el$12, () => p.fiveHour, _el$14);
237
- _$insert(_el$12, () => p.fiveHourReset, null);
236
+ var _el$10 = _$createElement("box"), _el$11 = _$createElement("text"), _el$13 = _$createElement("text"), _el$14 = _$createElement("text"), _el$15 = _$createElement("text"), _el$16 = _$createTextNode(` `), _el$17 = _$createTextNode(`% `);
237
+ _$insertNode(_el$10, _el$11);
238
+ _$insertNode(_el$10, _el$13);
239
+ _$insertNode(_el$10, _el$14);
240
+ _$insertNode(_el$10, _el$15);
241
+ _$setProp(_el$10, "flexDirection", "row");
242
+ _$insertNode(_el$11, _$createTextNode(` 5h `));
243
+ _$insert(_el$13, () => barFill(p.fiveHour));
244
+ _$insert(_el$14, () => barEmpty(p.fiveHour));
245
+ _$insertNode(_el$15, _el$16);
246
+ _$insertNode(_el$15, _el$17);
247
+ _$insert(_el$15, () => p.fiveHour, _el$17);
248
+ _$insert(_el$15, () => p.fiveHourReset, null);
238
249
  _$effect((_p$) => {
239
250
  var _v$ = st("text"), _v$2 = st("text");
240
- _v$ !== _p$.e && (_p$.e = _$setProp(_el$10, "style", _v$, _p$.e));
241
- _v$2 !== _p$.t && (_p$.t = _$setProp(_el$11, "style", _v$2, _p$.t));
251
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$13, "style", _v$, _p$.e));
252
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$14, "style", _v$2, _p$.t));
242
253
  return _p$;
243
254
  }, {
244
255
  e: void 0,
245
256
  t: void 0
246
257
  });
247
- return _el$9;
258
+ return _el$10;
248
259
  })());
249
260
  nodes.push((() => {
250
- var _el$15 = _$createElement("box"), _el$16 = _$createElement("text"), _el$18 = _$createElement("text"), _el$19 = _$createElement("text"), _el$20 = _$createElement("text"), _el$21 = _$createTextNode(` `), _el$22 = _$createTextNode(`% `);
251
- _$insertNode(_el$15, _el$16);
252
- _$insertNode(_el$15, _el$18);
253
- _$insertNode(_el$15, _el$19);
254
- _$insertNode(_el$15, _el$20);
255
- _$setProp(_el$15, "flexDirection", "row");
256
- _$insertNode(_el$16, _$createTextNode(` 1w `));
257
- _$insert(_el$18, () => barFill(p.weekly));
258
- _$insert(_el$19, () => barEmpty(p.weekly));
259
- _$insertNode(_el$20, _el$21);
260
- _$insertNode(_el$20, _el$22);
261
- _$insert(_el$20, () => p.weekly, _el$22);
262
- _$insert(_el$20, () => p.weeklyReset, null);
261
+ var _el$18 = _$createElement("box"), _el$19 = _$createElement("text"), _el$21 = _$createElement("text"), _el$22 = _$createElement("text"), _el$23 = _$createElement("text"), _el$24 = _$createTextNode(` `), _el$25 = _$createTextNode(`% `);
262
+ _$insertNode(_el$18, _el$19);
263
+ _$insertNode(_el$18, _el$21);
264
+ _$insertNode(_el$18, _el$22);
265
+ _$insertNode(_el$18, _el$23);
266
+ _$setProp(_el$18, "flexDirection", "row");
267
+ _$insertNode(_el$19, _$createTextNode(` 1w `));
268
+ _$insert(_el$21, () => barFill(p.weekly));
269
+ _$insert(_el$22, () => barEmpty(p.weekly));
270
+ _$insertNode(_el$23, _el$24);
271
+ _$insertNode(_el$23, _el$25);
272
+ _$insert(_el$23, () => p.weekly, _el$25);
273
+ _$insert(_el$23, () => p.weeklyReset, null);
263
274
  _$effect((_p$) => {
264
275
  var _v$3 = st("text"), _v$4 = st("text");
265
- _v$3 !== _p$.e && (_p$.e = _$setProp(_el$18, "style", _v$3, _p$.e));
266
- _v$4 !== _p$.t && (_p$.t = _$setProp(_el$19, "style", _v$4, _p$.t));
276
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$21, "style", _v$3, _p$.e));
277
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$22, "style", _v$4, _p$.t));
267
278
  return _p$;
268
279
  }, {
269
280
  e: void 0,
270
281
  t: void 0
271
282
  });
272
- return _el$15;
283
+ return _el$18;
273
284
  })());
274
285
  nodes.push((() => {
275
- var _el$23 = _$createElement("text"), _el$24 = _$createTextNode(` -> `);
276
- _$insertNode(_el$23, _el$24);
277
- _$insert(_el$23, () => p.advice, null);
278
- _$effect((_$p) => _$setProp(_el$23, "style", st(dKey), _$p));
279
- return _el$23;
286
+ var _el$26 = _$createElement("text"), _el$27 = _$createTextNode(` -> `);
287
+ _$insertNode(_el$26, _el$27);
288
+ _$insert(_el$26, () => p.advice, null);
289
+ _$effect((_$p) => _$setProp(_el$26, "style", st(dKey), _$p));
290
+ return _el$26;
280
291
  })());
281
292
  }
282
293
  } else {
283
294
  nodes.push((() => {
284
- var _el$27 = _$createElement("box"), _el$28 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text"), _el$32 = _$createElement("text");
285
- _$insertNode(_el$27, _el$28);
286
- _$insertNode(_el$27, _el$30);
287
- _$insertNode(_el$27, _el$31);
288
- _$insertNode(_el$27, _el$32);
289
- _$setProp(_el$27, "flexDirection", "row");
290
- _$insertNode(_el$28, _$createTextNode(` 5h `));
291
- _$insert(_el$30, () => barFill(s.fiveHour));
292
- _$insert(_el$31, () => barEmpty(s.fiveHour));
293
- _$insertNode(_el$32, _$createTextNode(` 0%`));
295
+ var _el$30 = _$createElement("box"), _el$31 = _$createElement("text"), _el$33 = _$createElement("text"), _el$34 = _$createElement("text"), _el$35 = _$createElement("text");
296
+ _$insertNode(_el$30, _el$31);
297
+ _$insertNode(_el$30, _el$33);
298
+ _$insertNode(_el$30, _el$34);
299
+ _$insertNode(_el$30, _el$35);
300
+ _$setProp(_el$30, "flexDirection", "row");
301
+ _$insertNode(_el$31, _$createTextNode(` 5h `));
302
+ _$insert(_el$33, () => barFill(s.fiveHour));
303
+ _$insert(_el$34, () => barEmpty(s.fiveHour));
304
+ _$insertNode(_el$35, _$createTextNode(` 0%`));
294
305
  _$effect((_p$) => {
295
306
  var _v$5 = st("text"), _v$6 = st("text");
296
- _v$5 !== _p$.e && (_p$.e = _$setProp(_el$30, "style", _v$5, _p$.e));
297
- _v$6 !== _p$.t && (_p$.t = _$setProp(_el$31, "style", _v$6, _p$.t));
307
+ _v$5 !== _p$.e && (_p$.e = _$setProp(_el$33, "style", _v$5, _p$.e));
308
+ _v$6 !== _p$.t && (_p$.t = _$setProp(_el$34, "style", _v$6, _p$.t));
298
309
  return _p$;
299
310
  }, {
300
311
  e: void 0,
301
312
  t: void 0
302
313
  });
303
- return _el$27;
314
+ return _el$30;
304
315
  })());
305
316
  nodes.push((() => {
306
- var _el$34 = _$createElement("box"), _el$35 = _$createElement("text"), _el$37 = _$createElement("text"), _el$38 = _$createElement("text"), _el$39 = _$createElement("text");
307
- _$insertNode(_el$34, _el$35);
308
- _$insertNode(_el$34, _el$37);
309
- _$insertNode(_el$34, _el$38);
310
- _$insertNode(_el$34, _el$39);
311
- _$setProp(_el$34, "flexDirection", "row");
312
- _$insertNode(_el$35, _$createTextNode(` 1w `));
313
- _$insert(_el$37, () => barFill(s.weekly));
314
- _$insert(_el$38, () => barEmpty(s.weekly));
315
- _$insertNode(_el$39, _$createTextNode(` 0%`));
317
+ var _el$37 = _$createElement("box"), _el$38 = _$createElement("text"), _el$40 = _$createElement("text"), _el$41 = _$createElement("text"), _el$42 = _$createElement("text");
318
+ _$insertNode(_el$37, _el$38);
319
+ _$insertNode(_el$37, _el$40);
320
+ _$insertNode(_el$37, _el$41);
321
+ _$insertNode(_el$37, _el$42);
322
+ _$setProp(_el$37, "flexDirection", "row");
323
+ _$insertNode(_el$38, _$createTextNode(` 1w `));
324
+ _$insert(_el$40, () => barFill(s.weekly));
325
+ _$insert(_el$41, () => barEmpty(s.weekly));
326
+ _$insertNode(_el$42, _$createTextNode(` 0%`));
316
327
  _$effect((_p$) => {
317
328
  var _v$7 = st("text"), _v$8 = st("text");
318
- _v$7 !== _p$.e && (_p$.e = _$setProp(_el$37, "style", _v$7, _p$.e));
319
- _v$8 !== _p$.t && (_p$.t = _$setProp(_el$38, "style", _v$8, _p$.t));
329
+ _v$7 !== _p$.e && (_p$.e = _$setProp(_el$40, "style", _v$7, _p$.e));
330
+ _v$8 !== _p$.t && (_p$.t = _$setProp(_el$41, "style", _v$8, _p$.t));
320
331
  return _p$;
321
332
  }, {
322
333
  e: void 0,
323
334
  t: void 0
324
335
  });
325
- return _el$34;
336
+ return _el$37;
326
337
  })());
327
338
  }
328
339
  } else {
329
340
  nodes.push((() => {
330
- var _el$41 = _$createElement("text");
331
- _$insertNode(_el$41, _$createTextNode(`usage-coach: ...`));
332
- return _el$41;
341
+ var _el$44 = _$createElement("text");
342
+ _$insertNode(_el$44, _$createTextNode(`usage-coach: ...`));
343
+ return _el$44;
333
344
  })());
334
345
  }
335
346
  if (h && h.active !== false && h.tasks.length > 0) {
336
347
  nodes.push((() => {
337
- var _el$43 = _$createElement("text");
338
- _$insertNode(_el$43, _$createTextNode(` `));
339
- return _el$43;
348
+ var _el$46 = _$createElement("text");
349
+ _$insertNode(_el$46, _$createTextNode(` `));
350
+ return _el$46;
340
351
  })());
341
352
  nodes.push((() => {
342
- var _el$45 = _$createElement("text"), _el$46 = _$createTextNode(`harness: `), _el$47 = _$createTextNode(` `), _el$48 = _$createTextNode(`/`);
343
- _$insertNode(_el$45, _el$46);
344
- _$insertNode(_el$45, _el$47);
345
- _$insertNode(_el$45, _el$48);
346
- _$insert(_el$45, () => h.name, _el$47);
347
- _$insert(_el$45, () => h.current, _el$48);
348
- _$insert(_el$45, () => h.total, null);
349
- _$effect((_$p) => _$setProp(_el$45, "style", st("textMuted"), _$p));
350
- return _el$45;
353
+ var _el$48 = _$createElement("text"), _el$49 = _$createTextNode(`harness: `), _el$50 = _$createTextNode(` `), _el$51 = _$createTextNode(`/`);
354
+ _$insertNode(_el$48, _el$49);
355
+ _$insertNode(_el$48, _el$50);
356
+ _$insertNode(_el$48, _el$51);
357
+ _$insert(_el$48, () => h.name, _el$50);
358
+ _$insert(_el$48, () => h.current, _el$51);
359
+ _$insert(_el$48, () => h.total, null);
360
+ _$effect((_$p) => _$setProp(_el$48, "style", st("textMuted"), _$p));
361
+ return _el$48;
351
362
  })());
352
363
  for (const t of h.tasks) {
353
364
  const sKey = statusKey[t.status] ?? "text";
@@ -357,54 +368,70 @@ function initializeTui(api, disposeRoot) {
357
368
  const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
358
369
  const elapsedStr = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
359
370
  nodes.push((() => {
360
- var _el$49 = _$createElement("text"), _el$50 = _$createTextNode(` \u25CF `), _el$51 = _$createTextNode(` `), _el$52 = _$createTextNode(` `);
361
- _$insertNode(_el$49, _el$50);
362
- _$insertNode(_el$49, _el$51);
363
- _$insertNode(_el$49, _el$52);
364
- _$insert(_el$49, () => t.id, _el$51);
365
- _$insert(_el$49, mdl, _el$51);
366
- _$insert(_el$49, lbl, _el$52);
367
- _$insert(_el$49, rev, _el$52);
368
- _$insert(_el$49, elapsedStr, _el$52);
369
- _$insert(_el$49, () => t.title, null);
370
- _$effect((_$p) => _$setProp(_el$49, "style", st(sKey), _$p));
371
- return _el$49;
372
- })());
373
- const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
374
- const provCoach = pv ? s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id)) : s?.providers?.[0];
375
- const rawPct = provCoach?.fiveHour ?? s?.fiveHour ?? -1;
376
- const pct = rawPct < 0 ? 0 : rawPct;
377
- const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
378
- nodes.push((() => {
379
- var _el$53 = _$createElement("box"), _el$54 = _$createElement("text"), _el$56 = _$createElement("text"), _el$57 = _$createElement("text"), _el$58 = _$createElement("text"), _el$59 = _$createTextNode(` `);
380
- _$insertNode(_el$53, _el$54);
381
- _$insertNode(_el$53, _el$56);
382
- _$insertNode(_el$53, _el$57);
383
- _$insertNode(_el$53, _el$58);
384
- _$setProp(_el$53, "flexDirection", "row");
385
- _$insertNode(_el$54, _$createTextNode(` 5h `));
386
- _$insert(_el$56, () => barFill(pct));
387
- _$insert(_el$57, () => barEmpty(pct));
388
- _$insertNode(_el$58, _el$59);
389
- _$insert(_el$58, pctLabel, null);
390
- _$effect((_p$) => {
391
- var _v$9 = st("text"), _v$0 = st("text");
392
- _v$9 !== _p$.e && (_p$.e = _$setProp(_el$56, "style", _v$9, _p$.e));
393
- _v$0 !== _p$.t && (_p$.t = _$setProp(_el$57, "style", _v$0, _p$.t));
394
- return _p$;
395
- }, {
396
- e: void 0,
397
- t: void 0
398
- });
399
- return _el$53;
371
+ var _el$52 = _$createElement("text"), _el$53 = _$createTextNode(` \u25CF `), _el$54 = _$createTextNode(` `), _el$55 = _$createTextNode(` `);
372
+ _$insertNode(_el$52, _el$53);
373
+ _$insertNode(_el$52, _el$54);
374
+ _$insertNode(_el$52, _el$55);
375
+ _$insert(_el$52, () => t.id, _el$54);
376
+ _$insert(_el$52, mdl, _el$54);
377
+ _$insert(_el$52, lbl, _el$55);
378
+ _$insert(_el$52, rev, _el$55);
379
+ _$insert(_el$52, elapsedStr, _el$55);
380
+ _$insert(_el$52, () => t.title, null);
381
+ _$effect((_$p) => _$setProp(_el$52, "style", st(sKey), _$p));
382
+ return _el$52;
400
383
  })());
384
+ if (t.model) {
385
+ const prefix = t.model.split("/")[0] ?? "";
386
+ if (prefix === "opencode") {
387
+ nodes.push((() => {
388
+ var _el$56 = _$createElement("box"), _el$57 = _$createElement("text"), _el$59 = _$createElement("text");
389
+ _$insertNode(_el$56, _el$57);
390
+ _$insertNode(_el$56, _el$59);
391
+ _$setProp(_el$56, "flexDirection", "row");
392
+ _$insertNode(_el$57, _$createTextNode(` `));
393
+ _$insertNode(_el$59, _$createTextNode(`free`));
394
+ _$effect((_$p) => _$setProp(_el$59, "style", st("success"), _$p));
395
+ return _el$56;
396
+ })());
397
+ } else {
398
+ const provCoach = s?.providers?.find((p) => p.id === prefix || p.id.startsWith(prefix) || prefix.startsWith(p.id));
399
+ const rawPct = provCoach?.fiveHour ?? -1;
400
+ if (rawPct >= 0) {
401
+ nodes.push((() => {
402
+ var _el$61 = _$createElement("box"), _el$62 = _$createElement("text"), _el$64 = _$createElement("text"), _el$65 = _$createElement("text"), _el$66 = _$createElement("text"), _el$67 = _$createTextNode(` `), _el$68 = _$createTextNode(`%`);
403
+ _$insertNode(_el$61, _el$62);
404
+ _$insertNode(_el$61, _el$64);
405
+ _$insertNode(_el$61, _el$65);
406
+ _$insertNode(_el$61, _el$66);
407
+ _$setProp(_el$61, "flexDirection", "row");
408
+ _$insertNode(_el$62, _$createTextNode(` 5h `));
409
+ _$insert(_el$64, () => barFill(rawPct));
410
+ _$insert(_el$65, () => barEmpty(rawPct));
411
+ _$insertNode(_el$66, _el$67);
412
+ _$insertNode(_el$66, _el$68);
413
+ _$insert(_el$66, rawPct, _el$68);
414
+ _$effect((_p$) => {
415
+ var _v$9 = st("text"), _v$0 = st("text");
416
+ _v$9 !== _p$.e && (_p$.e = _$setProp(_el$64, "style", _v$9, _p$.e));
417
+ _v$0 !== _p$.t && (_p$.t = _$setProp(_el$65, "style", _v$0, _p$.t));
418
+ return _p$;
419
+ }, {
420
+ e: void 0,
421
+ t: void 0
422
+ });
423
+ return _el$61;
424
+ })());
425
+ }
426
+ }
427
+ }
401
428
  }
402
429
  }
403
430
  return (() => {
404
- var _el$60 = _$createElement("box");
405
- _$setProp(_el$60, "flexDirection", "column");
406
- _$insert(_el$60, nodes);
407
- return _el$60;
431
+ var _el$69 = _$createElement("box");
432
+ _$setProp(_el$69, "flexDirection", "column");
433
+ _$insert(_el$69, nodes);
434
+ return _el$69;
408
435
  })();
409
436
  };
410
437
  tlog("registering slots");
@@ -419,9 +446,9 @@ function initializeTui(api, disposeRoot) {
419
446
  } catch (e) {
420
447
  tlog(`sidebar_footer err: ${String(e)}`);
421
448
  result = (() => {
422
- var _el$61 = _$createElement("text");
423
- _$insertNode(_el$61, _$createTextNode(`usage-coach`));
424
- return _el$61;
449
+ var _el$70 = _$createElement("text");
450
+ _$insertNode(_el$70, _$createTextNode(`usage-coach`));
451
+ return _el$70;
425
452
  })();
426
453
  }
427
454
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.5.0",
3
+ "version": "0.6.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",
@@ -58,5 +58,11 @@
58
58
  "tsup": "^8.5",
59
59
  "typescript": "^5",
60
60
  "typescript-eslint": "^8.63.0"
61
- }
62
- }
61
+ },
62
+ "dependencies": {
63
+ "@ladybugdb/core": "^0.18.0"
64
+ },
65
+ "trustedDependencies": [
66
+ "@ladybugdb/core"
67
+ ]
68
+ }