opencode-usage-coach 0.11.3 → 0.12.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/dist/index.js +212 -27
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,18 +3,26 @@ import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, appendFileSyn
|
|
|
3
3
|
import { spawn, spawnSync } from "child_process";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
5
|
import { homedir } from "os";
|
|
6
|
-
import { join as join2, resolve, dirname } from "path";
|
|
6
|
+
import { join as join2, resolve, dirname as dirname2 } from "path";
|
|
7
7
|
import { tool } from "@opencode-ai/plugin";
|
|
8
8
|
|
|
9
9
|
// src/domain.ts
|
|
10
10
|
import { mkdirSync, appendFileSync, readFileSync, existsSync, writeFileSync } from "fs";
|
|
11
|
-
import { join } from "path";
|
|
11
|
+
import { join, dirname, basename } from "path";
|
|
12
12
|
var BASE_DIR = "";
|
|
13
|
+
var SHARED_DIR = "";
|
|
13
14
|
function initDomain(stateDir) {
|
|
14
15
|
BASE_DIR = stateDir;
|
|
16
|
+
if (basename(dirname(stateDir)) === "projects") {
|
|
17
|
+
SHARED_DIR = join(dirname(dirname(stateDir)), "shared");
|
|
18
|
+
} else {
|
|
19
|
+
SHARED_DIR = join(stateDir, "_shared");
|
|
20
|
+
}
|
|
15
21
|
}
|
|
16
22
|
var nodesFile = () => join(BASE_DIR, "nodes.ndjson");
|
|
17
23
|
var edgesFile = () => join(BASE_DIR, "edges.ndjson");
|
|
24
|
+
var sharedNodesFile = () => join(SHARED_DIR, "nodes.ndjson");
|
|
25
|
+
var sharedEdgesFile = () => join(SHARED_DIR, "edges.ndjson");
|
|
18
26
|
function readNdjson(path) {
|
|
19
27
|
try {
|
|
20
28
|
if (!existsSync(path)) return [];
|
|
@@ -24,23 +32,14 @@ function readNdjson(path) {
|
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
function readNodes() {
|
|
27
|
-
return readNdjson(nodesFile());
|
|
35
|
+
return [...readNdjson(nodesFile()), ...readNdjson(sharedNodesFile())];
|
|
28
36
|
}
|
|
29
37
|
function readEdges() {
|
|
30
|
-
return readNdjson(edgesFile());
|
|
38
|
+
return [...readNdjson(edgesFile()), ...readNdjson(sharedEdgesFile())];
|
|
31
39
|
}
|
|
32
40
|
function uid(prefix) {
|
|
33
41
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
34
42
|
}
|
|
35
|
-
function addDomainNode(node) {
|
|
36
|
-
const full = { ...node, id: uid("node"), ts: (/* @__PURE__ */ new Date()).toISOString() };
|
|
37
|
-
try {
|
|
38
|
-
mkdirSync(BASE_DIR, { recursive: true });
|
|
39
|
-
appendFileSync(nodesFile(), JSON.stringify(full) + "\n");
|
|
40
|
-
} catch {
|
|
41
|
-
}
|
|
42
|
-
return full.id;
|
|
43
|
-
}
|
|
44
43
|
function addDomainEdge(edge) {
|
|
45
44
|
const full = { ...edge, ts: (/* @__PURE__ */ new Date()).toISOString() };
|
|
46
45
|
try {
|
|
@@ -49,6 +48,9 @@ function addDomainEdge(edge) {
|
|
|
49
48
|
} catch {
|
|
50
49
|
}
|
|
51
50
|
}
|
|
51
|
+
function readProjectNodes() {
|
|
52
|
+
return readNdjson(nodesFile());
|
|
53
|
+
}
|
|
52
54
|
function writeNodes(nodes) {
|
|
53
55
|
try {
|
|
54
56
|
mkdirSync(BASE_DIR, { recursive: true });
|
|
@@ -72,7 +74,7 @@ function queryDomain(keywords) {
|
|
|
72
74
|
function touchNodes(ids) {
|
|
73
75
|
if (ids.size === 0) return;
|
|
74
76
|
try {
|
|
75
|
-
const nodes =
|
|
77
|
+
const nodes = readProjectNodes();
|
|
76
78
|
let changed = false;
|
|
77
79
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
78
80
|
for (const n of nodes) {
|
|
@@ -88,7 +90,7 @@ function touchNodes(ids) {
|
|
|
88
90
|
}
|
|
89
91
|
function evictStale(maxAgeDays = 30, maxNodes = 1e3) {
|
|
90
92
|
try {
|
|
91
|
-
const nodes =
|
|
93
|
+
const nodes = readProjectNodes();
|
|
92
94
|
if (nodes.length === 0) return { removed: 0, kept: 0 };
|
|
93
95
|
const now = Date.now();
|
|
94
96
|
const ageMs = maxAgeDays * 864e5;
|
|
@@ -167,17 +169,56 @@ function queryDomainGraph(keywords, maxDepth = 2, opts = {}) {
|
|
|
167
169
|
}
|
|
168
170
|
function saveInvestigationResult(keywords, result, source, confidence = 0.7) {
|
|
169
171
|
try {
|
|
170
|
-
|
|
172
|
+
const nodeId = uid("node");
|
|
173
|
+
const full = {
|
|
174
|
+
id: nodeId,
|
|
171
175
|
type: "fact",
|
|
172
176
|
name: keywords.join(" "),
|
|
173
|
-
props: { result },
|
|
177
|
+
props: { result, keywords: [...new Set(keywords)] },
|
|
174
178
|
source: source || "investigation",
|
|
175
|
-
confidence
|
|
176
|
-
|
|
179
|
+
confidence,
|
|
180
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
181
|
+
};
|
|
182
|
+
try {
|
|
183
|
+
mkdirSync(SHARED_DIR, { recursive: true });
|
|
184
|
+
appendFileSync(sharedNodesFile(), JSON.stringify(full) + "\n");
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
autoLinkKeywords(nodeId, keywords);
|
|
188
|
+
return nodeId;
|
|
177
189
|
} catch {
|
|
178
190
|
return "";
|
|
179
191
|
}
|
|
180
192
|
}
|
|
193
|
+
function autoLinkKeywords(nodeId, keywords, minOverlap = 2, maxLinks = 8) {
|
|
194
|
+
if (keywords.length < minOverlap) return;
|
|
195
|
+
try {
|
|
196
|
+
const candidates = queryDomain(keywords);
|
|
197
|
+
let linked = 0;
|
|
198
|
+
for (const node of candidates.nodes) {
|
|
199
|
+
if (node.id === nodeId) continue;
|
|
200
|
+
const nodeKw = Array.isArray(node.props?.keywords) ? node.props.keywords : (node.name || "").toLowerCase().split(/[^a-z0-9_-]+/).filter((w) => w.length >= 3);
|
|
201
|
+
const overlap = keywords.filter((k) => nodeKw.includes(k));
|
|
202
|
+
if (overlap.length >= minOverlap) {
|
|
203
|
+
const edge = {
|
|
204
|
+
from: nodeId,
|
|
205
|
+
to: node.id,
|
|
206
|
+
rel: "related-to",
|
|
207
|
+
note: `auto: ${overlap.length} shared (${overlap.slice(0, 5).join(",")})`,
|
|
208
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
209
|
+
};
|
|
210
|
+
try {
|
|
211
|
+
mkdirSync(SHARED_DIR, { recursive: true });
|
|
212
|
+
appendFileSync(sharedEdgesFile(), JSON.stringify(edge) + "\n");
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
linked++;
|
|
216
|
+
if (linked >= maxLinks) break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
}
|
|
181
222
|
|
|
182
223
|
// src/web-search.ts
|
|
183
224
|
var FRAMEWORK_DOCS = {
|
|
@@ -231,8 +272,30 @@ function effectiveFrameworks(frameworks, keyDeps) {
|
|
|
231
272
|
async function ghFetch(url, signal) {
|
|
232
273
|
const res = await fetch(url, { headers: ghHeaders(), signal });
|
|
233
274
|
if (!res.ok) {
|
|
234
|
-
|
|
235
|
-
|
|
275
|
+
let ghError = null;
|
|
276
|
+
let ghErrorText = "";
|
|
277
|
+
try {
|
|
278
|
+
ghErrorText = await res.text();
|
|
279
|
+
ghError = JSON.parse(ghErrorText);
|
|
280
|
+
} catch {
|
|
281
|
+
}
|
|
282
|
+
const tag = res.status === 403 || res.status === 429 ? " (rate limit)" : res.status === 422 ? " (validation failed)" : "";
|
|
283
|
+
const errs = ghError?.errors;
|
|
284
|
+
const errMsg = ghError?.message ?? ghErrorText.slice(0, 300);
|
|
285
|
+
const detail = errs ? errs.map((e) => typeof e === "string" ? e : `${e?.field ?? "?"}: ${e?.message ?? e?.code ?? JSON.stringify(e)}`).join("; ") : "";
|
|
286
|
+
console.error(JSON.stringify({
|
|
287
|
+
level: "error",
|
|
288
|
+
module: "web-search",
|
|
289
|
+
event: "gh-fetch-error",
|
|
290
|
+
status: res.status,
|
|
291
|
+
tag,
|
|
292
|
+
url,
|
|
293
|
+
ghMessage: errMsg,
|
|
294
|
+
ghErrors: detail,
|
|
295
|
+
rateLimitRemaining: res.headers.get("x-ratelimit-remaining"),
|
|
296
|
+
rateLimitReset: res.headers.get("x-ratelimit-reset")
|
|
297
|
+
}));
|
|
298
|
+
throw new Error(`HTTP ${res.status}${tag}: ${errMsg}${detail ? ` | ${detail}` : ""}`);
|
|
236
299
|
}
|
|
237
300
|
return await res.json();
|
|
238
301
|
}
|
|
@@ -363,7 +426,7 @@ var DEFAULT_MAX_QUESTIONS = Math.max(1, Math.round(Number(process.env.UC_MAX_QUE
|
|
|
363
426
|
var PIPE_LOG = join2(homedir(), ".cache", "opencode-usage-coach", "pipeline.log");
|
|
364
427
|
function pipeLog(msg) {
|
|
365
428
|
try {
|
|
366
|
-
mkdirSync2(
|
|
429
|
+
mkdirSync2(dirname2(PIPE_LOG), { recursive: true });
|
|
367
430
|
appendFileSync2(PIPE_LOG, `[SERVER] ${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
368
431
|
`);
|
|
369
432
|
} catch {
|
|
@@ -515,15 +578,137 @@ function readImplNotesByGraph(keywords, limit = 5) {
|
|
|
515
578
|
}
|
|
516
579
|
function extractKeywords(text) {
|
|
517
580
|
try {
|
|
518
|
-
const STOP = /* @__PURE__ */ new Set([
|
|
581
|
+
const STOP = /* @__PURE__ */ new Set([
|
|
582
|
+
// articles / prepositions / conjunctions
|
|
583
|
+
"the",
|
|
584
|
+
"and",
|
|
585
|
+
"for",
|
|
586
|
+
"with",
|
|
587
|
+
"that",
|
|
588
|
+
"this",
|
|
589
|
+
"from",
|
|
590
|
+
"into",
|
|
591
|
+
"your",
|
|
592
|
+
"you",
|
|
593
|
+
"are",
|
|
594
|
+
"was",
|
|
595
|
+
"but",
|
|
596
|
+
"not",
|
|
597
|
+
"all",
|
|
598
|
+
"any",
|
|
599
|
+
"use",
|
|
600
|
+
"task",
|
|
601
|
+
"prompt",
|
|
602
|
+
// generic verbs
|
|
603
|
+
"apply",
|
|
604
|
+
"add",
|
|
605
|
+
"make",
|
|
606
|
+
"set",
|
|
607
|
+
"get",
|
|
608
|
+
"run",
|
|
609
|
+
"try",
|
|
610
|
+
"put",
|
|
611
|
+
"let",
|
|
612
|
+
"new",
|
|
613
|
+
"will",
|
|
614
|
+
"can",
|
|
615
|
+
"has",
|
|
616
|
+
"had",
|
|
617
|
+
"have",
|
|
618
|
+
"been",
|
|
619
|
+
"were",
|
|
620
|
+
"they",
|
|
621
|
+
"them",
|
|
622
|
+
"when",
|
|
623
|
+
"then",
|
|
624
|
+
"than",
|
|
625
|
+
"also",
|
|
626
|
+
"just",
|
|
627
|
+
"like",
|
|
628
|
+
"what",
|
|
629
|
+
"which",
|
|
630
|
+
"how",
|
|
631
|
+
"should",
|
|
632
|
+
"would",
|
|
633
|
+
"could",
|
|
634
|
+
"must",
|
|
635
|
+
"does",
|
|
636
|
+
"doing",
|
|
637
|
+
"done",
|
|
638
|
+
"via",
|
|
639
|
+
// generic nouns / adjectives
|
|
640
|
+
"some",
|
|
641
|
+
"more",
|
|
642
|
+
"most",
|
|
643
|
+
"such",
|
|
644
|
+
"each",
|
|
645
|
+
"other",
|
|
646
|
+
"very",
|
|
647
|
+
"much",
|
|
648
|
+
"here",
|
|
649
|
+
"there",
|
|
650
|
+
"where",
|
|
651
|
+
"while",
|
|
652
|
+
"about",
|
|
653
|
+
"after",
|
|
654
|
+
"before",
|
|
655
|
+
"main",
|
|
656
|
+
"call",
|
|
657
|
+
"file",
|
|
658
|
+
"code",
|
|
659
|
+
"data",
|
|
660
|
+
"line",
|
|
661
|
+
"name",
|
|
662
|
+
"type",
|
|
663
|
+
"true",
|
|
664
|
+
"false",
|
|
665
|
+
"null",
|
|
666
|
+
"void",
|
|
667
|
+
"return",
|
|
668
|
+
"function",
|
|
669
|
+
"const",
|
|
670
|
+
"note",
|
|
671
|
+
"notes",
|
|
672
|
+
"result",
|
|
673
|
+
"output",
|
|
674
|
+
"input",
|
|
675
|
+
"detail",
|
|
676
|
+
"reason",
|
|
677
|
+
"reasons",
|
|
678
|
+
"improve",
|
|
679
|
+
"fix",
|
|
680
|
+
"fixed",
|
|
681
|
+
"error",
|
|
682
|
+
"issue",
|
|
683
|
+
"thing",
|
|
684
|
+
"things",
|
|
685
|
+
"stuff",
|
|
686
|
+
"case",
|
|
687
|
+
"cases",
|
|
688
|
+
"way",
|
|
689
|
+
"ways",
|
|
690
|
+
"first",
|
|
691
|
+
"second",
|
|
692
|
+
"third",
|
|
693
|
+
"last",
|
|
694
|
+
"next",
|
|
695
|
+
"prev",
|
|
696
|
+
"previous",
|
|
697
|
+
"following",
|
|
698
|
+
"above",
|
|
699
|
+
"below",
|
|
700
|
+
"since",
|
|
701
|
+
"until",
|
|
702
|
+
"without"
|
|
703
|
+
]);
|
|
519
704
|
const seen = /* @__PURE__ */ new Set();
|
|
520
705
|
const out = [];
|
|
521
|
-
for (const raw of (text ?? "").toLowerCase().split(/[^a-z0-9_]+/)) {
|
|
706
|
+
for (const raw of (text ?? "").toLowerCase().split(/[^a-z0-9_-]+/)) {
|
|
522
707
|
const t = raw.trim();
|
|
523
708
|
if (t.length < 3 || STOP.has(t) || seen.has(t)) continue;
|
|
524
709
|
seen.add(t);
|
|
525
710
|
out.push(t);
|
|
526
|
-
if (out.length >=
|
|
711
|
+
if (out.length >= 8) break;
|
|
527
712
|
}
|
|
528
713
|
return out;
|
|
529
714
|
} catch {
|
|
@@ -545,7 +730,7 @@ function readHarness(sessionID) {
|
|
|
545
730
|
function writeHarness(sessionID, h) {
|
|
546
731
|
try {
|
|
547
732
|
const f = harnessFile(sessionID);
|
|
548
|
-
mkdirSync2(
|
|
733
|
+
mkdirSync2(dirname2(f), { recursive: true });
|
|
549
734
|
h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
550
735
|
writeFileSync2(f, JSON.stringify(h, null, 2));
|
|
551
736
|
} catch {
|
|
@@ -582,7 +767,7 @@ function readInterview(sessionID) {
|
|
|
582
767
|
function writeInterview(sessionID, s) {
|
|
583
768
|
try {
|
|
584
769
|
const f = interviewFile(sessionID);
|
|
585
|
-
mkdirSync2(
|
|
770
|
+
mkdirSync2(dirname2(f), { recursive: true });
|
|
586
771
|
s.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
587
772
|
writeFileSync2(f, JSON.stringify(s, null, 2));
|
|
588
773
|
} catch {
|
package/package.json
CHANGED