opencode-orchestrator-plugin 1.0.0-beta.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 (58) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +130 -0
  3. package/README.test.md +51 -0
  4. package/dist/commands/implement.d.ts +1 -0
  5. package/dist/commands/implement.js +30 -0
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.js +153 -0
  8. package/dist/prompts/agent/implementer.md +22 -0
  9. package/dist/prompts/agent/orchestrator.md +35 -0
  10. package/dist/prompts/agent.md +23 -0
  11. package/dist/prompts/orchestrator/implement.json +4 -0
  12. package/dist/prompts/orchestrator/newTrack.json +4 -0
  13. package/dist/prompts/orchestrator/revert.json +4 -0
  14. package/dist/prompts/orchestrator/setup.json +4 -0
  15. package/dist/prompts/orchestrator/status.json +4 -0
  16. package/dist/prompts/strategies/delegate.md +11 -0
  17. package/dist/prompts/strategies/manual.md +9 -0
  18. package/dist/templates/code_styleguides/c.md +28 -0
  19. package/dist/templates/code_styleguides/cpp.md +46 -0
  20. package/dist/templates/code_styleguides/csharp.md +115 -0
  21. package/dist/templates/code_styleguides/dart.md +238 -0
  22. package/dist/templates/code_styleguides/general.md +23 -0
  23. package/dist/templates/code_styleguides/go.md +48 -0
  24. package/dist/templates/code_styleguides/html-css.md +49 -0
  25. package/dist/templates/code_styleguides/java.md +39 -0
  26. package/dist/templates/code_styleguides/javascript.md +51 -0
  27. package/dist/templates/code_styleguides/julia.md +27 -0
  28. package/dist/templates/code_styleguides/kotlin.md +41 -0
  29. package/dist/templates/code_styleguides/php.md +37 -0
  30. package/dist/templates/code_styleguides/python.md +37 -0
  31. package/dist/templates/code_styleguides/react.md +37 -0
  32. package/dist/templates/code_styleguides/ruby.md +39 -0
  33. package/dist/templates/code_styleguides/rust.md +44 -0
  34. package/dist/templates/code_styleguides/shell.md +35 -0
  35. package/dist/templates/code_styleguides/solidity.md +60 -0
  36. package/dist/templates/code_styleguides/sql.md +39 -0
  37. package/dist/templates/code_styleguides/swift.md +36 -0
  38. package/dist/templates/code_styleguides/typescript.md +43 -0
  39. package/dist/templates/code_styleguides/vue.md +38 -0
  40. package/dist/templates/code_styleguides/zig.md +27 -0
  41. package/dist/templates/workflow.md +336 -0
  42. package/dist/tools/background.d.ts +54 -0
  43. package/dist/tools/background.js +198 -0
  44. package/dist/tools/commands.d.ts +11 -0
  45. package/dist/tools/commands.js +80 -0
  46. package/dist/tools/commands.test.d.ts +1 -0
  47. package/dist/tools/commands.test.js +145 -0
  48. package/dist/tools/delegate.d.ts +3 -0
  49. package/dist/tools/delegate.js +45 -0
  50. package/dist/utils/bootstrap.d.ts +1 -0
  51. package/dist/utils/bootstrap.js +46 -0
  52. package/dist/utils/commandFactory.d.ts +11 -0
  53. package/dist/utils/commandFactory.js +69 -0
  54. package/dist/utils/stateManager.d.ts +10 -0
  55. package/dist/utils/stateManager.js +30 -0
  56. package/package.json +88 -0
  57. package/scripts/convert-legacy.cjs +17 -0
  58. package/scripts/postinstall.cjs +38 -0
