opencode-usage-coach 0.11.2 → 0.11.4

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 +41 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -217,7 +217,7 @@ function truncate(input, max) {
217
217
  return text.length > max ? `${text.slice(0, max)}...` : text;
218
218
  }
219
219
  function sanitizeQuery(raw) {
220
- return raw.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]*`/g, " ").replace(/[#*_[\]()>|]/g, " ").replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, GH_QUERY_MAX);
220
+ return raw.replace(/```[\s\S]*?```/g, " ").replace(/`[^`]*`/g, " ").replace(/https?:\/\/\S+/g, " ").replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\b(NOT|AND|OR)\b/gi, " ").replace(/\s+/g, " ").trim().slice(0, GH_QUERY_MAX);
221
221
  }
222
222
  function effectiveFrameworks(frameworks, keyDeps) {
223
223
  const set = new Set((frameworks || []).filter(Boolean));
@@ -231,8 +231,30 @@ function effectiveFrameworks(frameworks, keyDeps) {
231
231
  async function ghFetch(url, signal) {
232
232
  const res = await fetch(url, { headers: ghHeaders(), signal });
233
233
  if (!res.ok) {
234
- const tag = res.status === 403 || res.status === 429 ? " (rate limit)" : "";
235
- throw new Error(`HTTP ${res.status}${tag}`);
234
+ let ghError = null;
235
+ let ghErrorText = "";
236
+ try {
237
+ ghErrorText = await res.text();
238
+ ghError = JSON.parse(ghErrorText);
239
+ } catch {
240
+ }
241
+ const tag = res.status === 403 || res.status === 429 ? " (rate limit)" : res.status === 422 ? " (validation failed)" : "";
242
+ const errs = ghError?.errors;
243
+ const errMsg = ghError?.message ?? ghErrorText.slice(0, 300);
244
+ const detail = errs ? errs.map((e) => typeof e === "string" ? e : `${e?.field ?? "?"}: ${e?.message ?? e?.code ?? JSON.stringify(e)}`).join("; ") : "";
245
+ console.error(JSON.stringify({
246
+ level: "error",
247
+ module: "web-search",
248
+ event: "gh-fetch-error",
249
+ status: res.status,
250
+ tag,
251
+ url,
252
+ ghMessage: errMsg,
253
+ ghErrors: detail,
254
+ rateLimitRemaining: res.headers.get("x-ratelimit-remaining"),
255
+ rateLimitReset: res.headers.get("x-ratelimit-reset")
256
+ }));
257
+ throw new Error(`HTTP ${res.status}${tag}: ${errMsg}${detail ? ` | ${detail}` : ""}`);
236
258
  }
237
259
  return await res.json();
238
260
  }
@@ -249,13 +271,15 @@ async function tier1OfficialDocs(query, fws, results, docRefs, seen, signal) {
249
271
  if (entry) docRefs.push({ name: entry.name, url: entry.docs });
250
272
  }
251
273
  if (!query) return errors;
274
+ const cleanQuery = sanitizeQuery(query);
275
+ if (!cleanQuery) return errors;
252
276
  for (const fw of fws) {
253
277
  if (signal.aborted || results.length >= TARGET_RESULT_COUNT) break;
254
278
  const entry = FRAMEWORK_DOCS[fw];
255
279
  if (!entry?.githubOrg) continue;
256
280
  try {
257
- const q = `${query} org:${entry.githubOrg}`;
258
- const url = `https://api.github.com/search/issues?q=${encodeURIComponent(sanitizeQuery(q))}&per_page=3`;
281
+ const fullQ = `${cleanQuery} org:${entry.githubOrg}`;
282
+ const url = `https://api.github.com/search/issues?q=${encodeURIComponent(fullQ)}&per_page=3`;
259
283
  const data = await ghFetch(url, signal);
260
284
  for (const item of data.items ?? []) {
261
285
  pushResult(results, seen, {
@@ -276,7 +300,9 @@ async function tier1OfficialDocs(query, fws, results, docRefs, seen, signal) {
276
300
  async function tier2GitHubIssues(query, results, seen, signal) {
277
301
  const errors = [];
278
302
  try {
279
- const url = `https://api.github.com/search/issues?q=${encodeURIComponent(sanitizeQuery(query))}&per_page=5`;
303
+ const q = sanitizeQuery(query);
304
+ if (!q) return errors;
305
+ const url = `https://api.github.com/search/issues?q=${encodeURIComponent(q)}&per_page=5`;
280
306
  const data = await ghFetch(url, signal);
281
307
  for (const item of data.items ?? []) {
282
308
  pushResult(results, seen, {
@@ -297,7 +323,9 @@ async function tier3GitHubCode(query, results, seen, signal) {
297
323
  const errors = [];
298
324
  if (!ghToken()) return errors;
299
325
  try {
300
- const url = `https://api.github.com/search/code?q=${encodeURIComponent(sanitizeQuery(query))}&per_page=3`;
326
+ const q = sanitizeQuery(query);
327
+ if (!q) return errors;
328
+ const url = `https://api.github.com/search/code?q=${encodeURIComponent(q)}&per_page=3`;
301
329
  const data = await ghFetch(url, signal);
302
330
  for (const item of data.items ?? []) {
303
331
  const repo = item.repository?.full_name;
@@ -1613,6 +1641,7 @@ PATH A \u2014 INDEPENDENT (parallel via generate_batch):
1613
1641
  4. for each i: PASS -> task_update(i, title, "completed", "PASS"); FAIL -> revise (up to 2x) or task_update(i, title, "failed", "FAIL")
1614
1642
 
1615
1643
  PATH B \u2014 DEPENDENT (sequential):
1644
+ Optional: task_update(1..N, title, "pending") \u2190 pre-register all tasks first
1616
1645
  for i in 1..${args.total}:
1617
1646
  1. task_update(i, title, "generating")
1618
1647
  2. generate({prompt:"Task: <title>. Perform it."}) -> work + NEXT
@@ -1795,25 +1824,23 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
1795
1824
  args: {
1796
1825
  id: tool.schema.number(),
1797
1826
  title: tool.schema.string(),
1798
- status: tool.schema.string().describe("generating | grading | revising | completed | failed | timed_out"),
1827
+ status: tool.schema.string().describe("pending | generating | grading | revising | completed | failed | timed_out"),
1799
1828
  revisions: tool.schema.number().optional(),
1800
1829
  score: tool.schema.string().optional().describe("PASS | FAIL"),
1801
1830
  model: tool.schema.string().optional()
1802
1831
  },
1803
1832
  async execute(args, ctx) {
1804
- const VALID_STATUSES = ["generating", "grading", "revising", "completed", "failed", "timed_out", "halted_quota"];
1805
- if (!args.status || !VALID_STATUSES.includes(args.status)) {
1806
- return `ERROR: task_update status must be one of: ${VALID_STATUSES.join(", ")}. Got: "${args.status}". Call task_update with a valid status.`;
1807
- }
1833
+ const VALID_STATUSES = ["pending", "generating", "grading", "revising", "completed", "failed", "timed_out", "halted_quota"];
1834
+ const status = args.status && VALID_STATUSES.includes(args.status) ? args.status : "generating";
1808
1835
  const cfg = readHarnessCfg(ctx.directory);
1809
1836
  const h = readHarness(ctx.sessionID) ?? { name: "batch", total: 0, current: 0, tasks: [], usage: {}, active: true };
1810
1837
  h.tasks = h.tasks.filter((x) => x.id !== args.id);
1811
1838
  const model = args.model || cfg.generator || "";
1812
1839
  if (!model) return `ERROR: task ${args.id} has no model and no generator configured. Set "generator" in harness.config.json.`;
1813
- h.tasks.push({ id: args.id, title: args.title, status: args.status, model, revisions: args.revisions ?? 0, score: args.score ?? null, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
1840
+ h.tasks.push({ id: args.id, title: args.title, status, model, revisions: args.revisions ?? 0, score: args.score ?? null, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
1814
1841
  if (args.id > h.current) h.current = args.id;
1815
1842
  writeHarness(ctx.sessionID, h);
1816
- return `task ${args.id} -> ${args.status}${args.score ? ` (${args.score})` : ""}`;
1843
+ return `task ${args.id} -> ${status}${args.score ? ` (${args.score})` : ""}`;
1817
1844
  }
1818
1845
  }),
1819
1846
  harness_done: tool({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
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",