opencode-usage-coach 0.6.1 → 0.7.0

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.
Files changed (4) hide show
  1. package/README.md +0 -1
  2. package/dist/index.js +155 -185
  3. package/dist/tui.js +198 -188
  4. package/package.json +3 -9
package/README.md CHANGED
@@ -155,7 +155,6 @@ 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 |
159
158
 
160
159
  ## Agent-mode scoping
161
160
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { mkdirSync, writeFileSync, appendFileSync, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
2
+ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, appendFileSync as appendFileSync2, 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,193 +7,123 @@ 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 { Database, Connection } from "@ladybugdb/core";
10
+ import { mkdirSync, appendFileSync, readFileSync, existsSync, writeFileSync } from "fs";
11
11
  import { join } from "path";
12
- import { existsSync, readFileSync } from "fs";
13
- var QUERY_TIMEOUT_MS = (() => {
14
- try {
15
- const v = Number(process.env.UC_DOMAIN_TIMEOUT_MS);
16
- return Number.isFinite(v) && v > 0 ? v : 5e3;
17
- } catch {
18
- return 5e3;
19
- }
20
- })();
21
12
  var BASE_DIR = "";
22
- var DB_PATH = "";
23
- var db = null;
24
- var conn = null;
25
- var schemaReady = false;
26
- var migrated = false;
27
13
  function initDomain(stateDir) {
28
14
  BASE_DIR = stateDir;
29
- DB_PATH = join(BASE_DIR, "domain.ladybug");
30
- db = null;
31
- conn = null;
32
- schemaReady = false;
33
- migrated = false;
34
15
  }
35
- function uid(prefix) {
36
- return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
16
+ var nodesFile = () => join(BASE_DIR, "nodes.ndjson");
17
+ var edgesFile = () => join(BASE_DIR, "edges.ndjson");
18
+ function readNdjson(path) {
19
+ try {
20
+ if (!existsSync(path)) return [];
21
+ return readFileSync(path, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
22
+ } catch {
23
+ return [];
24
+ }
37
25
  }
38
- function esc(s) {
39
- return String(s ?? "").replace(/\\/g, "\\\\").replace(/'/g, "\\'");
26
+ function readNodes() {
27
+ return readNdjson(nodesFile());
40
28
  }
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
- });
29
+ function readEdges() {
30
+ return readNdjson(edgesFile());
31
+ }
32
+ function uid(prefix) {
33
+ return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
49
34
  }
50
- function parseNode(r) {
51
- let props = {};
35
+ function addDomainNode(node) {
36
+ const full = { ...node, id: uid("node"), ts: (/* @__PURE__ */ new Date()).toISOString() };
52
37
  try {
53
- if (r.props) props = JSON.parse(r.props);
38
+ mkdirSync(BASE_DIR, { recursive: true });
39
+ appendFileSync(nodesFile(), JSON.stringify(full) + "\n");
54
40
  } catch {
55
41
  }
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;
42
+ return full.id;
90
43
  }
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;
44
+ function writeNodes(nodes) {
96
45
  try {
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
- }
46
+ mkdirSync(BASE_DIR, { recursive: true });
47
+ const lines = nodes.map((n) => JSON.stringify(n));
48
+ writeFileSync(nodesFile(), lines.length ? lines.join("\n") + "\n" : "");
115
49
  } catch {
116
50
  }
117
51
  }
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
- }
130
52
  function queryDomain(keywords) {
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
- }
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));
147
62
  return { nodes: matched, edges };
148
63
  }
