opencode-conductor-cdd-plugin 1.0.0-beta.20 → 1.0.0-beta.22

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 (30) hide show
  1. package/dist/index.js +2 -3
  2. package/dist/index.test.js +5 -0
  3. package/dist/prompts/strategies/delegate.md +124 -10
  4. package/dist/prompts/strategies/manual.md +138 -6
  5. package/dist/test/integration/modelConfigIntegration.test.d.ts +1 -0
  6. package/dist/test/integration/modelConfigIntegration.test.js +222 -0
  7. package/dist/test/integration/omo3-delegation.test.d.ts +1 -0
  8. package/dist/test/integration/omo3-delegation.test.js +583 -0
  9. package/dist/test/integration/rebrand.test.js +5 -1
  10. package/dist/tools/delegate.d.ts +12 -0
  11. package/dist/tools/delegate.js +84 -33
  12. package/dist/utils/agentInitialization.test.d.ts +1 -0
  13. package/dist/utils/agentInitialization.test.js +262 -0
  14. package/dist/utils/commandDefaults.d.ts +38 -0
  15. package/dist/utils/commandDefaults.js +54 -0
  16. package/dist/utils/commandDefaults.test.d.ts +1 -0
  17. package/dist/utils/commandDefaults.test.js +101 -0
  18. package/dist/utils/configDetection.d.ts +16 -0
  19. package/dist/utils/configDetection.js +103 -1
  20. package/dist/utils/configDetection.test.js +116 -1
  21. package/dist/utils/documentGeneration.d.ts +3 -0
  22. package/dist/utils/documentGeneration.js +29 -9
  23. package/dist/utils/interactiveMenu.test.js +5 -0
  24. package/dist/utils/languageSupport.d.ts +5 -0
  25. package/dist/utils/languageSupport.js +163 -0
  26. package/dist/utils/languageSupport.test.d.ts +1 -0
  27. package/dist/utils/languageSupport.test.js +158 -0
  28. package/dist/utils/modelConfigInjection.test.d.ts +1 -0
  29. package/dist/utils/modelConfigInjection.test.js +137 -0
  30. package/package.json +2 -2
