oc-blackbytes 0.4.1 → 0.6.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 (3) hide show
  1. package/README.md +15 -5
  2. package/dist/index.js +140 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@ An OpenCode plugin for workflow automation. It provisions built-in MCP servers,
6
6
 
7
7
  The plugin wires five OpenCode hook surfaces:
8
8
 
9
- - `config` — merges built-in MCP servers and agents into the active OpenCode config
9
+ - `config` — merges built-in MCP servers, agents, and commands into the active OpenCode config
10
10
  - `chat.headers` — injects `x-initiator: agent` for supported GitHub Copilot providers
11
11
  - `tool` — registers bundled local tools for structured editing and codebase search
12
12
  - `tool.execute.after` — post-processes `read`/`write` output when hashline editing is enabled
@@ -20,7 +20,8 @@ The plugin wires five OpenCode hook surfaces:
20
20
  - **Runtime model parameter adaptation** — the `chat.params` hook detects the actual model family at inference time and applies provider-correct parameters (Claude thinking, OpenAI reasoning effort) while stripping incompatible options
21
21
  - **Local tool registration** — exposes `hashline_edit`, `ast_grep_search`, `ast_grep_replace`, `grep`, and `glob`
22
22
  - **Hashline editing workflow** — transforms `read` output into `LINE#ID` anchors and turns successful `write` output into concise line-count summaries
23
- - **Config merging pipeline** — merges built-in MCPs and agents with user-defined config while preserving explicit user disables
23
+ - **Config merging pipeline** — merges built-in MCPs, agents, and commands with user-defined config while preserving explicit user disables
24
+ - **Built-in commands** — provides `/setup-models` for interactive model configuration setup
24
25
  - **JSONC config loading** — reads `oc-blackbytes.json` or `oc-blackbytes.jsonc` with comments and trailing commas
25
26
  - **Structured logging** — buffers plugin logs to `/tmp/oc-blackbytes.log`
26
27
  - **Binary auto-installation** — downloads cached search binaries when needed for bundled tools
@@ -97,11 +98,11 @@ Create `oc-blackbytes.jsonc` in the OpenCode config directory. For the full conf
97
98
  | `disabled_tools` | `string[]` | `[]` | Prevents bundled tools from being registered. |
98
99
  | `mcp_env_alllowlist` | `string[]` | `[]` | Recognized by the schema for MCP environment filtering workflows. |
99
100
  | `hashline_edit` | `boolean` | `true` | Enables the `hashline_edit` tool and `tool.execute.after` hashline post-processing for `read`/`write`. |
100
- | `model_fallback` | `boolean` | `false` | Enables model fallback resolution: discovers connected providers at init and resolves fallback chains when a preferred model's provider is unavailable. |
101
+ | `model_fallback` | `boolean` | `false` | Enables model fallback resolution: discovers connected providers at init and resolves fallback chains when a preferred model's provider is unavailable. Set to `true` to enable. |
101
102
  | `auto_update` | `boolean` | `false` | Recognized by the schema for maintenance workflows. |
102
103
  | `websearch.provider` | `"exa" \| "tavily"` | `"exa"` | Selects the built-in `websearch` MCP backend. |
