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,26 +1,22 @@
1
1
  /**
2
2
  * Shared constants for ai-starter-kit.
3
+ *
4
+ * Defines the agent ecosystem, model defaults, presets, and directory layout.
5
+ * All values here align with OpenCode's official concepts.
3
6
  */
4
7
 
5
- /** Name of the folder scaffolded into target repos. */
6
- export const SCAFFOLD_DIR = ".ai-starter-kit";
8
+ /** Name of the folder scaffolded into target repos (.opencode is OpenCode's convention). */
9
+ export const SCAFFOLD_DIR = ".opencode";
7
10
 
8
- /** Filename for the kit configuration. */
11
+ /** Filename for the kit configuration inside .opencode/. */
9
12
  export const CONFIG_FILE = "config.json";
10
13
 
14
+ /** Version written into scaffolded config for upgrade detection. */
15
+ export const SCHEMA_VERSION = "0.2.0";
16
+
11
17
  /** Known presets shipped with the package. */
12
18
  export const PRESETS = ["java", "springboot", "nextjs"];
13
19
 
14
- /** Standard agent names (order matters — this is the agent team). */
15
- export const AGENTS = [
16
- "orchestrator",
17
- "planner",
18
- "developer",
19
- "qa",
20
- "reviewer",
21
- "docs",
22
- ];
23
-
24
20
  /** Folder names inside the scaffold directory. */
