opencode-agenthub 0.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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +373 -0
  3. package/dist/composer/bootstrap.js +493 -0
  4. package/dist/composer/builtin-assets.js +139 -0
  5. package/dist/composer/capabilities.js +20 -0
  6. package/dist/composer/compose.js +824 -0
  7. package/dist/composer/defaults.js +10 -0
  8. package/dist/composer/home-transfer.js +288 -0
  9. package/dist/composer/install-home.js +5 -0
  10. package/dist/composer/library/README.md +93 -0
  11. package/dist/composer/library/bundles/auto.json +18 -0
  12. package/dist/composer/library/bundles/build.json +17 -0
  13. package/dist/composer/library/bundles/hr-adapter.json +26 -0
  14. package/dist/composer/library/bundles/hr-cto.json +24 -0
  15. package/dist/composer/library/bundles/hr-evaluator.json +26 -0
  16. package/dist/composer/library/bundles/hr-planner.json +26 -0
  17. package/dist/composer/library/bundles/hr-sourcer.json +24 -0
  18. package/dist/composer/library/bundles/hr-verifier.json +26 -0
  19. package/dist/composer/library/bundles/hr.json +35 -0
  20. package/dist/composer/library/bundles/plan.json +19 -0
  21. package/dist/composer/library/instructions/hr-boundaries.md +38 -0
  22. package/dist/composer/library/instructions/hr-protocol.md +102 -0
  23. package/dist/composer/library/profiles/auto.json +9 -0
  24. package/dist/composer/library/profiles/hr.json +9 -0
  25. package/dist/composer/library/souls/auto.md +29 -0
  26. package/dist/composer/library/souls/build.md +21 -0
  27. package/dist/composer/library/souls/hr-adapter.md +64 -0
  28. package/dist/composer/library/souls/hr-cto.md +57 -0
  29. package/dist/composer/library/souls/hr-evaluator.md +64 -0
  30. package/dist/composer/library/souls/hr-planner.md +48 -0
  31. package/dist/composer/library/souls/hr-sourcer.md +70 -0
  32. package/dist/composer/library/souls/hr-verifier.md +62 -0
  33. package/dist/composer/library/souls/hr.md +186 -0
  34. package/dist/composer/library/souls/plan.md +23 -0
  35. package/dist/composer/library/workflow/auto-mode.json +139 -0
  36. package/dist/composer/model-utils.js +39 -0
  37. package/dist/composer/opencode-profile.js +2299 -0
  38. package/dist/composer/package-manager.js +75 -0
  39. package/dist/composer/package-version.js +20 -0
  40. package/dist/composer/platform.js +48 -0
  41. package/dist/composer/query.js +133 -0
  42. package/dist/composer/settings.js +400 -0
  43. package/dist/plugins/opencode-agenthub.js +310 -0
  44. package/dist/plugins/opencode-question.js +223 -0
  45. package/dist/plugins/plan-guidance.js +263 -0
  46. package/dist/plugins/runtime-config.js +57 -0
  47. package/dist/skills/agenthub-doctor/SKILL.md +238 -0
  48. package/dist/skills/agenthub-doctor/diagnose.js +213 -0
  49. package/dist/skills/agenthub-doctor/fix.js +293 -0
  50. package/dist/skills/agenthub-doctor/index.js +30 -0
  51. package/dist/skills/agenthub-doctor/interactive.js +756 -0
  52. package/dist/skills/hr-assembly/SKILL.md +121 -0
  53. package/dist/skills/hr-final-check/SKILL.md +98 -0
  54. package/dist/skills/hr-review/SKILL.md +100 -0
  55. package/dist/skills/hr-staffing/SKILL.md +85 -0
  56. package/dist/skills/hr-support/bin/sync_sources.py +560 -0
  57. package/dist/skills/hr-support/bin/validate_staged_package.py +290 -0
  58. package/dist/skills/hr-support/bin/vendor_stage_mcps.py +234 -0
  59. package/dist/skills/hr-support/bin/vendor_stage_skills.py +104 -0
  60. package/dist/types.js +11 -0
  61. package/package.json +54 -0