149
- async function _touchNodes(ids) {
64
+ function touchNodes(ids) {
150
65
  if (ids.size === 0) return;
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);
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 {
173
79
  }
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`);
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 };
178
98
  }
179
- return { removed: toRemove.length, kept: kept.length };
180
99
  }
181
100
  function saveInvestigationResult(keywords, result, source) {
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
- });
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
+ }
192
112
  }
193
113
 
194
114
  // src/index.ts
195
115
  var PLUGIN_NAME = "opencode-usage-coach";
196
116
  var TTL_MS = Number(process.env.UC_TTL_MS ?? 6e4);
117
+ var PIPE_LOG = join2(homedir(), ".cache", "opencode-usage-coach", "pipeline.log");
118
+ function pipeLog(msg) {
119
+ try {
120
+ mkdirSync2(dirname(PIPE_LOG), { recursive: true });
121
+ appendFileSync2(PIPE_LOG, `[SERVER] ${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
122
+ `);
123
+ } catch {
124
+ }
125
+ }
126
+ pipeLog(`MODULE LOADED | node=${process.version} | pid=${process.pid}`);
197
127
  var STATE_DIR = join2(homedir(), ".cache", "opencode-usage-coach");
198
128
  var STATE_FILE = join2(STATE_DIR, "state.json");
199
129
  var LOG_FILE = join2(STATE_DIR, "coach.log");
@@ -210,15 +140,15 @@ function setStateDir(dir) {
210
140
  var NOOP_HOOKS = {};
211
141
  function log(msg) {
212
142
  try {
213
- appendFileSync(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
143
+ appendFileSync2(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
214
144
  `);
215
145
  } catch {
216
146
  }
217
147
  }
