opencode-usage-coach 0.9.0 → 0.10.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 +86 -11
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -909,6 +909,25 @@ function readHarnessCfg(dir) {
909
909
  ...tryRead(join2(dir, "harness.config.json"))
910
910
  };
911
911
  }
912
+ function writeHarnessCfg(updates) {
913
+ const configDir = join2(homedir(), ".config", "opencode-usage-coach");
914
+ const configPath = join2(configDir, "harness.config.json");
915
+ try {
916
+ mkdirSync2(configDir, { recursive: true });
917
+ const existing = (() => {
918
+ try {
919
+ return JSON.parse(readFileSync2(configPath, "utf8"));
920
+ } catch {
921
+ return {};
922
+ }
923
+ })();
924
+ const merged = { ...existing, ...updates };
925
+ writeFileSync2(configPath, JSON.stringify(merged, null, 2) + "\n");
926
+ return configPath;
927
+ } catch (e) {
928
+ throw new Error(`Failed to write harness config: ${String(e)}`, { cause: e });
929
+ }
930
+ }
912
931
  async function runModel(client, model, prompt, directory, track, maxSteps = DEFAULT_MAX_STEPS) {
913
932
  const t0 = Date.now();
914
933
  const subStart = Date.now();
@@ -1169,17 +1188,15 @@ function fetchQuota(provider) {
1169
1188
  });
1170
1189
  });
1171
1190
  }
1172
- function fetchQuotaWithRetry(provider, maxRetries = 3) {
1173
- return new Promise(async (resolve2) => {
1174
- for (let attempt = 0; attempt < maxRetries; attempt++) {
1175
- const q = await fetchQuota(provider);
1176
- if (q) return resolve2(q);
1177
- if (attempt < maxRetries - 1) {
1178
- await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
1179
- }
1191
+ async function fetchQuotaWithRetry(provider, maxRetries = 3) {
1192
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
1193
+ const q = await fetchQuota(provider);
1194
+ if (q) return q;
1195
+ if (attempt < maxRetries - 1) {
1196
+ await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
1180
1197
  }
1181
- resolve2(null);
1182
- });
1198
+ }
1199
+ return null;
1183
1200
  }
1184
1201
  function coach(q, lighter) {
1185
1202
  if (!q) return { decision: "GO", advice: "quota unavailable \u2014 retrying. proceeding cautiously.", weekly: -2, monthly: -2, fiveHour: -2 };
@@ -2141,6 +2158,62 @@ If the task is already well-specified with no significant ambiguities, return {"
2141
2158
  const qNum = state.currentIndex + 1;
2142
2159
  return formatQuestionOutput(qNum, total, q);
2143
2160
  }
2161
+ }),
2162
+ coach_config: tool({
2163
+ description: 'View or update harness model configuration (generator, grader, lighterModel, provider). Call with no args to view current config. Pass any combination of generator/grader/lighterModel/provider to update. Example: coach_config({ generator: "anthropic/claude-sonnet-4-20250514", grader: "opencode/mimo-v2.5-free" })',
2164
+ args: {
2165
+ generator: tool.schema.string().optional(),
2166
+ grader: tool.schema.string().optional(),
2167
+ lighterModel: tool.schema.string().optional(),
2168
+ provider: tool.schema.string().optional()
2169
+ },
2170
+ async execute(args, ctx) {
2171
+ const dir = ctx?.directory ?? input.directory;
2172
+ const current2 = readHarnessCfg(dir);
2173
+ const hasUpdates = args.generator !== void 0 || args.grader !== void 0 || args.lighterModel !== void 0 || args.provider !== void 0;
2174
+ if (!hasUpdates) {
2175
+ const envOverrides = [];
2176
+ if (process.env.UC_PROVIDER) envOverrides.push(`UC_PROVIDER=${process.env.UC_PROVIDER}`);
2177
+ if (process.env.UC_LIGHTER_MODEL) envOverrides.push(`UC_LIGHTER_MODEL=${process.env.UC_LIGHTER_MODEL}`);
2178
+ return [
2179
+ `Harness Configuration`,
2180
+ `\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
2181
+ ` generator: ${current2.generator ?? "(not set \u2014 harness won't work!)"}`,
2182
+ ` grader: ${current2.grader ?? "(defaults to generator)"}`,
2183
+ ` lighterModel: ${current2.lighterModel ?? "(not set \u2014 no THROTTLE fallback)"}`,
2184
+ ` provider: ${current2.provider ?? "(auto-detected from model)"}`,
2185
+ `\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
2186
+ envOverrides.length > 0 ? `
2187
+ Environment overrides (take precedence):
2188
+ ${envOverrides.join("\n ")}` : "",
2189
+ `
2190
+ Config file: ~/.config/opencode-usage-coach/harness.config.json`,
2191
+ `
2192
+ To update, call: coach_config({ generator: "provider/model-id", ... })`
2193
+ ].filter(Boolean).join("\n");
2194
+ }
2195
+ const updates = {};
2196
+ if (args.generator !== void 0) updates.generator = args.generator.trim();
2197
+ if (args.grader !== void 0) updates.grader = args.grader.trim();
2198
+ if (args.lighterModel !== void 0) updates.lighterModel = args.lighterModel.trim();
2199
+ if (args.provider !== void 0) updates.provider = args.provider.trim();
2200
+ const writtenPath = writeHarnessCfg(updates);
2201
+ const updated = readHarnessCfg(dir);
2202
+ return [
2203
+ `\u2705 Harness config updated successfully!`,
2204
+ `\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
2205
+ ` generator: ${updated.generator ?? "(not set)"}`,
2206
+ ` grader: ${updated.grader ?? "(defaults to generator)"}`,
2207
+ ` lighterModel: ${updated.lighterModel ?? "(not set)"}`,
2208
+ ` provider: ${updated.provider ?? "(auto-detected)"}`,
2209
+ `\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550`,
2210
+ `
2211
+ Saved to: ${writtenPath}`,
2212
+ `
2213
+ Note: Changes take effect immediately for new generate/grade calls.`,
2214
+ ` A running harness will pick up the new config on the next tool call.`
2215
+ ].join("\n");
2216
+ }
2144
2217
  })
2145
2218
  }
2146
2219
  };
@@ -2170,8 +2243,10 @@ export {
2170
2243
  providerAdvice,
2171
2244
  providerToCodexbar,
2172
2245
  readHarness,
2246
+ readHarnessCfg,
2173
2247
  readRules,
2174
2248
  setStateDir,
2175
2249
  updateSubSession,
2176
- writeHarness
2250
+ writeHarness,
2251
+ writeHarnessCfg
2177
2252
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.9.0",
3
+ "version": "0.10.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",
@@ -53,6 +53,7 @@
53
53
  "@opencode-ai/plugin": "*",
54
54
  "@opentui/core": ">=0.4.0",
55
55
  "@opentui/solid": ">=0.4.0",
56
+ "@types/node": "^26.1.1",
56
57
  "c8": "^11.0.0",
57
58
  "esbuild-plugin-solid": "^0.6.0",
58
59
  "eslint": "^10.6.0",