mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 21:23:07 +00:00
feat(server): Add Cursor provider routing to spec generation routes
Add Cursor model support to generate-spec.ts and generate-features-from-spec.ts routes, allowing them to use Cursor models when configured in phaseModels settings. - Both routes now detect Cursor models via isCursorModel() - Route to ProviderFactory for Cursor models, Claude SDK for Claude models - Use resolveModelString() for proper model ID resolution - Extract JSON from Cursor responses using shared json-extractor utility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,8 +9,10 @@ import { query } from '@anthropic-ai/claude-agent-sdk';
|
|||||||
import * as secureFs from '../../lib/secure-fs.js';
|
import * as secureFs from '../../lib/secure-fs.js';
|
||||||
import type { EventEmitter } from '../../lib/events.js';
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
|
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
|
||||||
|
import { resolveModelString } from '@automaker/model-resolver';
|
||||||
import { createFeatureGenerationOptions } from '../../lib/sdk-options.js';
|
import { createFeatureGenerationOptions } from '../../lib/sdk-options.js';
|
||||||
|
import { ProviderFactory } from '../../providers/provider-factory.js';
|
||||||
import { logAuthStatus } from './common.js';
|
import { logAuthStatus } from './common.js';
|
||||||
import { parseAndCreateFeatures } from './parse-and-create-features.js';
|
import { parseAndCreateFeatures } from './parse-and-create-features.js';
|
||||||
import { getAppSpecPath } from '@automaker/platform';
|
import { getAppSpecPath } from '@automaker/platform';
|
||||||
@@ -109,47 +111,33 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge
|
|||||||
const settings = await settingsService?.getGlobalSettings();
|
const settings = await settingsService?.getGlobalSettings();
|
||||||
const featureGenerationModel =
|
const featureGenerationModel =
|
||||||
settings?.phaseModels?.featureGenerationModel || DEFAULT_PHASE_MODELS.featureGenerationModel;
|
settings?.phaseModels?.featureGenerationModel || DEFAULT_PHASE_MODELS.featureGenerationModel;
|
||||||
|
const model = resolveModelString(featureGenerationModel);
|
||||||
|
|
||||||
logger.info('Using model:', featureGenerationModel);
|
logger.info('Using model:', model);
|
||||||
|
|
||||||
const options = createFeatureGenerationOptions({
|
|
||||||
cwd: projectPath,
|
|
||||||
abortController,
|
|
||||||
autoLoadClaudeMd,
|
|
||||||
model: featureGenerationModel,
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug('SDK Options:', JSON.stringify(options, null, 2));
|
|
||||||
logger.info('Calling Claude Agent SDK query() for features...');
|
|
||||||
|
|
||||||
logAuthStatus('Right before SDK query() for features');
|
|
||||||
|
|
||||||
let stream;
|
|
||||||
try {
|
|
||||||
stream = query({ prompt, options });
|
|
||||||
logger.debug('query() returned stream successfully');
|
|
||||||
} catch (queryError) {
|
|
||||||
logger.error('❌ query() threw an exception:');
|
|
||||||
logger.error('Error:', queryError);
|
|
||||||
throw queryError;
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
let messageCount = 0;
|
let messageCount = 0;
|
||||||
|
|
||||||
logger.debug('Starting to iterate over feature stream...');
|
// Route to appropriate provider based on model type
|
||||||
|
if (isCursorModel(model)) {
|
||||||
|
// Use Cursor provider for Cursor models
|
||||||
|
logger.info('[FeatureGeneration] Using Cursor provider');
|
||||||
|
|
||||||
try {
|
const provider = ProviderFactory.getProviderForModel(model);
|
||||||
for await (const msg of stream) {
|
|
||||||
|
for await (const msg of provider.executeQuery({
|
||||||
|
prompt,
|
||||||
|
model,
|
||||||
|
cwd: projectPath,
|
||||||
|
maxTurns: 250,
|
||||||
|
allowedTools: ['Read', 'Glob', 'Grep'],
|
||||||
|
abortController,
|
||||||
|
})) {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
logger.debug(
|
|
||||||
`Feature stream message #${messageCount}:`,
|
|
||||||
JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (msg.type === 'assistant' && msg.message.content) {
|
if (msg.type === 'assistant' && msg.message?.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === 'text') {
|
if (block.type === 'text' && block.text) {
|
||||||
responseText += block.text;
|
responseText += block.text;
|
||||||
logger.debug(`Feature text block received (${block.text.length} chars)`);
|
logger.debug(`Feature text block received (${block.text.length} chars)`);
|
||||||
events.emit('spec-regeneration:event', {
|
events.emit('spec-regeneration:event', {
|
||||||
@@ -159,18 +147,74 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === 'result' && (msg as any).subtype === 'success') {
|
} else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
|
||||||
logger.debug('Received success result for features');
|
// Use result if it's a final accumulated message
|
||||||
responseText = (msg as any).result || responseText;
|
if (msg.result.length > responseText.length) {
|
||||||
} else if ((msg as { type: string }).type === 'error') {
|
responseText = msg.result;
|
||||||
logger.error('❌ Received error message from feature stream:');
|
}
|
||||||
logger.error('Error message:', JSON.stringify(msg, null, 2));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (streamError) {
|
} else {
|
||||||
logger.error('❌ Error while iterating feature stream:');
|
// Use Claude SDK for Claude models
|
||||||
logger.error('Stream error:', streamError);
|
logger.info('[FeatureGeneration] Using Claude SDK');
|
||||||
throw streamError;
|
|
||||||
|
const options = createFeatureGenerationOptions({
|
||||||
|
cwd: projectPath,
|
||||||
|
abortController,
|
||||||
|
autoLoadClaudeMd,
|
||||||
|
model,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('SDK Options:', JSON.stringify(options, null, 2));
|
||||||
|
logger.info('Calling Claude Agent SDK query() for features...');
|
||||||
|
|
||||||
|
logAuthStatus('Right before SDK query() for features');
|
||||||
|
|
||||||
|
let stream;
|
||||||
|
try {
|
||||||
|
stream = query({ prompt, options });
|
||||||
|
logger.debug('query() returned stream successfully');
|
||||||
|
} catch (queryError) {
|
||||||
|
logger.error('❌ query() threw an exception:');
|
||||||
|
logger.error('Error:', queryError);
|
||||||
|
throw queryError;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('Starting to iterate over feature stream...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const msg of stream) {
|
||||||
|
messageCount++;
|
||||||
|
logger.debug(
|
||||||
|
`Feature stream message #${messageCount}:`,
|
||||||
|
JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (msg.type === 'assistant' && msg.message.content) {
|
||||||
|
for (const block of msg.message.content) {
|
||||||
|
if (block.type === 'text') {
|
||||||
|
responseText += block.text;
|
||||||
|
logger.debug(`Feature text block received (${block.text.length} chars)`);
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_regeneration_progress',
|
||||||
|
content: block.text,
|
||||||
|
projectPath: projectPath,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'result' && (msg as any).subtype === 'success') {
|
||||||
|
logger.debug('Received success result for features');
|
||||||
|
responseText = (msg as any).result || responseText;
|
||||||
|
} else if ((msg as { type: string }).type === 'error') {
|
||||||
|
logger.error('❌ Received error message from feature stream:');
|
||||||
|
logger.error('Error message:', JSON.stringify(msg, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (streamError) {
|
||||||
|
logger.error('❌ Error while iterating feature stream:');
|
||||||
|
logger.error('Stream error:', streamError);
|
||||||
|
throw streamError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
|
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ import {
|
|||||||
type SpecOutput,
|
type SpecOutput,
|
||||||
} from '../../lib/app-spec-format.js';
|
} from '../../lib/app-spec-format.js';
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import { DEFAULT_PHASE_MODELS } from '@automaker/types';
|
import { DEFAULT_PHASE_MODELS, isCursorModel } from '@automaker/types';
|
||||||
|
import { resolveModelString } from '@automaker/model-resolver';
|
||||||
import { createSpecGenerationOptions } from '../../lib/sdk-options.js';
|
import { createSpecGenerationOptions } from '../../lib/sdk-options.js';
|
||||||
|
import { extractJson } from '../../lib/json-extractor.js';
|
||||||
|
import { ProviderFactory } from '../../providers/provider-factory.js';
|
||||||
import { logAuthStatus } from './common.js';
|
import { logAuthStatus } from './common.js';
|
||||||
import { generateFeaturesFromSpec } from './generate-features-from-spec.js';
|
import { generateFeaturesFromSpec } from './generate-features-from-spec.js';
|
||||||
import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform';
|
import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform';
|
||||||
@@ -101,106 +104,168 @@ ${getStructuredSpecPromptInstruction()}`;
|
|||||||
const settings = await settingsService?.getGlobalSettings();
|
const settings = await settingsService?.getGlobalSettings();
|
||||||
const specGenerationModel =
|
const specGenerationModel =
|
||||||
settings?.phaseModels?.specGenerationModel || DEFAULT_PHASE_MODELS.specGenerationModel;
|
settings?.phaseModels?.specGenerationModel || DEFAULT_PHASE_MODELS.specGenerationModel;
|
||||||
|
const model = resolveModelString(specGenerationModel);
|
||||||
|
|
||||||
logger.info('Using model:', specGenerationModel);
|
logger.info('Using model:', model);
|
||||||
|
|
||||||
const options = createSpecGenerationOptions({
|
|
||||||
cwd: projectPath,
|
|
||||||
abortController,
|
|
||||||
autoLoadClaudeMd,
|
|
||||||
model: specGenerationModel,
|
|
||||||
outputFormat: {
|
|
||||||
type: 'json_schema',
|
|
||||||
schema: specOutputSchema,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug('SDK Options:', JSON.stringify(options, null, 2));
|
|
||||||
logger.info('Calling Claude Agent SDK query()...');
|
|
||||||
|
|
||||||
// Log auth status right before the SDK call
|
|
||||||
logAuthStatus('Right before SDK query()');
|
|
||||||
|
|
||||||
let stream;
|
|
||||||
try {
|
|
||||||
stream = query({ prompt, options });
|
|
||||||
logger.debug('query() returned stream successfully');
|
|
||||||
} catch (queryError) {
|
|
||||||
logger.error('❌ query() threw an exception:');
|
|
||||||
logger.error('Error:', queryError);
|
|
||||||
throw queryError;
|
|
||||||
}
|
|
||||||
|
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
let messageCount = 0;
|
let messageCount = 0;
|
||||||
let structuredOutput: SpecOutput | null = null;
|
let structuredOutput: SpecOutput | null = null;
|
||||||
|
|
||||||
logger.info('Starting to iterate over stream...');
|
// Route to appropriate provider based on model type
|
||||||
|
if (isCursorModel(model)) {
|
||||||
|
// Use Cursor provider for Cursor models
|
||||||
|
logger.info('[SpecGeneration] Using Cursor provider');
|
||||||
|
|
||||||
try {
|
const provider = ProviderFactory.getProviderForModel(model);
|
||||||
for await (const msg of stream) {
|
|
||||||
|
// For Cursor, include the JSON schema in the prompt
|
||||||
|
const cursorPrompt = `${prompt}
|
||||||
|
|
||||||
|
IMPORTANT: You must respond with a valid JSON object matching this schema:
|
||||||
|
${JSON.stringify(specOutputSchema, null, 2)}`;
|
||||||
|
|
||||||
|
for await (const msg of provider.executeQuery({
|
||||||
|
prompt: cursorPrompt,
|
||||||
|
model,
|
||||||
|
cwd: projectPath,
|
||||||
|
maxTurns: 250,
|
||||||
|
allowedTools: ['Read', 'Glob', 'Grep'],
|
||||||
|
abortController,
|
||||||
|
})) {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
logger.info(
|
|
||||||
`Stream message #${messageCount}: type=${msg.type}, subtype=${(msg as any).subtype}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (msg.type === 'assistant') {
|
if (msg.type === 'assistant' && msg.message?.content) {
|
||||||
const msgAny = msg as any;
|
for (const block of msg.message.content) {
|
||||||
if (msgAny.message?.content) {
|
if (block.type === 'text' && block.text) {
|
||||||
for (const block of msgAny.message.content) {
|
responseText += block.text;
|
||||||
if (block.type === 'text') {
|
logger.info(
|
||||||
responseText += block.text;
|
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
|
||||||
logger.info(
|
);
|
||||||
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
|
events.emit('spec-regeneration:event', {
|
||||||
);
|
type: 'spec_regeneration_progress',
|
||||||
events.emit('spec-regeneration:event', {
|
content: block.text,
|
||||||
type: 'spec_regeneration_progress',
|
projectPath: projectPath,
|
||||||
content: block.text,
|
});
|
||||||
projectPath: projectPath,
|
} else if (block.type === 'tool_use') {
|
||||||
});
|
logger.info('Tool use:', block.name);
|
||||||
} else if (block.type === 'tool_use') {
|
events.emit('spec-regeneration:event', {
|
||||||
logger.info('Tool use:', block.name);
|
type: 'spec_tool',
|
||||||
events.emit('spec-regeneration:event', {
|
tool: block.name,
|
||||||
type: 'spec_tool',
|
input: block.input,
|
||||||
tool: block.name,
|
});
|
||||||
input: block.input,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === 'result' && (msg as any).subtype === 'success') {
|
} else if (msg.type === 'result' && msg.subtype === 'success' && msg.result) {
|
||||||
logger.info('Received success result');
|
// Use result if it's a final accumulated message
|
||||||
// Check for structured output - this is the reliable way to get spec data
|
if (msg.result.length > responseText.length) {
|
||||||
const resultMsg = msg as any;
|
responseText = msg.result;
|
||||||
if (resultMsg.structured_output) {
|
|
||||||
structuredOutput = resultMsg.structured_output as SpecOutput;
|
|
||||||
logger.info('✅ Received structured output');
|
|
||||||
logger.debug('Structured output:', JSON.stringify(structuredOutput, null, 2));
|
|
||||||
} else {
|
|
||||||
logger.warn('⚠️ No structured output in result, will fall back to text parsing');
|
|
||||||
}
|
}
|
||||||
} else if (msg.type === 'result') {
|
|
||||||
// Handle error result types
|
|
||||||
const subtype = (msg as any).subtype;
|
|
||||||
logger.info(`Result message: subtype=${subtype}`);
|
|
||||||
if (subtype === 'error_max_turns') {
|
|
||||||
logger.error('❌ Hit max turns limit!');
|
|
||||||
} else if (subtype === 'error_max_structured_output_retries') {
|
|
||||||
logger.error('❌ Failed to produce valid structured output after retries');
|
|
||||||
throw new Error('Could not produce valid spec output');
|
|
||||||
}
|
|
||||||
} else if ((msg as { type: string }).type === 'error') {
|
|
||||||
logger.error('❌ Received error message from stream:');
|
|
||||||
logger.error('Error message:', JSON.stringify(msg, null, 2));
|
|
||||||
} else if (msg.type === 'user') {
|
|
||||||
// Log user messages (tool results)
|
|
||||||
logger.info(`User message (tool result): ${JSON.stringify(msg).substring(0, 500)}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (streamError) {
|
|
||||||
logger.error('❌ Error while iterating stream:');
|
// Parse JSON from the response text using shared utility
|
||||||
logger.error('Stream error:', streamError);
|
if (responseText) {
|
||||||
throw streamError;
|
structuredOutput = extractJson<SpecOutput>(responseText, { logger });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use Claude SDK for Claude models
|
||||||
|
logger.info('[SpecGeneration] Using Claude SDK');
|
||||||
|
|
||||||
|
const options = createSpecGenerationOptions({
|
||||||
|
cwd: projectPath,
|
||||||
|
abortController,
|
||||||
|
autoLoadClaudeMd,
|
||||||
|
model,
|
||||||
|
outputFormat: {
|
||||||
|
type: 'json_schema',
|
||||||
|
schema: specOutputSchema,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.debug('SDK Options:', JSON.stringify(options, null, 2));
|
||||||
|
logger.info('Calling Claude Agent SDK query()...');
|
||||||
|
|
||||||
|
// Log auth status right before the SDK call
|
||||||
|
logAuthStatus('Right before SDK query()');
|
||||||
|
|
||||||
|
let stream;
|
||||||
|
try {
|
||||||
|
stream = query({ prompt, options });
|
||||||
|
logger.debug('query() returned stream successfully');
|
||||||
|
} catch (queryError) {
|
||||||
|
logger.error('❌ query() threw an exception:');
|
||||||
|
logger.error('Error:', queryError);
|
||||||
|
throw queryError;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Starting to iterate over stream...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
for await (const msg of stream) {
|
||||||
|
messageCount++;
|
||||||
|
logger.info(
|
||||||
|
`Stream message #${messageCount}: type=${msg.type}, subtype=${(msg as any).subtype}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (msg.type === 'assistant') {
|
||||||
|
const msgAny = msg as any;
|
||||||
|
if (msgAny.message?.content) {
|
||||||
|
for (const block of msgAny.message.content) {
|
||||||
|
if (block.type === 'text') {
|
||||||
|
responseText += block.text;
|
||||||
|
logger.info(
|
||||||
|
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
|
||||||
|
);
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_regeneration_progress',
|
||||||
|
content: block.text,
|
||||||
|
projectPath: projectPath,
|
||||||
|
});
|
||||||
|
} else if (block.type === 'tool_use') {
|
||||||
|
logger.info('Tool use:', block.name);
|
||||||
|
events.emit('spec-regeneration:event', {
|
||||||
|
type: 'spec_tool',
|
||||||
|
tool: block.name,
|
||||||
|
input: block.input,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'result' && (msg as any).subtype === 'success') {
|
||||||
|
logger.info('Received success result');
|
||||||
|
// Check for structured output - this is the reliable way to get spec data
|
||||||
|
const resultMsg = msg as any;
|
||||||
|
if (resultMsg.structured_output) {
|
||||||
|
structuredOutput = resultMsg.structured_output as SpecOutput;
|
||||||
|
logger.info('✅ Received structured output');
|
||||||
|
logger.debug('Structured output:', JSON.stringify(structuredOutput, null, 2));
|
||||||
|
} else {
|
||||||
|
logger.warn('⚠️ No structured output in result, will fall back to text parsing');
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'result') {
|
||||||
|
// Handle error result types
|
||||||
|
const subtype = (msg as any).subtype;
|
||||||
|
logger.info(`Result message: subtype=${subtype}`);
|
||||||
|
if (subtype === 'error_max_turns') {
|
||||||
|
logger.error('❌ Hit max turns limit!');
|
||||||
|
} else if (subtype === 'error_max_structured_output_retries') {
|
||||||
|
logger.error('❌ Failed to produce valid structured output after retries');
|
||||||
|
throw new Error('Could not produce valid spec output');
|
||||||
|
}
|
||||||
|
} else if ((msg as { type: string }).type === 'error') {
|
||||||
|
logger.error('❌ Received error message from stream:');
|
||||||
|
logger.error('Error message:', JSON.stringify(msg, null, 2));
|
||||||
|
} else if (msg.type === 'user') {
|
||||||
|
// Log user messages (tool results)
|
||||||
|
logger.info(`User message (tool result): ${JSON.stringify(msg).substring(0, 500)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (streamError) {
|
||||||
|
logger.error('❌ Error while iterating stream:');
|
||||||
|
logger.error('Stream error:', streamError);
|
||||||
|
throw streamError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Stream iteration complete. Total messages: ${messageCount}`);
|
logger.info(`Stream iteration complete. Total messages: ${messageCount}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user