opencode-conductor-plugin 1.22.1 → 1.24.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.test.md ADDED
@@ -0,0 +1,51 @@
1
+ # Testing
2
+
3
+ This project uses [Vitest](https://vitest.dev/) for testing.
4
+
5
+ ## Setup
6
+
7
+ Install dependencies:
8
+
9
+ ```bash
10
+ npm install
11
+ ```
12
+
13
+ ## Running Tests
14
+
15
+ Run all tests:
16
+
17
+ ```bash
18
+ npm test
19
+ ```
20
+
21
+ Run tests in watch mode:
22
+
23
+ ```bash
24
+ npm run test:watch
25
+ ```
26
+
27
+ Run tests with coverage:
28
+
29
+ ```bash
30
+ npm run test:coverage
31
+ ```
32
+
33
+ ## Test Structure
34
+
35
+ Tests are located alongside the source files with the `.test.ts` extension:
36
+
37
+ - `src/tools/commands.test.ts` - Tests for command tools
38
+
39
+ ## Writing Tests
40
+
41
+ Tests use Vitest's API with TypeScript support. Example:
42
+
43
+ ```typescript
44
+ import { describe, it, expect, vi } from "vitest"
45
+
46
+ describe("MyFeature", () => {
47
+ it("should work correctly", () => {
48
+ expect(true).toBe(true)
49
+ })
50
+ })
51
+ ```
@@ -1,7 +1,11 @@
1
- import { type PluginInput } from "@opencode-ai/plugin";
2
1
  import { type ToolDefinition } from "@opencode-ai/plugin/tool";
3
- export declare function createSetupTool(ctx: PluginInput): ToolDefinition;
4
- export declare function createNewTrackTool(ctx: PluginInput): ToolDefinition;
5
- export declare function createImplementTool(ctx: PluginInput): ToolDefinition;
6
- export declare function createStatusTool(ctx: PluginInput): ToolDefinition;
7
- export declare function createRevertTool(ctx: PluginInput): ToolDefinition;
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;
@@ -1,132 +1,81 @@
1
1
  import { tool } from "@opencode-ai/plugin/tool";
2
+ import { createConductorCommand } from "../utils/commandFactory.js";
2
3
  import { join, dirname } from "path";
3
- import { readFile } from "fs/promises";
4
4
  import { fileURLToPath } from "url";
5
+ import { readFile } from "fs/promises";
5
6
  const __filename = fileURLToPath(import.meta.url);
6
7
  const __dirname = dirname(__filename);
7
- // Helper to load and process prompt templates
8
- async function loadPrompt(filename, replacements = {}) {
9
- const promptPath = join(__dirname, "..", "prompts", filename);
10
- try {
11
- const content = await readFile(promptPath, "utf-8");
12
- const descMatch = content.match(/description\s*=\s*"([^"]+)"/);
13
- const description = descMatch ? descMatch[1] : "Conductor Command";
14
- const promptMatch = content.match(/prompt\s*=\s*"""([\s\S]*?)"""/);
15
- let promptText = promptMatch ? promptMatch[1] : "";
16
- if (!promptText)
17
- throw new Error(`Could not parse prompt text from ${filename}`);
18
- const defaults = {
19
- templatesDir: join(dirname(dirname(__dirname)), "templates"),
8
+ export const setupCommand = createConductorCommand({
9
+ name: "setup.toml",
10
+ description: "Scaffolds the project and sets up the Conductor environment",
11
+ args: {},
12
+ });
13
+ export const newTrackCommand = createConductorCommand({
14
+ name: "newTrack.toml",
15
+ description: "Plans a track, generates track-specific spec documents and updates 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 || "",
20
22
  };
21
- const finalReplacements = { ...defaults, ...replacements };
22
- for (const [key, value] of Object.entries(finalReplacements)) {
23
- promptText = promptText.replaceAll(`{{${key}}}`, value || "");
23
+ },
24
+ });
25
+ export const implementCommand = createConductorCommand({
26
+ name: "implement.toml",
27
+ description: "Executes 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
+ // 1. Choose strategy based on OMO activity
33
+ const strategyFile = ctx.isOMOActive ? "delegate.md" : "manual.md";
34
+ const strategyPath = join(__dirname, "../prompts/strategies", strategyFile);
35
+ let strategySection = "";
36
+ try {
37
+ strategySection = await readFile(strategyPath, "utf-8");
38
+ }
39
+ catch (e) {
40
+ console.warn(`[Conductor] Failed to load strategy ${strategyFile}:`, e);
41
+ strategySection = "Error: Could not load execution strategy.";
24
42
  }
25
- return { prompt: promptText, description: description };
26
- }
27
- catch (error) {
28
- console.error(`[Conductor] Error loading prompt ${filename}:`, error);
29
43
  return {
30
- prompt: `SYSTEM ERROR: Failed to load prompt ${filename}`,
31
- description: "Error loading command",
44
+ strategy_section: strategySection,
45
+ track_name: args.track_name || "",
32
46
  };
33
- }
34
- }
35
- // Helper to execute a command prompt in a sub-session
36
- async function executeCommand(ctx, toolContext, promptText, agent, description) {
37
- // Create a sub-session linked to the current one
38
- const createResult = await ctx.client.session.create({
39
- body: {
40
- parentID: toolContext.sessionID,
41
- title: description,
42
- },
43
- });
44
- if (createResult.error)
45
- return `Error: ${createResult.error}`;
46
- const sessionID = createResult.data.id;
47
- // Send the prompt to the agent
48
- await ctx.client.session.prompt({
49
- path: { id: sessionID },
50
- body: {
51
- agent: agent,
52
- parts: [{ type: "text", text: promptText }],
53
- },
54
- });
55
- // Fetch and return the assistant's response
56
- const messagesResult = await ctx.client.session.messages({
57
- path: { id: sessionID },
58
- });
59
- const lastMessage = messagesResult.data
60
- ?.filter((m) => m.info.role === "assistant")
61
- .pop();
62
- const responseText = lastMessage?.parts
63
- ?.filter((p) => p.type === "text")
64
- .map((p) => p.text)
65
- .join("\n") || "No response.";
66
- return `${responseText}\n\n<task_metadata>\nsession_id: ${sessionID}\n</task_metadata>`;
67
- }
47
+ },
48
+ });
49
+ export const statusCommand = createConductorCommand({
50
+ name: "status.toml",
51
+ description: "Displays the current progress of the project",
52
+ args: {},
53
+ });
54
+ export const revertCommand = createConductorCommand({
55
+ name: "revert.toml",
56
+ description: "Reverts previous work",
57
+ args: {
58
+ target: tool.schema.string().optional().describe("Target to revert (e.g., 'track <track_id>', 'phase <phase_name>', 'task <task_name>')"),
59
+ },
60
+ additionalContext: async (ctx, args) => {
61
+ return {
62
+ target: args.target || "",
63
+ };
64
+ },
65
+ });
66
+ // Export as functions for backward compatibility
68
67
  export function createSetupTool(ctx) {
69
- return tool({
70
- description: "Scaffolds the project and sets up the Conductor environment",
71
- args: {},
72
- async execute(args, toolContext) {
73
- const { prompt, description } = await loadPrompt("setup.toml");
74
- return await executeCommand(ctx, toolContext, prompt, "conductor", description);
75
- },
76
- });
68
+ return setupCommand(ctx);
77
69
  }
