diff --git a/.claude/ralph-loop.local.md b/.claude/ralph-loop.local.md
index c9cb649..38942b5 100644
--- a/.claude/ralph-loop.local.md
+++ b/.claude/ralph-loop.local.md
@@ -1,37 +1,40 @@
---
active: true
iteration: 1
-max_iterations: 30
-completion_promise: "PHASE_1_COMPLETE"
-started_at: "2026-01-10T12:00:45Z"
+max_iterations: 40
+completion_promise: "PHASE_2_COMPLETE"
+started_at: "2026-01-10T12:10:57Z"
---
-Execute Phase 1 - Foundation from PROMPT.md.
+Execute Phase 2 - Core API Integration from PROMPT.md.
-You are building the Ralph PRD Generator CLI tool.
+Phase 1 is complete. Now implement Phase 2.
-Read PROMPT.md for full specification.
-Read prd.json for feature tracking.
-Read CLAUDE.md for project configuration.
+Read PROMPT.md for API specifications.
+Read prd.json for Phase 2 features.
-Phase 1 tasks:
-1. Set up TypeScript project with all dependencies
-2. Configure tsup, vitest, eslint
-3. Implement CLI framework with Commander.js
-4. Implement config management for API key storage
-5. Create basic project structure
-6. Write tests for config and CLI
+Phase 2 tasks:
+1. Claude API client with Anthropic SDK
+2. Perplexity API client with fetch
+3. Architecture classification prompt and parser
+4. Research query generation
+5. Tests for all API clients
+
+API Integration notes:
+- Use environment variables for API keys during testing
+- Implement retry logic with 3 attempts and exponential backoff
+- Handle rate limits gracefully
+- Parse JSON responses safely
For each feature:
-1. Write tests first
+1. Write tests first using mocked API calls
2. Implement the feature
-3. Run: npm run build && npm run test && npm run lint
-4. If all pass, update prd.json by setting passes to true for completed features
-5. Commit with descriptive message
-6. Append progress to progress.txt
+3. Verify: npm run build && npm run test && npm run lint
+4. Update prd.json when feature passes
+5. Commit and log progress
-When ALL Phase 1 features in prd.json have passes set to true:
-Output PHASE_1_COMPLETE
+When ALL Phase 2 features pass:
+Output PHASE_2_COMPLETE
-If blocked after 10 attempts on same issue:
-Document in progress.txt and output PHASE_1_BLOCKED
+If blocked:
+Output PHASE_2_BLOCKED
diff --git a/prd.json b/prd.json
index 6ff7e60..803f67e 100644
--- a/prd.json
+++ b/prd.json
@@ -26,7 +26,7 @@
"name": "Claude API Client",
"description": "Anthropic SDK integration with retry logic",
"priority": 1,
- "passes": false,
+ "passes": true,
"acceptance": "Can send prompt to Claude and receive response, retries on failure"
},
{
@@ -35,7 +35,7 @@
"name": "Perplexity API Client",
"description": "Perplexity REST API integration",
"priority": 2,
- "passes": false,
+ "passes": true,
"acceptance": "Can search Perplexity and parse response with sources"
},
{
@@ -44,7 +44,7 @@
"name": "Architecture Classification",
"description": "Claude prompt for classifying app type",
"priority": 3,
- "passes": false,
+ "passes": true,
"acceptance": "Given idea text, returns valid Architecture JSON"
},
{
@@ -53,7 +53,7 @@
"name": "Research Query Generation",
"description": "Generate Perplexity queries from architecture",
"priority": 4,
- "passes": false,
+ "passes": true,
"acceptance": "Given architecture, generates 4-6 relevant search queries"
},
{
diff --git a/progress.txt b/progress.txt
index bb3109b..981c08b 100644
--- a/progress.txt
+++ b/progress.txt
@@ -18,3 +18,24 @@
* ralph-vibe init/new/validate/research exist
* ralph-vibe new fails gracefully without keys
+[2026-01-10T12:18:00Z] [2] [COMPLETE] - Phase 2 Core API Integration implemented
+ - Claude API client with Anthropic SDK
+ * Retry logic with exponential backoff (3 attempts)
+ * Handles auth errors, rate limits, server errors
+ * Architecture classification with JSON parsing
+ * Research query generation
+ - Perplexity API client with native fetch
+ * Retry logic with exponential backoff
+ * Response parsing with sources and citations
+ * Research queries execution
+ - Prompt templates for architecture classification
+ - Default query generation for fallback
+ - ArchitectureGenerator for full pipeline
+ - 80 tests passing, all lint checks pass
+ - All Phase 2 acceptance criteria met:
+ * Can send prompt to Claude and receive response
+ * Retries on failure with exponential backoff
+ * Can search Perplexity and parse response with sources
+ * Given idea text, returns valid Architecture JSON
+ * Given architecture, generates 4-6 relevant search queries
+
diff --git a/src/__tests__/architecture.test.ts b/src/__tests__/architecture.test.ts
new file mode 100644
index 0000000..ca3f1b1
--- /dev/null
+++ b/src/__tests__/architecture.test.ts
@@ -0,0 +1,172 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import type { Config } from '../types/index.js';
+
+// Create mock functions
+const mockClassifyArchitecture = vi.fn();
+const mockGenerateResearchQueries = vi.fn();
+const mockResearchQueries = vi.fn();
+
+// Mock the clients as classes
+vi.mock('../clients/claude.js', () => ({
+ ClaudeClient: class MockClaudeClient {
+ classifyArchitecture = mockClassifyArchitecture;
+ generateResearchQueries = mockGenerateResearchQueries;
+ },
+}));
+
+vi.mock('../clients/perplexity.js', () => ({
+ PerplexityClient: class MockPerplexityClient {
+ researchQueries = mockResearchQueries;
+ },
+}));
+
+// Import after mocking
+import { ArchitectureGenerator } from '../generators/architecture.js';
+
+describe('ArchitectureGenerator', () => {
+ const mockConfig: Config = {
+ claudeApiKey: 'claude-test-key',
+ perplexityApiKey: 'perplexity-test-key',
+ };
+
+ const mockArchitecture = {
+ appType: 'cli' as const,
+ appTypeReason: 'Command-line tool',
+ interfaces: ['cli_args' as const],
+ interfacesReason: 'CLI arguments',
+ persistence: 'file_based' as const,
+ persistenceReason: 'File storage',
+ deployment: 'package_registry' as const,
+ deploymentReason: 'npm',
+ suggestedTechStack: {
+ language: 'TypeScript',
+ framework: 'Commander.js',
+ reasoning: 'CLI framework',
+ },
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ describe('constructor', () => {
+ it('throws error when Claude API key is missing', () => {
+ const configWithoutClaude: Config = {
+ perplexityApiKey: 'key',
+ };
+
+ expect(() => new ArchitectureGenerator({ config: configWithoutClaude }))
+ .toThrow('Claude API key is required');
+ });
+
+ it('creates generator with valid config', () => {
+ expect(() => new ArchitectureGenerator({ config: mockConfig }))
+ .not.toThrow();
+ });
+
+ it('creates generator without Perplexity when skipResearch is true', () => {
+ const configWithoutPerplexity: Config = {
+ claudeApiKey: 'key',
+ };
+
+ expect(() => new ArchitectureGenerator({
+ config: configWithoutPerplexity,
+ skipResearch: true,
+ })).not.toThrow();
+ });
+ });
+
+ describe('classifyIdea', () => {
+ it('calls Claude client to classify idea', async () => {
+ mockClassifyArchitecture.mockResolvedValue(mockArchitecture);
+
+ const generator = new ArchitectureGenerator({ config: mockConfig });
+ const result = await generator.classifyIdea('A CLI tool for converting files');
+
+ expect(mockClassifyArchitecture).toHaveBeenCalledWith('A CLI tool for converting files');
+ expect(result).toEqual(mockArchitecture);
+ });
+ });
+
+ describe('generateResearchQueries', () => {
+ it('uses Claude to generate queries', async () => {
+ const mockQueries = ['query1', 'query2', 'query3', 'query4'];
+ mockGenerateResearchQueries.mockResolvedValue(mockQueries);
+
+ const generator = new ArchitectureGenerator({ config: mockConfig });
+ const result = await generator.generateResearchQueries(mockArchitecture);
+
+ expect(mockGenerateResearchQueries).toHaveBeenCalledWith(mockArchitecture);
+ expect(result).toEqual(mockQueries);
+ });
+
+ it('falls back to default queries when Claude fails', async () => {
+ mockGenerateResearchQueries.mockRejectedValue(new Error('API error'));
+
+ const generator = new ArchitectureGenerator({ config: mockConfig });
+ const result = await generator.generateResearchQueries(mockArchitecture);
+
+ // Should return default queries
+ expect(result.length).toBeGreaterThanOrEqual(4);
+ expect(result.some(q => q.toLowerCase().includes('cli'))).toBe(true);
+ });
+ });
+
+ describe('conductResearch', () => {
+ it('uses Perplexity to research queries', async () => {
+ const mockResearch = {
+ queries: ['q1', 'q2'],
+ results: [
+ { answer: 'Answer 1', sources: [], citations: [] },
+ { answer: 'Answer 2', sources: [], citations: [] },
+ ],
+ summary: 'Research summary',
+ };
+ mockResearchQueries.mockResolvedValue(mockResearch);
+
+ const generator = new ArchitectureGenerator({ config: mockConfig });
+ const result = await generator.conductResearch(['q1', 'q2']);
+
+ expect(mockResearchQueries).toHaveBeenCalledWith(['q1', 'q2']);
+ expect(result).toEqual(mockResearch);
+ });
+
+ it('returns empty research when Perplexity is not available', async () => {
+ const configWithoutPerplexity: Config = {
+ claudeApiKey: 'key',
+ };
+
+ const generator = new ArchitectureGenerator({
+ config: configWithoutPerplexity,
+ skipResearch: true,
+ });
+
+ const result = await generator.conductResearch(['q1', 'q2']);
+
+ expect(result.results).toHaveLength(0);
+ expect(result.summary).toBe('Research was skipped.');
+ });
+ });
+
+ describe('analyzeAndResearch', () => {
+ it('performs full analysis and research pipeline', async () => {
+ const mockQueries = ['query1', 'query2', 'query3', 'query4'];
+ const mockResearch = {
+ queries: mockQueries,
+ results: [{ answer: 'Research result', sources: [], citations: [] }],
+ summary: 'Summary',
+ };
+
+ mockClassifyArchitecture.mockResolvedValue(mockArchitecture);
+ mockGenerateResearchQueries.mockResolvedValue(mockQueries);
+ mockResearchQueries.mockResolvedValue(mockResearch);
+
+ const generator = new ArchitectureGenerator({ config: mockConfig });
+ const result = await generator.analyzeAndResearch('A CLI tool for files');
+
+ expect(result.architecture).toEqual(mockArchitecture);
+ expect(result.queries).toEqual(mockQueries);
+ expect(result.research).toEqual(mockResearch);
+ });
+ });
+});
diff --git a/src/__tests__/claude.test.ts b/src/__tests__/claude.test.ts
new file mode 100644
index 0000000..9b2ac67
--- /dev/null
+++ b/src/__tests__/claude.test.ts
@@ -0,0 +1,199 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+
+// Create a mock for the messages.create function
+const mockCreate = vi.fn();
+
+// Mock the Anthropic SDK as a class
+vi.mock('@anthropic-ai/sdk', () => {
+ return {
+ default: class MockAnthropic {
+ messages = {
+ create: mockCreate,
+ };
+ },
+ };
+});
+
+// Import after mocking
+import { ClaudeClient } from '../clients/claude.js';
+
+describe('ClaudeClient', () => {
+ let client: ClaudeClient;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ client = new ClaudeClient({ apiKey: 'test-key' });
+ });
+
+ describe('analyze', () => {
+ it('returns text response from Claude', async () => {
+ mockCreate.mockResolvedValueOnce({
+ content: [{ type: 'text', text: 'Hello, world!' }],
+ });
+
+ const result = await client.analyze('test prompt');
+ expect(result).toBe('Hello, world!');
+ });
+
+ it('throws error when no text block in response', async () => {
+ mockCreate.mockResolvedValueOnce({
+ content: [],
+ });
+
+ await expect(client.analyze('test prompt')).rejects.toThrow('No text response from Claude');
+ });
+
+ it('throws error for invalid API key', async () => {
+ const authError = new Error('Unauthorized') as Error & { status: number };
+ authError.status = 401;
+ mockCreate.mockRejectedValueOnce(authError);
+
+ await expect(client.analyze('test prompt')).rejects.toThrow('Invalid Claude API key');
+ });
+
+ it('retries on server errors', async () => {
+ const serverError = new Error('Server Error') as Error & { status: number };
+ serverError.status = 500;
+
+ mockCreate
+ .mockRejectedValueOnce(serverError)
+ .mockRejectedValueOnce(serverError)
+ .mockResolvedValueOnce({
+ content: [{ type: 'text', text: 'Success after retry' }],
+ });
+
+ const result = await client.analyze('test prompt');
+ expect(result).toBe('Success after retry');
+ expect(mockCreate).toHaveBeenCalledTimes(3);
+ });
+ });
+
+ describe('classifyArchitecture', () => {
+ it('parses valid architecture JSON', async () => {
+ const architectureJson = JSON.stringify({
+ appType: 'cli',
+ appTypeReason: 'Command-line tool',
+ interfaces: ['cli_args'],
+ interfacesReason: 'CLI arguments',
+ persistence: 'file_based',
+ persistenceReason: 'File storage',
+ deployment: 'package_registry',
+ deploymentReason: 'npm',
+ suggestedTechStack: {
+ language: 'TypeScript',
+ framework: 'Commander.js',
+ reasoning: 'CLI framework',
+ },
+ });
+
+ mockCreate.mockResolvedValueOnce({
+ content: [{ type: 'text', text: architectureJson }],
+ });
+
+ const result = await client.classifyArchitecture('A CLI tool for converting files');
+
+ expect(result.appType).toBe('cli');
+ expect(result.interfaces).toContain('cli_args');
+ expect(result.persistence).toBe('file_based');
+ expect(result.deployment).toBe('package_registry');
+ });
+
+ it('handles JSON wrapped in markdown code blocks', async () => {
+ const architectureJson = `\`\`\`json
+{
+ "appType": "web",
+ "appTypeReason": "Web app",
+ "interfaces": ["rest_api"],
+ "interfacesReason": "REST API",
+ "persistence": "remote_db",
+ "persistenceReason": "Database",
+ "deployment": "cloud",
+ "deploymentReason": "Cloud deployment",
+ "suggestedTechStack": {
+ "language": "TypeScript",
+ "framework": "Express",
+ "reasoning": "Node.js backend"
+ }
+}
+\`\`\``;
+
+ mockCreate.mockResolvedValueOnce({
+ content: [{ type: 'text', text: architectureJson }],
+ });
+
+ const result = await client.classifyArchitecture('A web application');
+
+ expect(result.appType).toBe('web');
+ expect(result.interfaces).toContain('rest_api');
+ });
+
+ it('throws error for invalid JSON', async () => {
+ mockCreate.mockResolvedValueOnce({
+ content: [{ type: 'text', text: 'not valid json' }],
+ });
+
+ await expect(client.classifyArchitecture('test')).rejects.toThrow('Failed to parse architecture');
+ });
+
+ it('throws error for missing required fields', async () => {
+ mockCreate.mockResolvedValueOnce({
+ content: [{ type: 'text', text: '{"appType": "web"}' }],
+ });
+
+ await expect(client.classifyArchitecture('test')).rejects.toThrow('Missing required fields');
+ });
+ });
+
+ describe('generateResearchQueries', () => {
+ const mockArchitecture = {
+ appType: 'cli' as const,
+ appTypeReason: 'Command-line tool',
+ interfaces: ['cli_args' as const],
+ interfacesReason: 'CLI arguments',
+ persistence: 'file_based' as const,
+ persistenceReason: 'File storage',
+ deployment: 'package_registry' as const,
+ deploymentReason: 'npm',
+ suggestedTechStack: {
+ language: 'TypeScript',
+ framework: 'Commander.js',
+ reasoning: 'CLI framework',
+ },
+ };
+
+ it('parses valid query array', async () => {
+ const queries = ['query1', 'query2', 'query3', 'query4'];
+
+ mockCreate.mockResolvedValueOnce({
+ content: [{ type: 'text', text: JSON.stringify(queries) }],
+ });
+
+ const result = await client.generateResearchQueries(mockArchitecture);
+
+ expect(result).toEqual(queries);
+ expect(result.length).toBe(4);
+ });
+
+ it('handles JSON wrapped in markdown code blocks', async () => {
+ const queriesJson = `\`\`\`json
+["query1", "query2", "query3", "query4"]
+\`\`\``;
+
+ mockCreate.mockResolvedValueOnce({
+ content: [{ type: 'text', text: queriesJson }],
+ });
+
+ const result = await client.generateResearchQueries(mockArchitecture);
+
+ expect(result).toEqual(['query1', 'query2', 'query3', 'query4']);
+ });
+
+ it('throws error for non-array response', async () => {
+ mockCreate.mockResolvedValueOnce({
+ content: [{ type: 'text', text: '{"not": "an array"}' }],
+ });
+
+ await expect(client.generateResearchQueries(mockArchitecture)).rejects.toThrow('not an array');
+ });
+ });
+});
diff --git a/src/__tests__/perplexity.test.ts b/src/__tests__/perplexity.test.ts
new file mode 100644
index 0000000..8efff01
--- /dev/null
+++ b/src/__tests__/perplexity.test.ts
@@ -0,0 +1,235 @@
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { PerplexityClient } from '../clients/perplexity.js';
+
+describe('PerplexityClient', () => {
+ let client: PerplexityClient;
+ let mockFetch: ReturnType;
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockFetch = vi.fn();
+ global.fetch = mockFetch;
+ client = new PerplexityClient({ apiKey: 'test-key' });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe('search', () => {
+ it('returns parsed search result', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ id: 'test-id',
+ model: 'sonar-pro',
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: 'assistant',
+ content: 'This is the answer to your query.',
+ },
+ finish_reason: 'stop',
+ },
+ ],
+ citations: ['https://example.com/source1', 'https://example.com/source2'],
+ }),
+ });
+
+ const result = await client.search('What is TypeScript?');
+
+ expect(result.answer).toBe('This is the answer to your query.');
+ expect(result.citations).toHaveLength(2);
+ expect(result.sources).toHaveLength(2);
+ expect(result.sources[0].url).toBe('https://example.com/source1');
+ });
+
+ it('handles response without citations', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ id: 'test-id',
+ model: 'sonar-pro',
+ choices: [
+ {
+ index: 0,
+ message: {
+ role: 'assistant',
+ content: 'Answer without citations.',
+ },
+ finish_reason: 'stop',
+ },
+ ],
+ }),
+ });
+
+ const result = await client.search('Simple query');
+
+ expect(result.answer).toBe('Answer without citations.');
+ expect(result.citations).toHaveLength(0);
+ expect(result.sources).toHaveLength(0);
+ });
+
+ it('throws error for invalid API key', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 401,
+ statusText: 'Unauthorized',
+ });
+
+ await expect(client.search('test')).rejects.toThrow('Invalid Perplexity API key');
+ });
+
+ it('retries on server errors', async () => {
+ mockFetch
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ statusText: 'Internal Server Error',
+ })
+ .mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ statusText: 'Internal Server Error',
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ id: 'test-id',
+ model: 'sonar-pro',
+ choices: [
+ {
+ index: 0,
+ message: { role: 'assistant', content: 'Success!' },
+ finish_reason: 'stop',
+ },
+ ],
+ }),
+ });
+
+ const result = await client.search('test');
+ expect(result.answer).toBe('Success!');
+ expect(mockFetch).toHaveBeenCalledTimes(3);
+ });
+
+ it('throws error when no choices in response', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ id: 'test-id',
+ model: 'sonar-pro',
+ choices: [],
+ }),
+ });
+
+ await expect(client.search('test')).rejects.toThrow('No response from Perplexity');
+ });
+
+ it('sends correct request format', async () => {
+ mockFetch.mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ id: 'test-id',
+ model: 'sonar-pro',
+ choices: [
+ {
+ index: 0,
+ message: { role: 'assistant', content: 'Response' },
+ finish_reason: 'stop',
+ },
+ ],
+ }),
+ });
+
+ await client.search('test query');
+
+ expect(mockFetch).toHaveBeenCalledWith(
+ 'https://api.perplexity.ai/chat/completions',
+ expect.objectContaining({
+ method: 'POST',
+ headers: {
+ 'Authorization': 'Bearer test-key',
+ 'Content-Type': 'application/json',
+ },
+ })
+ );
+
+ const body = JSON.parse(mockFetch.mock.calls[0][1].body);
+ expect(body.model).toBe('sonar-pro');
+ expect(body.messages).toHaveLength(2);
+ expect(body.messages[1].content).toBe('test query');
+ });
+ });
+
+ describe('researchQueries', () => {
+ it('returns research with multiple query results', async () => {
+ mockFetch
+ .mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ id: '1',
+ model: 'sonar-pro',
+ choices: [{ index: 0, message: { role: 'assistant', content: 'Answer 1' }, finish_reason: 'stop' }],
+ citations: ['https://source1.com'],
+ }),
+ })
+ .mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ id: '2',
+ model: 'sonar-pro',
+ choices: [{ index: 0, message: { role: 'assistant', content: 'Answer 2' }, finish_reason: 'stop' }],
+ citations: ['https://source2.com'],
+ }),
+ });
+
+ const queries = ['query1', 'query2'];
+ const result = await client.researchQueries(queries);
+
+ expect(result.queries).toEqual(queries);
+ expect(result.results).toHaveLength(2);
+ expect(result.results[0].answer).toBe('Answer 1');
+ expect(result.results[1].answer).toBe('Answer 2');
+ expect(result.summary).toContain('Answer 1');
+ expect(result.summary).toContain('Answer 2');
+ });
+
+ it('continues when individual queries fail after retries', async () => {
+ // First query fails all 3 retries, second succeeds
+ mockFetch
+ .mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Error' })
+ .mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Error' })
+ .mockResolvedValueOnce({ ok: false, status: 500, statusText: 'Error' })
+ .mockResolvedValueOnce({
+ ok: true,
+ json: async () => ({
+ id: '2',
+ model: 'sonar-pro',
+ choices: [{ index: 0, message: { role: 'assistant', content: 'Success' }, finish_reason: 'stop' }],
+ }),
+ });
+
+ const result = await client.researchQueries(['fail-query', 'success-query']);
+
+ expect(result.results).toHaveLength(1);
+ expect(result.results[0].answer).toBe('Success');
+ }, 10000);
+
+ it('returns empty results when all queries fail', async () => {
+ // All retries fail for both queries (3 retries each = 6 calls)
+ for (let i = 0; i < 6; i++) {
+ mockFetch.mockResolvedValueOnce({
+ ok: false,
+ status: 500,
+ statusText: 'Error',
+ });
+ }
+
+ const result = await client.researchQueries(['fail1', 'fail2']);
+
+ expect(result.results).toHaveLength(0);
+ expect(result.summary).toBe('No research results available.');
+ }, 15000);
+ });
+});
diff --git a/src/__tests__/templates.test.ts b/src/__tests__/templates.test.ts
new file mode 100644
index 0000000..f6db6c0
--- /dev/null
+++ b/src/__tests__/templates.test.ts
@@ -0,0 +1,235 @@
+import { describe, it, expect } from 'vitest';
+import {
+ buildArchitectureClassificationPrompt,
+ buildResearchQueryPrompt,
+ generateDefaultResearchQueries,
+} from '../prompts/templates.js';
+import type { Architecture } from '../types/index.js';
+
+describe('Prompt Templates', () => {
+ describe('buildArchitectureClassificationPrompt', () => {
+ it('includes the user idea in the prompt', () => {
+ const idea = 'A CLI tool that converts markdown to PDF';
+ const prompt = buildArchitectureClassificationPrompt(idea);
+
+ expect(prompt).toContain('');
+ expect(prompt).toContain(idea);
+ expect(prompt).toContain('');
+ });
+
+ it('includes all app type options', () => {
+ const prompt = buildArchitectureClassificationPrompt('test');
+
+ expect(prompt).toContain('web:');
+ expect(prompt).toContain('desktop:');
+ expect(prompt).toContain('cli:');
+ expect(prompt).toContain('library:');
+ expect(prompt).toContain('mobile:');
+ expect(prompt).toContain('script:');
+ });
+
+ it('includes all interface type options', () => {
+ const prompt = buildArchitectureClassificationPrompt('test');
+
+ expect(prompt).toContain('rest_api:');
+ expect(prompt).toContain('graphql:');
+ expect(prompt).toContain('ipc:');
+ expect(prompt).toContain('cli_args:');
+ expect(prompt).toContain('websocket:');
+ });
+
+ it('includes all persistence options', () => {
+ const prompt = buildArchitectureClassificationPrompt('test');
+
+ expect(prompt).toContain('remote_db:');
+ expect(prompt).toContain('local_db:');
+ expect(prompt).toContain('file_based:');
+ expect(prompt).toContain('in_memory:');
+ });
+
+ it('includes all deployment options', () => {
+ const prompt = buildArchitectureClassificationPrompt('test');
+
+ expect(prompt).toContain('cloud:');
+ expect(prompt).toContain('self_hosted:');
+ expect(prompt).toContain('package_registry:');
+ expect(prompt).toContain('app_store:');
+ });
+
+ it('requests JSON output format', () => {
+ const prompt = buildArchitectureClassificationPrompt('test');
+
+ expect(prompt).toContain('Output ONLY valid JSON');
+ expect(prompt).toContain('"appType"');
+ expect(prompt).toContain('"interfaces"');
+ expect(prompt).toContain('"persistence"');
+ expect(prompt).toContain('"deployment"');
+ });
+ });
+
+ describe('buildResearchQueryPrompt', () => {
+ const mockArchitecture: Architecture = {
+ appType: 'cli',
+ appTypeReason: 'Command-line tool',
+ interfaces: ['cli_args'],
+ interfacesReason: 'CLI arguments',
+ persistence: 'file_based',
+ persistenceReason: 'File storage',
+ deployment: 'package_registry',
+ deploymentReason: 'npm',
+ suggestedTechStack: {
+ language: 'TypeScript',
+ framework: 'Commander.js',
+ reasoning: 'CLI framework',
+ },
+ };
+
+ it('includes the architecture JSON', () => {
+ const prompt = buildResearchQueryPrompt(mockArchitecture);
+
+ expect(prompt).toContain('');
+ expect(prompt).toContain('"appType": "cli"');
+ expect(prompt).toContain('');
+ });
+
+ it('requests 4-6 search queries', () => {
+ const prompt = buildResearchQueryPrompt(mockArchitecture);
+
+ expect(prompt).toContain('4-6 search queries');
+ });
+
+ it('mentions key research areas', () => {
+ const prompt = buildResearchQueryPrompt(mockArchitecture);
+
+ expect(prompt).toContain('best practices');
+ expect(prompt).toContain('Common mistakes');
+ expect(prompt).toContain('Recommended libraries');
+ expect(prompt).toContain('Testing approaches');
+ });
+
+ it('requests JSON array output', () => {
+ const prompt = buildResearchQueryPrompt(mockArchitecture);
+
+ expect(prompt).toContain('JSON array of query strings');
+ });
+ });
+
+ describe('generateDefaultResearchQueries', () => {
+ it('generates queries for CLI apps', () => {
+ const architecture: Architecture = {
+ appType: 'cli',
+ appTypeReason: 'CLI tool',
+ interfaces: ['cli_args'],
+ interfacesReason: 'CLI',
+ persistence: 'file_based',
+ persistenceReason: 'Files',
+ deployment: 'package_registry',
+ deploymentReason: 'npm',
+ suggestedTechStack: {
+ language: 'TypeScript',
+ framework: 'Commander.js',
+ reasoning: 'CLI framework',
+ },
+ };
+
+ const queries = generateDefaultResearchQueries(architecture);
+
+ expect(queries.length).toBeGreaterThanOrEqual(4);
+ expect(queries.some(q => q.toLowerCase().includes('cli'))).toBe(true);
+ expect(queries.some(q => q.toLowerCase().includes('typescript'))).toBe(true);
+ });
+
+ it('generates queries for web apps', () => {
+ const architecture: Architecture = {
+ appType: 'web',
+ appTypeReason: 'Web app',
+ interfaces: ['rest_api'],
+ interfacesReason: 'REST',
+ persistence: 'remote_db',
+ persistenceReason: 'Database',
+ deployment: 'cloud',
+ deploymentReason: 'Cloud',
+ suggestedTechStack: {
+ language: 'TypeScript',
+ framework: 'Express',
+ reasoning: 'Backend',
+ },
+ };
+
+ const queries = generateDefaultResearchQueries(architecture);
+
+ expect(queries.length).toBeGreaterThanOrEqual(4);
+ expect(queries.some(q => q.toLowerCase().includes('web'))).toBe(true);
+ expect(queries.some(q => q.toLowerCase().includes('express'))).toBe(true);
+ });
+
+ it('generates queries for desktop apps', () => {
+ const architecture: Architecture = {
+ appType: 'desktop',
+ appTypeReason: 'Desktop app',
+ interfaces: ['ipc'],
+ interfacesReason: 'IPC',
+ persistence: 'local_db',
+ persistenceReason: 'SQLite',
+ deployment: 'desktop_installer',
+ deploymentReason: 'Installer',
+ suggestedTechStack: {
+ language: 'TypeScript',
+ framework: 'Tauri',
+ reasoning: 'Desktop framework',
+ },
+ };
+
+ const queries = generateDefaultResearchQueries(architecture);
+
+ expect(queries.length).toBeGreaterThanOrEqual(4);
+ expect(queries.some(q => q.toLowerCase().includes('tauri'))).toBe(true);
+ });
+
+ it('generates queries for library packages', () => {
+ const architecture: Architecture = {
+ appType: 'library',
+ appTypeReason: 'Library',
+ interfaces: ['module'],
+ interfacesReason: 'Module',
+ persistence: 'in_memory',
+ persistenceReason: 'None',
+ deployment: 'package_registry',
+ deploymentReason: 'npm',
+ suggestedTechStack: {
+ language: 'TypeScript',
+ framework: '',
+ reasoning: 'Library',
+ },
+ };
+
+ const queries = generateDefaultResearchQueries(architecture);
+
+ expect(queries.length).toBeGreaterThanOrEqual(4);
+ expect(queries.some(q => q.toLowerCase().includes('library'))).toBe(true);
+ });
+
+ it('includes current year in queries', () => {
+ const architecture: Architecture = {
+ appType: 'web',
+ appTypeReason: 'Web',
+ interfaces: ['rest_api'],
+ interfacesReason: 'REST',
+ persistence: 'remote_db',
+ persistenceReason: 'DB',
+ deployment: 'cloud',
+ deploymentReason: 'Cloud',
+ suggestedTechStack: {
+ language: 'TypeScript',
+ framework: 'Express',
+ reasoning: 'Backend',
+ },
+ };
+
+ const queries = generateDefaultResearchQueries(architecture);
+ const currentYear = new Date().getFullYear().toString();
+
+ expect(queries.some(q => q.includes(currentYear))).toBe(true);
+ });
+ });
+});
diff --git a/src/clients/claude.ts b/src/clients/claude.ts
new file mode 100644
index 0000000..5b036fa
--- /dev/null
+++ b/src/clients/claude.ts
@@ -0,0 +1,274 @@
+import Anthropic from '@anthropic-ai/sdk';
+import { logger } from '../utils/logger.js';
+import type { Architecture, Specification, PRDOutput, ValidationResult, Research } from '../types/index.js';
+
+const MAX_RETRIES = 3;
+const INITIAL_BACKOFF_MS = 1000;
+
+export interface ClaudeClientOptions {
+ apiKey: string;
+ model?: string;
+}
+
+export class ClaudeClient {
+ private client: Anthropic;
+ private model: string;
+
+ constructor(options: ClaudeClientOptions) {
+ this.client = new Anthropic({ apiKey: options.apiKey });
+ this.model = options.model || 'claude-sonnet-4-20250514';
+ }
+
+ private async retryWithBackoff(
+ operation: () => Promise,
+ operationName: string
+ ): Promise {
+ let lastError: Error | null = null;
+
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
+ try {
+ return await operation();
+ } catch (err) {
+ const error = err as Error & { status?: number };
+ lastError = error;
+
+ // Don't retry on auth errors
+ if (error.status === 401) {
+ throw new Error('Invalid Claude API key. Run `ralph-vibe init` to configure.');
+ }
+
+ // Handle rate limits
+ if (error.status === 429) {
+ const waitTime = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
+ logger.warn(`Rate limited. Waiting ${waitTime}ms before retry ${attempt}/${MAX_RETRIES}...`);
+ await this.sleep(waitTime);
+ continue;
+ }
+
+ // Retry on server errors
+ if (error.status && error.status >= 500) {
+ const waitTime = INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1);
+ logger.debug(`${operationName} failed (attempt ${attempt}/${MAX_RETRIES}): ${error.message}`);
+ if (attempt < MAX_RETRIES) {
+ await this.sleep(waitTime);
+ }
+ continue;
+ }
+
+ // Don't retry on other errors
+ throw error;
+ }
+ }
+
+ throw lastError || new Error(`${operationName} failed after ${MAX_RETRIES} attempts`);
+ }
+
+ private sleep(ms: number): Promise {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ async analyze(prompt: string): Promise {
+ return this.retryWithBackoff(async () => {
+ const response = await this.client.messages.create({
+ model: this.model,
+ max_tokens: 4096,
+ messages: [{ role: 'user', content: prompt }],
+ });
+
+ const textBlock = response.content.find(block => block.type === 'text');
+ if (!textBlock || textBlock.type !== 'text') {
+ throw new Error('No text response from Claude');
+ }
+
+ return textBlock.text;
+ }, 'analyze');
+ }
+
+ async classifyArchitecture(idea: string): Promise {
+ const prompt = `Analyze this app idea and classify its architecture:
+
+
+${idea}
+
+
+Determine:
+
+1. APPLICATION TYPE (choose one):
+ - web: Frontend + backend web application
+ - desktop: Native or Electron/Tauri desktop application
+ - cli: Command-line tool
+ - library: Reusable package/module
+ - mobile: iOS/Android application
+ - script: Automation/utility script
+ - other: Specify
+
+2. INTERFACE TYPES (choose all that apply):
+ - rest_api: RESTful HTTP endpoints
+ - graphql: GraphQL API
+ - ipc: Inter-process communication (desktop)
+ - module: Internal module boundaries (library)
+ - cli_args: Command-line arguments/flags
+ - file_format: Custom file format handling
+ - websocket: Real-time communication
+ - grpc: gRPC services
+
+3. PERSISTENCE (choose one):
+ - remote_db: Cloud/remote database (PostgreSQL, MySQL, MongoDB)
+ - local_db: Local database (SQLite, LevelDB)
+ - file_based: File system storage
+ - in_memory: No persistence
+ - cloud_storage: Object storage (S3, etc.)
+
+4. DEPLOYMENT (choose one):
+ - cloud: Cloud platform (Vercel, AWS, GCP, etc.)
+ - self_hosted: Self-managed server
+ - desktop_installer: Native installers
+ - package_registry: npm, PyPI, crates.io, etc.
+ - app_store: Mobile app stores
+ - none: No deployment (local script)
+
+Output ONLY valid JSON (no markdown, no explanation):
+{
+ "appType": "...",
+ "appTypeReason": "...",
+ "interfaces": ["..."],
+ "interfacesReason": "...",
+ "persistence": "...",
+ "persistenceReason": "...",
+ "deployment": "...",
+ "deploymentReason": "...",
+ "suggestedTechStack": {
+ "language": "...",
+ "framework": "...",
+ "reasoning": "..."
+ }
+}`;
+
+ const response = await this.analyze(prompt);
+ return this.parseArchitectureResponse(response);
+ }
+
+ private parseArchitectureResponse(response: string): Architecture {
+ // Extract JSON from response (handle markdown code blocks)
+ let jsonStr = response.trim();
+
+ // Remove markdown code blocks if present
+ const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
+ if (jsonMatch) {
+ jsonStr = jsonMatch[1].trim();
+ }
+
+ try {
+ const parsed = JSON.parse(jsonStr);
+
+ // Validate required fields
+ if (!parsed.appType || !parsed.interfaces || !parsed.persistence || !parsed.deployment) {
+ throw new Error('Missing required fields in architecture response');
+ }
+
+ return parsed as Architecture;
+ } catch (err) {
+ const error = err as Error;
+ logger.debug(`Failed to parse architecture response: ${response}`);
+ throw new Error(`Failed to parse architecture classification: ${error.message}`);
+ }
+ }
+
+ async generateResearchQueries(architecture: Architecture): Promise {
+ const prompt = `Based on this architecture classification, generate Perplexity search queries
+to research current best practices:
+
+
+${JSON.stringify(architecture, null, 2)}
+
+
+Generate 4-6 search queries that will find:
+1. Project structure and architecture best practices for this stack
+2. Common mistakes and pitfalls to avoid
+3. Recommended libraries and tools (current year 2026)
+4. Testing approaches and frameworks
+5. (If applicable) Security considerations
+6. (If applicable) Performance optimization
+
+Output ONLY a valid JSON array of query strings (no markdown, no explanation):
+["query1", "query2", ...]`;
+
+ const response = await this.analyze(prompt);
+ return this.parseQueriesResponse(response);
+ }
+
+ private parseQueriesResponse(response: string): string[] {
+ let jsonStr = response.trim();
+
+ // Remove markdown code blocks if present
+ const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
+ if (jsonMatch) {
+ jsonStr = jsonMatch[1].trim();
+ }
+
+ try {
+ const parsed = JSON.parse(jsonStr);
+
+ if (!Array.isArray(parsed)) {
+ throw new Error('Response is not an array');
+ }
+
+ if (parsed.length < 4 || parsed.length > 6) {
+ logger.warn(`Expected 4-6 queries, got ${parsed.length}`);
+ }
+
+ return parsed.map(q => String(q));
+ } catch (err) {
+ const error = err as Error;
+ logger.debug(`Failed to parse queries response: ${response}`);
+ throw new Error(`Failed to parse research queries: ${error.message}`);
+ }
+ }
+
+ async generateSpec(
+ idea: string,
+ architecture: Architecture,
+ research: Research
+ ): Promise {
+ // This will be fully implemented in Phase 3
+ const prompt = `Generate a specification for this application:
+
+
+${idea}
+
+
+
+${JSON.stringify(architecture, null, 2)}
+
+
+
+${research.summary}
+
+
+Output a specification with features, data models, interfaces, and tech stack as JSON.`;
+
+ const response = await this.analyze(prompt);
+
+ // Basic parsing - full implementation in Phase 3
+ try {
+ let jsonStr = response.trim();
+ const jsonMatch = jsonStr.match(/```(?:json)?\s*([\s\S]*?)```/);
+ if (jsonMatch) {
+ jsonStr = jsonMatch[1].trim();
+ }
+ return JSON.parse(jsonStr) as Specification;
+ } catch {
+ throw new Error('Failed to parse specification response');
+ }
+ }
+
+ async generatePRD(_spec: Specification): Promise {
+ // Placeholder - full implementation in Phase 3
+ throw new Error('PRD generation not yet implemented');
+ }
+
+ async validate(_prompt: string): Promise {
+ // Placeholder - can use Claude for advanced validation in Phase 4
+ throw new Error('Claude-based validation not yet implemented');
+ }
+}
diff --git a/src/clients/perplexity.ts b/src/clients/perplexity.ts
new file mode 100644
index 0000000..f3f2015
--- /dev/null
+++ b/src/clients/perplexity.ts
@@ -0,0 +1,232 @@
+import { logger } from '../utils/logger.js';
+import type { SearchResult, Research, TechStack } from '../types/index.js';
+
+const MAX_RETRIES = 3;
+const INITIAL_BACKOFF_MS = 1000;
+const API_BASE_URL = 'https://api.perplexity.ai';
+
+export interface PerplexityClientOptions {
+ apiKey: string;
+ model?: string;
+}
+
+interface PerplexityMessage {
+ role: 'system' | 'user' | 'assistant';
+ content: string;
+}
+
+interface PerplexityResponse {
+ id: string;
+ model: string;
+ choices: {
+ index: number;
+ message: {
+ role: string;
+ content: string;
+ };
+ finish_reason: string;
+ }[];
+ citations?: string[];
+}
+
+export class PerplexityClient {
+ private apiKey: string;
+ private model: string;
+
+ constructor(options: PerplexityClientOptions) {
+ this.apiKey = options.apiKey;
+ this.model = options.model || 'sonar-pro';
+ }
+
+ private async retryWithBackoff(
+ operation: () => Promise,
+ operationName: string
+ ): Promise {
+ let lastError: Error | null = null;
+
+ for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
+ try {
+ return await operation();
+ } catch (err) {
+ const error = err as Error & { status?: number };
+ lastError = error;
+
+ // Don't retry on auth errors
+ if (error.status === 401) {
+ throw new Error('Invalid Perplexity API key. Run `ralph-vibe init` to configure.');
+ }
+
+ // Handle rate limits
+ if (error.status === 429) {
+ const waitTime = INITIAL_BACKOFF_MS * Math.pow(2, attempt);
+ logger.warn(`Rate limited. Waiting ${waitTime}ms before retry ${attempt}/${MAX_RETRIES}...`);
+ await this.sleep(waitTime);
+ continue;
+ }
+
+ // Retry on server errors
+ if (error.status && error.status >= 500) {
+ const waitTime = INITIAL_BACKOFF_MS * Math.pow(2, attempt - 1);
+ logger.debug(`${operationName} failed (attempt ${attempt}/${MAX_RETRIES}): ${error.message}`);
+ if (attempt < MAX_RETRIES) {
+ await this.sleep(waitTime);
+ }
+ continue;
+ }
+
+ // Don't retry on other errors
+ throw error;
+ }
+ }
+
+ throw lastError || new Error(`${operationName} failed after ${MAX_RETRIES} attempts`);
+ }
+
+ private sleep(ms: number): Promise {
+ return new Promise(resolve => setTimeout(resolve, ms));
+ }
+
+ async search(query: string): Promise {
+ return this.retryWithBackoff(async () => {
+ const messages: PerplexityMessage[] = [
+ {
+ role: 'system',
+ content: 'You are a helpful research assistant. Provide concise, factual answers with sources.',
+ },
+ {
+ role: 'user',
+ content: query,
+ },
+ ];
+
+ const response = await fetch(`${API_BASE_URL}/chat/completions`, {
+ method: 'POST',
+ headers: {
+ 'Authorization': `Bearer ${this.apiKey}`,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ model: this.model,
+ messages,
+ max_tokens: 2048,
+ }),
+ });
+
+ if (!response.ok) {
+ const error = new Error(`Perplexity API error: ${response.status} ${response.statusText}`) as Error & { status: number };
+ error.status = response.status;
+ throw error;
+ }
+
+ const data = await response.json() as PerplexityResponse;
+
+ if (!data.choices || data.choices.length === 0) {
+ throw new Error('No response from Perplexity');
+ }
+
+ const answer = data.choices[0].message.content;
+ const citations = data.citations || [];
+
+ // Parse sources from citations
+ const sources = citations.map((url, index) => ({
+ title: `Source ${index + 1}`,
+ url,
+ }));
+
+ return {
+ answer,
+ sources,
+ citations,
+ };
+ }, 'search');
+ }
+
+ async researchStack(stack: TechStack): Promise {
+ const queries = this.generateStackQueries(stack);
+ const results: SearchResult[] = [];
+
+ for (const query of queries) {
+ logger.debug(`Researching: ${query}`);
+ try {
+ const result = await this.search(query);
+ results.push(result);
+ } catch (err) {
+ const error = err as Error;
+ logger.warn(`Research query failed: ${error.message}`);
+ // Continue with other queries
+ }
+ }
+
+ // Generate summary from all results
+ const summary = this.summarizeResults(results);
+
+ return {
+ queries,
+ results,
+ summary,
+ };
+ }
+
+ async researchQueries(queries: string[]): Promise {
+ const results: SearchResult[] = [];
+
+ for (const query of queries) {
+ logger.debug(`Researching: ${query}`);
+ try {
+ const result = await this.search(query);
+ results.push(result);
+ } catch (err) {
+ const error = err as Error;
+ logger.warn(`Research query failed: ${error.message}`);
+ // Continue with other queries
+ }
+ }
+
+ // Generate summary from all results
+ const summary = this.summarizeResults(results);
+
+ return {
+ queries,
+ results,
+ summary,
+ };
+ }
+
+ private generateStackQueries(stack: TechStack): string[] {
+ const year = new Date().getFullYear();
+ const queries: string[] = [];
+
+ // Best practices query
+ if (stack.framework) {
+ queries.push(`${stack.language} ${stack.framework} project structure best practices ${year}`);
+ } else {
+ queries.push(`${stack.language} project structure best practices ${year}`);
+ }
+
+ // Common mistakes query
+ queries.push(`${stack.language} ${stack.framework || ''} common mistakes pitfalls to avoid`.trim());
+
+ // Recommended libraries query
+ queries.push(`${stack.language} recommended libraries ${year} ${stack.libraries.slice(0, 2).join(' ')}`.trim());
+
+ // Testing query
+ queries.push(`${stack.language} ${stack.testingFramework} testing best practices ${year}`);
+
+ return queries;
+ }
+
+ private summarizeResults(results: SearchResult[]): string {
+ if (results.length === 0) {
+ return 'No research results available.';
+ }
+
+ const summaryParts = results.map((result, index) => {
+ const sources = result.sources.length > 0
+ ? `\n Sources: ${result.sources.map(s => s.url).join(', ')}`
+ : '';
+ return `### Finding ${index + 1}\n${result.answer}${sources}`;
+ });
+
+ return summaryParts.join('\n\n');
+ }
+}
diff --git a/src/generators/architecture.ts b/src/generators/architecture.ts
new file mode 100644
index 0000000..ce57a29
--- /dev/null
+++ b/src/generators/architecture.ts
@@ -0,0 +1,93 @@
+import { ClaudeClient } from '../clients/claude.js';
+import { PerplexityClient } from '../clients/perplexity.js';
+import { generateDefaultResearchQueries } from '../prompts/templates.js';
+import { logger } from '../utils/logger.js';
+import type { Architecture, Research, Config } from '../types/index.js';
+
+export interface ArchitectureGeneratorOptions {
+ config: Config;
+ skipResearch?: boolean;
+}
+
+export class ArchitectureGenerator {
+ private claudeClient: ClaudeClient;
+ private perplexityClient: PerplexityClient | null;
+
+ constructor(options: ArchitectureGeneratorOptions) {
+ if (!options.config.claudeApiKey) {
+ throw new Error('Claude API key is required');
+ }
+
+ this.claudeClient = new ClaudeClient({
+ apiKey: options.config.claudeApiKey,
+ });
+
+ if (options.config.perplexityApiKey && !options.skipResearch) {
+ this.perplexityClient = new PerplexityClient({
+ apiKey: options.config.perplexityApiKey,
+ });
+ } else {
+ this.perplexityClient = null;
+ }
+ }
+
+ async classifyIdea(idea: string): Promise {
+ logger.info('Analyzing app idea...');
+ const architecture = await this.claudeClient.classifyArchitecture(idea);
+ logger.success('Architecture classification complete');
+ return architecture;
+ }
+
+ async generateResearchQueries(architecture: Architecture): Promise {
+ logger.info('Generating research queries...');
+
+ try {
+ // Try to use Claude to generate smart queries
+ const queries = await this.claudeClient.generateResearchQueries(architecture);
+ logger.success(`Generated ${queries.length} research queries`);
+ return queries;
+ } catch (err) {
+ const error = err as Error;
+ logger.warn(`Claude query generation failed: ${error.message}`);
+ logger.info('Using default research queries...');
+
+ // Fall back to default queries
+ const queries = generateDefaultResearchQueries(architecture);
+ logger.success(`Generated ${queries.length} default research queries`);
+ return queries;
+ }
+ }
+
+ async conductResearch(queries: string[]): Promise {
+ if (!this.perplexityClient) {
+ logger.warn('Perplexity client not available, skipping research');
+ return {
+ queries,
+ results: [],
+ summary: 'Research was skipped.',
+ };
+ }
+
+ logger.info(`Conducting research with ${queries.length} queries...`);
+ const research = await this.perplexityClient.researchQueries(queries);
+ logger.success(`Research complete with ${research.results.length} findings`);
+ return research;
+ }
+
+ async analyzeAndResearch(idea: string): Promise<{
+ architecture: Architecture;
+ queries: string[];
+ research: Research;
+ }> {
+ // Step 1: Classify the idea
+ const architecture = await this.classifyIdea(idea);
+
+ // Step 2: Generate research queries
+ const queries = await this.generateResearchQueries(architecture);
+
+ // Step 3: Conduct research
+ const research = await this.conductResearch(queries);
+
+ return { architecture, queries, research };
+ }
+}
diff --git a/src/prompts/templates.ts b/src/prompts/templates.ts
new file mode 100644
index 0000000..69e1532
--- /dev/null
+++ b/src/prompts/templates.ts
@@ -0,0 +1,151 @@
+import type { Architecture } from '../types/index.js';
+
+export const ARCHITECTURE_CLASSIFICATION_PROMPT = `Analyze this app idea and classify its architecture:
+
+
+{user_idea}
+
+
+Determine:
+
+1. APPLICATION TYPE (choose one):
+ - web: Frontend + backend web application
+ - desktop: Native or Electron/Tauri desktop application
+ - cli: Command-line tool
+ - library: Reusable package/module
+ - mobile: iOS/Android application
+ - script: Automation/utility script
+ - other: Specify
+
+2. INTERFACE TYPES (choose all that apply):
+ - rest_api: RESTful HTTP endpoints
+ - graphql: GraphQL API
+ - ipc: Inter-process communication (desktop)
+ - module: Internal module boundaries (library)
+ - cli_args: Command-line arguments/flags
+ - file_format: Custom file format handling
+ - websocket: Real-time communication
+ - grpc: gRPC services
+
+3. PERSISTENCE (choose one):
+ - remote_db: Cloud/remote database (PostgreSQL, MySQL, MongoDB)
+ - local_db: Local database (SQLite, LevelDB)
+ - file_based: File system storage
+ - in_memory: No persistence
+ - cloud_storage: Object storage (S3, etc.)
+
+4. DEPLOYMENT (choose one):
+ - cloud: Cloud platform (Vercel, AWS, GCP, etc.)
+ - self_hosted: Self-managed server
+ - desktop_installer: Native installers
+ - package_registry: npm, PyPI, crates.io, etc.
+ - app_store: Mobile app stores
+ - none: No deployment (local script)
+
+Output ONLY valid JSON (no markdown, no explanation):
+{
+ "appType": "...",
+ "appTypeReason": "...",
+ "interfaces": ["..."],
+ "interfacesReason": "...",
+ "persistence": "...",
+ "persistenceReason": "...",
+ "deployment": "...",
+ "deploymentReason": "...",
+ "suggestedTechStack": {
+ "language": "...",
+ "framework": "...",
+ "reasoning": "..."
+ }
+}`;
+
+export function buildArchitectureClassificationPrompt(idea: string): string {
+ return ARCHITECTURE_CLASSIFICATION_PROMPT.replace('{user_idea}', idea);
+}
+
+export const RESEARCH_QUERY_GENERATION_PROMPT = `Based on this architecture classification, generate Perplexity search queries
+to research current best practices:
+
+
+{architecture_json}
+
+
+Generate 4-6 search queries that will find:
+1. Project structure and architecture best practices for this stack
+2. Common mistakes and pitfalls to avoid
+3. Recommended libraries and tools (current year)
+4. Testing approaches and frameworks
+5. (If applicable) Security considerations
+6. (If applicable) Performance optimization
+
+Output ONLY a valid JSON array of query strings (no markdown, no explanation):
+["query1", "query2", ...]`;
+
+export function buildResearchQueryPrompt(architecture: Architecture): string {
+ return RESEARCH_QUERY_GENERATION_PROMPT.replace(
+ '{architecture_json}',
+ JSON.stringify(architecture, null, 2)
+ );
+}
+
+export function generateDefaultResearchQueries(architecture: Architecture): string[] {
+ const { appType, suggestedTechStack } = architecture;
+ const year = new Date().getFullYear();
+ const lang = suggestedTechStack.language;
+ const framework = suggestedTechStack.framework;
+
+ const queries: string[] = [];
+
+ // App type specific queries
+ switch (appType) {
+ case 'web':
+ queries.push(`${lang} ${framework} web application best practices ${year}`);
+ queries.push(`${framework} common security mistakes to avoid`);
+ queries.push(`${lang} web backend recommended libraries ${year} authentication database`);
+ queries.push(`${lang} ${framework} testing Jest Vitest comparison ${year}`);
+ break;
+
+ case 'desktop':
+ queries.push(`${framework} project structure best practices`);
+ queries.push(`${framework} common mistakes`);
+ queries.push(`${framework} recommended plugins`);
+ queries.push(`${framework} application testing approaches`);
+ break;
+
+ case 'cli':
+ queries.push(`${lang} CLI tool best practices argument parsing`);
+ queries.push(`CLI tool UX common mistakes`);
+ queries.push(`${lang} CLI framework comparison ${year}`);
+ queries.push(`CLI tool testing strategies`);
+ break;
+
+ case 'library':
+ queries.push(`${lang} library package best practices ${year}`);
+ queries.push(`${lang} npm package common mistakes`);
+ queries.push(`${lang} library testing and documentation ${year}`);
+ queries.push(`${lang} package bundling and tree shaking`);
+ break;
+
+ case 'mobile':
+ queries.push(`${framework} mobile app best practices ${year}`);
+ queries.push(`${framework} common performance mistakes`);
+ queries.push(`${framework} recommended libraries state management ${year}`);
+ queries.push(`${framework} mobile testing approaches`);
+ break;
+
+ case 'script':
+ queries.push(`${lang} automation script best practices`);
+ queries.push(`${lang} scripting common mistakes error handling`);
+ queries.push(`${lang} scripting libraries utilities ${year}`);
+ queries.push(`${lang} script testing`);
+ break;
+
+ default:
+ queries.push(`${lang} ${framework} best practices ${year}`);
+ queries.push(`${lang} common mistakes to avoid`);
+ queries.push(`${lang} recommended libraries ${year}`);
+ queries.push(`${lang} testing frameworks ${year}`);
+ }
+
+ return queries;
+}