opencode-auto-agent 1.0.0 → 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.
@@ -1,80 +1,187 @@
1
1
  /**
2
- * init command — scaffold .ai-starter-kit/ into the target repository.
2
+ * init command — model-aware, interactive scaffolding of the OpenCode agent team.
3
+ *
4
+ * Flow:
5
+ * 1. Validate prerequisites (opencode, ralphy, node)
6
+ * 2. Discover available models via `opencode models`
7
+ * 3. Let the user choose a preset
8
+ * 4. Let the user assign models to each agent role
9
+ * 5. Scaffold .opencode/ directory with agents, rules, presets, context
10
+ * 6. Write config.json with full agent-model mapping
11
+ * 7. Generate opencode.json at project root (with model assignments)
12
+ * 8. Generate .opencode/agents/*.md from templates (with model frontmatter)
13
+ * 9. Generate .ralphy/config.yaml stub if missing
14
+ * 10. Print summary and next steps
3
15
  *
4
16
  * Rules:
5
- * 1. Never overwrite files that already exist (safe re-run).
6
- * 2. Write config.json with schema version for future upgrades.
7
- * 3. Copy agent markdowns, rules, and selected preset.
8
- * 4. Create empty context/ folder for runtime context assembly.
17
+ * - Never overwrite files that already exist (safe re-run) unless --force
18
+ * - Abort if prerequisites fail
19
+ * - Do not auto-install anything
9
20
  */
10
21
 
11
- import { existsSync, mkdirSync } from "node:fs";
22
+ import { existsSync, mkdirSync, readFileSync } from "node:fs";
12
23
  import { join, resolve } from "node:path";
13
24
  import { fileURLToPath } from "node:url";
14
25
 
15
- import { SCAFFOLD_DIR, CONFIG_FILE, PRESETS, AGENTS, DIRS, SCHEMA_VERSION } from "../lib/constants.js";
26
+ import {
27
+ SCAFFOLD_DIR, CONFIG_FILE, PRESETS, AGENTS, AGENT_ROLES,
28
+ DIRS, SCHEMA_VERSION, DEFAULT_MODEL_MAP, WORKFLOW_STEPS,
29
+ } from "../lib/constants.js";
16
30
  import { copyDirSync, readJsonSync, writeJsonSync, writeTextSync } from "../lib/fs-utils.js";
31
+ import { validateAndReport } from "../lib/prerequisites.js";
32
+ import { discoverModelsInteractive, findModel, groupByProvider } from "../lib/models.js";
33
+ import {
34
+ banner, section, status, kv, success, error, c, boxList,
35
+ select, confirm, table,
36
+ } from "../lib/ui.js";
17
37
 
18
38
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
19
39
  const TEMPLATES_ROOT = resolve(__dirname, "..", "..", "templates");
20
40
 
21
41
  /**
22
- * Default config written on first init.
42
+ * Main init entry point.
43
+ *
44
+ * @param {string} targetDir - Project root directory
45
+ * @param {Object} opts
46
+ * @param {string} [opts.preset] - Preset name (skips interactive selection)
47
+ * @param {boolean} [opts.force] - Overwrite existing config
48
+ * @param {boolean} [opts.nonInteractive] - Skip all prompts, use defaults
23
49
  */