78
70
  export function createNewTrackTool(ctx) {
79
- return tool({
80
- description: "Plans a track, generates track-specific spec documents and updates the tracks file",
81
- args: {
82
- description: tool.schema.string().optional().describe("Brief description of the track (feature, bug fix, chore, etc.)"),
83
- },
84
- async execute(args, toolContext) {
85
- const trackDescription = args.description || "";
86
- const { prompt, description } = await loadPrompt("newTrack.toml", {
87
- args: trackDescription,
88
- });
89
- return await executeCommand(ctx, toolContext, prompt, "conductor", description);
90
- },
91
- });
71
+ return newTrackCommand(ctx);
92
72
  }
93
73
  export function createImplementTool(ctx) {
94
- return tool({
95
- description: "Executes the tasks defined in the specified track's plan",
96
- args: {
97
- track_name: tool.schema.string().optional().describe("Name or description of the track to implement"),
98
- },
99
- async execute(args, toolContext) {
100
- const trackName = args.track_name || "";
101
- const { prompt, description } = await loadPrompt("implement.toml", {
102
- track_name: trackName,
103
- });
104
- return await executeCommand(ctx, toolContext, prompt, "conductor_implementer", description);
105
- },
106
- });
74
+ return implementCommand(ctx);
107
75
  }
108
76
  export function createStatusTool(ctx) {
109
- return tool({
110
- description: "Displays the current progress of the project",
111
- args: {},
112
- async execute(args, toolContext) {
113
- const { prompt, description } = await loadPrompt("status.toml");
114
- return await executeCommand(ctx, toolContext, prompt, "conductor", description);
115
- },
116
- });
77
+ return statusCommand(ctx);
117
78
  }
