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.
- package/README.md +15 -5
- package/dist/index.js +140 -6
- 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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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}:
|
|
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
|
-
|
|
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,
|
|
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
|
|
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;
|