opencode-herdr-orchestration 0.1.4 → 0.1.5

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 CHANGED
@@ -19,6 +19,7 @@ The installer:
19
19
 
20
20
  - locates the global OpenCode config directory;
21
21
  - installs an exact package version there;
22
+ - interactively asks which models the shepherd, sheep worker, and shearer reviewer roles should use when run in a terminal;
22
23
  - preserves existing JSONC comments, trailing commas, plugins, and tuple options;
23
24
  - adds the stable file URL required by current OpenCode npm plugin loading;
24
25
  - creates timestamped backups of changed config and npm manifest files;
@@ -35,12 +36,15 @@ Inspect or remove the installation:
35
36
 
36
37
  ```bash
37
38
  npx -y opencode-herdr-orchestration@latest status
39
+ npx -y opencode-herdr-orchestration@latest configure-agents
38
40
  npx -y opencode-herdr-orchestration@latest uninstall
39
41
  npx -y opencode-herdr-orchestration@latest uninstall --with-hooks
40
42
  ```
41
43
 
42
44
  After installation or update, quit and restart OpenCode intentionally when ready. Existing processes keep their already-loaded configuration.
43
45
 
46
+ `configure-agents` updates the same model choices after installation. Press Enter to keep the displayed value, or enter `-` to restore that role's package default. Model names must include their provider prefix, such as `anthropic/claude-sonnet-4-6`.
47
+
44
48
  ## Manual Installation
45
49
 
46
50
  Add the published package to the global OpenCode configuration at `~/.config/opencode/opencode.json` or `~/.config/opencode/opencode.jsonc`:
@@ -148,6 +152,7 @@ Plugin tuple options can override model defaults:
148
152
  [
149
153
  "opencode-herdr-orchestration",
150
154
  {
155
+ "shepherdModel": "anthropic/claude-sonnet-4-6",
151
156
  "workerModel": "litellm/glm-5.3-flash",
152
157
  "reviewerModel": "litellm-responses/gpt-5.6-terra"
153
158
  }
@@ -1,16 +1,19 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { chmodSync, copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
3
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { execFileSync } from "node:child_process";
8
+ import { createInterface } from "node:readline/promises";
8
9
  import {
9
10
  configDirectory,
11
+ findConfigFile,
10
12
  installPackage,
11
13
  latestVersion,
12
14
  PACKAGE_NAME,
13
15
  packageVersion,
16
+ orchestrationOptions,
14
17
  restoreBackup,
15
18
  status as installationStatus,
16
19
  uninstallPackage,
@@ -24,6 +27,12 @@ const sourceHook = join(packageRoot, "hooks", "pre-push");
24
27
  const installedHook = join(hooksPath, "pre-push");
25
28
  const [command, ...flags] = process.argv.slice(2);
26
29
 
30
+ const MODEL_ROLES = [
31
+ ["shepherdModel", "Shepherd agents", "OpenCode active model"],
32
+ ["workerModel", "Sheep worker agents", "litellm/glm-5.3-flash"],
33
+ ["reviewerModel", "Shearer review agents", "litellm-responses/gpt-5.6-terra"],
34
+ ];
35
+
27
36
  function git(args, options = {}) {
28
37
  return execFileSync("git", args, { encoding: "utf8", windowsHide: true, ...options }).trim();
29
38
  }
@@ -59,11 +68,41 @@ function uninstallHooks() {
59
68
  }
60
69
  }
61
70
 
62
- function installOrUpdate(useLatest) {
71
+ function currentOptions(configDir) {
72
+ const file = findConfigFile(configDir);
73
+ return existsSync(file) ? orchestrationOptions(readFileSync(file, "utf8")) : {};
74
+ }
75
+
76
+ async function promptForModels(configDir) {
77
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
78
+ throw new Error("Agent model configuration requires an interactive terminal.");
79
+ }
80
+ const existing = currentOptions(configDir);
81
+ const answers = {};
82
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
83
+ process.stdout.write("Configure agent models. Press Enter to keep the shown value; enter - to restore the package default.\n");
84
+ try {
85
+ for (const [key, label, packageDefault] of MODEL_ROLES) {
86
+ const shown = existing[key] || packageDefault;
87
+ const answer = (await prompt.question(`${label} [${shown}]: `)).trim();
88
+ const value = answer === "-" ? "" : (answer || existing[key] || "");
89
+ if (value && !value.includes("/")) throw new Error(`${label} model must include a provider prefix, for example provider/model.`);
90
+ answers[key] = value;
91
+ }
92
+ } finally {
93
+ prompt.close();
94
+ }
95
+ return answers;
96
+ }
97
+
98
+ async function installOrUpdate(useLatest) {
63
99
  const configDir = configDirectory();
100
+ const models = command === "install" && process.stdin.isTTY && process.stdout.isTTY
101
+ ? await promptForModels(configDir)
102
+ : undefined;
64
103
  const version = useLatest ? latestVersion() : packageVersion(packageRoot);
65
104
  const packageBackups = installPackage(configDir, version);
66
- const result = writePluginConfig(configDir);
105
+ const result = writePluginConfig(configDir, false, models);
67
106
  try {
68
107
  validateOpenCode(configDir);
69
108
  } catch (error) {
@@ -77,6 +116,24 @@ function installOrUpdate(useLatest) {
77
116
  process.stdout.write("Restart OpenCode intentionally to load the new configuration.\n");
78
117
  }
79
118
 
119
+ async function configureAgents() {
120
+ const configDir = configDirectory();
121
+ if (!existsSync(join(configDir, "node_modules", PACKAGE_NAME, "package.json"))) {
122
+ throw new Error(`${PACKAGE_NAME} is not installed. Run the install command first.`);
123
+ }
124
+ const models = await promptForModels(configDir);
125
+ const result = writePluginConfig(configDir, false, models);
126
+ try {
127
+ validateOpenCode(configDir);
128
+ } catch (error) {
129
+ restoreBackup(result.file, result.backup, result.existed);
130
+ throw new Error(`OpenCode validation failed; restored the previous config. ${error.message}`);
131
+ }
132
+ process.stdout.write(`Configured agent models in ${result.file}\n`);
133
+ if (result.backup) process.stdout.write(`Backup: ${result.backup}\n`);
134
+ process.stdout.write("Restart OpenCode intentionally to load the new configuration.\n");
135
+ }
136
+
80
137
  function uninstallOrchestration() {
81
138
  const configDir = configDirectory();
82
139
  const result = writePluginConfig(configDir, true);
@@ -102,14 +159,15 @@ function captureHookStatus() {
102
159
  }
103
160
 
104
161
  try {
105
- if (command === "install") installOrUpdate(false);
106
- else if (command === "update") installOrUpdate(true);
162
+ if (command === "install") await installOrUpdate(false);
163
+ else if (command === "update") await installOrUpdate(true);
164
+ else if (command === "configure-agents") await configureAgents();
107
165
  else if (command === "uninstall") uninstallOrchestration();
108
166
  else if (command === "install-hooks") installHooks();
109
167
  else if (command === "uninstall-hooks") uninstallHooks();
110
168
  else if (command === "status") fullStatus();
111
169
  else {
112
- process.stderr.write("Usage: opencode-herdr-orchestration <install|update|status|uninstall|install-hooks|uninstall-hooks> [--with-hooks] [--force]\n");
170
+ process.stderr.write("Usage: opencode-herdr-orchestration <install|update|configure-agents|status|uninstall|install-hooks|uninstall-hooks> [--with-hooks] [--force]\n");
113
171
  process.exitCode = 2;
114
172
  }
115
173
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-herdr-orchestration",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Capability-separated Herdr orchestration agents for OpenCode",
5
5
  "author": "CodingJinxx",
6
6
  "repository": {
package/src/agents.js CHANGED
@@ -81,6 +81,7 @@ export function mergeAgent(defaults, override) {
81
81
  }
82
82
 
83
83
  export function createAgents(options = {}) {
84
+ const shepherdModel = options.shepherdModel;
84
85
  const workerModel = options.workerModel ?? "litellm/glm-5.3-flash";
85
86
  const reviewerModel = options.reviewerModel ?? "litellm-responses/gpt-5.6-terra";
86
87
  const shepherdBuildPermissions = options.shepherdBuildPermissions ?? {};
@@ -89,6 +90,7 @@ export function createAgents(options = {}) {
89
90
  return {
90
91
  "shepherd-plan": {
91
92
  mode: "primary",
93
+ ...(shepherdModel ? { model: shepherdModel } : {}),
92
94
  description: "Researches and presents implementation-ready plans through sheep-plan workers without implementing them.",
93
95
  prompt: SHEPHERD_PLAN_PROMPT,
94
96
  permission: {
@@ -135,6 +137,7 @@ export function createAgents(options = {}) {
135
137
 
136
138
  "shepherd-build": {
137
139
  mode: "primary",
140
+ ...(shepherdModel ? { model: shepherdModel } : {}),
138
141
  description: "Executes approved plans through planning, implementation, and independent review workers.",
139
142
  prompt: shepherdBuildPrompt,
140
143
  permission: {
package/src/installer.js CHANGED
@@ -65,6 +65,38 @@ export function updatePluginConfig(text, entry, remove = false) {
65
65
  return source;
66
66
  }
67
67
 
68
+ export function orchestrationOptions(text) {
69
+ const config = parse(text || "{}", [], { allowTrailingComma: true, disallowComments: false });
70
+ const entry = Array.isArray(config?.plugin)
71
+ ? config.plugin.find((candidate) => isOrchestrationPlugin(candidate))
72
+ : undefined;
73
+ return Array.isArray(entry) && entry[1] && typeof entry[1] === "object" ? entry[1] : {};
74
+ }
75
+
76
+ export function updateAgentModels(text, entry, models) {
77
+ const original = parse(text || "{}", [], { allowTrailingComma: true, disallowComments: false });
78
+ const matches = Array.isArray(original?.plugin)
79
+ ? original.plugin.map((candidate, index) => isOrchestrationPlugin(candidate) ? index : -1).filter((index) => index >= 0)
80
+ : [];
81
+ let updated = text || "{}";
82
+ const formattingOptions = { insertSpaces: true, tabSize: 2, eol: updated.includes("\r\n") ? "\r\n" : "\n" };
83
+ if (matches.length === 1 && Array.isArray(original.plugin[matches[0]])) {
84
+ updated = applyEdits(updated, modify(updated, ["plugin", matches[0], 0], entry, { formattingOptions }));
85
+ } else {
86
+ updated = updatePluginConfig(updated, entry);
87
+ }
88
+ const config = parse(updated, [], { allowTrailingComma: true, disallowComments: false });
89
+ const index = config.plugin.findIndex((candidate) => isOrchestrationPlugin(candidate));
90
+ if (!Array.isArray(config.plugin[index])) {
91
+ const options = Object.fromEntries(Object.entries(models).filter(([, value]) => value));
92
+ return applyEdits(updated, modify(updated, ["plugin", index], [entry, options], { formattingOptions }));
93
+ }
94
+ for (const [key, value] of Object.entries(models)) {
95
+ updated = applyEdits(updated, modify(updated, ["plugin", index, 1, key], value || undefined, { formattingOptions }));
96
+ }
97
+ return updated;
98
+ }
99
+
68
100
  export function findConfigFile(configDir) {
69
101
  for (const name of ["opencode.jsonc", "opencode.json"]) {
70
102
  const candidate = join(configDir, name);
@@ -132,12 +164,14 @@ export function uninstallPackage(configDir) {
132
164
  return backups;
133
165
  }
134
166
 
135
- export function writePluginConfig(configDir, remove = false) {
167
+ export function writePluginConfig(configDir, remove = false, models) {
136
168
  mkdirSync(configDir, { recursive: true });
137
169
  const file = findConfigFile(configDir);
138
170
  const existed = existsSync(file);
139
171
  const previous = existed ? readFileSync(file, "utf8") : '{\n "$schema": "https://opencode.ai/config.json"\n}\n';
140
- const next = updatePluginConfig(previous, packageEntry(configDir), remove);
172
+ const next = models && !remove
173
+ ? updateAgentModels(previous, packageEntry(configDir), models)
174
+ : updatePluginConfig(previous, packageEntry(configDir), remove);
141
175
  if (next === previous) return { file, backup: null, changed: false, existed };
142
176
  const backup = backupFile(file);
143
177
  writeFileSync(file, next, "utf8");