118
79
  export function createRevertTool(ctx) {
119
- return tool({
120
- description: "Reverts previous work",
121
- args: {
122
- target: tool.schema.string().optional().describe("Target to revert (e.g., 'track <track_id>', 'phase <phase_name>', 'task <task_name>')"),
123
- },
124
- async execute(args, toolContext) {
125
- const target = args.target || "";
126
- const { prompt, description } = await loadPrompt("revert.toml", {
127
- target: target,
128
- });
129
- return await executeCommand(ctx, toolContext, prompt, "conductor", description);
130
- },
131
- });
80
+ return revertCommand(ctx);
132
81
  }
@@ -0,0 +1,10 @@
1
+ import { type PluginInput } from "@opencode-ai/plugin";
2
+ import { type ToolDefinition } from "@opencode-ai/plugin/tool";
3
+ interface ConductorCommandConfig {
4
+ name: string;
5
+ description: string;
6
+ args: Record<string, any>;
7
+ additionalContext?: (ctx: PluginInput, args: any) => Promise<Record<string, string>>;
8
+ }
9
+ export declare function createConductorCommand(config: ConductorCommandConfig): (ctx: PluginInput) => ToolDefinition;
10
+ export {};
@@ -0,0 +1,53 @@
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
+ // Helper to load and process prompt templates
8
+ async function loadPrompt(filename, replacements = {}) {
9
+ const promptPath = join(__dirname, "..", "prompts", filename);
10
+ try {
11
+ const content = await readFile(promptPath, "utf-8");
12
+ const descMatch = content.match(/description\s*=\s*"([^"]+)"/);
13
+ const description = descMatch ? descMatch[1] : "Conductor Command";
14
+ const promptMatch = content.match(/prompt\s*=\s*"""([\s\S]*?)"""/);
15
+ let promptText = promptMatch ? promptMatch[1] : "";
16
+ if (!promptText)
17
+ throw new Error(`Could not parse prompt text from ${filename}`);
18
+ const defaults = {
19
+ templatesDir: join(dirname(dirname(__dirname)), "templates"),
20
+ };
21
+ const finalReplacements = { ...defaults, ...replacements };
22
+ for (const [key, value] of Object.entries(finalReplacements)) {
23
+ promptText = promptText.replaceAll(`{{${key}}}`, value || "");
24
+ }
25
+ return { prompt: promptText, description: description };
26
+ }
27
+ catch (error) {
28
+ console.error(`[Conductor] Error loading prompt ${filename}:`, error);
29
+ return {
30
+ prompt: `SYSTEM ERROR: Failed to load prompt ${filename}`,
31
+ description: "Error loading command",
32
+ };
33
+ }
34
+ }
35
+ export function createConductorCommand(config) {
36
+ return (ctx) => {
37
+ return tool({
38
+ description: config.description,
39
+ args: config.args,
40
+ async execute(args) {
41
+ // Get additional context if provided (this can override/extend args)
42
+ const additionalContext = config.additionalContext
43
+ ? await config.additionalContext(ctx, args)
44
+ : {};
45
+ // Merge additionalContext into replacements
46
+ // additionalContext takes precedence and can provide custom mappings
47
+ const replacements = { ...additionalContext };
48
+ const { prompt } = await loadPrompt(config.name, replacements);
49
+ return prompt;
50
+ },
51
+ });
52
+ };
53
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-conductor-plugin",
3
- "version": "1.22.1",
3
+ "version": "1.24.0",
4
4
  "description": "Conductor plugin for OpenCode",
5
5
  "type": "module",
6
6
  "repository": "derekbar90/opencode-conductor",
@@ -32,6 +32,9 @@
32
32
  "build": "tsc && npm run copy-prompts && npm run copy-templates",
33
33
  "copy-prompts": "mkdir -p dist/prompts && cp src/prompts/*.toml src/prompts/*.md dist/prompts/ && mkdir -p dist/prompts/agent && cp src/prompts/agent/*.md dist/prompts/agent/ && mkdir -p dist/prompts/strategies && cp src/prompts/strategies/*.md dist/prompts/strategies/",
34
34
  "copy-templates": "mkdir -p dist/templates && cp -r src/templates/* dist/templates/",
35
+ "test": "vitest",
36
+ "test:watch": "vitest --watch",
37
+ "test:coverage": "vitest --coverage",
35
38
  "prepublishOnly": "npm run build"
36
39
  },
37
40
  "dependencies": {
@@ -46,8 +49,10 @@
46
49
  "@semantic-release/npm": "^12.0.1",
47
50
  "@semantic-release/release-notes-generator": "^14.0.0",
48
51
  "@types/node": "^20.0.0",
52
+ "@vitest/ui": "^2.0.0",
49
53
  "semantic-release": "^24.2.1",
50
- "typescript": "^5.0.0"
54
+ "typescript": "^5.0.0",
55
+ "vitest": "^2.0.0"
51
56
  },
52
57
  "release": {
53
58
  "branches": [