opencode-usage-coach 0.4.0 → 0.5.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 +2 -0
- package/dist/index.js +59 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -153,6 +153,8 @@ Place in the **work directory**. Each role runs on its model, so per-model quota
|
|
|
153
153
|
| `UC_TTL_MS` | 60000 | quota cache TTL (ms) |
|
|
154
154
|
| `UC_DEBUG` | 0 | set to `1` for a diagnostic log at `~/.cache/opencode-usage-coach/coach.log` |
|
|
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
|
+
| `UC_WORM_MAX_AGE_DAYS` | 180 | domain DB worm (GC): drop nodes not accessed in N days (~6 months) |
|
|
157
|
+
| `UC_WORM_MAX_NODES` | 100000 | domain DB worm (GC): cap node count, evict oldest-accessed beyond this |
|
|
156
158
|
|
|
157
159
|
## Agent-mode scoping
|
|
158
160
|
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { mkdirSync as mkdirSync2, writeFileSync, appendFileSync as appendFileSync2, 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,7 +7,7 @@ import { join as join2, resolve, dirname } from "path";
|
|
|
7
7
|
import { tool } from "@opencode-ai/plugin";
|
|
8
8
|
|
|
9
9
|
// src/domain.ts
|
|
10
|
-
import { mkdirSync, appendFileSync, readFileSync, existsSync } from "fs";
|
|
10
|
+
import { mkdirSync, appendFileSync, readFileSync, existsSync, writeFileSync } from "fs";
|
|
11
11
|
import { join } from "path";
|
|
12
12
|
var BASE_DIR = "";
|
|
13
13
|
function initDomain(stateDir) {
|
|
@@ -41,6 +41,14 @@ function addDomainNode(node) {
|
|
|
41
41
|
}
|
|
42
42
|
return full.id;
|
|
43
43
|
}
|
|
44
|
+
function writeNodes(nodes) {
|
|
45
|
+
try {
|
|
46
|
+
mkdirSync(BASE_DIR, { recursive: true });
|
|
47
|
+
const lines = nodes.map((n) => JSON.stringify(n));
|
|
48
|
+
writeFileSync(nodesFile(), lines.length ? lines.join("\n") + "\n" : "");
|
|
49
|
+
} catch {
|
|
50
|
+
}
|
|
51
|
+
}
|
|
44
52
|
function queryDomain(keywords) {
|
|
45
53
|
const lc = keywords.map((k) => k.toLowerCase());
|
|
46
54
|
const nodes = readNodes();
|
|
@@ -48,10 +56,47 @@ function queryDomain(keywords) {
|
|
|
48
56
|
const hay = (n.name + " " + JSON.stringify(n.props)).toLowerCase();
|
|
49
57
|
return lc.some((k) => k && hay.includes(k));
|
|
50
58
|
});
|
|
59
|
+
if (matched.length) touchNodes(new Set(matched.map((n) => n.id)));
|
|
51
60
|
const ids = new Set(matched.map((n) => n.id));
|
|
52
61
|
const edges = readEdges().filter((e) => ids.has(e.from) || ids.has(e.to));
|
|
53
62
|
return { nodes: matched, edges };
|
|
54
63
|
}
|
|
64
|
+
function touchNodes(ids) {
|
|
65
|
+
if (ids.size === 0) return;
|
|
66
|
+
try {
|
|
67
|
+
const nodes = readNodes();
|
|
68
|
+
let changed = false;
|
|
69
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
70
|
+
for (const n of nodes) {
|
|
71
|
+
if (ids.has(n.id)) {
|
|
72
|
+
n.lastAccessed = now;
|
|
73
|
+
n.accessCount = (n.accessCount ?? 0) + 1;
|
|
74
|
+
changed = true;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (changed) writeNodes(nodes);
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
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 };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
55
100
|
function saveInvestigationResult(keywords, result, source) {
|
|
56
101
|
try {
|
|
57
102
|
return addDomainNode({
|
|
@@ -93,7 +138,7 @@ function log(msg) {
|
|
|
93
138
|
function writeState(c) {
|
|
94
139
|
try {
|
|
95
140
|
mkdirSync2(STATE_DIR, { recursive: true });
|
|
96
|
-
|
|
141
|
+
writeFileSync2(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
97
142
|
} catch {
|
|
98
143
|
}
|
|
99
144
|
}
|
|
@@ -146,7 +191,7 @@ function writeHarness(sessionID, h) {
|
|
|
146
191
|
const f = harnessFile(sessionID);
|
|
147
192
|
mkdirSync2(dirname(f), { recursive: true });
|
|
148
193
|
h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
149
|
-
|
|
194
|
+
writeFileSync2(f, JSON.stringify(h, null, 2));
|
|
150
195
|
} catch {
|
|
151
196
|
}
|
|
152
197
|
}
|
|
@@ -211,6 +256,8 @@ var THR_5H = num("UC_THROTTLE_5H", 70);
|
|
|
211
256
|
var STOP_WK = num("UC_STOP_WEEKLY", 95);
|
|
212
257
|
var THR_WK = num("UC_THROTTLE_WEEKLY", 85);
|
|
213
258
|
var STOP_MO = num("UC_STOP_MONTHLY", 98);
|
|
259
|
+
var WORM_MAX_AGE_DAYS = num("UC_WORM_MAX_AGE_DAYS", 180);
|
|
260
|
+
var WORM_MAX_NODES = num("UC_WORM_MAX_NODES", 1e5);
|
|
214
261
|
function humanRemaining(iso) {
|
|
215
262
|
try {
|
|
216
263
|
if (!iso) return "";
|
|
@@ -397,6 +444,14 @@ async function UsageCoachPlugin(input) {
|
|
|
397
444
|
event: async ({ event }) => {
|
|
398
445
|
try {
|
|
399
446
|
if (event.type === "session.created" || event.type === "session.idle") refreshBackground();
|
|
447
|
+
if (event.type === "session.idle") {
|
|
448
|
+
try {
|
|
449
|
+
const r = evictStale(WORM_MAX_AGE_DAYS, WORM_MAX_NODES);
|
|
450
|
+
if (r.removed) log(`evictStale: removed ${r.removed}, kept ${r.kept} (maxAge=${WORM_MAX_AGE_DAYS}d, maxNodes=${WORM_MAX_NODES})`);
|
|
451
|
+
} catch (e) {
|
|
452
|
+
log(`evictStale err: ${String(e)}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
400
455
|
} catch (e) {
|
|
401
456
|
log(`event err: ${String(e)}`);
|
|
402
457
|
}
|
package/package.json
CHANGED