opencode-usage-coach 0.6.2 → 0.7.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 +0 -1
- package/dist/index.js +341 -199
- package/dist/tui.js +194 -150
- package/package.json +2 -8
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,125 @@ 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
|
-
};
|
|
42
|
+
return full.id;
|
|
67
43
|
}
|
|
68
|
-
|
|
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;
|
|
90
|
-
}
|
|
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
|
-
|
|
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 DEFAULT_MAX_STEPS = Number(process.env.UC_MAX_STEPS ?? 30) || 30;
|
|
118
|
+
var WATCHDOG_POLL_MS = Math.max(1e3, Number(process.env.UC_WATCHDOG_POLL_MS ?? 3e3) || 3e3);
|
|
119
|
+
var PIPE_LOG = join2(homedir(), ".cache", "opencode-usage-coach", "pipeline.log");
|
|
120
|
+
function pipeLog(msg) {
|
|
121
|
+
try {
|
|
122
|
+
mkdirSync2(dirname(PIPE_LOG), { recursive: true });
|
|
123
|
+
appendFileSync2(PIPE_LOG, `[SERVER] ${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
124
|
+
`);
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
pipeLog(`MODULE LOADED | node=${process.version} | pid=${process.pid}`);
|
|
197
129
|
var STATE_DIR = join2(homedir(), ".cache", "opencode-usage-coach");
|
|
198
130
|
var STATE_FILE = join2(STATE_DIR, "state.json");
|
|
199
131
|
var LOG_FILE = join2(STATE_DIR, "coach.log");
|
|
@@ -210,15 +142,15 @@ function setStateDir(dir) {
|
|
|
210
142
|
var NOOP_HOOKS = {};
|
|
211
143
|
function log(msg) {
|
|
212
144
|
try {
|
|
213
|
-
|
|
145
|
+
appendFileSync2(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
214
146
|
`);
|
|
215
147
|
} catch {
|
|
216
148
|
}
|
|
217
149
|
}
|
|
218
150
|
function writeState(c) {
|
|
219
151
|
try {
|
|
220
|
-
|
|
221
|
-
|
|
152
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
153
|
+
writeFileSync2(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
222
154
|
} catch {
|
|
223
155
|
}
|
|
224
156
|
}
|
|
@@ -269,12 +201,46 @@ function readHarness(sessionID) {
|
|
|
269
201
|
function writeHarness(sessionID, h) {
|
|
270
202
|
try {
|
|
271
203
|
const f = harnessFile(sessionID);
|
|
272
|
-
|
|
204
|
+
mkdirSync2(dirname(f), { recursive: true });
|
|
273
205
|
h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
274
|
-
|
|
206
|
+
writeFileSync2(f, JSON.stringify(h, null, 2));
|
|
207
|
+
} catch {
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function updateSubSession(sessionID, taskId, fields) {
|
|
211
|
+
try {
|
|
212
|
+
const h = readHarness(sessionID);
|
|
213
|
+
if (!h) return;
|
|
214
|
+
const t = h.tasks.find((x) => x.id === taskId);
|
|
215
|
+
if (!t) return;
|
|
216
|
+
Object.assign(t, fields);
|
|
217
|
+
writeHarness(sessionID, h);
|
|
275
218
|
} catch {
|
|
276
219
|
}
|
|
277
220
|
}
|
|
221
|
+
function clearSubSession(sessionID, taskId) {
|
|
222
|
+
try {
|
|
223
|
+
const h = readHarness(sessionID);
|
|
224
|
+
if (!h) return;
|
|
225
|
+
const t = h.tasks.find((x) => x.id === taskId);
|
|
226
|
+
if (!t) return;
|
|
227
|
+
t.subSessionId = void 0;
|
|
228
|
+
t.subStep = void 0;
|
|
229
|
+
t.lastActivity = void 0;
|
|
230
|
+
t.subElapsed = void 0;
|
|
231
|
+
writeHarness(sessionID, h);
|
|
232
|
+
} catch {
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function findActiveTaskId(sessionID, status) {
|
|
236
|
+
try {
|
|
237
|
+
const h = readHarness(sessionID);
|
|
238
|
+
if (!h) return void 0;
|
|
239
|
+
return h.tasks.find((x) => x.status === status)?.id;
|
|
240
|
+
} catch {
|
|
241
|
+
return void 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
278
244
|
function readHarnessCfg(dir) {
|
|
279
245
|
const tryRead = (p) => {
|
|
280
246
|
try {
|
|
@@ -288,8 +254,16 @@ function readHarnessCfg(dir) {
|
|
|
288
254
|
...tryRead(join2(dir, "harness.config.json"))
|
|
289
255
|
};
|
|
290
256
|
}
|
|
291
|
-
async function runModel(client, model, prompt, directory) {
|
|
257
|
+
async function runModel(client, model, prompt, directory, track, maxSteps = DEFAULT_MAX_STEPS) {
|
|
292
258
|
const t0 = Date.now();
|
|
259
|
+
const subStart = Date.now();
|
|
260
|
+
let poller = null;
|
|
261
|
+
let subId = null;
|
|
262
|
+
let timedOut = false;
|
|
263
|
+
let signalTimeout;
|
|
264
|
+
const timeoutSignal = new Promise((resolve2) => {
|
|
265
|
+
signalTimeout = resolve2;
|
|
266
|
+
});
|
|
293
267
|
try {
|
|
294
268
|
const slash = model.indexOf("/");
|
|
295
269
|
const providerID = slash >= 0 ? model.slice(0, slash) : model;
|
|
@@ -297,12 +271,75 @@ async function runModel(client, model, prompt, directory) {
|
|
|
297
271
|
const s = await client.session.create({ body: { title: "uc-harness-sub" }, query: { directory } });
|
|
298
272
|
const id = s?.data?.info?.id ?? s?.data?.id ?? s?.id;
|
|
299
273
|
if (!id) return `ERROR: session.create returned no id (response: ${JSON.stringify(s?.data ?? s).slice(0, 200)})`;
|
|
300
|
-
|
|
301
|
-
|
|
274
|
+
subId = id;
|
|
275
|
+
log(`runModel(${model}): session ${id} created, sending prompt (${prompt.length} chars), max_steps=${maxSteps}`);
|
|
276
|
+
poller = setInterval(async () => {
|
|
277
|
+
if (timedOut) return;
|
|
278
|
+
try {
|
|
279
|
+
let step = 0;
|
|
280
|
+
let lastTs = (/* @__PURE__ */ new Date()).toISOString();
|
|
281
|
+
try {
|
|
282
|
+
const msgs = await client.session.messages?.({ path: { id } });
|
|
283
|
+
const msgList = Array.isArray(msgs?.data) ? msgs.data : Array.isArray(msgs) ? msgs : [];
|
|
284
|
+
if (msgList.length) {
|
|
285
|
+
step = msgList.filter((m) => {
|
|
286
|
+
const role = m?.role ?? m?.info?.role;
|
|
287
|
+
return role === "assistant";
|
|
288
|
+
}).length;
|
|
289
|
+
const last = msgList[msgList.length - 1];
|
|
290
|
+
const ts = last?.ts ?? last?.info?.updatedAt ?? last?.info?.completedAt ?? last?.updatedAt;
|
|
291
|
+
if (ts) lastTs = String(ts);
|
|
292
|
+
}
|
|
293
|
+
} catch {
|
|
294
|
+
}
|
|
295
|
+
if (step > maxSteps) {
|
|
296
|
+
log(`runModel(${model}): STEP LIMIT exceeded (${step} > ${maxSteps}), aborting session ${id}`);
|
|
297
|
+
timedOut = true;
|
|
298
|
+
try {
|
|
299
|
+
await client.session.abort?.({ path: { id } });
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
signalTimeout();
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (track) {
|
|
306
|
+
const elapsed2 = Math.round((Date.now() - subStart) / 1e3);
|
|
307
|
+
updateSubSession(track.sessionID, track.taskId, {
|
|
308
|
+
subSessionId: id,
|
|
309
|
+
subStep: step,
|
|
310
|
+
lastActivity: lastTs,
|
|
311
|
+
subElapsed: elapsed2
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
log(`runModel poller err: ${String(e)}`);
|
|
316
|
+
}
|
|
317
|
+
}, WATCHDOG_POLL_MS);
|
|
318
|
+
const promptP = client.session.prompt({
|
|
302
319
|
path: { id },
|
|
303
320
|
body: { model: { providerID, modelID }, parts: [{ type: "text", text: prompt }] }
|
|
304
|
-
})
|
|
321
|
+
}).then(
|
|
322
|
+
(r) => r,
|
|
323
|
+
() => null
|
|
324
|
+
// abort causes rejection -> return null (handled via timedOut flag)
|
|
325
|
+
);
|
|
326
|
+
const resp = await Promise.race([promptP, timeoutSignal.then(() => null)]);
|
|
305
327
|
const elapsed = Math.round((Date.now() - t0) / 1e3);
|
|
328
|
+
if (timedOut) {
|
|
329
|
+
try {
|
|
330
|
+
const summary = await client.session.summarize?.({ path: { id } });
|
|
331
|
+
log(`runModel(${model}): TIMED OUT summary: ${JSON.stringify(summary?.data ?? summary).slice(0, 300)}`);
|
|
332
|
+
} catch {
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
await client.session.delete?.({ path: { id } });
|
|
336
|
+
} catch {
|
|
337
|
+
}
|
|
338
|
+
subId = null;
|
|
339
|
+
log(`runModel(${model}): TIMED OUT after ${elapsed}s (${maxSteps} steps exceeded)`);
|
|
340
|
+
return `Task appears too large (exceeded ${maxSteps} steps). Consider splitting into smaller subtasks.
|
|
341
|
+
[usage-coach NEXT] split the original task into smaller subtasks (each should complete within ${maxSteps} steps), then re-run generate for each subtask.`;
|
|
342
|
+
}
|
|
306
343
|
const parts = resp?.data?.parts ?? resp?.parts ?? [];
|
|
307
344
|
const text = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("");
|
|
308
345
|
try {
|
|
@@ -314,12 +351,27 @@ async function runModel(client, model, prompt, directory) {
|
|
|
314
351
|
await client.session.delete?.({ path: { id } });
|
|
315
352
|
} catch {
|
|
316
353
|
}
|
|
354
|
+
subId = null;
|
|
317
355
|
log(`runModel(${model}): done ${elapsed}s, ${text.length} chars`);
|
|
318
356
|
return text.trim() || `ERROR: no assistant text in prompt response after ${elapsed}s (parts: ${parts.length}, types: ${parts.map((p) => p?.type).join(",")})`;
|
|
319
357
|
} catch (e) {
|
|
320
358
|
const elapsed = Math.round((Date.now() - t0) / 1e3);
|
|
321
359
|
log(`runModel err (${model}, ${elapsed}s): ${String(e)}`);
|
|
322
360
|
return `ERROR: runModel exception after ${elapsed}s: ${String(e)}`;
|
|
361
|
+
} finally {
|
|
362
|
+
if (poller) clearInterval(poller);
|
|
363
|
+
if (track) {
|
|
364
|
+
try {
|
|
365
|
+
clearSubSession(track.sessionID, track.taskId);
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (subId) {
|
|
370
|
+
try {
|
|
371
|
+
await client.session.delete?.({ path: { id: subId } });
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
}
|
|
323
375
|
}
|
|
324
376
|
}
|
|
325
377
|
var HARNESS_AGENTS = (process.env.UC_HARNESS_AGENT ?? "Usage-Coach-Harness").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
@@ -451,14 +503,44 @@ function coach(q, lighter) {
|
|
|
451
503
|
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
504
|
}
|
|
453
505
|
var agentCache = /* @__PURE__ */ new Map();
|
|
506
|
+
var currentModel = "";
|
|
507
|
+
var currentProvider = "";
|
|
508
|
+
var currentAgent = "";
|
|
509
|
+
var modelChanged = false;
|
|
510
|
+
function isFreeModel(model, provider) {
|
|
511
|
+
if (!model && !provider) return false;
|
|
512
|
+
if (provider === "opencode") return true;
|
|
513
|
+
if (model.toLowerCase().includes("free")) return true;
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
function providerToCodexbar(provider) {
|
|
517
|
+
if (!provider) return "";
|
|
518
|
+
return provider.split("-")[0];
|
|
519
|
+
}
|
|
454
520
|
async function resolveAgent(client, sessionID) {
|
|
455
521
|
if (!sessionID) return "";
|
|
456
522
|
const hit = agentCache.get(sessionID);
|
|
457
|
-
if (hit && Date.now() - hit.ts < 6e4)
|
|
523
|
+
if (hit && Date.now() - hit.ts < 6e4) {
|
|
524
|
+
currentModel = hit.model;
|
|
525
|
+
currentProvider = hit.provider;
|
|
526
|
+
return hit.agent;
|
|
527
|
+
}
|
|
458
528
|
try {
|
|
459
529
|
const s = await client.session.get({ path: { id: sessionID } });
|
|
460
|
-
|
|
461
|
-
|
|
530
|
+
log(`resolveAgent raw session: ${JSON.stringify(s?.data?.info ?? s?.data ?? s?.info ?? s).slice(0, 500)}`);
|
|
531
|
+
const info = s?.data?.info ?? s?.data ?? s?.info ?? s;
|
|
532
|
+
const agent = String(info?.agent ?? "");
|
|
533
|
+
const rawModel = info?.model;
|
|
534
|
+
const model = typeof rawModel === "string" ? rawModel : rawModel?.id ?? rawModel?.modelID ?? rawModel?.name ?? "";
|
|
535
|
+
const rawProvider = info?.providerID ?? info?.provider ?? (typeof rawModel === "object" ? rawModel?.providerID ?? rawModel?.provider : "");
|
|
536
|
+
const provider = typeof rawProvider === "string" ? rawProvider : rawProvider?.id ?? "";
|
|
537
|
+
if (currentModel && model && currentModel !== model) {
|
|
538
|
+
log(`MODEL CHANGED: ${currentModel} \u2192 ${model} (provider: ${currentProvider} \u2192 ${provider})`);
|
|
539
|
+
modelChanged = true;
|
|
540
|
+
}
|
|
541
|
+
agentCache.set(sessionID, { agent, model, provider, ts: Date.now() });
|
|
542
|
+
currentModel = model;
|
|
543
|
+
currentProvider = provider;
|
|
462
544
|
return agent;
|
|
463
545
|
} catch (e) {
|
|
464
546
|
log(`resolveAgent err: ${String(e)}`);
|
|
@@ -483,9 +565,19 @@ async function UsageCoachPlugin(input) {
|
|
|
483
565
|
const refreshBackground = () => {
|
|
484
566
|
try {
|
|
485
567
|
if (refreshing) return;
|
|
486
|
-
if (last && Date.now() - lastFetchedAt < TTL_MS) return;
|
|
568
|
+
if (last && !modelChanged && Date.now() - lastFetchedAt < TTL_MS) return;
|
|
487
569
|
refreshing = true;
|
|
488
|
-
|
|
570
|
+
modelChanged = false;
|
|
571
|
+
if (isFreeModel(currentModel, currentProvider)) {
|
|
572
|
+
last = { decision: "GO", advice: `${currentModel || currentProvider || "free model"} \u2014 no quota limit.`, weekly: -1, monthly: -1, fiveHour: -1, model: currentModel, provider: currentProvider, isFree: true };
|
|
573
|
+
lastFetchedAt = Date.now();
|
|
574
|
+
writeState({ ...last, providers: [], model: currentModel, provider: currentProvider, isFree: true, agent: currentAgent, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
575
|
+
log(`FREE | model=${currentModel} provider=${currentProvider}`);
|
|
576
|
+
refreshing = false;
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const activeProvider = providerToCodexbar(currentProvider) || PROVIDER;
|
|
580
|
+
fetchQuota(activeProvider).then(async (q) => {
|
|
489
581
|
try {
|
|
490
582
|
last = coach(q, LIGHTER);
|
|
491
583
|
lastFetchedAt = Date.now();
|
|
@@ -498,7 +590,7 @@ async function UsageCoachPlugin(input) {
|
|
|
498
590
|
const p0 = providers[0];
|
|
499
591
|
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" };
|
|
500
592
|
}
|
|
501
|
-
writeState({ ...last, providers, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
593
|
+
writeState({ ...last, providers, model: currentModel, provider: currentProvider, isFree: false, agent: currentAgent, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
502
594
|
log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
|
|
503
595
|
} catch (e) {
|
|
504
596
|
log(`refresh-in-then err: ${String(e)}`);
|
|
@@ -526,7 +618,7 @@ async function UsageCoachPlugin(input) {
|
|
|
526
618
|
if (event.type === "session.created" || event.type === "session.idle") refreshBackground();
|
|
527
619
|
if (event.type === "session.idle") {
|
|
528
620
|
try {
|
|
529
|
-
const r =
|
|
621
|
+
const r = evictStale(WORM_MAX_AGE_DAYS, WORM_MAX_NODES);
|
|
530
622
|
if (r.removed) log(`evictStale: removed ${r.removed}, kept ${r.kept} (maxAge=${WORM_MAX_AGE_DAYS}d, maxNodes=${WORM_MAX_NODES})`);
|
|
531
623
|
} catch (e) {
|
|
532
624
|
log(`evictStale err: ${String(e)}`);
|
|
@@ -536,13 +628,14 @@ async function UsageCoachPlugin(input) {
|
|
|
536
628
|
log(`event err: ${String(e)}`);
|
|
537
629
|
}
|
|
538
630
|
},
|
|
539
|
-
// ACT(1)
|
|
540
|
-
//
|
|
541
|
-
// are NEVER gated, in ANY mode — they don't consume model quota.
|
|
631
|
+
// ACT(1) — model/agent detection on EVERY tool call (cached 60s, negligible overhead).
|
|
632
|
+
// Then hard-gate harness tools by agent mode + quota STOP.
|
|
542
633
|
"tool.execute.before": async (_input) => {
|
|
634
|
+
const agent = await resolveAgent(input.client, _input.sessionID);
|
|
635
|
+
currentAgent = agent;
|
|
636
|
+
refreshBackground();
|
|
543
637
|
const harnessTools = ["generate", "generate_batch", "grade", "investigate", "verify_diagnosis", "generalize", "harness_start", "task_update", "harness_done", "record_failure"];
|
|
544
638
|
if (!harnessTools.includes(_input.tool)) return;
|
|
545
|
-
const agent = await resolveAgent(input.client, _input.sessionID);
|
|
546
639
|
if (!isHarnessAgent(agent)) {
|
|
547
640
|
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.`);
|
|
548
641
|
}
|
|
@@ -562,6 +655,7 @@ async function UsageCoachPlugin(input) {
|
|
|
562
655
|
try {
|
|
563
656
|
if (_input.sessionID) {
|
|
564
657
|
const agent = await resolveAgent(input.client, _input.sessionID);
|
|
658
|
+
refreshBackground();
|
|
565
659
|
if (!isHarnessAgent(agent)) return;
|
|
566
660
|
}
|
|
567
661
|
const c = current();
|
|
@@ -577,12 +671,14 @@ async function UsageCoachPlugin(input) {
|
|
|
577
671
|
// Custom tools for the harness agent mode — report status to the panel.
|
|
578
672
|
tool: {
|
|
579
673
|
harness_start: tool({
|
|
580
|
-
description: "Start the harness: register the total task count on the panel. Call once when the harness loop begins.",
|
|
674
|
+
description: "Start the harness: register the total task count on the panel. Call once when the harness loop begins. IMPORTANT: each generate/generate_batch sub-session is step-limited (default 30). If any task seems too large, split it into smaller subtasks BEFORE starting \u2014 oversized tasks will timeout.",
|
|
581
675
|
args: { name: tool.schema.string(), total: tool.schema.number() },
|
|
582
676
|
async execute(args, ctx) {
|
|
583
677
|
writeHarness(ctx.sessionID, { name: args.name, total: args.total, current: 0, tasks: [], usage: {}, active: true, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
584
678
|
return `Harness '${args.name}' started (${args.total} tasks).
|
|
585
679
|
|
|
680
|
+
STEP LIMIT (default ${DEFAULT_MAX_STEPS}): each generate call creates a sub-session that is automatically aborted if it exceeds ${DEFAULT_MAX_STEPS} assistant steps. Before starting the loop, review each task: can it be completed in a focused, single-pass effort? If a task seems too broad (multiple files, multiple features, open-ended research), SPLIT it now into 2-3 smaller subtasks. A timeout wastes quota \u2014 split upfront.
|
|
681
|
+
|
|
586
682
|
DETERMINISTIC LOOP \u2014 first classify the tasks:
|
|
587
683
|
INDEPENDENT = task B does NOT need task A's output -> use PATH A (parallel, faster)
|
|
588
684
|
DEPENDENT = task B needs task A's output -> use PATH B (sequential)
|
|
@@ -651,8 +747,8 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
651
747
|
async execute(args, _ctx) {
|
|
652
748
|
const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), task: args.task, prompt: args.prompt, gradeResult: args.gradeResult, model: args.model, revisions: args.revisions };
|
|
653
749
|
try {
|
|
654
|
-
|
|
655
|
-
|
|
750
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
751
|
+
appendFileSync2(failuresFile(), JSON.stringify(rec) + "\n");
|
|
656
752
|
} catch (e) {
|
|
657
753
|
log(`record_failure err: ${String(e)}`);
|
|
658
754
|
}
|
|
@@ -676,7 +772,7 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
676
772
|
try {
|
|
677
773
|
keywords = extractKeywords(`${args.task} ${args.gradeResult}`);
|
|
678
774
|
if (keywords.length) {
|
|
679
|
-
const { nodes, edges } =
|
|
775
|
+
const { nodes, edges } = queryDomain(keywords);
|
|
680
776
|
if (nodes && nodes.length || edges && edges.length) {
|
|
681
777
|
domainEmpty = false;
|
|
682
778
|
domainPrefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
|
|
@@ -697,10 +793,17 @@ Output a structured root cause:
|
|
|
697
793
|
category: (one of: constraint-violation, missing-context, tool-misuse, model-limitation, other)
|
|
698
794
|
explanation: <why it failed>
|
|
699
795
|
evidence: <file/line or specific quote>`;
|
|
700
|
-
const
|
|
796
|
+
const invTaskId = findActiveTaskId(ctx.sessionID, "revising");
|
|
797
|
+
const out = await runModel(
|
|
798
|
+
input.client,
|
|
799
|
+
cfg.generator,
|
|
800
|
+
domainPrefix + rcaPrompt,
|
|
801
|
+
ctx.directory,
|
|
802
|
+
invTaskId ? { sessionID: ctx.sessionID, taskId: invTaskId } : void 0
|
|
803
|
+
);
|
|
701
804
|
if (domainEmpty && keywords.length) {
|
|
702
805
|
try {
|
|
703
|
-
|
|
806
|
+
saveInvestigationResult(keywords, out, "investigate");
|
|
704
807
|
} catch (e) {
|
|
705
808
|
log(`investigate save err: ${String(e)}`);
|
|
706
809
|
}
|
|
@@ -725,7 +828,14 @@ Grade feedback: ${args.gradeResult}
|
|
|
725
828
|
Diagnosis: ${args.diagnosis}
|
|
726
829
|
Is the diagnosis CORRECT and ACTIONABLE (leads to a useful rule)?
|
|
727
830
|
Output PASS (the diagnosis is right) or FAIL (re-investigate needed), then reason.`;
|
|
728
|
-
const
|
|
831
|
+
const verTaskId = findActiveTaskId(ctx.sessionID, "revising");
|
|
832
|
+
const out = await runModel(
|
|
833
|
+
input.client,
|
|
834
|
+
model,
|
|
835
|
+
verifyPrompt,
|
|
836
|
+
ctx.directory,
|
|
837
|
+
verTaskId ? { sessionID: ctx.sessionID, taskId: verTaskId } : void 0
|
|
838
|
+
);
|
|
729
839
|
let verdict = "FAIL";
|
|
730
840
|
if (!out.startsWith("ERROR:")) {
|
|
731
841
|
const f = (out.split("\n").find((l) => l.trim()) ?? "").trim();
|
|
@@ -752,12 +862,19 @@ Diagnosis: ${args.diagnosis}
|
|
|
752
862
|
Failed task: ${args.task}
|
|
753
863
|
Output a single rule in the form: 'For <task-type> tasks, always <check/do X> because <reason>.'
|
|
754
864
|
Keep it concrete and actionable.`;
|
|
755
|
-
const
|
|
865
|
+
const genRuleTaskId = findActiveTaskId(ctx.sessionID, "revising");
|
|
866
|
+
const out = await runModel(
|
|
867
|
+
input.client,
|
|
868
|
+
cfg.generator,
|
|
869
|
+
genPrompt,
|
|
870
|
+
ctx.directory,
|
|
871
|
+
genRuleTaskId ? { sessionID: ctx.sessionID, taskId: genRuleTaskId } : void 0
|
|
872
|
+
);
|
|
756
873
|
const rule = out;
|
|
757
874
|
try {
|
|
758
875
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
759
|
-
|
|
760
|
-
|
|
876
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
877
|
+
appendFileSync2(rulesFile(), `## Rule (${date})
|
|
761
878
|
${rule}
|
|
762
879
|
Origin: ${args.task}
|
|
763
880
|
|
|
@@ -772,8 +889,8 @@ Origin: ${args.task}
|
|
|
772
889
|
// Per-role model execution (config-driven, quota-aware, same server, no deadlock).
|
|
773
890
|
// P1: quota decision drives model selection + concurrency.
|
|
774
891
|
generate: tool({
|
|
775
|
-
description: "Run the GENERATOR model on a prompt. Quota-aware: on THROTTLE, auto-switches to lighterModel if configured. Returns the model's text response.",
|
|
776
|
-
args: { prompt: tool.schema.string() },
|
|
892
|
+
description: "Run the GENERATOR model on a prompt. Quota-aware: on THROTTLE, auto-switches to lighterModel if configured. Returns the model's text response. Step-limited: aborts after max_steps (default 30) to prevent runaway tasks.",
|
|
893
|
+
args: { prompt: tool.schema.string(), max_steps: tool.schema.number().optional().describe("Maximum sub-session steps before timeout (default 30). Increase for complex tasks, decrease to fail fast on scope creep.") },
|
|
777
894
|
async execute(args, ctx) {
|
|
778
895
|
const cfg = readHarnessCfg(ctx.directory);
|
|
779
896
|
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
@@ -796,7 +913,7 @@ ${rules}
|
|
|
796
913
|
try {
|
|
797
914
|
keywords = extractKeywords(args.prompt);
|
|
798
915
|
if (keywords.length) {
|
|
799
|
-
const { nodes, edges } =
|
|
916
|
+
const { nodes, edges } = queryDomain(keywords);
|
|
800
917
|
if (nodes && nodes.length || edges && edges.length) {
|
|
801
918
|
domainEmpty = false;
|
|
802
919
|
prefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
|
|
@@ -809,22 +926,33 @@ ${rules}
|
|
|
809
926
|
} catch (e) {
|
|
810
927
|
log(`generate domain query err: ${String(e)}`);
|
|
811
928
|
}
|
|
812
|
-
const
|
|
813
|
-
|
|
929
|
+
const genTaskId = findActiveTaskId(ctx.sessionID, "generating");
|
|
930
|
+
const maxSteps = args.max_steps ?? DEFAULT_MAX_STEPS;
|
|
931
|
+
const out = await runModel(
|
|
932
|
+
input.client,
|
|
933
|
+
model,
|
|
934
|
+
prefix + args.prompt,
|
|
935
|
+
ctx.directory,
|
|
936
|
+
genTaskId ? { sessionID: ctx.sessionID, taskId: genTaskId } : void 0,
|
|
937
|
+
maxSteps
|
|
938
|
+
);
|
|
939
|
+
const isTimeoutOrError = out.startsWith("Task appears too large") || out.startsWith("ERROR:");
|
|
940
|
+
if (domainEmpty && keywords.length && !isTimeoutOrError) {
|
|
814
941
|
try {
|
|
815
|
-
|
|
942
|
+
saveInvestigationResult(keywords, out, "generate");
|
|
816
943
|
} catch (e) {
|
|
817
944
|
log(`generate save err: ${String(e)}`);
|
|
818
945
|
}
|
|
819
946
|
}
|
|
947
|
+
if (out.startsWith("Task appears too large")) return out;
|
|
820
948
|
return out + (throttle ? `
|
|
821
949
|
[usage-coach] quota THROTTLE \u2014 used lighter model ${cfg.lighterModel}` : "") + `
|
|
822
950
|
[usage-coach NEXT] call task_update(i, title, "grading"), then grade to evaluate this work.`;
|
|
823
951
|
}
|
|
824
952
|
}),
|
|
825
953
|
generate_batch: tool({
|
|
826
|
-
description: "Run the GENERATOR model on MULTIPLE tasks. Quota-aware: GO = full parallel; THROTTLE = lighter model + concurrency capped at 2; STOP = refused. Use for INDEPENDENT tasks.",
|
|
827
|
-
args: { tasks: tool.schema.array(tool.schema.object({ id: tool.schema.number(), prompt: tool.schema.string() })) },
|
|
954
|
+
description: "Run the GENERATOR model on MULTIPLE tasks. Quota-aware: GO = full parallel; THROTTLE = lighter model + concurrency capped at 2; STOP = refused. Use for INDEPENDENT tasks. Step-limited: each sub-session aborts after max_steps (default 30).",
|
|
955
|
+
args: { tasks: tool.schema.array(tool.schema.object({ id: tool.schema.number(), prompt: tool.schema.string() })), max_steps: tool.schema.number().optional().describe("Maximum sub-session steps per task before timeout (default 30).") },
|
|
828
956
|
async execute(args, ctx) {
|
|
829
957
|
const cfg = readHarnessCfg(ctx.directory);
|
|
830
958
|
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
@@ -841,7 +969,14 @@ ${rules}
|
|
|
841
969
|
for (let i = 0; i < args.tasks.length; i += limit) {
|
|
842
970
|
const batch = args.tasks.slice(i, i + limit);
|
|
843
971
|
const out = await Promise.all(batch.map(async (t) => {
|
|
844
|
-
const r = await runModel(
|
|
972
|
+
const r = await runModel(
|
|
973
|
+
input.client,
|
|
974
|
+
model,
|
|
975
|
+
t.prompt,
|
|
976
|
+
ctx.directory,
|
|
977
|
+
{ sessionID: ctx.sessionID, taskId: t.id },
|
|
978
|
+
args.max_steps ?? DEFAULT_MAX_STEPS
|
|
979
|
+
);
|
|
845
980
|
return `[task ${t.id}] ${r}`;
|
|
846
981
|
}));
|
|
847
982
|
results.push(...out);
|
|
@@ -858,7 +993,14 @@ ${rules}
|
|
|
858
993
|
const cfg = readHarnessCfg(ctx.directory);
|
|
859
994
|
const model = cfg.grader ?? cfg.generator;
|
|
860
995
|
if (!model) return "FAIL\n(ERROR: no grader/generator model configured.)\n[usage-coach NEXT] configure grader in harness.config.json, then retry grade.";
|
|
861
|
-
const
|
|
996
|
+
const gradeTaskId = findActiveTaskId(ctx.sessionID, "grading");
|
|
997
|
+
const out = await runModel(
|
|
998
|
+
input.client,
|
|
999
|
+
model,
|
|
1000
|
+
args.prompt,
|
|
1001
|
+
ctx.directory,
|
|
1002
|
+
gradeTaskId ? { sessionID: ctx.sessionID, taskId: gradeTaskId } : void 0
|
|
1003
|
+
);
|
|
862
1004
|
let verdict = "FAIL";
|
|
863
1005
|
if (!out.startsWith("ERROR:")) {
|
|
864
1006
|
const f = (out.split("\n").find((l) => l.trim()) ?? "").trim();
|
package/dist/tui.js
CHANGED
|
@@ -202,173 +202,217 @@ function initializeTui(api, disposeRoot) {
|
|
|
202
202
|
h = null;
|
|
203
203
|
}
|
|
204
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
|
+
}
|
|
205
209
|
if (s) {
|
|
206
210
|
const dKey = s.decision === "GO" ? "success" : s.decision === "THROTTLE" ? "warning" : "error";
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
var
|
|
219
|
-
_$
|
|
220
|
-
_$
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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) {
|
|
224
234
|
nodes.push((() => {
|
|
225
|
-
var _el$
|
|
226
|
-
_$insertNode(_el$
|
|
227
|
-
_$insertNode(_el$
|
|
228
|
-
_$
|
|
229
|
-
_$insertNode(_el$
|
|
230
|
-
_$
|
|
231
|
-
_$
|
|
232
|
-
_$insert(_el$10, () => barFill(p.fiveHour));
|
|
233
|
-
_$insert(_el$11, () => barEmpty(p.fiveHour));
|
|
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(` `);
|
|
236
|
+
_$insertNode(_el$0, _el$1);
|
|
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);
|
|
234
242
|
_$insertNode(_el$12, _el$13);
|
|
235
|
-
_$
|
|
236
|
-
_$insert(_el$12, () => p.fiveHour, _el$14);
|
|
237
|
-
_$insert(_el$12, () => p.fiveHourReset, null);
|
|
243
|
+
_$insert(_el$12, modelShort, null);
|
|
238
244
|
_$effect((_p$) => {
|
|
239
|
-
var _v$ = st(
|
|
240
|
-
_v$ !== _p$.e && (_p$.e = _$setProp(_el$
|
|
241
|
-
_v$
|
|
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));
|
|
242
248
|
return _p$;
|
|
243
249
|
}, {
|
|
244
250
|
e: void 0,
|
|
245
251
|
t: void 0
|
|
246
252
|
});
|
|
247
|
-
return _el$
|
|
253
|
+
return _el$0;
|
|
254
|
+
})());
|
|
255
|
+
} else {
|
|
256
|
+
nodes.push((() => {
|
|
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;
|
|
248
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 {
|
|
249
319
|
nodes.push((() => {
|
|
250
|
-
var _el$
|
|
251
|
-
_$insertNode(_el$
|
|
252
|
-
_$insertNode(_el$
|
|
253
|
-
_$insertNode(_el$
|
|
254
|
-
_$insertNode(_el$
|
|
255
|
-
_$setProp(_el$
|
|
256
|
-
_$insertNode(_el$
|
|
257
|
-
_$insert(_el$
|
|
258
|
-
_$insert(_el$
|
|
259
|
-
_$insertNode(_el$
|
|
260
|
-
_$insertNode(_el$20, _el$22);
|
|
261
|
-
_$insert(_el$20, () => p.weekly, _el$22);
|
|
262
|
-
_$insert(_el$20, () => p.weeklyReset, null);
|
|
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%`));
|
|
263
330
|
_$effect((_p$) => {
|
|
264
|
-
var _v$
|
|
265
|
-
_v$
|
|
266
|
-
_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));
|
|
267
334
|
return _p$;
|
|
268
335
|
}, {
|
|
269
336
|
e: void 0,
|
|
270
337
|
t: void 0
|
|
271
338
|
});
|
|
272
|
-
return _el$
|
|
339
|
+
return _el$33;
|
|
273
340
|
})());
|
|
274
341
|
nodes.push((() => {
|
|
275
|
-
var _el$
|
|
276
|
-
_$insertNode(_el$
|
|
277
|
-
_$
|
|
278
|
-
_$
|
|
279
|
-
|
|
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%`));
|
|
352
|
+
_$effect((_p$) => {
|
|
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));
|
|
356
|
+
return _p$;
|
|
357
|
+
}, {
|
|
358
|
+
e: void 0,
|
|
359
|
+
t: void 0
|
|
360
|
+
});
|
|
361
|
+
return _el$40;
|
|
280
362
|
})());
|
|
281
363
|
}
|
|
282
|
-
} else {
|
|
283
|
-
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%`));
|
|
294
|
-
_$effect((_p$) => {
|
|
295
|
-
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));
|
|
298
|
-
return _p$;
|
|
299
|
-
}, {
|
|
300
|
-
e: void 0,
|
|
301
|
-
t: void 0
|
|
302
|
-
});
|
|
303
|
-
return _el$27;
|
|
304
|
-
})());
|
|
305
|
-
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%`));
|
|
316
|
-
_$effect((_p$) => {
|
|
317
|
-
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));
|
|
320
|
-
return _p$;
|
|
321
|
-
}, {
|
|
322
|
-
e: void 0,
|
|
323
|
-
t: void 0
|
|
324
|
-
});
|
|
325
|
-
return _el$34;
|
|
326
|
-
})());
|
|
327
364
|
}
|
|
328
365
|
} else {
|
|
329
366
|
nodes.push((() => {
|
|
330
|
-
var _el$
|
|
331
|
-
_$insertNode(_el$
|
|
332
|
-
return _el$
|
|
367
|
+
var _el$47 = _$createElement("text");
|
|
368
|
+
_$insertNode(_el$47, _$createTextNode(`usage-coach: ...`));
|
|
369
|
+
return _el$47;
|
|
333
370
|
})());
|
|
334
371
|
}
|
|
335
372
|
if (h && h.active !== false && h.tasks.length > 0) {
|
|
336
373
|
nodes.push((() => {
|
|
337
|
-
var _el$
|
|
338
|
-
_$insertNode(_el$
|
|
339
|
-
return _el$
|
|
374
|
+
var _el$49 = _$createElement("text");
|
|
375
|
+
_$insertNode(_el$49, _$createTextNode(` `));
|
|
376
|
+
return _el$49;
|
|
340
377
|
})());
|
|
341
378
|
nodes.push((() => {
|
|
342
|
-
var _el$
|
|
343
|
-
_$insertNode(_el$
|
|
344
|
-
_$insertNode(_el$
|
|
345
|
-
_$insertNode(_el$
|
|
346
|
-
_$insert(_el$
|
|
347
|
-
_$insert(_el$
|
|
348
|
-
_$insert(_el$
|
|
349
|
-
_$effect((_$p) => _$setProp(_el$
|
|
350
|
-
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;
|
|
351
388
|
})());
|
|
352
389
|
for (const t of h.tasks) {
|
|
353
390
|
const sKey = statusKey[t.status] ?? "text";
|
|
354
391
|
const lbl = TLABEL[t.status] ?? t.status;
|
|
355
392
|
const rev = t.revisions > 0 && t.status === "revising" ? `(${t.revisions})` : "";
|
|
356
393
|
const mdl = t.model ? ` ${t.model.split("/").pop() ?? t.model}` : "";
|
|
394
|
+
const hasSub = !!t.subSessionId;
|
|
395
|
+
const subStepStr = hasSub && t.subStep !== void 0 && t.subStep > 0 ? ` step:${t.subStep}` : "";
|
|
396
|
+
const subEl = hasSub && t.subElapsed !== void 0 ? ` ${t.subElapsed}s` : "";
|
|
397
|
+
const subWarn = hasSub && (t.subElapsed ?? 0) > 300;
|
|
357
398
|
const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
|
|
358
|
-
const
|
|
399
|
+
const taskEl = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
|
|
400
|
+
const displayEl = hasSub ? subEl : taskEl;
|
|
401
|
+
const lineKey = subWarn ? "warning" : sKey;
|
|
359
402
|
nodes.push((() => {
|
|
360
|
-
var _el$
|
|
361
|
-
_$insertNode(_el$
|
|
362
|
-
_$insertNode(_el$
|
|
363
|
-
_$insertNode(_el$
|
|
364
|
-
_$insert(_el$
|
|
365
|
-
_$insert(_el$
|
|
366
|
-
_$insert(_el$
|
|
367
|
-
_$insert(_el$
|
|
368
|
-
_$insert(_el$
|
|
369
|
-
_$insert(_el$
|
|
370
|
-
_$
|
|
371
|
-
|
|
403
|
+
var _el$55 = _$createElement("text"), _el$56 = _$createTextNode(` \u25CF `), _el$57 = _$createTextNode(` `), _el$58 = _$createTextNode(` `);
|
|
404
|
+
_$insertNode(_el$55, _el$56);
|
|
405
|
+
_$insertNode(_el$55, _el$57);
|
|
406
|
+
_$insertNode(_el$55, _el$58);
|
|
407
|
+
_$insert(_el$55, () => t.id, _el$57);
|
|
408
|
+
_$insert(_el$55, mdl, _el$57);
|
|
409
|
+
_$insert(_el$55, lbl, _el$58);
|
|
410
|
+
_$insert(_el$55, rev, _el$58);
|
|
411
|
+
_$insert(_el$55, subStepStr, _el$58);
|
|
412
|
+
_$insert(_el$55, displayEl, _el$58);
|
|
413
|
+
_$insert(_el$55, () => t.title, null);
|
|
414
|
+
_$effect((_$p) => _$setProp(_el$55, "style", st(lineKey), _$p));
|
|
415
|
+
return _el$55;
|
|
372
416
|
})());
|
|
373
417
|
const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
|
|
374
418
|
const provCoach = pv ? s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id)) : s?.providers?.[0];
|
|
@@ -376,35 +420,35 @@ function initializeTui(api, disposeRoot) {
|
|
|
376
420
|
const pct = rawPct < 0 ? 0 : rawPct;
|
|
377
421
|
const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
|
|
378
422
|
nodes.push((() => {
|
|
379
|
-
var _el$
|
|
380
|
-
_$insertNode(_el$
|
|
381
|
-
_$insertNode(_el$
|
|
382
|
-
_$insertNode(_el$
|
|
383
|
-
_$insertNode(_el$
|
|
384
|
-
_$setProp(_el$
|
|
385
|
-
_$insertNode(_el$
|
|
386
|
-
_$insert(_el$
|
|
387
|
-
_$insert(_el$
|
|
388
|
-
_$insertNode(_el$
|
|
389
|
-
_$insert(_el$
|
|
423
|
+
var _el$59 = _$createElement("box"), _el$60 = _$createElement("text"), _el$62 = _$createElement("text"), _el$63 = _$createElement("text"), _el$64 = _$createElement("text"), _el$65 = _$createTextNode(` `);
|
|
424
|
+
_$insertNode(_el$59, _el$60);
|
|
425
|
+
_$insertNode(_el$59, _el$62);
|
|
426
|
+
_$insertNode(_el$59, _el$63);
|
|
427
|
+
_$insertNode(_el$59, _el$64);
|
|
428
|
+
_$setProp(_el$59, "flexDirection", "row");
|
|
429
|
+
_$insertNode(_el$60, _$createTextNode(` 5h `));
|
|
430
|
+
_$insert(_el$62, () => barFill(pct));
|
|
431
|
+
_$insert(_el$63, () => barEmpty(pct));
|
|
432
|
+
_$insertNode(_el$64, _el$65);
|
|
433
|
+
_$insert(_el$64, pctLabel, null);
|
|
390
434
|
_$effect((_p$) => {
|
|
391
|
-
var _v$
|
|
392
|
-
_v$
|
|
393
|
-
_v$
|
|
435
|
+
var _v$11 = st("text"), _v$12 = st("text");
|
|
436
|
+
_v$11 !== _p$.e && (_p$.e = _$setProp(_el$62, "style", _v$11, _p$.e));
|
|
437
|
+
_v$12 !== _p$.t && (_p$.t = _$setProp(_el$63, "style", _v$12, _p$.t));
|
|
394
438
|
return _p$;
|
|
395
439
|
}, {
|
|
396
440
|
e: void 0,
|
|
397
441
|
t: void 0
|
|
398
442
|
});
|
|
399
|
-
return _el$
|
|
443
|
+
return _el$59;
|
|
400
444
|
})());
|
|
401
445
|
}
|
|
402
446
|
}
|
|
403
447
|
return (() => {
|
|
404
|
-
var _el$
|
|
405
|
-
_$setProp(_el$
|
|
406
|
-
_$insert(_el$
|
|
407
|
-
return _el$
|
|
448
|
+
var _el$66 = _$createElement("box");
|
|
449
|
+
_$setProp(_el$66, "flexDirection", "column");
|
|
450
|
+
_$insert(_el$66, nodes);
|
|
451
|
+
return _el$66;
|
|
408
452
|
})();
|
|
409
453
|
};
|
|
410
454
|
tlog("registering slots");
|
|
@@ -419,9 +463,9 @@ function initializeTui(api, disposeRoot) {
|
|
|
419
463
|
} catch (e) {
|
|
420
464
|
tlog(`sidebar_footer err: ${String(e)}`);
|
|
421
465
|
result = (() => {
|
|
422
|
-
var _el$
|
|
423
|
-
_$insertNode(_el$
|
|
424
|
-
return _el$
|
|
466
|
+
var _el$67 = _$createElement("text");
|
|
467
|
+
_$insertNode(_el$67, _$createTextNode(`usage-coach`));
|
|
468
|
+
return _el$67;
|
|
425
469
|
})();
|
|
426
470
|
}
|
|
427
471
|
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.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,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
|
-
]
|
|
61
|
+
}
|
|
68
62
|
}
|