mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-04 09:13:08 +00:00
feat: implement project setup dialog and refactor sidebar integration
- Added a new ProjectSetupDialog component to facilitate project specification generation, enhancing user experience by guiding users through project setup. - Refactored the Sidebar component to integrate the new ProjectSetupDialog, replacing the previous inline dialog implementation for improved code organization and maintainability. - Updated the sidebar to handle project overview and feature generation options, streamlining the project setup process. - Removed the old dialog implementation from the Sidebar, reducing code duplication and improving clarity.
This commit is contained in:
@@ -1,172 +0,0 @@
|
|||||||
import {
|
|
||||||
query,
|
|
||||||
Options,
|
|
||||||
SDKAssistantMessage,
|
|
||||||
} from "@anthropic-ai/claude-agent-sdk";
|
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import path from "path";
|
|
||||||
|
|
||||||
const systemPrompt = `You are an AI assistant helping users build software. You are part of the Automaker application,
|
|
||||||
which is designed to help developers plan, design, and implement software projects autonomously.
|
|
||||||
|
|
||||||
Your role is to:
|
|
||||||
- Help users define their project requirements and specifications
|
|
||||||
- Ask clarifying questions to better understand their needs
|
|
||||||
- Suggest technical approaches and architectures
|
|
||||||
- Guide them through the development process
|
|
||||||
- Be conversational and helpful
|
|
||||||
- Write, edit, and modify code files as requested
|
|
||||||
- Execute commands and tests
|
|
||||||
- Search and analyze the codebase
|
|
||||||
|
|
||||||
When discussing projects, help users think through:
|
|
||||||
- Core functionality and features
|
|
||||||
- Technical stack choices
|
|
||||||
- Data models and architecture
|
|
||||||
- User experience considerations
|
|
||||||
- Testing strategies
|
|
||||||
|
|
||||||
You have full access to the codebase and can:
|
|
||||||
- Read files to understand existing code
|
|
||||||
- Write new files
|
|
||||||
- Edit existing files
|
|
||||||
- Run bash commands
|
|
||||||
- Search for code patterns
|
|
||||||
- Execute tests and builds`;
|
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { messages, workingDirectory } = await request.json();
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[API] CLAUDE_CODE_OAUTH_TOKEN present:",
|
|
||||||
!!process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: "CLAUDE_CODE_OAUTH_TOKEN not configured" },
|
|
||||||
{ status: 500 }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the last user message
|
|
||||||
const lastMessage = messages[messages.length - 1];
|
|
||||||
|
|
||||||
// Determine working directory - default to parent of app directory
|
|
||||||
const cwd = workingDirectory || path.resolve(process.cwd(), "..");
|
|
||||||
|
|
||||||
console.log("[API] Working directory:", cwd);
|
|
||||||
|
|
||||||
// Create query with options that enable code modification
|
|
||||||
const options: Options = {
|
|
||||||
// model: "claude-sonnet-4-20250514",
|
|
||||||
model: "claude-opus-4-5-20251101",
|
|
||||||
systemPrompt,
|
|
||||||
maxTurns: 20,
|
|
||||||
cwd,
|
|
||||||
// Enable all core tools for code modification
|
|
||||||
allowedTools: [
|
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
],
|
|
||||||
// Auto-accept file edits within the working directory
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
// Enable sandbox for safer bash execution
|
|
||||||
sandbox: {
|
|
||||||
enabled: true,
|
|
||||||
autoAllowBashIfSandboxed: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Convert message history to SDK format to preserve conversation context
|
|
||||||
// Include both user and assistant messages for full context
|
|
||||||
const sessionId = `api-session-${Date.now()}`;
|
|
||||||
const conversationMessages = messages.map(
|
|
||||||
(msg: { role: string; content: string }) => {
|
|
||||||
if (msg.role === "user") {
|
|
||||||
return {
|
|
||||||
type: "user" as const,
|
|
||||||
message: {
|
|
||||||
role: "user" as const,
|
|
||||||
content: msg.content,
|
|
||||||
},
|
|
||||||
parent_tool_use_id: null,
|
|
||||||
session_id: sessionId,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
// Assistant message
|
|
||||||
return {
|
|
||||||
type: "assistant" as const,
|
|
||||||
message: {
|
|
||||||
role: "assistant" as const,
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: "text" as const,
|
|
||||||
text: msg.content,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
session_id: sessionId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Execute query with full conversation context
|
|
||||||
const queryResult = query({
|
|
||||||
prompt:
|
|
||||||
conversationMessages.length > 0
|
|
||||||
? conversationMessages
|
|
||||||
: lastMessage.content,
|
|
||||||
options,
|
|
||||||
});
|
|
||||||
|
|
||||||
let responseText = "";
|
|
||||||
const toolUses: Array<{ name: string; input: unknown }> = [];
|
|
||||||
|
|
||||||
// Collect the response from the async generator
|
|
||||||
for await (const msg of queryResult) {
|
|
||||||
if (msg.type === "assistant") {
|
|
||||||
const assistantMsg = msg as SDKAssistantMessage;
|
|
||||||
if (assistantMsg.message.content) {
|
|
||||||
for (const block of assistantMsg.message.content) {
|
|
||||||
if (block.type === "text") {
|
|
||||||
responseText += block.text;
|
|
||||||
} else if (block.type === "tool_use") {
|
|
||||||
// Track tool usage for transparency
|
|
||||||
toolUses.push({
|
|
||||||
name: block.name,
|
|
||||||
input: block.input,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === "result") {
|
|
||||||
if (msg.subtype === "success") {
|
|
||||||
if (msg.result) {
|
|
||||||
responseText = msg.result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
|
||||||
content: responseText || "Sorry, I couldn't generate a response.",
|
|
||||||
toolUses: toolUses.length > 0 ? toolUses : undefined,
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error("Claude API error:", error);
|
|
||||||
const errorMessage =
|
|
||||||
error instanceof Error
|
|
||||||
? error.message
|
|
||||||
: "Failed to get response from Claude";
|
|
||||||
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
114
apps/app/src/components/layout/project-setup-dialog.tsx
Normal file
114
apps/app/src/components/layout/project-setup-dialog.tsx
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Sparkles } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
||||||
|
interface ProjectSetupDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
projectOverview: string;
|
||||||
|
onProjectOverviewChange: (value: string) => void;
|
||||||
|
generateFeatures: boolean;
|
||||||
|
onGenerateFeaturesChange: (value: boolean) => void;
|
||||||
|
onCreateSpec: () => void;
|
||||||
|
onSkip: () => void;
|
||||||
|
isCreatingSpec: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ProjectSetupDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
projectOverview,
|
||||||
|
onProjectOverviewChange,
|
||||||
|
generateFeatures,
|
||||||
|
onGenerateFeaturesChange,
|
||||||
|
onCreateSpec,
|
||||||
|
onSkip,
|
||||||
|
isCreatingSpec,
|
||||||
|
}: ProjectSetupDialogProps) {
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open && !isCreatingSpec) {
|
||||||
|
onSkip();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent className="max-w-2xl">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Set Up Your Project</DialogTitle>
|
||||||
|
<DialogDescription className="text-muted-foreground">
|
||||||
|
We didn't find an app_spec.txt file. Let us help you generate
|
||||||
|
your app_spec.txt to help describe your project for our system.
|
||||||
|
We'll analyze your project's tech stack and create a
|
||||||
|
comprehensive specification.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 py-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="text-sm font-medium">Project Overview</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Describe what your project does and what features you want to
|
||||||
|
build. Be as detailed as you want - this will help us create a
|
||||||
|
better specification.
|
||||||
|
</p>
|
||||||
|
<textarea
|
||||||
|
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
||||||
|
value={projectOverview}
|
||||||
|
onChange={(e) => onProjectOverviewChange(e.target.value)}
|
||||||
|
placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..."
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-3 pt-2">
|
||||||
|
<Checkbox
|
||||||
|
id="sidebar-generate-features"
|
||||||
|
checked={generateFeatures}
|
||||||
|
onCheckedChange={(checked) =>
|
||||||
|
onGenerateFeaturesChange(checked === true)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label
|
||||||
|
htmlFor="sidebar-generate-features"
|
||||||
|
className="text-sm font-medium cursor-pointer"
|
||||||
|
>
|
||||||
|
Generate feature list
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Automatically create features in the features folder from the
|
||||||
|
implementation roadmap after the spec is generated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" onClick={onSkip}>
|
||||||
|
Skip for now
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={onCreateSpec}
|
||||||
|
disabled={!projectOverview.trim()}
|
||||||
|
>
|
||||||
|
<Sparkles className="w-4 h-4 mr-2" />
|
||||||
|
Generate Spec
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -79,10 +79,10 @@ import {
|
|||||||
} from "@/lib/project-init";
|
} from "@/lib/project-init";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { themeOptions } from "@/config/theme-options";
|
import { themeOptions } from "@/config/theme-options";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
|
||||||
import type { SpecRegenerationEvent } from "@/types/electron";
|
import type { SpecRegenerationEvent } from "@/types/electron";
|
||||||
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
|
import { DeleteProjectDialog } from "@/components/views/settings-view/components/delete-project-dialog";
|
||||||
import { NewProjectModal } from "@/components/new-project-modal";
|
import { NewProjectModal } from "@/components/new-project-modal";
|
||||||
|
import { ProjectSetupDialog } from "@/components/layout/project-setup-dialog";
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
DragEndEvent,
|
DragEndEvent,
|
||||||
@@ -471,6 +471,12 @@ export function Sidebar() {
|
|||||||
toast.error("Failed to create specification", {
|
toast.error("Failed to create specification", {
|
||||||
description: result.error,
|
description: result.error,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// Show processing toast to inform user
|
||||||
|
toast.info("Generating app specification...", {
|
||||||
|
description:
|
||||||
|
"This may take a minute. You'll be notified when complete.",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// If successful, we'll wait for the events to update the state
|
// If successful, we'll wait for the events to update the state
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1904,79 +1910,17 @@ export function Sidebar() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
{/* New Project Setup Dialog */}
|
{/* New Project Setup Dialog */}
|
||||||
<Dialog
|
<ProjectSetupDialog
|
||||||
open={showSetupDialog}
|
open={showSetupDialog}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={setShowSetupDialog}
|
||||||
if (!open && !isCreatingSpec) {
|
projectOverview={projectOverview}
|
||||||
handleSkipSetup();
|
onProjectOverviewChange={setProjectOverview}
|
||||||
}
|
generateFeatures={generateFeatures}
|
||||||
}}
|
onGenerateFeaturesChange={setGenerateFeatures}
|
||||||
>
|
onCreateSpec={handleCreateInitialSpec}
|
||||||
<DialogContent className="max-w-2xl">
|
onSkip={handleSkipSetup}
|
||||||
<DialogHeader>
|
isCreatingSpec={isCreatingSpec}
|
||||||
<DialogTitle>Set Up Your Project</DialogTitle>
|
|
||||||
<DialogDescription className="text-muted-foreground">
|
|
||||||
We didn't find an app_spec.txt file. Let us help you generate
|
|
||||||
your app_spec.txt to help describe your project for our system.
|
|
||||||
We'll analyze your project's tech stack and create a
|
|
||||||
comprehensive specification.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="space-y-4 py-4">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<label className="text-sm font-medium">Project Overview</label>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Describe what your project does and what features you want to
|
|
||||||
build. Be as detailed as you want - this will help us create a
|
|
||||||
better specification.
|
|
||||||
</p>
|
|
||||||
<textarea
|
|
||||||
className="w-full h-48 p-3 rounded-md border border-border bg-background font-mono text-sm resize-none focus:outline-none focus:ring-2 focus:ring-ring"
|
|
||||||
value={projectOverview}
|
|
||||||
onChange={(e) => setProjectOverview(e.target.value)}
|
|
||||||
placeholder="e.g., A project management tool that allows teams to track tasks, manage sprints, and visualize progress through kanban boards. It should support user authentication, real-time updates, and file attachments..."
|
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-start space-x-3 pt-2">
|
|
||||||
<Checkbox
|
|
||||||
id="sidebar-generate-features"
|
|
||||||
checked={generateFeatures}
|
|
||||||
onCheckedChange={(checked) =>
|
|
||||||
setGenerateFeatures(checked === true)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label
|
|
||||||
htmlFor="sidebar-generate-features"
|
|
||||||
className="text-sm font-medium cursor-pointer"
|
|
||||||
>
|
|
||||||
Generate feature list
|
|
||||||
</label>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Automatically create features in the features folder from the
|
|
||||||
implementation roadmap after the spec is generated.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
|
||||||
<Button variant="ghost" onClick={handleSkipSetup}>
|
|
||||||
Skip for now
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={handleCreateInitialSpec}
|
|
||||||
disabled={!projectOverview.trim()}
|
|
||||||
>
|
|
||||||
<Sparkles className="w-4 h-4 mr-2" />
|
|
||||||
Generate Spec
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* New Project Onboarding Dialog */}
|
{/* New Project Onboarding Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|||||||
@@ -275,7 +275,7 @@ export function BoardView() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Subscribe to spec regeneration events to clear state on completion
|
// Subscribe to spec regeneration events to clear creating state on completion
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api.specRegeneration) return;
|
if (!api.specRegeneration) return;
|
||||||
@@ -495,6 +495,30 @@ export function BoardView() {
|
|||||||
}
|
}
|
||||||
}, [currentProject, setFeatures]);
|
}, [currentProject, setFeatures]);
|
||||||
|
|
||||||
|
// Subscribe to spec regeneration complete events to refresh kanban board
|
||||||
|
useEffect(() => {
|
||||||
|
const api = getElectronAPI();
|
||||||
|
if (!api.specRegeneration) return;
|
||||||
|
|
||||||
|
const unsubscribe = api.specRegeneration.onEvent((event) => {
|
||||||
|
// Refresh the kanban board when spec regeneration completes for the current project
|
||||||
|
if (
|
||||||
|
event.type === "spec_regeneration_complete" &&
|
||||||
|
currentProject &&
|
||||||
|
event.projectPath === currentProject.path
|
||||||
|
) {
|
||||||
|
console.log(
|
||||||
|
"[BoardView] Spec regeneration complete, refreshing features"
|
||||||
|
);
|
||||||
|
loadFeatures();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
};
|
||||||
|
}, [currentProject, loadFeatures]);
|
||||||
|
|
||||||
// Load persisted categories from file
|
// Load persisted categories from file
|
||||||
const loadCategories = useCallback(async () => {
|
const loadCategories = useCallback(async () => {
|
||||||
if (!currentProject) return;
|
if (!currentProject) return;
|
||||||
|
|||||||
93
apps/app/src/config/model-config.ts
Normal file
93
apps/app/src/config/model-config.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
/**
|
||||||
|
* Model Configuration - Centralized model settings for the app
|
||||||
|
*
|
||||||
|
* Models can be overridden via environment variables:
|
||||||
|
* - AUTOMAKER_MODEL_CHAT: Model for chat interactions
|
||||||
|
* - AUTOMAKER_MODEL_DEFAULT: Fallback model for all operations
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Claude model aliases for convenience
|
||||||
|
*/
|
||||||
|
export const CLAUDE_MODEL_MAP: Record<string, string> = {
|
||||||
|
haiku: "claude-haiku-4-5",
|
||||||
|
sonnet: "claude-sonnet-4-20250514",
|
||||||
|
opus: "claude-opus-4-5-20251101",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default models per use case
|
||||||
|
*/
|
||||||
|
export const DEFAULT_MODELS = {
|
||||||
|
chat: "claude-opus-4-5-20251101",
|
||||||
|
default: "claude-opus-4-5-20251101",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve a model alias to a full model string
|
||||||
|
*/
|
||||||
|
export function resolveModelString(
|
||||||
|
modelKey?: string,
|
||||||
|
defaultModel: string = DEFAULT_MODELS.default
|
||||||
|
): string {
|
||||||
|
if (!modelKey) {
|
||||||
|
return defaultModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full Claude model string - pass through
|
||||||
|
if (modelKey.includes("claude-")) {
|
||||||
|
return modelKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check alias map
|
||||||
|
const resolved = CLAUDE_MODEL_MAP[modelKey];
|
||||||
|
if (resolved) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown key - use default
|
||||||
|
return defaultModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the model for chat operations
|
||||||
|
*
|
||||||
|
* Priority:
|
||||||
|
* 1. Explicit model parameter
|
||||||
|
* 2. AUTOMAKER_MODEL_CHAT environment variable
|
||||||
|
* 3. AUTOMAKER_MODEL_DEFAULT environment variable
|
||||||
|
* 4. Default chat model
|
||||||
|
*/
|
||||||
|
export function getChatModel(explicitModel?: string): string {
|
||||||
|
if (explicitModel) {
|
||||||
|
return resolveModelString(explicitModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envModel =
|
||||||
|
process.env.AUTOMAKER_MODEL_CHAT || process.env.AUTOMAKER_MODEL_DEFAULT;
|
||||||
|
|
||||||
|
if (envModel) {
|
||||||
|
return resolveModelString(envModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DEFAULT_MODELS.chat;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default allowed tools for chat interactions
|
||||||
|
*/
|
||||||
|
export const CHAT_TOOLS = [
|
||||||
|
"Read",
|
||||||
|
"Write",
|
||||||
|
"Edit",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"Bash",
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default max turns for chat
|
||||||
|
*/
|
||||||
|
export const CHAT_MAX_TURNS = 1000;
|
||||||
@@ -48,7 +48,9 @@ export function resolveModelString(
|
|||||||
// Look up Claude model alias
|
// Look up Claude model alias
|
||||||
const resolved = CLAUDE_MODEL_MAP[modelKey];
|
const resolved = CLAUDE_MODEL_MAP[modelKey];
|
||||||
if (resolved) {
|
if (resolved) {
|
||||||
console.log(`[ModelResolver] Resolved model alias: "${modelKey}" -> "${resolved}"`);
|
console.log(
|
||||||
|
`[ModelResolver] Resolved model alias: "${modelKey}" -> "${resolved}"`
|
||||||
|
);
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,8 +75,5 @@ export function getEffectiveModel(
|
|||||||
sessionModel?: string,
|
sessionModel?: string,
|
||||||
defaultModel?: string
|
defaultModel?: string
|
||||||
): string {
|
): string {
|
||||||
return resolveModelString(
|
return resolveModelString(explicitModel || sessionModel, defaultModel);
|
||||||
explicitModel || sessionModel,
|
|
||||||
defaultModel
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
291
apps/server/src/lib/sdk-options.ts
Normal file
291
apps/server/src/lib/sdk-options.ts
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
/**
|
||||||
|
* SDK Options Factory - Centralized configuration for Claude Agent SDK
|
||||||
|
*
|
||||||
|
* Provides presets for common use cases:
|
||||||
|
* - Spec generation: Long-running analysis with read-only tools
|
||||||
|
* - Feature generation: Quick JSON generation from specs
|
||||||
|
* - Feature building: Autonomous feature implementation with full tool access
|
||||||
|
* - Suggestions: Analysis with read-only tools
|
||||||
|
* - Chat: Full tool access for interactive coding
|
||||||
|
*
|
||||||
|
* Uses model-resolver for consistent model handling across the application.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
||||||
|
import {
|
||||||
|
resolveModelString,
|
||||||
|
DEFAULT_MODELS,
|
||||||
|
CLAUDE_MODEL_MAP,
|
||||||
|
} from "./model-resolver.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tool presets for different use cases
|
||||||
|
*/
|
||||||
|
export const TOOL_PRESETS = {
|
||||||
|
/** Read-only tools for analysis */
|
||||||
|
readOnly: ["Read", "Glob", "Grep"] as const,
|
||||||
|
|
||||||
|
/** Tools for spec generation that needs to read the codebase */
|
||||||
|
specGeneration: ["Read", "Glob", "Grep"] as const,
|
||||||
|
|
||||||
|
/** Full tool access for feature implementation */
|
||||||
|
fullAccess: [
|
||||||
|
"Read",
|
||||||
|
"Write",
|
||||||
|
"Edit",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"Bash",
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch",
|
||||||
|
] as const,
|
||||||
|
|
||||||
|
/** Tools for chat/interactive mode */
|
||||||
|
chat: [
|
||||||
|
"Read",
|
||||||
|
"Write",
|
||||||
|
"Edit",
|
||||||
|
"Glob",
|
||||||
|
"Grep",
|
||||||
|
"Bash",
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch",
|
||||||
|
] as const,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Max turns presets for different use cases
|
||||||
|
*/
|
||||||
|
export const MAX_TURNS = {
|
||||||
|
/** Quick operations that shouldn't need many iterations */
|
||||||
|
quick: 5,
|
||||||
|
|
||||||
|
/** Standard operations */
|
||||||
|
standard: 20,
|
||||||
|
|
||||||
|
/** Long-running operations like full spec generation */
|
||||||
|
extended: 50,
|
||||||
|
|
||||||
|
/** Very long operations that may require extensive exploration */
|
||||||
|
maximum: 1000,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model presets for different use cases
|
||||||
|
*
|
||||||
|
* These can be overridden via environment variables:
|
||||||
|
* - AUTOMAKER_MODEL_SPEC: Model for spec generation
|
||||||
|
* - AUTOMAKER_MODEL_FEATURES: Model for feature generation
|
||||||
|
* - AUTOMAKER_MODEL_SUGGESTIONS: Model for suggestions
|
||||||
|
* - AUTOMAKER_MODEL_CHAT: Model for chat
|
||||||
|
* - AUTOMAKER_MODEL_DEFAULT: Fallback model for all operations
|
||||||
|
*/
|
||||||
|
export function getModelForUseCase(
|
||||||
|
useCase: "spec" | "features" | "suggestions" | "chat" | "auto" | "default",
|
||||||
|
explicitModel?: string
|
||||||
|
): string {
|
||||||
|
// Explicit model takes precedence
|
||||||
|
if (explicitModel) {
|
||||||
|
return resolveModelString(explicitModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check environment variable override for this use case
|
||||||
|
const envVarMap: Record<string, string | undefined> = {
|
||||||
|
spec: process.env.AUTOMAKER_MODEL_SPEC,
|
||||||
|
features: process.env.AUTOMAKER_MODEL_FEATURES,
|
||||||
|
suggestions: process.env.AUTOMAKER_MODEL_SUGGESTIONS,
|
||||||
|
chat: process.env.AUTOMAKER_MODEL_CHAT,
|
||||||
|
auto: process.env.AUTOMAKER_MODEL_AUTO,
|
||||||
|
default: process.env.AUTOMAKER_MODEL_DEFAULT,
|
||||||
|
};
|
||||||
|
|
||||||
|
const envModel = envVarMap[useCase] || envVarMap.default;
|
||||||
|
if (envModel) {
|
||||||
|
return resolveModelString(envModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultModels: Record<string, string> = {
|
||||||
|
spec: CLAUDE_MODEL_MAP["haiku"], // used to generate app specs
|
||||||
|
features: CLAUDE_MODEL_MAP["haiku"], // used to generate features from app specs
|
||||||
|
suggestions: CLAUDE_MODEL_MAP["haiku"], // used for suggestions
|
||||||
|
chat: CLAUDE_MODEL_MAP["haiku"], // used for chat
|
||||||
|
auto: CLAUDE_MODEL_MAP["opus"], // used to implement kanban cards
|
||||||
|
default: CLAUDE_MODEL_MAP["opus"],
|
||||||
|
};
|
||||||
|
|
||||||
|
return resolveModelString(defaultModels[useCase] || DEFAULT_MODELS.claude);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base options that apply to all SDK calls
|
||||||
|
*/
|
||||||
|
function getBaseOptions(): Partial<Options> {
|
||||||
|
return {
|
||||||
|
permissionMode: "acceptEdits",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options configuration for creating SDK options
|
||||||
|
*/
|
||||||
|
export interface CreateSdkOptionsConfig {
|
||||||
|
/** Working directory for the agent */
|
||||||
|
cwd: string;
|
||||||
|
|
||||||
|
/** Optional explicit model override */
|
||||||
|
model?: string;
|
||||||
|
|
||||||
|
/** Optional session model (used as fallback if explicit model not provided) */
|
||||||
|
sessionModel?: string;
|
||||||
|
|
||||||
|
/** Optional system prompt */
|
||||||
|
systemPrompt?: string;
|
||||||
|
|
||||||
|
/** Optional abort controller for cancellation */
|
||||||
|
abortController?: AbortController;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SDK options for spec generation
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* - Uses read-only tools for codebase analysis
|
||||||
|
* - Extended turns for thorough exploration
|
||||||
|
* - Opus model by default (can be overridden)
|
||||||
|
*/
|
||||||
|
export function createSpecGenerationOptions(
|
||||||
|
config: CreateSdkOptionsConfig
|
||||||
|
): Options {
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("spec", config.model),
|
||||||
|
maxTurns: MAX_TURNS.maximum,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: [...TOOL_PRESETS.specGeneration],
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SDK options for feature generation from specs
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* - Uses read-only tools (just needs to read the spec)
|
||||||
|
* - Quick turns since it's mostly JSON generation
|
||||||
|
* - Sonnet model by default for speed
|
||||||
|
*/
|
||||||
|
export function createFeatureGenerationOptions(
|
||||||
|
config: CreateSdkOptionsConfig
|
||||||
|
): Options {
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("features", config.model),
|
||||||
|
maxTurns: MAX_TURNS.quick,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: [...TOOL_PRESETS.readOnly],
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SDK options for generating suggestions
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* - Uses read-only tools for analysis
|
||||||
|
* - Quick turns for focused suggestions
|
||||||
|
* - Opus model by default for thorough analysis
|
||||||
|
*/
|
||||||
|
export function createSuggestionsOptions(
|
||||||
|
config: CreateSdkOptionsConfig
|
||||||
|
): Options {
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("suggestions", config.model),
|
||||||
|
maxTurns: MAX_TURNS.quick,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: [...TOOL_PRESETS.readOnly],
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SDK options for chat/interactive mode
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* - Full tool access for code modification
|
||||||
|
* - Standard turns for interactive sessions
|
||||||
|
* - Model priority: explicit model > session model > chat default
|
||||||
|
* - Sandbox enabled for bash safety
|
||||||
|
*/
|
||||||
|
export function createChatOptions(config: CreateSdkOptionsConfig): Options {
|
||||||
|
// Model priority: explicit model > session model > chat default
|
||||||
|
const effectiveModel = config.model || config.sessionModel;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("chat", effectiveModel),
|
||||||
|
maxTurns: MAX_TURNS.standard,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: [...TOOL_PRESETS.chat],
|
||||||
|
sandbox: {
|
||||||
|
enabled: true,
|
||||||
|
autoAllowBashIfSandboxed: true,
|
||||||
|
},
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create SDK options for autonomous feature building/implementation
|
||||||
|
*
|
||||||
|
* Configuration:
|
||||||
|
* - Full tool access for code modification and implementation
|
||||||
|
* - Extended turns for thorough feature implementation
|
||||||
|
* - Uses default model (can be overridden)
|
||||||
|
* - Sandbox enabled for bash safety
|
||||||
|
*/
|
||||||
|
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("auto", config.model),
|
||||||
|
maxTurns: MAX_TURNS.maximum,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: [...TOOL_PRESETS.fullAccess],
|
||||||
|
sandbox: {
|
||||||
|
enabled: true,
|
||||||
|
autoAllowBashIfSandboxed: true,
|
||||||
|
},
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create custom SDK options with explicit configuration
|
||||||
|
*
|
||||||
|
* Use this when the preset options don't fit your use case.
|
||||||
|
*/
|
||||||
|
export function createCustomOptions(
|
||||||
|
config: CreateSdkOptionsConfig & {
|
||||||
|
maxTurns?: number;
|
||||||
|
allowedTools?: readonly string[];
|
||||||
|
sandbox?: { enabled: boolean; autoAllowBashIfSandboxed?: boolean };
|
||||||
|
}
|
||||||
|
): Options {
|
||||||
|
return {
|
||||||
|
...getBaseOptions(),
|
||||||
|
model: getModelForUseCase("default", config.model),
|
||||||
|
maxTurns: config.maxTurns ?? MAX_TURNS.maximum,
|
||||||
|
cwd: config.cwd,
|
||||||
|
allowedTools: config.allowedTools
|
||||||
|
? [...config.allowedTools]
|
||||||
|
: [...TOOL_PRESETS.readOnly],
|
||||||
|
...(config.sandbox && { sandbox: config.sandbox }),
|
||||||
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,16 +2,19 @@
|
|||||||
* Generate features from existing app_spec.txt
|
* Generate features from existing app_spec.txt
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from "../../lib/events.js";
|
||||||
import { createLogger } from "../../lib/logger.js";
|
import { createLogger } from "../../lib/logger.js";
|
||||||
|
import { createFeatureGenerationOptions } from "../../lib/sdk-options.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";
|
||||||
|
|
||||||
const logger = createLogger("SpecRegeneration");
|
const logger = createLogger("SpecRegeneration");
|
||||||
|
|
||||||
|
const MAX_FEATURES = 100;
|
||||||
|
|
||||||
export async function generateFeaturesFromSpec(
|
export async function generateFeaturesFromSpec(
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
events: EventEmitter,
|
events: EventEmitter,
|
||||||
@@ -29,6 +32,10 @@ export async function generateFeaturesFromSpec(
|
|||||||
try {
|
try {
|
||||||
spec = await fs.readFile(specPath, "utf-8");
|
spec = await fs.readFile(specPath, "utf-8");
|
||||||
logger.info(`Spec loaded successfully (${spec.length} chars)`);
|
logger.info(`Spec loaded successfully (${spec.length} chars)`);
|
||||||
|
logger.info(`Spec preview (first 500 chars): ${spec.substring(0, 500)}`);
|
||||||
|
logger.info(
|
||||||
|
`Spec preview (last 500 chars): ${spec.substring(spec.length - 500)}`
|
||||||
|
);
|
||||||
} catch (readError) {
|
} catch (readError) {
|
||||||
logger.error("❌ Failed to read spec file:", readError);
|
logger.error("❌ Failed to read spec file:", readError);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
@@ -66,9 +73,16 @@ Format as JSON:
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
Generate 5-15 features that build on each other logically.`;
|
Generate ${MAX_FEATURES} features that build on each other logically.
|
||||||
|
|
||||||
logger.debug("Prompt length:", `${prompt.length} chars`);
|
IMPORTANT: Do not ask for clarification. The specification is provided above. Generate the JSON immediately.`;
|
||||||
|
|
||||||
|
logger.info("========== PROMPT BEING SENT ==========");
|
||||||
|
logger.info(`Prompt length: ${prompt.length} chars`);
|
||||||
|
logger.info(
|
||||||
|
`Prompt preview (first 1000 chars):\n${prompt.substring(0, 1000)}`
|
||||||
|
);
|
||||||
|
logger.info("========== END PROMPT PREVIEW ==========");
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
@@ -76,14 +90,10 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options: Options = {
|
const options = createFeatureGenerationOptions({
|
||||||
model: "claude-sonnet-4-20250514",
|
|
||||||
maxTurns: 5,
|
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
allowedTools: ["Read", "Glob"],
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
abortController,
|
abortController,
|
||||||
};
|
});
|
||||||
|
|
||||||
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
|
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
|
||||||
logger.info("Calling Claude Agent SDK query() for features...");
|
logger.info("Calling Claude Agent SDK query() for features...");
|
||||||
@@ -120,7 +130,7 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
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") {
|
||||||
responseText = block.text;
|
responseText += block.text;
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Feature text block received (${block.text.length} chars)`
|
`Feature text block received (${block.text.length} chars)`
|
||||||
);
|
);
|
||||||
@@ -147,6 +157,9 @@ Generate 5-15 features that build on each other logically.`;
|
|||||||
|
|
||||||
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
|
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
|
||||||
logger.info(`Feature response length: ${responseText.length} chars`);
|
logger.info(`Feature response length: ${responseText.length} chars`);
|
||||||
|
logger.info("========== FULL RESPONSE TEXT ==========");
|
||||||
|
logger.info(responseText);
|
||||||
|
logger.info("========== END RESPONSE TEXT ==========");
|
||||||
|
|
||||||
await parseAndCreateFeatures(projectPath, responseText, events);
|
await parseAndCreateFeatures(projectPath, responseText, events);
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
* Generate app_spec.txt from project overview
|
* Generate app_spec.txt from project overview
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from "../../lib/events.js";
|
||||||
import { getAppSpecFormatInstruction } from "../../lib/app-spec-format.js";
|
import { getAppSpecFormatInstruction } from "../../lib/app-spec-format.js";
|
||||||
import { createLogger } from "../../lib/logger.js";
|
import { createLogger } from "../../lib/logger.js";
|
||||||
|
import { createSpecGenerationOptions } from "../../lib/sdk-options.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";
|
||||||
|
|
||||||
@@ -21,11 +22,12 @@ export async function generateSpec(
|
|||||||
generateFeatures?: boolean,
|
generateFeatures?: boolean,
|
||||||
analyzeProject?: boolean
|
analyzeProject?: boolean
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.debug("========== generateSpec() started ==========");
|
logger.info("========== generateSpec() started ==========");
|
||||||
logger.debug("projectPath:", projectPath);
|
logger.info("projectPath:", projectPath);
|
||||||
logger.debug("projectOverview length:", `${projectOverview.length} chars`);
|
logger.info("projectOverview length:", `${projectOverview.length} chars`);
|
||||||
logger.debug("generateFeatures:", generateFeatures);
|
logger.info("projectOverview preview:", projectOverview.substring(0, 300));
|
||||||
logger.debug("analyzeProject:", analyzeProject);
|
logger.info("generateFeatures:", generateFeatures);
|
||||||
|
logger.info("analyzeProject:", analyzeProject);
|
||||||
|
|
||||||
// Build the prompt based on whether we should analyze the project
|
// Build the prompt based on whether we should analyze the project
|
||||||
let analysisInstructions = "";
|
let analysisInstructions = "";
|
||||||
@@ -63,21 +65,20 @@ ${analysisInstructions}
|
|||||||
|
|
||||||
${getAppSpecFormatInstruction()}`;
|
${getAppSpecFormatInstruction()}`;
|
||||||
|
|
||||||
logger.debug("Prompt length:", `${prompt.length} chars`);
|
logger.info("========== PROMPT BEING SENT ==========");
|
||||||
|
logger.info(`Prompt length: ${prompt.length} chars`);
|
||||||
|
logger.info(`Prompt preview (first 500 chars):\n${prompt.substring(0, 500)}`);
|
||||||
|
logger.info("========== END PROMPT PREVIEW ==========");
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_progress",
|
type: "spec_progress",
|
||||||
content: "Starting spec generation...\n",
|
content: "Starting spec generation...\n",
|
||||||
});
|
});
|
||||||
|
|
||||||
const options: Options = {
|
const options = createSpecGenerationOptions({
|
||||||
model: "claude-opus-4-5-20251101",
|
|
||||||
maxTurns: 10,
|
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
allowedTools: ["Read", "Glob", "Grep"],
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
abortController,
|
abortController,
|
||||||
};
|
});
|
||||||
|
|
||||||
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
|
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
|
||||||
logger.info("Calling Claude Agent SDK query()...");
|
logger.info("Calling Claude Agent SDK query()...");
|
||||||
@@ -98,32 +99,48 @@ ${getAppSpecFormatInstruction()}`;
|
|||||||
let responseText = "";
|
let responseText = "";
|
||||||
let messageCount = 0;
|
let messageCount = 0;
|
||||||
|
|
||||||
logger.debug("Starting to iterate over stream...");
|
logger.info("Starting to iterate over stream...");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
logger.debug(
|
logger.info(
|
||||||
`Stream message #${messageCount}:`,
|
`Stream message #${messageCount}: type=${msg.type}, subtype=${
|
||||||
JSON.stringify(
|
(msg as any).subtype
|
||||||
{ type: msg.type, subtype: (msg as any).subtype },
|
}`
|
||||||
null,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (msg.type === "assistant" && msg.message.content) {
|
if (msg.type === "assistant") {
|
||||||
for (const block of msg.message.content) {
|
// Log the full message structure to debug
|
||||||
|
logger.info(`Assistant msg keys: ${Object.keys(msg).join(", ")}`);
|
||||||
|
const msgAny = msg as any;
|
||||||
|
if (msgAny.message) {
|
||||||
|
logger.info(
|
||||||
|
`msg.message keys: ${Object.keys(msgAny.message).join(", ")}`
|
||||||
|
);
|
||||||
|
if (msgAny.message.content) {
|
||||||
|
logger.info(
|
||||||
|
`msg.message.content length: ${msgAny.message.content.length}`
|
||||||
|
);
|
||||||
|
for (const block of msgAny.message.content) {
|
||||||
|
logger.info(
|
||||||
|
`Block keys: ${Object.keys(block).join(", ")}, type: ${
|
||||||
|
block.type
|
||||||
|
}`
|
||||||
|
);
|
||||||
if (block.type === "text") {
|
if (block.type === "text") {
|
||||||
responseText += block.text;
|
responseText += block.text;
|
||||||
logger.debug(`Text block received (${block.text.length} chars)`);
|
logger.info(
|
||||||
|
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
|
||||||
|
);
|
||||||
|
logger.info(`Text preview: ${block.text.substring(0, 200)}...`);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_regeneration_progress",
|
type: "spec_regeneration_progress",
|
||||||
content: block.text,
|
content: block.text,
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
} else if (block.type === "tool_use") {
|
} else if (block.type === "tool_use") {
|
||||||
logger.debug("Tool use:", block.name);
|
logger.info("Tool use:", block.name);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit("spec-regeneration:event", {
|
||||||
type: "spec_tool",
|
type: "spec_tool",
|
||||||
tool: block.name,
|
tool: block.name,
|
||||||
@@ -131,12 +148,47 @@ ${getAppSpecFormatInstruction()}`;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("msg.message.content is falsy");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn("msg.message is falsy");
|
||||||
|
// Log full message to see structure
|
||||||
|
logger.info(
|
||||||
|
`Full assistant msg: ${JSON.stringify(msg).substring(0, 1000)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
} else if (msg.type === "result" && (msg as any).subtype === "success") {
|
} else if (msg.type === "result" && (msg as any).subtype === "success") {
|
||||||
logger.debug("Received success result");
|
logger.info("Received success result");
|
||||||
responseText = (msg as any).result || responseText;
|
logger.info(`Result value: "${(msg as any).result}"`);
|
||||||
|
logger.info(
|
||||||
|
`Current responseText length before result: ${responseText.length}`
|
||||||
|
);
|
||||||
|
// Only use result if it has content, otherwise keep accumulated text
|
||||||
|
if ((msg as any).result && (msg as any).result.length > 0) {
|
||||||
|
logger.info("Using result value as responseText");
|
||||||
|
responseText = (msg as any).result;
|
||||||
|
} else {
|
||||||
|
logger.info("Result is empty, keeping accumulated responseText");
|
||||||
|
}
|
||||||
|
} else if (msg.type === "result") {
|
||||||
|
// Handle all 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! Claude used too many tool calls."
|
||||||
|
);
|
||||||
|
logger.info(`responseText so far: ${responseText.length} chars`);
|
||||||
|
}
|
||||||
} else if ((msg as { type: string }).type === "error") {
|
} else if ((msg as { type: string }).type === "error") {
|
||||||
logger.error("❌ Received error message from stream:");
|
logger.error("❌ Received error message from stream:");
|
||||||
logger.error("Error message:", JSON.stringify(msg, null, 2));
|
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) {
|
} catch (streamError) {
|
||||||
@@ -147,16 +199,31 @@ ${getAppSpecFormatInstruction()}`;
|
|||||||
|
|
||||||
logger.info(`Stream iteration complete. Total messages: ${messageCount}`);
|
logger.info(`Stream iteration complete. Total messages: ${messageCount}`);
|
||||||
logger.info(`Response text length: ${responseText.length} chars`);
|
logger.info(`Response text length: ${responseText.length} chars`);
|
||||||
|
logger.info("========== FINAL RESPONSE TEXT ==========");
|
||||||
|
logger.info(responseText || "(empty)");
|
||||||
|
logger.info("========== END RESPONSE TEXT ==========");
|
||||||
|
|
||||||
|
if (!responseText || responseText.trim().length === 0) {
|
||||||
|
logger.error("❌ WARNING: responseText is empty! Nothing to save.");
|
||||||
|
}
|
||||||
|
|
||||||
// Save spec
|
// Save spec
|
||||||
const specDir = path.join(projectPath, ".automaker");
|
const specDir = path.join(projectPath, ".automaker");
|
||||||
const specPath = path.join(specDir, "app_spec.txt");
|
const specPath = path.join(specDir, "app_spec.txt");
|
||||||
|
|
||||||
logger.info("Saving spec to:", specPath);
|
logger.info("Saving spec to:", specPath);
|
||||||
|
logger.info(`Content to save (${responseText.length} chars)`);
|
||||||
|
|
||||||
await fs.mkdir(specDir, { recursive: true });
|
await fs.mkdir(specDir, { recursive: true });
|
||||||
await fs.writeFile(specPath, responseText);
|
await fs.writeFile(specPath, responseText);
|
||||||
|
|
||||||
|
// Verify the file was written
|
||||||
|
const savedContent = await fs.readFile(specPath, "utf-8");
|
||||||
|
logger.info(`Verified saved file: ${savedContent.length} chars`);
|
||||||
|
if (savedContent.length === 0) {
|
||||||
|
logger.error("❌ File was saved but is empty!");
|
||||||
|
}
|
||||||
|
|
||||||
logger.info("Spec saved successfully");
|
logger.info("Spec saved successfully");
|
||||||
|
|
||||||
// Emit spec completion event
|
// Emit spec completion event
|
||||||
|
|||||||
@@ -14,23 +14,32 @@ export async function parseAndCreateFeatures(
|
|||||||
content: string,
|
content: string,
|
||||||
events: EventEmitter
|
events: EventEmitter
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.debug("========== parseAndCreateFeatures() started ==========");
|
logger.info("========== parseAndCreateFeatures() started ==========");
|
||||||
logger.debug("Content length:", `${content.length} chars`);
|
logger.info(`Content length: ${content.length} chars`);
|
||||||
|
logger.info("========== CONTENT RECEIVED FOR PARSING ==========");
|
||||||
|
logger.info(content);
|
||||||
|
logger.info("========== END CONTENT ==========");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract JSON from response
|
// Extract JSON from response
|
||||||
logger.debug("Extracting JSON from response...");
|
logger.info("Extracting JSON from response...");
|
||||||
|
logger.info(`Looking for pattern: /{[\\s\\S]*"features"[\\s\\S]*}/`);
|
||||||
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
|
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
|
||||||
if (!jsonMatch) {
|
if (!jsonMatch) {
|
||||||
logger.error("❌ No valid JSON found in response");
|
logger.error("❌ No valid JSON found in response");
|
||||||
logger.error("Content preview:", content.substring(0, 500));
|
logger.error("Full content received:");
|
||||||
|
logger.error(content);
|
||||||
throw new Error("No valid JSON found in response");
|
throw new Error("No valid JSON found in response");
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`JSON match found (${jsonMatch[0].length} chars)`);
|
logger.info(`JSON match found (${jsonMatch[0].length} chars)`);
|
||||||
|
logger.info("========== MATCHED JSON ==========");
|
||||||
|
logger.info(jsonMatch[0]);
|
||||||
|
logger.info("========== END MATCHED JSON ==========");
|
||||||
|
|
||||||
const parsed = JSON.parse(jsonMatch[0]);
|
const parsed = JSON.parse(jsonMatch[0]);
|
||||||
logger.info(`Parsed ${parsed.features?.length || 0} features`);
|
logger.info(`Parsed ${parsed.features?.length || 0} features`);
|
||||||
|
logger.info("Parsed features:", JSON.stringify(parsed.features, null, 2));
|
||||||
|
|
||||||
const featuresDir = path.join(projectPath, ".automaker", "features");
|
const featuresDir = path.join(projectPath, ".automaker", "features");
|
||||||
await fs.mkdir(featuresDir, { recursive: true });
|
await fs.mkdir(featuresDir, { recursive: true });
|
||||||
|
|||||||
@@ -2,9 +2,10 @@
|
|||||||
* Business logic for generating suggestions
|
* Business logic for generating suggestions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from "../../lib/events.js";
|
||||||
import { createLogger } from "../../lib/logger.js";
|
import { createLogger } from "../../lib/logger.js";
|
||||||
|
import { createSuggestionsOptions } from "../../lib/sdk-options.js";
|
||||||
|
|
||||||
const logger = createLogger("Suggestions");
|
const logger = createLogger("Suggestions");
|
||||||
|
|
||||||
@@ -54,14 +55,10 @@ Format your response as JSON:
|
|||||||
content: `Starting ${suggestionType} analysis...\n`,
|
content: `Starting ${suggestionType} analysis...\n`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const options: Options = {
|
const options = createSuggestionsOptions({
|
||||||
model: "claude-opus-4-5-20251101",
|
|
||||||
maxTurns: 5,
|
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
allowedTools: ["Read", "Glob", "Grep"],
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
abortController,
|
abortController,
|
||||||
};
|
});
|
||||||
|
|
||||||
const stream = query({ prompt, options });
|
const stream = query({ prompt, options });
|
||||||
let responseText = "";
|
let responseText = "";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { ProviderFactory } from "../providers/provider-factory.js";
|
|||||||
import type { ExecuteOptions } from "../providers/types.js";
|
import type { ExecuteOptions } from "../providers/types.js";
|
||||||
import { readImageAsBase64 } from "../lib/image-handler.js";
|
import { readImageAsBase64 } from "../lib/image-handler.js";
|
||||||
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
||||||
import { getEffectiveModel } from "../lib/model-resolver.js";
|
import { createChatOptions } from "../lib/sdk-options.js";
|
||||||
import { isAbortError } from "../lib/error-handler.js";
|
import { isAbortError } from "../lib/error-handler.js";
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
@@ -176,8 +176,19 @@ export class AgentService {
|
|||||||
await this.saveSession(sessionId, session.messages);
|
await this.saveSession(sessionId, session.messages);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use session model, parameter model, or default
|
// Build SDK options using centralized configuration
|
||||||
const effectiveModel = getEffectiveModel(model, session.model);
|
const sdkOptions = createChatOptions({
|
||||||
|
cwd: workingDirectory || session.workingDirectory,
|
||||||
|
model: model,
|
||||||
|
sessionModel: session.model,
|
||||||
|
systemPrompt: this.getSystemPrompt(),
|
||||||
|
abortController: session.abortController!,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract model, maxTurns, and allowedTools from SDK options
|
||||||
|
const effectiveModel = sdkOptions.model!;
|
||||||
|
const maxTurns = sdkOptions.maxTurns;
|
||||||
|
const allowedTools = sdkOptions.allowedTools as string[] | undefined;
|
||||||
|
|
||||||
// Get provider for this model
|
// Get provider for this model
|
||||||
const provider = ProviderFactory.getProviderForModel(effectiveModel);
|
const provider = ProviderFactory.getProviderForModel(effectiveModel);
|
||||||
@@ -192,17 +203,8 @@ export class AgentService {
|
|||||||
model: effectiveModel,
|
model: effectiveModel,
|
||||||
cwd: workingDirectory || session.workingDirectory,
|
cwd: workingDirectory || session.workingDirectory,
|
||||||
systemPrompt: this.getSystemPrompt(),
|
systemPrompt: this.getSystemPrompt(),
|
||||||
maxTurns: 20,
|
maxTurns: maxTurns,
|
||||||
allowedTools: [
|
allowedTools: allowedTools,
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
],
|
|
||||||
abortController: session.abortController!,
|
abortController: session.abortController!,
|
||||||
conversationHistory:
|
conversationHistory:
|
||||||
conversationHistory.length > 0 ? conversationHistory : undefined,
|
conversationHistory.length > 0 ? conversationHistory : undefined,
|
||||||
|
|||||||
@@ -9,16 +9,16 @@
|
|||||||
* - Verification and merge workflows
|
* - Verification and merge workflows
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AbortError } from "@anthropic-ai/claude-agent-sdk";
|
|
||||||
import { ProviderFactory } from "../providers/provider-factory.js";
|
import { ProviderFactory } from "../providers/provider-factory.js";
|
||||||
import type { ExecuteOptions } from "../providers/types.js";
|
import type { ExecuteOptions } from "../providers/types.js";
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import type { EventEmitter, EventType } from "../lib/events.js";
|
import type { EventEmitter } from "../lib/events.js";
|
||||||
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
import { buildPromptWithImages } from "../lib/prompt-builder.js";
|
||||||
import { resolveModelString, DEFAULT_MODELS } from "../lib/model-resolver.js";
|
import { resolveModelString, DEFAULT_MODELS } from "../lib/model-resolver.js";
|
||||||
|
import { createAutoModeOptions } from "../lib/sdk-options.js";
|
||||||
import { isAbortError, classifyError } from "../lib/error-handler.js";
|
import { isAbortError, classifyError } from "../lib/error-handler.js";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
@@ -1085,7 +1085,18 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
imagePaths?: string[],
|
imagePaths?: string[],
|
||||||
model?: string
|
model?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const finalModel = resolveModelString(model, DEFAULT_MODELS.claude);
|
// Build SDK options using centralized configuration for feature implementation
|
||||||
|
const sdkOptions = createAutoModeOptions({
|
||||||
|
cwd: workDir,
|
||||||
|
model: model,
|
||||||
|
abortController,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extract model, maxTurns, and allowedTools from SDK options
|
||||||
|
const finalModel = sdkOptions.model!;
|
||||||
|
const maxTurns = sdkOptions.maxTurns;
|
||||||
|
const allowedTools = sdkOptions.allowedTools as string[] | undefined;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}`
|
`[AutoMode] runAgent called for feature ${featureId} with model: ${finalModel}`
|
||||||
);
|
);
|
||||||
@@ -1108,9 +1119,9 @@ When done, summarize what you implemented and any notes for the developer.`;
|
|||||||
const options: ExecuteOptions = {
|
const options: ExecuteOptions = {
|
||||||
prompt: promptContent,
|
prompt: promptContent,
|
||||||
model: finalModel,
|
model: finalModel,
|
||||||
maxTurns: 50,
|
maxTurns: maxTurns,
|
||||||
cwd: workDir,
|
cwd: workDir,
|
||||||
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
allowedTools: allowedTools,
|
||||||
abortController,
|
abortController,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user