@kodrunhq/opencode-autopilot 1.1.2 → 1.2.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/assets/commands/oc-configure.md +16 -103
- package/bin/cli.ts +9 -6
- package/bin/configure-tui.ts +338 -0
- package/package.json +5 -1
- package/src/installer.ts +48 -5
- package/src/tools/configure.ts +73 -3
|
@@ -2,113 +2,26 @@
|
|
|
2
2
|
description: Configure opencode-autopilot model assignments for each agent group
|
|
3
3
|
agent: autopilot
|
|
4
4
|
---
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Model configuration uses an interactive terminal wizard with searchable
|
|
6
|
+
model selection. This cannot be done inside the OpenCode TUI — it requires
|
|
7
|
+
a separate terminal.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
Tell the user to open a terminal and run:
|
|
9
10
|
|
|
10
|
-
Call the oc_configure tool with subcommand "start". This returns:
|
|
11
|
-
- `availableModels`: a map of provider -> list of "provider/model" strings
|
|
12
|
-
- `groups`: the 8 agent groups with descriptions and recommendations
|
|
13
|
-
- `currentConfig`: existing assignments if reconfiguring
|
|
14
|
-
- `diversityRules`: adversarial diversity constraints
|
|
15
|
-
|
|
16
|
-
If `availableModels` is empty or has no entries, tell the user:
|
|
17
|
-
"No models were discovered from your providers. Run `opencode models`
|
|
18
|
-
in your terminal to see available models, then type them manually below."
|
|
19
|
-
|
|
20
|
-
## Step 2: Build the complete model list
|
|
21
|
-
|
|
22
|
-
Combine ALL models from ALL providers into a single numbered list.
|
|
23
|
-
Every model the user has access to must appear. Do NOT filter, summarize,
|
|
24
|
-
or show only "recommended" models. The user decides — you present options.
|
|
25
|
-
|
|
26
|
-
Example (show ALL of them, not a subset). The IDs must match exactly
|
|
27
|
-
what `availableModels` returns (provider prefix comes from the provider):
|
|
28
11
|
```
|
|
29
|
-
|
|
30
|
-
1. anthropic/claude-opus-4-6
|
|
31
|
-
2. anthropic/claude-sonnet-4-6
|
|
32
|
-
3. anthropic/claude-haiku-4-5
|
|
33
|
-
4. openai/gpt-5.4
|
|
34
|
-
5. openai/gpt-5.4-mini
|
|
35
|
-
6. openai/gpt-5.4-codex
|
|
36
|
-
7. google/gemini-3.1-pro
|
|
37
|
-
8. google/gemini-3-flash
|
|
38
|
-
...
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
## Step 3: Walk through each group
|
|
42
|
-
|
|
43
|
-
For each of the 8 groups (architects first, utilities last):
|
|
44
|
-
|
|
45
|
-
1. Explain what the group does and which agents belong to it
|
|
46
|
-
2. Show the tier recommendation
|
|
47
|
-
3. For adversarial groups (challengers, reviewers, red-team): explain WHY
|
|
48
|
-
model diversity matters and which group they are adversarial to
|
|
49
|
-
4. Show the full numbered model list again whenever asking for selections
|
|
50
|
-
|
|
51
|
-
### Collecting models for each group
|
|
52
|
-
|
|
53
|
-
For each group, collect an ORDERED LIST of models (not just one):
|
|
54
|
-
|
|
55
|
-
```
|
|
56
|
-
Group: Architects
|
|
57
|
-
Pick models in priority order. The first is the primary; the rest are
|
|
58
|
-
fallbacks tried in sequence when the primary is rate-limited or fails.
|
|
59
|
-
|
|
60
|
-
Enter model numbers separated by commas (e.g. 1,4,7):
|
|
12
|
+
bunx @kodrunhq/opencode-autopilot configure
|
|
61
13
|
```
|
|
62
14
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
Parse the user's response:
|
|
71
|
-
- If they send numbers like "1,4,7": map to model IDs
|
|
72
|
-
- If they send model IDs directly: use as-is
|
|
73
|
-
- If they send a single number: that's the primary with no fallbacks
|
|
74
|
-
|
|
75
|
-
Call oc_configure with subcommand "assign":
|
|
76
|
-
- `group`: the group ID
|
|
77
|
-
- `primary`: first model from the user's list
|
|
78
|
-
- `fallbacks`: remaining models as comma-separated string
|
|
79
|
-
|
|
80
|
-
### Diversity warnings
|
|
81
|
-
|
|
82
|
-
If the assign response contains `diversityWarnings`, explain them
|
|
83
|
-
conversationally. Strong warnings should be highlighted — the user can
|
|
84
|
-
still proceed, but make the quality trade-off clear.
|
|
85
|
-
|
|
86
|
-
Example: "Heads up: Architects and Challengers both use Claude models.
|
|
87
|
-
Challengers are supposed to critique Architect decisions — using the same
|
|
88
|
-
model family means you get confirmation bias instead of genuine challenge.
|
|
89
|
-
Consider picking a different family for one of them. Continue anyway?"
|
|
90
|
-
|
|
91
|
-
## Step 4: Commit and verify
|
|
92
|
-
|
|
93
|
-
After all 8 groups are assigned, call oc_configure with subcommand "commit".
|
|
94
|
-
|
|
95
|
-
Then call oc_configure with subcommand "doctor" to verify health.
|
|
96
|
-
|
|
97
|
-
Show a final summary table:
|
|
98
|
-
|
|
99
|
-
```
|
|
100
|
-
Group | Primary | Fallbacks
|
|
101
|
-
───────────────┼──────────────────────────────┼──────────────────────────
|
|
102
|
-
Architects | anthropic/claude-opus-4-6 | openai/gpt-5.4
|
|
103
|
-
Challengers | openai/gpt-5.4 | google/gemini-3.1-pro
|
|
104
|
-
...
|
|
105
|
-
```
|
|
15
|
+
This will:
|
|
16
|
+
1. Discover all available models from their configured providers
|
|
17
|
+
2. Walk through each of the 8 agent groups with a searchable model picker
|
|
18
|
+
3. For each group, select a primary model and optional fallback models
|
|
19
|
+
4. Check adversarial diversity (different families for groups that review each other)
|
|
20
|
+
5. Save the configuration
|
|
106
21
|
|
|
107
|
-
|
|
22
|
+
After running the wizard, tell the user to restart OpenCode to pick up
|
|
23
|
+
the new model assignments.
|
|
108
24
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
- If the user says "pick for me" or "use defaults", THEN you may suggest
|
|
113
|
-
assignments based on the tier recommendations and diversity rules, but
|
|
114
|
-
still show what you picked and ask for confirmation.
|
|
25
|
+
Do NOT attempt to configure models through the oc_configure tool's assign
|
|
26
|
+
subcommand directly. The interactive wizard is the supported configuration
|
|
27
|
+
method.
|
package/bin/cli.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { diagnose } from "../src/registry/doctor";
|
|
|
10
10
|
import { ALL_GROUP_IDS, DIVERSITY_RULES, GROUP_DEFINITIONS } from "../src/registry/model-groups";
|
|
11
11
|
import type { GroupId } from "../src/registry/types";
|
|
12
12
|
import { fileExists } from "../src/utils/fs-helpers";
|
|
13
|
+
import { runConfigure } from "./configure-tui";
|
|
13
14
|
|
|
14
15
|
const execFile = promisify(execFileCb);
|
|
15
16
|
|
|
@@ -124,14 +125,12 @@ export async function runInstall(options: CliOptions = {}): Promise<void> {
|
|
|
124
125
|
console.log("");
|
|
125
126
|
console.log(bold("Next steps:"));
|
|
126
127
|
console.log("");
|
|
127
|
-
console.log("
|
|
128
|
-
console.log(" 2. Run /oc-configure to set up your model assignments");
|
|
128
|
+
console.log(" Run the interactive configuration wizard:");
|
|
129
129
|
console.log("");
|
|
130
|
-
console.log("
|
|
130
|
+
console.log(` ${bold("bunx @kodrunhq/opencode-autopilot configure")}`);
|
|
131
131
|
console.log("");
|
|
132
|
-
console.log(
|
|
133
|
-
|
|
134
|
-
);
|
|
132
|
+
console.log(" This walks through each agent group with searchable model");
|
|
133
|
+
console.log(" selection and fallback configuration.");
|
|
135
134
|
console.log("");
|
|
136
135
|
}
|
|
137
136
|
|
|
@@ -313,6 +312,7 @@ function printUsage(): void {
|
|
|
313
312
|
console.log("");
|
|
314
313
|
console.log("Commands:");
|
|
315
314
|
console.log(" install Register the plugin and create starter config");
|
|
315
|
+
console.log(" configure Interactive model assignment for each agent group");
|
|
316
316
|
console.log(" doctor Check installation health and model assignments");
|
|
317
317
|
console.log("");
|
|
318
318
|
console.log("Options:");
|
|
@@ -330,6 +330,9 @@ if (import.meta.main) {
|
|
|
330
330
|
case "install":
|
|
331
331
|
await runInstall({ noTui: args.includes("--no-tui") });
|
|
332
332
|
break;
|
|
333
|
+
case "configure":
|
|
334
|
+
await runConfigure();
|
|
335
|
+
break;
|
|
333
336
|
case "doctor":
|
|
334
337
|
await runDoctor();
|
|
335
338
|
break;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive TUI for configuring model assignments.
|
|
3
|
+
* Uses @inquirer/search (filterable single-select) and @inquirer/checkbox
|
|
4
|
+
* (multi-select) to handle 100+ models deterministically — no LLM involved.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
import checkbox, { Separator as CheckboxSeparator } from "@inquirer/checkbox";
|
|
10
|
+
import confirm from "@inquirer/confirm";
|
|
11
|
+
import search from "@inquirer/search";
|
|
12
|
+
import { CONFIG_PATH, createDefaultConfig, loadConfig, saveConfig } from "../src/config";
|
|
13
|
+
import { checkDiversity } from "../src/registry/diversity";
|
|
14
|
+
import { ALL_GROUP_IDS, DIVERSITY_RULES, GROUP_DEFINITIONS } from "../src/registry/model-groups";
|
|
15
|
+
import type { GroupId, GroupModelAssignment } from "../src/registry/types";
|
|
16
|
+
|
|
17
|
+
const execFile = promisify(execFileCb);
|
|
18
|
+
|
|
19
|
+
// ── ANSI helpers ───────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
|
|
22
|
+
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
|
|
23
|
+
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
|
|
24
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
25
|
+
const dim = (s: string) => `\x1b[2m${s}\x1b[0m`;
|
|
26
|
+
const cyan = (s: string) => `\x1b[36m${s}\x1b[0m`;
|
|
27
|
+
|
|
28
|
+
// ── Model discovery ────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
interface DiscoveredModel {
|
|
31
|
+
readonly id: string; // "provider/model"
|
|
32
|
+
readonly provider: string;
|
|
33
|
+
readonly model: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Discover available models by running `opencode models`.
|
|
38
|
+
* Each line of output is a "provider/model" string.
|
|
39
|
+
*/
|
|
40
|
+
async function discoverModels(): Promise<readonly DiscoveredModel[]> {
|
|
41
|
+
try {
|
|
42
|
+
const { stdout } = await execFile("opencode", ["models"]);
|
|
43
|
+
const lines = stdout
|
|
44
|
+
.split("\n")
|
|
45
|
+
.map((l) => l.trim())
|
|
46
|
+
.filter(Boolean);
|
|
47
|
+
|
|
48
|
+
return lines.map((id) => {
|
|
49
|
+
const slashIndex = id.indexOf("/");
|
|
50
|
+
return {
|
|
51
|
+
id,
|
|
52
|
+
provider: slashIndex > 0 ? id.slice(0, slashIndex) : "unknown",
|
|
53
|
+
model: slashIndex > 0 ? id.slice(slashIndex + 1) : id,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
} catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Group models by provider for display with separators.
|
|
63
|
+
*/
|
|
64
|
+
function groupByProvider(models: readonly DiscoveredModel[]): Map<string, DiscoveredModel[]> {
|
|
65
|
+
const grouped = new Map<string, DiscoveredModel[]>();
|
|
66
|
+
for (const m of models) {
|
|
67
|
+
const existing = grouped.get(m.provider) ?? [];
|
|
68
|
+
existing.push(m);
|
|
69
|
+
grouped.set(m.provider, existing);
|
|
70
|
+
}
|
|
71
|
+
return grouped;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── Search source for @inquirer/search ─────────────────────────────
|
|
75
|
+
|
|
76
|
+
function createSearchSource(models: readonly DiscoveredModel[], exclude?: Set<string>) {
|
|
77
|
+
const byProvider = groupByProvider(models);
|
|
78
|
+
|
|
79
|
+
return async (term: string | undefined) => {
|
|
80
|
+
const results: Array<
|
|
81
|
+
{ name: string; value: string; description: string } | typeof CheckboxSeparator.prototype
|
|
82
|
+
> = [];
|
|
83
|
+
|
|
84
|
+
for (const [provider, providerModels] of byProvider) {
|
|
85
|
+
const filtered = providerModels.filter((m) => {
|
|
86
|
+
if (exclude?.has(m.id)) return false;
|
|
87
|
+
if (!term) return true;
|
|
88
|
+
return m.id.toLowerCase().includes(term.toLowerCase());
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (filtered.length === 0) continue;
|
|
92
|
+
|
|
93
|
+
results.push(new CheckboxSeparator(`── ${provider} ──`));
|
|
94
|
+
for (const m of filtered) {
|
|
95
|
+
results.push({
|
|
96
|
+
name: m.id,
|
|
97
|
+
value: m.id,
|
|
98
|
+
description: m.model,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return results;
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Checkbox choices for fallback selection ─────────────────────────
|
|
108
|
+
|
|
109
|
+
function createCheckboxChoices(models: readonly DiscoveredModel[], excludePrimary: string) {
|
|
110
|
+
const byProvider = groupByProvider(models);
|
|
111
|
+
const choices: Array<{ name: string; value: string } | InstanceType<typeof CheckboxSeparator>> =
|
|
112
|
+
[];
|
|
113
|
+
|
|
114
|
+
for (const [provider, providerModels] of byProvider) {
|
|
115
|
+
const filtered = providerModels.filter((m) => m.id !== excludePrimary);
|
|
116
|
+
if (filtered.length === 0) continue;
|
|
117
|
+
|
|
118
|
+
choices.push(new CheckboxSeparator(`── ${provider} ──`));
|
|
119
|
+
for (const m of filtered) {
|
|
120
|
+
choices.push({
|
|
121
|
+
name: m.id,
|
|
122
|
+
value: m.id,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return choices;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── Diversity check display ────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
function extractFamily(model: string): string {
|
|
133
|
+
const slashIndex = model.indexOf("/");
|
|
134
|
+
return slashIndex > 0 ? model.slice(0, slashIndex) : model;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function showDiversityWarnings(assignments: Record<string, GroupModelAssignment>): void {
|
|
138
|
+
const warnings = checkDiversity(assignments);
|
|
139
|
+
if (warnings.length === 0) return;
|
|
140
|
+
|
|
141
|
+
console.log("");
|
|
142
|
+
for (const w of warnings) {
|
|
143
|
+
const groupLabels = w.groups.map((g) => GROUP_DEFINITIONS[g as GroupId]?.label ?? g);
|
|
144
|
+
const label = groupLabels.join(" & ");
|
|
145
|
+
const severity = w.rule.severity === "strong" ? red("WARNING") : yellow("note");
|
|
146
|
+
console.log(` ${severity}: ${label} both use ${cyan(w.sharedFamily)} family`);
|
|
147
|
+
console.log(` ${dim(w.rule.reason)}`);
|
|
148
|
+
console.log("");
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Group walkthrough ──────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
async function configureGroup(
|
|
155
|
+
groupId: GroupId,
|
|
156
|
+
models: readonly DiscoveredModel[],
|
|
157
|
+
assignments: Record<string, GroupModelAssignment>,
|
|
158
|
+
): Promise<GroupModelAssignment> {
|
|
159
|
+
const def = GROUP_DEFINITIONS[groupId];
|
|
160
|
+
|
|
161
|
+
console.log("");
|
|
162
|
+
console.log(bold(`── ${def.label} ──────────────────────────────────────`));
|
|
163
|
+
console.log(` ${dim("Purpose:")} ${def.purpose}`);
|
|
164
|
+
console.log(` ${dim("Recommendation:")} ${def.recommendation}`);
|
|
165
|
+
|
|
166
|
+
// Check if this group is adversarial to another
|
|
167
|
+
for (const rule of DIVERSITY_RULES) {
|
|
168
|
+
if (rule.groups.includes(groupId)) {
|
|
169
|
+
const others = rule.groups.filter((g) => g !== groupId);
|
|
170
|
+
const assignedOthers = others.filter((g) => assignments[g]);
|
|
171
|
+
if (assignedOthers.length > 0) {
|
|
172
|
+
const otherLabels = assignedOthers.map(
|
|
173
|
+
(g) =>
|
|
174
|
+
`${GROUP_DEFINITIONS[g as GroupId].label} (${extractFamily(assignments[g].primary)})`,
|
|
175
|
+
);
|
|
176
|
+
console.log(
|
|
177
|
+
` ${yellow("⚡")} Adversarial to: ${otherLabels.join(", ")} — pick a ${bold("different")} family`,
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
console.log("");
|
|
184
|
+
|
|
185
|
+
// Primary model — searchable select
|
|
186
|
+
const primary = await search({
|
|
187
|
+
message: `Primary model for ${def.label}:`,
|
|
188
|
+
source: createSearchSource(models),
|
|
189
|
+
pageSize: 15,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Fallback models — checkbox multi-select
|
|
193
|
+
const wantFallbacks = await confirm({
|
|
194
|
+
message: "Add fallback models? (recommended for resilience)",
|
|
195
|
+
default: true,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
let fallbacks: string[] = [];
|
|
199
|
+
if (wantFallbacks) {
|
|
200
|
+
fallbacks = await checkbox({
|
|
201
|
+
message: `Fallback models for ${def.label} (space to select, enter to confirm):`,
|
|
202
|
+
choices: createCheckboxChoices(models, primary),
|
|
203
|
+
pageSize: 15,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const assignment: GroupModelAssignment = Object.freeze({
|
|
208
|
+
primary,
|
|
209
|
+
fallbacks: Object.freeze(fallbacks),
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// Show what was selected
|
|
213
|
+
console.log(
|
|
214
|
+
` ${green("✓")} ${def.label}: ${cyan(primary)}${fallbacks.length > 0 ? ` → ${fallbacks.map(cyan).join(" → ")}` : ""}`,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
return assignment;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Main configure flow ────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
export async function runConfigure(configPath: string = CONFIG_PATH): Promise<void> {
|
|
223
|
+
console.log("");
|
|
224
|
+
console.log(bold("opencode-autopilot configure"));
|
|
225
|
+
console.log("────────────────────────────");
|
|
226
|
+
console.log("");
|
|
227
|
+
|
|
228
|
+
// 1. Discover models
|
|
229
|
+
console.log(" Discovering available models...");
|
|
230
|
+
const models = await discoverModels();
|
|
231
|
+
|
|
232
|
+
if (models.length === 0) {
|
|
233
|
+
console.log("");
|
|
234
|
+
console.log(red(" No models found."));
|
|
235
|
+
console.log(" Make sure OpenCode is installed and you have providers configured.");
|
|
236
|
+
console.log(" Run: opencode providers list");
|
|
237
|
+
console.log("");
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const byProvider = groupByProvider(models);
|
|
242
|
+
console.log(
|
|
243
|
+
` ${green("✓")} Found ${bold(String(models.length))} models from ${bold(String(byProvider.size))} providers`,
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// 2. Load existing config
|
|
247
|
+
const existingConfig = await loadConfig(configPath);
|
|
248
|
+
if (existingConfig?.configured) {
|
|
249
|
+
console.log(` ${yellow("⚠")} Existing configuration found — this will overwrite it`);
|
|
250
|
+
const proceed = await confirm({
|
|
251
|
+
message: "Continue with reconfiguration?",
|
|
252
|
+
default: true,
|
|
253
|
+
});
|
|
254
|
+
if (!proceed) {
|
|
255
|
+
console.log(" Cancelled.");
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log("");
|
|
261
|
+
console.log(bold("Walk through each agent group and assign models."));
|
|
262
|
+
console.log(dim("Type to search, arrow keys to navigate, enter to select."));
|
|
263
|
+
console.log(dim("For fallbacks: space to toggle, enter to confirm selection."));
|
|
264
|
+
|
|
265
|
+
// 3. Walk through each group
|
|
266
|
+
const assignments: Record<string, GroupModelAssignment> = {};
|
|
267
|
+
|
|
268
|
+
for (const groupId of ALL_GROUP_IDS) {
|
|
269
|
+
assignments[groupId] = await configureGroup(groupId, models, assignments);
|
|
270
|
+
showDiversityWarnings(assignments);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// 4. Show summary
|
|
274
|
+
console.log("");
|
|
275
|
+
console.log(bold("── Summary ───────────────────────────────────────────"));
|
|
276
|
+
console.log("");
|
|
277
|
+
|
|
278
|
+
const labelWidth = Math.max(...ALL_GROUP_IDS.map((id) => GROUP_DEFINITIONS[id].label.length)) + 2;
|
|
279
|
+
|
|
280
|
+
for (const groupId of ALL_GROUP_IDS) {
|
|
281
|
+
const def = GROUP_DEFINITIONS[groupId];
|
|
282
|
+
const a = assignments[groupId];
|
|
283
|
+
const label = def.label.padEnd(labelWidth);
|
|
284
|
+
const fallbackStr = a.fallbacks.length > 0 ? ` → ${a.fallbacks.join(" → ")}` : "";
|
|
285
|
+
console.log(` ${label} ${cyan(a.primary)}${dim(fallbackStr)}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log("");
|
|
289
|
+
|
|
290
|
+
// 5. Confirm and save
|
|
291
|
+
const doCommit = await confirm({
|
|
292
|
+
message: "Save this configuration?",
|
|
293
|
+
default: true,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (!doCommit) {
|
|
297
|
+
console.log(" Configuration discarded.");
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Build and save config
|
|
302
|
+
const baseConfig = existingConfig ?? createDefaultConfig();
|
|
303
|
+
const groupsRecord: Record<string, { primary: string; fallbacks: string[] }> = {};
|
|
304
|
+
for (const [key, val] of Object.entries(assignments)) {
|
|
305
|
+
groupsRecord[key] = { primary: val.primary, fallbacks: [...val.fallbacks] };
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const newConfig = {
|
|
309
|
+
...baseConfig,
|
|
310
|
+
version: 4 as const,
|
|
311
|
+
configured: true,
|
|
312
|
+
groups: groupsRecord,
|
|
313
|
+
overrides: baseConfig.overrides ?? {},
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
await saveConfig(newConfig, configPath);
|
|
317
|
+
console.log(` ${green("✓")} Configuration saved`);
|
|
318
|
+
|
|
319
|
+
// 6. Final diversity check
|
|
320
|
+
console.log("");
|
|
321
|
+
console.log(bold("Adversarial Diversity Check"));
|
|
322
|
+
const finalWarnings = checkDiversity(groupsRecord);
|
|
323
|
+
if (finalWarnings.length === 0) {
|
|
324
|
+
console.log(` ${green("✓")} All adversarial groups use different model families`);
|
|
325
|
+
} else {
|
|
326
|
+
for (const w of finalWarnings) {
|
|
327
|
+
const groupLabels = w.groups.map((g) => GROUP_DEFINITIONS[g as GroupId]?.label ?? g);
|
|
328
|
+
console.log(
|
|
329
|
+
` ${yellow("⚠")} ${groupLabels.join(" & ")}: shared ${cyan(w.sharedFamily)} family — ${dim(w.rule.reason)}`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
console.log("");
|
|
335
|
+
console.log(green("Configuration complete!"));
|
|
336
|
+
console.log(dim("Run 'bunx @kodrunhq/opencode-autopilot doctor' to verify."));
|
|
337
|
+
console.log("");
|
|
338
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kodrunhq/opencode-autopilot",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Curated agents, skills, and commands for the OpenCode AI coding CLI — autonomous orchestrator, multi-agent code review, model fallback, and in-session asset creation tools.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"keywords": [
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
},
|
|
52
52
|
"type": "module",
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"@inquirer/checkbox": "^5.1.2",
|
|
55
|
+
"@inquirer/confirm": "^6.0.10",
|
|
56
|
+
"@inquirer/search": "^4.1.6",
|
|
57
|
+
"@inquirer/select": "^5.1.2",
|
|
54
58
|
"yaml": "^2.8.3"
|
|
55
59
|
}
|
|
56
60
|
}
|
package/src/installer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { readdir, unlink } from "node:fs/promises";
|
|
2
|
-
import { join } from "node:path";
|
|
3
|
-
import { copyIfMissing, isEnoentError } from "./utils/fs-helpers";
|
|
1
|
+
import { access, copyFile, readdir, unlink } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { copyIfMissing, ensureDir, isEnoentError } from "./utils/fs-helpers";
|
|
4
4
|
import { getAssetsDir, getGlobalConfigDir } from "./utils/paths";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -9,6 +9,13 @@ import { getAssetsDir, getGlobalConfigDir } from "./utils/paths";
|
|
|
9
9
|
*/
|
|
10
10
|
const DEPRECATED_ASSETS = ["agents/placeholder-agent.md", "commands/configure.md"] as const;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Assets that must be overwritten on every install, even if the user has a copy.
|
|
14
|
+
* Used when the shipped version has critical fixes that override user customizations.
|
|
15
|
+
* Remove entries once the fix has been deployed long enough.
|
|
16
|
+
*/
|
|
17
|
+
const FORCE_UPDATE_ASSETS = ["commands/oc-configure.md"] as const;
|
|
18
|
+
|
|
12
19
|
export interface InstallResult {
|
|
13
20
|
readonly copied: readonly string[];
|
|
14
21
|
readonly skipped: readonly string[];
|
|
@@ -138,6 +145,33 @@ async function cleanupDeprecatedAssets(
|
|
|
138
145
|
return { removed, errors };
|
|
139
146
|
}
|
|
140
147
|
|
|
148
|
+
async function forceUpdateAssets(
|
|
149
|
+
sourceDir: string,
|
|
150
|
+
targetDir: string,
|
|
151
|
+
): Promise<{ readonly updated: readonly string[]; readonly errors: readonly string[] }> {
|
|
152
|
+
const updated: string[] = [];
|
|
153
|
+
const errors: string[] = [];
|
|
154
|
+
for (const asset of FORCE_UPDATE_ASSETS) {
|
|
155
|
+
const source = join(sourceDir, asset);
|
|
156
|
+
const target = join(targetDir, asset);
|
|
157
|
+
try {
|
|
158
|
+
// Verify source exists before attempting copy — skip silently if
|
|
159
|
+
// the source isn't in the bundle (test environments, partial installs)
|
|
160
|
+
await access(source);
|
|
161
|
+
await ensureDir(dirname(target));
|
|
162
|
+
await copyFile(source, target);
|
|
163
|
+
updated.push(asset);
|
|
164
|
+
} catch (error: unknown) {
|
|
165
|
+
if (!isEnoentError(error)) {
|
|
166
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
167
|
+
errors.push(`force-update ${asset}: ${message}`);
|
|
168
|
+
}
|
|
169
|
+
// ENOENT = source not in bundle, skip silently
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { updated, errors };
|
|
173
|
+
}
|
|
174
|
+
|
|
141
175
|
export async function installAssets(
|
|
142
176
|
assetsDir: string = getAssetsDir(),
|
|
143
177
|
targetDir: string = getGlobalConfigDir(),
|
|
@@ -145,6 +179,9 @@ export async function installAssets(
|
|
|
145
179
|
// Remove deprecated assets before copying new ones
|
|
146
180
|
const cleanup = await cleanupDeprecatedAssets(targetDir);
|
|
147
181
|
|
|
182
|
+
// Force-overwrite assets with critical fixes
|
|
183
|
+
const forceUpdate = await forceUpdateAssets(assetsDir, targetDir);
|
|
184
|
+
|
|
148
185
|
const [agents, commands, skills] = await Promise.all([
|
|
149
186
|
processFiles(assetsDir, targetDir, "agents"),
|
|
150
187
|
processFiles(assetsDir, targetDir, "commands"),
|
|
@@ -152,8 +189,14 @@ export async function installAssets(
|
|
|
152
189
|
]);
|
|
153
190
|
|
|
154
191
|
return {
|
|
155
|
-
copied: [...agents.copied, ...commands.copied, ...skills.copied],
|
|
192
|
+
copied: [...forceUpdate.updated, ...agents.copied, ...commands.copied, ...skills.copied],
|
|
156
193
|
skipped: [...agents.skipped, ...commands.skipped, ...skills.skipped],
|
|
157
|
-
errors: [
|
|
194
|
+
errors: [
|
|
195
|
+
...cleanup.errors,
|
|
196
|
+
...forceUpdate.errors,
|
|
197
|
+
...agents.errors,
|
|
198
|
+
...commands.errors,
|
|
199
|
+
...skills.errors,
|
|
200
|
+
],
|
|
158
201
|
};
|
|
159
202
|
}
|
package/src/tools/configure.ts
CHANGED
|
@@ -117,6 +117,37 @@ function serializeDiversityWarnings(warnings: readonly DiversityWarning[]): read
|
|
|
117
117
|
}));
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Build a flat numbered list of all available models and an index map.
|
|
122
|
+
* Returns { numberedList: "1. provider/model\n2. ...", indexMap: { "1": "provider/model", ... } }
|
|
123
|
+
*/
|
|
124
|
+
function buildNumberedModelList(modelsByProvider: Map<string, string[]>): {
|
|
125
|
+
numberedList: string;
|
|
126
|
+
indexMap: Record<string, string>;
|
|
127
|
+
totalCount: number;
|
|
128
|
+
} {
|
|
129
|
+
const allModels: string[] = [];
|
|
130
|
+
for (const models of modelsByProvider.values()) {
|
|
131
|
+
allModels.push(...models);
|
|
132
|
+
}
|
|
133
|
+
// Sort alphabetically for stable ordering
|
|
134
|
+
allModels.sort();
|
|
135
|
+
|
|
136
|
+
const indexMap: Record<string, string> = {};
|
|
137
|
+
const lines: string[] = [];
|
|
138
|
+
for (let i = 0; i < allModels.length; i++) {
|
|
139
|
+
const num = String(i + 1);
|
|
140
|
+
indexMap[num] = allModels[i];
|
|
141
|
+
lines.push(` ${num}. ${allModels[i]}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
numberedList: lines.join("\n"),
|
|
146
|
+
indexMap,
|
|
147
|
+
totalCount: allModels.length,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
120
151
|
async function handleStart(configPath?: string): Promise<string> {
|
|
121
152
|
// Wait for background provider discovery (up to 5s) before building model list
|
|
122
153
|
await Promise.race([
|
|
@@ -125,6 +156,7 @@ async function handleStart(configPath?: string): Promise<string> {
|
|
|
125
156
|
]);
|
|
126
157
|
|
|
127
158
|
const modelsByProvider = discoverAvailableModels();
|
|
159
|
+
const { numberedList, indexMap, totalCount } = buildNumberedModelList(modelsByProvider);
|
|
128
160
|
|
|
129
161
|
// Load current plugin config to show existing assignments
|
|
130
162
|
const currentConfig = await loadConfig(configPath);
|
|
@@ -148,15 +180,53 @@ async function handleStart(configPath?: string): Promise<string> {
|
|
|
148
180
|
};
|
|
149
181
|
});
|
|
150
182
|
|
|
183
|
+
// Pre-formatted text the LLM should show verbatim — avoids summarization
|
|
184
|
+
const displayText =
|
|
185
|
+
totalCount > 0
|
|
186
|
+
? [
|
|
187
|
+
`Available models (${totalCount} total):`,
|
|
188
|
+
numberedList,
|
|
189
|
+
"",
|
|
190
|
+
"For each group below, enter model numbers separated by commas (e.g. 1,4,7).",
|
|
191
|
+
"First number = primary model. Remaining = fallbacks tried in order.",
|
|
192
|
+
"More fallbacks = more resilience when a model is rate-limited.",
|
|
193
|
+
].join("\n")
|
|
194
|
+
: [
|
|
195
|
+
"No models were discovered from your providers.",
|
|
196
|
+
"Run `opencode models` in your terminal to see available models,",
|
|
197
|
+
"then type model IDs manually (e.g. anthropic/claude-opus-4-6).",
|
|
198
|
+
].join("\n");
|
|
199
|
+
|
|
200
|
+
// Compact group summaries — only fields the LLM needs for the walkthrough
|
|
201
|
+
const compactGroups = groups.map((g) => ({
|
|
202
|
+
id: g.id,
|
|
203
|
+
label: g.label,
|
|
204
|
+
purpose: g.purpose,
|
|
205
|
+
recommendation: g.recommendation,
|
|
206
|
+
agents: g.agents,
|
|
207
|
+
currentAssignment: g.currentAssignment,
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
// Compact diversity rules — just the text the LLM should mention
|
|
211
|
+
const compactRules = DIVERSITY_RULES.map((r) => ({
|
|
212
|
+
groups: r.groups,
|
|
213
|
+
severity: r.severity,
|
|
214
|
+
reason: r.reason,
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
// NOTE: availableModels is intentionally excluded — it's redundant with
|
|
218
|
+
// displayText/modelIndex and can be 400KB+ with many providers, causing
|
|
219
|
+
// OpenCode to truncate the tool output and lose everything after it.
|
|
151
220
|
return JSON.stringify({
|
|
152
221
|
action: "configure",
|
|
153
222
|
stage: "start",
|
|
154
|
-
|
|
155
|
-
|
|
223
|
+
displayText,
|
|
224
|
+
modelIndex: indexMap,
|
|
225
|
+
groups: compactGroups,
|
|
226
|
+
diversityRules: compactRules,
|
|
156
227
|
currentConfig: currentConfig
|
|
157
228
|
? { configured: currentConfig.configured, groups: currentConfig.groups }
|
|
158
229
|
: null,
|
|
159
|
-
diversityRules: DIVERSITY_RULES,
|
|
160
230
|
});
|
|
161
231
|
}
|
|
162
232
|
|