opencode-usage-coach 0.10.1 → 0.11.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 +225 -16
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -179,6 +179,170 @@ function saveInvestigationResult(keywords, result, source, confidence = 0.7) {
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
// src/web-search.ts
|
|
183
|
+
var FRAMEWORK_DOCS = {
|
|
184
|
+
"react": { name: "React", docs: "https://react.dev", githubOrg: "facebook" },
|
|
185
|
+
"solid-js": { name: "Solid.js", docs: "https://solidjs.com", githubOrg: "solidjs" },
|
|
186
|
+
"vue": { name: "Vue", docs: "https://vuejs.org", githubOrg: "vuejs" },
|
|
187
|
+
"svelte": { name: "Svelte", docs: "https://svelte.dev", githubOrg: "sveltejs" },
|
|
188
|
+
"express": { name: "Express", docs: "https://expressjs.com", githubOrg: "expressjs" },
|
|
189
|
+
"fastify": { name: "Fastify", docs: "https://fastify.dev", githubOrg: "fastify" },
|
|
190
|
+
"hono": { name: "Hono", docs: "https://hono.dev", githubOrg: "honojs" },
|
|
191
|
+
"vitest": { name: "Vitest", docs: "https://vitest.dev", githubOrg: "vitest-dev" },
|
|
192
|
+
"jest": { name: "Jest", docs: "https://jestjs.io", githubOrg: "jestjs" },
|
|
193
|
+
"tsup": { name: "tsup", docs: "https://tsup.egoist.dev", githubOrg: "egoist" },
|
|
194
|
+
"eslint": { name: "ESLint", docs: "https://eslint.org", githubOrg: "eslint" },
|
|
195
|
+
"cloudflare": { name: "Cloudflare", docs: "https://developers.cloudflare.com", githubOrg: "cloudflare" }
|
|
196
|
+
};
|
|
197
|
+
var DEFAULT_TIMEOUT_MS = 8e3;
|
|
198
|
+
var TARGET_RESULT_COUNT = 5;
|
|
199
|
+
function ghToken() {
|
|
200
|
+
return process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
|
201
|
+
}
|
|
202
|
+
function ghHeaders() {
|
|
203
|
+
const headers = {
|
|
204
|
+
"Accept": "application/vnd.github.v3+json",
|
|
205
|
+
"User-Agent": "opencode-usage-coach"
|
|
206
|
+
};
|
|
207
|
+
const token = ghToken();
|
|
208
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
209
|
+
return headers;
|
|
210
|
+
}
|
|
211
|
+
function errMessage(e) {
|
|
212
|
+
return e instanceof Error ? e.message : String(e);
|
|
213
|
+
}
|
|
214
|
+
function truncate(input, max) {
|
|
215
|
+
const text = (input ?? "").replace(/[\r\n]+/g, " ").trim();
|
|
216
|
+
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
217
|
+
}
|
|
218
|
+
function effectiveFrameworks(frameworks, keyDeps) {
|
|
219
|
+
const set = new Set((frameworks || []).filter(Boolean));
|
|
220
|
+
if (keyDeps) {
|
|
221
|
+
for (const dep of keyDeps) {
|
|
222
|
+
if (dep === "wrangler" || dep.startsWith("@cloudflare/")) set.add("cloudflare");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return [...set];
|
|
226
|
+
}
|
|
227
|
+
async function ghFetch(url, signal) {
|
|
228
|
+
const res = await fetch(url, { headers: ghHeaders(), signal });
|
|
229
|
+
if (!res.ok) {
|
|
230
|
+
const tag = res.status === 403 || res.status === 429 ? " (rate limit)" : "";
|
|
231
|
+
throw new Error(`HTTP ${res.status}${tag}`);
|
|
232
|
+
}
|
|
233
|
+
return await res.json();
|
|
234
|
+
}
|
|
235
|
+
function pushResult(results, seen, r) {
|
|
236
|
+
if (r.url && !seen.has(r.url)) {
|
|
237
|
+
seen.add(r.url);
|
|
238
|
+
results.push(r);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async function tier1OfficialDocs(query, fws, results, docRefs, seen, signal) {
|
|
242
|
+
const errors = [];
|
|
243
|
+
for (const fw of fws) {
|
|
244
|
+
const entry = FRAMEWORK_DOCS[fw];
|
|
245
|
+
if (entry) docRefs.push({ name: entry.name, url: entry.docs });
|
|
246
|
+
}
|
|
247
|
+
if (!query) return errors;
|
|
248
|
+
for (const fw of fws) {
|
|
249
|
+
if (signal.aborted || results.length >= TARGET_RESULT_COUNT) break;
|
|
250
|
+
const entry = FRAMEWORK_DOCS[fw];
|
|
251
|
+
if (!entry?.githubOrg) continue;
|
|
252
|
+
try {
|
|
253
|
+
const q = `${query} org:${entry.githubOrg}`;
|
|
254
|
+
const url = `https://api.github.com/search/issues?q=${encodeURIComponent(q)}&per_page=3`;
|
|
255
|
+
const data = await ghFetch(url, signal);
|
|
256
|
+
for (const item of data.items ?? []) {
|
|
257
|
+
pushResult(results, seen, {
|
|
258
|
+
tier: "official-docs",
|
|
259
|
+
title: item.title,
|
|
260
|
+
url: item.html_url,
|
|
261
|
+
snippet: truncate(item.body, 200)
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
} catch (e) {
|
|
265
|
+
const m = errMessage(e);
|
|
266
|
+
console.error(`[web-search] tier1 ${fw}: ${m}`);
|
|
267
|
+
errors.push(`tier1:${fw}:${m.slice(0, 80)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return errors;
|
|
271
|
+
}
|
|
272
|
+
async function tier2GitHubIssues(query, results, seen, signal) {
|
|
273
|
+
const errors = [];
|
|
274
|
+
try {
|
|
275
|
+
const url = `https://api.github.com/search/issues?q=${encodeURIComponent(query)}&sort=relevance&per_page=5`;
|
|
276
|
+
const data = await ghFetch(url, signal);
|
|
277
|
+
for (const item of data.items ?? []) {
|
|
278
|
+
pushResult(results, seen, {
|
|
279
|
+
tier: "github-issues",
|
|
280
|
+
title: item.title,
|
|
281
|
+
url: item.html_url,
|
|
282
|
+
snippet: truncate(item.body, 200)
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
} catch (e) {
|
|
286
|
+
const m = errMessage(e);
|
|
287
|
+
console.error(`[web-search] tier2: ${m}`);
|
|
288
|
+
errors.push(`tier2:${m.slice(0, 80)}`);
|
|
289
|
+
}
|
|
290
|
+
return errors;
|
|
291
|
+
}
|
|
292
|
+
async function tier3GitHubCode(query, results, seen, signal) {
|
|
293
|
+
const errors = [];
|
|
294
|
+
if (!ghToken()) return errors;
|
|
295
|
+
try {
|
|
296
|
+
const url = `https://api.github.com/search/code?q=${encodeURIComponent(query)}&per_page=3`;
|
|
297
|
+
const data = await ghFetch(url, signal);
|
|
298
|
+
for (const item of data.items ?? []) {
|
|
299
|
+
const repo = item.repository?.full_name;
|
|
300
|
+
pushResult(results, seen, {
|
|
301
|
+
tier: "github-code",
|
|
302
|
+
title: item.path || item.name,
|
|
303
|
+
url: item.html_url,
|
|
304
|
+
snippet: repo ? `${repo} \u2014 ${item.path}` : item.path
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
} catch (e) {
|
|
308
|
+
const m = errMessage(e);
|
|
309
|
+
console.error(`[web-search] tier3: ${m}`);
|
|
310
|
+
errors.push(`tier3:${m.slice(0, 80)}`);
|
|
311
|
+
}
|
|
312
|
+
return errors;
|
|
313
|
+
}
|
|
314
|
+
async function searchContext(query, frameworks, keyDeps, timeoutMs) {
|
|
315
|
+
const results = [];
|
|
316
|
+
const docRefs = [];
|
|
317
|
+
const errors = [];
|
|
318
|
+
const seen = /* @__PURE__ */ new Set();
|
|
319
|
+
const timeout = Math.max(100, Number(timeoutMs ?? DEFAULT_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS);
|
|
320
|
+
const controller = new AbortController();
|
|
321
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
322
|
+
const signal = controller.signal;
|
|
323
|
+
try {
|
|
324
|
+
const q = (query || "").trim();
|
|
325
|
+
const fws = effectiveFrameworks(frameworks, keyDeps);
|
|
326
|
+
errors.push(...await tier1OfficialDocs(q, fws, results, docRefs, seen, signal));
|
|
327
|
+
if (q && results.length < TARGET_RESULT_COUNT && !signal.aborted) {
|
|
328
|
+
errors.push(...await tier2GitHubIssues(q, results, seen, signal));
|
|
329
|
+
}
|
|
330
|
+
if (q && results.length < TARGET_RESULT_COUNT && !signal.aborted) {
|
|
331
|
+
errors.push(...await tier3GitHubCode(q, results, seen, signal));
|
|
332
|
+
}
|
|
333
|
+
} catch (e) {
|
|
334
|
+
const m = errMessage(e);
|
|
335
|
+
console.error(`[web-search] unexpected error: ${m}`);
|
|
336
|
+
errors.push(`unexpected:${m.slice(0, 80)}`);
|
|
337
|
+
} finally {
|
|
338
|
+
clearTimeout(timer);
|
|
339
|
+
}
|
|
340
|
+
const response = { results, docRefs };
|
|
341
|
+
const joined = errors.filter(Boolean).join("; ");
|
|
342
|
+
if (joined) response.error = joined;
|
|
343
|
+
return response;
|
|
344
|
+
}
|
|
345
|
+
|
|
182
346
|
// src/index.ts
|
|
183
347
|
var PLUGIN_NAME = "opencode-usage-coach";
|
|
184
348
|
var TTL_MS = Number(process.env.UC_TTL_MS ?? 6e4);
|
|
@@ -377,6 +541,22 @@ function writeHarness(sessionID, h) {
|
|
|
377
541
|
} catch {
|
|
378
542
|
}
|
|
379
543
|
}
|
|
544
|
+
var harnessQueue = /* @__PURE__ */ new Map();
|
|
545
|
+
function mutateHarness(sessionID, fn) {
|
|
546
|
+
const prev = harnessQueue.get(sessionID) ?? Promise.resolve();
|
|
547
|
+
const next = prev.then(() => {
|
|
548
|
+
const h = readHarness(sessionID);
|
|
549
|
+
if (!h) return;
|
|
550
|
+
const result = fn(h);
|
|
551
|
+
writeHarness(sessionID, result ?? h);
|
|
552
|
+
}).catch(() => {
|
|
553
|
+
});
|
|
554
|
+
harnessQueue.set(sessionID, next);
|
|
555
|
+
next.finally(() => {
|
|
556
|
+
if (harnessQueue.get(sessionID) === next) harnessQueue.delete(sessionID);
|
|
557
|
+
});
|
|
558
|
+
return next;
|
|
559
|
+
}
|
|
380
560
|
function interviewFile(sessionID) {
|
|
381
561
|
return join2(STATE_DIR, sessionID || "_default", "interview.json");
|
|
382
562
|
}
|
|
@@ -508,29 +688,20 @@ Output JSON ONLY (no markdown fences, no prose):
|
|
|
508
688
|
return formatCompleteOutput(state);
|
|
509
689
|
}
|
|
510
690
|
function updateSubSession(sessionID, taskId, fields) {
|
|
511
|
-
|
|
512
|
-
const h = readHarness(sessionID);
|
|
513
|
-
if (!h) return;
|
|
691
|
+
return mutateHarness(sessionID, (h) => {
|
|
514
692
|
const t = h.tasks.find((x) => x.id === taskId);
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
writeHarness(sessionID, h);
|
|
518
|
-
} catch {
|
|
519
|
-
}
|
|
693
|
+
if (t) Object.assign(t, fields);
|
|
694
|
+
});
|
|
520
695
|
}
|
|
521
696
|
function clearSubSession(sessionID, taskId) {
|
|
522
|
-
|
|
523
|
-
const h = readHarness(sessionID);
|
|
524
|
-
if (!h) return;
|
|
697
|
+
return mutateHarness(sessionID, (h) => {
|
|
525
698
|
const t = h.tasks.find((x) => x.id === taskId);
|
|
526
699
|
if (!t) return;
|
|
527
700
|
t.subSessionId = void 0;
|
|
528
701
|
t.subStep = void 0;
|
|
529
702
|
t.lastActivity = void 0;
|
|
530
703
|
t.subElapsed = void 0;
|
|
531
|
-
|
|
532
|
-
} catch {
|
|
533
|
-
}
|
|
704
|
+
});
|
|
534
705
|
}
|
|
535
706
|
function findActiveTaskId(sessionID, status) {
|
|
536
707
|
try {
|
|
@@ -660,7 +831,7 @@ function detectLanguage(extCounts) {
|
|
|
660
831
|
if ((extCounts["java"] || 0) > 0) return "Java";
|
|
661
832
|
return "unknown";
|
|
662
833
|
}
|
|
663
|
-
function buildGapPrompt(userRequest, tasks, profile, domainNodes) {
|
|
834
|
+
function buildGapPrompt(userRequest, tasks, profile, domainNodes, webResults, docRefs) {
|
|
664
835
|
const taskList = tasks.map((t) => `${t.id}: ${t.title}`).join("\n");
|
|
665
836
|
const profileStr = profile.skipped ? `(skipped \u2014 ${profile.reason || "unknown reason"})` : [
|
|
666
837
|
`- Language: ${profile.language}`,
|
|
@@ -673,6 +844,10 @@ function buildGapPrompt(userRequest, tasks, profile, domainNodes) {
|
|
|
673
844
|
`- Total files: ${profile.totalFiles}`
|
|
674
845
|
].join("\n");
|
|
675
846
|
const domainStr = domainNodes.length ? domainNodes.slice(0, 15).map((n) => `- ${n.name}: ${JSON.stringify(n.props).slice(0, 200)}`).join("\n") : "(empty \u2014 no prior knowledge stored)";
|
|
847
|
+
const webResultsStr = webResults && webResults.length ? webResults.slice(0, 8).map((r, i) => `${i + 1}. [${r.tier}] ${r.title}
|
|
848
|
+
${r.url}
|
|
849
|
+
${r.snippet}`).join("\n") : "(no web results \u2014 either network issue or no relevant findings)";
|
|
850
|
+
const docRefsStr = docRefs && docRefs.length ? docRefs.map((d) => `- ${d.name}: ${d.url}`).join("\n") : "(none)";
|
|
676
851
|
return `You are a pre-flight gap analyst. Compare the user's request (the MAP) with the actual codebase (the TERRITORY) and classify every gap.
|
|
677
852
|
|
|
678
853
|
USER REQUEST:
|
|
@@ -687,6 +862,12 @@ ${profileStr}
|
|
|
687
862
|
EXISTING DOMAIN KNOWLEDGE (from local DB):
|
|
688
863
|
${domainStr}
|
|
689
864
|
|
|
865
|
+
WEB SEARCH RESULTS (from official docs references, GitHub issues, and code search):
|
|
866
|
+
${webResultsStr}
|
|
867
|
+
|
|
868
|
+
OFFICIAL DOCUMENTATION REFERENCES:
|
|
869
|
+
${docRefsStr}
|
|
870
|
+
|
|
690
871
|
Classify into EXACTLY these 4 categories:
|
|
691
872
|
|
|
692
873
|
1. KNOWN KNOWNS \u2014 requirements explicitly stated in the user request.
|
|
@@ -825,6 +1006,20 @@ Questions for the user (${r.questions.length}):`);
|
|
|
825
1006
|
L.push("\nTask Refinement Suggestions:");
|
|
826
1007
|
for (const t of r.taskRefinements) L.push(` -> task ${t.taskId}: ${t.action} - ${t.detail}`);
|
|
827
1008
|
}
|
|
1009
|
+
if (r.webResults && r.webResults.length) {
|
|
1010
|
+
L.push(`
|
|
1011
|
+
WEB SEARCH (${r.webResults.length} results):`);
|
|
1012
|
+
if (r.docRefs && r.docRefs.length) {
|
|
1013
|
+
L.push(` Official docs: ${r.docRefs.map((d) => d.name).join(", ")}`);
|
|
1014
|
+
}
|
|
1015
|
+
L.push(" Top results:");
|
|
1016
|
+
for (let i = 0; i < Math.min(r.webResults.length, 5); i++) {
|
|
1017
|
+
const w = r.webResults[i];
|
|
1018
|
+
L.push(` ${i + 1}. [${w.tier}] ${w.title}`);
|
|
1019
|
+
L.push(` ${w.url}`);
|
|
1020
|
+
L.push(` ${w.snippet.slice(0, 100)}`);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
828
1023
|
if (r.rawAnalysis) L.push(`
|
|
829
1024
|
Raw analysis: ${r.rawAnalysis.slice(0, 200)}`);
|
|
830
1025
|
L.push("\n[usage-coach NEXT] unknowns reviewed:");
|
|
@@ -1480,6 +1675,18 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
1480
1675
|
} catch (e) {
|
|
1481
1676
|
log(`unknown_scan domain query err: ${String(e)}`);
|
|
1482
1677
|
}
|
|
1678
|
+
let webResults = [];
|
|
1679
|
+
let docRefs = [];
|
|
1680
|
+
try {
|
|
1681
|
+
const searchQuery = `${args.prompt} ${tasks.map((t) => t.title).join(" ")}`.slice(0, 500);
|
|
1682
|
+
const webResp = await searchContext(searchQuery, profile.frameworks, profile.keyDeps);
|
|
1683
|
+
webResults = webResp.results;
|
|
1684
|
+
docRefs = webResp.docRefs;
|
|
1685
|
+
if (webResp.error) log(`unknown_scan web search: ${webResp.error}`);
|
|
1686
|
+
log(`unknown_scan web search: ${webResults.length} results, ${docRefs.length} doc refs`);
|
|
1687
|
+
} catch (e) {
|
|
1688
|
+
log(`unknown_scan web search err: ${String(e).slice(0, 200)}`);
|
|
1689
|
+
}
|
|
1483
1690
|
const cfg = readHarnessCfg(ctx.directory);
|
|
1484
1691
|
if (!cfg.generator) {
|
|
1485
1692
|
const result2 = {
|
|
@@ -1522,9 +1729,11 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
1522
1729
|
}
|
|
1523
1730
|
const throttle = decision === "THROTTLE" && cfg.lighterModel;
|
|
1524
1731
|
const model = throttle ? cfg.lighterModel : cfg.generator;
|
|
1525
|
-
const gapPrompt = buildGapPrompt(args.prompt, tasks, profile, domainNodes);
|
|
1732
|
+
const gapPrompt = buildGapPrompt(args.prompt, tasks, profile, domainNodes, webResults, docRefs);
|
|
1526
1733
|
const raw = await runModel(input.client, model, gapPrompt, ctx.directory, void 0, 15);
|
|
1527
1734
|
const result = parseGapAnalysis(raw, profile, domainHits);
|
|
1735
|
+
result.webResults = webResults;
|
|
1736
|
+
result.docRefs = docRefs;
|
|
1528
1737
|
try {
|
|
1529
1738
|
for (const uk of result.unknownKnowns.slice(0, 5)) {
|
|
1530
1739
|
const kw = extractKeywords(uk.finding);
|
package/package.json
CHANGED