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.
- package/README.md +0 -1
- package/dist/index.js +155 -185
- package/dist/tui.js +198 -188
- 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 {
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
39
|
-
return
|
|
26
|
+
function readNodes() {
|
|
27
|
+
return readNdjson(nodesFile());
|
|
40
28
|
}
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
|
51
|
-
|
|
35
|
+
function addDomainNode(node) {
|
|
36
|
+
const full = { ...node, id: uid("node"), ts: (/* @__PURE__ */ new Date()).toISOString() };
|
|
52
37
|
try {
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
const
|
|
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
|
-
|
|
64
|
+
function touchNodes(ids) {
|
|
150
65
|
if (ids.size === 0) return;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
221
|
-
|
|
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
|
-
|
|
202
|
+
mkdirSync2(dirname(f), { recursive: true });
|
|
273
203
|
h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
274
|
-
|
|
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
|
|
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)
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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)
|
|
542
|
-
//
|
|
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
|
-
|
|
657
|
-
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
762
|
-
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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("
|
|
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
|
-
_$
|
|
232
|
-
_$
|
|
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$
|
|
237
|
-
_$insertNode(_el$
|
|
238
|
-
_$insertNode(_el$
|
|
239
|
-
_$
|
|
240
|
-
_$
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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$
|
|
251
|
-
_v$ !== _p$.e && (_p$.e = _$setProp(_el$
|
|
252
|
-
_v$
|
|
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$
|
|
339
|
+
return _el$33;
|
|
259
340
|
})());
|
|
260
341
|
nodes.push((() => {
|
|
261
|
-
var _el$
|
|
262
|
-
_$insertNode(_el$
|
|
263
|
-
_$insertNode(_el$
|
|
264
|
-
_$insertNode(_el$
|
|
265
|
-
_$insertNode(_el$
|
|
266
|
-
_$setProp(_el$
|
|
267
|
-
_$insertNode(_el$
|
|
268
|
-
_$insert(_el$
|
|
269
|
-
_$insert(_el$
|
|
270
|
-
_$insertNode(_el$
|
|
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$
|
|
276
|
-
_v$
|
|
277
|
-
_v$
|
|
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$
|
|
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$
|
|
342
|
-
_$insertNode(_el$
|
|
343
|
-
return _el$
|
|
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$
|
|
349
|
-
_$insertNode(_el$
|
|
350
|
-
return _el$
|
|
374
|
+
var _el$49 = _$createElement("text");
|
|
375
|
+
_$insertNode(_el$49, _$createTextNode(` `));
|
|
376
|
+
return _el$49;
|
|
351
377
|
})());
|
|
352
378
|
nodes.push((() => {
|
|
353
|
-
var _el$
|
|
354
|
-
_$insertNode(_el$
|
|
355
|
-
_$insertNode(_el$
|
|
356
|
-
_$insertNode(_el$
|
|
357
|
-
_$insert(_el$
|
|
358
|
-
_$insert(_el$
|
|
359
|
-
_$insert(_el$
|
|
360
|
-
_$effect((_$p) => _$setProp(_el$
|
|
361
|
-
return _el$
|
|
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$
|
|
372
|
-
_$insertNode(_el$
|
|
373
|
-
_$insertNode(_el$
|
|
374
|
-
_$insertNode(_el$
|
|
375
|
-
_$insert(_el$
|
|
376
|
-
_$insert(_el$
|
|
377
|
-
_$insert(_el$
|
|
378
|
-
_$insert(_el$
|
|
379
|
-
_$insert(_el$
|
|
380
|
-
_$insert(_el$
|
|
381
|
-
_$effect((_$p) => _$setProp(_el$
|
|
382
|
-
return _el$
|
|
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$
|
|
432
|
-
_$setProp(_el$
|
|
433
|
-
_$insert(_el$
|
|
434
|
-
return _el$
|
|
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$
|
|
450
|
-
_$insertNode(_el$
|
|
451
|
-
return _el$
|
|
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.
|
|
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
|
-
|
|
63
|
-
"@ladybugdb/core": "^0.18.0"
|
|
64
|
-
},
|
|
65
|
-
"trustedDependencies": [
|
|
66
|
-
"@ladybugdb/core"
|
|
67
|
-
]
|
|
68
|
-
}
|
|
61
|
+
}
|
|
62
|
+
}
|