@kodrunhq/opencode-autopilot 0.1.0 → 1.0.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 +303 -1
- package/assets/commands/oc-configure.md +33 -0
- package/assets/logo.svg +35 -0
- package/bin/cli.ts +346 -0
- package/package.json +6 -2
- package/src/agents/index.ts +53 -19
- package/src/config.ts +114 -19
- package/src/index.ts +11 -5
- package/src/registry/diversity.ts +78 -0
- package/src/registry/doctor.ts +84 -0
- package/src/registry/model-groups.ts +163 -0
- package/src/registry/resolver.ts +74 -0
- package/src/registry/types.ts +91 -0
- package/src/tools/configure.ts +328 -0
- package/assets/agents/placeholder-agent.md +0 -13
- package/assets/commands/configure.md +0 -17
- package/src/tools/placeholder.ts +0 -11
package/bin/cli.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { execFile as execFileCb } from "node:child_process";
|
|
4
|
+
import { randomBytes } from "node:crypto";
|
|
5
|
+
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { promisify } from "node:util";
|
|
8
|
+
import { CONFIG_PATH, createDefaultConfig, loadConfig, saveConfig } from "../src/config";
|
|
9
|
+
import { diagnose } from "../src/registry/doctor";
|
|
10
|
+
import { ALL_GROUP_IDS, DIVERSITY_RULES, GROUP_DEFINITIONS } from "../src/registry/model-groups";
|
|
11
|
+
import type { GroupId } from "../src/registry/types";
|
|
12
|
+
import { fileExists } from "../src/utils/fs-helpers";
|
|
13
|
+
|
|
14
|
+
const execFile = promisify(execFileCb);
|
|
15
|
+
|
|
16
|
+
// ── ANSI color helpers (zero dependencies) ──────────────────────────
|
|
17
|
+
|
|
18
|
+
const green = (s: string) => `\x1b[32m${s}\x1b[0m`;
|
|
19
|
+
const red = (s: string) => `\x1b[31m${s}\x1b[0m`;
|
|
20
|
+
const yellow = (s: string) => `\x1b[33m${s}\x1b[0m`;
|
|
21
|
+
const bold = (s: string) => `\x1b[1m${s}\x1b[0m`;
|
|
22
|
+
|
|
23
|
+
// ── Types ───────────────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
export interface CliOptions {
|
|
26
|
+
readonly cwd?: string;
|
|
27
|
+
readonly noTui?: boolean;
|
|
28
|
+
readonly configDir?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Constants ───────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const PLUGIN_NAME = "@kodrunhq/opencode-autopilot";
|
|
34
|
+
const OPENCODE_JSON = "opencode.json";
|
|
35
|
+
|
|
36
|
+
// ── Helpers ─────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
async function checkOpenCodeInstalled(): Promise<string | null> {
|
|
39
|
+
try {
|
|
40
|
+
const { stdout } = await execFile("opencode", ["--version"]);
|
|
41
|
+
return stdout.trim();
|
|
42
|
+
} catch (error: unknown) {
|
|
43
|
+
if (
|
|
44
|
+
error instanceof Error &&
|
|
45
|
+
"code" in error &&
|
|
46
|
+
(error as NodeJS.ErrnoException).code === "ENOENT"
|
|
47
|
+
) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── runInstall ──────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export async function runInstall(options: CliOptions = {}): Promise<void> {
|
|
57
|
+
const cwd = options.cwd ?? process.cwd();
|
|
58
|
+
const configPath = options.configDir ?? CONFIG_PATH;
|
|
59
|
+
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log(bold("opencode-autopilot install"));
|
|
62
|
+
console.log("─────────────────────────");
|
|
63
|
+
console.log("");
|
|
64
|
+
|
|
65
|
+
// 1. Check OpenCode installed (warn but don't fail)
|
|
66
|
+
const version = await checkOpenCodeInstalled();
|
|
67
|
+
if (version) {
|
|
68
|
+
console.log(` ${green("✓")} OpenCode installed: ${version}`);
|
|
69
|
+
} else {
|
|
70
|
+
console.log(` ${yellow("⚠")} OpenCode not found — install from https://opencode.ai`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 2. Locate or create opencode.json
|
|
74
|
+
const jsonPath = join(cwd, OPENCODE_JSON);
|
|
75
|
+
let opencodeJson: { plugin?: string[]; [key: string]: unknown };
|
|
76
|
+
|
|
77
|
+
if (await fileExists(jsonPath)) {
|
|
78
|
+
const raw = await readFile(jsonPath, "utf-8");
|
|
79
|
+
try {
|
|
80
|
+
opencodeJson = JSON.parse(raw) as typeof opencodeJson;
|
|
81
|
+
} catch {
|
|
82
|
+
console.error(
|
|
83
|
+
` ${red("✗")} ${OPENCODE_JSON} contains invalid JSON. Please fix it and try again.`,
|
|
84
|
+
);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
console.log(` ${green("✓")} Found ${OPENCODE_JSON}`);
|
|
88
|
+
} else {
|
|
89
|
+
opencodeJson = { plugin: [] };
|
|
90
|
+
console.log(` ${green("✓")} Created ${OPENCODE_JSON}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 3. Register plugin (idempotent)
|
|
94
|
+
const existingPlugins: string[] = Array.isArray(opencodeJson.plugin) ? opencodeJson.plugin : [];
|
|
95
|
+
|
|
96
|
+
if (existingPlugins.includes(PLUGIN_NAME)) {
|
|
97
|
+
console.log(` ${green("✓")} Plugin already registered`);
|
|
98
|
+
} else {
|
|
99
|
+
opencodeJson = {
|
|
100
|
+
...opencodeJson,
|
|
101
|
+
plugin: [...existingPlugins, PLUGIN_NAME],
|
|
102
|
+
};
|
|
103
|
+
const tmpJsonPath = `${jsonPath}.tmp.${randomBytes(8).toString("hex")}`;
|
|
104
|
+
await writeFile(tmpJsonPath, JSON.stringify(opencodeJson, null, 2), "utf-8");
|
|
105
|
+
await rename(tmpJsonPath, jsonPath);
|
|
106
|
+
console.log(` ${green("✓")} Plugin registered`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 4. Create starter config (skip if exists)
|
|
110
|
+
if (await fileExists(configPath)) {
|
|
111
|
+
const config = await loadConfig(configPath);
|
|
112
|
+
if (config?.configured) {
|
|
113
|
+
console.log(` ${green("✓")} Config already configured`);
|
|
114
|
+
} else {
|
|
115
|
+
console.log(` ${yellow("⚠")} Config exists, not yet configured`);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
const defaultConfig = createDefaultConfig();
|
|
119
|
+
await saveConfig(defaultConfig, configPath);
|
|
120
|
+
console.log(` ${green("✓")} Created starter config`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// 5. Print next steps
|
|
124
|
+
console.log("");
|
|
125
|
+
console.log(bold("Next steps:"));
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log(" 1. Launch OpenCode");
|
|
128
|
+
console.log(" 2. Run /oc-configure to set up your model assignments");
|
|
129
|
+
console.log("");
|
|
130
|
+
console.log(" Or paste this into your AI session for guided setup:");
|
|
131
|
+
console.log("");
|
|
132
|
+
console.log(
|
|
133
|
+
" https://raw.githubusercontent.com/kodrunhq/opencode-autopilot/main/docs/guide/installation.md",
|
|
134
|
+
);
|
|
135
|
+
console.log("");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── runDoctor helpers ──────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
async function printSystemChecks(
|
|
141
|
+
cwd: string,
|
|
142
|
+
configPath: string,
|
|
143
|
+
): Promise<{ hasFailure: boolean; config: Awaited<ReturnType<typeof loadConfig>> }> {
|
|
144
|
+
let hasFailure = false;
|
|
145
|
+
|
|
146
|
+
console.log(bold("System"));
|
|
147
|
+
|
|
148
|
+
// 1. OpenCode installed
|
|
149
|
+
const version = await checkOpenCodeInstalled();
|
|
150
|
+
if (version) {
|
|
151
|
+
console.log(` OpenCode installed ${green("✓")} ${version}`);
|
|
152
|
+
} else {
|
|
153
|
+
console.log(
|
|
154
|
+
` OpenCode installed ${red("✗")} not found — install from https://opencode.ai`,
|
|
155
|
+
);
|
|
156
|
+
hasFailure = true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 2. Plugin registered
|
|
160
|
+
const jsonPath = join(cwd, OPENCODE_JSON);
|
|
161
|
+
if (await fileExists(jsonPath)) {
|
|
162
|
+
try {
|
|
163
|
+
const raw = await readFile(jsonPath, "utf-8");
|
|
164
|
+
const parsed = JSON.parse(raw) as { plugin?: string[] };
|
|
165
|
+
if (Array.isArray(parsed.plugin) && parsed.plugin.includes(PLUGIN_NAME)) {
|
|
166
|
+
console.log(` Plugin registered ${green("✓")} ${OPENCODE_JSON}`);
|
|
167
|
+
} else {
|
|
168
|
+
console.log(` Plugin registered ${red("✗")} not in ${OPENCODE_JSON} — run install`);
|
|
169
|
+
hasFailure = true;
|
|
170
|
+
}
|
|
171
|
+
} catch (error: unknown) {
|
|
172
|
+
if (error instanceof SyntaxError) {
|
|
173
|
+
console.log(
|
|
174
|
+
` Plugin registered ${red("✗")} invalid ${OPENCODE_JSON} — fix JSON syntax`,
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
console.log(
|
|
178
|
+
` Plugin registered ${red("✗")} could not read ${OPENCODE_JSON}: ${error instanceof Error ? error.message : String(error)}`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
hasFailure = true;
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
console.log(` Plugin registered ${red("✗")} ${OPENCODE_JSON} not found — run install`);
|
|
185
|
+
hasFailure = true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 3. Config file exists + schema valid
|
|
189
|
+
const config = await loadConfig(configPath);
|
|
190
|
+
if (config) {
|
|
191
|
+
console.log(` Config file ${green("✓")} found`);
|
|
192
|
+
console.log(` Config schema ${green("✓")} v${config.version}`);
|
|
193
|
+
} else {
|
|
194
|
+
console.log(` Config file ${red("✗")} not found — run install`);
|
|
195
|
+
hasFailure = true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 4. Setup completed
|
|
199
|
+
if (config) {
|
|
200
|
+
if (config.configured) {
|
|
201
|
+
console.log(` Setup completed ${green("✓")} configured: true`);
|
|
202
|
+
} else {
|
|
203
|
+
console.log(
|
|
204
|
+
` Setup completed ${red("✗")} configured: false — run /oc-configure in OpenCode`,
|
|
205
|
+
);
|
|
206
|
+
hasFailure = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return { hasFailure, config };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function printModelAssignments(result: ReturnType<typeof diagnose>): void {
|
|
214
|
+
console.log("");
|
|
215
|
+
console.log(bold("Model Assignments"));
|
|
216
|
+
|
|
217
|
+
if (result.configExists) {
|
|
218
|
+
for (const groupId of ALL_GROUP_IDS) {
|
|
219
|
+
const def = GROUP_DEFINITIONS[groupId];
|
|
220
|
+
const info = result.groupsAssigned[groupId];
|
|
221
|
+
const label = def.label.padEnd(20);
|
|
222
|
+
|
|
223
|
+
if (info?.assigned && info.primary) {
|
|
224
|
+
const fallbackStr = info.fallbacks.length > 0 ? ` -> ${info.fallbacks.join(", ")}` : "";
|
|
225
|
+
console.log(` ${label} ${info.primary}${fallbackStr}`);
|
|
226
|
+
} else {
|
|
227
|
+
console.log(` ${label} ${red("✗")} not assigned`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} else {
|
|
231
|
+
console.log(` ${red("✗")} no config loaded`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function printDiversityResults(
|
|
236
|
+
result: ReturnType<typeof diagnose>,
|
|
237
|
+
config: Awaited<ReturnType<typeof loadConfig>>,
|
|
238
|
+
): void {
|
|
239
|
+
console.log("");
|
|
240
|
+
console.log(bold("Adversarial Diversity"));
|
|
241
|
+
|
|
242
|
+
if (config && Object.keys(config.groups).length > 0) {
|
|
243
|
+
// Derive display rules from DIVERSITY_RULES
|
|
244
|
+
const rules = DIVERSITY_RULES.map((rule) => {
|
|
245
|
+
const groupLabels = rule.groups.map((g) => GROUP_DEFINITIONS[g as GroupId].label);
|
|
246
|
+
const label =
|
|
247
|
+
groupLabels.length === 2
|
|
248
|
+
? `${groupLabels[0]} <-> ${groupLabels[1]}`
|
|
249
|
+
: `${groupLabels[0]} <-> ${groupLabels.slice(1).join("+")}`;
|
|
250
|
+
return { label, groups: rule.groups };
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
for (const rule of rules) {
|
|
254
|
+
const key = [...rule.groups].sort().join(",");
|
|
255
|
+
const allAssigned = rule.groups.every((g) => config.groups[g]);
|
|
256
|
+
|
|
257
|
+
if (!allAssigned) {
|
|
258
|
+
console.log(` ${rule.label.padEnd(28)} ${yellow("⚠")} groups not fully assigned`);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const warning = result.diversityWarnings.find((w) => [...w.groups].sort().join(",") === key);
|
|
263
|
+
|
|
264
|
+
if (warning) {
|
|
265
|
+
console.log(
|
|
266
|
+
` ${rule.label.padEnd(28)} ${yellow("⚠")} shared family: ${warning.sharedFamily} — consider different families`,
|
|
267
|
+
);
|
|
268
|
+
} else {
|
|
269
|
+
console.log(` ${rule.label.padEnd(28)} ${green("✓")} different families`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
console.log(` ${yellow("⚠")} no model assignments to check`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// ── runDoctor ───────────────────────────────────────────────────────
|
|
278
|
+
|
|
279
|
+
export async function runDoctor(options: CliOptions = {}): Promise<void> {
|
|
280
|
+
const cwd = options.cwd ?? process.cwd();
|
|
281
|
+
const configPath = options.configDir ?? CONFIG_PATH;
|
|
282
|
+
|
|
283
|
+
console.log("");
|
|
284
|
+
console.log(bold("opencode-autopilot doctor"));
|
|
285
|
+
console.log("─────────────────────────");
|
|
286
|
+
console.log("");
|
|
287
|
+
|
|
288
|
+
const { hasFailure, config } = await printSystemChecks(cwd, configPath);
|
|
289
|
+
|
|
290
|
+
// Run shared diagnosis logic
|
|
291
|
+
const result = diagnose(config);
|
|
292
|
+
|
|
293
|
+
printModelAssignments(result);
|
|
294
|
+
printDiversityResults(result, config);
|
|
295
|
+
|
|
296
|
+
// ── Summary ────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
console.log("");
|
|
299
|
+
if (hasFailure) {
|
|
300
|
+
console.log(red("Some checks failed."));
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
} else {
|
|
303
|
+
console.log(green("All checks passed."));
|
|
304
|
+
}
|
|
305
|
+
console.log("");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── Help ────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
function printUsage(): void {
|
|
311
|
+
console.log("");
|
|
312
|
+
console.log(bold("Usage:") + " opencode-autopilot <command>");
|
|
313
|
+
console.log("");
|
|
314
|
+
console.log("Commands:");
|
|
315
|
+
console.log(" install Register the plugin and create starter config");
|
|
316
|
+
console.log(" doctor Check installation health and model assignments");
|
|
317
|
+
console.log("");
|
|
318
|
+
console.log("Options:");
|
|
319
|
+
console.log(" --help, -h Show this help message");
|
|
320
|
+
console.log("");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ── CLI dispatch (only when run directly, not imported) ─────────────
|
|
324
|
+
|
|
325
|
+
if (import.meta.main) {
|
|
326
|
+
const args = process.argv.slice(2);
|
|
327
|
+
const command = args[0];
|
|
328
|
+
|
|
329
|
+
switch (command) {
|
|
330
|
+
case "install":
|
|
331
|
+
await runInstall({ noTui: args.includes("--no-tui") });
|
|
332
|
+
break;
|
|
333
|
+
case "doctor":
|
|
334
|
+
await runDoctor();
|
|
335
|
+
break;
|
|
336
|
+
case "--help":
|
|
337
|
+
case "-h":
|
|
338
|
+
case undefined:
|
|
339
|
+
printUsage();
|
|
340
|
+
break;
|
|
341
|
+
default:
|
|
342
|
+
console.error(`Unknown command: ${command}`);
|
|
343
|
+
printUsage();
|
|
344
|
+
process.exit(1);
|
|
345
|
+
}
|
|
346
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kodrunhq/opencode-autopilot",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.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": [
|
|
@@ -35,9 +35,13 @@
|
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@opencode-ai/plugin": ">=1.3.0"
|
|
37
37
|
},
|
|
38
|
+
"bin": {
|
|
39
|
+
"opencode-autopilot": "bin/cli.ts"
|
|
40
|
+
},
|
|
38
41
|
"files": [
|
|
39
42
|
"src/",
|
|
40
|
-
"assets/"
|
|
43
|
+
"assets/",
|
|
44
|
+
"bin/"
|
|
41
45
|
],
|
|
42
46
|
"scripts": {
|
|
43
47
|
"test": "bun test",
|
package/src/agents/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import type { Config } from "@opencode-ai/plugin";
|
|
2
|
+
import { loadConfig } from "../config";
|
|
3
|
+
import { resolveModelForAgent } from "../registry/resolver";
|
|
4
|
+
import type { AgentOverride, GroupModelAssignment } from "../registry/types";
|
|
2
5
|
import { autopilotAgent } from "./autopilot";
|
|
3
6
|
import { documenterAgent } from "./documenter";
|
|
4
7
|
import { metaprompterAgent } from "./metaprompter";
|
|
@@ -6,6 +9,11 @@ import { pipelineAgents } from "./pipeline/index";
|
|
|
6
9
|
import { prReviewerAgent } from "./pr-reviewer";
|
|
7
10
|
import { researcherAgent } from "./researcher";
|
|
8
11
|
|
|
12
|
+
interface AgentConfig {
|
|
13
|
+
readonly [key: string]: unknown;
|
|
14
|
+
readonly permission?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
const agents = {
|
|
10
18
|
researcher: researcherAgent,
|
|
11
19
|
metaprompter: metaprompterAgent,
|
|
@@ -14,32 +22,58 @@ const agents = {
|
|
|
14
22
|
autopilot: autopilotAgent,
|
|
15
23
|
} as const;
|
|
16
24
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Register a set of agents into the OpenCode config, resolving model
|
|
27
|
+
* assignments from groups/overrides. Skips agents already defined
|
|
28
|
+
* to preserve user customizations.
|
|
29
|
+
*
|
|
30
|
+
* Mutation of config.agent is intentional: the OpenCode plugin configHook
|
|
31
|
+
* API requires mutating the provided Config object to register agents.
|
|
32
|
+
*/
|
|
33
|
+
function registerAgents(
|
|
34
|
+
agentMap: Readonly<Record<string, Readonly<AgentConfig>>>,
|
|
35
|
+
config: Config,
|
|
36
|
+
groups: Readonly<Record<string, GroupModelAssignment>>,
|
|
37
|
+
overrides: Readonly<Record<string, AgentOverride>>,
|
|
38
|
+
): void {
|
|
39
|
+
for (const [name, agentConfig] of Object.entries(agentMap)) {
|
|
40
|
+
if (config.agent![name] === undefined) {
|
|
41
|
+
// Deep-copy agent config including nested permission to avoid shared references.
|
|
42
|
+
const resolved = resolveModelForAgent(name, groups, overrides);
|
|
43
|
+
config.agent![name] = {
|
|
28
44
|
...agentConfig,
|
|
29
45
|
...(agentConfig.permission && { permission: { ...agentConfig.permission } }),
|
|
46
|
+
...(resolved && { model: resolved.primary }),
|
|
47
|
+
...(resolved &&
|
|
48
|
+
resolved.fallbacks.length > 0 && {
|
|
49
|
+
fallback_models: [...resolved.fallbacks],
|
|
50
|
+
}),
|
|
30
51
|
};
|
|
31
52
|
}
|
|
32
53
|
}
|
|
54
|
+
}
|
|
33
55
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
config.agent[name] = {
|
|
38
|
-
...agentConfig,
|
|
39
|
-
...(agentConfig.permission && { permission: { ...agentConfig.permission } }),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
56
|
+
export async function configHook(config: Config, configPath?: string): Promise<void> {
|
|
57
|
+
if (!config.agent) {
|
|
58
|
+
config.agent = {};
|
|
42
59
|
}
|
|
60
|
+
|
|
61
|
+
// Load plugin config for model group resolution
|
|
62
|
+
let pluginConfig = null;
|
|
63
|
+
try {
|
|
64
|
+
pluginConfig = await loadConfig(configPath);
|
|
65
|
+
} catch (error: unknown) {
|
|
66
|
+
console.error(
|
|
67
|
+
"[opencode-autopilot] Failed to load plugin config:",
|
|
68
|
+
error instanceof Error ? error.message : String(error),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const groups: Readonly<Record<string, GroupModelAssignment>> = pluginConfig?.groups ?? {};
|
|
72
|
+
const overrides: Readonly<Record<string, AgentOverride>> = pluginConfig?.overrides ?? {};
|
|
73
|
+
|
|
74
|
+
// Register standard agents and pipeline agents (v2 orchestrator subagents)
|
|
75
|
+
registerAgents(agents, config, groups, overrides);
|
|
76
|
+
registerAgents(pipelineAgents, config, groups, overrides);
|
|
43
77
|
}
|
|
44
78
|
|
|
45
79
|
export { autopilotAgent } from "./autopilot";
|
package/src/config.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
1
2
|
import { readFile, rename, writeFile } from "node:fs/promises";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { fallbackConfigSchema, fallbackDefaults } from "./orchestrator/fallback/fallback-config";
|
|
6
|
+
import { AGENT_REGISTRY, ALL_GROUP_IDS } from "./registry/model-groups";
|
|
5
7
|
import { ensureDir, isEnoentError } from "./utils/fs-helpers";
|
|
6
8
|
import { getGlobalConfigDir } from "./utils/paths";
|
|
7
9
|
|
|
@@ -60,7 +62,7 @@ const pluginConfigSchemaV2 = z.object({
|
|
|
60
62
|
|
|
61
63
|
type PluginConfigV2 = z.infer<typeof pluginConfigSchemaV2>;
|
|
62
64
|
|
|
63
|
-
// --- V3 schema ---
|
|
65
|
+
// --- V3 schema (internal, for migration) ---
|
|
64
66
|
|
|
65
67
|
const pluginConfigSchemaV3 = z.object({
|
|
66
68
|
version: z.literal(3),
|
|
@@ -72,10 +74,48 @@ const pluginConfigSchemaV3 = z.object({
|
|
|
72
74
|
fallback_models: z.union([z.string(), z.array(z.string())]).optional(),
|
|
73
75
|
});
|
|
74
76
|
|
|
75
|
-
|
|
76
|
-
export const pluginConfigSchema = pluginConfigSchemaV3;
|
|
77
|
+
type PluginConfigV3 = z.infer<typeof pluginConfigSchemaV3>;
|
|
77
78
|
|
|
78
|
-
|
|
79
|
+
// --- V4 sub-schemas ---
|
|
80
|
+
|
|
81
|
+
const groupModelAssignmentSchema = z.object({
|
|
82
|
+
primary: z.string().min(1),
|
|
83
|
+
fallbacks: z.array(z.string().min(1)).default([]),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const agentOverrideSchema = z.object({
|
|
87
|
+
primary: z.string().min(1),
|
|
88
|
+
fallbacks: z.array(z.string().min(1)).optional(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// --- V4 schema ---
|
|
92
|
+
|
|
93
|
+
const pluginConfigSchemaV4 = z
|
|
94
|
+
.object({
|
|
95
|
+
version: z.literal(4),
|
|
96
|
+
configured: z.boolean(),
|
|
97
|
+
groups: z.record(z.string(), groupModelAssignmentSchema).default({}),
|
|
98
|
+
overrides: z.record(z.string(), agentOverrideSchema).default({}),
|
|
99
|
+
orchestrator: orchestratorConfigSchema.default(orchestratorDefaults),
|
|
100
|
+
confidence: confidenceConfigSchema.default(confidenceDefaults),
|
|
101
|
+
fallback: fallbackConfigSchema.default(fallbackDefaults),
|
|
102
|
+
})
|
|
103
|
+
.superRefine((config, ctx) => {
|
|
104
|
+
for (const groupId of Object.keys(config.groups)) {
|
|
105
|
+
if (!ALL_GROUP_IDS.includes(groupId as (typeof ALL_GROUP_IDS)[number])) {
|
|
106
|
+
ctx.addIssue({
|
|
107
|
+
code: z.ZodIssueCode.custom,
|
|
108
|
+
path: ["groups", groupId],
|
|
109
|
+
message: `Unknown group id "${groupId}". Expected one of: ${ALL_GROUP_IDS.join(", ")}`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Export aliases updated to v4
|
|
116
|
+
export const pluginConfigSchema = pluginConfigSchemaV4;
|
|
117
|
+
|
|
118
|
+
export type PluginConfig = z.infer<typeof pluginConfigSchemaV4>;
|
|
79
119
|
|
|
80
120
|
export const CONFIG_PATH = join(getGlobalConfigDir(), "opencode-autopilot.json");
|
|
81
121
|
|
|
@@ -91,7 +131,7 @@ function migrateV1toV2(v1Config: PluginConfigV1): PluginConfigV2 {
|
|
|
91
131
|
};
|
|
92
132
|
}
|
|
93
133
|
|
|
94
|
-
function migrateV2toV3(v2Config: PluginConfigV2):
|
|
134
|
+
function migrateV2toV3(v2Config: PluginConfigV2): PluginConfigV3 {
|
|
95
135
|
return {
|
|
96
136
|
version: 3 as const,
|
|
97
137
|
configured: v2Config.configured,
|
|
@@ -102,6 +142,55 @@ function migrateV2toV3(v2Config: PluginConfigV2): PluginConfig {
|
|
|
102
142
|
};
|
|
103
143
|
}
|
|
104
144
|
|
|
145
|
+
function migrateV3toV4(v3Config: PluginConfigV3): PluginConfig {
|
|
146
|
+
const groups: Record<string, { primary: string; fallbacks: string[] }> = {};
|
|
147
|
+
const overrides: Record<string, { primary: string }> = {};
|
|
148
|
+
|
|
149
|
+
// Step 1: Reverse-map v3 flat models to groups
|
|
150
|
+
// v3.models is Record<string, string> where key is agent name, value is model id
|
|
151
|
+
for (const [agentName, modelId] of Object.entries(v3Config.models)) {
|
|
152
|
+
const entry = AGENT_REGISTRY[agentName];
|
|
153
|
+
if (!entry) {
|
|
154
|
+
// Agent not in registry — preserve as per-agent override
|
|
155
|
+
overrides[agentName] = { primary: modelId };
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const groupId = entry.group;
|
|
160
|
+
if (!groups[groupId]) {
|
|
161
|
+
// First agent in this group sets the primary
|
|
162
|
+
groups[groupId] = { primary: modelId, fallbacks: [] };
|
|
163
|
+
} else if (groups[groupId].primary !== modelId) {
|
|
164
|
+
// Different model for same group — becomes an override
|
|
165
|
+
overrides[agentName] = { primary: modelId };
|
|
166
|
+
}
|
|
167
|
+
// Same model as group primary — no override needed
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Step 2: Migrate global fallback_models to per-group fallbacks
|
|
171
|
+
const globalFallbacks = v3Config.fallback_models
|
|
172
|
+
? typeof v3Config.fallback_models === "string"
|
|
173
|
+
? [v3Config.fallback_models]
|
|
174
|
+
: [...v3Config.fallback_models]
|
|
175
|
+
: [];
|
|
176
|
+
|
|
177
|
+
for (const group of Object.values(groups)) {
|
|
178
|
+
if (group.fallbacks.length === 0 && globalFallbacks.length > 0) {
|
|
179
|
+
group.fallbacks = [...globalFallbacks];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
version: 4 as const,
|
|
185
|
+
configured: v3Config.configured,
|
|
186
|
+
groups,
|
|
187
|
+
overrides,
|
|
188
|
+
orchestrator: v3Config.orchestrator,
|
|
189
|
+
confidence: v3Config.confidence,
|
|
190
|
+
fallback: v3Config.fallback,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
105
194
|
// --- Public API ---
|
|
106
195
|
|
|
107
196
|
export async function loadConfig(configPath: string = CONFIG_PATH): Promise<PluginConfig | null> {
|
|
@@ -109,35 +198,40 @@ export async function loadConfig(configPath: string = CONFIG_PATH): Promise<Plug
|
|
|
109
198
|
const raw = await readFile(configPath, "utf-8");
|
|
110
199
|
const parsed = JSON.parse(raw);
|
|
111
200
|
|
|
112
|
-
// Try
|
|
201
|
+
// Try v4 first
|
|
202
|
+
const v4Result = pluginConfigSchemaV4.safeParse(parsed);
|
|
203
|
+
if (v4Result.success) return v4Result.data;
|
|
204
|
+
|
|
205
|
+
// Try v3 and migrate to v4
|
|
113
206
|
const v3Result = pluginConfigSchemaV3.safeParse(parsed);
|
|
114
207
|
if (v3Result.success) {
|
|
115
|
-
|
|
208
|
+
const migrated = migrateV3toV4(v3Result.data);
|
|
209
|
+
await saveConfig(migrated, configPath);
|
|
210
|
+
return migrated;
|
|
116
211
|
}
|
|
117
212
|
|
|
118
|
-
// Try v2
|
|
213
|
+
// Try v2 → v3 → v4
|
|
119
214
|
const v2Result = pluginConfigSchemaV2.safeParse(parsed);
|
|
120
215
|
if (v2Result.success) {
|
|
121
|
-
const
|
|
216
|
+
const v3 = migrateV2toV3(v2Result.data);
|
|
217
|
+
const migrated = migrateV3toV4(v3);
|
|
122
218
|
await saveConfig(migrated, configPath);
|
|
123
219
|
return migrated;
|
|
124
220
|
}
|
|
125
221
|
|
|
126
|
-
// Try v1
|
|
222
|
+
// Try v1 → v2 → v3 → v4
|
|
127
223
|
const v1Result = pluginConfigSchemaV1.safeParse(parsed);
|
|
128
224
|
if (v1Result.success) {
|
|
129
225
|
const v2 = migrateV1toV2(v1Result.data);
|
|
130
|
-
const
|
|
226
|
+
const v3 = migrateV2toV3(v2);
|
|
227
|
+
const migrated = migrateV3toV4(v3);
|
|
131
228
|
await saveConfig(migrated, configPath);
|
|
132
229
|
return migrated;
|
|
133
230
|
}
|
|
134
231
|
|
|
135
|
-
|
|
136
|
-
return pluginConfigSchemaV3.parse(parsed);
|
|
232
|
+
return pluginConfigSchemaV4.parse(parsed); // throw with proper error
|
|
137
233
|
} catch (error: unknown) {
|
|
138
|
-
if (isEnoentError(error))
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
234
|
+
if (isEnoentError(error)) return null;
|
|
141
235
|
throw error;
|
|
142
236
|
}
|
|
143
237
|
}
|
|
@@ -147,7 +241,7 @@ export async function saveConfig(
|
|
|
147
241
|
configPath: string = CONFIG_PATH,
|
|
148
242
|
): Promise<void> {
|
|
149
243
|
await ensureDir(dirname(configPath));
|
|
150
|
-
const tmpPath = `${configPath}.tmp.${
|
|
244
|
+
const tmpPath = `${configPath}.tmp.${randomBytes(8).toString("hex")}`;
|
|
151
245
|
await writeFile(tmpPath, JSON.stringify(config, null, 2), "utf-8");
|
|
152
246
|
await rename(tmpPath, configPath);
|
|
153
247
|
}
|
|
@@ -158,9 +252,10 @@ export function isFirstLoad(config: PluginConfig | null): boolean {
|
|
|
158
252
|
|
|
159
253
|
export function createDefaultConfig(): PluginConfig {
|
|
160
254
|
return {
|
|
161
|
-
version:
|
|
255
|
+
version: 4 as const,
|
|
162
256
|
configured: false,
|
|
163
|
-
|
|
257
|
+
groups: {},
|
|
258
|
+
overrides: {},
|
|
164
259
|
orchestrator: orchestratorDefaults,
|
|
165
260
|
confidence: confidenceDefaults,
|
|
166
261
|
fallback: fallbackDefaults,
|