opencode-usage-coach 0.11.4 → 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 +188 -25
- 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 = {
|
|
@@ -385,7 +426,7 @@ var DEFAULT_MAX_QUESTIONS = Math.max(1, Math.round(Number(process.env.UC_MAX_QUE
|
|
|
385
426
|
var PIPE_LOG = join2(homedir(), ".cache", "opencode-usage-coach", "pipeline.log");
|
|
386
427
|
function pipeLog(msg) {
|
|
387
428
|
try {
|
|
388
|
-
mkdirSync2(
|
|
429
|
+
mkdirSync2(dirname2(PIPE_LOG), { recursive: true });
|
|
389
430
|
appendFileSync2(PIPE_LOG, `[SERVER] ${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
390
431
|
`);
|
|
391
432
|
} catch {
|
|
@@ -537,15 +578,137 @@ function readImplNotesByGraph(keywords, limit = 5) {
|
|
|
537
578
|
}
|
|
538
579
|
function extractKeywords(text) {
|
|
539
580
|
try {
|
|
540
|
-
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
|
+
]);
|
|
541
704
|
const seen = /* @__PURE__ */ new Set();
|
|
542
705
|
const out = [];
|
|
543
|
-
for (const raw of (text ?? "").toLowerCase().split(/[^a-z0-9_]+/)) {
|
|
706
|
+
for (const raw of (text ?? "").toLowerCase().split(/[^a-z0-9_-]+/)) {
|
|
544
707
|
const t = raw.trim();
|
|
545
708
|
if (t.length < 3 || STOP.has(t) || seen.has(t)) continue;
|
|
546
709
|
seen.add(t);
|
|
547
710
|
out.push(t);
|
|
548
|
-
if (out.length >=
|
|
711
|
+
if (out.length >= 8) break;
|
|
549
712
|
}
|
|
550
713
|
return out;
|
|
551
714
|
} catch {
|
|
@@ -567,7 +730,7 @@ function readHarness(sessionID) {
|
|
|
567
730
|
function writeHarness(sessionID, h) {
|
|
568
731
|
try {
|
|
569
732
|
const f = harnessFile(sessionID);
|
|
570
|
-
mkdirSync2(
|
|
733
|
+
mkdirSync2(dirname2(f), { recursive: true });
|
|
571
734
|
h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
572
735
|
writeFileSync2(f, JSON.stringify(h, null, 2));
|
|
573
736
|
} catch {
|
|
@@ -604,7 +767,7 @@ function readInterview(sessionID) {
|
|
|
604
767
|
function writeInterview(sessionID, s) {
|
|
605
768
|
try {
|
|
606
769
|
const f = interviewFile(sessionID);
|
|
607
|
-
mkdirSync2(
|
|
770
|
+
mkdirSync2(dirname2(f), { recursive: true });
|
|
608
771
|
s.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
609
772
|
writeFileSync2(f, JSON.stringify(s, null, 2));
|
|
610
773
|
} catch {
|
package/package.json
CHANGED