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.
- package/dist/index.js +2 -3
- package/dist/index.test.js +5 -0
- package/dist/prompts/strategies/delegate.md +124 -10
- package/dist/prompts/strategies/manual.md +138 -6
- package/dist/test/integration/modelConfigIntegration.test.d.ts +1 -0
- package/dist/test/integration/modelConfigIntegration.test.js +222 -0
- package/dist/test/integration/omo3-delegation.test.d.ts +1 -0
- package/dist/test/integration/omo3-delegation.test.js +583 -0
- package/dist/test/integration/rebrand.test.js +5 -1
- package/dist/tools/delegate.d.ts +12 -0
- package/dist/tools/delegate.js +84 -33
- package/dist/utils/agentInitialization.test.d.ts +1 -0
- package/dist/utils/agentInitialization.test.js +262 -0
- package/dist/utils/commandDefaults.d.ts +38 -0
- package/dist/utils/commandDefaults.js +54 -0
- package/dist/utils/commandDefaults.test.d.ts +1 -0
- package/dist/utils/commandDefaults.test.js +101 -0
- package/dist/utils/configDetection.d.ts +16 -0
- package/dist/utils/configDetection.js +103 -1
- package/dist/utils/configDetection.test.js +116 -1
- package/dist/utils/documentGeneration.d.ts +3 -0
- package/dist/utils/documentGeneration.js +29 -9
- package/dist/utils/interactiveMenu.test.js +5 -0
- package/dist/utils/languageSupport.d.ts +5 -0
- package/dist/utils/languageSupport.js +163 -0
- package/dist/utils/languageSupport.test.d.ts +1 -0
- package/dist/utils/languageSupport.test.js +158 -0
- package/dist/utils/modelConfigInjection.test.d.ts +1 -0
- package/dist/utils/modelConfigInjection.test.js +137 -0
- package/package.json +2 -2
package/dist/tools/delegate.js
CHANGED
|
@@ -1,45 +1,96 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import { detectCDDConfig, getAvailableOMOAgents, getAgentModel } from "../utils/configDetection.js";
|
|
3
|
+
import { resolveAgentForDelegation } from "../utils/synergyDelegation.js";
|
|
4
|
+
/**
|
|
5
|
+
* Creates a delegation tool that follows the OMO 3.0 agent invocation pattern.
|
|
6
|
+
*
|
|
7
|
+
* This implementation uses the synchronous prompt() API pattern:
|
|
8
|
+
* 1. Create a child session with parentID
|
|
9
|
+
* 2. Send prompt with agent specification and tool restrictions
|
|
10
|
+
* 3. Poll for completion by checking when session becomes idle
|
|
11
|
+
* 4. Extract and return the final response
|
|
12
|
+
*
|
|
13
|
+
* Based on OMO 3.0 call_omo_agent implementation:
|
|
14
|
+
* https://github.com/code-yeongyu/oh-my-opencode/blob/main/src/tools/call-omo-agent/tools.ts
|
|
15
|
+
*/
|
|
2
16
|
export function createDelegationTool(ctx) {
|
|
3
17
|
return tool({
|
|
4
|
-
description: "Delegate a specific task to a specialized subagent",
|
|
18
|
+
description: "Delegate a specific task to a specialized subagent using OMO 3.0 invocation pattern",
|
|
5
19
|
args: {
|
|
6
20
|
task_description: tool.schema.string().describe("Summary of the work"),
|
|
7
|
-
subagent_type: tool.schema.string().describe("The name of the agent to call"),
|
|
21
|
+
subagent_type: tool.schema.string().describe("The name of the agent to call (e.g., explore, oracle, librarian)"),
|
|
8
22
|
prompt: tool.schema.string().describe("Detailed instructions for the subagent"),
|
|
9
23
|
},
|
|
10
24
|
async execute(args, toolContext) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
body: {
|
|
25
|
-
agent: args.subagent_type,
|
|
26
|
-
tools: {
|
|
27
|
-
"cdd_delegate": false,
|
|
25
|
+
try {
|
|
26
|
+
const config = detectCDDConfig();
|
|
27
|
+
const availableAgents = getAvailableOMOAgents();
|
|
28
|
+
const delegationResult = resolveAgentForDelegation(args.subagent_type, config.synergyFramework, availableAgents);
|
|
29
|
+
if (!delegationResult.success) {
|
|
30
|
+
return `Cannot delegate to '${args.subagent_type}': ${delegationResult.reason}\n\nFalling back to @cdd for manual implementation.`;
|
|
31
|
+
}
|
|
32
|
+
const resolvedAgentName = delegationResult.resolvedAgent;
|
|
33
|
+
// 1. Create a sub-session linked to the current one
|
|
34
|
+
const createResult = await ctx.client.session.create({
|
|
35
|
+
body: {
|
|
36
|
+
parentID: toolContext.sessionID,
|
|
37
|
+
title: `${args.task_description} (@${resolvedAgentName})`,
|
|
28
38
|
},
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
});
|
|
40
|
+
if (createResult.error) {
|
|
41
|
+
return `Error creating session: ${createResult.error}`;
|
|
42
|
+
}
|
|
43
|
+
const sessionID = createResult.data.id;
|
|
44
|
+
const agentModel = getAgentModel(resolvedAgentName);
|
|
45
|
+
await ctx.client.session.prompt({
|
|
46
|
+
path: { id: sessionID },
|
|
47
|
+
body: {
|
|
48
|
+
agent: resolvedAgentName,
|
|
49
|
+
model: agentModel,
|
|
50
|
+
tools: {
|
|
51
|
+
cdd_delegate: false,
|
|
52
|
+
cdd_bg_task: false,
|
|
53
|
+
},
|
|
54
|
+
parts: [{ type: "text", text: args.prompt }],
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
const MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
58
|
+
const POLL_INTERVAL_MS = 2000;
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
while (Date.now() - startTime < MAX_POLL_TIME_MS) {
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
62
|
+
try {
|
|
63
|
+
const statusResult = await ctx.client.session.status();
|
|
64
|
+
const sessionStatus = statusResult.data?.[sessionID];
|
|
65
|
+
if (sessionStatus?.type === "idle") {
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (statusError) {
|
|
70
|
+
console.warn("[CDD Delegate] Status check failed:", statusError);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const messagesResult = await ctx.client.session.messages({
|
|
74
|
+
path: { id: sessionID },
|
|
75
|
+
});
|
|
76
|
+
if (messagesResult.error) {
|
|
77
|
+
return `Error fetching messages: ${messagesResult.error}`;
|
|
78
|
+
}
|
|
79
|
+
const assistantMessages = (messagesResult.data || [])
|
|
80
|
+
.filter((m) => m.info.role === "assistant");
|
|
81
|
+
if (assistantMessages.length === 0) {
|
|
82
|
+
return `No response from agent ${resolvedAgentName}`;
|
|
83
|
+
}
|
|
84
|
+
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
|
85
|
+
const responseText = lastMessage.parts
|
|
86
|
+
.filter((p) => p.type === "text")
|
|
87
|
+
.map((p) => p.text)
|
|
88
|
+
.join("\n") || "No response.";
|
|
89
|
+
return `${responseText}\n\n<task_metadata>\nsession_id: ${sessionID}\nagent: ${resolvedAgentName}\nrequested: ${args.subagent_type}\n</task_metadata>`;
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return `Error during delegation: ${error.message || String(error)}`;
|
|
93
|
+
}
|
|
43
94
|
},
|
|
44
95
|
});
|
|
45
96
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import * as configDetection from "./configDetection.js";
|
|
3
|
+
/**
|
|
4
|
+
* Phase 2: Agent Initialization with Resolved Model
|
|
5
|
+
*
|
|
6
|
+
* Tests verify that the resolved model configuration is properly
|
|
7
|
+
* passed to @cdd agent initialization and delegated agents.
|
|
8
|
+
*/
|
|
9
|
+
describe("Phase 2: Agent Initialization with Resolved Model", () => {
|
|
10
|
+
let originalEnv;
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
originalEnv = { ...process.env };
|
|
13
|
+
vi.clearAllMocks();
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
process.env = originalEnv;
|
|
17
|
+
});
|
|
18
|
+
describe("@cdd agent receives correct model from resolved config", () => {
|
|
19
|
+
it("should pass resolved model to agent initialization when oh-my-opencode-slim has model", () => {
|
|
20
|
+
// Arrange
|
|
21
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
22
|
+
// Simulate what the plugin does with the config
|
|
23
|
+
const commandDefaults = configDetectionResult.cddModel
|
|
24
|
+
? { agent: "cdd", model: configDetectionResult.cddModel }
|
|
25
|
+
: { agent: "cdd" };
|
|
26
|
+
// Assert
|
|
27
|
+
if (configDetectionResult.cddModel) {
|
|
28
|
+
expect(commandDefaults).toHaveProperty("model");
|
|
29
|
+
expect(commandDefaults).toMatchObject({
|
|
30
|
+
agent: "cdd",
|
|
31
|
+
model: configDetectionResult.cddModel,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
it("should use fallback defaults when no model is configured", () => {
|
|
36
|
+
// Arrange
|
|
37
|
+
const configDetectionResult = {
|
|
38
|
+
hasCDDInOpenCode: false,
|
|
39
|
+
hasCDDInOMO: false,
|
|
40
|
+
hasCDDInSlim: false,
|
|
41
|
+
synergyActive: false,
|
|
42
|
+
cddModel: undefined,
|
|
43
|
+
synergyFramework: "none",
|
|
44
|
+
};
|
|
45
|
+
// Act - simulate plugin behavior
|
|
46
|
+
const commandDefaults = configDetectionResult.cddModel
|
|
47
|
+
? { agent: "cdd", model: configDetectionResult.cddModel }
|
|
48
|
+
: { agent: "cdd" };
|
|
49
|
+
// Assert
|
|
50
|
+
expect(commandDefaults).toEqual({
|
|
51
|
+
agent: "cdd",
|
|
52
|
+
});
|
|
53
|
+
expect(commandDefaults).not.toHaveProperty("model");
|
|
54
|
+
});
|
|
55
|
+
it("should maintain model throughout all CDD command defaults", () => {
|
|
56
|
+
// Arrange
|
|
57
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
58
|
+
const commandDefaults = configDetectionResult.cddModel
|
|
59
|
+
? { agent: "cdd", model: configDetectionResult.cddModel }
|
|
60
|
+
: { agent: "cdd" };
|
|
61
|
+
// Simulate all CDD commands
|
|
62
|
+
const commands = {
|
|
63
|
+
"cdd:setup": { ...commandDefaults },
|
|
64
|
+
"cdd:newTrack": { ...commandDefaults },
|
|
65
|
+
"cdd:implement": { ...commandDefaults },
|
|
66
|
+
"cdd:status": { ...commandDefaults },
|
|
67
|
+
"cdd:revert": { ...commandDefaults },
|
|
68
|
+
};
|
|
69
|
+
// Assert: all commands should have consistent model config
|
|
70
|
+
Object.entries(commands).forEach(([name, config]) => {
|
|
71
|
+
expect(config).toMatchObject({
|
|
72
|
+
agent: "cdd",
|
|
73
|
+
});
|
|
74
|
+
if (configDetectionResult.cddModel) {
|
|
75
|
+
expect(config).toHaveProperty("model", configDetectionResult.cddModel);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
describe("Agent initialization handles invalid model config gracefully", () => {
|
|
81
|
+
it("should handle undefined model gracefully", () => {
|
|
82
|
+
// Arrange
|
|
83
|
+
const configDetectionResult = {
|
|
84
|
+
hasCDDInOpenCode: false,
|
|
85
|
+
hasCDDInOMO: false,
|
|
86
|
+
hasCDDInSlim: false,
|
|
87
|
+
synergyActive: false,
|
|
88
|
+
cddModel: undefined,
|
|
89
|
+
synergyFramework: "none",
|
|
90
|
+
};
|
|
91
|
+
// Act
|
|
92
|
+
const commandDefaults = configDetectionResult.cddModel
|
|
93
|
+
? { agent: "cdd", model: configDetectionResult.cddModel }
|
|
94
|
+
: { agent: "cdd" };
|
|
95
|
+
// Assert
|
|
96
|
+
expect(commandDefaults).toBeDefined();
|
|
97
|
+
expect(commandDefaults.agent).toBe("cdd");
|
|
98
|
+
expect(() => {
|
|
99
|
+
if (!commandDefaults.agent)
|
|
100
|
+
throw new Error("Agent not set");
|
|
101
|
+
}).not.toThrow();
|
|
102
|
+
});
|
|
103
|
+
it("should handle empty model string gracefully", () => {
|
|
104
|
+
// Arrange
|
|
105
|
+
const configDetectionResult = {
|
|
106
|
+
hasCDDInOpenCode: false,
|
|
107
|
+
hasCDDInOMO: false,
|
|
108
|
+
hasCDDInSlim: false,
|
|
109
|
+
synergyActive: false,
|
|
110
|
+
cddModel: "",
|
|
111
|
+
synergyFramework: "none",
|
|
112
|
+
};
|
|
113
|
+
// Act
|
|
114
|
+
const commandDefaults = configDetectionResult.cddModel
|
|
115
|
+
? { agent: "cdd", model: configDetectionResult.cddModel }
|
|
116
|
+
: { agent: "cdd" };
|
|
117
|
+
// Assert - empty string is falsy, should fall back
|
|
118
|
+
expect(commandDefaults).not.toHaveProperty("model");
|
|
119
|
+
});
|
|
120
|
+
it("should validate model string format before passing to agent", () => {
|
|
121
|
+
// Arrange
|
|
122
|
+
const validModel = "anthropic/claude-3-5-sonnet";
|
|
123
|
+
const configDefaults = {
|
|
124
|
+
agent: "cdd",
|
|
125
|
+
model: validModel,
|
|
126
|
+
};
|
|
127
|
+
// Assert - model should be a valid string identifier
|
|
128
|
+
if (configDefaults.model) {
|
|
129
|
+
expect(typeof configDefaults.model).toBe("string");
|
|
130
|
+
expect(configDefaults.model).toMatch(/^[a-z0-9\-]+\/[a-z0-9\-]+$/i);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe("Delegated agent model propagation", () => {
|
|
135
|
+
it("should pass model to @explorer agent when configured", () => {
|
|
136
|
+
// Arrange
|
|
137
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
138
|
+
const parentModel = configDetectionResult.cddModel;
|
|
139
|
+
// Act - simulate delegation
|
|
140
|
+
const delegationConfig = parentModel
|
|
141
|
+
? { agent: "explorer", model: parentModel }
|
|
142
|
+
: { agent: "explorer" };
|
|
143
|
+
// Assert
|
|
144
|
+
expect(delegationConfig).toMatchObject({
|
|
145
|
+
agent: "explorer",
|
|
146
|
+
});
|
|
147
|
+
if (parentModel) {
|
|
148
|
+
expect(delegationConfig).toHaveProperty("model", parentModel);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
it("should pass model to @designer agent when configured", () => {
|
|
152
|
+
// Arrange
|
|
153
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
154
|
+
const parentModel = configDetectionResult.cddModel;
|
|
155
|
+
// Act - simulate delegation
|
|
156
|
+
const delegationConfig = parentModel
|
|
157
|
+
? { agent: "designer", model: parentModel }
|
|
158
|
+
: { agent: "designer" };
|
|
159
|
+
// Assert
|
|
160
|
+
expect(delegationConfig).toMatchObject({
|
|
161
|
+
agent: "designer",
|
|
162
|
+
});
|
|
163
|
+
if (parentModel) {
|
|
164
|
+
expect(delegationConfig).toHaveProperty("model", parentModel);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
it("should pass model to @librarian agent when configured", () => {
|
|
168
|
+
// Arrange
|
|
169
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
170
|
+
const parentModel = configDetectionResult.cddModel;
|
|
171
|
+
// Act - simulate delegation
|
|
172
|
+
const delegationConfig = parentModel
|
|
173
|
+
? { agent: "librarian", model: parentModel }
|
|
174
|
+
: { agent: "librarian" };
|
|
175
|
+
// Assert
|
|
176
|
+
expect(delegationConfig).toMatchObject({
|
|
177
|
+
agent: "librarian",
|
|
178
|
+
});
|
|
179
|
+
if (parentModel) {
|
|
180
|
+
expect(delegationConfig).toHaveProperty("model", parentModel);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
it("should pass model to @oracle agent when configured", () => {
|
|
184
|
+
// Arrange
|
|
185
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
186
|
+
const parentModel = configDetectionResult.cddModel;
|
|
187
|
+
// Act - simulate delegation
|
|
188
|
+
const delegationConfig = parentModel
|
|
189
|
+
? { agent: "oracle", model: parentModel }
|
|
190
|
+
: { agent: "oracle" };
|
|
191
|
+
// Assert
|
|
192
|
+
expect(delegationConfig).toMatchObject({
|
|
193
|
+
agent: "oracle",
|
|
194
|
+
});
|
|
195
|
+
if (parentModel) {
|
|
196
|
+
expect(delegationConfig).toHaveProperty("model", parentModel);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
it("should maintain consistent model across delegation chain", () => {
|
|
200
|
+
// Arrange
|
|
201
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
202
|
+
const parentModel = configDetectionResult.cddModel;
|
|
203
|
+
// Act - simulate full delegation chain
|
|
204
|
+
const cddConfig = parentModel
|
|
205
|
+
? { agent: "cdd", model: parentModel }
|
|
206
|
+
: { agent: "cdd" };
|
|
207
|
+
const delegatedAgents = ["explorer", "designer", "librarian", "oracle"];
|
|
208
|
+
const delegationConfigs = delegatedAgents.map((agent) => parentModel
|
|
209
|
+
? { agent: agent, model: parentModel }
|
|
210
|
+
: { agent: agent });
|
|
211
|
+
// Assert - all delegates should have same model as parent
|
|
212
|
+
delegationConfigs.forEach((delegateConfig) => {
|
|
213
|
+
if (parentModel) {
|
|
214
|
+
expect(delegateConfig).toHaveProperty("model", parentModel);
|
|
215
|
+
}
|
|
216
|
+
if (cddConfig && "model" in cddConfig) {
|
|
217
|
+
expect(delegateConfig).toHaveProperty("model", cddConfig.model);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
describe("Synergy framework agent model propagation", () => {
|
|
223
|
+
it("should detect and use slim framework model when active", () => {
|
|
224
|
+
// Arrange
|
|
225
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
226
|
+
// Act
|
|
227
|
+
const hasSlimModel = configDetectionResult.hasCDDInSlim && configDetectionResult.cddModel;
|
|
228
|
+
// Assert
|
|
229
|
+
if (hasSlimModel) {
|
|
230
|
+
expect(configDetectionResult.synergyFramework).toBe("oh-my-opencode-slim");
|
|
231
|
+
expect(configDetectionResult.cddModel).toBeDefined();
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
it("should detect and use OMO framework model when slim not active", () => {
|
|
235
|
+
// Arrange
|
|
236
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
237
|
+
// Act
|
|
238
|
+
const hasOmoModel = configDetectionResult.hasCDDInOMO &&
|
|
239
|
+
!configDetectionResult.hasCDDInSlim &&
|
|
240
|
+
configDetectionResult.cddModel;
|
|
241
|
+
// Assert
|
|
242
|
+
if (hasOmoModel) {
|
|
243
|
+
expect(configDetectionResult.synergyFramework).toBe("oh-my-opencode");
|
|
244
|
+
expect(configDetectionResult.cddModel).toBeDefined();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
it("should respect synergy priority when resolving model", () => {
|
|
248
|
+
// Arrange
|
|
249
|
+
const configDetectionResult = configDetection.detectCDDConfig();
|
|
250
|
+
// Act & Assert - verify priority chain
|
|
251
|
+
if (configDetectionResult.hasCDDInSlim) {
|
|
252
|
+
expect(configDetectionResult.synergyFramework).toBe("oh-my-opencode-slim");
|
|
253
|
+
}
|
|
254
|
+
else if (configDetectionResult.hasCDDInOMO) {
|
|
255
|
+
expect(configDetectionResult.synergyFramework).toBe("oh-my-opencode");
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
expect(configDetectionResult.synergyFramework).toBe("none");
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for creating OpenCode command defaults with CDD agent configuration.
|
|
3
|
+
* Handles model resolution and provides type-safe command default objects.
|
|
4
|
+
*/
|
|
5
|
+
export interface CommandDefaults {
|
|
6
|
+
agent: "cdd";
|
|
7
|
+
model?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Creates command defaults for CDD commands with optional model configuration.
|
|
11
|
+
*
|
|
12
|
+
* @param cddModel - The resolved model string from config detection (undefined = use OpenCode default)
|
|
13
|
+
* @returns Command defaults object with agent and optional model
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // With model configured
|
|
17
|
+
* const defaults = createCommandDefaults("anthropic/claude-3-5-sonnet");
|
|
18
|
+
* // { agent: "cdd", model: "anthropic/claude-3-5-sonnet" }
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // Without model (fallback to OpenCode default)
|
|
22
|
+
* const defaults = createCommandDefaults(undefined);
|
|
23
|
+
* // { agent: "cdd" }
|
|
24
|
+
*/
|
|
25
|
+
export declare function createCommandDefaults(cddModel: string | undefined): CommandDefaults;
|
|
26
|
+
/**
|
|
27
|
+
* Validates that a model string is properly formatted.
|
|
28
|
+
*
|
|
29
|
+
* @param model - Model string to validate
|
|
30
|
+
* @returns true if valid, false otherwise
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* validateModelString("anthropic/claude-3-5-sonnet") // true
|
|
34
|
+
* validateModelString("google/gemini-flash-1.5") // true
|
|
35
|
+
* validateModelString("") // false
|
|
36
|
+
* validateModelString(" ") // false
|
|
37
|
+
*/
|
|
38
|
+
export declare function validateModelString(model: string | undefined): boolean;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Factory for creating OpenCode command defaults with CDD agent configuration.
|
|
3
|
+
* Handles model resolution and provides type-safe command default objects.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Creates command defaults for CDD commands with optional model configuration.
|
|
7
|
+
*
|
|
8
|
+
* @param cddModel - The resolved model string from config detection (undefined = use OpenCode default)
|
|
9
|
+
* @returns Command defaults object with agent and optional model
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* // With model configured
|
|
13
|
+
* const defaults = createCommandDefaults("anthropic/claude-3-5-sonnet");
|
|
14
|
+
* // { agent: "cdd", model: "anthropic/claude-3-5-sonnet" }
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* // Without model (fallback to OpenCode default)
|
|
18
|
+
* const defaults = createCommandDefaults(undefined);
|
|
19
|
+
* // { agent: "cdd" }
|
|
20
|
+
*/
|
|
21
|
+
export function createCommandDefaults(cddModel) {
|
|
22
|
+
if (typeof cddModel === "string" && cddModel.trim() === "") {
|
|
23
|
+
console.warn("[CDD] Invalid model configuration detected. Model must be a non-empty string. Falling back to OpenCode default.");
|
|
24
|
+
return { agent: "cdd" };
|
|
25
|
+
}
|
|
26
|
+
if (!cddModel) {
|
|
27
|
+
return { agent: "cdd" };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
agent: "cdd",
|
|
31
|
+
model: cddModel,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validates that a model string is properly formatted.
|
|
36
|
+
*
|
|
37
|
+
* @param model - Model string to validate
|
|
38
|
+
* @returns true if valid, false otherwise
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* validateModelString("anthropic/claude-3-5-sonnet") // true
|
|
42
|
+
* validateModelString("google/gemini-flash-1.5") // true
|
|
43
|
+
* validateModelString("") // false
|
|
44
|
+
* validateModelString(" ") // false
|
|
45
|
+
*/
|
|
46
|
+
export function validateModelString(model) {
|
|
47
|
+
if (!model)
|
|
48
|
+
return false;
|
|
49
|
+
if (typeof model !== "string")
|
|
50
|
+
return false;
|
|
51
|
+
if (model.trim() === "")
|
|
52
|
+
return false;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { createCommandDefaults, validateModelString } from './commandDefaults.js';
|
|
3
|
+
describe('commandDefaults', () => {
|
|
4
|
+
describe('createCommandDefaults', () => {
|
|
5
|
+
it('should return defaults with model when valid model provided', () => {
|
|
6
|
+
const result = createCommandDefaults('anthropic/claude-3-5-sonnet');
|
|
7
|
+
expect(result).toEqual({
|
|
8
|
+
agent: 'cdd',
|
|
9
|
+
model: 'anthropic/claude-3-5-sonnet',
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
it('should return defaults without model when undefined provided', () => {
|
|
13
|
+
const result = createCommandDefaults(undefined);
|
|
14
|
+
expect(result).toEqual({
|
|
15
|
+
agent: 'cdd',
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
it('should return defaults without model when empty string provided', () => {
|
|
19
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
20
|
+
const result = createCommandDefaults('');
|
|
21
|
+
expect(result).toEqual({
|
|
22
|
+
agent: 'cdd',
|
|
23
|
+
});
|
|
24
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('[CDD] Invalid model configuration detected'));
|
|
25
|
+
consoleWarnSpy.mockRestore();
|
|
26
|
+
});
|
|
27
|
+
it('should return defaults without model when whitespace-only string provided', () => {
|
|
28
|
+
const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
29
|
+
const result = createCommandDefaults(' ');
|
|
30
|
+
expect(result).toEqual({
|
|
31
|
+
agent: 'cdd',
|
|
32
|
+
});
|
|
33
|
+
expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('[CDD] Invalid model configuration detected'));
|
|
34
|
+
consoleWarnSpy.mockRestore();
|
|
35
|
+
});
|
|
36
|
+
it('should handle various valid model formats', () => {
|
|
37
|
+
const testCases = [
|
|
38
|
+
'anthropic/claude-3-5-sonnet',
|
|
39
|
+
'google/gemini-flash-1.5',
|
|
40
|
+
'openai/gpt-4',
|
|
41
|
+
'anthropic/claude-3-opus-20240229',
|
|
42
|
+
];
|
|
43
|
+
testCases.forEach(model => {
|
|
44
|
+
const result = createCommandDefaults(model);
|
|
45
|
+
expect(result.model).toBe(model);
|
|
46
|
+
expect(result.agent).toBe('cdd');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
it('should return object with correct TypeScript type', () => {
|
|
50
|
+
const result = createCommandDefaults('anthropic/claude-3-5-sonnet');
|
|
51
|
+
expect(result.agent).toBe('cdd');
|
|
52
|
+
expect(typeof result.model).toBe('string');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
describe('validateModelString', () => {
|
|
56
|
+
it('should return true for valid model strings', () => {
|
|
57
|
+
expect(validateModelString('anthropic/claude-3-5-sonnet')).toBe(true);
|
|
58
|
+
expect(validateModelString('google/gemini-flash-1.5')).toBe(true);
|
|
59
|
+
expect(validateModelString('openai/gpt-4')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
it('should return false for undefined', () => {
|
|
62
|
+
expect(validateModelString(undefined)).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
it('should return false for empty string', () => {
|
|
65
|
+
expect(validateModelString('')).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
it('should return false for whitespace-only string', () => {
|
|
68
|
+
expect(validateModelString(' ')).toBe(false);
|
|
69
|
+
expect(validateModelString('\t\n')).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
it('should return false for non-string types', () => {
|
|
72
|
+
expect(validateModelString(null)).toBe(false);
|
|
73
|
+
expect(validateModelString(123)).toBe(false);
|
|
74
|
+
expect(validateModelString({})).toBe(false);
|
|
75
|
+
expect(validateModelString([])).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
describe('integration with index.ts', () => {
|
|
79
|
+
it('should create command defaults compatible with OpenCode command config', () => {
|
|
80
|
+
const defaults = createCommandDefaults('anthropic/claude-3-5-sonnet');
|
|
81
|
+
const commandConfig = {
|
|
82
|
+
...defaults,
|
|
83
|
+
template: 'Some prompt template',
|
|
84
|
+
description: 'Command description',
|
|
85
|
+
};
|
|
86
|
+
expect(commandConfig.agent).toBe('cdd');
|
|
87
|
+
expect(commandConfig.model).toBe('anthropic/claude-3-5-sonnet');
|
|
88
|
+
expect(commandConfig.template).toBe('Some prompt template');
|
|
89
|
+
});
|
|
90
|
+
it('should work with spread operator for command defaults', () => {
|
|
91
|
+
const defaults = createCommandDefaults('google/gemini-flash-1.5');
|
|
92
|
+
const command = {
|
|
93
|
+
...defaults,
|
|
94
|
+
someOtherProperty: 'value',
|
|
95
|
+
};
|
|
96
|
+
expect(command.agent).toBe('cdd');
|
|
97
|
+
expect(command.model).toBe('google/gemini-flash-1.5');
|
|
98
|
+
expect(command.someOtherProperty).toBe('value');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|
|
@@ -18,5 +18,21 @@ export interface ConfigDetectionResult {
|
|
|
18
18
|
synergyFramework: SynergyFramework;
|
|
19
19
|
/** Available agents from slim config (filtered by disabled_agents) */
|
|
20
20
|
slimAgents?: string[];
|
|
21
|
+
/** Available agents from OMO config (filtered by disabled_agents) */
|
|
22
|
+
omoAgents?: string[];
|
|
21
23
|
}
|
|
22
24
|
export declare function detectCDDConfig(): ConfigDetectionResult;
|
|
25
|
+
export declare function getAvailableOMOAgents(): string[];
|
|
26
|
+
/**
|
|
27
|
+
* Get the configured model for a specific agent from synergy framework config.
|
|
28
|
+
*
|
|
29
|
+
* Priority Order:
|
|
30
|
+
* 1. oh-my-opencode-slim.json → agents.<agentName>.model
|
|
31
|
+
* 2. oh-my-opencode.json → agents.<agentName>.model
|
|
32
|
+
* 3. opencode.json → agent.<agentName>.model
|
|
33
|
+
* 4. undefined (no model configured)
|
|
34
|
+
*
|
|
35
|
+
* @param agentName - Name of the agent (e.g., 'explorer', 'designer', 'cdd')
|
|
36
|
+
* @returns Model string if configured, undefined otherwise
|
|
37
|
+
*/
|
|
38
|
+
export declare function getAgentModel(agentName: string): string | undefined;
|