@@ -0,0 +1,158 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { detectLanguage, translateQuestion, translateInstruction, } from './languageSupport.js';
3
+ describe('languageSupport', () => {
4
+ describe('detectLanguage', () => {
5
+ it('should detect Spanish from common Spanish words', () => {
6
+ expect(detectLanguage('¿Cuál es el objetivo principal?')).toBe('es');
7
+ expect(detectLanguage('Los usuarios principales son')).toBe('es');
8
+ expect(detectLanguage('Necesito crear una aplicación')).toBe('es');
9
+ });
10
+ it('should detect Spanish from Spanish-specific characters', () => {
11
+ expect(detectLanguage('Añadir funcionalidad')).toBe('es');
12
+ expect(detectLanguage('Configuración básica')).toBe('es');
13
+ });
14
+ it('should detect English from common English words', () => {
15
+ expect(detectLanguage('What is the primary goal?')).toBe('en');
16
+ expect(detectLanguage('The main users are')).toBe('en');
17
+ expect(detectLanguage('I need to create an application')).toBe('en');
18
+ });
19
+ it('should default to English for ambiguous input', () => {
20
+ expect(detectLanguage('123')).toBe('en');
21
+ expect(detectLanguage('React TypeScript')).toBe('en');
22
+ expect(detectLanguage('')).toBe('en');
23
+ });
24
+ it('should handle mixed language input', () => {
25
+ // Should detect based on predominant language
26
+ expect(detectLanguage('Create a proyecto nuevo')).toBe('en');
27
+ expect(detectLanguage('Necesito create something')).toBe('es');
28
+ });
29
+ });
30
+ describe('translateQuestion', () => {
31
+ const englishQuestion = {
32
+ id: 'product-1',
33
+ text: 'What is the primary purpose of this project?',
34
+ type: 'exclusive',
35
+ section: 'product',
36
+ options: [
37
+ 'Build a web application',
38
+ 'Build a CLI tool',
39
+ 'Create a library',
40
+ 'Enter custom text',
41
+ 'Auto-generate from context',
42
+ ],
43
+ };
44
+ it('should translate question text to Spanish', () => {
45
+ const translated = translateQuestion(englishQuestion, 'es');
46
+ expect(translated.text).toContain('propósito');
47
+ expect(translated.text).toContain('proyecto');
48
+ });
49
+ it('should translate options to Spanish', () => {
50
+ const translated = translateQuestion(englishQuestion, 'es');
51
+ expect(translated.options[0]).toContain('aplicación web');
52
+ expect(translated.options[3]).toContain('texto personalizado');
53
+ expect(translated.options[4]).toContain('Auto-generar');
54
+ });
55
+ it('should keep English text when language is English', () => {
56
+ const translated = translateQuestion(englishQuestion, 'en');
57
+ expect(translated.text).toBe(englishQuestion.text);
58
+ expect(translated.options).toEqual(englishQuestion.options);
59
+ });
60
+ it('should preserve question metadata (id, type, section)', () => {
61
+ const translated = translateQuestion(englishQuestion, 'es');
62
+ expect(translated.id).toBe(englishQuestion.id);
63
+ expect(translated.type).toBe(englishQuestion.type);
64
+ expect(translated.section).toBe(englishQuestion.section);
65
+ });
66
+ it('should use fallback translation for target users question', () => {
67
+ const question = {
68
+ id: 'users-1',
69
+ text: 'Who are the target users?',
70
+ type: 'exclusive',
71
+ section: 'product',
72
+ options: ['Developers', 'End users', 'Businesses'],
73
+ };
74
+ const translated = translateQuestion(question, 'es');
75
+ expect(translated.text).toContain('usuarios objetivo');
76
+ expect(translated.options[0]).toBe('Desarrolladores');
77
+ expect(translated.options[1]).toContain('Usuarios finales');
78
+ expect(translated.options[2]).toBe('Empresas');
79
+ });
80
+ it('should use fallback translation for key features question', () => {
81
+ const question = {
82
+ id: 'features-1',
83
+ text: 'What are the key features?',
84
+ type: 'exclusive',
85
+ section: 'product',
86
+ options: ['Feature 1', 'Feature 2'],
87
+ };
88
+ const translated = translateQuestion(question, 'es');
89
+ expect(translated.text).toContain('características principales');
90
+ });
91
+ it('should use fallback translation for programming language question', () => {
92
+ const question = {
93
+ id: 'tech-1',
94
+ text: 'What programming language will you use?',
95
+ type: 'exclusive',
96
+ section: 'tech-stack',
97
+ options: ['TypeScript', 'Python', 'JavaScript'],
98
+ };
99
+ const translated = translateQuestion(question, 'es');
100
+ expect(translated.text).toContain('lenguajes de programación');
101
+ expect(translated.options[0]).toBe('TypeScript');
102
+ expect(translated.options[1]).toBe('Python');
103
+ expect(translated.options[2]).toBe('JavaScript');
104
+ });
105
+ it('should use fallback translation for framework question', () => {
106
+ const question = {
107
+ id: 'tech-2',
108
+ text: 'What framework will you use?',
109
+ type: 'exclusive',
110
+ section: 'tech-stack',
111
+ options: ['React', 'Vue', 'Angular'],
112
+ };
113
+ const translated = translateQuestion(question, 'es');
114
+ expect(translated.text).toContain('frameworks');
115
+ });
116
+ it('should return original text if no translation matches', () => {
117
+ const question = {
118
+ id: 'custom-question',
119
+ text: 'Some custom question text without matches',
120
+ type: 'exclusive',
121
+ section: 'product',
122
+ options: ['Option A', 'Option B'],
123
+ };
124
+ const translated = translateQuestion(question, 'es');
125
+ expect(translated.text).toBe(question.text);
126
+ expect(translated.options).toEqual(question.options);
127
+ });
128
+ });
129
+ describe('translateInstruction', () => {
130
+ it('should translate menu instructions to Spanish', () => {
131
+ const key = 'menu.select_option';
132
+ const translated = translateInstruction(key, 'es');
133
+ expect(translated).toContain('Selecciona');
134
+ expect(translated).toContain('opción');
135
+ });
136
+ it('should translate menu instructions to English', () => {
137
+ const key = 'menu.select_option';
138
+ const translated = translateInstruction(key, 'en');
139
+ expect(translated).toContain('Select');
140
+ expect(translated).toContain('option');
141
+ });
142
+ it('should handle approval instructions in Spanish', () => {
143
+ const key = 'approval.approve';
144
+ const translated = translateInstruction(key, 'es');
145
+ expect(translated.toLowerCase()).toContain('aprobar');
146
+ });
147
+ it('should handle approval instructions in English', () => {
148
+ const key = 'approval.approve';
149
+ const translated = translateInstruction(key, 'en');
150
+ expect(translated.toLowerCase()).toContain('approve');
151
+ });
152
+ it('should return key if translation not found', () => {
153
+ const key = 'unknown.key';
154
+ const translated = translateInstruction(key, 'es');
155
+ expect(translated).toBe(key);
156
+ });
157
+ });
158
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
+ vi.mock("fs");
3
+ vi.mock("os");
4
+ describe("Model Configuration Injection in Plugin Initialization", () => {
5
+ beforeEach(() => {
6
+ vi.clearAllMocks();
7
+ });
8
+ afterEach(() => {
9
+ vi.resetModules();
10
+ });
11
+ it("should inject model into command defaults when oh-my-opencode.json has model", async () => {
12
+ const { existsSync: mockExistsSync, readFileSync: mockReadFileSync, readdirSync: mockReaddirSync, statSync: mockStatSync } = await import("fs");
13
+ const { homedir: mockHomedir } = await import("os");
14
+ vi.mocked(mockHomedir).mockReturnValue("/home/testuser");
15
+ vi.mocked(mockExistsSync).mockImplementation((path) => {
16
+ return String(path).includes("oh-my-opencode.json");
17
+ });
18
+ vi.mocked(mockReaddirSync).mockReturnValue([]);
19
+ vi.mocked(mockStatSync).mockReturnValue({ isDirectory: () => false });
20
+ vi.mocked(mockReadFileSync).mockImplementation((path) => {
21
+ if (String(path).includes("oh-my-opencode.json")) {
22
+ return JSON.stringify({
23
+ agents: {
24
+ cdd: { model: "anthropic/claude-3-5-sonnet" },
25
+ },
26
+ });
27
+ }
28
+ return "";
29
+ });
30
+ const { MyPlugin } = await import("../index.js");
31
+ const plugin = await MyPlugin({
32
+ directory: "/test/project",
33
+ client: {},
34
+ project: {},
35
+ worktree: {},
36
+ serverUrl: new URL("http://localhost"),
37
+ $: {},
38
+ });
39
+ const config = { command: {} };
40
+ await plugin.config?.(config);
41
+ const setupCmd = config.command["cdd:setup"];
42
+ expect(setupCmd.model).toBe("anthropic/claude-3-5-sonnet");
43
+ expect(setupCmd.agent).toBe("cdd");
44
+ });
45
+ it("should inject model into ALL CDD commands", async () => {
46
+ const { existsSync: mockExistsSync, readFileSync: mockReadFileSync, readdirSync: mockReaddirSync, statSync: mockStatSync } = await import("fs");
47
+ const { homedir: mockHomedir } = await import("os");
48
+ vi.mocked(mockHomedir).mockReturnValue("/home/testuser");
49
+ vi.mocked(mockExistsSync).mockImplementation((path) => {
50
+ const pathStr = String(path);
51
+ return pathStr.includes("opencode.json") && !pathStr.includes("oh-my");
52
+ });
53
+ vi.mocked(mockReaddirSync).mockReturnValue([]);
54
+ vi.mocked(mockStatSync).mockReturnValue({ isDirectory: () => false });
55
+ vi.mocked(mockReadFileSync).mockImplementation((path) => {
56
+ const pathStr = String(path);
57
+ if (pathStr.includes("opencode.json") && !pathStr.includes("oh-my")) {
58
+ return JSON.stringify({
59
+ agent: {
60
+ cdd: { model: "anthropic/claude-3-5-haiku" },
61
+ },
62
+ });
63
+ }
64
+ return "";
65
+ });
66
+ const { MyPlugin } = await import("../index.js");
67
+ const plugin = await MyPlugin({
68
+ directory: "/test/project",
69
+ client: {},
70
+ project: {},
71
+ worktree: {},
72
+ serverUrl: new URL("http://localhost"),
73
+ $: {},
74
+ });
75
+ const config = { command: {} };
76
+ await plugin.config?.(config);
77
+ const cmdConfig = config.command;
78
+ ["cdd:setup", "cdd:newTrack", "cdd:implement", "cdd:status", "cdd:revert"].forEach((cmd) => {
79
+ expect(cmdConfig[cmd].model).toBe("anthropic/claude-3-5-haiku");
80
+ expect(cmdConfig[cmd].agent).toBe("cdd");
81
+ });
82
+ });
83
+ it("should prioritize oh-my-opencode-slim > oh-my-opencode > opencode.json", async () => {
84
+ const { existsSync: mockExistsSync, readFileSync: mockReadFileSync, readdirSync: mockReaddirSync, statSync: mockStatSync } = await import("fs");
85
+ const { homedir: mockHomedir } = await import("os");
86
+ vi.mocked(mockHomedir).mockReturnValue("/home/testuser");
87
+ vi.mocked(mockExistsSync).mockReturnValue(true);
88
+ vi.mocked(mockReaddirSync).mockReturnValue([]);
89
+ vi.mocked(mockStatSync).mockReturnValue({ isDirectory: () => false });
90
+ vi.mocked(mockReadFileSync).mockImplementation((path) => {
91
+ const pathStr = String(path);
92
+ if (pathStr.includes("oh-my-opencode-slim.json")) {
93
+ return JSON.stringify({ agents: { cdd: { model: "google/gemini-2-flash" } } });
94
+ }
95
+ if (pathStr.includes("oh-my-opencode.json")) {
96
+ return JSON.stringify({ agents: { cdd: { model: "anthropic/claude-3-5-sonnet" } } });
97
+ }
98
+ if (pathStr.includes("opencode.json")) {
99
+ return JSON.stringify({ agent: { cdd: { model: "anthropic/claude-3-5-haiku" } } });
100
+ }
101
+ return "";
102
+ });
103
+ const { MyPlugin } = await import("../index.js");
104
+ const plugin = await MyPlugin({
105
+ directory: "/test/project",
106
+ client: {},
107
+ project: {},
108
+ worktree: {},
109
+ serverUrl: new URL("http://localhost"),
110
+ $: {},
111
+ });
112
+ const config = { command: {} };
113
+ await plugin.config?.(config);
114
+ expect(config.command["cdd:setup"].model).toBe("google/gemini-2-flash");
115
+ });
116
+ it("should handle missing model config gracefully", async () => {
117
+ const { existsSync: mockExistsSync, readdirSync: mockReaddirSync, statSync: mockStatSync } = await import("fs");
118
+ const { homedir: mockHomedir } = await import("os");
119
+ vi.mocked(mockHomedir).mockReturnValue("/home/testuser");
120
+ vi.mocked(mockExistsSync).mockReturnValue(false);
121
+ vi.mocked(mockReaddirSync).mockReturnValue([]);
122
+ vi.mocked(mockStatSync).mockReturnValue({ isDirectory: () => false });
123
+ const { MyPlugin } = await import("../index.js");
124
+ const plugin = await MyPlugin({
125
+ directory: "/test/project",
126
+ client: {},
127
+ project: {},
128
+ worktree: {},
129
+ serverUrl: new URL("http://localhost"),
130
+ $: {},
131
+ });
132
+ const config = { command: {} };
133
+ await plugin.config?.(config);
134
+ expect(config.command["cdd:setup"].model).toBeUndefined();
135
+ expect(config.command["cdd:setup"].agent).toBe("cdd");
136
+ });
137
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-conductor-cdd-plugin",
3
- "version": "1.0.0-beta.20",
3
+ "version": "1.0.0-beta.22",
4
4
  "description": "Context-Driven Development (CDD) plugin for OpenCode - Transform your AI coding workflow with structured specifications, plans, and implementation tracking",
5
5
  "type": "module",
6
6
  "repository": {
@@ -42,7 +42,7 @@
42
42
  "prepublishOnly": "npm run build"
43
43
  },
44
44
  "dependencies": {
45
- "@opencode-ai/plugin": "1.0.223",
45
+ "@opencode-ai/plugin": "^1.1.27",
46
46
  "smol-toml": "^1.6.0"
47
47
  },
48
48
  "devDependencies": {