@@ -0,0 +1,198 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ const BACKGROUND_TASK_DESCRIPTION = "Launch a specialized agent in the background to perform research or implementation tasks.";
3
+ const BACKGROUND_OUTPUT_DESCRIPTION = "Retrieve the results or status of a background task.";
4
+ export class BackgroundManager {
5
+ tasks;
6
+ client;
7
+ pollingInterval;
8
+ constructor(ctx) {
9
+ this.tasks = new Map();
10
+ this.client = ctx.client;
11
+ }
12
+ async launch(input) {
13
+ if (!input.agent || input.agent.trim() === "") {
14
+ throw new Error("Agent parameter is required");
15
+ }
16
+ const createResult = await this.client.session.create({
17
+ body: {
18
+ parentID: input.parentSessionID,
19
+ title: `Background: ${input.description}`,
20
+ },
21
+ });
22
+ if (createResult.error) {
23
+ throw new Error(`Failed to create background session: ${createResult.error}`);
24
+ }
25
+ const sessionID = createResult.data.id;
26
+ const task = {
27
+ id: `bg_${Math.random().toString(36).substring(2, 10)}`,
28
+ sessionID,
29
+ parentSessionID: input.parentSessionID,
30
+ parentMessageID: input.parentMessageID,
31
+ description: input.description,
32
+ prompt: input.prompt,
33
+ agent: input.agent,
34
+ status: "running",
35
+ startedAt: new Date(),
36
+ progress: {
37
+ toolCalls: 0,
38
+ lastUpdate: new Date(),
39
+ },
40
+ };
41
+ this.tasks.set(task.id, task);
42
+ this.startPolling();
43
+ this.client.session.promptAsync({
44
+ path: { id: sessionID },
45
+ body: {
46
+ agent: input.agent,
47
+ tools: {
48
+ "orchestrator_bg_task": false,
49
+ "orchestrator_delegate": false,
50
+ },
51
+ parts: [{ type: "text", text: input.prompt }],
52
+ },
53
+ }).catch((error) => {
54
+ console.error("[Orchestrator] Background task error:", error);
55
+ task.status = "failed";
56
+ task.error = String(error);
57
+ });
58
+ return task;
59
+ }
60
+ async cancel(id) {
61
+ const task = this.tasks.get(id);
62
+ if (!task)
63
+ return `Task not found: ${id}`;
64
+ if (task.status !== "running")
65
+ return `Task is not running (status: ${task.status})`;
66
+ task.status = "cancelled";
67
+ task.completedAt = new Date();
68
+ // Attempt to notify parent session
69
+ await this.client.session.prompt({
70
+ path: { id: task.parentSessionID },
71
+ body: {
72
+ parts: [{ type: "text", text: `[BACKGROUND TASK CANCELLED] Task "${task.description}" has been manually cancelled.` }],
73
+ },
74
+ }).catch(() => { });
75
+ return `Task ${id} cancelled successfully.`;
76
+ }
77
+ async pollRunningTasks() {
78
+ try {
79
+ const statusResult = await this.client.session.status();
80
+ const allStatuses = (statusResult.data ?? {});
81
+ for (const task of this.tasks.values()) {
82
+ if (task.status !== "running")
83
+ continue;
84
+ const sessionStatus = allStatuses[task.sessionID];
85
+ if (sessionStatus?.type === "idle") {
86
+ this.completeTask(task);
87
+ }
88
+ }
89
+ if (!this.hasRunningTasks()) {
90
+ this.stopPolling();
91
+ }
92
+ }
93
+ catch (e) {
94
+ console.error("[Orchestrator] Polling error:", e);
95
+ }
96
+ }
97
+ completeTask(task) {
98
+ task.status = "completed";
99
+ task.completedAt = new Date();
100
+ this.notifyParentSession(task);
101
+ }
102
+ async notifyParentSession(task) {
103
+ const message = `[BACKGROUND TASK COMPLETED] Task "${task.description}" finished. Use orchestrator_bg_output with task_id="${task.id}" to get results.`;
104
+ await this.client.session.prompt({
105
+ path: { id: task.parentSessionID },
106
+ body: {
107
+ parts: [{ type: "text", text: message }],
108
+ },
109
+ }).catch(() => { });
110
+ }
111
+ getTask(id) {
112
+ return this.tasks.get(id);
113
+ }
114
+ startPolling() {
115
+ if (this.pollingInterval)
116
+ return;
117
+ this.pollingInterval = setInterval(() => this.pollRunningTasks(), 3000);
118
+ }
119
+ stopPolling() {
120
+ if (this.pollingInterval) {
121
+ clearInterval(this.pollingInterval);
122
+ this.pollingInterval = undefined;
123
+ }
124
+ }
125
+ hasRunningTasks() {
126
+ return Array.from(this.tasks.values()).some(t => t.status === "running");
127
+ }
128
+ }
129
+ export function createBackgroundTask(manager) {
130
+ return tool({
131
+ description: BACKGROUND_TASK_DESCRIPTION,
132
+ args: {
133
+ description: tool.schema.string().describe("Short task description"),
134
+ prompt: tool.schema.string().describe("Full detailed prompt for the agent"),
135
+ agent: tool.schema.string().describe("Agent type to use"),
136
+ },
137
+ async execute(args, toolContext) {
138
+ const ctx = toolContext;
139
+ const task = await manager.launch({
140
+ description: args.description,
141
+ prompt: args.prompt,
142
+ agent: args.agent.trim(),
143
+ parentSessionID: ctx.sessionID,
144
+ parentMessageID: ctx.messageID,
145
+ });
146
+ return `Background task launched successfully. Task ID: ${task.id}`;
147
+ },
148
+ });
149
+ }
150
+ export function createBackgroundOutput(manager) {
151
+ return tool({
152
+ description: BACKGROUND_OUTPUT_DESCRIPTION,
153
+ args: {
154
+ task_id: tool.schema.string().describe("Task ID to get output from"),
155
+ block: tool.schema.boolean().optional().describe("Wait for completion"),
156
+ timeout: tool.schema.number().optional().describe("Max wait time in ms"),
157
+ },
158
+ async execute(args, toolContext) {
159
+ const task = manager.getTask(args.task_id);
160
+ if (!task)
161
+ return `Task not found: ${args.task_id}`;
162
+ if (args.block && task.status === "running") {
163
+ const startTime = Date.now();
164
+ const timeoutMs = Math.min(args.timeout ?? 60000, 600000);
165
+ while (Date.now() - startTime < timeoutMs) {
166
+ await new Promise(r => setTimeout(r, 2000));
167
+ if (manager.getTask(args.task_id)?.status === "completed")
168
+ break;
169
+ }
170
+ }
171
+ if (task.status === "completed") {
172
+ const client = toolContext.client || manager.client;
173
+ const messagesResult = await client.session.messages({
174
+ path: { id: task.sessionID },
175
+ });
176
+ const lastMessage = messagesResult.data
177
+ ?.filter((m) => m.info.role === "assistant")
178
+ .pop();
179
+ const responseText = lastMessage?.parts
180
+ .filter((p) => p.type === "text")
181
+ .map((p) => p.text).join("\n") || "No response.";
182
+ return `### Results for: ${task.description}\n\n${responseText}`;
183
+ }
184
+ return `Task status: ${task.status}. (Started at: ${task.startedAt.toISOString()})`;
185
+ },
186
+ });
187
+ }
188
+ export function createBackgroundCancel(manager) {
189
+ return tool({
190
+ description: "Cancel a running background task",
191
+ args: {
192
+ taskId: tool.schema.string().describe("Task ID to cancel"),
193
+ },
194
+ async execute(args) {
195
+ return await manager.cancel(args.taskId);
196
+ },
197
+ });
198
+ }
@@ -0,0 +1,11 @@
1
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
2
+ export declare const setupCommand: (ctx: import("@opencode-ai/plugin").PluginInput) => ToolDefinition;
3
+ export declare const newTrackCommand: (ctx: import("@opencode-ai/plugin").PluginInput) => ToolDefinition;
4
+ export declare const implementCommand: (ctx: import("@opencode-ai/plugin").PluginInput) => ToolDefinition;
5
+ export declare const statusCommand: (ctx: import("@opencode-ai/plugin").PluginInput) => ToolDefinition;
6
+ export declare const revertCommand: (ctx: import("@opencode-ai/plugin").PluginInput) => ToolDefinition;
7
+ export declare function createSetupTool(ctx: any): ToolDefinition;
8
+ export declare function createNewTrackTool(ctx: any): ToolDefinition;
9
+ export declare function createImplementTool(ctx: any): ToolDefinition;
10
+ export declare function createStatusTool(ctx: any): ToolDefinition;
11
+ export declare function createRevertTool(ctx: any): ToolDefinition;
@@ -0,0 +1,80 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import { createOrchestratorCommand } from "../utils/commandFactory.js";
3
+ import { join, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+ import { readFile } from "fs/promises";
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ export const setupCommand = createOrchestratorCommand({
9
+ name: "orchestrator/setup.json",
10
+ description: "Directives lookup tool for scaffolding the project and setting up the Orchestrator environment",
11
+ args: {},
12
+ });
13
+ export const newTrackCommand = createOrchestratorCommand({
14
+ name: "orchestrator/newTrack.json",
15
+ description: "Directives lookup tool for planning a track, generating track-specific spec documents and updating the tracks file",
16
+ args: {
17
+ description: tool.schema.string().optional().describe("Brief description of the track (feature, bug fix, chore, etc.)"),
18
+ },
19
+ additionalContext: async (_ctx, args) => {
20
+ return {
21
+ args: args.description || "",
22
+ };
23
+ },
24
+ });
25
+ export const implementCommand = createOrchestratorCommand({
26
+ name: "orchestrator/implement.json",
27
+ description: "Directives lookup tool for executing the tasks defined in the specified track's plan",
28
+ args: {
29
+ track_name: tool.schema.string().optional().describe("Name or description of the track to implement"),
30
+ },
31
+ additionalContext: async (ctx, args) => {
32
+ const strategyFile = ctx.isOMOActive ? "delegate.md" : "manual.md";
33
+ const strategyPath = join(__dirname, "../prompts/strategies", strategyFile);
34
+ let strategySection = "";
35
+ try {
36
+ strategySection = await readFile(strategyPath, "utf-8");
37
+ }
38
+ catch (e) {
39
+ console.warn(`[Orchestrator] Failed to load strategy ${strategyFile}:`, e);
40
+ strategySection = "Error: Could not load execution strategy.";
41
+ }
42
+ return {
43
+ strategy_section: strategySection,
44
+ track_name: args.track_name || "",
45
+ };
46
+ },
47
+ });
48
+ export const statusCommand = createOrchestratorCommand({
49
+ name: "orchestrator/status.json",
50
+ description: "Directives lookup tool for displaying the current progress of the project",
51
+ args: {},
52
+ });
53
+ export const revertCommand = createOrchestratorCommand({
54
+ name: "orchestrator/revert.json",
55
+ description: "Directives lookup tool for reverting previous work",
56
+ args: {
57
+ target: tool.schema.string().optional().describe("Target to revert (e.g., 'track <track_id>', 'phase <phase_name>', 'task <task_name>')"),
58
+ },
59
+ additionalContext: async (_ctx, args) => {
60
+ return {
61
+ target: args.target || "",
62
+ };
63
+ },
64
+ });
65
+ // Export as functions for backward compatibility
66
+ export function createSetupTool(ctx) {
67
+ return setupCommand(ctx);
68
+ }
69
+ export function createNewTrackTool(ctx) {
70
+ return newTrackCommand(ctx);
71
+ }
72
+ export function createImplementTool(ctx) {
73
+ return implementCommand(ctx);
74
+ }
75
+ export function createStatusTool(ctx) {
76
+ return statusCommand(ctx);
77
+ }
78
+ export function createRevertTool(ctx) {
79
+ return revertCommand(ctx);
80
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,145 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { createSetupTool, createNewTrackTool, createImplementTool, createStatusTool, createRevertTool, } from "./commands.js";
3
+ import { readFile } from "fs/promises";
4
+ // Mock fs/promises
5
+ vi.mock("fs/promises", () => ({
6
+ readFile: vi.fn(),
7
+ }));
8
+ describe("Command Tools", () => {
9
+ let mockCtx;
10
+ let mockToolContext;
11
+ beforeEach(() => {
12
+ vi.clearAllMocks();
13
+ mockCtx = {
14
+ directory: "/test/project",
15
+ isOMOActive: false,
16
+ };
17
+ mockToolContext = {
18
+ sessionID: "test-session",
19
+ messageID: "test-message",
20
+ };
21
+ // Default mocks - must use triple quotes for the regex in commandFactory
22
+ vi.mocked(readFile).mockResolvedValue(`
23
+ description = "Test command"
24
+ prompt = """
25
+ Test prompt content
26
+ """
27
+ `);
28
+ });
29
+ describe("createSetupTool", () => {
30
+ it("should create a tool with correct description", () => {
31
+ const tool = createSetupTool(mockCtx);
32
+ expect(tool.description).toBe("Directives lookup tool for scaffolding the project and setting up the Orchestrator environment");
33
+ });
34
+ it("should return directives JSON string when executed", async () => {
35
+ vi.mocked(readFile).mockResolvedValue(`
36
+ description = "Setup"
37
+ prompt = """Setup Prompt"""
38
+ `);
39
+ const tool = createSetupTool(mockCtx);
40
+ const result = await tool.execute({}, mockToolContext);
41
+ expect(JSON.parse(result)).toEqual({ directives: "Setup Prompt" });
42
+ });
43
+ });
44
+ describe("createNewTrackTool", () => {
45
+ it("should create a tool with correct description", () => {
46
+ const tool = createNewTrackTool(mockCtx);
47
+ expect(tool.description).toBe("Directives lookup tool for planning a track, generating track-specific spec documents and updating the tracks file");
48
+ });
49
+ it("should have optional description argument", () => {
50
+ const tool = createNewTrackTool(mockCtx);
51
+ expect(tool.args).toHaveProperty("description");
52
+ });
53
+ it("should replace description in directives", async () => {
54
+ vi.mocked(readFile).mockResolvedValue(`
55
+ description = "New Track"
56
+ prompt = """Track description: {{args}}"""
57
+ `);
58
+ const tool = createNewTrackTool(mockCtx);
59
+ const result = await tool.execute({ description: "Login feature" }, mockToolContext);
60
+ expect(JSON.parse(result)).toEqual({ directives: "Track description: Login feature" });
61
+ });
62
+ });
63
+ describe("createImplementTool", () => {
64
+ it("should create a tool with correct description", () => {
65
+ const tool = createImplementTool(mockCtx);
66
+ expect(tool.description).toBe("Directives lookup tool for executing the tasks defined in the specified track's plan");
67
+ });
68
+ it("should have optional track_name argument", () => {
69
+ const tool = createImplementTool(mockCtx);
70
+ expect(tool.args).toHaveProperty("track_name");
71
+ });
72
+ it("should replace track_name in directives", async () => {
73
+ vi.mocked(readFile).mockResolvedValue(`
74
+ description = "Implement"
75
+ prompt = """Track: {{track_name}}"""
76
+ `);
77
+ const tool = createImplementTool(mockCtx);
78
+ const result = await tool.execute({ track_name: "auth-track" }, mockToolContext);
79
+ expect(JSON.parse(result)).toEqual({ directives: "Track: auth-track" });
80
+ });
81
+ it("should include strategy section in directives", async () => {
82
+ vi.mocked(readFile).mockImplementation(async (path) => {
83
+ if (typeof path === 'string' && path.endsWith("manual.md")) {
84
+ return "Manual Strategy";
85
+ }
86
+ return `
87
+ description = "Implement"
88
+ prompt = """Strategy: {{strategy_section}}"""
89
+ `;
90
+ });
91
+ const tool = createImplementTool(mockCtx);
92
+ const result = await tool.execute({}, mockToolContext);
93
+ expect(JSON.parse(result)).toEqual({ directives: "Strategy: Manual Strategy" });
94
+ });
95
+ });
96
+ describe("createStatusTool", () => {
97
+ it("should create a tool with correct description", () => {
98
+ const tool = createStatusTool(mockCtx);
99
+ expect(tool.description).toBe("Directives lookup tool for displaying the current progress of the project");
100
+ });
101
+ it("should execute and return directives", async () => {
102
+ vi.mocked(readFile).mockResolvedValue(`
103
+ description = "Status"
104
+ prompt = """Status Prompt"""
105
+ `);
106
+ const tool = createStatusTool(mockCtx);
107
+ const result = await tool.execute({}, mockToolContext);
108
+ expect(JSON.parse(result)).toEqual({ directives: "Status Prompt" });
109
+ });
110
+ });
111
+ describe("createRevertTool", () => {
112
+ it("should create a tool with correct description", () => {
113
+ const tool = createRevertTool(mockCtx);
114
+ expect(tool.description).toBe("Directives lookup tool for reverting previous work");
115
+ });
116
+ it("should replace target in directives", async () => {
117
+ vi.mocked(readFile).mockResolvedValue(`
118
+ description = "Revert"
119
+ prompt = """Target: {{target}}"""
120
+ `);
121
+ const tool = createRevertTool(mockCtx);
122
+ const result = await tool.execute({ target: "track 1" }, mockToolContext);
123
+ expect(JSON.parse(result)).toEqual({ directives: "Target: track 1" });
124
+ });
125
+ });
126
+ describe("Error Handling", () => {
127
+ it("should return error in directives if readFile fails", async () => {
128
+ vi.mocked(readFile).mockRejectedValue(new Error("File not found"));
129
+ const tool = createSetupTool(mockCtx);
130
+ const result = await tool.execute({}, mockToolContext);
131
+ expect(JSON.parse(result).directives).toContain("SYSTEM ERROR: Failed to load prompt");
132
+ });
133
+ });
134
+ describe("Prompt Replacement", () => {
135
+ it("should replace standard variables in directives", async () => {
136
+ vi.mocked(readFile).mockResolvedValue(`
137
+ description = "Test"
138
+ prompt = """Templates: {{templatesDir}}"""
139
+ `);
140
+ const tool = createNewTrackTool(mockCtx);
141
+ const result = await tool.execute({}, mockToolContext);
142
+ expect(JSON.parse(result).directives).toContain("Templates:");
143
+ });
144
+ });
145
+ });
@@ -0,0 +1,3 @@
1
+ import { type PluginInput } from "@opencode-ai/plugin";
2
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
3
+ export declare function createDelegationTool(ctx: PluginInput): ToolDefinition;
@@ -0,0 +1,45 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ export function createDelegationTool(ctx) {
3
+ return tool({
4
+ description: "Delegate a specific task to a specialized subagent",
5
+ args: {
6
+ task_description: tool.schema.string().describe("Summary of the work"),
7
+ subagent_type: tool.schema.string().describe("The name of the agent to call"),
8
+ prompt: tool.schema.string().describe("Detailed instructions for the subagent"),
9
+ },
10
+ async execute(args, toolContext) {
11
+ // 1. Create a sub-session linked to the current one
12
+ const createResult = await ctx.client.session.create({
13
+ body: {
14
+ parentID: toolContext.sessionID,
15
+ title: `${args.task_description} (Delegated to ${args.subagent_type})`,
16
+ },
17
+ });
18
+ if (createResult.error)
19
+ return `Error: ${createResult.error}`;
20
+ const sessionID = createResult.data.id;
21
+ // 2. Send the prompt to the subagent
22
+ await ctx.client.session.prompt({
23
+ path: { id: sessionID },
24
+ body: {
25
+ agent: args.subagent_type,
26
+ tools: {
27
+ "orchestrator_delegate": false,
28
+ },
29
+ parts: [{ type: "text", text: args.prompt }],
30
+ },
31
+ });
32
+ // 3. Fetch and return the assistant's response
33
+ const messagesResult = await ctx.client.session.messages({
34
+ path: { id: sessionID },
35
+ });
36
+ const lastMessage = messagesResult.data
37
+ ?.filter((m) => m.info.role === "assistant")
38
+ .pop();
39
+ const responseText = lastMessage?.parts
40
+ .filter((p) => p.type === "text")
41
+ .map((p) => p.text).join("\n") || "No response.";
42
+ return `${responseText}\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`;
43
+ },
44
+ });
45
+ }
@@ -0,0 +1 @@
1
+ export declare function bootstrap(ctx: any): Promise<void>;
@@ -0,0 +1,46 @@
1
+ import { existsSync, mkdirSync, copyFileSync, readdirSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { homedir } from "os";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ export async function bootstrap(ctx) {
8
+ const opencodeConfigDir = join(homedir(), ".config", "opencode");
9
+ const targetAgentDir = join(opencodeConfigDir, "agent");
10
+ const targetCommandDir = join(opencodeConfigDir, "command");
11
+ const sourcePromptsDir = join(__dirname, "../prompts");
12
+ const sourceAgentFile = join(sourcePromptsDir, "agent/orchestrator.md");
13
+ const sourceCommandsDir = join(sourcePromptsDir, "commands");
14
+ let installedAnything = false;
15
+ // 1. Ensure directories exist
16
+ if (!existsSync(targetAgentDir))
17
+ mkdirSync(targetAgentDir, { recursive: true });
18
+ if (!existsSync(targetCommandDir))
19
+ mkdirSync(targetCommandDir, { recursive: true });
20
+ // 2. Install/Update Agent
21
+ const targetAgentFile = join(targetAgentDir, "orchestrator.md");
22
+ if (existsSync(sourceAgentFile)) {
23
+ copyFileSync(sourceAgentFile, targetAgentFile);
24
+ installedAnything = true;
25
+ }
26
+ // 3. Install/Update Commands
27
+ if (existsSync(sourceCommandsDir)) {
28
+ const commands = readdirSync(sourceCommandsDir);
29
+ for (const cmdFile of commands) {
30
+ const targetCmdFile = join(targetCommandDir, cmdFile);
31
+ copyFileSync(join(sourceCommandsDir, cmdFile), targetCmdFile);
32
+ installedAnything = true;
33
+ }
34
+ }
35
+ if (installedAnything) {
36
+ // Do not await toasts during bootstrapping as the TUI might not be ready
37
+ ctx.client.tui.showToast({
38
+ body: {
39
+ title: "Orchestrator",
40
+ message: "First-run setup: Orchestrator agent and commands installed globally. Please restart OpenCode to enable slash commands.",
41
+ variant: "info",
42
+ duration: 5000
43
+ }
44
+ }).catch(() => { });
45
+ }
46
+ }
@@ -0,0 +1,11 @@
1
+ import { type PluginInput } from "@opencode-ai/plugin";
2
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
3
+ interface OrchestratorCommandConfig {
4
+ name: string;
5
+ description: string;
6
+ args: Record<string, any>;
7
+ requiresSetup?: boolean;
8
+ additionalContext?: (ctx: PluginInput, args: any) => Promise<Record<string, string>>;
9
+ }
10
+ export declare function createOrchestratorCommand(config: OrchestratorCommandConfig): (ctx: PluginInput) => ToolDefinition;
11
+ export {};
@@ -0,0 +1,69 @@
1
+ import { tool } from "@opencode-ai/plugin/tool";
2
+ import { join, dirname } from "path";
3
+ import { readFile } from "fs/promises";
4
+ import { fileURLToPath } from "url";
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ async function loadPrompt(filename, replacements = {}) {
8
+ const pathsToTry = [
9
+ join(__dirname, "..", "prompts", filename),
10
+ join(__dirname, "..", "..", filename),
11
+ ];
12
+ let content = "";
13
+ for (const p of pathsToTry) {
14
+ try {
15
+ content = await readFile(p, "utf-8");
16
+ break;
17
+ }
18
+ catch (e) {
19
+ continue;
20
+ }
21
+ }
22
+ if (!content) {
23
+ console.error(`[Orchestrator] Error loading prompt ${filename}: Not found in any tried paths`);
24
+ return {
25
+ prompt: `SYSTEM ERROR: Failed to load prompt ${filename}`,
26
+ description: "Error loading command",
27
+ };
28
+ }
29
+ try {
30
+ const parsed = JSON.parse(content);
31
+ const description = parsed.description || "Orchestrator Command";
32
+ let promptText = parsed.prompt || "";
33
+ if (!promptText)
34
+ throw new Error(`Could not parse prompt text from ${filename}`);
35
+ const defaults = {
36
+ templatesDir: join(dirname(dirname(__dirname)), "templates"),
37
+ };
38
+ const finalReplacements = { ...defaults, ...replacements };
39
+ for (const [key, value] of Object.entries(finalReplacements)) {
40
+ promptText = promptText.replaceAll(`{{${key}}}`, value || "");
41
+ }
42
+ return { prompt: promptText, description };
43
+ }
44
+ catch (error) {
45
+ console.error(`[Orchestrator] Error parsing prompt ${filename}:`, error);
46
+ return {
47
+ prompt: `SYSTEM ERROR: Failed to parse prompt ${filename}`,
48
+ description: "Error parsing command",
49
+ };
50
+ }
51
+ }
52
+ export function createOrchestratorCommand(config) {
53
+ return (ctx) => {
54
+ return tool({
55
+ description: config.description,
56
+ args: config.args,
57
+ async execute(args) {
58
+ const additionalContext = config.additionalContext
59
+ ? await config.additionalContext(ctx, args)
60
+ : {};
61
+ const replacements = { ...additionalContext };
62
+ const { prompt } = await loadPrompt(config.name, replacements);
63
+ return JSON.stringify({
64
+ directives: prompt
65
+ });
66
+ },
67
+ });
68
+ };
69
+ }
@@ -0,0 +1,10 @@
1
+ export interface SetupState {
2
+ last_successful_step: string;
3
+ }
4
+ export declare class StateManager {
5
+ private statePath;
6
+ constructor(workDir: string);
7
+ ensureOrchestratorDir(): void;
8
+ readState(): SetupState;
9
+ writeState(step: string): void;
10
+ }
@@ -0,0 +1,30 @@
1
+ import { join } from "path";
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
3
+ export class StateManager {
4
+ statePath;
5
+ constructor(workDir) {
6
+ this.statePath = join(workDir, "orchestrator", "setup_state.json");
7
+ }
8
+ ensureOrchestratorDir() {
9
+ const dir = join(this.statePath, "..");
10
+ if (!existsSync(dir)) {
11
+ mkdirSync(dir, { recursive: true });
12
+ }
13
+ }
14
+ readState() {
15
+ if (!existsSync(this.statePath)) {
16
+ return { last_successful_step: "" };
17
+ }
18
+ try {
19
+ return JSON.parse(readFileSync(this.statePath, "utf-8"));
20
+ }
21
+ catch (e) {
22
+ return { last_successful_step: "" };
23
+ }
24
+ }
25
+ writeState(step) {
26
+ this.ensureOrchestratorDir();
27
+ const state = { last_successful_step: step };
28
+ writeFileSync(this.statePath, JSON.stringify(state, null, 2));
29
+ }
30
+ }