opencode-conductor-cdd-plugin 1.0.0-beta.20 → 1.0.0-beta.21
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/prompts/strategies/delegate.md +124 -10
- package/dist/prompts/strategies/manual.md +138 -6
- package/dist/test/integration/omo3-delegation.test.d.ts +1 -0
- package/dist/test/integration/omo3-delegation.test.js +581 -0
- package/dist/tools/delegate.d.ts +12 -0
- package/dist/tools/delegate.js +82 -33
- package/dist/utils/configDetection.d.ts +3 -0
- package/dist/utils/configDetection.js +16 -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/package.json +2 -2
package/dist/tools/delegate.js
CHANGED
|
@@ -1,45 +1,94 @@
|
|
|
1
1
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
2
|
+
import { detectCDDConfig, getAvailableOMOAgents } 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
|
+
await ctx.client.session.prompt({
|
|
45
|
+
path: { id: sessionID },
|
|
46
|
+
body: {
|
|
47
|
+
agent: resolvedAgentName,
|
|
48
|
+
tools: {
|
|
49
|
+
cdd_delegate: false,
|
|
50
|
+
cdd_bg_task: false,
|
|
51
|
+
},
|
|
52
|
+
parts: [{ type: "text", text: args.prompt }],
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
const MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
56
|
+
const POLL_INTERVAL_MS = 2000;
|
|
57
|
+
const startTime = Date.now();
|
|
58
|
+
while (Date.now() - startTime < MAX_POLL_TIME_MS) {
|
|
59
|
+
await new Promise(resolve => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
60
|
+
try {
|
|
61
|
+
const statusResult = await ctx.client.session.status();
|
|
62
|
+
const sessionStatus = statusResult.data?.[sessionID];
|
|
63
|
+
if (sessionStatus?.type === "idle") {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (statusError) {
|
|
68
|
+
console.warn("[CDD Delegate] Status check failed:", statusError);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const messagesResult = await ctx.client.session.messages({
|
|
72
|
+
path: { id: sessionID },
|
|
73
|
+
});
|
|
74
|
+
if (messagesResult.error) {
|
|
75
|
+
return `Error fetching messages: ${messagesResult.error}`;
|
|
76
|
+
}
|
|
77
|
+
const assistantMessages = (messagesResult.data || [])
|
|
78
|
+
.filter((m) => m.info.role === "assistant");
|
|
79
|
+
if (assistantMessages.length === 0) {
|
|
80
|
+
return `No response from agent ${resolvedAgentName}`;
|
|
81
|
+
}
|
|
82
|
+
const lastMessage = assistantMessages[assistantMessages.length - 1];
|
|
83
|
+
const responseText = lastMessage.parts
|
|
84
|
+
.filter((p) => p.type === "text")
|
|
85
|
+
.map((p) => p.text)
|
|
86
|
+
.join("\n") || "No response.";
|
|
87
|
+
return `${responseText}\n\n<task_metadata>\nsession_id: ${sessionID}\nagent: ${resolvedAgentName}\nrequested: ${args.subagent_type}\n</task_metadata>`;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return `Error during delegation: ${error.message || String(error)}`;
|
|
91
|
+
}
|
|
43
92
|
},
|
|
44
93
|
});
|
|
45
94
|
}
|
|
@@ -18,5 +18,8 @@ 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[];
|
|
@@ -12,6 +12,7 @@ export function detectCDDConfig() {
|
|
|
12
12
|
let cddModel;
|
|
13
13
|
let synergyFramework = 'none';
|
|
14
14
|
let slimAgents;
|
|
15
|
+
let omoAgents;
|
|
15
16
|
// Check oh-my-opencode-slim.json first (highest priority for synergy)
|
|
16
17
|
if (existsSync(slimJsonPath)) {
|
|
17
18
|
try {
|
|
@@ -38,7 +39,6 @@ export function detectCDDConfig() {
|
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
41
|
// Check oh-my-opencode.json (only if slim doesn't have CDD)
|
|
41
|
-
// NOTE: We still check OMO to set hasCDDInOMO flag even if slim has priority
|
|
42
42
|
if (existsSync(omoJsonPath)) {
|
|
43
43
|
try {
|
|
44
44
|
const config = JSON.parse(readFileSync(omoJsonPath, "utf-8"));
|
|
@@ -52,6 +52,10 @@ export function detectCDDConfig() {
|
|
|
52
52
|
if (!cddModel && config.agents.cdd.model) {
|
|
53
53
|
cddModel = config.agents.cdd.model;
|
|
54
54
|
}
|
|
55
|
+
// Extract available OMO agents (filter out disabled ones)
|
|
56
|
+
const allConfiguredAgents = Object.keys(config.agents || {});
|
|
57
|
+
const disabledAgents = new Set(config.disabled_agents ?? []);
|
|
58
|
+
omoAgents = allConfiguredAgents.filter(agent => !disabledAgents.has(agent));
|
|
55
59
|
}
|
|
56
60
|
}
|
|
57
61
|
catch (e) {
|
|
@@ -82,5 +86,16 @@ export function detectCDDConfig() {
|
|
|
82
86
|
cddModel,
|
|
83
87
|
synergyFramework,
|
|
84
88
|
slimAgents,
|
|
89
|
+
omoAgents,
|
|
85
90
|
};
|
|
86
91
|
}
|
|
92
|
+
export function getAvailableOMOAgents() {
|
|
93
|
+
const config = detectCDDConfig();
|
|
94
|
+
if (config.synergyFramework === 'oh-my-opencode-slim' && config.slimAgents) {
|
|
95
|
+
return config.slimAgents;
|
|
96
|
+
}
|
|
97
|
+
if (config.synergyFramework === 'oh-my-opencode' && config.omoAgents) {
|
|
98
|
+
return config.omoAgents;
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
2
|
import { existsSync, readFileSync } from "fs";
|
|
3
|
-
import { detectCDDConfig } from "./configDetection.js";
|
|
3
|
+
import { detectCDDConfig, getAvailableOMOAgents } from "./configDetection.js";
|
|
4
4
|
vi.mock("fs", () => ({
|
|
5
5
|
existsSync: vi.fn(),
|
|
6
6
|
readFileSync: vi.fn(),
|
|
@@ -400,4 +400,119 @@ describe("configDetection", () => {
|
|
|
400
400
|
expect(result.synergyActive).toBe(false);
|
|
401
401
|
});
|
|
402
402
|
});
|
|
403
|
+
describe("OMO agent availability detection", () => {
|
|
404
|
+
it("should extract available agents from oh-my-opencode config", () => {
|
|
405
|
+
vi.mocked(existsSync).mockImplementation((path) => path === omoJsonPath);
|
|
406
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
407
|
+
if (path === omoJsonPath) {
|
|
408
|
+
return JSON.stringify({
|
|
409
|
+
agents: {
|
|
410
|
+
cdd: { model: "anthropic/claude-3-5-sonnet" },
|
|
411
|
+
sisyphus: { model: "anthropic/claude-3-5-sonnet" },
|
|
412
|
+
explore: { model: "google/gemini-3-flash" },
|
|
413
|
+
oracle: { model: "anthropic/claude-3-5-sonnet" }
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
return "";
|
|
418
|
+
});
|
|
419
|
+
const result = detectCDDConfig();
|
|
420
|
+
expect(result.omoAgents).toEqual(['cdd', 'sisyphus', 'explore', 'oracle']);
|
|
421
|
+
});
|
|
422
|
+
it("should filter out disabled agents from oh-my-opencode config", () => {
|
|
423
|
+
vi.mocked(existsSync).mockImplementation((path) => path === omoJsonPath);
|
|
424
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
425
|
+
if (path === omoJsonPath) {
|
|
426
|
+
return JSON.stringify({
|
|
427
|
+
agents: {
|
|
428
|
+
cdd: { model: "anthropic/claude-3-5-sonnet" },
|
|
429
|
+
sisyphus: { model: "anthropic/claude-3-5-sonnet" },
|
|
430
|
+
explore: { model: "google/gemini-3-flash" },
|
|
431
|
+
oracle: { model: "anthropic/claude-3-5-sonnet" }
|
|
432
|
+
},
|
|
433
|
+
disabled_agents: ['explore', 'oracle']
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
return "";
|
|
437
|
+
});
|
|
438
|
+
const result = detectCDDConfig();
|
|
439
|
+
expect(result.omoAgents).toEqual(['cdd', 'sisyphus']);
|
|
440
|
+
});
|
|
441
|
+
it("should handle empty agents object in oh-my-opencode config", () => {
|
|
442
|
+
vi.mocked(existsSync).mockImplementation((path) => path === omoJsonPath);
|
|
443
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
444
|
+
if (path === omoJsonPath) {
|
|
445
|
+
return JSON.stringify({
|
|
446
|
+
agents: {}
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
return "";
|
|
450
|
+
});
|
|
451
|
+
const result = detectCDDConfig();
|
|
452
|
+
expect(result.omoAgents).toBeUndefined();
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
describe("getAvailableOMOAgents", () => {
|
|
456
|
+
it("should return slim agents when slim framework is active", () => {
|
|
457
|
+
vi.mocked(existsSync).mockImplementation((path) => path === slimJsonPath);
|
|
458
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
459
|
+
if (path === slimJsonPath) {
|
|
460
|
+
return JSON.stringify({
|
|
461
|
+
agents: {
|
|
462
|
+
cdd: { model: "anthropic/claude-3-5-sonnet" }
|
|
463
|
+
},
|
|
464
|
+
disabled_agents: ['oracle']
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
return "";
|
|
468
|
+
});
|
|
469
|
+
const agents = getAvailableOMOAgents();
|
|
470
|
+
expect(agents).toEqual(['explorer', 'librarian', 'designer']);
|
|
471
|
+
});
|
|
472
|
+
it("should return omo agents when oh-my-opencode framework is active", () => {
|
|
473
|
+
vi.mocked(existsSync).mockImplementation((path) => path === omoJsonPath);
|
|
474
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
475
|
+
if (path === omoJsonPath) {
|
|
476
|
+
return JSON.stringify({
|
|
477
|
+
agents: {
|
|
478
|
+
cdd: { model: "anthropic/claude-3-5-sonnet" },
|
|
479
|
+
sisyphus: { model: "anthropic/claude-3-5-sonnet" },
|
|
480
|
+
explore: { model: "google/gemini-3-flash" }
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
return "";
|
|
485
|
+
});
|
|
486
|
+
const agents = getAvailableOMOAgents();
|
|
487
|
+
expect(agents).toEqual(['cdd', 'sisyphus', 'explore']);
|
|
488
|
+
});
|
|
489
|
+
it("should return empty array when no synergy framework is active", () => {
|
|
490
|
+
vi.mocked(existsSync).mockReturnValue(false);
|
|
491
|
+
const agents = getAvailableOMOAgents();
|
|
492
|
+
expect(agents).toEqual([]);
|
|
493
|
+
});
|
|
494
|
+
it("should prioritize slim agents over omo agents when both configs exist", () => {
|
|
495
|
+
vi.mocked(existsSync).mockReturnValue(true);
|
|
496
|
+
vi.mocked(readFileSync).mockImplementation((path) => {
|
|
497
|
+
if (path === slimJsonPath) {
|
|
498
|
+
return JSON.stringify({
|
|
499
|
+
agents: {
|
|
500
|
+
cdd: { model: "model-from-slim" }
|
|
501
|
+
}
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (path === omoJsonPath) {
|
|
505
|
+
return JSON.stringify({
|
|
506
|
+
agents: {
|
|
507
|
+
cdd: { model: "model-from-omo" },
|
|
508
|
+
sisyphus: { model: "model" }
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
return "";
|
|
513
|
+
});
|
|
514
|
+
const agents = getAvailableOMOAgents();
|
|
515
|
+
expect(agents).toEqual(['explorer', 'librarian', 'oracle', 'designer']);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
403
518
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Question, Section } from './questionGenerator.js';
|
|
2
2
|
import { CodebaseAnalysis } from './codebaseAnalysis.js';
|
|
3
|
+
import { Language } from './languageSupport.js';
|
|
3
4
|
/**
|
|
4
5
|
* Document Generation Module
|
|
5
6
|
*
|
|
@@ -65,6 +66,7 @@ export interface DocumentGenerationOptions {
|
|
|
65
66
|
outputPath: string;
|
|
66
67
|
maxRevisions?: number;
|
|
67
68
|
customInputPrompt?: (question: Question) => Promise<string>;
|
|
69
|
+
language?: Language;
|
|
68
70
|
}
|
|
69
71
|
export interface DocumentGenerationResult {
|
|
70
72
|
success: boolean;
|
|
@@ -76,6 +78,7 @@ export interface DocumentGenerationResult {
|
|
|
76
78
|
export interface PresentQuestionsOptions {
|
|
77
79
|
maxQuestions?: number;
|
|
78
80
|
customInputPrompt?: (question: Question) => Promise<string>;
|
|
81
|
+
language?: Language;
|
|
79
82
|
}
|
|
80
83
|
/**
|
|
81
84
|
* Present questions sequentially to the user
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { validateSelection, parseSelection } from './interactiveMenu.js';
|
|
4
|
+
import { translateQuestion, detectLanguage } from './languageSupport.js';
|
|
4
5
|
// Constants
|
|
5
6
|
const MAX_QUESTIONS = 5;
|
|
6
7
|
const MAX_REVISION_ATTEMPTS = 3;
|
|
@@ -18,6 +19,7 @@ const CHECKPOINT_MAP = {
|
|
|
18
19
|
*/
|
|
19
20
|
export async function presentQuestionsSequentially(questions, responder, options = {}) {
|
|
20
21
|
const { maxQuestions = MAX_QUESTIONS, customInputPrompt } = options;
|
|
22
|
+
let { language = 'en' } = options;
|
|
21
23
|
const session = {
|
|
22
24
|
section: questions[0]?.section || 'product',
|
|
23
25
|
questionsAsked: [],
|
|
@@ -28,30 +30,47 @@ export async function presentQuestionsSequentially(questions, responder, options
|
|
|
28
30
|
};
|
|
29
31
|
for (let i = 0; i < Math.min(questions.length, maxQuestions); i++) {
|
|
30
32
|
const question = questions[i];
|
|
31
|
-
|
|
33
|
+
// Translate question if language is Spanish
|
|
34
|
+
const translatedQuestion = language === 'es' ? translateQuestion(question, 'es') : question;
|
|
35
|
+
session.questionsAsked.push(translatedQuestion);
|
|
32
36
|
// Get user response
|
|
33
|
-
let selections;
|
|
37
|
+
let selections = [];
|
|
34
38
|
let isValid = false;
|
|
35
39
|
// Keep asking until valid response
|
|
36
40
|
while (!isValid) {
|
|
37
|
-
selections = await responder(
|
|
38
|
-
isValid = validateSelection(selections.join(','),
|
|
41
|
+
selections = await responder(translatedQuestion, i + 1);
|
|
42
|
+
isValid = validateSelection(selections.join(','), translatedQuestion.type);
|
|
39
43
|
}
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
// Auto-detect language from first user response if custom input provided
|
|
45
|
+
if (i === 0 && selections.length > 0 && selections[0].length > 5) {
|
|
46
|
+
const detectedLang = detectLanguage(selections.join(' '));
|
|
47
|
+
if (detectedLang !== language) {
|
|
48
|
+
language = detectedLang;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const parsedSelections = parseSelection(selections.join(','), translatedQuestion.type);
|
|
52
|
+
// Map selections to actual option text (from translated question)
|
|
42
53
|
const selectedOptions = parsedSelections.map(letter => {
|
|
43
54
|
const index = letter.charCodeAt(0) - 'A'.charCodeAt(0);
|
|
44
|
-
return
|
|
55
|
+
return translatedQuestion.options[index];
|
|
45
56
|
});
|
|
46
57
|
const answer = {
|
|
47
|
-
questionId:
|
|
58
|
+
questionId: translatedQuestion.id,
|
|
48
59
|
selections: parsedSelections,
|
|
49
60
|
selectedOptions,
|
|
50
61
|
timestamp: new Date().toISOString(),
|
|
51
62
|
};
|
|
52
63
|
// Handle option D (custom input)
|
|
53
64
|
if (parsedSelections.includes('D') && customInputPrompt) {
|
|
54
|
-
|
|
65
|
+
const customInput = await customInputPrompt(translatedQuestion);
|
|
66
|
+
answer.customText = customInput;
|
|
67
|
+
// Detect language from custom text input
|
|
68
|
+
if (customInput && customInput.length > 5) {
|
|
69
|
+
const detectedLang = detectLanguage(customInput);
|
|
70
|
+
if (detectedLang !== language) {
|
|
71
|
+
language = detectedLang;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
55
74
|
}
|
|
56
75
|
session.answers.push(answer);
|
|
57
76
|
// Handle option E (auto-generate) - stop asking questions
|
|
@@ -242,6 +261,7 @@ export async function generateDocument(options) {
|
|
|
242
261
|
// Step 1: Present questions sequentially
|
|
243
262
|
const session = await presentQuestionsSequentially(questions, responder, {
|
|
244
263
|
customInputPrompt,
|
|
264
|
+
language: options.language || 'en',
|
|
245
265
|
});
|
|
246
266
|
// Step 2: Draft document from answers
|
|
247
267
|
let draft = draftDocumentFromAnswers(session, analysis);
|
|
@@ -121,6 +121,11 @@ describe('interactiveMenu', () => {
|
|
|
121
121
|
expect(validateSelection('', 'additive')).toBe(false);
|
|
122
122
|
expect(validateSelection(' ', 'exclusive')).toBe(false);
|
|
123
123
|
});
|
|
124
|
+
it('should reject input with only commas or whitespace', () => {
|
|
125
|
+
expect(validateSelection(',,,', 'exclusive')).toBe(false);
|
|
126
|
+
expect(validateSelection(', , ,', 'additive')).toBe(false);
|
|
127
|
+
expect(validateSelection(' , ', 'exclusive')).toBe(false);
|
|
128
|
+
});
|
|
124
129
|
it('should reject option E in combination with others for additive', () => {
|
|
125
130
|
expect(validateSelection('A,E', 'additive')).toBe(false);
|
|
126
131
|
expect(validateSelection('E,A', 'additive')).toBe(false);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Question } from './questionGenerator.js';
|
|
2
|
+
export type Language = 'en' | 'es';
|
|
3
|
+
export declare function detectLanguage(text: string): Language;
|
|
4
|
+
export declare function translateQuestion(question: Question, language: Language): Question;
|
|
5
|
+
export declare function translateInstruction(key: string, language: Language): string;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
const SPANISH_INDICATORS = [
|
|
2
|
+
'¿', '¡', 'á', 'é', 'í', 'ó', 'ú', 'ñ', 'ü',
|
|
3
|
+
'cuál', 'qué', 'cómo', 'dónde', 'cuándo', 'quién',
|
|
4
|
+
'los', 'las', 'una', 'del', 'con', 'para', 'por',
|
|
5
|
+
'necesito', 'quiero', 'aplicación', 'proyecto', 'crear',
|
|
6
|
+
'objetivo', 'principal', 'usuarios', 'principales',
|
|
7
|
+
'añadir', 'configuración', 'nuevo', 'algo',
|
|
8
|
+
];
|
|
9
|
+
const ENGLISH_INDICATORS = [
|
|
10
|
+
'what', 'how', 'where', 'when', 'who', 'which',
|
|
11
|
+
'the', 'a', 'an', 'of', 'with', 'for', 'by',
|
|
12
|
+
'need', 'want', 'application', 'project', 'create',
|
|
13
|
+
'main', 'primary', 'users', 'add', 'something', 'new',
|
|
14
|
+
];
|
|
15
|
+
const TRANSLATIONS = {
|
|
16
|
+
'menu.select_option': {
|
|
17
|
+
en: 'Select your option (e.g., "A", "B", "C")',
|
|
18
|
+
es: 'Selecciona tu opción (ej: "A", "B", "C")',
|
|
19
|
+
},
|
|
20
|
+
'menu.custom_input': {
|
|
21
|
+
en: 'Enter custom text',
|
|
22
|
+
es: 'Ingresa texto personalizado',
|
|
23
|
+
},
|
|
24
|
+
'menu.auto_generate': {
|
|
25
|
+
en: 'Auto-generate from context',
|
|
26
|
+
es: 'Auto-generar desde contexto',
|
|
27
|
+
},
|
|
28
|
+
'approval.approve': {
|
|
29
|
+
en: 'Approve this document',
|
|
30
|
+
es: 'Aprobar este documento',
|
|
31
|
+
},
|
|
32
|
+
'approval.revise': {
|
|
33
|
+
en: 'Request revisions',
|
|
34
|
+
es: 'Solicitar revisiones',
|
|
35
|
+
},
|
|
36
|
+
'question.primary_purpose': {
|
|
37
|
+
en: 'What is the primary purpose of this project?',
|
|
38
|
+
es: '¿Cuál es el propósito principal de este proyecto?',
|
|
39
|
+
},
|
|
40
|
+
'option.web_application': {
|
|
41
|
+
en: 'Build a web application',
|
|
42
|
+
es: 'Construir una aplicación web',
|
|
43
|
+
},
|
|
44
|
+
'option.cli_tool': {
|
|
45
|
+
en: 'Build a CLI tool',
|
|
46
|
+
es: 'Construir una herramienta CLI',
|
|
47
|
+
},
|
|
48
|
+
'option.library': {
|
|
49
|
+
en: 'Create a library',
|
|
50
|
+
es: 'Crear una biblioteca',
|
|
51
|
+
},
|
|
52
|
+
'option.custom': {
|
|
53
|
+
en: 'Enter custom text',
|
|
54
|
+
es: 'Ingresar texto personalizado',
|
|
55
|
+
},
|
|
56
|
+
'option.auto_generate': {
|
|
57
|
+
en: 'Auto-generate from context',
|
|
58
|
+
es: 'Auto-generar desde contexto',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
export function detectLanguage(text) {
|
|
62
|
+
if (!text || text.trim().length === 0) {
|
|
63
|
+
return 'en';
|
|
64
|
+
}
|
|
65
|
+
const lowerText = text.toLowerCase();
|
|
66
|
+
const words = lowerText.split(/\s+/);
|
|
67
|
+
let spanishScore = 0;
|
|
68
|
+
let englishScore = 0;
|
|
69
|
+
for (let i = 0; i < words.length; i++) {
|
|
70
|
+
const word = words[i];
|
|
71
|
+
// First word gets significantly more weight (10x) to dominate language detection
|
|
72
|
+
const weight = i === 0 ? 10 : 1;
|
|
73
|
+
if (SPANISH_INDICATORS.includes(word)) {
|
|
74
|
+
spanishScore += weight;
|
|
75
|
+
}
|
|
76
|
+
if (ENGLISH_INDICATORS.includes(word)) {
|
|
77
|
+
englishScore += weight;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Special characters strongly indicate Spanish
|
|
81
|
+
for (const char of ['¿', '¡', 'á', 'é', 'í', 'ó', 'ú', 'ñ', 'ü']) {
|
|
82
|
+
if (lowerText.includes(char)) {
|
|
83
|
+
spanishScore += 5;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return spanishScore > englishScore ? 'es' : 'en';
|
|
87
|
+
}
|
|
88
|
+
export function translateQuestion(question, language) {
|
|
89
|
+
if (language === 'en') {
|
|
90
|
+
return question;
|
|
91
|
+
}
|
|
92
|
+
const translated = {
|
|
93
|
+
...question,
|
|
94
|
+
text: translateQuestionText(question.text, question.id),
|
|
95
|
+
options: question.options.map((opt, idx) => translateOption(opt, idx)),
|
|
96
|
+
};
|
|
97
|
+
return translated;
|
|
98
|
+
}
|
|
99
|
+
function translateQuestionText(text, questionId) {
|
|
100
|
+
const translationKey = `question.${questionId}`;
|
|
101
|
+
if (TRANSLATIONS[translationKey]) {
|
|
102
|
+
return TRANSLATIONS[translationKey].es;
|
|
103
|
+
}
|
|
104
|
+
if (text.includes('primary purpose')) {
|
|
105
|
+
return '¿Cuál es el propósito principal de este proyecto?';
|
|
106
|
+
}
|
|
107
|
+
if (text.includes('target users')) {
|
|
108
|
+
return '¿Quiénes son los usuarios objetivo?';
|
|
109
|
+
}
|
|
110
|
+
if (text.includes('key features')) {
|
|
111
|
+
return '¿Cuáles son las características principales?';
|
|
112
|
+
}
|
|
113
|
+
if (text.includes('programming language')) {
|
|
114
|
+
return '¿Qué lenguajes de programación usarás?';
|
|
115
|
+
}
|
|
116
|
+
if (text.includes('framework')) {
|
|
117
|
+
return '¿Qué frameworks usarás?';
|
|
118
|
+
}
|
|
119
|
+
return text;
|
|
120
|
+
}
|
|
121
|
+
function translateOption(option, index) {
|
|
122
|
+
const optionLower = option.toLowerCase();
|
|
123
|
+
if (optionLower.includes('web application')) {
|
|
124
|
+
return 'Construir una aplicación web';
|
|
125
|
+
}
|
|
126
|
+
if (optionLower.includes('cli tool')) {
|
|
127
|
+
return 'Construir una herramienta CLI';
|
|
128
|
+
}
|
|
129
|
+
if (optionLower.includes('library')) {
|
|
130
|
+
return 'Crear una biblioteca';
|
|
131
|
+
}
|
|
132
|
+
if (optionLower.includes('enter custom') || optionLower.includes('custom text')) {
|
|
133
|
+
return 'Ingresar texto personalizado';
|
|
134
|
+
}
|
|
135
|
+
if (optionLower.includes('auto-generate')) {
|
|
136
|
+
return 'Auto-generar desde contexto';
|
|
137
|
+
}
|
|
138
|
+
if (optionLower.includes('developers')) {
|
|
139
|
+
return 'Desarrolladores';
|
|
140
|
+
}
|
|
141
|
+
if (optionLower.includes('end users')) {
|
|
142
|
+
return 'Usuarios finales';
|
|
143
|
+
}
|
|
144
|
+
if (optionLower.includes('businesses')) {
|
|
145
|
+
return 'Empresas';
|
|
146
|
+
}
|
|
147
|
+
if (optionLower.includes('typescript')) {
|
|
148
|
+
return 'TypeScript';
|
|
149
|
+
}
|
|
150
|
+
if (optionLower.includes('python')) {
|
|
151
|
+
return 'Python';
|
|
152
|
+
}
|
|
153
|
+
if (optionLower.includes('javascript')) {
|
|
154
|
+
return 'JavaScript';
|
|
155
|
+
}
|
|
156
|
+
return option;
|
|
157
|
+
}
|
|
158
|
+
export function translateInstruction(key, language) {
|
|
159
|
+
if (TRANSLATIONS[key] && TRANSLATIONS[key][language]) {
|
|
160
|
+
return TRANSLATIONS[key][language];
|
|
161
|
+
}
|
|
162
|
+
return key;
|
|
163
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|