@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.
@@ -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.
@@ -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
- });