@@ -0,0 +1,756 @@
1
+ import readline from "node:readline/promises";
2
+ import { readdir, readFile, access, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import {
5
+ loadNativeOpenCodeConfig,
6
+ readAgentHubSettings,
7
+ writeAgentHubSettings
8
+ } from "../../composer/settings.js";
9
+ import {
10
+ fixMissingGuards,
11
+ createBundleForSoul,
12
+ createProfile,
13
+ resolveDefaultAgentForBundles
14
+ } from "./fix.js";
15
+ const pathExists = async (p) => {
16
+ try {
17
+ await access(p);
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ };
23
+ const readJson = async (filePath) => {
24
+ const content = await readFile(filePath, "utf-8");
25
+ return JSON.parse(content);
26
+ };
27
+ const writeJson = async (filePath, data) => {
28
+ await writeFile(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
29
+ };
30
+ async function interactiveDoctor(targetRoot, report) {
31
+ const rl = readline.createInterface({
32
+ input: process.stdin,
33
+ output: process.stdout
34
+ });
35
+ try {
36
+ while (true) {
37
+ process.stdout.write("\n");
38
+ process.stdout.write("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
39
+ process.stdout.write(" Agent Hub Doctor - Main Menu\n");
40
+ process.stdout.write("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n");
41
+ process.stdout.write("What do you want to do?\n\n");
42
+ process.stdout.write(" 1. Create a bundle\n");
43
+ process.stdout.write(" 2. Create a profile\n");
44
+ process.stdout.write(" 3. Assign skills to a bundle\n");
45
+ process.stdout.write(" 4. Add bundle(s) to a profile\n");
46
+ process.stdout.write(" 5. Apply/modify guards\n");
47
+ process.stdout.write(" 6. Fix setup issues\n");
48
+ process.stdout.write(" 7. Show current structure\n");
49
+ process.stdout.write(" 8. Manage agent models\n");
50
+ process.stdout.write(" 9. Exit\n\n");
51
+ const choice = await rl.question("Select [1-9]: ");
52
+ const choiceNum = parseInt(choice.trim(), 10);
53
+ switch (choiceNum) {
54
+ case 1:
55
+ await createBundleFlow(rl, targetRoot);
56
+ break;
57
+ case 2:
58
+ await createProfileFlow(rl, targetRoot);
59
+ break;
60
+ case 3:
61
+ await assignSkillsFlow(rl, targetRoot);
62
+ break;
63
+ case 4:
64
+ await addBundleToProfileFlow(rl, targetRoot);
65
+ break;
66
+ case 5:
67
+ await applyGuardsFlow(rl, targetRoot);
68
+ break;
69
+ case 6:
70
+ await fixIssuesFlow(rl, targetRoot, report);
71
+ break;
72
+ case 7:
73
+ await showStructure(targetRoot);
74
+ break;
75
+ case 8:
76
+ await manageAgentModelsFlow(rl, targetRoot);
77
+ break;
78
+ case 9:
79
+ process.stdout.write("\nGoodbye!\n");
80
+ return;
81
+ default:
82
+ process.stdout.write("\nInvalid choice. Please select 1-9.\n");
83
+ }
84
+ }
85
+ } finally {
86
+ rl.close();
87
+ }
88
+ }
89
+ async function createBundleFlow(rl, targetRoot) {
90
+ process.stdout.write("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
91
+ process.stdout.write(" Create Bundle\n");
92
+ process.stdout.write("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n");
93
+ const souls = await getAvailableSouls(targetRoot);
94
+ if (souls.length === 0) {
95
+ process.stdout.write("No souls found. Import or create souls first.\n");
96
+ return;
97
+ }
98
+ process.stdout.write("Available souls:\n");
99
+ for (const soul of souls) {
100
+ process.stdout.write(` - ${soul}
101
+ `);
102
+ }
103
+ process.stdout.write("\n");
104
+ const soulName = await rl.question("Select soul: ");
105
+ if (!soulName.trim() || !souls.includes(soulName.trim())) {
106
+ process.stdout.write("Invalid soul selection. Cancelled.\n");
107
+ return;
108
+ }
109
+ process.stdout.write(
110
+ "\n\u26A0\uFE0F Note: Skills are globally mounted, not exclusive to this bundle.\n"
111
+ );
112
+ const skillsInput = await rl.question(
113
+ "Skills (comma-separated, or press Enter to skip): "
114
+ );
115
+ const additionalSkills = skillsInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
116
+ const mcps = await getAvailableMcps(targetRoot);
117
+ let selectedMcps = [];
118
+ if (mcps.length > 0) {
119
+ process.stdout.write("\nAvailable MCP servers:\n");
120
+ for (const mcp of mcps) {
121
+ process.stdout.write(` - ${mcp}
122
+ `);
123
+ }
124
+ process.stdout.write("\n");
125
+ const mcpsInput = await rl.question(
126
+ "Select MCP servers (comma-separated, or press Enter to skip): "
127
+ );
128
+ selectedMcps = mcpsInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
129
+ }
130
+ process.stdout.write(
131
+ "\n\u2705 Guards are per-agent and will actually restrict this agent's permissions.\n"
132
+ );
133
+ process.stdout.write("Available guards:\n");
134
+ process.stdout.write(" - read_only: Block edit, write, bash\n");
135
+ process.stdout.write(" - no_task: Block task tool\n");
136
+ process.stdout.write(" - no_subagent: Legacy alias for no_task\n");
137
+ process.stdout.write(" - no_omo: Block OMO multi-agent calls\n\n");
138
+ const guardsInput = await rl.question(
139
+ "Select guards (comma-separated, or press Enter for none): "
140
+ );
141
+ const selectedGuards = guardsInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
142
+ process.stdout.write("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n");
143
+ process.stdout.write("Agent Configuration\n");
144
+ process.stdout.write("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n");
145
+ const agentName = await rl.question(
146
+ `Agent name [default: ${soulName.trim()}]: `
147
+ );
148
+ const finalAgentName = agentName.trim() || soulName.trim();
149
+ const modeInput = await rl.question("Mode [primary/subagent, default: primary]: ");
150
+ const mode = modeInput.trim() === "subagent" ? "subagent" : "primary";
151
+ const modelInput = await rl.question(
152
+ "Model [default: github-copilot/claude-sonnet-4.5]: "
153
+ );
154
+ const model = modelInput.trim() || "github-copilot/claude-sonnet-4.5";
155
+ const result = await createBundleForSoul(targetRoot, soulName.trim(), {
156
+ agentName: finalAgentName,
157
+ mode,
158
+ skills: additionalSkills,
159
+ mcp: selectedMcps,
160
+ guards: selectedGuards,
161
+ model
162
+ });
163
+ process.stdout.write(`
164
+ ${result.success ? "\u2713" : "\u2717"} ${result.message}
165
+ `);
166
+ if (!result.success) return;
167
+ const addToProfile = await promptBoolean(
168
+ rl,
169
+ "\nAdd this bundle to a profile?",
170
+ true
171
+ );
172
+ if (addToProfile) {
173
+ await addBundleToProfileFlow(rl, targetRoot, soulName.trim());
174
+ }
175
+ }
176
+ async function createProfileFlow(rl, targetRoot) {
177
+ process.stdout.write("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
178
+ process.stdout.write(" Create Profile\n");
179
+ process.stdout.write("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n");
180
+ const bundles = await getAvailableBundles(targetRoot);
181
+ if (bundles.length === 0) {
182
+ process.stdout.write("No bundles found. Create bundles first.\n");
183
+ return;
184
+ }
185
+ process.stdout.write("Available bundles:\n");
186
+ for (const bundle of bundles) {
187
+ process.stdout.write(` - ${bundle}
188
+ `);
189
+ }
190
+ process.stdout.write("\n");
191
+ const bundlesInput = await rl.question(
192
+ "Select bundles (comma-separated): "
193
+ );
194
+ const selectedBundles = bundlesInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
195
+ if (selectedBundles.length === 0) {
196
+ process.stdout.write("No bundles selected. Cancelled.\n");
197
+ return;
198
+ }
199
+ const name = await rl.question("Profile name [default: imported]: ");
200
+ const profileName = name.trim() || "imported";
201
+ const description = await rl.question(
202
+ "Description (optional, press Enter to skip): "
203
+ );
204
+ process.stdout.write(`
205
+ Selected bundles: ${selectedBundles.join(", ")}
206
+ `);
207
+ const suggestedDefaultAgent = await resolveDefaultAgentForBundles(
208
+ targetRoot,
209
+ selectedBundles
210
+ );
211
+ const defaultAgentInput = await rl.question(
212
+ `Default agent [default: ${suggestedDefaultAgent}]: `
213
+ );
214
+ const defaultAgent = defaultAgentInput.trim() || suggestedDefaultAgent;
215
+ const result = await createProfile(targetRoot, profileName, {
216
+ bundleNames: selectedBundles,
217
+ description: description.trim() || "Auto-generated profile",
218
+ defaultAgent
219
+ });
220
+ process.stdout.write(`
221
+ ${result.success ? "\u2713" : "\u2717"} ${result.message}
222
+ `);
223
+ }
224
+ async function assignSkillsFlow(rl, targetRoot) {
225
+ process.stdout.write("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
226
+ process.stdout.write(" Assign Skills to Bundle\n");
227
+ process.stdout.write("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n");
228
+ process.stdout.write("\u26A0\uFE0F Important:\n");
229
+ process.stdout.write(" Skills are globally mounted to .opencode-agenthub/current/skills/\n");
230
+ process.stdout.write(" This is NOT 'exclusive' or 'isolated' per-agent.\n");
231
+ process.stdout.write(" Assignment only writes to the agent's config for organization.\n\n");
232
+ const profiles = await getAvailableProfiles(targetRoot);
233
+ if (profiles.length === 0) {
234
+ process.stdout.write("No profiles found. Create a profile first.\n");
235
+ return;
236
+ }
237
+ process.stdout.write("Available profiles:\n");
238
+ for (const profile2 of profiles) {
239
+ process.stdout.write(` - ${profile2}
240
+ `);
241
+ }
242
+ process.stdout.write("\n");
243
+ const profileName = await rl.question("Select profile: ");
244
+ if (!profileName.trim() || !profiles.includes(profileName.trim())) {
245
+ process.stdout.write("Invalid profile. Cancelled.\n");
246
+ return;
247
+ }
248
+ const profilePath = path.join(targetRoot, "profiles", `${profileName.trim()}.json`);
249
+ const profile = await readJson(profilePath);
250
+ const agents = profile.bundles || [];
251
+ if (agents.length === 0) {
252
+ process.stdout.write("No agents in this profile. Cancelled.\n");
253
+ return;
254
+ }
255
+ process.stdout.write("\nAgents in this profile:\n");
256
+ for (const agent of agents) {
257
+ process.stdout.write(` - ${agent}
258
+ `);
259
+ }
260
+ process.stdout.write("\n");
261
+ const agentName = await rl.question("Select agent: ");
262
+ if (!agentName.trim() || !agents.includes(agentName.trim())) {
263
+ process.stdout.write("Invalid agent. Cancelled.\n");
264
+ return;
265
+ }
266
+ const skills = await getAvailableSkills(targetRoot);
267
+ if (skills.length === 0) {
268
+ process.stdout.write("No skills found. Import skills first.\n");
269
+ return;
270
+ }
271
+ process.stdout.write("\nAvailable skills:\n");
272
+ for (const skill of skills) {
273
+ process.stdout.write(` - ${skill}
274
+ `);
275
+ }
276
+ process.stdout.write("\n");
277
+ const skillsInput = await rl.question(
278
+ "Select skills to add (comma-separated): "
279
+ );
280
+ const selectedSkills = skillsInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
281
+ if (selectedSkills.length === 0) {
282
+ process.stdout.write("No skills selected. Cancelled.\n");
283
+ return;
284
+ }
285
+ const bundlePath = path.join(
286
+ targetRoot,
287
+ "bundles",
288
+ `${agentName.trim()}.json`
289
+ );
290
+ const bundle = await readJson(bundlePath);
291
+ bundle.skills = unique([...bundle.skills || [], ...selectedSkills]);
292
+ await writeJson(bundlePath, bundle);
293
+ process.stdout.write(`
294
+ \u2713 Updated bundle skills: ${agentName.trim()}
295
+ `);
296
+ }
297
+ async function addBundleToProfileFlow(rl, targetRoot, preselectedBundle) {
298
+ process.stdout.write("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
299
+ process.stdout.write(" Add Bundle(s) to Profile\n");
300
+ process.stdout.write("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n");
301
+ const profiles = await getAvailableProfiles(targetRoot);
302
+ if (profiles.length === 0) {
303
+ process.stdout.write("No profiles found. Create a profile first.\n");
304
+ return;
305
+ }
306
+ process.stdout.write("Available profiles:\n");
307
+ for (const profile2 of profiles) {
308
+ process.stdout.write(` - ${profile2}
309
+ `);
310
+ }
311
+ process.stdout.write("\n");
312
+ const profileName = await rl.question("Select profile: ");
313
+ if (!profileName.trim() || !profiles.includes(profileName.trim())) {
314
+ process.stdout.write("Invalid profile. Cancelled.\n");
315
+ return;
316
+ }
317
+ const bundles = await getAvailableBundles(targetRoot);
318
+ const profilePath = path.join(targetRoot, "profiles", `${profileName.trim()}.json`);
319
+ const profile = await readJson(profilePath);
320
+ const existingBundles = profile.bundles || [];
321
+ const availableBundles = bundles.filter((b) => !existingBundles.includes(b));
322
+ if (availableBundles.length === 0) {
323
+ process.stdout.write("No new bundles to add.\n");
324
+ return;
325
+ }
326
+ process.stdout.write("\nAvailable bundles (not in profile):\n");
327
+ for (const bundle of availableBundles) {
328
+ process.stdout.write(` - ${bundle}
329
+ `);
330
+ }
331
+ process.stdout.write("\n");
332
+ let selectedBundles;
333
+ if (preselectedBundle) {
334
+ selectedBundles = [preselectedBundle];
335
+ } else {
336
+ const bundlesInput = await rl.question(
337
+ "Select bundles (comma-separated): "
338
+ );
339
+ selectedBundles = bundlesInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
340
+ }
341
+ if (selectedBundles.length === 0) {
342
+ process.stdout.write("No bundles selected. Cancelled.\n");
343
+ return;
344
+ }
345
+ profile.bundles = unique([...existingBundles, ...selectedBundles]);
346
+ await writeJson(profilePath, profile);
347
+ process.stdout.write(
348
+ `
349
+ \u2713 Added ${selectedBundles.length} bundle(s) to profile: ${profileName.trim()}
350
+ `
351
+ );
352
+ }
353
+ async function applyGuardsFlow(rl, targetRoot) {
354
+ process.stdout.write("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
355
+ process.stdout.write(" Apply / Modify Guards\n");
356
+ process.stdout.write("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n");
357
+ process.stdout.write("\u2705 Guards are per-agent permission controls.\n");
358
+ process.stdout.write("They will actually restrict what this agent can do.\n\n");
359
+ const bundles = await getAvailableBundles(targetRoot);
360
+ if (bundles.length === 0) {
361
+ process.stdout.write("No bundles found. Create bundles first.\n");
362
+ return;
363
+ }
364
+ process.stdout.write("Available bundles:\n");
365
+ for (const bundle2 of bundles) {
366
+ process.stdout.write(` - ${bundle2}
367
+ `);
368
+ }
369
+ process.stdout.write("\n");
370
+ const bundleName = await rl.question("Select bundle: ");
371
+ if (!bundleName.trim() || !bundles.includes(bundleName.trim())) {
372
+ process.stdout.write("Invalid bundle. Cancelled.\n");
373
+ return;
374
+ }
375
+ const bundlePath = path.join(targetRoot, "bundles", `${bundleName.trim()}.json`);
376
+ const bundle = await readJson(bundlePath);
377
+ const currentGuards = bundle.guards || [];
378
+ process.stdout.write(`
379
+ Current guards: ${currentGuards.length > 0 ? currentGuards.join(", ") : "none"}
380
+
381
+ `);
382
+ process.stdout.write("Available guards:\n");
383
+ process.stdout.write(" - read_only: Block edit, write, bash\n");
384
+ process.stdout.write(" - no_task: Block task tool\n");
385
+ process.stdout.write(" - no_subagent: Legacy alias for no_task\n");
386
+ process.stdout.write(" - no_omo: Block OMO multi-agent calls\n\n");
387
+ process.stdout.write("\u26A0\uFE0F blockedTools limitation:\n");
388
+ process.stdout.write(" Currently uses 'union blocking' strategy.\n");
389
+ process.stdout.write(" If ANY agent blocks a tool, it's blocked for ALL agents.\n");
390
+ process.stdout.write(" This is because the plugin cannot determine current agent in hooks.\n\n");
391
+ const guardsInput = await rl.question(
392
+ "Select guards (comma-separated, or press Enter for none): "
393
+ );
394
+ const selectedGuards = guardsInput.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
395
+ bundle.guards = selectedGuards.length > 0 ? selectedGuards : void 0;
396
+ await writeJson(bundlePath, bundle);
397
+ process.stdout.write(
398
+ `
399
+ \u2713 Updated guards for bundle: ${bundleName.trim()}
400
+ `
401
+ );
402
+ }
403
+ async function fixIssuesFlow(rl, targetRoot, report) {
404
+ process.stdout.write("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
405
+ process.stdout.write(" Fix Setup Issues\n");
406
+ process.stdout.write("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n");
407
+ if (!report) {
408
+ const { runDiagnostics } = await import("./diagnose.js");
409
+ process.stdout.write("Scanning...\n\n");
410
+ report = await runDiagnostics(targetRoot);
411
+ }
412
+ if (report.issues.length === 0) {
413
+ process.stdout.write("\u2705 No issues found!\n");
414
+ return;
415
+ }
416
+ process.stdout.write("Issues found:\n");
417
+ for (const issue of report.issues) {
418
+ const icon = issue.severity === "error" ? "\u274C" : issue.severity === "warning" ? "\u26A0\uFE0F " : "\u2139\uFE0F ";
419
+ process.stdout.write(` ${icon} ${issue.message}
420
+ `);
421
+ }
422
+ process.stdout.write("\n");
423
+ const shouldFix = await promptBoolean(rl, "Fix these issues?", true);
424
+ if (!shouldFix) {
425
+ process.stdout.write("No fixes applied.\n");
426
+ return;
427
+ }
428
+ process.stdout.write("\n");
429
+ const missingGuardsIssue = report.issues.find(
430
+ (i) => i.type === "missing_guards"
431
+ );
432
+ if (missingGuardsIssue) {
433
+ const guards = missingGuardsIssue.details.guards;
434
+ const result = await fixMissingGuards(targetRoot, guards);
435
+ process.stdout.write(`${result.success ? "\u2713" : "\u2717"} ${result.message}
436
+ `);
437
+ }
438
+ const orphanedSoulsIssue = report.issues.find(
439
+ (i) => i.type === "orphaned_souls"
440
+ );
441
+ if (orphanedSoulsIssue) {
442
+ const souls = orphanedSoulsIssue.details.souls;
443
+ for (const soul of souls) {
444
+ const result = await createBundleForSoul(targetRoot, soul);
445
+ process.stdout.write(`${result.success ? "\u2713" : "\u2717"} ${result.message}
446
+ `);
447
+ }
448
+ }
449
+ const noProfilesIssue = report.issues.find((i) => i.type === "no_profiles");
450
+ if (noProfilesIssue) {
451
+ const bundles = await getAvailableBundles(targetRoot);
452
+ if (bundles.length > 0) {
453
+ const result = await createProfile(targetRoot, "imported", {
454
+ bundleNames: bundles
455
+ });
456
+ process.stdout.write(
457
+ `${result.success ? "\u2713" : "\u2717"} ${result.message}
458
+ `
459
+ );
460
+ }
461
+ }
462
+ process.stdout.write("\n\u2705 Fixes applied!\n");
463
+ }
464
+ async function showStructure(targetRoot) {
465
+ process.stdout.write("\n\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n");
466
+ process.stdout.write(" Current Structure\n");
467
+ process.stdout.write("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n\n");
468
+ const souls = await getAvailableSouls(targetRoot);
469
+ process.stdout.write(`\u{1F4C1} Souls (${souls.length}):
470
+ `);
471
+ for (const soul of souls) {
472
+ process.stdout.write(` - ${soul}.md
473
+ `);
474
+ }
475
+ process.stdout.write("\n");
476
+ const skills = await getAvailableSkills(targetRoot);
477
+ process.stdout.write(
478
+ `\u{1F4C1} Skills (${skills.length}) [globally mounted, no per-agent isolation]:
479
+ `
480
+ );
481
+ process.stdout.write(` - ${skills.join(", ")}
482
+
483
+ `);
484
+ const bundles = await getAvailableBundles(targetRoot);
485
+ process.stdout.write(`\u{1F4C1} Bundles (${bundles.length}):
486
+ `);
487
+ for (const bundleName of bundles) {
488
+ const bundlePath = path.join(targetRoot, "bundles", `${bundleName}.json`);
489
+ try {
490
+ const bundle = await readJson(bundlePath);
491
+ const soul = bundle.soul || "none";
492
+ const skills2 = bundle.skills?.join(", ") || "none";
493
+ const guards = bundle.guards?.join(", ") || "none";
494
+ const runtime = bundle.runtime || "native";
495
+ process.stdout.write(
496
+ ` - ${bundleName}: soul=${soul}, skills=${skills2}, guards=${guards}, runtime=${runtime}
497
+ `
498
+ );
499
+ } catch {
500
+ process.stdout.write(` - ${bundleName}: (error reading)
501
+ `);
502
+ }
503
+ }
504
+ process.stdout.write("\n");
505
+ const profiles = await getAvailableProfiles(targetRoot);
506
+ process.stdout.write(`\u{1F4C1} Profiles (${profiles.length}):
507
+ `);
508
+ for (const profileName of profiles) {
509
+ const profilePath = path.join(
510
+ targetRoot,
511
+ "profiles",
512
+ `${profileName}.json`
513
+ );
514
+ try {
515
+ const profile = await readJson(profilePath);
516
+ const bundles2 = profile.bundles?.join(", ") || "none";
517
+ const defaultAgent = profile.defaultAgent || "none";
518
+ process.stdout.write(
519
+ ` - ${profileName}: [${bundles2}], default=${defaultAgent}
520
+ `
521
+ );
522
+ } catch {
523
+ process.stdout.write(` - ${profileName}: (error reading)
524
+ `);
525
+ }
526
+ }
527
+ process.stdout.write("\n");
528
+ const nativeAgents = await getNativeAgentMap(targetRoot);
529
+ const unmanagedNativeAgents = Object.entries(nativeAgents).filter(
530
+ ([name, details]) => !details.managedByBundle
531
+ );
532
+ process.stdout.write(
533
+ `\u{1F4C1} Native Agents (${unmanagedNativeAgents.length}) [available via native OpenCode config]:
534
+ `
535
+ );
536
+ if (unmanagedNativeAgents.length === 0) {
537
+ process.stdout.write(" - none\n\n");
538
+ } else {
539
+ for (const [name, details] of unmanagedNativeAgents) {
540
+ const override = details.overrideModel ? `, overrideModel=${details.overrideModel}` : "";
541
+ const model = details.model || "none";
542
+ process.stdout.write(` - ${name}: model=${model}${override}
543
+ `);
544
+ }
545
+ process.stdout.write("\n");
546
+ }
547
+ process.stdout.write("\u{1F4C1} Guards (per-agent permission controls):\n");
548
+ process.stdout.write(" - read_only: Block edit, write, bash\n");
549
+ process.stdout.write(" - no_task: Block task tool\n");
550
+ process.stdout.write(" - no_subagent: Legacy alias for no_task\n");
551
+ process.stdout.write(" - no_omo: Block OMO multi-agent calls\n\n");
552
+ process.stdout.write("\u26A0\uFE0F Important Notes:\n");
553
+ process.stdout.write(" - Skills are globally mounted, not per-agent exclusive\n");
554
+ process.stdout.write(" - blockedTools uses union blocking (any block = all blocked)\n");
555
+ process.stdout.write(" - Guards/Permission are true per-agent controls\n");
556
+ }
557
+ async function promptBoolean(rl, question, defaultValue) {
558
+ const suffix = defaultValue ? "[Y/n]" : "[y/N]";
559
+ while (true) {
560
+ const answer = (await rl.question(`${question} ${suffix}: `)).trim().toLowerCase();
561
+ if (!answer) return defaultValue;
562
+ if (answer === "y" || answer === "yes") return true;
563
+ if (answer === "n" || answer === "no") return false;
564
+ process.stdout.write("Please answer y or n.\n");
565
+ }
566
+ }
567
+ function unique(arr) {
568
+ return [...new Set(arr)];
569
+ }
570
+ async function getAvailableSouls(targetRoot) {
571
+ const soulsDir = path.join(targetRoot, "souls");
572
+ if (!await pathExists(soulsDir)) return [];
573
+ const entries = await readdir(soulsDir, { withFileTypes: true });
574
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, ""));
575
+ }
576
+ async function getAvailableSkills(targetRoot) {
577
+ const skillsDir = path.join(targetRoot, "skills");
578
+ if (!await pathExists(skillsDir)) return [];
579
+ const entries = await readdir(skillsDir, { withFileTypes: true });
580
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
581
+ }
582
+ async function getAvailableBundles(targetRoot) {
583
+ const bundlesDir = path.join(targetRoot, "bundles");
584
+ if (!await pathExists(bundlesDir)) return [];
585
+ const entries = await readdir(bundlesDir, { withFileTypes: true });
586
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name.replace(/\.json$/, ""));
587
+ }
588
+ async function getAvailableProfiles(targetRoot) {
589
+ const profilesDir = path.join(targetRoot, "profiles");
590
+ if (!await pathExists(profilesDir)) return [];
591
+ const entries = await readdir(profilesDir, { withFileTypes: true });
592
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name.replace(/\.json$/, ""));
593
+ }
594
+ async function getAvailableMcps(targetRoot) {
595
+ const mcpDir = path.join(targetRoot, "mcp");
596
+ if (!await pathExists(mcpDir)) return [];
597
+ const entries = await readdir(mcpDir, { withFileTypes: true });
598
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".json")).map((e) => e.name.replace(/\.json$/, ""));
599
+ }
600
+ async function getBundleAgentNames(targetRoot) {
601
+ const bundles = await getAvailableBundles(targetRoot);
602
+ const names = [];
603
+ for (const bundleName of bundles) {
604
+ const bundlePath = path.join(targetRoot, "bundles", `${bundleName}.json`);
605
+ try {
606
+ const bundle = await readJson(bundlePath);
607
+ if (bundle.agent?.name) names.push(bundle.agent.name);
608
+ } catch {
609
+ }
610
+ }
611
+ return unique(names);
612
+ }
613
+ async function getNativeAgentMap(targetRoot) {
614
+ const nativeConfig = await loadNativeOpenCodeConfig();
615
+ const settings = await readAgentHubSettings(targetRoot);
616
+ const bundleAgentNames = new Set(await getBundleAgentNames(targetRoot));
617
+ const agentEntries = Object.entries(nativeConfig?.agent || {}).filter(
618
+ ([, agent]) => agent && typeof agent === "object"
619
+ );
620
+ return Object.fromEntries(
621
+ agentEntries.map(([name, agent]) => [
622
+ name,
623
+ {
624
+ model: typeof agent.model === "string" && agent.model.trim().length > 0 ? agent.model : void 0,
625
+ overrideModel: settings?.agents?.[name]?.model,
626
+ managedByBundle: bundleAgentNames.has(name)
627
+ }
628
+ ])
629
+ );
630
+ }
631
+ async function manageAgentModelsFlow(rl, targetRoot) {
632
+ const settings = await readAgentHubSettings(targetRoot) || {};
633
+ const nativeAgents = await getNativeAgentMap(targetRoot);
634
+ const bundleAgentNames = await getBundleAgentNames(targetRoot);
635
+ const allAgentNames = unique([
636
+ ...bundleAgentNames,
637
+ ...Object.keys(nativeAgents),
638
+ ...Object.keys(settings.agents || {})
639
+ ]);
640
+ if (allAgentNames.length === 0) {
641
+ process.stdout.write("\nNo agents available to manage.\n");
642
+ return;
643
+ }
644
+ process.stdout.write("\nAvailable agents:\n");
645
+ for (const [index, name] of allAgentNames.entries()) {
646
+ const native = nativeAgents[name];
647
+ const source = bundleAgentNames.includes(name) ? native ? "managed + native" : "managed" : native ? "native unmanaged" : "settings-only";
648
+ const currentModel2 = settings.agents?.[name]?.model || native?.model || "none";
649
+ process.stdout.write(
650
+ ` ${index + 1}. ${name} (${source}, model=${currentModel2})
651
+ `
652
+ );
653
+ }
654
+ const answer = await rl.question(
655
+ "\nSelect agent number to update model (or Enter to cancel): "
656
+ );
657
+ if (!answer.trim()) return;
658
+ const selectedIndex = Number.parseInt(answer.trim(), 10);
659
+ if (!Number.isFinite(selectedIndex) || selectedIndex < 1 || selectedIndex > allAgentNames.length) {
660
+ process.stdout.write("Invalid selection.\n");
661
+ return;
662
+ }
663
+ const agentName = allAgentNames[selectedIndex - 1];
664
+ const currentModel = settings.agents?.[agentName]?.model || nativeAgents[agentName]?.model || "";
665
+ const nextModel = await rl.question(
666
+ `New model for '${agentName}' (current: ${currentModel || "none"}; blank clears override): `
667
+ );
668
+ process.stdout.write(`${await updateAgentModelOverride(targetRoot, agentName, nextModel)}
669
+ `);
670
+ }
671
+ async function updateAgentModelOverride(targetRoot, agentName, model) {
672
+ const settings = await readAgentHubSettings(targetRoot) || {};
673
+ const trimmed = model.trim();
674
+ settings.agents = settings.agents || {};
675
+ const existing = settings.agents[agentName] || {};
676
+ if (!trimmed) {
677
+ const { model: _ignored, ...rest } = existing;
678
+ if (Object.keys(rest).length > 0) {
679
+ settings.agents[agentName] = rest;
680
+ } else {
681
+ delete settings.agents[agentName];
682
+ }
683
+ if (Object.keys(settings.agents).length === 0) {
684
+ delete settings.agents;
685
+ }
686
+ await writeAgentHubSettings(targetRoot, settings);
687
+ return `Cleared model override for '${agentName}'.`;
688
+ }
689
+ settings.agents[agentName] = {
690
+ ...existing,
691
+ model: trimmed
692
+ };
693
+ await writeAgentHubSettings(targetRoot, settings);
694
+ return `Updated model for '${agentName}' to '${trimmed}'.`;
695
+ }
696
+ async function updateAgentPromptOverride(targetRoot, agentName, prompt) {
697
+ const settings = await readAgentHubSettings(targetRoot) || {};
698
+ const trimmed = prompt.trim();
699
+ settings.agents = settings.agents || {};
700
+ const existing = settings.agents[agentName] || {};
701
+ if (!trimmed) {
702
+ const { prompt: _ignored, ...rest } = existing;
703
+ if (Object.keys(rest).length > 0) {
704
+ settings.agents[agentName] = rest;
705
+ } else {
706
+ delete settings.agents[agentName];
707
+ }
708
+ if (Object.keys(settings.agents).length === 0) {
709
+ delete settings.agents;
710
+ }
711
+ await writeAgentHubSettings(targetRoot, settings);
712
+ return `Cleared prompt override for '${agentName}'.`;
713
+ }
714
+ settings.agents[agentName] = {
715
+ ...existing,
716
+ prompt: trimmed
717
+ };
718
+ await writeAgentHubSettings(targetRoot, settings);
719
+ return `Updated prompt override for '${agentName}'.`;
720
+ }
721
+ async function interactiveAssembly(targetRoot, report, options = {}) {
722
+ if (report.issues.length === 0) {
723
+ if (options.continueToMenu === false) return;
724
+ await interactiveDoctor(targetRoot, report);
725
+ return;
726
+ }
727
+ const rl = readline.createInterface({
728
+ input: process.stdin,
729
+ output: process.stdout
730
+ });
731
+ try {
732
+ process.stdout.write("\n\u26A0\uFE0F Issues Found:\n");
733
+ for (const issue of report.issues) {
734
+ const icon = issue.severity === "error" ? "\u274C" : issue.severity === "warning" ? "\u26A0\uFE0F " : "\u2139\uFE0F ";
735
+ process.stdout.write(` ${icon} ${issue.message}
736
+ `);
737
+ }
738
+ process.stdout.write("\n");
739
+ await fixIssuesFlow(rl, targetRoot, report);
740
+ if (options.continueToMenu === false) return;
741
+ const continueToMenu = await promptBoolean(rl, "\nContinue to main menu?", true);
742
+ if (continueToMenu) {
743
+ rl.close();
744
+ await interactiveDoctor(targetRoot);
745
+ }
746
+ } finally {
747
+ rl.close();
748
+ }
749
+ }
750
+ export {
751
+ getAvailableBundles,
752
+ interactiveAssembly,
753
+ interactiveDoctor,
754
+ updateAgentModelOverride,
755
+ updateAgentPromptOverride
756
+ };