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.
- package/README.md +24 -6
- package/bin/cli.js +275 -51
- package/package.json +1 -1
- package/src/commands/doctor.js +337 -54
- package/src/commands/init.js +337 -114
- package/src/commands/run.js +182 -61
- package/src/commands/setup.js +321 -21
- package/src/lib/constants.js +91 -15
- package/src/lib/models.js +172 -0
- package/src/lib/prerequisites.js +151 -0
- package/src/lib/ui.js +446 -0
- package/templates/agents/developer.md +1 -1
- package/templates/agents/docs.md +1 -1
- package/templates/agents/orchestrator.md +4 -4
- package/templates/agents/planner.md +1 -1
- package/templates/agents/qa.md +1 -1
- package/templates/agents/reviewer.md +1 -1
- package/templates/sample-config.json +47 -8
package/src/lib/constants.js
CHANGED
|
@@ -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 = ".
|
|
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
|
-
|
|
33
|
-
|
|
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
|
+
}
|