24
- function makeDefaultConfig(preset) {
25
- return {
26
- $schema: "https://github.com/opencode-auto-agent/config-schema",
27
- version: SCHEMA_VERSION,
28
- preset: preset || "java",
29
- docsPath: "./Docs",
30
- tasksPath: "./PRD.md",
31
- outputPath: "./",
32
- engine: "opencode",
33
- agents: AGENTS.reduce((acc, name) => {
34
- acc[name] = { enabled: true };
35
- return acc;
36
- }, {}),
37
- orchestrator: {
38
- workflowSteps: ["plan", "implement", "test", "verify"],
39
- requirePlanApproval: false,
40
- },
41
- };
42
- }
50
+ export async function init(targetDir, { preset, force, nonInteractive } = {}) {
51
+ banner(
52
+ "OpenCode Auto-Agent Initializer",
53
+ `Scaffolding an AI agent team into ${targetDir}`
54
+ );
43
55
 
44
- export async function init(targetDir, { preset } = {}) {
56
+ // ── Step 1: Prerequisites ────────────────────────────────────────────────
57
+ section("Prerequisites");
58
+ validateAndReport({ requireRalphy: false, abortOnFail: true });
59
+ console.log();
60
+
61
+ // ── Step 2: Discover models ──────────────────────────────────────────────
62
+ section("Model Discovery");
63
+ const models = discoverModelsInteractive();
64
+
65
+ if (models.length === 0) {
66
+ error(
67
+ "No models available from OpenCode.",
68
+ "Configure at least one provider: opencode auth login"
69
+ );
70
+ process.exit(1);
71
+ }
72
+
73
+ // Show provider summary
74
+ const groups = groupByProvider(models);
75
+ const providerList = [...groups.entries()].map(
76
+ ([p, ms]) => `${c.bold(p)} ${c.gray(`(${ms.length} models)`)}`
77
+ );
78
+ boxList("Available Providers", providerList);
79
+
80
+ // ── Step 3: Choose preset ────────────────────────────────────────────────
81
+ section("Preset Selection");
82
+ let chosenPreset = preset;
83
+
84
+ if (!chosenPreset && !nonInteractive) {
85
+ const presetItems = PRESETS.map((p) => ({
86
+ label: p,
87
+ value: p,
88
+ hint: presetDescription(p),
89
+ }));
90
+ const result = await select("Choose a project preset", presetItems);
91
+ chosenPreset = result.value;
92
+ }
93
+ chosenPreset = chosenPreset && PRESETS.includes(chosenPreset) ? chosenPreset : "java";
94
+ status("pass", `Preset: ${c.bold(chosenPreset)}`, presetDescription(chosenPreset));
95
+
96
+ // ── Step 4: Assign models to agent roles ─────────────────────────────────
97
+ section("Agent Model Assignment");
98
+ console.log(` ${c.gray("Assign a model to each agent role. Defaults are shown.")}`);
99
+ console.log();
100
+
101
+ const modelMapping = {};
102
+
103
+ for (const role of AGENTS) {
104
+ const roleDef = AGENT_ROLES[role];
105
+ const defaultId = DEFAULT_MODEL_MAP[role];
106
+ const defaultModel = findModel(models, defaultId);
107
+
108
+ if (nonInteractive) {
109
+ // Use default or first available
110
+ modelMapping[role] = defaultModel ? defaultModel.id : models[0].id;
111
+ status("dot", `${padRole(role)} ${c.cyan(modelMapping[role])}`, "(default)");
112
+ continue;
113
+ }
114
+
115
+ // Build selection list: default first, then all others
116
+ const items = [];
117
+ if (defaultModel) {
118
+ items.push({
119
+ label: defaultModel.id,
120
+ value: defaultModel.id,
121
+ hint: "(recommended)",
122
+ });
123
+ }
124
+ for (const m of models) {
125
+ if (m.id !== defaultModel?.id) {
126
+ items.push({ label: m.id, value: m.id });
127
+ }
128
+ }
129
+
130
+ const result = await select(
131
+ `Model for ${c.bold(role)} ${c.gray(`\u2014 ${roleDef.description}`)}`,
132
+ items
133
+ );
134
+ modelMapping[role] = result.value;
135
+ }
136
+
137
+ // Show summary table
138
+ console.log();
139
+ table(
140
+ ["Agent Role", "Model", "Mode"],
141
+ AGENTS.map((role) => [
142
+ c.bold(role),
143
+ c.cyan(modelMapping[role]),
144
+ c.gray(AGENT_ROLES[role].mode),
145
+ ])
146
+ );
147
+
148
+ // ── Step 5: Confirm ──────────────────────────────────────────────────────
149
+ if (!nonInteractive) {
150
+ console.log();
151
+ const proceed = await confirm("Proceed with this configuration?", true);
152
+ if (!proceed) {
153
+ console.log(c.gray("\n Aborted.\n"));
154
+ process.exit(0);
155
+ }
156
+ }
157
+
158
+ // ── Step 6: Scaffold directories ─────────────────────────────────────────
159
+ section("Scaffolding");
45
160
  const scaffoldDir = join(targetDir, SCAFFOLD_DIR);
46
161
  const isUpgrade = existsSync(scaffoldDir);
47
162
 
48
163
  if (isUpgrade) {
49
- console.log(`[init] Existing ${SCAFFOLD_DIR}/ detected upgrading (no overwrites).`);
50
- } else {
51
- console.log(`[init] Scaffolding ${SCAFFOLD_DIR}/ into ${targetDir}`);
164
+ status("info", `Existing ${SCAFFOLD_DIR}/ detected \u2014 upgrading (no overwrites)`);
52
165
  }
53
166
 
54
- // 1. Create directory skeleton
55
167
  for (const sub of Object.values(DIRS)) {
56
168
  mkdirSync(join(scaffoldDir, sub), { recursive: true });
57
169
  }
58
170
 
59
- // 2. Copy agent markdowns (skip existing)
60
- const agentsSrc = join(TEMPLATES_ROOT, "agents");
61
- const agentsDest = join(scaffoldDir, DIRS.agents);
62
- copyDirSync(agentsSrc, agentsDest, { overwrite: false });
63
- console.log(`[init] Agents written to ${DIRS.agents}/`);
64
-
65
- // 3. Copy rules (skip existing)
66
- const rulesSrc = join(TEMPLATES_ROOT, "rules");
67
- const rulesDest = join(scaffoldDir, DIRS.rules);
68
- copyDirSync(rulesSrc, rulesDest, { overwrite: false });
69
- console.log(`[init] Rules written to ${DIRS.rules}/`);
70
-
71
- // 4. Copy selected preset (or all presets)
72
- const presetsSrc = join(TEMPLATES_ROOT, "presets");
73
- const presetsDest = join(scaffoldDir, DIRS.presets);
74
- copyDirSync(presetsSrc, presetsDest, { overwrite: false });
75
- console.log(`[init] Presets written to ${DIRS.presets}/`);
76
-
77
- // 5. Seed empty context readme
171
+ // Copy template agents (skip existing unless --force)
172
+ const overwrite = !!force;
173
+ copyDirSync(join(TEMPLATES_ROOT, "agents"), join(scaffoldDir, DIRS.agents), { overwrite });
174
+ status("pass", "Agent templates", `${DIRS.agents}/`);
175
+
176
+ // Copy rules
177
+ copyDirSync(join(TEMPLATES_ROOT, "rules"), join(scaffoldDir, DIRS.rules), { overwrite });
178
+ status("pass", "Rules", `${DIRS.rules}/`);
179
+
180
+ // Copy presets
181
+ copyDirSync(join(TEMPLATES_ROOT, "presets"), join(scaffoldDir, DIRS.presets), { overwrite });
182
+ status("pass", "Presets", `${DIRS.presets}/`);
183
+
184
+ // Seed context readme
78
185
  const contextReadme = join(scaffoldDir, DIRS.context, "README.md");
79
186
  if (!existsSync(contextReadme)) {
80
187
  writeTextSync(
@@ -82,95 +189,176 @@ export async function init(targetDir, { preset } = {}) {
82
189
  "# Runtime Context\n\nThis folder is populated at runtime by the orchestrator.\nDo not manually edit files here; they are regenerated on each `run`.\n"
83
190
  );
84
191
  }
192
+ status("pass", "Context directory", `${DIRS.context}/`);
85
193
 
86
- // 6. Write config.json (only if missing)
194
+ // ── Step 7: Write config.json ────────────────────────────────────────────
87
195
  const configPath = join(scaffoldDir, CONFIG_FILE);
88
- if (!existsSync(configPath)) {
89
- const chosenPreset = preset && PRESETS.includes(preset) ? preset : undefined;
90
- writeJsonSync(configPath, makeDefaultConfig(chosenPreset));
91
- console.log(`[init] Config written to ${CONFIG_FILE} (preset: ${chosenPreset || "java"})`);
196
+ if (!existsSync(configPath) || force) {
197
+ const config = buildProjectConfig(chosenPreset, modelMapping);
198
+ writeJsonSync(configPath, config);
199
+ status("pass", "Project config", CONFIG_FILE);
92
200
  } else {
93
- console.log(`[init] Config already exists skipped.`);
201
+ status("info", "Project config already exists \u2014 skipped", CONFIG_FILE);
94
202
  }
95
203
 
96
- // 7. Generate opencode.json at project root if missing
204
+ // ── Step 8: Generate opencode.json ───────────────────────────────────────
97
205
  const opencodeConfig = join(targetDir, "opencode.json");
98
- if (!existsSync(opencodeConfig)) {
99
- writeJsonSync(opencodeConfig, buildOpencodeConfig());
100
- console.log(`[init] Generated opencode.json at project root.`);
206
+ if (!existsSync(opencodeConfig) || force) {
207
+ writeJsonSync(opencodeConfig, buildOpencodeJson(modelMapping, chosenPreset));
208
+ status("pass", "OpenCode config", "opencode.json");
209
+ } else {
210
+ status("info", "opencode.json already exists \u2014 skipped");
211
+ }
212
+
213
+ // ── Step 9: Generate agent markdown files with model frontmatter ─────────
214
+ for (const role of AGENTS) {
215
+ const agentFile = join(scaffoldDir, DIRS.agents, `${role}.md`);
216
+ const templateFile = join(TEMPLATES_ROOT, "agents", `${role}.md`);
217
+ if (existsSync(templateFile) && (!existsSync(agentFile) || force)) {
218
+ const template = readFileSync(templateFile, "utf8");
219
+ const patched = patchAgentModelFrontmatter(template, modelMapping[role]);
220
+ writeTextSync(agentFile, patched);
221
+ }
101
222
  }
223
+ status("pass", "Agent markdown files updated with model assignments");
102
224
 
103
- // 8. Generate .ralphy/config.yaml stub if missing
225
+ // ── Step 10: .ralphy/config.yaml ─────────────────────────────────────────
104
226
  const ralphyDir = join(targetDir, ".ralphy");
105
227
  const ralphyConfig = join(ralphyDir, "config.yaml");
106
228
  if (!existsSync(ralphyConfig)) {
107
229
  mkdirSync(ralphyDir, { recursive: true });
108
230
  writeTextSync(ralphyConfig, buildRalphyConfig());
109
- console.log(`[init] Generated .ralphy/config.yaml`);
231
+ status("pass", "Ralphy config", ".ralphy/config.yaml");
232
+ } else {
233
+ status("info", "Ralphy config already exists \u2014 skipped");
110
234
  }
111
235
 
112
- console.log(`\n[init] Done. Next steps:`);
113
- console.log(` 1. Edit ${SCAFFOLD_DIR}/${CONFIG_FILE} to set your preset and paths.`);
114
- console.log(` 2. Run: npx opencode-auto-agent doctor`);
115
- console.log(` 3. Run: npx opencode-auto-agent run`);
236
+ // ── Done ─────────────────────────────────────────────────────────────────
237
+ success("Initialization complete!");
238
+
239
+ console.log(` ${c.bold("Next steps:")}`);
240
+ console.log(` ${c.gray("1.")} Edit ${c.cyan(`${SCAFFOLD_DIR}/${CONFIG_FILE}`)} to fine-tune agents and paths`);
241
+ console.log(` ${c.gray("2.")} Run ${c.cyan("npx opencode-auto-agent doctor")} to validate`);
242
+ console.log(` ${c.gray("3.")} Run ${c.cyan("npx opencode-auto-agent run")} to start the orchestrator`);
243
+ console.log();
116
244
  }
117
245
 
118
- // ── helpers ────────────────────────────────────────────────────────────────
246
+ // ── Config builders ────────────────────────────────────────────────────────
119
247
 
120
- function buildOpencodeConfig() {
248
+ function buildProjectConfig(preset, modelMapping) {
121
249
  return {
122
- $schema: "https://opencode.ai/config.json",
250
+ $schema: "https://github.com/opencode-auto-agent/config-schema",
251
+ version: SCHEMA_VERSION,
252
+ preset,
253
+ docsPath: "./Docs",
254
+ tasksPath: "./PRD.md",
255
+ outputPath: "./",
256
+ engine: "opencode",
257
+ models: { ...modelMapping },
258
+ agents: Object.fromEntries(
259
+ AGENTS.map((name) => [
260
+ name,
261
+ {
262
+ enabled: true,
263
+ model: modelMapping[name],
264
+ mode: AGENT_ROLES[name].mode,
265
+ },
266
+ ])
267
+ ),
268
+ rules: [
269
+ `.opencode/rules/universal.md`,
270
+ ],
123
271
  instructions: [
124
- ".ai-starter-kit/agents/orchestrator.md",
125
- ".ai-starter-kit/rules/universal.md",
272
+ "AGENTS.md",
273
+ `.opencode/rules/universal.md`,
126
274
  ],
127
- agent: {
128
- orchestrator: {
129
- description: "Primary orchestrator — routes tasks to specialist sub-agents",
130
- mode: "primary",
131
- prompt: "You are the orchestrator. Read .ai-starter-kit/context/ for active context. Follow the workflow: plan -> implement -> test -> verify.",
132
- tools: { "*": true },
133
- permission: {
134
- task: { "*": "allow" },
135
- },
136
- },
137
- planner: {
138
- description: "Architect & planner — breaks objectives into steps",
139
- mode: "subagent",
140
- prompt: "You are the planner agent. Produce step-by-step implementation plans. Do not write code. Output plans as numbered markdown lists.",
141
- tools: { write: false, edit: false, bash: false },
142
- },
143
- developer: {
144
- description: "Developer — writes and edits production code",
145
- mode: "subagent",
146
- prompt: "You are the developer agent. Implement code changes per the plan. Follow project conventions. Write clean, tested code.",
147
- tools: { "*": true },
148
- },
149
- qa: {
150
- description: "QA / Tester — runs tests, verifies correctness",
151
- mode: "subagent",
152
- prompt: "You are the QA agent. Run tests, check for regressions, verify acceptance criteria. Report pass/fail status clearly.",
153
- tools: { write: false },
154
- permission: { bash: "allow" },
155
- },
156
- reviewer: {
157
- description: "Code reviewer — reviews for quality, security, performance",
158
- mode: "subagent",
159
- prompt: "You are the reviewer agent. Review code for correctness, security issues, performance problems, and style violations. Do not make changes.",
160
- tools: { write: false, edit: false, bash: false },
161
- },
162
- docs: {
163
- description: "Documentation — keeps docs aligned with code changes",
164
- mode: "subagent",
165
- prompt: "You are the docs agent. Update documentation to reflect code changes. Keep README, API docs, and inline comments accurate.",
166
- tools: { bash: false },
167
- },
275
+ orchestrator: {
276
+ workflowSteps: WORKFLOW_STEPS,
277
+ requirePlanApproval: false,
168
278
  },
169
279
  };
170
280
  }
171
281
 
282
+ function buildOpencodeJson(modelMapping, preset) {
283
+ const agents = {};
284
+ for (const role of AGENTS) {
285
+ const def = AGENT_ROLES[role];
286
+ agents[role] = {
287
+ description: def.description,
288
+ mode: def.mode,
289
+ model: modelMapping[role],
290
+ temperature: def.temperature,
291
+ prompt: buildAgentPrompt(role, preset),
292
+ tools: def.tools,
293
+ };
294
+ if (Object.keys(def.permission).length > 0) {
295
+ agents[role].permission = def.permission;
296
+ }
297
+ }
298
+
299
+ return {
300
+ $schema: "https://opencode.ai/config.json",
301
+ model: modelMapping.orchestrator,
302
+ instructions: [
303
+ "AGENTS.md",
304
+ `.opencode/rules/universal.md`,
305
+ ],
306
+ agent: agents,
307
+ };
308
+ }
309
+
310
+ function buildAgentPrompt(role, preset) {
311
+ const prompts = {
312
+ orchestrator:
313
+ `You are the orchestrator. Read .opencode/context/ for active context. Follow the workflow: plan -> implement -> test -> verify. Preset: ${preset}.`,
314
+ planner:
315
+ "You are the planner agent. Produce step-by-step implementation plans. Do not write code. Output plans as numbered markdown lists.",
316
+ developer:
317
+ "You are the developer agent. Implement code changes per the plan. Follow project conventions. Write clean, tested code.",
318
+ qa:
319
+ "You are the QA agent. Run tests, check for regressions, verify acceptance criteria. Report pass/fail status clearly.",
320
+ reviewer:
321
+ "You are the reviewer agent. Review code for correctness, security issues, performance problems, and style violations. Do not make changes.",
322
+ docs:
323
+ "You are the docs agent. Update documentation to reflect code changes. Keep README, API docs, and inline comments accurate.",
324
+ };
325
+ return prompts[role] || `You are the ${role} agent.`;
326
+ }
327
+
328
+ /**
329
+ * Patch the `model:` field in a markdown agent file's YAML frontmatter.
330
+ * If no model field exists, add one.
331
+ */
332
+ function patchAgentModelFrontmatter(content, modelId) {
333
+ // Check if content has YAML frontmatter
334
+ if (!content.startsWith("---")) return content;
335
+
336
+ const endIdx = content.indexOf("---", 3);
337
+ if (endIdx === -1) return content;
338
+
339
+ let frontmatter = content.slice(3, endIdx);
340
+ const body = content.slice(endIdx);
341
+
342
+ if (/^model:/m.test(frontmatter)) {
343
+ // Replace existing model line
344
+ frontmatter = frontmatter.replace(/^model:.*$/m, `model: ${modelId}`);
345
+ } else {
346
+ // Add model field after description or at the end
347
+ const lines = frontmatter.split("\n");
348
+ const descIdx = lines.findIndex((l) => l.startsWith("description:"));
349
+ if (descIdx >= 0) {
350
+ lines.splice(descIdx + 1, 0, `model: ${modelId}`);
351
+ } else {
352
+ lines.push(`model: ${modelId}`);
353
+ }
354
+ frontmatter = lines.join("\n");
355
+ }
356
+
357
+ return `---${frontmatter}${body}`;
358
+ }
359
+
172
360
  function buildRalphyConfig() {
173
- return `# Ralphy configuration generated by opencode-auto-agent
361
+ return `# Ralphy configuration \u2014 generated by opencode-auto-agent
174
362
  project:
175
363
  name: ""
176
364
  language: ""
@@ -186,5 +374,40 @@ rules:
186
374
  - "Follow the plan produced by the planner agent before implementing."
187
375
  - "Run tests after every implementation step."
188
376
  - "Do not modify files outside the project source tree."
377
+
378
+ boundaries:
379
+ never_touch:
380
+ - ".env"
381
+ - ".env.*"
382
+ - "secrets/**"
383
+ - "**/*.pem"
384
+ - "**/*.key"
385
+ - "node_modules/**"
386
+ - ".git/**"
387
+ - "dist/**"
388
+ - "build/**"
389
+
390
+ capabilities:
391
+ browser: "auto"
392
+
393
+ notifications:
394
+ discord_webhook: ""
395
+ slack_webhook: ""
396
+ custom_webhook: ""
189
397
  `;
190
398
  }
399
+
400
+ // ── Helpers ────────────────────────────────────────────────────────────────
401
+
402
+ function presetDescription(name) {
403
+ const desc = {
404
+ java: "General Java project (Maven/Gradle)",
405
+ springboot: "Spring Boot 3.x web application",
406
+ nextjs: "Next.js 14+ with App Router",
407
+ };
408
+ return desc[name] || "";
409
+ }
410
+
411
+ function padRole(role) {
412
+ return role.padEnd(14);
413
+ }