opencode-usage-coach 0.10.2 → 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 +204 -2
- 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);
|
|
@@ -667,7 +831,7 @@ function detectLanguage(extCounts) {
|
|
|
667
831
|
if ((extCounts["java"] || 0) > 0) return "Java";
|
|
668
832
|
return "unknown";
|
|
669
833
|
}
|
|
670
|
-
function buildGapPrompt(userRequest, tasks, profile, domainNodes) {
|
|
834
|
+
function buildGapPrompt(userRequest, tasks, profile, domainNodes, webResults, docRefs) {
|
|
671
835
|
const taskList = tasks.map((t) => `${t.id}: ${t.title}`).join("\n");
|
|
672
836
|
const profileStr = profile.skipped ? `(skipped \u2014 ${profile.reason || "unknown reason"})` : [
|
|
673
837
|
`- Language: ${profile.language}`,
|
|
@@ -680,6 +844,10 @@ function buildGapPrompt(userRequest, tasks, profile, domainNodes) {
|
|
|
680
844
|
`- Total files: ${profile.totalFiles}`
|
|
681
845
|
].join("\n");
|
|
682
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)";
|
|
683
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.
|
|
684
852
|
|
|
685
853
|
USER REQUEST:
|
|
@@ -694,6 +862,12 @@ ${profileStr}
|
|
|
694
862
|
EXISTING DOMAIN KNOWLEDGE (from local DB):
|
|
695
863
|
${domainStr}
|
|
696
864
|
|
|
865
|
+
WEB SEARCH RESULTS (from official docs references, GitHub issues, and code search):
|
|
866
|
+
${webResultsStr}
|
|
867
|
+
|
|
868
|
+
OFFICIAL DOCUMENTATION REFERENCES:
|
|
869
|
+
${docRefsStr}
|
|
870
|
+
|
|
697
871
|
Classify into EXACTLY these 4 categories:
|
|
698
872
|
|
|
699
873
|
1. KNOWN KNOWNS \u2014 requirements explicitly stated in the user request.
|
|
@@ -832,6 +1006,20 @@ Questions for the user (${r.questions.length}):`);
|
|
|
832
1006
|
L.push("\nTask Refinement Suggestions:");
|
|
833
1007
|
for (const t of r.taskRefinements) L.push(` -> task ${t.taskId}: ${t.action} - ${t.detail}`);
|
|
834
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
|
+
}
|
|
835
1023
|
if (r.rawAnalysis) L.push(`
|
|
836
1024
|
Raw analysis: ${r.rawAnalysis.slice(0, 200)}`);
|
|
837
1025
|
L.push("\n[usage-coach NEXT] unknowns reviewed:");
|
|
@@ -1487,6 +1675,18 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
1487
1675
|
} catch (e) {
|
|
1488
1676
|
log(`unknown_scan domain query err: ${String(e)}`);
|
|
1489
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
|
+
}
|
|
1490
1690
|
const cfg = readHarnessCfg(ctx.directory);
|
|
1491
1691
|
if (!cfg.generator) {
|
|
1492
1692
|
const result2 = {
|
|
@@ -1529,9 +1729,11 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
1529
1729
|
}
|
|
1530
1730
|
const throttle = decision === "THROTTLE" && cfg.lighterModel;
|
|
1531
1731
|
const model = throttle ? cfg.lighterModel : cfg.generator;
|
|
1532
|
-
const gapPrompt = buildGapPrompt(args.prompt, tasks, profile, domainNodes);
|
|
1732
|
+
const gapPrompt = buildGapPrompt(args.prompt, tasks, profile, domainNodes, webResults, docRefs);
|
|
1533
1733
|
const raw = await runModel(input.client, model, gapPrompt, ctx.directory, void 0, 15);
|
|
1534
1734
|
const result = parseGapAnalysis(raw, profile, domainHits);
|
|
1735
|
+
result.webResults = webResults;
|
|
1736
|
+
result.docRefs = docRefs;
|
|
1535
1737
|
try {
|
|
1536
1738
|
for (const uk of result.unknownKnowns.slice(0, 5)) {
|
|
1537
1739
|
const kw = extractKeywords(uk.finding);
|
package/package.json
CHANGED