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