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.
Files changed (2) hide show
  1. package/dist/index.js +188 -25
  2. 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 = readNodes();
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 = readNodes();
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
- return addDomainNode({
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(dirname(PIPE_LOG), { recursive: true });
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(["the", "and", "for", "with", "that", "this", "from", "into", "your", "you", "are", "was", "but", "not", "all", "any", "use", "task", "prompt"]);
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 >= 16) break;
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(dirname(f), { recursive: true });
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(dirname(f), { recursive: true });
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.11.4",
3
+ "version": "0.12.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",