Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"phoenix": "node dist/cli.js"
},
"devDependencies": {
"@types/node": "^25.6.0",
"typescript": "^5.4.0",
"vitest": "^2.0.0"
}
Expand Down
13 changes: 10 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import { runShadowPipeline } from './shadow-pipeline.js';
import { parseCommand, routeCommand, getAllCommands } from './bot-router.js';

// Scaffold
import { deriveServices, generateScaffold } from './scaffold.js';
import { deriveServices, deriveInterfaces, generateScaffold } from './scaffold.js';

// Inspect
import { collectInspectData, renderInspectHTML, serveInspect } from './inspect.js';
Expand Down Expand Up @@ -413,12 +413,15 @@ async function cmdBootstrap(): Promise<void> {
} catch { /* best effort */ }
}

const interfaces = deriveInterfaces(ius, canonNodes);

const regenCtx: RegenContext = {
llm: llm ?? undefined,
canonNodes,
allIUs: ius,
projectRoot,
target: arch,
interfaces,
onProgress: (iu, status, msg) => {
if (status === 'start') process.stdout.write(` ⏳ ${iu.name}…`);
else if (status === 'done') process.stdout.write(` ${green('✔')}\n`);
Expand All @@ -445,7 +448,7 @@ async function cmdBootstrap(): Promise<void> {
console.log(` ${dim('Scaffold:')} Service wiring + project config`);
const services = deriveServices(ius);
const projectName = basename(projectRoot);
const scaffold = generateScaffold(services, projectName, arch);
const scaffold = generateScaffold(services, projectName, arch, interfaces);
for (const [filePath, content] of scaffold.files) {
const fullPath = join(projectRoot, filePath);
mkdirSync(join(fullPath, '..'), { recursive: true });
Expand Down Expand Up @@ -1066,12 +1069,15 @@ async function cmdRegen(args: string[]): Promise<void> {
} catch { /* ignore */ }
}

const regenInterfaces = deriveInterfaces(ius, canonNodes);

const regenCtx: RegenContext = {
llm: llm ?? undefined,
canonNodes,
allIUs: ius,
projectRoot,
target: regenArch,
interfaces: regenInterfaces,
onProgress: (iu, status, msg) => {
if (status === 'start') process.stdout.write(` ⏳ ${iu.name}…`);
else if (status === 'done') process.stdout.write(` ${green('✔')}\n`);
Expand Down Expand Up @@ -1101,8 +1107,9 @@ async function cmdRegen(args: string[]): Promise<void> {

// Re-generate scaffold wiring
const allIUs = loadIUs(phoenixDir);
const allInterfaces = deriveInterfaces(allIUs, canonNodes);
const services = deriveServices(allIUs);
const scaffold = generateScaffold(services, basename(projectRoot));
const scaffold = generateScaffold(services, basename(projectRoot), regenArch, allInterfaces);
for (const [filePath, content] of scaffold.files) {
const fullPath = join(projectRoot, filePath);
mkdirSync(join(fullPath, '..'), { recursive: true });
Expand Down
62 changes: 62 additions & 0 deletions src/llm/claude-cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Claude CLI LLM Provider.
*
* Uses the `claude` CLI in print mode (-p) for code generation.
* This allows Phoenix to use Claude Code's existing authentication
* instead of requiring a separate API key.
*/

import { execFileSync } from 'node:child_process';
import type { LLMProvider, GenerateOptions } from './provider.js';

export class ClaudeCliProvider implements LLMProvider {
readonly name = 'claude-cli';
readonly model: string;

constructor(model: string = 'sonnet') {
this.model = model;
}

async generate(prompt: string, options?: GenerateOptions): Promise<string> {
const args = [
'-p',
'--model', this.model,
'--tools', '',
'--no-session-persistence',
];

if (options?.system) {
args.push('--system-prompt', options.system);
}

// Pass prompt via stdin to avoid argument length limits
const result = execFileSync('claude', args, {
encoding: 'utf8',
input: prompt,
maxBuffer: 10 * 1024 * 1024, // 10MB
timeout: 600_000, // 10 minutes for large generations
});

if (!result || result.trim().length === 0) {
throw new Error('Claude CLI returned empty response');
}

return result;
}
}

/**
* Check if the `claude` CLI is available on PATH.
*/
export function isClaudeCliAvailable(): boolean {
try {
execFileSync('claude', ['--version'], {
encoding: 'utf8',
timeout: 5000,
stdio: ['pipe', 'pipe', 'pipe'],
});
return true;
} catch {
return false;
}
}
22 changes: 12 additions & 10 deletions src/llm/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import type { ImplementationUnit } from '../models/iu.js';
import type { CanonicalNode } from '../models/canonical.js';
import type { ResolvedTarget } from '../models/architecture.js';
import type { InterfaceEntry } from '../scaffold.js';

export const SYSTEM_PROMPT = `You are a senior TypeScript engineer generating production-quality module implementations for Phoenix VCS.

Expand Down Expand Up @@ -61,7 +62,7 @@ ${rt.promptExtension}`;
export function buildPrompt(
iu: ImplementationUnit,
canonNodes: CanonicalNode[],
siblingModules?: string[],
siblingModules?: InterfaceEntry[],
target?: ResolvedTarget | null,
): string {
const lines: string[] = [];
Expand Down Expand Up @@ -145,21 +146,22 @@ export function buildPrompt(
lines.push(`## Risk Tier: ${iu.risk_tier}`);
lines.push('');

// Context: sibling modules with mount paths for architecture mode
// Context: sibling modules with mount paths from the interface registry
if (siblingModules && siblingModules.length > 0) {
if (target) {
lines.push(`## Other API modules (do NOT import them — call their HTTP endpoints from JavaScript):`);
for (const m of siblingModules) {
const lowerName = m.toLowerCase();
const isWebUI = /\b(web|ui|frontend|interface|page|dashboard)\b/.test(lowerName);
if (isWebUI) continue; // skip other web modules
const mountPath = '/' + lowerName.replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
lines.push(`- "${m}" mounted at ${mountPath} — use fetch('${mountPath}') or fetch('${mountPath}/...') to call it`);
for (const entry of siblingModules) {
if (entry.role === 'web-ui') continue; // skip other web modules
let line = `- "${entry.name}" mounted at ${entry.mount_path} — use fetch('${entry.mount_path}') or fetch('${entry.mount_path}/...') to call it`;
if (entry.resource_fields) {
line += `. Resource shape: ${entry.resource_fields}`;
}
lines.push(line);
}
} else {
lines.push(`## Other modules in this service (for context, do NOT import them):`);
for (const m of siblingModules) {
lines.push(`- ${m}`);
for (const entry of siblingModules) {
lines.push(`- ${entry.name}`);
}
}
lines.push('');
Expand Down
1 change: 1 addition & 0 deletions src/llm/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ export interface LLMConfig {
export const DEFAULT_MODELS: Record<string, string> = {
anthropic: 'claude-sonnet-4-20250514',
openai: 'gpt-4o',
'claude-cli': 'sonnet',
};
8 changes: 7 additions & 1 deletion src/llm/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { LLMProvider, LLMConfig } from './provider.js';
import { DEFAULT_MODELS } from './provider.js';
import { AnthropicProvider } from './anthropic.js';
import { OpenAIProvider } from './openai.js';
import { ClaudeCliProvider, isClaudeCliAvailable } from './claude-cli.js';

interface PhoenixConfig {
llm?: LLMConfig;
Expand Down Expand Up @@ -60,6 +61,7 @@ export function resolveProvider(phoenixDir?: string): LLMProvider | null {
function detectProvider(): string | null {
if (process.env.ANTHROPIC_API_KEY) return 'anthropic';
if (process.env.OPENAI_API_KEY) return 'openai';
if (isClaudeCliAvailable()) return 'claude-cli';
return null;
}

Expand All @@ -78,6 +80,9 @@ function buildProvider(name: string, model: string): LLMProvider | null {
if (!key) return null;
return new OpenAIProvider(key, model);
}
case 'claude-cli': {
return new ClaudeCliProvider(model || 'sonnet');
}
default:
return null;
}
Expand Down Expand Up @@ -115,12 +120,13 @@ export function describeAvailability(): { available: string[]; configured: strin
const available: string[] = [];
if (process.env.ANTHROPIC_API_KEY) available.push('anthropic');
if (process.env.OPENAI_API_KEY) available.push('openai');
if (isClaudeCliAvailable()) available.push('claude-cli');

const configured = process.env.PHOENIX_LLM_PROVIDER || null;

let hint: string;
if (available.length === 0) {
hint = 'No LLM API keys found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY to enable code generation. Falling back to stubs.';
hint = 'No LLM providers found. Set ANTHROPIC_API_KEY or OPENAI_API_KEY, or install Claude Code CLI to enable code generation. Falling back to stubs.';
} else if (available.length === 1) {
hint = `Using ${available[0]} (detected from env).`;
} else {
Expand Down
Loading