25
21
  export const DIRS = {
26
22
  agents: "agents",
@@ -29,5 +25,85 @@ export const DIRS = {
29
25
  rules: "rules",
30
26
  };
31
27
 
32
- /** Version written into scaffolded config for upgrade detection. */
33
- export const SCHEMA_VERSION = "0.1.0";
28
+ // ── Agent definitions ──────────────────────────────────────────────────────
29
+
30
+ /**
31
+ * Agent role definitions with OpenCode-aligned metadata.
32
+ *
33
+ * Each role maps to an OpenCode agent (mode: primary | subagent).
34
+ * The `defaultModel` is a sensible default that can be overridden during init.
35
+ * `tools` and `permission` follow OpenCode's tool/permission schema.
36
+ */
37
+ export const AGENT_ROLES = {
38
+ orchestrator: {
39
+ description: "Primary orchestrator — routes tasks to specialist sub-agents",
40
+ mode: "primary",
41
+ defaultModel: "google/gemini-3-pro",
42
+ temperature: 0.2,
43
+ tools: { "*": true },
44
+ permission: {
45
+ task: { "*": "allow" },
46
+ edit: "deny",
47
+ bash: "ask",
48
+ todoread: "allow",
49
+ todowrite: "allow",
50
+ },
51
+ },
52
+ planner: {
53
+ description: "Architect & planner — breaks objectives into steps",
54
+ mode: "subagent",
55
+ defaultModel: "google/gemini-3-pro",
56
+ temperature: 0.2,
57
+ tools: { write: false, edit: false, bash: false },
58
+ permission: {},
59
+ },
60
+ developer: {
61
+ description: "Developer — writes and edits production code",
62
+ mode: "subagent",
63
+ defaultModel: "openai/gpt-5.1-codex",
64
+ temperature: 0.3,
65
+ tools: { "*": true },
66
+ permission: { bash: "allow" },
67
+ },
68
+ qa: {
69
+ description: "QA / Tester — runs tests, verifies correctness",
70
+ mode: "subagent",
71
+ defaultModel: "anthropic/claude-sonnet-4-5",
72
+ temperature: 0.1,
73
+ tools: { write: false, edit: false },
74
+ permission: { bash: "allow" },
75
+ },
76
+ reviewer: {
77
+ description: "Code reviewer — reviews for quality, security, performance",
78
+ mode: "subagent",
79
+ defaultModel: "anthropic/claude-sonnet-4-5",
80
+ temperature: 0.1,
81
+ tools: { write: false, edit: false, bash: false },
82
+ permission: {},
83
+ },
84
+ docs: {
85
+ description: "Documentation — keeps docs aligned with code changes",
86
+ mode: "subagent",
87
+ defaultModel: "anthropic/claude-sonnet-4-5",
88
+ temperature: 0.3,
89
+ tools: { bash: false },
90
+ permission: {},
91
+ },
92
+ };
93
+
94
+ /** Standard agent names in workflow order. */
95
+ export const AGENTS = Object.keys(AGENT_ROLES);
96
+
97
+ /** Map of role -> sensible default model ID (used when discovered models don't overlap). */
98
+ export const DEFAULT_MODEL_MAP = Object.fromEntries(
99
+ Object.entries(AGENT_ROLES).map(([role, def]) => [role, def.defaultModel])
100
+ );
101
+
102
+ // ── OpenCode workflow ──────────────────────────────────────────────────────
103
+
104
+ /** Standard workflow steps the orchestrator enforces. */
105
+ export const WORKFLOW_STEPS = ["plan", "implement", "test", "verify"];
106
+
107
+ /** Package name and version for CLI help text. */
108
+ export const PKG_NAME = "opencode-auto-agent";
109
+ export const PKG_ALIAS = "aisk";
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Model discovery — runs `opencode models` and parses the output
3
+ * to discover available models from the user's configured providers.
4
+ *
5
+ * Models are returned in the OpenCode format: provider/model-id
6
+ */
7
+
8
+ import { execSync } from "node:child_process";
9
+ import { createSpinner, c } from "./ui.js";
10
+
11
+ /**
12
+ * @typedef {Object} DiscoveredModel
13
+ * @property {string} id - Full model ID (e.g. "openai/gpt-5.1-codex")
14
+ * @property {string} provider - Provider name (e.g. "openai")
15
+ * @property {string} model - Model name (e.g. "gpt-5.1-codex")
16
+ */
17
+
18
+ /**
19
+ * Discover available models by running `opencode models`.
20
+ * Returns a parsed list of model objects.
21
+ *
22
+ * @returns {DiscoveredModel[]}
23
+ */
24
+ export function discoverModels() {
25
+ let raw;
26
+ try {
27
+ raw = execSync("opencode models", {
28
+ encoding: "utf8",
29
+ stdio: ["pipe", "pipe", "pipe"],
30
+ timeout: 30_000,
31
+ });
32
+ } catch (err) {
33
+ // If opencode is not installed or fails, return empty
34
+ return [];
35
+ }
36
+
37
+ return parseModelsOutput(raw);
38
+ }
39
+
40
+ /**
41
+ * Parse the text output of `opencode models` into structured data.
42
+ * The output format is typically lines containing provider/model pairs.
43
+ *
44
+ * @param {string} raw
45
+ * @returns {DiscoveredModel[]}
46
+ */
47
+ export function parseModelsOutput(raw) {
48
+ const models = [];
49
+ const seen = new Set();
50
+
51
+ for (const line of raw.split("\n")) {
52
+ const trimmed = line.trim();
53
+ if (!trimmed) continue;
54
+
55
+ // Skip header/decorative lines
56
+ if (trimmed.startsWith("─") || trimmed.startsWith("=") || trimmed.startsWith("-")) continue;
57
+ if (/^(provider|model|name|id)/i.test(trimmed)) continue;
58
+ if (trimmed.startsWith("#")) continue;
59
+
60
+ // Try to extract provider/model pattern
61
+ // Common formats:
62
+ // provider/model-name
63
+ // provider/model-name (some description)
64
+ // provider model-name description
65
+ const slashMatch = trimmed.match(/^([a-zA-Z0-9_-]+\/[a-zA-Z0-9._-]+)/);
66
+ if (slashMatch) {
67
+ const id = slashMatch[1];
68
+ if (!seen.has(id)) {
69
+ seen.add(id);
70
+ const [provider, ...modelParts] = id.split("/");
71
+ models.push({
72
+ id,
73
+ provider,
74
+ model: modelParts.join("/"),
75
+ });
76
+ }
77
+ continue;
78
+ }
79
+
80
+ // Try tab/space separated: provider model
81
+ const parts = trimmed.split(/\s{2,}|\t+/).filter(Boolean);
82
+ if (parts.length >= 2) {
83
+ const candidate = `${parts[0]}/${parts[1]}`;
84
+ // Validate it looks like provider/model
85
+ if (/^[a-zA-Z0-9_-]+\/[a-zA-Z0-9._-]+$/.test(candidate) && !seen.has(candidate)) {
86
+ seen.add(candidate);
87
+ models.push({
88
+ id: candidate,
89
+ provider: parts[0],
90
+ model: parts[1],
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ return models;
97
+ }
98
+
99
+ /**
100
+ * Discover models with a spinner and status output.
101
+ * @returns {DiscoveredModel[]}
102
+ */
103
+ export function discoverModelsInteractive() {
104
+ const spinner = createSpinner("Discovering available models via opencode...");
105
+ spinner.start();
106
+
107
+ const models = discoverModels();
108
+
109
+ if (models.length === 0) {
110
+ spinner.fail("No models found. Is opencode configured with at least one provider?");
111
+ } else {
112
+ spinner.stop(`Found ${c.bold(String(models.length))} models from ${c.bold(String(countProviders(models)))} providers`);
113
+ }
114
+
115
+ return models;
116
+ }
117
+
118
+ /**
119
+ * Group models by provider.
120
+ * @param {DiscoveredModel[]} models
121
+ * @returns {Map<string, DiscoveredModel[]>}
122
+ */
123
+ export function groupByProvider(models) {
124
+ const groups = new Map();
125
+ for (const m of models) {
126
+ if (!groups.has(m.provider)) groups.set(m.provider, []);
127
+ groups.get(m.provider).push(m);
128
+ }
129
+ return groups;
130
+ }
131
+
132
+ /**
133
+ * Count unique providers.
134
+ * @param {DiscoveredModel[]} models
135
+ * @returns {number}
136
+ */
137
+ function countProviders(models) {
138
+ return new Set(models.map((m) => m.provider)).size;
139
+ }
140
+
141
+ /**
142
+ * Find the best matching model from the available list given a preferred ID.
143
+ * Falls back to fuzzy matching on the model name portion.
144
+ *
145
+ * @param {DiscoveredModel[]} models
146
+ * @param {string} preferredId - e.g. "openai/gpt-5.1-codex"
147
+ * @returns {DiscoveredModel|null}
148
+ */
149
+ export function findModel(models, preferredId) {
150
+ // Exact match
151
+ const exact = models.find((m) => m.id === preferredId);
152
+ if (exact) return exact;
153
+
154
+ // Match by model name only (ignoring provider)
155
+ const modelName = preferredId.includes("/") ? preferredId.split("/").slice(1).join("/") : preferredId;
156
+ const byName = models.find((m) => m.model === modelName);
157
+ if (byName) return byName;
158
+
159
+ // Fuzzy: model name contains the search term
160
+ const fuzzy = models.find((m) => m.model.includes(modelName) || m.id.includes(preferredId));
161
+ return fuzzy || null;
162
+ }
163
+
164
+ /**
165
+ * Validate that a model ID exists in the discovered models.
166
+ * @param {DiscoveredModel[]} models
167
+ * @param {string} modelId
168
+ * @returns {boolean}
169
+ */
170
+ export function isValidModel(models, modelId) {
171
+ return findModel(models, modelId) !== null;
172
+ }
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Prerequisite validation — checks that required tools are installed
3
+ * and accessible before allowing initialization or execution to proceed.
4
+ *
5
+ * Rules:
6
+ * - Never auto-install anything
7
+ * - Abort early with clear messages if required tools are missing
8
+ * - Distinguish between hard requirements (abort) and soft requirements (warn)
9
+ */
10
+
11
+ import { execSync } from "node:child_process";
12
+ import { status, error, c } from "./ui.js";
13
+
14
+ /**
15
+ * @typedef {Object} ToolCheck
16
+ * @property {string} name - Tool name
17
+ * @property {string} command - Command to check (e.g. "opencode --version")
18
+ * @property {boolean} ok - Whether the tool was found
19
+ * @property {string} version - Detected version string
20
+ * @property {string} hint - Install hint if missing
21
+ */
22
+
23
+ /**
24
+ * Check if a CLI tool is available and return its version.
25
+ *
26
+ * @param {string} name - Display name
27
+ * @param {string} command - Command to run for version check
28
+ * @param {string} installHint - How to install if missing
29
+ * @returns {ToolCheck}
30
+ */
31
+ export function checkTool(name, command, installHint) {
32
+ try {
33
+ const output = execSync(command, {
34
+ encoding: "utf8",
35
+ stdio: ["pipe", "pipe", "pipe"],
36
+ timeout: 10_000,
37
+ }).trim();
38
+
39
+ // Try to extract version number
40
+ const vMatch = output.match(/v?([\d]+\.[\d]+[\d.]*)/);
41
+ const version = vMatch ? vMatch[1] : output.slice(0, 40);
42
+
43
+ return { name, command, ok: true, version, hint: installHint };
44
+ } catch {
45
+ return { name, command, ok: false, version: "", hint: installHint };
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Check Node.js version meets minimum requirement.
51
+ * @param {number} minMajor - Minimum major version (default 18)
52
+ * @returns {ToolCheck}
53
+ */
54
+ export function checkNodeVersion(minMajor = 18) {
55
+ const current = process.versions.node;
56
+ const major = parseInt(current.split(".")[0], 10);
57
+ return {
58
+ name: `Node.js (>=${minMajor})`,
59
+ command: "node --version",
60
+ ok: major >= minMajor,
61
+ version: current,
62
+ hint: `Node.js ${minMajor}+ is required. Current: ${current}`,
63
+ };
64
+ }
65
+
66
+ /**
67
+ * Run all prerequisite checks and return results.
68
+ *
69
+ * @param {Object} options
70
+ * @param {boolean} options.requireRalphy - Whether ralphy is a hard requirement
71
+ * @returns {{ checks: ToolCheck[], allPassed: boolean, hardFailed: boolean }}
72
+ */
73
+ export function validatePrerequisites({ requireRalphy = false } = {}) {
74
+ const checks = [];
75
+
76
+ // 1. Node.js version
77
+ checks.push(checkNodeVersion(18));
78
+
79
+ // 2. OpenCode CLI (always required)
80
+ checks.push(checkTool(
81
+ "OpenCode CLI",
82
+ "opencode --version",
83
+ "Install: npm install -g opencode-ai | https://opencode.ai/docs"
84
+ ));
85
+
86
+ // 3. Ralphy CLI (conditional)
87
+ checks.push(checkTool(
88
+ "Ralphy CLI",
89
+ "ralphy --version",
90
+ "Install: npm install -g ralphy-cli | https://github.com/michaelshimeles/ralphy"
91
+ ));
92
+
93
+ // 4. Git (nice to have)
94
+ checks.push(checkTool(
95
+ "Git",
96
+ "git --version",
97
+ "Install Git: https://git-scm.com"
98
+ ));
99
+
100
+ // Determine hard failures
101
+ const hardRequired = ["Node.js", "OpenCode CLI"];
102
+ if (requireRalphy) hardRequired.push("Ralphy CLI");
103
+
104
+ const hardFailed = checks.some(
105
+ (ch) => !ch.ok && hardRequired.some((name) => ch.name.startsWith(name))
106
+ );
107
+ const allPassed = checks.every((ch) => ch.ok);
108
+
109
+ return { checks, allPassed, hardFailed };
110
+ }
111
+
112
+ /**
113
+ * Run prerequisite checks and print results to terminal.
114
+ * Aborts (process.exit) if hard requirements fail.
115
+ *
116
+ * @param {Object} options
117
+ * @param {boolean} options.requireRalphy
118
+ * @param {boolean} options.abortOnFail - Whether to exit on hard failure (default true)
119
+ * @returns {{ checks: ToolCheck[], allPassed: boolean }}
120
+ */
121
+ export function validateAndReport({ requireRalphy = false, abortOnFail = true } = {}) {
122
+ const result = validatePrerequisites({ requireRalphy });
123
+
124
+ for (const ch of result.checks) {
125
+ if (ch.ok) {
126
+ status("pass", `${ch.name}`, `v${ch.version}`);
127
+ } else {
128
+ // Is this a hard failure?
129
+ const isHard = ch.name.startsWith("Node.js") ||
130
+ ch.name.startsWith("OpenCode") ||
131
+ (requireRalphy && ch.name.startsWith("Ralphy"));
132
+ if (isHard) {
133
+ status("fail", `${ch.name}`, "not found");
134
+ console.log(` ${c.gray(ch.hint)}`);
135
+ } else {
136
+ status("warn", `${ch.name}`, "not found (optional)");
137
+ console.log(` ${c.gray(ch.hint)}`);
138
+ }
139
+ }
140
+ }
141
+
142
+ if (result.hardFailed && abortOnFail) {
143
+ error(
144
+ "Required tools are missing. Cannot proceed.",
145
+ "Install the missing tools above and try again."
146
+ );
147
+ process.exit(1);
148
+ }
149
+
150
+ return result;
151
+ }