218
148
  function writeState(c) {
219
149
  try {
220
- mkdirSync(STATE_DIR, { recursive: true });
221
- writeFileSync(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
150
+ mkdirSync2(STATE_DIR, { recursive: true });
151
+ writeFileSync2(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
222
152
  } catch {
223
153
  }
224
154
  }
@@ -269,9 +199,9 @@ function readHarness(sessionID) {
269
199
  function writeHarness(sessionID, h) {
270
200
  try {
271
201
  const f = harnessFile(sessionID);
272
- mkdirSync(dirname(f), { recursive: true });
202
+ mkdirSync2(dirname(f), { recursive: true });
273
203
  h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
274
- writeFileSync(f, JSON.stringify(h, null, 2));
204
+ writeFileSync2(f, JSON.stringify(h, null, 2));
275
205
  } catch {
276
206
  }
277
207
  }
@@ -451,16 +381,44 @@ function coach(q, lighter) {
451
381
  return { decision: "GO", advice: `Comfortable \u2014 weekly ${wk}% \xB7 5h ${h5}% \xB7 monthly ${mo}%. proceed. 5h window ${h5R}.`, weekly: wk, monthly: mo, fiveHour: h5 };
452
382
  }
453
383
  var agentCache = /* @__PURE__ */ new Map();
454
- var lastResolvedAgent = "";
384
+ var currentModel = "";
385
+ var currentProvider = "";
386
+ var currentAgent = "";
387
+ var modelChanged = false;
388
+ function isFreeModel(model, provider) {
389
+ if (!model && !provider) return false;
390
+ if (provider === "opencode") return true;
391
+ if (model.toLowerCase().includes("free")) return true;
392
+ return false;
393
+ }
394
+ function providerToCodexbar(provider) {
395
+ if (!provider) return "";
396
+ return provider.split("-")[0];
397
+ }
455
398
  async function resolveAgent(client, sessionID) {
456
399
  if (!sessionID) return "";
457
400
  const hit = agentCache.get(sessionID);
458
- if (hit && Date.now() - hit.ts < 6e4) return hit.agent;
401
+ if (hit && Date.now() - hit.ts < 6e4) {
402
+ currentModel = hit.model;
403
+ currentProvider = hit.provider;
404
+ return hit.agent;
405
+ }
459
406
  try {
460
407
  const s = await client.session.get({ path: { id: sessionID } });
461
- const agent = String(s?.data?.info?.agent ?? s?.data?.agent ?? s?.info?.agent ?? "");
462
- agentCache.set(sessionID, { agent, ts: Date.now() });
463
- lastResolvedAgent = agent;
408
+ log(`resolveAgent raw session: ${JSON.stringify(s?.data?.info ?? s?.data ?? s?.info ?? s).slice(0, 500)}`);
409
+ const info = s?.data?.info ?? s?.data ?? s?.info ?? s;
410
+ const agent = String(info?.agent ?? "");
411
+ const rawModel = info?.model;
412
+ const model = typeof rawModel === "string" ? rawModel : rawModel?.id ?? rawModel?.modelID ?? rawModel?.name ?? "";
413
+ const rawProvider = info?.providerID ?? info?.provider ?? (typeof rawModel === "object" ? rawModel?.providerID ?? rawModel?.provider : "");
414
+ const provider = typeof rawProvider === "string" ? rawProvider : rawProvider?.id ?? "";
415
+ if (currentModel && model && currentModel !== model) {
416
+ log(`MODEL CHANGED: ${currentModel} \u2192 ${model} (provider: ${currentProvider} \u2192 ${provider})`);
417
+ modelChanged = true;
418
+ }
419
+ agentCache.set(sessionID, { agent, model, provider, ts: Date.now() });
420
+ currentModel = model;
421
+ currentProvider = provider;
464
422
  return agent;
465
423
  } catch (e) {
466
424
  log(`resolveAgent err: ${String(e)}`);
@@ -485,9 +443,19 @@ async function UsageCoachPlugin(input) {
485
443
  const refreshBackground = () => {
486
444
  try {
487
445
  if (refreshing) return;
488
- if (last && Date.now() - lastFetchedAt < TTL_MS) return;
446
+ if (last && !modelChanged && Date.now() - lastFetchedAt < TTL_MS) return;
489
447
  refreshing = true;
490
- fetchQuota(PROVIDER).then(async (q) => {
448
+ modelChanged = false;
449
+ if (isFreeModel(currentModel, currentProvider)) {
450
+ last = { decision: "GO", advice: `${currentModel || currentProvider || "free model"} \u2014 no quota limit.`, weekly: -1, monthly: -1, fiveHour: -1, model: currentModel, provider: currentProvider, isFree: true };
451
+ lastFetchedAt = Date.now();
452
+ writeState({ ...last, providers: [], model: currentModel, provider: currentProvider, isFree: true, agent: currentAgent, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
453
+ log(`FREE | model=${currentModel} provider=${currentProvider}`);
454
+ refreshing = false;
455
+ return;
456
+ }
457
+ const activeProvider = providerToCodexbar(currentProvider) || PROVIDER;
458
+ fetchQuota(activeProvider).then(async (q) => {
491
459
  try {
492
460
  last = coach(q, LIGHTER);
493
461
  lastFetchedAt = Date.now();
@@ -500,7 +468,7 @@ async function UsageCoachPlugin(input) {
500
468
  const p0 = providers[0];
501
469
  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" };
502
470
  }
503
- writeState({ ...last, agent: lastResolvedAgent, providers, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
471
+ writeState({ ...last, providers, model: currentModel, provider: currentProvider, isFree: false, agent: currentAgent, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
504
472
  log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
505
473
  } catch (e) {
506
474
  log(`refresh-in-then err: ${String(e)}`);
@@ -528,7 +496,7 @@ async function UsageCoachPlugin(input) {
528
496
  if (event.type === "session.created" || event.type === "session.idle") refreshBackground();
529
497
  if (event.type === "session.idle") {
530
498
  try {
531
- const r = await evictStale(WORM_MAX_AGE_DAYS, WORM_MAX_NODES);
499
+ const r = evictStale(WORM_MAX_AGE_DAYS, WORM_MAX_NODES);
532
500
  if (r.removed) log(`evictStale: removed ${r.removed}, kept ${r.kept} (maxAge=${WORM_MAX_AGE_DAYS}d, maxNodes=${WORM_MAX_NODES})`);
533
501
  } catch (e) {
534
502
  log(`evictStale err: ${String(e)}`);
@@ -538,13 +506,14 @@ async function UsageCoachPlugin(input) {
538
506
  log(`event err: ${String(e)}`);
539
507
  }
540
508
  },
541
- // ACT(1) hard gate harness tools are restricted to the configured harness
542
- // agent mode AND gated by quota STOP. General tools (read/edit/bash/grep/task)
543
- // are NEVER gated, in ANY mode — they don't consume model quota.
509
+ // ACT(1) model/agent detection on EVERY tool call (cached 60s, negligible overhead).
510
+ // Then hard-gate harness tools by agent mode + quota STOP.
544
511
  "tool.execute.before": async (_input) => {
512
+ const agent = await resolveAgent(input.client, _input.sessionID);
513
+ currentAgent = agent;
514
+ refreshBackground();
545
515
  const harnessTools = ["generate", "generate_batch", "grade", "investigate", "verify_diagnosis", "generalize", "harness_start", "task_update", "harness_done", "record_failure"];
546
516
  if (!harnessTools.includes(_input.tool)) return;
547
- const agent = await resolveAgent(input.client, _input.sessionID);
548
517
  if (!isHarnessAgent(agent)) {
549
518
  throw new Error(`[${PLUGIN_NAME}] '${_input.tool}' is restricted to agent mode ${JSON.stringify(HARNESS_AGENTS)} (current: ${JSON.stringify(agent || "unknown")}). Switch to that agent mode to use it.`);
550
519
  }
@@ -564,6 +533,7 @@ async function UsageCoachPlugin(input) {
564
533
  try {
565
534
  if (_input.sessionID) {
566
535
  const agent = await resolveAgent(input.client, _input.sessionID);
536
+ refreshBackground();
567
537
  if (!isHarnessAgent(agent)) return;
568
538
  }
569
539
  const c = current();
@@ -653,8 +623,8 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
653
623
  async execute(args, _ctx) {
654
624
  const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), task: args.task, prompt: args.prompt, gradeResult: args.gradeResult, model: args.model, revisions: args.revisions };
655
625
  try {
656
- mkdirSync(STATE_DIR, { recursive: true });
657
- appendFileSync(failuresFile(), JSON.stringify(rec) + "\n");
626
+ mkdirSync2(STATE_DIR, { recursive: true });
627
+ appendFileSync2(failuresFile(), JSON.stringify(rec) + "\n");
658
628
  } catch (e) {
659
629
  log(`record_failure err: ${String(e)}`);
660
630
  }
@@ -678,7 +648,7 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
678
648
  try {
679
649
  keywords = extractKeywords(`${args.task} ${args.gradeResult}`);
680
650
  if (keywords.length) {
681
- const { nodes, edges } = await queryDomain(keywords);
651
+ const { nodes, edges } = queryDomain(keywords);
682
652
  if (nodes && nodes.length || edges && edges.length) {
683
653
  domainEmpty = false;
684
654
  domainPrefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
@@ -702,7 +672,7 @@ evidence: <file/line or specific quote>`;
702
672
  const out = await runModel(input.client, cfg.generator, domainPrefix + rcaPrompt, ctx.directory);
703
673
  if (domainEmpty && keywords.length) {
704
674
  try {
705
- await saveInvestigationResult(keywords, out, "investigate");
675
+ saveInvestigationResult(keywords, out, "investigate");
706
676
  } catch (e) {
707
677
  log(`investigate save err: ${String(e)}`);
708
678
  }
@@ -758,8 +728,8 @@ Keep it concrete and actionable.`;
758
728
  const rule = out;
759
729
  try {
760
730
  const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
761
- mkdirSync(STATE_DIR, { recursive: true });
762
- appendFileSync(rulesFile(), `## Rule (${date})
731
+ mkdirSync2(STATE_DIR, { recursive: true });
732
+ appendFileSync2(rulesFile(), `## Rule (${date})
763
733
  ${rule}
764
734
  Origin: ${args.task}
765
735
 
@@ -798,7 +768,7 @@ ${rules}
798
768
  try {
799
769
  keywords = extractKeywords(args.prompt);
800
770
  if (keywords.length) {
801
- const { nodes, edges } = await queryDomain(keywords);
771
+ const { nodes, edges } = queryDomain(keywords);
802
772
  if (nodes && nodes.length || edges && edges.length) {
803
773
  domainEmpty = false;
804
774
  prefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
@@ -814,7 +784,7 @@ ${rules}
814
784
  const out = await runModel(input.client, model, prefix + args.prompt, ctx.directory);
815
785
  if (domainEmpty && keywords.length) {
816
786
  try {
817
- await saveInvestigationResult(keywords, out, "generate");
787
+ saveInvestigationResult(keywords, out, "generate");
818
788
  } catch (e) {
819
789
  log(`generate save err: ${String(e)}`);
820
790
  }
package/dist/tui.js CHANGED
@@ -19,7 +19,6 @@ 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);
23
22
  function readState() {
24
23
  try {
25
24
  if (!existsSync(STATE_FILE)) return null;
@@ -189,16 +188,6 @@ function initializeTui(api, disposeRoot) {
189
188
  } catch {
190
189
  s = null;
191
190
  }
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
- }
202
191
  let h = null;
203
192
  try {
204
193
  const routeSid = api.route?.current?.params?.sessionID ?? "";
@@ -213,152 +202,189 @@ function initializeTui(api, disposeRoot) {
213
202
  h = null;
214
203
  }
215
204
  const nodes = [];
205
+ const HARNESS_AGENTS = ["usage-coach-harness"];
206
+ if (s && s.agent && !HARNESS_AGENTS.includes(s.agent)) {
207
+ return _$createElement("box");
208
+ }
216
209
  if (s) {
217
210
  const dKey = s.decision === "GO" ? "success" : s.decision === "THROTTLE" ? "warning" : "error";
218
- nodes.push((() => {
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;
225
- })());
226
- if (s.providers && s.providers.length > 0) {
227
- for (const p of s.providers) {
211
+ const modelShort = s.model ? s.model.split("/").pop() ?? s.model : "";
212
+ if (s.isFree) {
213
+ nodes.push((() => {
214
+ var _el$5 = _$createElement("box"), _el$6 = _$createElement("text"), _el$8 = _$createElement("text"), _el$9 = _$createTextNode(` `);
215
+ _$insertNode(_el$5, _el$6);
216
+ _$insertNode(_el$5, _el$8);
217
+ _$setProp(_el$5, "flexDirection", "row");
218
+ _$insertNode(_el$6, _$createTextNode(`usage-coach [free]`));
219
+ _$insertNode(_el$8, _el$9);
220
+ _$insert(_el$8, modelShort, null);
221
+ _$effect((_p$) => {
222
+ var _v$ = st(dKey), _v$2 = st("textMuted");
223
+ _v$ !== _p$.e && (_p$.e = _$setProp(_el$6, "style", _v$, _p$.e));
224
+ _v$2 !== _p$.t && (_p$.t = _$setProp(_el$8, "style", _v$2, _p$.t));
225
+ return _p$;
226
+ }, {
227
+ e: void 0,
228
+ t: void 0
229
+ });
230
+ return _el$5;
231
+ })());
232
+ } else {
233
+ if (modelShort) {
228
234
  nodes.push((() => {
229
- var _el$0 = _$createElement("text"), _el$1 = _$createTextNode(` `);
235
+ var _el$0 = _$createElement("box"), _el$1 = _$createElement("text"), _el$10 = _$createTextNode(`usage-coach [`), _el$11 = _$createTextNode(`]`), _el$12 = _$createElement("text"), _el$13 = _$createTextNode(` `);
230
236
  _$insertNode(_el$0, _el$1);
231
- _$insert(_el$0, () => p.name, null);
232
- _$effect((_$p) => _$setProp(_el$0, "style", st("textMuted"), _$p));
237
+ _$insertNode(_el$0, _el$12);
238
+ _$setProp(_el$0, "flexDirection", "row");
239
+ _$insertNode(_el$1, _el$10);
240
+ _$insertNode(_el$1, _el$11);
241
+ _$insert(_el$1, () => TAG[s.decision], _el$11);
242
+ _$insertNode(_el$12, _el$13);
243
+ _$insert(_el$12, modelShort, null);
244
+ _$effect((_p$) => {
245
+ var _v$3 = st(dKey), _v$4 = st("textMuted");
246
+ _v$3 !== _p$.e && (_p$.e = _$setProp(_el$1, "style", _v$3, _p$.e));
247
+ _v$4 !== _p$.t && (_p$.t = _$setProp(_el$12, "style", _v$4, _p$.t));
248
+ return _p$;
249
+ }, {
250
+ e: void 0,
251
+ t: void 0
252
+ });
233
253
  return _el$0;
234
254
  })());
255
+ } else {
235
256
  nodes.push((() => {
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);
257
+ var _el$14 = _$createElement("text"), _el$15 = _$createTextNode(`usage-coach [`), _el$16 = _$createTextNode(`]`);
258
+ _$insertNode(_el$14, _el$15);
259
+ _$insertNode(_el$14, _el$16);
260
+ _$insert(_el$14, () => TAG[s.decision], _el$16);
261
+ _$effect((_$p) => _$setProp(_el$14, "style", st(dKey), _$p));
262
+ return _el$14;
263
+ })());
264
+ }
265
+ if (s.providers && s.providers.length > 0) {
266
+ for (const p of s.providers) {
267
+ nodes.push((() => {
268
+ var _el$17 = _$createElement("box"), _el$18 = _$createElement("text"), _el$20 = _$createElement("text"), _el$21 = _$createElement("text"), _el$22 = _$createElement("text"), _el$23 = _$createTextNode(` `), _el$24 = _$createTextNode(`% `);
269
+ _$insertNode(_el$17, _el$18);
270
+ _$insertNode(_el$17, _el$20);
271
+ _$insertNode(_el$17, _el$21);
272
+ _$insertNode(_el$17, _el$22);
273
+ _$setProp(_el$17, "flexDirection", "row");
274
+ _$insertNode(_el$18, _$createTextNode(` 5h `));
275
+ _$insert(_el$20, () => barFill(p.fiveHour));
276
+ _$insert(_el$21, () => barEmpty(p.fiveHour));
277
+ _$insertNode(_el$22, _el$23);
278
+ _$insertNode(_el$22, _el$24);
279
+ _$insert(_el$22, () => p.fiveHour, _el$24);
280
+ _$insert(_el$22, () => p.fiveHourReset, null);
281
+ _$effect((_p$) => {
282
+ var _v$5 = st("text"), _v$6 = st("text");
283
+ _v$5 !== _p$.e && (_p$.e = _$setProp(_el$20, "style", _v$5, _p$.e));
284
+ _v$6 !== _p$.t && (_p$.t = _$setProp(_el$21, "style", _v$6, _p$.t));
285
+ return _p$;
286
+ }, {
287
+ e: void 0,
288
+ t: void 0
289
+ });
290
+ return _el$17;
291
+ })());
292
+ nodes.push((() => {
293
+ var _el$25 = _$createElement("box"), _el$26 = _$createElement("text"), _el$28 = _$createElement("text"), _el$29 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createTextNode(` `), _el$32 = _$createTextNode(`% `);
294
+ _$insertNode(_el$25, _el$26);
295
+ _$insertNode(_el$25, _el$28);
296
+ _$insertNode(_el$25, _el$29);
297
+ _$insertNode(_el$25, _el$30);
298
+ _$setProp(_el$25, "flexDirection", "row");
299
+ _$insertNode(_el$26, _$createTextNode(` 1w `));
300
+ _$insert(_el$28, () => barFill(p.weekly));
301
+ _$insert(_el$29, () => barEmpty(p.weekly));
302
+ _$insertNode(_el$30, _el$31);
303
+ _$insertNode(_el$30, _el$32);
304
+ _$insert(_el$30, () => p.weekly, _el$32);
305
+ _$insert(_el$30, () => p.weeklyReset, null);
306
+ _$effect((_p$) => {
307
+ var _v$7 = st("text"), _v$8 = st("text");
308
+ _v$7 !== _p$.e && (_p$.e = _$setProp(_el$28, "style", _v$7, _p$.e));
309
+ _v$8 !== _p$.t && (_p$.t = _$setProp(_el$29, "style", _v$8, _p$.t));
310
+ return _p$;
311
+ }, {
312
+ e: void 0,
313
+ t: void 0
314
+ });
315
+ return _el$25;
316
+ })());
317
+ }
318
+ } else {
319
+ nodes.push((() => {
320
+ var _el$33 = _$createElement("box"), _el$34 = _$createElement("text"), _el$36 = _$createElement("text"), _el$37 = _$createElement("text"), _el$38 = _$createElement("text");
321
+ _$insertNode(_el$33, _el$34);
322
+ _$insertNode(_el$33, _el$36);
323
+ _$insertNode(_el$33, _el$37);
324
+ _$insertNode(_el$33, _el$38);
325
+ _$setProp(_el$33, "flexDirection", "row");
326
+ _$insertNode(_el$34, _$createTextNode(` 5h `));
327
+ _$insert(_el$36, () => barFill(s.fiveHour));
328
+ _$insert(_el$37, () => barEmpty(s.fiveHour));
329
+ _$insertNode(_el$38, _$createTextNode(` 0%`));
249
330
  _$effect((_p$) => {
250
- var _v$ = st("text"), _v$2 = st("text");
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));
331
+ var _v$9 = st("text"), _v$0 = st("text");
332
+ _v$9 !== _p$.e && (_p$.e = _$setProp(_el$36, "style", _v$9, _p$.e));
333
+ _v$0 !== _p$.t && (_p$.t = _$setProp(_el$37, "style", _v$0, _p$.t));
253
334
  return _p$;
254
335
  }, {
255
336
  e: void 0,
256
337
  t: void 0
257
338
  });
258
- return _el$10;
339
+ return _el$33;
259
340
  })());
260
341
  nodes.push((() => {
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);
342
+ var _el$40 = _$createElement("box"), _el$41 = _$createElement("text"), _el$43 = _$createElement("text"), _el$44 = _$createElement("text"), _el$45 = _$createElement("text");
343
+ _$insertNode(_el$40, _el$41);
344
+ _$insertNode(_el$40, _el$43);
345
+ _$insertNode(_el$40, _el$44);
346
+ _$insertNode(_el$40, _el$45);
347
+ _$setProp(_el$40, "flexDirection", "row");
348
+ _$insertNode(_el$41, _$createTextNode(` 1w `));
349
+ _$insert(_el$43, () => barFill(s.weekly));
350
+ _$insert(_el$44, () => barEmpty(s.weekly));
351
+ _$insertNode(_el$45, _$createTextNode(` 0%`));
274
352
  _$effect((_p$) => {
275
- var _v$3 = st("text"), _v$4 = st("text");
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));
353
+ var _v$1 = st("text"), _v$10 = st("text");
354
+ _v$1 !== _p$.e && (_p$.e = _$setProp(_el$43, "style", _v$1, _p$.e));
355
+ _v$10 !== _p$.t && (_p$.t = _$setProp(_el$44, "style", _v$10, _p$.t));
278
356
  return _p$;
279
357
  }, {
280
358
  e: void 0,
281
359
  t: void 0
282
360
  });
283
- return _el$18;
284
- })());
285
- nodes.push((() => {
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;
361
+ return _el$40;
291
362
  })());
292
363
  }
293
- } else {
294
- nodes.push((() => {
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%`));
305
- _$effect((_p$) => {
306
- var _v$5 = st("text"), _v$6 = st("text");
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));
309
- return _p$;
310
- }, {
311
- e: void 0,
312
- t: void 0
313
- });
314
- return _el$30;
315
- })());
316
- nodes.push((() => {
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%`));
327
- _$effect((_p$) => {
328
- var _v$7 = st("text"), _v$8 = st("text");
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));
331
- return _p$;
332
- }, {
333
- e: void 0,
334
- t: void 0
335
- });
336
- return _el$37;
337
- })());
338
364
  }
339
365
  } else {
340
366
  nodes.push((() => {
341
- var _el$44 = _$createElement("text");
342
- _$insertNode(_el$44, _$createTextNode(`usage-coach: ...`));
343
- return _el$44;
367
+ var _el$47 = _$createElement("text");
368
+ _$insertNode(_el$47, _$createTextNode(`usage-coach: ...`));
369
+ return _el$47;
344
370
  })());
345
371
  }
346
372
  if (h && h.active !== false && h.tasks.length > 0) {
347
373
  nodes.push((() => {
348
- var _el$46 = _$createElement("text");
349
- _$insertNode(_el$46, _$createTextNode(` `));
350
- return _el$46;
374
+ var _el$49 = _$createElement("text");
375
+ _$insertNode(_el$49, _$createTextNode(` `));
376
+ return _el$49;
351
377
  })());
352
378
  nodes.push((() => {
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;
379
+ var _el$51 = _$createElement("text"), _el$52 = _$createTextNode(`harness: `), _el$53 = _$createTextNode(` `), _el$54 = _$createTextNode(`/`);
380
+ _$insertNode(_el$51, _el$52);
381
+ _$insertNode(_el$51, _el$53);
382
+ _$insertNode(_el$51, _el$54);
383
+ _$insert(_el$51, () => h.name, _el$53);
384
+ _$insert(_el$51, () => h.current, _el$54);
385
+ _$insert(_el$51, () => h.total, null);
386
+ _$effect((_$p) => _$setProp(_el$51, "style", st("textMuted"), _$p));
387
+ return _el$51;
362
388
  })());
363
389
  for (const t of h.tasks) {
364
390
  const sKey = statusKey[t.status] ?? "text";
@@ -368,70 +394,54 @@ function initializeTui(api, disposeRoot) {
368
394
  const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
369
395
  const elapsedStr = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
370
396
  nodes.push((() => {
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;
397
+ var _el$55 = _$createElement("text"), _el$56 = _$createTextNode(` \u25CF `), _el$57 = _$createTextNode(` `), _el$58 = _$createTextNode(` `);
398
+ _$insertNode(_el$55, _el$56);
399
+ _$insertNode(_el$55, _el$57);
400
+ _$insertNode(_el$55, _el$58);
401
+ _$insert(_el$55, () => t.id, _el$57);
402
+ _$insert(_el$55, mdl, _el$57);
403
+ _$insert(_el$55, lbl, _el$58);
404
+ _$insert(_el$55, rev, _el$58);
405
+ _$insert(_el$55, elapsedStr, _el$58);
406
+ _$insert(_el$55, () => t.title, null);
407
+ _$effect((_$p) => _$setProp(_el$55, "style", st(sKey), _$p));
408
+ return _el$55;
409
+ })());
410
+ const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
411
+ const provCoach = pv ? s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id)) : s?.providers?.[0];
412
+ const rawPct = provCoach?.fiveHour ?? s?.fiveHour ?? -1;
413
+ const pct = rawPct < 0 ? 0 : rawPct;
414
+ const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
415
+ nodes.push((() => {
416
+ var _el$59 = _$createElement("box"), _el$60 = _$createElement("text"), _el$62 = _$createElement("text"), _el$63 = _$createElement("text"), _el$64 = _$createElement("text"), _el$65 = _$createTextNode(` `);
417
+ _$insertNode(_el$59, _el$60);
418
+ _$insertNode(_el$59, _el$62);
419
+ _$insertNode(_el$59, _el$63);
420
+ _$insertNode(_el$59, _el$64);
421
+ _$setProp(_el$59, "flexDirection", "row");
422
+ _$insertNode(_el$60, _$createTextNode(` 5h `));
423
+ _$insert(_el$62, () => barFill(pct));
424
+ _$insert(_el$63, () => barEmpty(pct));
425
+ _$insertNode(_el$64, _el$65);
426
+ _$insert(_el$64, pctLabel, null);
427
+ _$effect((_p$) => {
428
+ var _v$11 = st("text"), _v$12 = st("text");
429
+ _v$11 !== _p$.e && (_p$.e = _$setProp(_el$62, "style", _v$11, _p$.e));
430
+ _v$12 !== _p$.t && (_p$.t = _$setProp(_el$63, "style", _v$12, _p$.t));
431
+ return _p$;
432
+ }, {
433
+ e: void 0,
434
+ t: void 0
435
+ });
436
+ return _el$59;
383
437
  })());
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
- }
428
438
  }
429
439
  }
430
440
  return (() => {
431
- var _el$69 = _$createElement("box");
432
- _$setProp(_el$69, "flexDirection", "column");
433
- _$insert(_el$69, nodes);
434
- return _el$69;
441
+ var _el$66 = _$createElement("box");
442
+ _$setProp(_el$66, "flexDirection", "column");
443
+ _$insert(_el$66, nodes);
444
+ return _el$66;
435
445
  })();
436
446
  };
437
447
  tlog("registering slots");
@@ -446,9 +456,9 @@ function initializeTui(api, disposeRoot) {
446
456
  } catch (e) {
447
457
  tlog(`sidebar_footer err: ${String(e)}`);
448
458
  result = (() => {
449
- var _el$70 = _$createElement("text");
450
- _$insertNode(_el$70, _$createTextNode(`usage-coach`));
451
- return _el$70;
459
+ var _el$67 = _$createElement("text");
460
+ _$insertNode(_el$67, _$createTextNode(`usage-coach`));
461
+ return _el$67;
452
462
  })();
453
463
  }
454
464
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
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,11 +58,5 @@
58
58
  "tsup": "^8.5",
59
59
  "typescript": "^5",
60
60
  "typescript-eslint": "^8.63.0"
61
- },
62
- "dependencies": {
63
- "@ladybugdb/core": "^0.18.0"
64
- },
65
- "trustedDependencies": [
66
- "@ladybugdb/core"
67
- ]
68
- }
61
+ }
62
+ }