@kodrunhq/opencode-autopilot 0.1.3 → 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
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import type { Config } from "@opencode-ai/plugin";
|
|
2
|
+
import { tool } from "@opencode-ai/plugin";
|
|
3
|
+
import { createDefaultConfig, loadConfig, saveConfig } from "../config";
|
|
4
|
+
import { checkDiversity } from "../registry/diversity";
|
|
5
|
+
import { diagnose } from "../registry/doctor";
|
|
6
|
+
import {
|
|
7
|
+
AGENT_REGISTRY,
|
|
8
|
+
ALL_GROUP_IDS,
|
|
9
|
+
DIVERSITY_RULES,
|
|
10
|
+
GROUP_DEFINITIONS,
|
|
11
|
+
} from "../registry/model-groups";
|
|
12
|
+
import { extractFamily } from "../registry/resolver";
|
|
13
|
+
import type { DiversityWarning, GroupModelAssignment } from "../registry/types";
|
|
14
|
+
|
|
15
|
+
// --- Module-level state ---
|
|
16
|
+
|
|
17
|
+
// Module-level mutable state is intentional: oc_configure is a session-scoped
|
|
18
|
+
// workflow where assignments accumulate across multiple "assign" calls before
|
|
19
|
+
// being persisted by "commit". The Map is cleared on commit and reset.
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* In-progress group assignments, keyed by GroupId.
|
|
23
|
+
* Populated by "assign" subcommand, persisted by "commit", cleared by "reset".
|
|
24
|
+
* Held in memory — configuration is a single-session flow.
|
|
25
|
+
*/
|
|
26
|
+
let pendingAssignments: Map<string, GroupModelAssignment> = new Map();
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Reference to the OpenCode host config, set by the plugin's config hook.
|
|
30
|
+
* Used by "start" subcommand to discover available models.
|
|
31
|
+
*/
|
|
32
|
+
let openCodeConfig: Config | null = null;
|
|
33
|
+
|
|
34
|
+
// --- Exported helpers for test/plugin wiring ---
|
|
35
|
+
|
|
36
|
+
export function resetPendingAssignments(): void {
|
|
37
|
+
pendingAssignments = new Map();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function setOpenCodeConfig(config: Config | null): void {
|
|
41
|
+
openCodeConfig = config;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --- Core logic ---
|
|
45
|
+
|
|
46
|
+
interface ConfigureArgs {
|
|
47
|
+
readonly subcommand: "start" | "assign" | "commit" | "doctor" | "reset";
|
|
48
|
+
readonly group?: string;
|
|
49
|
+
readonly primary?: string;
|
|
50
|
+
readonly fallbacks?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getStringField(obj: Record<string, unknown>, key: string): string | undefined {
|
|
54
|
+
const val = obj[key];
|
|
55
|
+
return typeof val === "string" ? val : undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extract available models from the OpenCode host config, grouped by family.
|
|
60
|
+
* Returns a map of family -> model IDs.
|
|
61
|
+
*/
|
|
62
|
+
function discoverAvailableModels(config: Config | null): Map<string, string[]> {
|
|
63
|
+
const modelsByFamily = new Map<string, string[]>();
|
|
64
|
+
if (!config) return modelsByFamily;
|
|
65
|
+
|
|
66
|
+
const configRecord = config as Record<string, unknown>;
|
|
67
|
+
const seen = new Set<string>();
|
|
68
|
+
|
|
69
|
+
// Collect from agent configs
|
|
70
|
+
const agentConfigs = configRecord.agent;
|
|
71
|
+
if (agentConfigs && typeof agentConfigs === "object" && agentConfigs !== null) {
|
|
72
|
+
for (const agentCfg of Object.values(agentConfigs as Record<string, unknown>)) {
|
|
73
|
+
if (agentCfg && typeof agentCfg === "object" && agentCfg !== null) {
|
|
74
|
+
const model = getStringField(agentCfg as Record<string, unknown>, "model");
|
|
75
|
+
if (model && !seen.has(model)) {
|
|
76
|
+
seen.add(model);
|
|
77
|
+
const family = extractFamily(model);
|
|
78
|
+
const list = modelsByFamily.get(family) ?? [];
|
|
79
|
+
list.push(model);
|
|
80
|
+
modelsByFamily.set(family, list);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Also include top-level model and small_model
|
|
87
|
+
const topModel = getStringField(configRecord, "model");
|
|
88
|
+
const smallModel = getStringField(configRecord, "small_model");
|
|
89
|
+
for (const m of [topModel, smallModel]) {
|
|
90
|
+
if (m && !seen.has(m)) {
|
|
91
|
+
seen.add(m);
|
|
92
|
+
const family = extractFamily(m);
|
|
93
|
+
const list = modelsByFamily.get(family) ?? [];
|
|
94
|
+
list.push(m);
|
|
95
|
+
modelsByFamily.set(family, list);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return modelsByFamily;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function serializeDiversityWarnings(warnings: readonly DiversityWarning[]): readonly {
|
|
103
|
+
groups: readonly string[];
|
|
104
|
+
severity: string;
|
|
105
|
+
sharedFamily: string;
|
|
106
|
+
reason: string;
|
|
107
|
+
}[] {
|
|
108
|
+
return warnings.map((w) => ({
|
|
109
|
+
groups: w.groups,
|
|
110
|
+
severity: w.rule.severity,
|
|
111
|
+
sharedFamily: w.sharedFamily,
|
|
112
|
+
reason: w.rule.reason,
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function handleStart(configPath?: string): Promise<string> {
|
|
117
|
+
const modelsByFamily = discoverAvailableModels(openCodeConfig);
|
|
118
|
+
|
|
119
|
+
// Load current plugin config to show existing assignments
|
|
120
|
+
const currentConfig = await loadConfig(configPath);
|
|
121
|
+
|
|
122
|
+
// Build groups with agents derived from AGENT_REGISTRY
|
|
123
|
+
const groups = ALL_GROUP_IDS.map((groupId) => {
|
|
124
|
+
const def = GROUP_DEFINITIONS[groupId];
|
|
125
|
+
const agents = Object.entries(AGENT_REGISTRY)
|
|
126
|
+
.filter(([, entry]) => entry.group === groupId)
|
|
127
|
+
.map(([name]) => name);
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
id: def.id,
|
|
131
|
+
label: def.label,
|
|
132
|
+
purpose: def.purpose,
|
|
133
|
+
recommendation: def.recommendation,
|
|
134
|
+
tier: def.tier,
|
|
135
|
+
order: def.order,
|
|
136
|
+
agents,
|
|
137
|
+
currentAssignment: currentConfig?.groups[groupId] ?? null,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
return JSON.stringify({
|
|
142
|
+
action: "configure",
|
|
143
|
+
stage: "start",
|
|
144
|
+
availableModels: Object.fromEntries(modelsByFamily),
|
|
145
|
+
groups,
|
|
146
|
+
currentConfig: currentConfig
|
|
147
|
+
? { configured: currentConfig.configured, groups: currentConfig.groups }
|
|
148
|
+
: null,
|
|
149
|
+
diversityRules: DIVERSITY_RULES,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function handleAssign(args: ConfigureArgs): string {
|
|
154
|
+
const { group, primary, fallbacks: fallbacksStr } = args;
|
|
155
|
+
|
|
156
|
+
// Validate group
|
|
157
|
+
if (!group || !ALL_GROUP_IDS.includes(group as (typeof ALL_GROUP_IDS)[number])) {
|
|
158
|
+
return JSON.stringify({
|
|
159
|
+
action: "error",
|
|
160
|
+
message: `Invalid group: '${group ?? ""}'. Valid groups: ${ALL_GROUP_IDS.join(", ")}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Validate primary
|
|
165
|
+
if (!primary || primary.trim().length === 0) {
|
|
166
|
+
return JSON.stringify({
|
|
167
|
+
action: "error",
|
|
168
|
+
message: "Primary model is required for assign subcommand.",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const trimmedPrimary = primary.trim();
|
|
173
|
+
|
|
174
|
+
// Parse fallbacks
|
|
175
|
+
const parsedFallbacks = fallbacksStr
|
|
176
|
+
? fallbacksStr
|
|
177
|
+
.split(",")
|
|
178
|
+
.map((s) => s.trim())
|
|
179
|
+
.filter(Boolean)
|
|
180
|
+
: [];
|
|
181
|
+
|
|
182
|
+
// Store assignment
|
|
183
|
+
const assignment: GroupModelAssignment = Object.freeze({
|
|
184
|
+
primary: trimmedPrimary,
|
|
185
|
+
fallbacks: Object.freeze(parsedFallbacks),
|
|
186
|
+
});
|
|
187
|
+
pendingAssignments.set(group, assignment);
|
|
188
|
+
|
|
189
|
+
// Run diversity check on all pending assignments
|
|
190
|
+
const assignmentRecord: Record<string, GroupModelAssignment> =
|
|
191
|
+
Object.fromEntries(pendingAssignments);
|
|
192
|
+
const diversityWarnings = serializeDiversityWarnings(checkDiversity(assignmentRecord));
|
|
193
|
+
|
|
194
|
+
return JSON.stringify({
|
|
195
|
+
action: "configure",
|
|
196
|
+
stage: "assigned",
|
|
197
|
+
group,
|
|
198
|
+
primary: trimmedPrimary,
|
|
199
|
+
fallbacks: parsedFallbacks,
|
|
200
|
+
assignedCount: pendingAssignments.size,
|
|
201
|
+
totalGroups: ALL_GROUP_IDS.length,
|
|
202
|
+
diversityWarnings,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function handleCommit(configPath?: string): Promise<string> {
|
|
207
|
+
// Validate all groups assigned
|
|
208
|
+
if (pendingAssignments.size < ALL_GROUP_IDS.length) {
|
|
209
|
+
const assigned = new Set(pendingAssignments.keys());
|
|
210
|
+
const missing = ALL_GROUP_IDS.filter((id) => !assigned.has(id));
|
|
211
|
+
return JSON.stringify({
|
|
212
|
+
action: "error",
|
|
213
|
+
message: `Cannot commit: ${missing.length} group(s) missing assignments: ${missing.join(", ")}`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Load current config or create default
|
|
218
|
+
const currentConfig = (await loadConfig(configPath)) ?? createDefaultConfig();
|
|
219
|
+
|
|
220
|
+
// Build new config — convert readonly fallbacks to mutable for Zod schema compatibility
|
|
221
|
+
const groupsRecord: Record<string, { primary: string; fallbacks: string[] }> = {};
|
|
222
|
+
for (const [key, val] of pendingAssignments) {
|
|
223
|
+
groupsRecord[key] = { primary: val.primary, fallbacks: [...val.fallbacks] };
|
|
224
|
+
}
|
|
225
|
+
const newConfig = {
|
|
226
|
+
...currentConfig,
|
|
227
|
+
version: 4 as const,
|
|
228
|
+
configured: true,
|
|
229
|
+
groups: groupsRecord,
|
|
230
|
+
overrides: currentConfig.overrides ?? {},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
// Save
|
|
234
|
+
await saveConfig(newConfig, configPath);
|
|
235
|
+
|
|
236
|
+
// Clear pending
|
|
237
|
+
const savedGroups = { ...groupsRecord };
|
|
238
|
+
pendingAssignments.clear();
|
|
239
|
+
|
|
240
|
+
// Final diversity check
|
|
241
|
+
const diversityWarnings = serializeDiversityWarnings(checkDiversity(savedGroups));
|
|
242
|
+
|
|
243
|
+
return JSON.stringify({
|
|
244
|
+
action: "configure",
|
|
245
|
+
stage: "committed",
|
|
246
|
+
groups: savedGroups,
|
|
247
|
+
diversityWarnings,
|
|
248
|
+
configPath: configPath ?? "~/.config/opencode/opencode-autopilot.json",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function handleDoctor(configPath?: string): Promise<string> {
|
|
253
|
+
const config = await loadConfig(configPath);
|
|
254
|
+
const result = diagnose(config);
|
|
255
|
+
|
|
256
|
+
return JSON.stringify({
|
|
257
|
+
action: "configure",
|
|
258
|
+
stage: "doctor",
|
|
259
|
+
checks: {
|
|
260
|
+
configExists: result.configExists,
|
|
261
|
+
schemaValid: result.schemaValid,
|
|
262
|
+
configured: result.configured,
|
|
263
|
+
groupsAssigned: result.groupsAssigned,
|
|
264
|
+
},
|
|
265
|
+
diversityWarnings: serializeDiversityWarnings(result.diversityWarnings),
|
|
266
|
+
allPassed: result.allPassed,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function handleReset(): string {
|
|
271
|
+
pendingAssignments.clear();
|
|
272
|
+
return JSON.stringify({
|
|
273
|
+
action: "configure",
|
|
274
|
+
stage: "reset",
|
|
275
|
+
message: "All in-progress assignments cleared.",
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// --- Public API ---
|
|
280
|
+
|
|
281
|
+
export async function configureCore(args: ConfigureArgs, configPath?: string): Promise<string> {
|
|
282
|
+
switch (args.subcommand) {
|
|
283
|
+
case "start":
|
|
284
|
+
return handleStart(configPath);
|
|
285
|
+
case "assign":
|
|
286
|
+
return handleAssign(args);
|
|
287
|
+
case "commit":
|
|
288
|
+
return handleCommit(configPath);
|
|
289
|
+
case "doctor":
|
|
290
|
+
return handleDoctor(configPath);
|
|
291
|
+
case "reset":
|
|
292
|
+
return handleReset();
|
|
293
|
+
default:
|
|
294
|
+
return JSON.stringify({
|
|
295
|
+
action: "error",
|
|
296
|
+
message: `Unknown subcommand: '${args.subcommand}'`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// --- Tool wrapper ---
|
|
302
|
+
|
|
303
|
+
export const ocConfigure = tool({
|
|
304
|
+
description:
|
|
305
|
+
"Configure model assignments for opencode-autopilot agent groups. " +
|
|
306
|
+
"Subcommands: start (discover models), assign (set group model), " +
|
|
307
|
+
"commit (persist), doctor (diagnose), reset (clear in-progress).",
|
|
308
|
+
args: {
|
|
309
|
+
subcommand: tool.schema
|
|
310
|
+
.enum(["start", "assign", "commit", "doctor", "reset"])
|
|
311
|
+
.describe("Action to perform"),
|
|
312
|
+
group: tool.schema
|
|
313
|
+
.string()
|
|
314
|
+
.optional()
|
|
315
|
+
.describe("Group ID for assign subcommand (e.g. 'architects')"),
|
|
316
|
+
primary: tool.schema
|
|
317
|
+
.string()
|
|
318
|
+
.optional()
|
|
319
|
+
.describe("Primary model ID for assign subcommand (e.g. 'anthropic/claude-opus-4-6')"),
|
|
320
|
+
fallbacks: tool.schema
|
|
321
|
+
.string()
|
|
322
|
+
.optional()
|
|
323
|
+
.describe("Comma-separated fallback model IDs for assign subcommand"),
|
|
324
|
+
},
|
|
325
|
+
async execute(args) {
|
|
326
|
+
return configureCore(args);
|
|
327
|
+
},
|
|
328
|
+
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: A placeholder agent installed by opencode-autopilot to verify the plugin is working
|
|
3
|
-
mode: all
|
|
4
|
-
permission:
|
|
5
|
-
read: allow
|
|
6
|
-
edit: deny
|
|
7
|
-
bash: deny
|
|
8
|
-
webfetch: deny
|
|
9
|
-
task: deny
|
|
10
|
-
---
|
|
11
|
-
You are a placeholder agent installed by the opencode-autopilot plugin. Your purpose is to confirm the plugin's asset installation is working correctly.
|
|
12
|
-
|
|
13
|
-
When invoked, explain that you are a placeholder and suggest the user explore other agents provided by the opencode-autopilot plugin.
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Configure the opencode-autopilot plugin -- assign models to agents
|
|
3
|
-
---
|
|
4
|
-
The opencode-autopilot plugin supports configuration for model mapping. You can assign specific models to each agent provided by the plugin by editing the config file at `~/.config/opencode/opencode-autopilot.json`.
|
|
5
|
-
|
|
6
|
-
The config file uses this format:
|
|
7
|
-
```json
|
|
8
|
-
{
|
|
9
|
-
"version": 1,
|
|
10
|
-
"configured": true,
|
|
11
|
-
"models": {
|
|
12
|
-
"agent-name": "provider/model-id"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
Note: An interactive configuration tool will be added in a future phase to automate this process.
|
package/src/tools/placeholder.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
|
|
3
|
-
export const ocPlaceholder = tool({
|
|
4
|
-
description: "Verifies that the OpenCode Assets plugin is loaded and working",
|
|
5
|
-
args: {
|
|
6
|
-
message: tool.schema.string().max(1000).describe("A test message to echo back"),
|
|
7
|
-
},
|
|
8
|
-
async execute(args) {
|
|
9
|
-
return `OpenCode Assets plugin is active. Your message: ${args.message}`;
|
|
10
|
-
},
|
|
11
|
-
});
|