103
104
  | `agents` | `Record<string, AgentModelConfig>` | `{}` | Per-agent model configuration overrides. See [Per-agent model configuration](#per-agent-model-configuration). |
104
- | `fallback_models` | `string \| (string \| FallbackModelObject)[]` | — | Global fallback model chain. When `model_fallback: true` and an agent's primary model is unavailable, the plugin walks this chain and uses the first model whose provider is connected. |
105
+ | `fallback_models` | `string \| (string \| FallbackModelObject)[]` | — | Global fallback model chain. When an agent's primary model is unavailable, the plugin walks this chain and uses the first model whose provider is connected. |
105
106
  | `_migrations` | `string[]` | `[]` | Internal migration bookkeeping. |
106
107
 
107
108
  ## Built-in agents
@@ -118,6 +119,14 @@ The plugin merges these agents into the OpenCode config and sets `default_agent`
118
119
 
119
120
  The merge behavior also preserves explicit user disables (`disable: true`), removes entries listed in `disabled_agents`, uses the OpenCode `permission` map format, and marks OpenCode's default `build` and `plan` agents as disabled unless the user configures them directly.
120
121
 
122
+ ## Built-in commands
123
+
124
+ The plugin registers these built-in slash commands into the OpenCode config:
125
+
126
+ | Command | Description |
127
+ |---|---|
128
+ | `/setup-models` | Interactive wizard that discovers available models, recommends optimal assignments per agent role, and writes the configuration to `oc-blackbytes.jsonc`. |
129
+
121
130
  ### Per-agent model configuration
122
131
 
123
132
  The `agents` field accepts a record of agent names to model configuration objects:
@@ -150,7 +159,7 @@ Each agent model config supports:
150
159
  | `model` | `string` | Model identifier (e.g., `"openai/gpt-5.4"`). Drives prompt variant selection and, for subagents, sets the model hint. |
151
160
  | `reasoningEffort` | `string` | Override reasoning effort level for OpenAI reasoning models (`"low"`, `"medium"`, `"high"`). |
152
161
  | `temperature` | `number` | Override temperature for the agent. |
153
- | `fallback_models` | `string \| (string \| object)[]` | Per-agent fallback chain — tried before the global `fallback_models` when the primary model's provider is unavailable. Requires `model_fallback: true`. |
162
+ | `fallback_models` | `string \| (string \| object)[]` | Per-agent fallback chain — tried before the global `fallback_models` when the primary model's provider is unavailable. |
154
163
 
155
164
  When a `model` is specified for a subagent, the factory selects the appropriate prompt variant for that model family (Claude, GPT, or Gemini). The primary agent (`bytes`) uses the model hint for prompt selection only — the actual model is determined by the OpenCode UI selection. For recommended models per agent, see [docs/configuration.md](docs/configuration.md).
156
165
 
@@ -303,6 +312,7 @@ oc-blackbytes/
303
312
  │ │ ├── hooks/ # Hook-related extension helpers
304
313
  │ │ ├── mcp/ # Built-in MCP server configs
305
314
  │ │ ├── skills/ # Skill extension entrypoints
315
+ │ │ ├── commands/ # Built-in slash commands (setup-models)
306
316
  │ │ └── tools/ # hashline_edit, ast-grep, grep, glob
307
317
  │ ├── shared/ # Logger, constants, config path resolution, JSONC utils
308
318
  │ ├── compat/ # Compatibility integrations
package/dist/index.js CHANGED
@@ -2314,8 +2314,15 @@ var BUILTIN_FALLBACK_CHAINS = {
2314
2314
  };
2315
2315
  // src/services/model-resolver.ts
2316
2316
  async function discoverAvailableModels(client) {
2317
+ let timeoutId;
2317
2318
  try {
2318
- const result = await client.provider.list();
2319
+ const result = await Promise.race([
2320
+ client.provider.list(),
2321
+ new Promise((_, reject) => {
2322
+ timeoutId = setTimeout(() => reject(new Error("Provider discovery timed out")), 20000);
2323
+ })
2324
+ ]);
2325
+ clearTimeout(timeoutId);
2319
2326
  const data = result.data;
2320
2327
  if (!data) {
2321
2328
  log("[model-resolver] Provider list returned no data, skipping fallback resolution");
@@ -2335,6 +2342,7 @@ async function discoverAvailableModels(client) {
2335
2342
  log(`[model-resolver] Available: ${providerSummary || "(none)"}`);
2336
2343
  return models;
2337
2344
  } catch (e) {
2345
+ clearTimeout(timeoutId);
2338
2346
  log(`[model-resolver] Failed to discover providers: ${e}`);
2339
2347
  return new Map;
2340
2348
  }
@@ -2433,6 +2441,7 @@ function resolveAgentModel(agentName, agentConfig, globalFallbacks, availableMod
2433
2441
  log(` [model-resolver] ${agentName}: resolved \u2192 ${globalResolved2.model} (global fallback)`);
2434
2442
  return globalResolved2;
2435
2443
  }
2444
+ log(` [model-resolver] ${agentName}: no model configured, all fallback chains exhausted \u2192 using OpenCode default`);
2436
2445
  return { model: "", fromFallback: false };
2437
2446
  }
2438
2447
  const resolvedPrimaryModel = resolveModelRef(primaryModel, availableModels);
@@ -2456,7 +2465,7 @@ function resolveAgentModel(agentName, agentConfig, globalFallbacks, availableMod
2456
2465
  log(` [model-resolver] ${agentName}: resolved \u2192 ${globalResolved.model} (global fallback)`);
2457
2466
  return globalResolved;
2458
2467
  }
2459
- log(` [model-resolver] ${agentName}: no available fallback, using OpenCode default`);
2468
+ log(` [model-resolver] ${agentName}: primary model unavailable, all fallback chains exhausted \u2192 using OpenCode default`);
2460
2469
  return { model: "", fromFallback: false };
2461
2470
  }
2462
2471
  function resolveAllAgentModels(pluginConfig, availableModels) {
@@ -2480,6 +2489,8 @@ function resolveAllAgentModels(pluginConfig, availableModels) {
2480
2489
  ...resolution.fromFallback && resolution.temperature !== undefined && config?.temperature === undefined ? { temperature: resolution.temperature } : {}
2481
2490
  };
2482
2491
  }
2492
+ const summary = Object.entries(resolved).map(([name, cfg]) => `${name}=${cfg.model || "(default)"}`).join(", ");
2493
+ log(`[model-resolver] Resolution complete: ${summary}`);
2483
2494
  return resolved;
2484
2495
  }
2485
2496
  // src/handlers/config-handler/agent-config-handler.ts
@@ -2496,6 +2507,7 @@ function createBuiltinAgents(agentOverrides) {
2496
2507
  const agents = {};
2497
2508
  for (const [name, factory] of Object.entries(BUILTIN_AGENT_FACTORIES)) {
2498
2509
  const modelHint = agentOverrides?.[name]?.model ?? "";
2510
+ log(` [agents] Factory '${name}': modelHint=${modelHint || "(empty)"}`);
2499
2511
  agents[name] = factory(modelHint);
2500
2512
  }
2501
2513
  if (agentOverrides) {
@@ -2558,7 +2570,14 @@ function handleAgentConfig(ctx) {
2558
2570
  const { disabled_agents: pluginDisabledAgents = [] } = ctx.pluginConfig;
2559
2571
  const userAgents = ctx.config.agent;
2560
2572
  const userDisabledAgentNames = captureUserDisabledAgents(userAgents);
2561
- const effectiveOverrides = ctx.availableModels.size > 0 ? resolveAllAgentModels(ctx.pluginConfig, ctx.availableModels) ?? ctx.pluginConfig.agents : ctx.pluginConfig.agents;
2573
+ let effectiveOverrides;
2574
+ if (ctx.availableModels.size > 0) {
2575
+ effectiveOverrides = resolveAllAgentModels(ctx.pluginConfig, ctx.availableModels) ?? ctx.pluginConfig.agents;
2576
+ log(` [agents] Model resolution: used fallback chains (${ctx.availableModels.size} provider(s) available)`);
2577
+ } else {
2578
+ effectiveOverrides = ctx.pluginConfig.agents;
2579
+ log(` [agents] Model resolution: SKIPPED (no providers discovered, using static config)`);
2580
+ }
2562
2581
  const builtinAgents = createBuiltinAgents(effectiveOverrides);
2563
2582
  const merged = {
2564
2583
  ...builtinAgents
@@ -2586,12 +2605,106 @@ function handleAgentConfig(ctx) {
2586
2605
  }
2587
2606
  const enabledCount = Object.values(merged).filter((a) => !a.disable).length;
2588
2607
  log(` Total agents enabled: ${enabledCount}`);
2608
+ for (const [name, agent] of Object.entries(merged)) {
2609
+ if (agent.disable)
2610
+ continue;
2611
+ const model2 = agent.model;
2612
+ log(` [agents] Final: ${name} \u2192 model=${model2 || "(opencode default)"}`);
2613
+ }
2589
2614
  ctx.config.agent = merged;
2590
2615
  if (!ctx.config.default_agent && merged[DEFAULT_AGENT_NAME] && !merged[DEFAULT_AGENT_NAME].disable) {
2591
2616
  ctx.config.default_agent = DEFAULT_AGENT_NAME;
2592
2617
  log(` Default agent set to: ${DEFAULT_AGENT_NAME}`);
2593
2618
  }
2594
2619
  }
2620
+ // src/extensions/commands/setup-models.ts
2621
+ var setupModels = {
2622
+ description: "Set up optimal model assignments for each agent based on available providers",
2623
+ agent: "bytes",
2624
+ template: `You are running the /setup-models command. Your job is to help the user configure optimal model assignments for the oc-blackbytes plugin.
2625
+
2626
+ ## Step 1: Discover Available Models
2627
+
2628
+ Run this command to see what models are available:
2629
+ \`\`\`
2630
+ opencode models
2631
+ \`\`\`
2632
+
2633
+ ## Step 2: Check for Existing Config
2634
+
2635
+ Before writing anything, check if the user already has a plugin config file:
2636
+ \`\`\`
2637
+ ls ~/.config/opencode/oc-blackbytes.jsonc ~/.config/opencode/oc-blackbytes.json 2>/dev/null
2638
+ \`\`\`
2639
+
2640
+ If a config file already exists, read it first and MERGE your changes with the existing settings \u2014 do not discard other fields the user may have configured (like \`disabled_mcps\`, \`disabled_tools\`, etc.).
2641
+
2642
+ ## Step 3: Analyze & Recommend
2643
+
2644
+ Based on the available models, determine the best assignment for each agent role following these guidelines:
2645
+
2646
+ ### Agent Roles & Requirements
2647
+
2648
+ | Agent | Role | Requirements | Ideal Tier |
2649
+ |-------|------|-------------|------------|
2650
+ | **bytes** | Primary coding agent | Strong reasoning, large context, code generation | Flagship (user's UI choice \u2014 do NOT set a model) |
2651
+ | **oracle** | Architecture advisor, deep debugging | Highest reasoning, complex analysis | Flagship \u2014 different provider than user's primary for diversity |
2652
+ | **explore** | Codebase search, read-only | Fast, cheap, good tool calling | Small/fast model |
2653
+ | **librarian** | Documentation research, read-only | Good tool calling, summarization | Small/fast model |
2654
+ | **general** | Multi-file implementation executor | Strong coding, moderate reasoning | Mid-tier coding model |
2655
+
2656
+ ### Model Preference (per tier)
2657
+
2658
+ **Flagship tier** (oracle): Prefer cross-provider diversity. If user's primary is Claude, prefer GPT for oracle and vice versa.
2659
+ - claude-opus-4-6, gpt-5.4, gemini-3.1-pro
2660
+
2661
+ **Mid-tier** (general): Good coding models, cost-effective.
2662
+ - claude-sonnet-4-6, gpt-5.4-mini, kimi-k2.5, gemini-3.1-pro
2663
+
2664
+ **Small/fast tier** (explore, librarian): Cheapest available with decent tool calling.
2665
+ - gemini-3-flash, claude-haiku-4-5, gpt-5-nano, minimax-m2.7
2666
+
2667
+ ### Key Rules
2668
+ 1. **bytes**: Do NOT include in the agents config \u2014 it respects the user's UI model selection
2669
+ 2. **oracle**: Pick a flagship from a DIFFERENT provider than the user's likely primary model for diversity
2670
+ 3. **explore & librarian**: Pick the cheapest/fastest available model \u2014 they are read-only search agents
2671
+ 4. **general**: Pick a solid mid-tier coding model
2672
+ 5. Only assign models from providers that are actually connected/available
2673
+ 6. Include the provider prefix (e.g., "anthropic/claude-sonnet-4-6", "openai/gpt-5.4")
2674
+
2675
+ ## Step 4: Generate & Write Config
2676
+
2677
+ Write the config as \`oc-blackbytes.jsonc\` (JSONC format \u2014 comments are supported) in the OpenCode config directory (the same directory where \`opencode.json\` or \`opencode.jsonc\` lives, typically \`~/.config/opencode/\`).
2678
+
2679
+ Use this structure:
2680
+ \`\`\`jsonc
2681
+ {
2682
+ // Enable model fallback resolution for automatic provider failover
2683
+ "model_fallback": true,
2684
+
2685
+ "agents": {
2686
+ // bytes is NOT included \u2014 it uses whatever model you select in the UI
2687
+ "oracle": { "model": "<provider>/<model>" },
2688
+ "explore": { "model": "<provider>/<model>" },
2689
+ "librarian": { "model": "<provider>/<model>" },
2690
+ "general": { "model": "<provider>/<model>" }
2691
+ }
2692
+ }
2693
+ \`\`\`
2694
+
2695
+ If an existing config file was found in Step 2, merge the \`agents\` and \`model_fallback\` fields into it, preserving all other existing fields.
2696
+
2697
+ After writing, show a summary table of what was configured and why each model was chosen.
2698
+
2699
+ **Important**: If a provider has very few models or only one flagship, prefer not to duplicate the same model across agents. Spread across providers when possible for resilience.`
2700
+ };
2701
+
2702
+ // src/extensions/commands/index.ts
2703
+ function createBuiltinCommands() {
2704
+ return {
2705
+ "setup-models": setupModels
2706
+ };
2707
+ }
2595
2708
  // src/extensions/mcp/context7.ts
2596
2709
  var context7 = {
2597
2710
  type: "remote",
@@ -2648,6 +2761,25 @@ function createBuiltinMcps(config) {
2648
2761
  mcps.grep_app = grep_app;
2649
2762
  return mcps;
2650
2763
  }
2764
+ // src/handlers/config-handler/command-config-handler.ts
2765
+ function handleCommandConfig(ctx) {
2766
+ const { config } = ctx;
2767
+ if (!config.command) {
2768
+ config.command = {};
2769
+ }
2770
+ const builtinCommands = createBuiltinCommands();
2771
+ let registered = 0;
2772
+ for (const [name, command] of Object.entries(builtinCommands)) {
2773
+ if (config.command[name]) {
2774
+ log(` [commands] Skipping '${name}': user-defined override exists`);
2775
+ continue;
2776
+ }
2777
+ config.command[name] = command;
2778
+ registered++;
2779
+ }
2780
+ log(` [commands] Registered ${registered} built-in command(s)`);
2781
+ }
2782
+
2651
2783
  // src/handlers/config-handler/mcp-config-handler.ts
2652
2784
  function isDisabledMcpEntry(value) {
2653
2785
  return typeof value === "object" && value !== null && "enabled" in value && value.enabled === false;
@@ -2711,8 +2843,10 @@ function handleConfig(pluginConfig, availableModels) {
2711
2843
  return {
2712
2844
  config: async (config) => {
2713
2845
  const configCtx = { config, pluginConfig, availableModels };
2846
+ log(`[config] Available models: ${availableModels.size} provider(s) discovered`);
2714
2847
  handleMcpConfig(configCtx);
2715
2848
  handleAgentConfig(configCtx);
2849
+ handleCommandConfig(configCtx);
2716
2850
  }
2717
2851
  };
2718
2852
  }
@@ -17515,10 +17649,10 @@ async function resolveFormatters(client, directory) {
17515
17649
  }
17516
17650
  }
17517
17651
  if (config2.experimental?.hook?.file_edited) {
17518
- for (const [ext, commands] of Object.entries(config2.experimental.hook.file_edited)) {
17652
+ for (const [ext, commands2] of Object.entries(config2.experimental.hook.file_edited)) {
17519
17653
  const normalizedExt = ext.startsWith(".") ? ext : `.${ext}`;
17520
17654
  const existing = result.get(normalizedExt) ?? [];
17521
- for (const cmd of commands) {
17655
+ for (const cmd of commands2) {
17522
17656
  existing.push({
17523
17657
  command: cmd.command,
17524
17658
  environment: cmd.environment ?? {}
@@ -31447,7 +31581,7 @@ function loadPluginConfig(_input) {
31447
31581
  // src/index.ts
31448
31582
  var BlackbytesPlugin = async (ctx) => {
31449
31583
  const pluginConfig = loadPluginConfig(ctx);
31450
- const availableModels = pluginConfig.model_fallback ? await discoverAvailableModels(ctx.client) : new Map;
31584
+ const availableModels = pluginConfig.model_fallback === true ? await discoverAvailableModels(ctx.client) : new Map;
31451
31585
  return createOpenCodePlugin({ input: ctx, pluginConfig, availableModels });
31452
31586
  };
31453
31587
  var src_default = BlackbytesPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oc-blackbytes",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "description": "An OpenCode plugin tailored to streamline my everyday workflow.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",