mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 08:53:36 +00:00
Merge branch 'main' into new-project-from-template
This commit is contained in:
@@ -1,682 +0,0 @@
|
|||||||
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
|
|
||||||
const path = require("path");
|
|
||||||
const fs = require("fs/promises");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Agent Service - Runs Claude agents in the Electron main process
|
|
||||||
* This service survives Next.js restarts and maintains conversation state
|
|
||||||
*/
|
|
||||||
class AgentService {
|
|
||||||
constructor() {
|
|
||||||
this.sessions = new Map(); // sessionId -> { messages, isRunning, abortController }
|
|
||||||
this.stateDir = null; // Will be set when app is ready
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the service with app data directory
|
|
||||||
*/
|
|
||||||
async initialize(appDataPath) {
|
|
||||||
this.stateDir = path.join(appDataPath, "agent-sessions");
|
|
||||||
this.metadataFile = path.join(appDataPath, "sessions-metadata.json");
|
|
||||||
await fs.mkdir(this.stateDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start or resume a conversation
|
|
||||||
*/
|
|
||||||
async startConversation({ sessionId, workingDirectory }) {
|
|
||||||
|
|
||||||
// Initialize session if it doesn't exist
|
|
||||||
if (!this.sessions.has(sessionId)) {
|
|
||||||
const messages = await this.loadSession(sessionId);
|
|
||||||
|
|
||||||
this.sessions.set(sessionId, {
|
|
||||||
messages,
|
|
||||||
isRunning: false,
|
|
||||||
abortController: null,
|
|
||||||
workingDirectory: workingDirectory || process.cwd(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const session = this.sessions.get(sessionId);
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
messages: session.messages,
|
|
||||||
sessionId,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send a message to the agent and stream responses
|
|
||||||
*/
|
|
||||||
async sendMessage({
|
|
||||||
sessionId,
|
|
||||||
message,
|
|
||||||
workingDirectory,
|
|
||||||
imagePaths,
|
|
||||||
sendToRenderer,
|
|
||||||
}) {
|
|
||||||
const session = this.sessions.get(sessionId);
|
|
||||||
if (!session) {
|
|
||||||
throw new Error(`Session ${sessionId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.isRunning) {
|
|
||||||
throw new Error("Agent is already processing a message");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read images from temp files and convert to base64 for storage
|
|
||||||
const images = [];
|
|
||||||
if (imagePaths && imagePaths.length > 0) {
|
|
||||||
const fs = require("fs/promises");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
for (const imagePath of imagePaths) {
|
|
||||||
try {
|
|
||||||
const imageBuffer = await fs.readFile(imagePath);
|
|
||||||
const base64Data = imageBuffer.toString("base64");
|
|
||||||
|
|
||||||
// Determine media type from file extension
|
|
||||||
const ext = path.extname(imagePath).toLowerCase();
|
|
||||||
const mimeTypeMap = {
|
|
||||||
".jpg": "image/jpeg",
|
|
||||||
".jpeg": "image/jpeg",
|
|
||||||
".png": "image/png",
|
|
||||||
".gif": "image/gif",
|
|
||||||
".webp": "image/webp",
|
|
||||||
};
|
|
||||||
const mediaType = mimeTypeMap[ext] || "image/png";
|
|
||||||
|
|
||||||
images.push({
|
|
||||||
data: base64Data,
|
|
||||||
mimeType: mediaType,
|
|
||||||
filename: path.basename(imagePath),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[AgentService] Loaded image from ${imagePath} for storage`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[AgentService] Failed to load image from ${imagePath}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add user message to conversation with base64 images
|
|
||||||
const userMessage = {
|
|
||||||
id: this.generateId(),
|
|
||||||
role: "user",
|
|
||||||
content: message,
|
|
||||||
images: images.length > 0 ? images : undefined,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
|
|
||||||
session.messages.push(userMessage);
|
|
||||||
session.isRunning = true;
|
|
||||||
session.abortController = new AbortController();
|
|
||||||
|
|
||||||
// Send initial user message to renderer
|
|
||||||
sendToRenderer({
|
|
||||||
type: "message",
|
|
||||||
message: userMessage,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save state with base64 images
|
|
||||||
await this.saveSession(sessionId, session.messages);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Configure Claude Agent SDK options
|
|
||||||
const options = {
|
|
||||||
// model: "claude-sonnet-4-20250514",
|
|
||||||
model: "claude-opus-4-5-20251101",
|
|
||||||
systemPrompt: this.getSystemPrompt(),
|
|
||||||
maxTurns: 20,
|
|
||||||
cwd: workingDirectory || session.workingDirectory,
|
|
||||||
allowedTools: [
|
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
],
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
sandbox: {
|
|
||||||
enabled: true,
|
|
||||||
autoAllowBashIfSandboxed: true,
|
|
||||||
},
|
|
||||||
abortController: session.abortController,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Build prompt content with text and images
|
|
||||||
let promptContent = message;
|
|
||||||
|
|
||||||
// If there are images, create a content array
|
|
||||||
if (imagePaths && imagePaths.length > 0) {
|
|
||||||
const contentBlocks = [];
|
|
||||||
|
|
||||||
// Add text block
|
|
||||||
if (message && message.trim()) {
|
|
||||||
contentBlocks.push({
|
|
||||||
type: "text",
|
|
||||||
text: message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add image blocks
|
|
||||||
const fs = require("fs");
|
|
||||||
for (const imagePath of imagePaths) {
|
|
||||||
try {
|
|
||||||
const imageBuffer = fs.readFileSync(imagePath);
|
|
||||||
const base64Data = imageBuffer.toString("base64");
|
|
||||||
const ext = path.extname(imagePath).toLowerCase();
|
|
||||||
const mimeTypeMap = {
|
|
||||||
".jpg": "image/jpeg",
|
|
||||||
".jpeg": "image/jpeg",
|
|
||||||
".png": "image/png",
|
|
||||||
".gif": "image/gif",
|
|
||||||
".webp": "image/webp",
|
|
||||||
};
|
|
||||||
const mediaType = mimeTypeMap[ext] || "image/png";
|
|
||||||
|
|
||||||
contentBlocks.push({
|
|
||||||
type: "image",
|
|
||||||
source: {
|
|
||||||
type: "base64",
|
|
||||||
media_type: mediaType,
|
|
||||||
data: base64Data,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[AgentService] Failed to load image ${imagePath}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use content blocks if we have images
|
|
||||||
if (
|
|
||||||
contentBlocks.length > 1 ||
|
|
||||||
(contentBlocks.length === 1 && contentBlocks[0].type === "image")
|
|
||||||
) {
|
|
||||||
promptContent = contentBlocks;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build payload for the SDK
|
|
||||||
const promptPayload = Array.isArray(promptContent)
|
|
||||||
? (async function* () {
|
|
||||||
yield {
|
|
||||||
type: "user",
|
|
||||||
session_id: "",
|
|
||||||
message: {
|
|
||||||
role: "user",
|
|
||||||
content: promptContent,
|
|
||||||
},
|
|
||||||
parent_tool_use_id: null,
|
|
||||||
};
|
|
||||||
})()
|
|
||||||
: promptContent;
|
|
||||||
|
|
||||||
// Send the query via the SDK (conversation state handled by the SDK)
|
|
||||||
const stream = query({ prompt: promptPayload, options });
|
|
||||||
|
|
||||||
let currentAssistantMessage = null;
|
|
||||||
let responseText = "";
|
|
||||||
const toolUses = [];
|
|
||||||
|
|
||||||
// Stream responses from the SDK
|
|
||||||
for await (const msg of stream) {
|
|
||||||
if (msg.type === "assistant") {
|
|
||||||
if (msg.message.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
if (block.type === "text") {
|
|
||||||
responseText += block.text;
|
|
||||||
|
|
||||||
// Create or update assistant message
|
|
||||||
if (!currentAssistantMessage) {
|
|
||||||
currentAssistantMessage = {
|
|
||||||
id: this.generateId(),
|
|
||||||
role: "assistant",
|
|
||||||
content: responseText,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
session.messages.push(currentAssistantMessage);
|
|
||||||
} else {
|
|
||||||
currentAssistantMessage.content = responseText;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stream to renderer
|
|
||||||
sendToRenderer({
|
|
||||||
type: "stream",
|
|
||||||
messageId: currentAssistantMessage.id,
|
|
||||||
content: responseText,
|
|
||||||
isComplete: false,
|
|
||||||
});
|
|
||||||
} else if (block.type === "tool_use") {
|
|
||||||
const toolUse = {
|
|
||||||
name: block.name,
|
|
||||||
input: block.input,
|
|
||||||
};
|
|
||||||
toolUses.push(toolUse);
|
|
||||||
|
|
||||||
// Send tool use notification
|
|
||||||
sendToRenderer({
|
|
||||||
type: "tool_use",
|
|
||||||
tool: toolUse,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (msg.type === "result") {
|
|
||||||
if (msg.subtype === "success" && msg.result) {
|
|
||||||
// Use the final result
|
|
||||||
if (currentAssistantMessage) {
|
|
||||||
currentAssistantMessage.content = msg.result;
|
|
||||||
responseText = msg.result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send completion
|
|
||||||
sendToRenderer({
|
|
||||||
type: "complete",
|
|
||||||
messageId: currentAssistantMessage?.id,
|
|
||||||
content: responseText,
|
|
||||||
toolUses,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save final state
|
|
||||||
await this.saveSession(sessionId, session.messages);
|
|
||||||
|
|
||||||
session.isRunning = false;
|
|
||||||
session.abortController = null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: currentAssistantMessage,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AbortError || error?.name === "AbortError") {
|
|
||||||
// Query aborted
|
|
||||||
session.isRunning = false;
|
|
||||||
session.abortController = null;
|
|
||||||
return { success: false, aborted: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("[AgentService] Error:", error);
|
|
||||||
|
|
||||||
session.isRunning = false;
|
|
||||||
session.abortController = null;
|
|
||||||
|
|
||||||
// Add error message
|
|
||||||
const errorMessage = {
|
|
||||||
id: this.generateId(),
|
|
||||||
role: "assistant",
|
|
||||||
content: `Error: ${error.message}`,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
isError: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
session.messages.push(errorMessage);
|
|
||||||
await this.saveSession(sessionId, session.messages);
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "error",
|
|
||||||
error: error.message,
|
|
||||||
message: errorMessage,
|
|
||||||
});
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get conversation history
|
|
||||||
*/
|
|
||||||
getHistory(sessionId) {
|
|
||||||
const session = this.sessions.get(sessionId);
|
|
||||||
if (!session) {
|
|
||||||
return { success: false, error: "Session not found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
messages: session.messages,
|
|
||||||
isRunning: session.isRunning,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop current agent execution
|
|
||||||
*/
|
|
||||||
async stopExecution(sessionId) {
|
|
||||||
const session = this.sessions.get(sessionId);
|
|
||||||
if (!session) {
|
|
||||||
return { success: false, error: "Session not found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (session.abortController) {
|
|
||||||
session.abortController.abort();
|
|
||||||
session.isRunning = false;
|
|
||||||
session.abortController = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear conversation history
|
|
||||||
*/
|
|
||||||
async clearSession(sessionId) {
|
|
||||||
const session = this.sessions.get(sessionId);
|
|
||||||
if (session) {
|
|
||||||
session.messages = [];
|
|
||||||
session.isRunning = false;
|
|
||||||
await this.saveSession(sessionId, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load session from disk
|
|
||||||
*/
|
|
||||||
async loadSession(sessionId) {
|
|
||||||
if (!this.stateDir) return [];
|
|
||||||
|
|
||||||
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await fs.readFile(sessionFile, "utf-8");
|
|
||||||
const parsed = JSON.parse(data);
|
|
||||||
console.log(
|
|
||||||
`[AgentService] Loaded ${parsed.length} messages for ${sessionId}`
|
|
||||||
);
|
|
||||||
return parsed;
|
|
||||||
} catch (error) {
|
|
||||||
// Session doesn't exist yet
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save session to disk
|
|
||||||
*/
|
|
||||||
async saveSession(sessionId, messages) {
|
|
||||||
if (!this.stateDir) return;
|
|
||||||
|
|
||||||
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.writeFile(
|
|
||||||
sessionFile,
|
|
||||||
JSON.stringify(messages, null, 2),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[AgentService] Saved ${messages.length} messages for ${sessionId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update timestamp
|
|
||||||
await this.updateSessionTimestamp(sessionId);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[AgentService] Failed to save session:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get system prompt
|
|
||||||
*/
|
|
||||||
getSystemPrompt() {
|
|
||||||
return `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.
|
|
||||||
|
|
||||||
**Feature Storage:**
|
|
||||||
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
|
|
||||||
Use the UpdateFeatureStatus tool to manage features, not direct file edits.
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
IMPORTANT: When making file changes, be aware that the Next.js development server may restart.
|
|
||||||
This is normal and expected. Your conversation state is preserved across these restarts.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate unique ID
|
|
||||||
*/
|
|
||||||
generateId() {
|
|
||||||
return `msg_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Session Management
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load all session metadata
|
|
||||||
*/
|
|
||||||
async loadMetadata() {
|
|
||||||
if (!this.metadataFile) return {};
|
|
||||||
|
|
||||||
try {
|
|
||||||
const data = await fs.readFile(this.metadataFile, "utf-8");
|
|
||||||
return JSON.parse(data);
|
|
||||||
} catch (error) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save session metadata
|
|
||||||
*/
|
|
||||||
async saveMetadata(metadata) {
|
|
||||||
if (!this.metadataFile) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.writeFile(
|
|
||||||
this.metadataFile,
|
|
||||||
JSON.stringify(metadata, null, 2),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[AgentService] Failed to save metadata:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List all sessions
|
|
||||||
*/
|
|
||||||
async listSessions({ includeArchived = false } = {}) {
|
|
||||||
const metadata = await this.loadMetadata();
|
|
||||||
const sessions = [];
|
|
||||||
|
|
||||||
for (const [sessionId, meta] of Object.entries(metadata)) {
|
|
||||||
if (!includeArchived && meta.isArchived) continue;
|
|
||||||
|
|
||||||
const messages = await this.loadSession(sessionId);
|
|
||||||
const lastMessage = messages[messages.length - 1];
|
|
||||||
|
|
||||||
sessions.push({
|
|
||||||
id: sessionId,
|
|
||||||
name: meta.name || sessionId,
|
|
||||||
projectPath: meta.projectPath || "",
|
|
||||||
createdAt: meta.createdAt,
|
|
||||||
updatedAt: meta.updatedAt,
|
|
||||||
messageCount: messages.length,
|
|
||||||
isArchived: meta.isArchived || false,
|
|
||||||
tags: meta.tags || [],
|
|
||||||
preview: lastMessage?.content.substring(0, 100) || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by most recently updated
|
|
||||||
sessions.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
||||||
|
|
||||||
return sessions;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new session
|
|
||||||
*/
|
|
||||||
async createSession({ name, projectPath, workingDirectory }) {
|
|
||||||
const sessionId = `session_${Date.now()}_${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substring(2, 11)}`;
|
|
||||||
|
|
||||||
const metadata = await this.loadMetadata();
|
|
||||||
metadata[sessionId] = {
|
|
||||||
name,
|
|
||||||
projectPath,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString(),
|
|
||||||
isArchived: false,
|
|
||||||
tags: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.saveMetadata(metadata);
|
|
||||||
|
|
||||||
this.sessions.set(sessionId, {
|
|
||||||
messages: [],
|
|
||||||
isRunning: false,
|
|
||||||
abortController: null,
|
|
||||||
workingDirectory: workingDirectory || projectPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.saveSession(sessionId, []);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
sessionId,
|
|
||||||
session: metadata[sessionId],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update session metadata
|
|
||||||
*/
|
|
||||||
async updateSession({ sessionId, name, tags }) {
|
|
||||||
const metadata = await this.loadMetadata();
|
|
||||||
|
|
||||||
if (!metadata[sessionId]) {
|
|
||||||
return { success: false, error: "Session not found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name !== undefined) metadata[sessionId].name = name;
|
|
||||||
if (tags !== undefined) metadata[sessionId].tags = tags;
|
|
||||||
metadata[sessionId].updatedAt = new Date().toISOString();
|
|
||||||
|
|
||||||
await this.saveMetadata(metadata);
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Archive a session
|
|
||||||
*/
|
|
||||||
async archiveSession(sessionId) {
|
|
||||||
const metadata = await this.loadMetadata();
|
|
||||||
|
|
||||||
if (!metadata[sessionId]) {
|
|
||||||
return { success: false, error: "Session not found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata[sessionId].isArchived = true;
|
|
||||||
metadata[sessionId].updatedAt = new Date().toISOString();
|
|
||||||
|
|
||||||
await this.saveMetadata(metadata);
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unarchive a session
|
|
||||||
*/
|
|
||||||
async unarchiveSession(sessionId) {
|
|
||||||
const metadata = await this.loadMetadata();
|
|
||||||
|
|
||||||
if (!metadata[sessionId]) {
|
|
||||||
return { success: false, error: "Session not found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata[sessionId].isArchived = false;
|
|
||||||
metadata[sessionId].updatedAt = new Date().toISOString();
|
|
||||||
|
|
||||||
await this.saveMetadata(metadata);
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a session permanently
|
|
||||||
*/
|
|
||||||
async deleteSession(sessionId) {
|
|
||||||
const metadata = await this.loadMetadata();
|
|
||||||
|
|
||||||
if (!metadata[sessionId]) {
|
|
||||||
return { success: false, error: "Session not found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from metadata
|
|
||||||
delete metadata[sessionId];
|
|
||||||
await this.saveMetadata(metadata);
|
|
||||||
|
|
||||||
// Remove from memory
|
|
||||||
this.sessions.delete(sessionId);
|
|
||||||
|
|
||||||
// Delete session file
|
|
||||||
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
|
||||||
try {
|
|
||||||
await fs.unlink(sessionFile);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("[AgentService] Failed to delete session file:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update session metadata when messages change
|
|
||||||
*/
|
|
||||||
async updateSessionTimestamp(sessionId) {
|
|
||||||
const metadata = await this.loadMetadata();
|
|
||||||
|
|
||||||
if (metadata[sessionId]) {
|
|
||||||
metadata[sessionId].updatedAt = new Date().toISOString();
|
|
||||||
await this.saveMetadata(metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export singleton instance
|
|
||||||
module.exports = new AgentService();
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,241 +0,0 @@
|
|||||||
/**
|
|
||||||
* Simplified Electron main process
|
|
||||||
*
|
|
||||||
* This version spawns the backend server and uses HTTP API for most operations.
|
|
||||||
* Only native features (dialogs, shell) use IPC.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const path = require("path");
|
|
||||||
const { spawn } = require("child_process");
|
|
||||||
|
|
||||||
// Load environment variables from .env file
|
|
||||||
require("dotenv").config({ path: path.join(__dirname, "../.env") });
|
|
||||||
|
|
||||||
const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
|
|
||||||
|
|
||||||
let mainWindow = null;
|
|
||||||
let serverProcess = null;
|
|
||||||
const SERVER_PORT = 3008;
|
|
||||||
|
|
||||||
// Get icon path - works in both dev and production
|
|
||||||
function getIconPath() {
|
|
||||||
return app.isPackaged
|
|
||||||
? path.join(process.resourcesPath, "app", "public", "logo.png")
|
|
||||||
: path.join(__dirname, "../public/logo.png");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the backend server
|
|
||||||
*/
|
|
||||||
async function startServer() {
|
|
||||||
const isDev = !app.isPackaged;
|
|
||||||
|
|
||||||
// Server entry point
|
|
||||||
const serverPath = isDev
|
|
||||||
? path.join(__dirname, "../../server/dist/index.js")
|
|
||||||
: path.join(process.resourcesPath, "server", "index.js");
|
|
||||||
|
|
||||||
// Set environment variables for server
|
|
||||||
const env = {
|
|
||||||
...process.env,
|
|
||||||
PORT: SERVER_PORT.toString(),
|
|
||||||
DATA_DIR: app.getPath("userData"),
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log("[Electron] Starting backend server...");
|
|
||||||
|
|
||||||
serverProcess = spawn("node", [serverPath], {
|
|
||||||
env,
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
|
|
||||||
serverProcess.stdout.on("data", (data) => {
|
|
||||||
console.log(`[Server] ${data.toString().trim()}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
serverProcess.stderr.on("data", (data) => {
|
|
||||||
console.error(`[Server Error] ${data.toString().trim()}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
serverProcess.on("close", (code) => {
|
|
||||||
console.log(`[Server] Process exited with code ${code}`);
|
|
||||||
serverProcess = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Wait for server to be ready
|
|
||||||
await waitForServer();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for server to be available
|
|
||||||
*/
|
|
||||||
async function waitForServer(maxAttempts = 30) {
|
|
||||||
const http = require("http");
|
|
||||||
|
|
||||||
for (let i = 0; i < maxAttempts; i++) {
|
|
||||||
try {
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
const req = http.get(`http://localhost:${SERVER_PORT}/api/health`, (res) => {
|
|
||||||
if (res.statusCode === 200) {
|
|
||||||
resolve();
|
|
||||||
} else {
|
|
||||||
reject(new Error(`Status: ${res.statusCode}`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
req.on("error", reject);
|
|
||||||
req.setTimeout(1000, () => {
|
|
||||||
req.destroy();
|
|
||||||
reject(new Error("Timeout"));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
console.log("[Electron] Server is ready");
|
|
||||||
return;
|
|
||||||
} catch {
|
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Server failed to start");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the main window
|
|
||||||
*/
|
|
||||||
function createWindow() {
|
|
||||||
mainWindow = new BrowserWindow({
|
|
||||||
width: 1400,
|
|
||||||
height: 900,
|
|
||||||
minWidth: 1024,
|
|
||||||
minHeight: 700,
|
|
||||||
icon: getIconPath(),
|
|
||||||
webPreferences: {
|
|
||||||
preload: path.join(__dirname, "preload-simplified.js"),
|
|
||||||
contextIsolation: true,
|
|
||||||
nodeIntegration: false,
|
|
||||||
},
|
|
||||||
titleBarStyle: "hiddenInset",
|
|
||||||
backgroundColor: "#0a0a0a",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Load Next.js dev server in development or production build
|
|
||||||
const isDev = !app.isPackaged;
|
|
||||||
if (isDev) {
|
|
||||||
mainWindow.loadURL("http://localhost:3007");
|
|
||||||
if (process.env.OPEN_DEVTOOLS === "true") {
|
|
||||||
mainWindow.webContents.openDevTools();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html"));
|
|
||||||
}
|
|
||||||
|
|
||||||
mainWindow.on("closed", () => {
|
|
||||||
mainWindow = null;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// App lifecycle
|
|
||||||
app.whenReady().then(async () => {
|
|
||||||
// Set app icon (dock icon on macOS)
|
|
||||||
if (process.platform === "darwin" && app.dock) {
|
|
||||||
app.dock.setIcon(getIconPath());
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Start backend server
|
|
||||||
await startServer();
|
|
||||||
|
|
||||||
// Create window
|
|
||||||
createWindow();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Electron] Failed to start:", error);
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.on("activate", () => {
|
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
|
||||||
createWindow();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("window-all-closed", () => {
|
|
||||||
if (process.platform !== "darwin") {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on("before-quit", () => {
|
|
||||||
// Kill server process
|
|
||||||
if (serverProcess) {
|
|
||||||
console.log("[Electron] Stopping server...");
|
|
||||||
serverProcess.kill();
|
|
||||||
serverProcess = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// ============================================
|
|
||||||
// IPC Handlers - Only native features
|
|
||||||
// ============================================
|
|
||||||
|
|
||||||
// Native file dialogs
|
|
||||||
ipcMain.handle("dialog:openDirectory", async () => {
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
|
||||||
properties: ["openDirectory", "createDirectory"],
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("dialog:openFile", async (_, options = {}) => {
|
|
||||||
const result = await dialog.showOpenDialog(mainWindow, {
|
|
||||||
properties: ["openFile"],
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("dialog:saveFile", async (_, options = {}) => {
|
|
||||||
const result = await dialog.showSaveDialog(mainWindow, options);
|
|
||||||
return result;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Shell operations
|
|
||||||
ipcMain.handle("shell:openExternal", async (_, url) => {
|
|
||||||
try {
|
|
||||||
await shell.openExternal(url);
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("shell:openPath", async (_, filePath) => {
|
|
||||||
try {
|
|
||||||
await shell.openPath(filePath);
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// App info
|
|
||||||
ipcMain.handle("app:getPath", async (_, name) => {
|
|
||||||
return app.getPath(name);
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("app:getVersion", async () => {
|
|
||||||
return app.getVersion();
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.handle("app:isPackaged", async () => {
|
|
||||||
return app.isPackaged;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Ping - for connection check
|
|
||||||
ipcMain.handle("ping", async () => {
|
|
||||||
return "pong";
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get server URL for HTTP client
|
|
||||||
ipcMain.handle("server:getUrl", async () => {
|
|
||||||
return `http://localhost:${SERVER_PORT}`;
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,21 @@
|
|||||||
|
/**
|
||||||
|
* Simplified Electron main process
|
||||||
|
*
|
||||||
|
* This version spawns the backend server and uses HTTP API for most operations.
|
||||||
|
* Only native features (dialogs, shell) use IPC.
|
||||||
|
*/
|
||||||
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const { app, BrowserWindow, shell } = require("electron");
|
const { spawn } = require("child_process");
|
||||||
|
|
||||||
|
// Load environment variables from .env file
|
||||||
|
require("dotenv").config({ path: path.join(__dirname, "../.env") });
|
||||||
|
|
||||||
|
const { app, BrowserWindow, ipcMain, dialog, shell } = require("electron");
|
||||||
|
|
||||||
let mainWindow = null;
|
let mainWindow = null;
|
||||||
|
let serverProcess = null;
|
||||||
|
const SERVER_PORT = 3008;
|
||||||
|
|
||||||
// Get icon path - works in both dev and production
|
// Get icon path - works in both dev and production
|
||||||
function getIconPath() {
|
function getIconPath() {
|
||||||
@@ -10,6 +24,83 @@ function getIconPath() {
|
|||||||
: path.join(__dirname, "../public/logo.png");
|
: path.join(__dirname, "../public/logo.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the backend server
|
||||||
|
*/
|
||||||
|
async function startServer() {
|
||||||
|
const isDev = !app.isPackaged;
|
||||||
|
|
||||||
|
// Server entry point
|
||||||
|
const serverPath = isDev
|
||||||
|
? path.join(__dirname, "../../server/dist/index.js")
|
||||||
|
: path.join(process.resourcesPath, "server", "index.js");
|
||||||
|
|
||||||
|
// Set environment variables for server
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
PORT: SERVER_PORT.toString(),
|
||||||
|
DATA_DIR: app.getPath("userData"),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("[Electron] Starting backend server...");
|
||||||
|
|
||||||
|
serverProcess = spawn("node", [serverPath], {
|
||||||
|
env,
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
});
|
||||||
|
|
||||||
|
serverProcess.stdout.on("data", (data) => {
|
||||||
|
console.log(`[Server] ${data.toString().trim()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
serverProcess.stderr.on("data", (data) => {
|
||||||
|
console.error(`[Server Error] ${data.toString().trim()}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
serverProcess.on("close", (code) => {
|
||||||
|
console.log(`[Server] Process exited with code ${code}`);
|
||||||
|
serverProcess = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for server to be ready
|
||||||
|
await waitForServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wait for server to be available
|
||||||
|
*/
|
||||||
|
async function waitForServer(maxAttempts = 30) {
|
||||||
|
const http = require("http");
|
||||||
|
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
try {
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const req = http.get(`http://localhost:${SERVER_PORT}/api/health`, (res) => {
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(new Error(`Status: ${res.statusCode}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
req.on("error", reject);
|
||||||
|
req.setTimeout(1000, () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error("Timeout"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
console.log("[Electron] Server is ready");
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
await new Promise((r) => setTimeout(r, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Server failed to start");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the main window
|
||||||
|
*/
|
||||||
function createWindow() {
|
function createWindow() {
|
||||||
mainWindow = new BrowserWindow({
|
mainWindow = new BrowserWindow({
|
||||||
width: 1400,
|
width: 1400,
|
||||||
@@ -30,7 +121,6 @@ function createWindow() {
|
|||||||
const isDev = !app.isPackaged;
|
const isDev = !app.isPackaged;
|
||||||
if (isDev) {
|
if (isDev) {
|
||||||
mainWindow.loadURL("http://localhost:3007");
|
mainWindow.loadURL("http://localhost:3007");
|
||||||
// Open DevTools if OPEN_DEVTOOLS environment variable is set
|
|
||||||
if (process.env.OPEN_DEVTOOLS === "true") {
|
if (process.env.OPEN_DEVTOOLS === "true") {
|
||||||
mainWindow.webContents.openDevTools();
|
mainWindow.webContents.openDevTools();
|
||||||
}
|
}
|
||||||
@@ -38,24 +128,34 @@ function createWindow() {
|
|||||||
mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html"));
|
mainWindow.loadFile(path.join(__dirname, "../.next/server/app/index.html"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mainWindow.on("closed", () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
|
||||||
// Handle external links - open in default browser
|
// Handle external links - open in default browser
|
||||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
shell.openExternal(url);
|
shell.openExternal(url);
|
||||||
return { action: "deny" };
|
return { action: "deny" };
|
||||||
});
|
});
|
||||||
|
|
||||||
mainWindow.on("closed", () => {
|
|
||||||
mainWindow = null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
// App lifecycle
|
||||||
|
app.whenReady().then(async () => {
|
||||||
// Set app icon (dock icon on macOS)
|
// Set app icon (dock icon on macOS)
|
||||||
if (process.platform === "darwin" && app.dock) {
|
if (process.platform === "darwin" && app.dock) {
|
||||||
app.dock.setIcon(getIconPath());
|
app.dock.setIcon(getIconPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
createWindow();
|
try {
|
||||||
|
// Start backend server
|
||||||
|
await startServer();
|
||||||
|
|
||||||
|
// Create window
|
||||||
|
createWindow();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[Electron] Failed to start:", error);
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
app.on("activate", () => {
|
app.on("activate", () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
@@ -69,3 +169,79 @@ app.on("window-all-closed", () => {
|
|||||||
app.quit();
|
app.quit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
// Kill server process
|
||||||
|
if (serverProcess) {
|
||||||
|
console.log("[Electron] Stopping server...");
|
||||||
|
serverProcess.kill();
|
||||||
|
serverProcess = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================
|
||||||
|
// IPC Handlers - Only native features
|
||||||
|
// ============================================
|
||||||
|
|
||||||
|
// Native file dialogs
|
||||||
|
ipcMain.handle("dialog:openDirectory", async () => {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
properties: ["openDirectory", "createDirectory"],
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("dialog:openFile", async (_, options = {}) => {
|
||||||
|
const result = await dialog.showOpenDialog(mainWindow, {
|
||||||
|
properties: ["openFile"],
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("dialog:saveFile", async (_, options = {}) => {
|
||||||
|
const result = await dialog.showSaveDialog(mainWindow, options);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Shell operations
|
||||||
|
ipcMain.handle("shell:openExternal", async (_, url) => {
|
||||||
|
try {
|
||||||
|
await shell.openExternal(url);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("shell:openPath", async (_, filePath) => {
|
||||||
|
try {
|
||||||
|
await shell.openPath(filePath);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// App info
|
||||||
|
ipcMain.handle("app:getPath", async (_, name) => {
|
||||||
|
return app.getPath(name);
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("app:getVersion", async () => {
|
||||||
|
return app.getVersion();
|
||||||
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("app:isPackaged", async () => {
|
||||||
|
return app.isPackaged;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ping - for connection check
|
||||||
|
ipcMain.handle("ping", async () => {
|
||||||
|
return "pong";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get server URL for HTTP client
|
||||||
|
ipcMain.handle("server:getUrl", async () => {
|
||||||
|
return `http://localhost:${SERVER_PORT}`;
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
/**
|
|
||||||
* Simplified Electron preload script
|
|
||||||
*
|
|
||||||
* Only exposes native features (dialogs, shell) and server URL.
|
|
||||||
* All other operations go through HTTP API.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { contextBridge, ipcRenderer } = require("electron");
|
|
||||||
|
|
||||||
// Expose minimal API for native features
|
|
||||||
contextBridge.exposeInMainWorld("electronAPI", {
|
|
||||||
// Platform info
|
|
||||||
platform: process.platform,
|
|
||||||
isElectron: true,
|
|
||||||
|
|
||||||
// Connection check
|
|
||||||
ping: () => ipcRenderer.invoke("ping"),
|
|
||||||
|
|
||||||
// Get server URL for HTTP client
|
|
||||||
getServerUrl: () => ipcRenderer.invoke("server:getUrl"),
|
|
||||||
|
|
||||||
// Native dialogs - better UX than prompt()
|
|
||||||
openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"),
|
|
||||||
openFile: (options) => ipcRenderer.invoke("dialog:openFile", options),
|
|
||||||
saveFile: (options) => ipcRenderer.invoke("dialog:saveFile", options),
|
|
||||||
|
|
||||||
// Shell operations
|
|
||||||
openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url),
|
|
||||||
openPath: (filePath) => ipcRenderer.invoke("shell:openPath", filePath),
|
|
||||||
|
|
||||||
// App info
|
|
||||||
getPath: (name) => ipcRenderer.invoke("app:getPath", name),
|
|
||||||
getVersion: () => ipcRenderer.invoke("app:getVersion"),
|
|
||||||
isPackaged: () => ipcRenderer.invoke("app:isPackaged"),
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("[Preload] Electron API exposed (simplified mode)");
|
|
||||||
@@ -1,10 +1,37 @@
|
|||||||
const { contextBridge } = require("electron");
|
/**
|
||||||
|
* Simplified Electron preload script
|
||||||
|
*
|
||||||
|
* Only exposes native features (dialogs, shell) and server URL.
|
||||||
|
* All other operations go through HTTP API.
|
||||||
|
*/
|
||||||
|
|
||||||
// Only expose a flag to detect Electron environment
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
// All API calls go through HTTP to the backend server
|
|
||||||
contextBridge.exposeInMainWorld("isElectron", true);
|
|
||||||
|
|
||||||
// Expose platform info for UI purposes
|
// Expose minimal API for native features
|
||||||
contextBridge.exposeInMainWorld("electronPlatform", process.platform);
|
contextBridge.exposeInMainWorld("electronAPI", {
|
||||||
|
// Platform info
|
||||||
|
platform: process.platform,
|
||||||
|
isElectron: true,
|
||||||
|
|
||||||
console.log("[Preload] Electron flag exposed (HTTP-only mode)");
|
// Connection check
|
||||||
|
ping: () => ipcRenderer.invoke("ping"),
|
||||||
|
|
||||||
|
// Get server URL for HTTP client
|
||||||
|
getServerUrl: () => ipcRenderer.invoke("server:getUrl"),
|
||||||
|
|
||||||
|
// Native dialogs - better UX than prompt()
|
||||||
|
openDirectory: () => ipcRenderer.invoke("dialog:openDirectory"),
|
||||||
|
openFile: (options) => ipcRenderer.invoke("dialog:openFile", options),
|
||||||
|
saveFile: (options) => ipcRenderer.invoke("dialog:saveFile", options),
|
||||||
|
|
||||||
|
// Shell operations
|
||||||
|
openExternalLink: (url) => ipcRenderer.invoke("shell:openExternal", url),
|
||||||
|
openPath: (filePath) => ipcRenderer.invoke("shell:openPath", filePath),
|
||||||
|
|
||||||
|
// App info
|
||||||
|
getPath: (name) => ipcRenderer.invoke("app:getPath", name),
|
||||||
|
getVersion: () => ipcRenderer.invoke("app:getVersion"),
|
||||||
|
isPackaged: () => ipcRenderer.invoke("app:isPackaged"),
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[Preload] Electron API exposed (simplified mode)");
|
||||||
|
|||||||
@@ -1,721 +0,0 @@
|
|||||||
const { execSync, spawn } = require("child_process");
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const os = require("os");
|
|
||||||
|
|
||||||
let runPtyCommand = null;
|
|
||||||
try {
|
|
||||||
({ runPtyCommand } = require("./pty-runner"));
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
"[ClaudeCliDetector] node-pty unavailable, will fall back to external terminal:",
|
|
||||||
error?.message || error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const ANSI_REGEX =
|
|
||||||
// eslint-disable-next-line no-control-regex
|
|
||||||
/\u001b\[[0-9;?]*[ -/]*[@-~]|\u001b[@-_]|\u001b\][^\u0007]*\u0007/g;
|
|
||||||
|
|
||||||
const stripAnsi = (text = "") => text.replace(ANSI_REGEX, "");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Claude CLI Detector
|
|
||||||
*
|
|
||||||
* Authentication options:
|
|
||||||
* 1. OAuth Token (Subscription): User runs `claude setup-token` and provides the token to the app
|
|
||||||
* 2. API Key (Pay-per-use): User provides their Anthropic API key directly
|
|
||||||
*/
|
|
||||||
class ClaudeCliDetector {
|
|
||||||
/**
|
|
||||||
* Check if Claude Code CLI is installed and accessible
|
|
||||||
* @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'none' }
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Try to get updated PATH from shell config files
|
|
||||||
* This helps detect CLI installations that modify shell config but haven't updated the current process PATH
|
|
||||||
*/
|
|
||||||
static getUpdatedPathFromShellConfig() {
|
|
||||||
const homeDir = os.homedir();
|
|
||||||
const shell = process.env.SHELL || "/bin/bash";
|
|
||||||
const shellName = path.basename(shell);
|
|
||||||
|
|
||||||
const configFiles = [];
|
|
||||||
if (shellName.includes("zsh")) {
|
|
||||||
configFiles.push(path.join(homeDir, ".zshrc"));
|
|
||||||
configFiles.push(path.join(homeDir, ".zshenv"));
|
|
||||||
configFiles.push(path.join(homeDir, ".zprofile"));
|
|
||||||
} else if (shellName.includes("bash")) {
|
|
||||||
configFiles.push(path.join(homeDir, ".bashrc"));
|
|
||||||
configFiles.push(path.join(homeDir, ".bash_profile"));
|
|
||||||
configFiles.push(path.join(homeDir, ".profile"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const commonPaths = [
|
|
||||||
path.join(homeDir, ".local", "bin"),
|
|
||||||
path.join(homeDir, ".cargo", "bin"),
|
|
||||||
"/usr/local/bin",
|
|
||||||
"/opt/homebrew/bin",
|
|
||||||
path.join(homeDir, "bin"),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const configFile of configFiles) {
|
|
||||||
if (fs.existsSync(configFile)) {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(configFile, "utf-8");
|
|
||||||
const pathMatches = content.match(
|
|
||||||
/export\s+PATH=["']?([^"'\n]+)["']?/g
|
|
||||||
);
|
|
||||||
if (pathMatches) {
|
|
||||||
for (const match of pathMatches) {
|
|
||||||
const pathValue = match
|
|
||||||
.replace(/export\s+PATH=["']?/, "")
|
|
||||||
.replace(/["']?$/, "");
|
|
||||||
const paths = pathValue
|
|
||||||
.split(":")
|
|
||||||
.filter((p) => p && !p.includes("$"));
|
|
||||||
commonPaths.push(...paths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore errors reading config files
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [...new Set(commonPaths)];
|
|
||||||
}
|
|
||||||
|
|
||||||
static detectClaudeInstallation() {
|
|
||||||
try {
|
|
||||||
// Check if 'claude' command is in PATH (Unix)
|
|
||||||
if (process.platform !== "win32") {
|
|
||||||
try {
|
|
||||||
const claudePath = execSync("which claude 2>/dev/null", {
|
|
||||||
encoding: "utf-8",
|
|
||||||
}).trim();
|
|
||||||
if (claudePath) {
|
|
||||||
const version = this.getClaudeVersion(claudePath);
|
|
||||||
return {
|
|
||||||
installed: true,
|
|
||||||
path: claudePath,
|
|
||||||
version: version,
|
|
||||||
method: "cli",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// CLI not in PATH
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Windows path
|
|
||||||
if (process.platform === "win32") {
|
|
||||||
try {
|
|
||||||
const claudePath = execSync("where claude 2>nul", {
|
|
||||||
encoding: "utf-8",
|
|
||||||
})
|
|
||||||
.trim()
|
|
||||||
.split("\n")[0];
|
|
||||||
if (claudePath) {
|
|
||||||
const version = this.getClaudeVersion(claudePath);
|
|
||||||
return {
|
|
||||||
installed: true,
|
|
||||||
path: claudePath,
|
|
||||||
version: version,
|
|
||||||
method: "cli",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Not found on Windows
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for local installation
|
|
||||||
const localClaudePath = path.join(
|
|
||||||
os.homedir(),
|
|
||||||
".claude",
|
|
||||||
"local",
|
|
||||||
"claude"
|
|
||||||
);
|
|
||||||
if (fs.existsSync(localClaudePath)) {
|
|
||||||
const version = this.getClaudeVersion(localClaudePath);
|
|
||||||
return {
|
|
||||||
installed: true,
|
|
||||||
path: localClaudePath,
|
|
||||||
version: version,
|
|
||||||
method: "cli-local",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check common installation locations
|
|
||||||
const commonPaths = this.getUpdatedPathFromShellConfig();
|
|
||||||
const binaryNames = ["claude", "claude-code"];
|
|
||||||
|
|
||||||
for (const basePath of commonPaths) {
|
|
||||||
for (const binaryName of binaryNames) {
|
|
||||||
const claudePath = path.join(basePath, binaryName);
|
|
||||||
if (fs.existsSync(claudePath)) {
|
|
||||||
try {
|
|
||||||
const version = this.getClaudeVersion(claudePath);
|
|
||||||
return {
|
|
||||||
installed: true,
|
|
||||||
path: claudePath,
|
|
||||||
version: version,
|
|
||||||
method: "cli",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
// File exists but can't get version
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to source shell config and check PATH again (Unix)
|
|
||||||
if (process.platform !== "win32") {
|
|
||||||
try {
|
|
||||||
const shell = process.env.SHELL || "/bin/bash";
|
|
||||||
const shellName = path.basename(shell);
|
|
||||||
const homeDir = os.homedir();
|
|
||||||
|
|
||||||
let sourceCmd = "";
|
|
||||||
if (shellName.includes("zsh")) {
|
|
||||||
sourceCmd = `source ${homeDir}/.zshrc 2>/dev/null && which claude`;
|
|
||||||
} else if (shellName.includes("bash")) {
|
|
||||||
sourceCmd = `source ${homeDir}/.bashrc 2>/dev/null && which claude`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceCmd) {
|
|
||||||
const claudePath = execSync(`bash -c "${sourceCmd}"`, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
timeout: 2000,
|
|
||||||
}).trim();
|
|
||||||
if (claudePath && claudePath.startsWith("/")) {
|
|
||||||
const version = this.getClaudeVersion(claudePath);
|
|
||||||
return {
|
|
||||||
installed: true,
|
|
||||||
path: claudePath,
|
|
||||||
version: version,
|
|
||||||
method: "cli",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Failed to source shell config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
installed: false,
|
|
||||||
path: null,
|
|
||||||
version: null,
|
|
||||||
method: "none",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
installed: false,
|
|
||||||
path: null,
|
|
||||||
version: null,
|
|
||||||
method: "none",
|
|
||||||
error: error.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Claude CLI version
|
|
||||||
* @param {string} claudePath Path to claude executable
|
|
||||||
* @returns {string|null} Version string or null
|
|
||||||
*/
|
|
||||||
static getClaudeVersion(claudePath) {
|
|
||||||
try {
|
|
||||||
const version = execSync(`"${claudePath}" --version 2>/dev/null`, {
|
|
||||||
encoding: "utf-8",
|
|
||||||
timeout: 5000,
|
|
||||||
}).trim();
|
|
||||||
return version || null;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get authentication status
|
|
||||||
* Checks for:
|
|
||||||
* 1. OAuth token stored in app's credentials (from `claude setup-token`)
|
|
||||||
* 2. API key stored in app's credentials
|
|
||||||
* 3. API key in environment variable
|
|
||||||
*
|
|
||||||
* @param {string} appCredentialsPath Path to app's credentials.json
|
|
||||||
* @returns {Object} Authentication status
|
|
||||||
*/
|
|
||||||
static getAuthStatus(appCredentialsPath) {
|
|
||||||
const envApiKey = process.env.ANTHROPIC_API_KEY;
|
|
||||||
const envOAuthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
||||||
|
|
||||||
let storedOAuthToken = null;
|
|
||||||
let storedApiKey = null;
|
|
||||||
|
|
||||||
if (appCredentialsPath && fs.existsSync(appCredentialsPath)) {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(appCredentialsPath, "utf-8");
|
|
||||||
const credentials = JSON.parse(content);
|
|
||||||
storedOAuthToken = credentials.anthropic_oauth_token || null;
|
|
||||||
storedApiKey =
|
|
||||||
credentials.anthropic || credentials.anthropic_api_key || null;
|
|
||||||
} catch (error) {
|
|
||||||
// Ignore credential read errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Authentication priority (highest to lowest):
|
|
||||||
// 1. Environment OAuth Token (CLAUDE_CODE_OAUTH_TOKEN)
|
|
||||||
// 2. Stored OAuth Token (from credentials file)
|
|
||||||
// 3. Stored API Key (from credentials file)
|
|
||||||
// 4. Environment API Key (ANTHROPIC_API_KEY)
|
|
||||||
let authenticated = false;
|
|
||||||
let method = "none";
|
|
||||||
|
|
||||||
if (envOAuthToken) {
|
|
||||||
authenticated = true;
|
|
||||||
method = "oauth_token_env";
|
|
||||||
} else if (storedOAuthToken) {
|
|
||||||
authenticated = true;
|
|
||||||
method = "oauth_token";
|
|
||||||
} else if (storedApiKey) {
|
|
||||||
authenticated = true;
|
|
||||||
method = "api_key";
|
|
||||||
} else if (envApiKey) {
|
|
||||||
authenticated = true;
|
|
||||||
method = "api_key_env";
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authenticated,
|
|
||||||
method,
|
|
||||||
hasStoredOAuthToken: !!storedOAuthToken,
|
|
||||||
hasStoredApiKey: !!storedApiKey,
|
|
||||||
hasEnvApiKey: !!envApiKey,
|
|
||||||
hasEnvOAuthToken: !!envOAuthToken,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Get installation info (installation status only, no auth)
|
|
||||||
* @returns {Object} Installation info with status property
|
|
||||||
*/
|
|
||||||
static getInstallationInfo() {
|
|
||||||
const installation = this.detectClaudeInstallation();
|
|
||||||
return {
|
|
||||||
status: installation.installed ? "installed" : "not_installed",
|
|
||||||
installed: installation.installed,
|
|
||||||
path: installation.path,
|
|
||||||
version: installation.version,
|
|
||||||
method: installation.method,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get full status including installation and auth
|
|
||||||
* @param {string} appCredentialsPath Path to app's credentials.json
|
|
||||||
* @returns {Object} Full status
|
|
||||||
*/
|
|
||||||
static getFullStatus(appCredentialsPath) {
|
|
||||||
const installation = this.detectClaudeInstallation();
|
|
||||||
const auth = this.getAuthStatus(appCredentialsPath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
status: installation.installed ? "installed" : "not_installed",
|
|
||||||
installed: installation.installed,
|
|
||||||
path: installation.path,
|
|
||||||
version: installation.version,
|
|
||||||
method: installation.method,
|
|
||||||
auth,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get installation info and recommendations
|
|
||||||
* @returns {Object} Installation status and recommendations
|
|
||||||
*/
|
|
||||||
static getInstallationInfo() {
|
|
||||||
const detection = this.detectClaudeInstallation();
|
|
||||||
|
|
||||||
if (detection.installed) {
|
|
||||||
return {
|
|
||||||
status: 'installed',
|
|
||||||
method: detection.method,
|
|
||||||
version: detection.version,
|
|
||||||
path: detection.path,
|
|
||||||
recommendation: 'Claude Code CLI is ready for ultrathink'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'not_installed',
|
|
||||||
recommendation: 'Install Claude Code CLI for optimal ultrathink performance',
|
|
||||||
installCommands: this.getInstallCommands()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get installation commands for different platforms
|
|
||||||
* @returns {Object} Installation commands
|
|
||||||
*/
|
|
||||||
static getInstallCommands() {
|
|
||||||
return {
|
|
||||||
macos: "curl -fsSL https://claude.ai/install.sh | bash",
|
|
||||||
windows: "irm https://claude.ai/install.ps1 | iex",
|
|
||||||
linux: "curl -fsSL https://claude.ai/install.sh | bash",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install Claude CLI using the official script
|
|
||||||
* @param {Function} onProgress Callback for progress updates
|
|
||||||
* @returns {Promise<Object>} Installation result
|
|
||||||
*/
|
|
||||||
static async installCli(onProgress) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const platform = process.platform;
|
|
||||||
let command, args;
|
|
||||||
|
|
||||||
if (platform === "win32") {
|
|
||||||
command = "powershell";
|
|
||||||
args = ["-Command", "irm https://claude.ai/install.ps1 | iex"];
|
|
||||||
} else {
|
|
||||||
command = "bash";
|
|
||||||
args = ["-c", "curl -fsSL https://claude.ai/install.sh | bash"];
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("[ClaudeCliDetector] Installing Claude CLI...");
|
|
||||||
|
|
||||||
const proc = spawn(command, args, {
|
|
||||||
stdio: ["pipe", "pipe", "pipe"],
|
|
||||||
shell: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
let output = "";
|
|
||||||
let errorOutput = "";
|
|
||||||
|
|
||||||
proc.stdout.on("data", (data) => {
|
|
||||||
const text = data.toString();
|
|
||||||
output += text;
|
|
||||||
if (onProgress) {
|
|
||||||
onProgress({ type: "stdout", data: text });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.stderr.on("data", (data) => {
|
|
||||||
const text = data.toString();
|
|
||||||
errorOutput += text;
|
|
||||||
if (onProgress) {
|
|
||||||
onProgress({ type: "stderr", data: text });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on("close", (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
console.log(
|
|
||||||
"[ClaudeCliDetector] Installation completed successfully"
|
|
||||||
);
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
output,
|
|
||||||
message: "Claude CLI installed successfully",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
"[ClaudeCliDetector] Installation failed with code:",
|
|
||||||
code
|
|
||||||
);
|
|
||||||
reject({
|
|
||||||
success: false,
|
|
||||||
error: errorOutput || `Installation failed with code ${code}`,
|
|
||||||
output,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on("error", (error) => {
|
|
||||||
console.error("[ClaudeCliDetector] Installation error:", error);
|
|
||||||
reject({
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
output,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get instructions for setup-token command
|
|
||||||
* @returns {Object} Setup token instructions
|
|
||||||
*/
|
|
||||||
static getSetupTokenInstructions() {
|
|
||||||
const detection = this.detectClaudeInstallation();
|
|
||||||
|
|
||||||
if (!detection.installed) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: "Claude CLI is not installed. Please install it first.",
|
|
||||||
installCommands: this.getInstallCommands(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
command: "claude setup-token",
|
|
||||||
instructions: [
|
|
||||||
"1. Open your terminal",
|
|
||||||
"2. Run: claude setup-token",
|
|
||||||
"3. Follow the prompts to authenticate",
|
|
||||||
"4. Copy the token that is displayed",
|
|
||||||
"5. Paste the token in the field below",
|
|
||||||
],
|
|
||||||
note: "This token is from your Claude subscription and allows you to use Claude without API charges.",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract OAuth token from command output
|
|
||||||
* Tries multiple patterns to find the token
|
|
||||||
* @param {string} output The command output
|
|
||||||
* @returns {string|null} Extracted token or null
|
|
||||||
*/
|
|
||||||
static extractTokenFromOutput(output) {
|
|
||||||
// Pattern 1: CLAUDE_CODE_OAUTH_TOKEN=<token> or CLAUDE_CODE_OAUTH_TOKEN: <token>
|
|
||||||
const envMatch = output.match(
|
|
||||||
/CLAUDE_CODE_OAUTH_TOKEN[=:]\s*["']?([a-zA-Z0-9_\-\.]+)["']?/i
|
|
||||||
);
|
|
||||||
if (envMatch) return envMatch[1];
|
|
||||||
|
|
||||||
// Pattern 2: "Token: <token>" or "token: <token>"
|
|
||||||
const tokenLabelMatch = output.match(
|
|
||||||
/\btoken[:\s]+["']?([a-zA-Z0-9_\-\.]{40,})["']?/i
|
|
||||||
);
|
|
||||||
if (tokenLabelMatch) return tokenLabelMatch[1];
|
|
||||||
|
|
||||||
// Pattern 3: Look for token after success/authenticated message
|
|
||||||
const successMatch = output.match(
|
|
||||||
/(?:success|authenticated|generated|token is)[^\n]*\n\s*([a-zA-Z0-9_\-\.]{40,})/i
|
|
||||||
);
|
|
||||||
if (successMatch) return successMatch[1];
|
|
||||||
|
|
||||||
// Pattern 4: Standalone long alphanumeric string on its own line (last resort)
|
|
||||||
// This catches tokens that are printed on their own line
|
|
||||||
const lines = output.split("\n");
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
// Token should be 40+ chars, alphanumeric with possible hyphens/underscores/dots
|
|
||||||
if (/^[a-zA-Z0-9_\-\.]{40,}$/.test(trimmed)) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Run claude setup-token command to generate OAuth token
|
|
||||||
* Opens an external terminal window since Claude CLI requires TTY for its Ink-based UI
|
|
||||||
* @param {Function} onProgress Callback for progress updates
|
|
||||||
* @returns {Promise<Object>} Result indicating terminal was opened
|
|
||||||
*/
|
|
||||||
static async runSetupToken(onProgress) {
|
|
||||||
const detection = this.detectClaudeInstallation();
|
|
||||||
|
|
||||||
if (!detection.installed) {
|
|
||||||
throw {
|
|
||||||
success: false,
|
|
||||||
error: "Claude CLI is not installed. Please install it first.",
|
|
||||||
requiresManualAuth: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const claudePath = detection.path;
|
|
||||||
const platform = process.platform;
|
|
||||||
const preferPty =
|
|
||||||
(platform === "win32" ||
|
|
||||||
platform === "darwin" ||
|
|
||||||
process.env.CLAUDE_AUTH_FORCE_PTY === "1") &&
|
|
||||||
process.env.CLAUDE_AUTH_DISABLE_PTY !== "1";
|
|
||||||
|
|
||||||
const send = (data) => {
|
|
||||||
if (onProgress && data) {
|
|
||||||
onProgress({ type: "stdout", data });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (preferPty && runPtyCommand) {
|
|
||||||
try {
|
|
||||||
send("Starting in-app terminal session for Claude auth...\n");
|
|
||||||
send("If your browser opens, complete sign-in and return here.\n\n");
|
|
||||||
|
|
||||||
const ptyResult = await runPtyCommand(claudePath, ["setup-token"], {
|
|
||||||
cols: 120,
|
|
||||||
rows: 30,
|
|
||||||
onData: (chunk) => send(chunk),
|
|
||||||
env: {
|
|
||||||
FORCE_COLOR: "1",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const cleanedOutput = stripAnsi(ptyResult.output || "");
|
|
||||||
const token = this.extractTokenFromOutput(cleanedOutput);
|
|
||||||
|
|
||||||
if (ptyResult.success && token) {
|
|
||||||
send("\nCaptured token automatically.\n");
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
token,
|
|
||||||
requiresManualAuth: false,
|
|
||||||
terminalOpened: false,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ptyResult.success && !token) {
|
|
||||||
send(
|
|
||||||
"\nCLI completed but token was not detected automatically. You can copy it above or retry.\n"
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
requiresManualAuth: true,
|
|
||||||
terminalOpened: false,
|
|
||||||
error: "Could not capture token automatically",
|
|
||||||
output: cleanedOutput,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
send(
|
|
||||||
`\nClaude CLI exited with code ${ptyResult.exitCode}. Falling back to manual copy.\n`
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: `Claude CLI exited with code ${ptyResult.exitCode}`,
|
|
||||||
requiresManualAuth: true,
|
|
||||||
output: cleanedOutput,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[ClaudeCliDetector] PTY auth failed, falling back:", error);
|
|
||||||
send(
|
|
||||||
`In-app terminal failed (${error?.message || "unknown error"}). Falling back to external terminal...\n`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: external terminal window
|
|
||||||
if (preferPty && !runPtyCommand) {
|
|
||||||
send("In-app terminal unavailable (node-pty not loaded).");
|
|
||||||
} else if (!preferPty) {
|
|
||||||
send("Using system terminal for authentication on this platform.");
|
|
||||||
}
|
|
||||||
send("Opening system terminal for authentication...\n");
|
|
||||||
|
|
||||||
// Helper function to check if a command exists asynchronously
|
|
||||||
const commandExists = (cmd) => {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
require("child_process").exec(
|
|
||||||
`which ${cmd}`,
|
|
||||||
{ timeout: 1000 },
|
|
||||||
(error) => {
|
|
||||||
resolve(!error);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// For Linux, find available terminal first (async)
|
|
||||||
let linuxTerminal = null;
|
|
||||||
if (platform !== "win32" && platform !== "darwin") {
|
|
||||||
const terminals = [
|
|
||||||
["gnome-terminal", ["--", claudePath, "setup-token"]],
|
|
||||||
["konsole", ["-e", claudePath, "setup-token"]],
|
|
||||||
["xterm", ["-e", claudePath, "setup-token"]],
|
|
||||||
["x-terminal-emulator", ["-e", `${claudePath} setup-token`]],
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const [term, termArgs] of terminals) {
|
|
||||||
const exists = await commandExists(term);
|
|
||||||
if (exists) {
|
|
||||||
linuxTerminal = { command: term, args: termArgs };
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
// Open command in external terminal since Claude CLI requires TTY
|
|
||||||
let command, args;
|
|
||||||
|
|
||||||
if (platform === "win32") {
|
|
||||||
// Windows: Open new cmd window that stays open
|
|
||||||
command = "cmd";
|
|
||||||
args = ["/c", "start", "cmd", "/k", `"${claudePath}" setup-token`];
|
|
||||||
} else if (platform === "darwin") {
|
|
||||||
// macOS: Open Terminal.app
|
|
||||||
command = "osascript";
|
|
||||||
args = [
|
|
||||||
"-e",
|
|
||||||
`tell application "Terminal" to do script "${claudePath} setup-token"`,
|
|
||||||
"-e",
|
|
||||||
'tell application "Terminal" to activate',
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
// Linux: Use the terminal we found earlier
|
|
||||||
if (!linuxTerminal) {
|
|
||||||
reject({
|
|
||||||
success: false,
|
|
||||||
error:
|
|
||||||
"Could not find a terminal emulator. Please run 'claude setup-token' manually in your terminal.",
|
|
||||||
requiresManualAuth: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
command = linuxTerminal.command;
|
|
||||||
args = linuxTerminal.args;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
"[ClaudeCliDetector] Spawning terminal:",
|
|
||||||
command,
|
|
||||||
args.join(" ")
|
|
||||||
);
|
|
||||||
|
|
||||||
const proc = spawn(command, args, {
|
|
||||||
detached: true,
|
|
||||||
stdio: "ignore",
|
|
||||||
shell: platform === "win32",
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.unref();
|
|
||||||
|
|
||||||
proc.on("error", (error) => {
|
|
||||||
console.error("[ClaudeCliDetector] Failed to open terminal:", error);
|
|
||||||
reject({
|
|
||||||
success: false,
|
|
||||||
error: `Failed to open terminal: ${error.message}`,
|
|
||||||
requiresManualAuth: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Give the terminal a moment to open
|
|
||||||
setTimeout(() => {
|
|
||||||
send("Terminal window opened!\n\n");
|
|
||||||
send("1. Complete the sign-in in your browser\n");
|
|
||||||
send("2. Copy the token from the terminal\n");
|
|
||||||
send("3. Paste it below\n");
|
|
||||||
|
|
||||||
// Resolve with manual auth required since we can't capture from external terminal
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
requiresManualAuth: true,
|
|
||||||
terminalOpened: true,
|
|
||||||
message:
|
|
||||||
"Terminal opened. Complete authentication and paste the token below.",
|
|
||||||
});
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = ClaudeCliDetector;
|
|
||||||
@@ -1,566 +0,0 @@
|
|||||||
const { execSync, spawn } = require('child_process');
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const os = require('os');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Codex CLI Detector - Checks if OpenAI Codex CLI is installed
|
|
||||||
*
|
|
||||||
* Codex CLI is OpenAI's agent CLI tool that allows users to use
|
|
||||||
* GPT-5.1 Codex models (gpt-5.1-codex-max, gpt-5.1-codex, etc.)
|
|
||||||
* for code generation and agentic tasks.
|
|
||||||
*/
|
|
||||||
class CodexCliDetector {
|
|
||||||
/**
|
|
||||||
* Get the path to Codex config directory
|
|
||||||
* @returns {string} Path to .codex directory
|
|
||||||
*/
|
|
||||||
static getConfigDir() {
|
|
||||||
return path.join(os.homedir(), '.codex');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the path to Codex auth file
|
|
||||||
* @returns {string} Path to auth.json
|
|
||||||
*/
|
|
||||||
static getAuthPath() {
|
|
||||||
return path.join(this.getConfigDir(), 'auth.json');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check Codex authentication status
|
|
||||||
* @returns {Object} Authentication status
|
|
||||||
*/
|
|
||||||
static checkAuth() {
|
|
||||||
try {
|
|
||||||
const authPath = this.getAuthPath();
|
|
||||||
const envApiKey = process.env.OPENAI_API_KEY;
|
|
||||||
|
|
||||||
// Try to verify authentication using codex CLI command if available
|
|
||||||
try {
|
|
||||||
const detection = this.detectCodexInstallation();
|
|
||||||
if (detection.installed) {
|
|
||||||
try {
|
|
||||||
const statusOutput = execSync(`"${detection.path || 'codex'}" login status 2>/dev/null`, {
|
|
||||||
encoding: 'utf-8',
|
|
||||||
timeout: 5000
|
|
||||||
});
|
|
||||||
|
|
||||||
if (statusOutput && (statusOutput.includes('Logged in') || statusOutput.includes('Authenticated'))) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
method: 'cli_verified',
|
|
||||||
hasAuthFile: fs.existsSync(authPath),
|
|
||||||
hasEnvKey: !!envApiKey,
|
|
||||||
authPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (statusError) {
|
|
||||||
// status command failed, continue with file-based check
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (verifyError) {
|
|
||||||
// CLI verification failed, continue with file-based check
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if auth file exists
|
|
||||||
if (fs.existsSync(authPath)) {
|
|
||||||
let auth = null;
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(authPath, 'utf-8');
|
|
||||||
auth = JSON.parse(content);
|
|
||||||
|
|
||||||
// Check for token object structure
|
|
||||||
if (auth.token && typeof auth.token === 'object') {
|
|
||||||
const token = auth.token;
|
|
||||||
if (token.Id_token || token.access_token || token.refresh_token || token.id_token) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
method: 'cli_tokens',
|
|
||||||
hasAuthFile: true,
|
|
||||||
hasEnvKey: !!envApiKey,
|
|
||||||
authPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for tokens at root level
|
|
||||||
if (auth.access_token || auth.refresh_token || auth.Id_token || auth.id_token) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
method: 'cli_tokens',
|
|
||||||
hasAuthFile: true,
|
|
||||||
hasEnvKey: !!envApiKey,
|
|
||||||
authPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for API key fields
|
|
||||||
if (auth.api_key || auth.openai_api_key || auth.apiKey) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
method: 'auth_file',
|
|
||||||
hasAuthFile: true,
|
|
||||||
hasEnvKey: !!envApiKey,
|
|
||||||
authPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
method: 'none',
|
|
||||||
hasAuthFile: false,
|
|
||||||
hasEnvKey: !!envApiKey,
|
|
||||||
authPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!auth) {
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
method: 'none',
|
|
||||||
hasAuthFile: true,
|
|
||||||
hasEnvKey: !!envApiKey,
|
|
||||||
authPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const keys = Object.keys(auth);
|
|
||||||
if (keys.length > 0) {
|
|
||||||
const hasTokens = keys.some(key =>
|
|
||||||
key.toLowerCase().includes('token') ||
|
|
||||||
key.toLowerCase().includes('refresh') ||
|
|
||||||
(auth[key] && typeof auth[key] === 'object' && (
|
|
||||||
auth[key].access_token || auth[key].refresh_token || auth[key].Id_token || auth[key].id_token
|
|
||||||
))
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasTokens) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
method: 'cli_tokens',
|
|
||||||
hasAuthFile: true,
|
|
||||||
hasEnvKey: !!envApiKey,
|
|
||||||
authPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// File exists and has content - check if it's tokens or API key
|
|
||||||
const likelyTokens = keys.some(key => key.toLowerCase().includes('token') || key.toLowerCase().includes('refresh'));
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
method: likelyTokens ? 'cli_tokens' : 'auth_file',
|
|
||||||
hasAuthFile: true,
|
|
||||||
hasEnvKey: !!envApiKey,
|
|
||||||
authPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check environment variable
|
|
||||||
if (envApiKey) {
|
|
||||||
return {
|
|
||||||
authenticated: true,
|
|
||||||
method: 'env_var',
|
|
||||||
hasAuthFile: false,
|
|
||||||
hasEnvKey: true,
|
|
||||||
authPath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
method: 'none',
|
|
||||||
hasAuthFile: false,
|
|
||||||
hasEnvKey: false,
|
|
||||||
authPath
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
authenticated: false,
|
|
||||||
method: 'none',
|
|
||||||
error: error.message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Check if Codex CLI is installed and accessible
|
|
||||||
* @returns {Object} { installed: boolean, path: string|null, version: string|null, method: 'cli'|'npm'|'brew'|'none' }
|
|
||||||
*/
|
|
||||||
static detectCodexInstallation() {
|
|
||||||
try {
|
|
||||||
// Method 1: Check if 'codex' command is in PATH
|
|
||||||
try {
|
|
||||||
const codexPath = execSync('which codex 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
||||||
if (codexPath) {
|
|
||||||
const version = this.getCodexVersion(codexPath);
|
|
||||||
return {
|
|
||||||
installed: true,
|
|
||||||
path: codexPath,
|
|
||||||
version: version,
|
|
||||||
method: 'cli'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// CLI not in PATH, continue checking other methods
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 2: Check for npm global installation
|
|
||||||
try {
|
|
||||||
const npmListOutput = execSync('npm list -g @openai/codex --depth=0 2>/dev/null', { encoding: 'utf-8' });
|
|
||||||
if (npmListOutput && npmListOutput.includes('@openai/codex')) {
|
|
||||||
// Get the path from npm bin
|
|
||||||
const npmBinPath = execSync('npm bin -g', { encoding: 'utf-8' }).trim();
|
|
||||||
const codexPath = path.join(npmBinPath, 'codex');
|
|
||||||
const version = this.getCodexVersion(codexPath);
|
|
||||||
return {
|
|
||||||
installed: true,
|
|
||||||
path: codexPath,
|
|
||||||
version: version,
|
|
||||||
method: 'npm'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// npm global not found
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 3: Check for Homebrew installation on macOS
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
try {
|
|
||||||
const brewList = execSync('brew list --formula 2>/dev/null', { encoding: 'utf-8' });
|
|
||||||
if (brewList.includes('codex')) {
|
|
||||||
const brewPrefixOutput = execSync('brew --prefix codex 2>/dev/null', { encoding: 'utf-8' }).trim();
|
|
||||||
const codexPath = path.join(brewPrefixOutput, 'bin', 'codex');
|
|
||||||
const version = this.getCodexVersion(codexPath);
|
|
||||||
return {
|
|
||||||
installed: true,
|
|
||||||
path: codexPath,
|
|
||||||
version: version,
|
|
||||||
method: 'brew'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Homebrew not found or codex not installed via brew
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 4: Check Windows path
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
try {
|
|
||||||
const codexPath = execSync('where codex 2>nul', { encoding: 'utf-8' }).trim().split('\n')[0];
|
|
||||||
if (codexPath) {
|
|
||||||
const version = this.getCodexVersion(codexPath);
|
|
||||||
return {
|
|
||||||
installed: true,
|
|
||||||
path: codexPath,
|
|
||||||
version: version,
|
|
||||||
method: 'cli'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Not found on Windows
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 5: Check common installation paths
|
|
||||||
const commonPaths = [
|
|
||||||
path.join(os.homedir(), '.local', 'bin', 'codex'),
|
|
||||||
path.join(os.homedir(), '.npm-global', 'bin', 'codex'),
|
|
||||||
'/usr/local/bin/codex',
|
|
||||||
'/opt/homebrew/bin/codex',
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const checkPath of commonPaths) {
|
|
||||||
if (fs.existsSync(checkPath)) {
|
|
||||||
const version = this.getCodexVersion(checkPath);
|
|
||||||
return {
|
|
||||||
installed: true,
|
|
||||||
path: checkPath,
|
|
||||||
version: version,
|
|
||||||
method: 'cli'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method 6: Check if OPENAI_API_KEY is set (can use Codex API directly)
|
|
||||||
if (process.env.OPENAI_API_KEY) {
|
|
||||||
return {
|
|
||||||
installed: false,
|
|
||||||
path: null,
|
|
||||||
version: null,
|
|
||||||
method: 'api-key-only',
|
|
||||||
hasApiKey: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
installed: false,
|
|
||||||
path: null,
|
|
||||||
version: null,
|
|
||||||
method: 'none'
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
// Error detecting Codex installation
|
|
||||||
return {
|
|
||||||
installed: false,
|
|
||||||
path: null,
|
|
||||||
version: null,
|
|
||||||
method: 'none',
|
|
||||||
error: error.message
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Codex CLI version from executable path
|
|
||||||
* @param {string} codexPath Path to codex executable
|
|
||||||
* @returns {string|null} Version string or null
|
|
||||||
*/
|
|
||||||
static getCodexVersion(codexPath) {
|
|
||||||
try {
|
|
||||||
const version = execSync(`"${codexPath}" --version 2>/dev/null`, { encoding: 'utf-8' }).trim();
|
|
||||||
return version || null;
|
|
||||||
} catch (error) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get installation info and recommendations
|
|
||||||
* @returns {Object} Installation status and recommendations
|
|
||||||
*/
|
|
||||||
static getInstallationInfo() {
|
|
||||||
const detection = this.detectCodexInstallation();
|
|
||||||
|
|
||||||
if (detection.installed) {
|
|
||||||
return {
|
|
||||||
status: 'installed',
|
|
||||||
method: detection.method,
|
|
||||||
version: detection.version,
|
|
||||||
path: detection.path,
|
|
||||||
recommendation: detection.method === 'cli'
|
|
||||||
? 'Using Codex CLI - ready for GPT-5.1 Codex models'
|
|
||||||
: `Using Codex CLI via ${detection.method} - ready for GPT-5.1 Codex models`
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not installed but has API key
|
|
||||||
if (detection.method === 'api-key-only') {
|
|
||||||
return {
|
|
||||||
status: 'api_key_only',
|
|
||||||
method: 'api-key-only',
|
|
||||||
recommendation: 'OPENAI_API_KEY detected but Codex CLI not installed. Install Codex CLI for full agentic capabilities.',
|
|
||||||
installCommands: this.getInstallCommands()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: 'not_installed',
|
|
||||||
recommendation: 'Install OpenAI Codex CLI to use GPT-5.1 Codex models for agentic tasks',
|
|
||||||
installCommands: this.getInstallCommands()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get installation commands for different platforms
|
|
||||||
* @returns {Object} Installation commands by platform
|
|
||||||
*/
|
|
||||||
static getInstallCommands() {
|
|
||||||
return {
|
|
||||||
npm: 'npm install -g @openai/codex@latest',
|
|
||||||
macos: 'brew install codex',
|
|
||||||
linux: 'npm install -g @openai/codex@latest',
|
|
||||||
windows: 'npm install -g @openai/codex@latest'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if Codex CLI supports a specific model
|
|
||||||
* @param {string} model Model name to check
|
|
||||||
* @returns {boolean} Whether the model is supported
|
|
||||||
*/
|
|
||||||
static isModelSupported(model) {
|
|
||||||
const supportedModels = [
|
|
||||||
'gpt-5.1-codex-max',
|
|
||||||
'gpt-5.1-codex',
|
|
||||||
'gpt-5.1-codex-mini',
|
|
||||||
'gpt-5.1'
|
|
||||||
];
|
|
||||||
return supportedModels.includes(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get default model for Codex CLI
|
|
||||||
* @returns {string} Default model name
|
|
||||||
*/
|
|
||||||
static getDefaultModel() {
|
|
||||||
return 'gpt-5.1-codex-max';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get comprehensive installation info including auth status
|
|
||||||
* @returns {Object} Full status object
|
|
||||||
*/
|
|
||||||
static getFullStatus() {
|
|
||||||
const installation = this.detectCodexInstallation();
|
|
||||||
const auth = this.checkAuth();
|
|
||||||
const info = this.getInstallationInfo();
|
|
||||||
|
|
||||||
return {
|
|
||||||
...info,
|
|
||||||
auth,
|
|
||||||
installation
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Install Codex CLI using npm
|
|
||||||
* @param {Function} onProgress Callback for progress updates
|
|
||||||
* @returns {Promise<Object>} Installation result
|
|
||||||
*/
|
|
||||||
static async installCli(onProgress) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const command = 'npm';
|
|
||||||
const args = ['install', '-g', '@openai/codex@latest'];
|
|
||||||
|
|
||||||
const proc = spawn(command, args, {
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
shell: true
|
|
||||||
});
|
|
||||||
|
|
||||||
let output = '';
|
|
||||||
let errorOutput = '';
|
|
||||||
|
|
||||||
proc.stdout.on('data', (data) => {
|
|
||||||
const text = data.toString();
|
|
||||||
output += text;
|
|
||||||
if (onProgress) {
|
|
||||||
onProgress({ type: 'stdout', data: text });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.stderr.on('data', (data) => {
|
|
||||||
const text = data.toString();
|
|
||||||
errorOutput += text;
|
|
||||||
// npm often outputs progress to stderr
|
|
||||||
if (onProgress) {
|
|
||||||
onProgress({ type: 'stderr', data: text });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('close', (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
output,
|
|
||||||
message: 'Codex CLI installed successfully'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject({
|
|
||||||
success: false,
|
|
||||||
error: errorOutput || `Installation failed with code ${code}`,
|
|
||||||
output
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('error', (error) => {
|
|
||||||
reject({
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
output
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authenticate Codex CLI - opens browser for OAuth or stores API key
|
|
||||||
* @param {string} apiKey Optional API key to store
|
|
||||||
* @param {Function} onProgress Callback for progress updates
|
|
||||||
* @returns {Promise<Object>} Authentication result
|
|
||||||
*/
|
|
||||||
static async authenticate(apiKey, onProgress) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const detection = this.detectCodexInstallation();
|
|
||||||
|
|
||||||
if (!detection.installed) {
|
|
||||||
reject({
|
|
||||||
success: false,
|
|
||||||
error: 'Codex CLI is not installed'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const codexPath = detection.path || 'codex';
|
|
||||||
|
|
||||||
if (apiKey) {
|
|
||||||
// Store API key directly using codex auth command
|
|
||||||
const proc = spawn(codexPath, ['auth', 'login', '--api-key', apiKey], {
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe'],
|
|
||||||
shell: false
|
|
||||||
});
|
|
||||||
|
|
||||||
let output = '';
|
|
||||||
let errorOutput = '';
|
|
||||||
|
|
||||||
proc.stdout.on('data', (data) => {
|
|
||||||
const text = data.toString();
|
|
||||||
output += text;
|
|
||||||
if (onProgress) {
|
|
||||||
onProgress({ type: 'stdout', data: text });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.stderr.on('data', (data) => {
|
|
||||||
const text = data.toString();
|
|
||||||
errorOutput += text;
|
|
||||||
if (onProgress) {
|
|
||||||
onProgress({ type: 'stderr', data: text });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('close', (code) => {
|
|
||||||
if (code === 0) {
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
output,
|
|
||||||
message: 'Codex CLI authenticated successfully'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
reject({
|
|
||||||
success: false,
|
|
||||||
error: errorOutput || `Authentication failed with code ${code}`,
|
|
||||||
output
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('error', (error) => {
|
|
||||||
reject({
|
|
||||||
success: false,
|
|
||||||
error: error.message,
|
|
||||||
output
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Require manual authentication
|
|
||||||
if (onProgress) {
|
|
||||||
onProgress({
|
|
||||||
type: 'info',
|
|
||||||
data: 'Please run the following command in your terminal to authenticate:\n\ncodex auth login\n\nThen return here to continue setup.'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve({
|
|
||||||
success: true,
|
|
||||||
requiresManualAuth: true,
|
|
||||||
command: `${codexPath} auth login`,
|
|
||||||
message: 'Please authenticate Codex CLI manually'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = CodexCliDetector;
|
|
||||||
@@ -1,353 +0,0 @@
|
|||||||
/**
|
|
||||||
* Codex TOML Configuration Manager
|
|
||||||
*
|
|
||||||
* Manages Codex CLI's TOML configuration file to add/update MCP server settings.
|
|
||||||
* Codex CLI looks for config at:
|
|
||||||
* - ~/.codex/config.toml (user-level)
|
|
||||||
* - .codex/config.toml (project-level, takes precedence)
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs/promises');
|
|
||||||
const path = require('path');
|
|
||||||
const os = require('os');
|
|
||||||
|
|
||||||
class CodexConfigManager {
|
|
||||||
constructor() {
|
|
||||||
this.userConfigPath = path.join(os.homedir(), '.codex', 'config.toml');
|
|
||||||
this.projectConfigPath = null; // Will be set per project
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the project path for project-level config
|
|
||||||
*/
|
|
||||||
setProjectPath(projectPath) {
|
|
||||||
this.projectConfigPath = path.join(projectPath, '.codex', 'config.toml');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the effective config path (project-level if exists, otherwise user-level)
|
|
||||||
*/
|
|
||||||
async getConfigPath() {
|
|
||||||
if (this.projectConfigPath) {
|
|
||||||
try {
|
|
||||||
await fs.access(this.projectConfigPath);
|
|
||||||
return this.projectConfigPath;
|
|
||||||
} catch (e) {
|
|
||||||
// Project config doesn't exist, fall back to user config
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure user config directory exists
|
|
||||||
const userConfigDir = path.dirname(this.userConfigPath);
|
|
||||||
try {
|
|
||||||
await fs.mkdir(userConfigDir, { recursive: true });
|
|
||||||
} catch (e) {
|
|
||||||
// Directory might already exist
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.userConfigPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read existing TOML config (simple parser for our needs)
|
|
||||||
*/
|
|
||||||
async readConfig(configPath) {
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(configPath, 'utf-8');
|
|
||||||
return this.parseToml(content);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.code === 'ENOENT') {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple TOML parser for our specific use case
|
|
||||||
* This is a minimal parser that handles the MCP server config structure
|
|
||||||
*/
|
|
||||||
parseToml(content) {
|
|
||||||
const config = {};
|
|
||||||
let currentSection = null;
|
|
||||||
let currentSubsection = null;
|
|
||||||
|
|
||||||
const lines = content.split('\n');
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
const trimmed = line.trim();
|
|
||||||
|
|
||||||
// Skip comments and empty lines
|
|
||||||
if (!trimmed || trimmed.startsWith('#')) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Section header: [section]
|
|
||||||
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
||||||
if (sectionMatch) {
|
|
||||||
const sectionName = sectionMatch[1];
|
|
||||||
const parts = sectionName.split('.');
|
|
||||||
|
|
||||||
if (parts.length === 1) {
|
|
||||||
currentSection = parts[0];
|
|
||||||
currentSubsection = null;
|
|
||||||
if (!config[currentSection]) {
|
|
||||||
config[currentSection] = {};
|
|
||||||
}
|
|
||||||
} else if (parts.length === 2) {
|
|
||||||
currentSection = parts[0];
|
|
||||||
currentSubsection = parts[1];
|
|
||||||
if (!config[currentSection]) {
|
|
||||||
config[currentSection] = {};
|
|
||||||
}
|
|
||||||
if (!config[currentSection][currentSubsection]) {
|
|
||||||
config[currentSection][currentSubsection] = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key-value pair: key = value
|
|
||||||
const kvMatch = trimmed.match(/^([^=]+)=(.+)$/);
|
|
||||||
if (kvMatch) {
|
|
||||||
const key = kvMatch[1].trim();
|
|
||||||
let value = kvMatch[2].trim();
|
|
||||||
|
|
||||||
// Remove quotes if present
|
|
||||||
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
||||||
(value.startsWith("'") && value.endsWith("'"))) {
|
|
||||||
value = value.slice(1, -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse boolean
|
|
||||||
if (value === 'true') value = true;
|
|
||||||
else if (value === 'false') value = false;
|
|
||||||
// Parse number
|
|
||||||
else if (/^-?\d+$/.test(value)) value = parseInt(value, 10);
|
|
||||||
else if (/^-?\d+\.\d+$/.test(value)) value = parseFloat(value);
|
|
||||||
|
|
||||||
if (currentSubsection) {
|
|
||||||
if (!config[currentSection][currentSubsection]) {
|
|
||||||
config[currentSection][currentSubsection] = {};
|
|
||||||
}
|
|
||||||
config[currentSection][currentSubsection][key] = value;
|
|
||||||
} else if (currentSection) {
|
|
||||||
if (!config[currentSection]) {
|
|
||||||
config[currentSection] = {};
|
|
||||||
}
|
|
||||||
config[currentSection][key] = value;
|
|
||||||
} else {
|
|
||||||
config[key] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert config object back to TOML format
|
|
||||||
*/
|
|
||||||
stringifyToml(config, indent = 0) {
|
|
||||||
const indentStr = ' '.repeat(indent);
|
|
||||||
let result = '';
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(config)) {
|
|
||||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
||||||
// Section
|
|
||||||
result += `${indentStr}[${key}]\n`;
|
|
||||||
result += this.stringifyToml(value, indent);
|
|
||||||
} else {
|
|
||||||
// Key-value
|
|
||||||
let valueStr = value;
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
// Escape quotes and wrap in quotes if needed
|
|
||||||
if (value.includes('"') || value.includes("'") || value.includes(' ')) {
|
|
||||||
valueStr = `"${value.replace(/"/g, '\\"')}"`;
|
|
||||||
}
|
|
||||||
} else if (typeof value === 'boolean') {
|
|
||||||
valueStr = value.toString();
|
|
||||||
}
|
|
||||||
result += `${indentStr}${key} = ${valueStr}\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Configure the automaker-tools MCP server
|
|
||||||
*/
|
|
||||||
async configureMcpServer(projectPath, mcpServerScriptPath) {
|
|
||||||
this.setProjectPath(projectPath);
|
|
||||||
const configPath = await this.getConfigPath();
|
|
||||||
|
|
||||||
// Read existing config
|
|
||||||
const config = await this.readConfig(configPath);
|
|
||||||
|
|
||||||
// Ensure mcp_servers section exists
|
|
||||||
if (!config.mcp_servers) {
|
|
||||||
config.mcp_servers = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure automaker-tools server
|
|
||||||
config.mcp_servers['automaker-tools'] = {
|
|
||||||
command: 'node',
|
|
||||||
args: [mcpServerScriptPath],
|
|
||||||
env: {
|
|
||||||
AUTOMAKER_PROJECT_PATH: projectPath
|
|
||||||
},
|
|
||||||
startup_timeout_sec: 10,
|
|
||||||
tool_timeout_sec: 60,
|
|
||||||
enabled_tools: ['UpdateFeatureStatus']
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ensure experimental_use_rmcp_client is enabled (if needed)
|
|
||||||
if (!config.experimental_use_rmcp_client) {
|
|
||||||
config.experimental_use_rmcp_client = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write config back
|
|
||||||
await this.writeConfig(configPath, config);
|
|
||||||
|
|
||||||
console.log(`[CodexConfigManager] Configured automaker-tools MCP server in ${configPath}`);
|
|
||||||
return configPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write config to TOML file
|
|
||||||
*/
|
|
||||||
async writeConfig(configPath, config) {
|
|
||||||
let content = '';
|
|
||||||
|
|
||||||
// Write top-level keys first (preserve existing non-MCP config)
|
|
||||||
for (const [key, value] of Object.entries(config)) {
|
|
||||||
if (key === 'mcp_servers' || key === 'experimental_use_rmcp_client') {
|
|
||||||
continue; // Handle these separately
|
|
||||||
}
|
|
||||||
if (typeof value !== 'object') {
|
|
||||||
content += `${key} = ${this.formatValue(value)}\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write experimental flag if enabled
|
|
||||||
if (config.experimental_use_rmcp_client) {
|
|
||||||
if (content && !content.endsWith('\n\n')) {
|
|
||||||
content += '\n';
|
|
||||||
}
|
|
||||||
content += `experimental_use_rmcp_client = true\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write mcp_servers section
|
|
||||||
if (config.mcp_servers && Object.keys(config.mcp_servers).length > 0) {
|
|
||||||
if (content && !content.endsWith('\n\n')) {
|
|
||||||
content += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [serverName, serverConfig] of Object.entries(config.mcp_servers)) {
|
|
||||||
content += `\n[mcp_servers.${serverName}]\n`;
|
|
||||||
|
|
||||||
// Write command first
|
|
||||||
if (serverConfig.command) {
|
|
||||||
content += `command = "${this.escapeTomlString(serverConfig.command)}"\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write args
|
|
||||||
if (serverConfig.args && Array.isArray(serverConfig.args)) {
|
|
||||||
const argsStr = serverConfig.args.map(a => `"${this.escapeTomlString(a)}"`).join(', ');
|
|
||||||
content += `args = [${argsStr}]\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write timeouts (must be before env subsection)
|
|
||||||
if (serverConfig.startup_timeout_sec !== undefined) {
|
|
||||||
content += `startup_timeout_sec = ${serverConfig.startup_timeout_sec}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (serverConfig.tool_timeout_sec !== undefined) {
|
|
||||||
content += `tool_timeout_sec = ${serverConfig.tool_timeout_sec}\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write enabled_tools (must be before env subsection - at server level, not env level)
|
|
||||||
if (serverConfig.enabled_tools && Array.isArray(serverConfig.enabled_tools)) {
|
|
||||||
const toolsStr = serverConfig.enabled_tools.map(t => `"${this.escapeTomlString(t)}"`).join(', ');
|
|
||||||
content += `enabled_tools = [${toolsStr}]\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write env section last (as a separate subsection)
|
|
||||||
// IMPORTANT: In TOML, once we start [mcp_servers.server_name.env],
|
|
||||||
// everything after belongs to that subsection until a new section starts
|
|
||||||
if (serverConfig.env && typeof serverConfig.env === 'object' && Object.keys(serverConfig.env).length > 0) {
|
|
||||||
content += `\n[mcp_servers.${serverName}.env]\n`;
|
|
||||||
for (const [envKey, envValue] of Object.entries(serverConfig.env)) {
|
|
||||||
content += `${envKey} = "${this.escapeTomlString(String(envValue))}"\n`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure directory exists
|
|
||||||
const configDir = path.dirname(configPath);
|
|
||||||
await fs.mkdir(configDir, { recursive: true });
|
|
||||||
|
|
||||||
// Write file
|
|
||||||
await fs.writeFile(configPath, content, 'utf-8');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Escape special characters in TOML strings
|
|
||||||
*/
|
|
||||||
escapeTomlString(str) {
|
|
||||||
return str
|
|
||||||
.replace(/\\/g, '\\\\')
|
|
||||||
.replace(/"/g, '\\"')
|
|
||||||
.replace(/\n/g, '\\n')
|
|
||||||
.replace(/\r/g, '\\r')
|
|
||||||
.replace(/\t/g, '\\t');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Format a value for TOML output
|
|
||||||
*/
|
|
||||||
formatValue(value) {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
// Escape quotes
|
|
||||||
const escaped = value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
||||||
return `"${escaped}"`;
|
|
||||||
} else if (typeof value === 'boolean') {
|
|
||||||
return value.toString();
|
|
||||||
} else if (typeof value === 'number') {
|
|
||||||
return value.toString();
|
|
||||||
}
|
|
||||||
return `"${String(value)}"`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove automaker-tools MCP server configuration
|
|
||||||
*/
|
|
||||||
async removeMcpServer(projectPath) {
|
|
||||||
this.setProjectPath(projectPath);
|
|
||||||
const configPath = await this.getConfigPath();
|
|
||||||
|
|
||||||
try {
|
|
||||||
const config = await this.readConfig(configPath);
|
|
||||||
|
|
||||||
if (config.mcp_servers && config.mcp_servers['automaker-tools']) {
|
|
||||||
delete config.mcp_servers['automaker-tools'];
|
|
||||||
|
|
||||||
// If no more MCP servers, remove the section
|
|
||||||
if (Object.keys(config.mcp_servers).length === 0) {
|
|
||||||
delete config.mcp_servers;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.writeConfig(configPath, config);
|
|
||||||
console.log(`[CodexConfigManager] Removed automaker-tools MCP server from ${configPath}`);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(`[CodexConfigManager] Error removing MCP server config:`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new CodexConfigManager();
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,610 +0,0 @@
|
|||||||
/**
|
|
||||||
* Codex CLI Execution Wrapper
|
|
||||||
*
|
|
||||||
* This module handles spawning and managing Codex CLI processes
|
|
||||||
* for executing OpenAI model queries.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const { spawn } = require('child_process');
|
|
||||||
const { EventEmitter } = require('events');
|
|
||||||
const readline = require('readline');
|
|
||||||
const path = require('path');
|
|
||||||
const CodexCliDetector = require('./codex-cli-detector');
|
|
||||||
const codexConfigManager = require('./codex-config-manager');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Message types from Codex CLI JSON output
|
|
||||||
*/
|
|
||||||
const CODEX_EVENT_TYPES = {
|
|
||||||
THREAD_STARTED: 'thread.started',
|
|
||||||
ITEM_STARTED: 'item.started',
|
|
||||||
ITEM_COMPLETED: 'item.completed',
|
|
||||||
THREAD_COMPLETED: 'thread.completed',
|
|
||||||
ERROR: 'error'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Codex Executor - Manages Codex CLI process execution
|
|
||||||
*/
|
|
||||||
class CodexExecutor extends EventEmitter {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.currentProcess = null;
|
|
||||||
this.codexPath = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find and cache the Codex CLI path
|
|
||||||
* @returns {string|null} Path to codex executable
|
|
||||||
*/
|
|
||||||
findCodexPath() {
|
|
||||||
if (this.codexPath) {
|
|
||||||
return this.codexPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
const installation = CodexCliDetector.detectCodexInstallation();
|
|
||||||
if (installation.installed && installation.path) {
|
|
||||||
this.codexPath = installation.path;
|
|
||||||
return this.codexPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a Codex CLI query
|
|
||||||
* @param {Object} options Execution options
|
|
||||||
* @param {string} options.prompt The prompt to execute
|
|
||||||
* @param {string} options.model Model to use (default: gpt-5.1-codex-max)
|
|
||||||
* @param {string} options.cwd Working directory
|
|
||||||
* @param {string} options.systemPrompt System prompt (optional, will be prepended to prompt)
|
|
||||||
* @param {number} options.maxTurns Not used - Codex CLI doesn't support this parameter
|
|
||||||
* @param {string[]} options.allowedTools Not used - Codex CLI doesn't support this parameter
|
|
||||||
* @param {Object} options.env Environment variables
|
|
||||||
* @param {Object} options.mcpServers MCP servers configuration (for configuring Codex TOML)
|
|
||||||
* @returns {AsyncGenerator} Generator yielding messages
|
|
||||||
*/
|
|
||||||
async *execute(options) {
|
|
||||||
const {
|
|
||||||
prompt,
|
|
||||||
model = 'gpt-5.1-codex-max',
|
|
||||||
cwd = process.cwd(),
|
|
||||||
systemPrompt,
|
|
||||||
maxTurns, // Not used by Codex CLI
|
|
||||||
allowedTools, // Not used by Codex CLI
|
|
||||||
env = {},
|
|
||||||
mcpServers = null
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const codexPath = this.findCodexPath();
|
|
||||||
if (!codexPath) {
|
|
||||||
yield {
|
|
||||||
type: 'error',
|
|
||||||
error: 'Codex CLI not found. Please install it with: npm install -g @openai/codex@latest'
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure MCP server if provided
|
|
||||||
if (mcpServers && mcpServers['automaker-tools']) {
|
|
||||||
try {
|
|
||||||
// Get the absolute path to the MCP server script
|
|
||||||
const mcpServerScriptPath = path.resolve(__dirname, 'mcp-server-stdio.js');
|
|
||||||
|
|
||||||
// Verify the script exists
|
|
||||||
const fs = require('fs');
|
|
||||||
if (!fs.existsSync(mcpServerScriptPath)) {
|
|
||||||
console.warn(`[CodexExecutor] MCP server script not found at ${mcpServerScriptPath}, skipping MCP configuration`);
|
|
||||||
} else {
|
|
||||||
// Configure Codex TOML to use the MCP server
|
|
||||||
await codexConfigManager.configureMcpServer(cwd, mcpServerScriptPath);
|
|
||||||
console.log('[CodexExecutor] Configured automaker-tools MCP server for Codex CLI');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[CodexExecutor] Failed to configure MCP server:', error);
|
|
||||||
// Continue execution even if MCP config fails - Codex will work without MCP tools
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Combine system prompt with main prompt if provided
|
|
||||||
// Codex CLI doesn't support --system-prompt argument, so we prepend it to the prompt
|
|
||||||
let combinedPrompt = prompt;
|
|
||||||
console.log('[CodexExecutor] Original prompt length:', prompt?.length || 0);
|
|
||||||
if (systemPrompt) {
|
|
||||||
combinedPrompt = `${systemPrompt}\n\n---\n\n${prompt}`;
|
|
||||||
console.log('[CodexExecutor] System prompt prepended to main prompt');
|
|
||||||
console.log('[CodexExecutor] System prompt length:', systemPrompt.length);
|
|
||||||
console.log('[CodexExecutor] Combined prompt length:', combinedPrompt.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build command arguments
|
|
||||||
// Note: maxTurns and allowedTools are not supported by Codex CLI
|
|
||||||
console.log('[CodexExecutor] Building command arguments...');
|
|
||||||
const args = this.buildArgs({
|
|
||||||
prompt: combinedPrompt,
|
|
||||||
model
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('[CodexExecutor] Executing command:', codexPath);
|
|
||||||
console.log('[CodexExecutor] Number of args:', args.length);
|
|
||||||
console.log('[CodexExecutor] Args (without prompt):', args.slice(0, -1).join(' '));
|
|
||||||
console.log('[CodexExecutor] Prompt length in args:', args[args.length - 1]?.length || 0);
|
|
||||||
console.log('[CodexExecutor] Prompt preview (first 200 chars):', args[args.length - 1]?.substring(0, 200));
|
|
||||||
console.log('[CodexExecutor] Working directory:', cwd);
|
|
||||||
|
|
||||||
// Spawn the process
|
|
||||||
const processEnv = {
|
|
||||||
...process.env,
|
|
||||||
...env,
|
|
||||||
// Ensure OPENAI_API_KEY is available
|
|
||||||
OPENAI_API_KEY: env.OPENAI_API_KEY || process.env.OPENAI_API_KEY
|
|
||||||
};
|
|
||||||
|
|
||||||
// Log API key status (without exposing the key)
|
|
||||||
if (processEnv.OPENAI_API_KEY) {
|
|
||||||
console.log('[CodexExecutor] OPENAI_API_KEY is set (length:', processEnv.OPENAI_API_KEY.length, ')');
|
|
||||||
} else {
|
|
||||||
console.warn('[CodexExecutor] WARNING: OPENAI_API_KEY is not set!');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[CodexExecutor] Spawning process...');
|
|
||||||
const proc = spawn(codexPath, args, {
|
|
||||||
cwd,
|
|
||||||
env: processEnv,
|
|
||||||
stdio: ['pipe', 'pipe', 'pipe']
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentProcess = proc;
|
|
||||||
console.log('[CodexExecutor] Process spawned with PID:', proc.pid);
|
|
||||||
|
|
||||||
// Track process events
|
|
||||||
proc.on('error', (error) => {
|
|
||||||
console.error('[CodexExecutor] Process error:', error);
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.on('spawn', () => {
|
|
||||||
console.log('[CodexExecutor] Process spawned successfully');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Collect stderr output as it comes in
|
|
||||||
let stderr = '';
|
|
||||||
let hasOutput = false;
|
|
||||||
let stdoutChunks = [];
|
|
||||||
let stderrChunks = [];
|
|
||||||
|
|
||||||
proc.stderr.on('data', (data) => {
|
|
||||||
const errorText = data.toString();
|
|
||||||
stderr += errorText;
|
|
||||||
stderrChunks.push(errorText);
|
|
||||||
hasOutput = true;
|
|
||||||
console.error('[CodexExecutor] stderr chunk received (', data.length, 'bytes):', errorText.substring(0, 200));
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.stderr.on('end', () => {
|
|
||||||
console.log('[CodexExecutor] stderr stream ended. Total chunks:', stderrChunks.length, 'Total length:', stderr.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.stdout.on('data', (data) => {
|
|
||||||
const text = data.toString();
|
|
||||||
stdoutChunks.push(text);
|
|
||||||
hasOutput = true;
|
|
||||||
console.log('[CodexExecutor] stdout chunk received (', data.length, 'bytes):', text.substring(0, 200));
|
|
||||||
});
|
|
||||||
|
|
||||||
proc.stdout.on('end', () => {
|
|
||||||
console.log('[CodexExecutor] stdout stream ended. Total chunks:', stdoutChunks.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create readline interface for parsing JSONL output
|
|
||||||
console.log('[CodexExecutor] Creating readline interface...');
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: proc.stdout,
|
|
||||||
crlfDelay: Infinity
|
|
||||||
});
|
|
||||||
|
|
||||||
// Track accumulated content for converting to Claude format
|
|
||||||
let accumulatedText = '';
|
|
||||||
let toolUses = [];
|
|
||||||
let lastOutputTime = Date.now();
|
|
||||||
const OUTPUT_TIMEOUT = 30000; // 30 seconds timeout for no output
|
|
||||||
let lineCount = 0;
|
|
||||||
let jsonParseErrors = 0;
|
|
||||||
|
|
||||||
// Set up timeout check
|
|
||||||
const checkTimeout = setInterval(() => {
|
|
||||||
const timeSinceLastOutput = Date.now() - lastOutputTime;
|
|
||||||
if (timeSinceLastOutput > OUTPUT_TIMEOUT && !hasOutput) {
|
|
||||||
console.warn('[CodexExecutor] No output received for', timeSinceLastOutput, 'ms. Process still alive:', !proc.killed);
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
console.log('[CodexExecutor] Starting to read lines from stdout...');
|
|
||||||
|
|
||||||
// Process stdout line by line (JSONL format)
|
|
||||||
try {
|
|
||||||
for await (const line of rl) {
|
|
||||||
hasOutput = true;
|
|
||||||
lastOutputTime = Date.now();
|
|
||||||
lineCount++;
|
|
||||||
|
|
||||||
console.log('[CodexExecutor] Line', lineCount, 'received (length:', line.length, '):', line.substring(0, 100));
|
|
||||||
|
|
||||||
if (!line.trim()) {
|
|
||||||
console.log('[CodexExecutor] Skipping empty line');
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const event = JSON.parse(line);
|
|
||||||
console.log('[CodexExecutor] Successfully parsed JSON event. Type:', event.type, 'Keys:', Object.keys(event));
|
|
||||||
|
|
||||||
const convertedMsg = this.convertToClaudeFormat(event);
|
|
||||||
console.log('[CodexExecutor] Converted message:', convertedMsg ? { type: convertedMsg.type } : 'null');
|
|
||||||
|
|
||||||
if (convertedMsg) {
|
|
||||||
// Accumulate text content
|
|
||||||
if (convertedMsg.type === 'assistant' && convertedMsg.message?.content) {
|
|
||||||
for (const block of convertedMsg.message.content) {
|
|
||||||
if (block.type === 'text') {
|
|
||||||
accumulatedText += block.text;
|
|
||||||
console.log('[CodexExecutor] Accumulated text block (total length:', accumulatedText.length, ')');
|
|
||||||
} else if (block.type === 'tool_use') {
|
|
||||||
toolUses.push(block);
|
|
||||||
console.log('[CodexExecutor] Tool use detected:', block.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
console.log('[CodexExecutor] Yielding message of type:', convertedMsg.type);
|
|
||||||
yield convertedMsg;
|
|
||||||
} else {
|
|
||||||
console.log('[CodexExecutor] Converted message is null, skipping');
|
|
||||||
}
|
|
||||||
} catch (parseError) {
|
|
||||||
jsonParseErrors++;
|
|
||||||
// Non-JSON output, yield as text
|
|
||||||
console.log('[CodexExecutor] JSON parse error (', jsonParseErrors, 'total):', parseError.message);
|
|
||||||
console.log('[CodexExecutor] Non-JSON line content:', line.substring(0, 200));
|
|
||||||
yield {
|
|
||||||
type: 'assistant',
|
|
||||||
message: {
|
|
||||||
content: [{ type: 'text', text: line + '\n' }]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('[CodexExecutor] Finished reading all lines. Total lines:', lineCount, 'JSON errors:', jsonParseErrors);
|
|
||||||
} catch (readError) {
|
|
||||||
console.error('[CodexExecutor] Error reading from readline:', readError);
|
|
||||||
throw readError;
|
|
||||||
} finally {
|
|
||||||
clearInterval(checkTimeout);
|
|
||||||
console.log('[CodexExecutor] Cleaned up timeout checker');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle process completion
|
|
||||||
console.log('[CodexExecutor] Waiting for process to close...');
|
|
||||||
const exitCode = await new Promise((resolve) => {
|
|
||||||
proc.on('close', (code, signal) => {
|
|
||||||
console.log('[CodexExecutor] Process closed with code:', code, 'signal:', signal);
|
|
||||||
resolve(code);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
this.currentProcess = null;
|
|
||||||
console.log('[CodexExecutor] Process completed. Exit code:', exitCode, 'Has output:', hasOutput, 'Stderr length:', stderr.length);
|
|
||||||
|
|
||||||
// Wait a bit for any remaining stderr data to be collected
|
|
||||||
console.log('[CodexExecutor] Waiting 200ms for any remaining stderr data...');
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
|
||||||
console.log('[CodexExecutor] Final stderr length:', stderr.length, 'Final stdout chunks:', stdoutChunks.length);
|
|
||||||
|
|
||||||
if (exitCode !== 0) {
|
|
||||||
const errorMessage = stderr.trim()
|
|
||||||
? `Codex CLI exited with code ${exitCode}.\n\nError output:\n${stderr}`
|
|
||||||
: `Codex CLI exited with code ${exitCode}. No error output captured.`;
|
|
||||||
|
|
||||||
console.error('[CodexExecutor] Process failed with exit code', exitCode);
|
|
||||||
console.error('[CodexExecutor] Error message:', errorMessage);
|
|
||||||
console.error('[CodexExecutor] Stderr chunks:', stderrChunks.length, 'Stdout chunks:', stdoutChunks.length);
|
|
||||||
|
|
||||||
yield {
|
|
||||||
type: 'error',
|
|
||||||
error: errorMessage
|
|
||||||
};
|
|
||||||
} else if (!hasOutput && !stderr) {
|
|
||||||
// Process exited successfully but produced no output - might be API key issue
|
|
||||||
const warningMessage = 'Codex CLI completed but produced no output. This might indicate:\n' +
|
|
||||||
'- Missing or invalid OPENAI_API_KEY\n' +
|
|
||||||
'- Codex CLI configuration issue\n' +
|
|
||||||
'- The process completed without generating any response\n\n' +
|
|
||||||
`Debug info: Exit code ${exitCode}, stdout chunks: ${stdoutChunks.length}, stderr chunks: ${stderrChunks.length}, lines read: ${lineCount}`;
|
|
||||||
|
|
||||||
console.warn('[CodexExecutor] No output detected:', warningMessage);
|
|
||||||
console.warn('[CodexExecutor] Stdout chunks:', stdoutChunks);
|
|
||||||
console.warn('[CodexExecutor] Stderr chunks:', stderrChunks);
|
|
||||||
|
|
||||||
yield {
|
|
||||||
type: 'error',
|
|
||||||
error: warningMessage
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
console.log('[CodexExecutor] Process completed successfully. Exit code:', exitCode, 'Lines processed:', lineCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build command arguments for Codex CLI
|
|
||||||
* Only includes supported arguments based on Codex CLI help:
|
|
||||||
* - --model: Model to use
|
|
||||||
* - --json: JSON output format
|
|
||||||
* - --full-auto: Non-interactive automatic execution
|
|
||||||
*
|
|
||||||
* Note: Codex CLI does NOT support:
|
|
||||||
* - --system-prompt (system prompt is prepended to main prompt)
|
|
||||||
* - --max-turns (not available in CLI)
|
|
||||||
* - --tools (not available in CLI)
|
|
||||||
*
|
|
||||||
* @param {Object} options Options
|
|
||||||
* @returns {string[]} Command arguments
|
|
||||||
*/
|
|
||||||
buildArgs(options) {
|
|
||||||
const { prompt, model } = options;
|
|
||||||
|
|
||||||
console.log('[CodexExecutor] buildArgs called with model:', model, 'prompt length:', prompt?.length || 0);
|
|
||||||
|
|
||||||
const args = ['exec'];
|
|
||||||
|
|
||||||
// Add model (required for most use cases)
|
|
||||||
if (model) {
|
|
||||||
args.push('--model', model);
|
|
||||||
console.log('[CodexExecutor] Added model argument:', model);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add JSON output flag for structured parsing
|
|
||||||
args.push('--json');
|
|
||||||
console.log('[CodexExecutor] Added --json flag');
|
|
||||||
|
|
||||||
// Add full-auto mode (non-interactive)
|
|
||||||
// This enables automatic execution with workspace-write sandbox
|
|
||||||
args.push('--full-auto');
|
|
||||||
console.log('[CodexExecutor] Added --full-auto flag');
|
|
||||||
|
|
||||||
// Add the prompt at the end
|
|
||||||
args.push(prompt);
|
|
||||||
console.log('[CodexExecutor] Added prompt (length:', prompt?.length || 0, ')');
|
|
||||||
|
|
||||||
console.log('[CodexExecutor] Final args count:', args.length);
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map Claude tool names to Codex tool names
|
|
||||||
* @param {string[]} tools Array of tool names
|
|
||||||
* @returns {string[]} Mapped tool names
|
|
||||||
*/
|
|
||||||
mapToolsToCodex(tools) {
|
|
||||||
const toolMap = {
|
|
||||||
'Read': 'read',
|
|
||||||
'Write': 'write',
|
|
||||||
'Edit': 'edit',
|
|
||||||
'Bash': 'bash',
|
|
||||||
'Glob': 'glob',
|
|
||||||
'Grep': 'grep',
|
|
||||||
'WebSearch': 'web-search',
|
|
||||||
'WebFetch': 'web-fetch'
|
|
||||||
};
|
|
||||||
|
|
||||||
return tools
|
|
||||||
.map(tool => toolMap[tool] || tool.toLowerCase())
|
|
||||||
.filter(tool => tool); // Remove undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert Codex JSONL event to Claude SDK message format
|
|
||||||
* @param {Object} event Codex event object
|
|
||||||
* @returns {Object|null} Claude-format message or null
|
|
||||||
*/
|
|
||||||
convertToClaudeFormat(event) {
|
|
||||||
console.log('[CodexExecutor] Converting event:', JSON.stringify(event).substring(0, 200));
|
|
||||||
const { type, data, item, thread_id } = event;
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case CODEX_EVENT_TYPES.THREAD_STARTED:
|
|
||||||
case 'thread.started':
|
|
||||||
// Session initialization
|
|
||||||
return {
|
|
||||||
type: 'session_start',
|
|
||||||
sessionId: thread_id || data?.thread_id || event.thread_id
|
|
||||||
};
|
|
||||||
|
|
||||||
case CODEX_EVENT_TYPES.ITEM_COMPLETED:
|
|
||||||
case 'item.completed':
|
|
||||||
// Codex uses 'item' field, not 'data'
|
|
||||||
return this.convertItemCompleted(item || data);
|
|
||||||
|
|
||||||
case CODEX_EVENT_TYPES.ITEM_STARTED:
|
|
||||||
case 'item.started':
|
|
||||||
// Convert item.started events - these indicate tool/command usage
|
|
||||||
const startedItem = item || data;
|
|
||||||
if (startedItem?.type === 'command_execution' && startedItem?.command) {
|
|
||||||
return {
|
|
||||||
type: 'assistant',
|
|
||||||
message: {
|
|
||||||
content: [{
|
|
||||||
type: 'tool_use',
|
|
||||||
name: 'bash',
|
|
||||||
input: { command: startedItem.command }
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// For other item.started types, return null (we'll show the completed version)
|
|
||||||
return null;
|
|
||||||
|
|
||||||
case CODEX_EVENT_TYPES.THREAD_COMPLETED:
|
|
||||||
case 'thread.completed':
|
|
||||||
return {
|
|
||||||
type: 'complete',
|
|
||||||
sessionId: thread_id || data?.thread_id || event.thread_id
|
|
||||||
};
|
|
||||||
|
|
||||||
case CODEX_EVENT_TYPES.ERROR:
|
|
||||||
case 'error':
|
|
||||||
return {
|
|
||||||
type: 'error',
|
|
||||||
error: data?.message || item?.message || event.message || 'Unknown error from Codex CLI'
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'turn.started':
|
|
||||||
// Turn started - just a marker, no need to convert
|
|
||||||
return null;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Pass through other events
|
|
||||||
console.log('[CodexExecutor] Unhandled event type:', type);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert item.completed event to Claude format
|
|
||||||
* @param {Object} item Event item data
|
|
||||||
* @returns {Object|null} Claude-format message
|
|
||||||
*/
|
|
||||||
convertItemCompleted(item) {
|
|
||||||
if (!item) {
|
|
||||||
console.log('[CodexExecutor] convertItemCompleted: item is null/undefined');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const itemType = item.type || item.item_type;
|
|
||||||
console.log('[CodexExecutor] convertItemCompleted: itemType =', itemType, 'item keys:', Object.keys(item));
|
|
||||||
|
|
||||||
switch (itemType) {
|
|
||||||
case 'reasoning':
|
|
||||||
// Thinking/reasoning output - Codex uses 'text' field
|
|
||||||
const reasoningText = item.text || item.content || '';
|
|
||||||
console.log('[CodexExecutor] Converting reasoning, text length:', reasoningText.length);
|
|
||||||
return {
|
|
||||||
type: 'assistant',
|
|
||||||
message: {
|
|
||||||
content: [{
|
|
||||||
type: 'thinking',
|
|
||||||
thinking: reasoningText
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'agent_message':
|
|
||||||
case 'message':
|
|
||||||
// Assistant text message
|
|
||||||
const messageText = item.content || item.text || '';
|
|
||||||
console.log('[CodexExecutor] Converting message, text length:', messageText.length);
|
|
||||||
return {
|
|
||||||
type: 'assistant',
|
|
||||||
message: {
|
|
||||||
content: [{
|
|
||||||
type: 'text',
|
|
||||||
text: messageText
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'command_execution':
|
|
||||||
// Command execution - show both the command and its output
|
|
||||||
const command = item.command || '';
|
|
||||||
const output = item.aggregated_output || item.output || '';
|
|
||||||
console.log('[CodexExecutor] Converting command_execution, command:', command.substring(0, 50), 'output length:', output.length);
|
|
||||||
|
|
||||||
// Return as text message showing the command and output
|
|
||||||
return {
|
|
||||||
type: 'assistant',
|
|
||||||
message: {
|
|
||||||
content: [{
|
|
||||||
type: 'text',
|
|
||||||
text: `\`\`\`bash\n${command}\n\`\`\`\n\n${output}`
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'tool_use':
|
|
||||||
// Tool use
|
|
||||||
return {
|
|
||||||
type: 'assistant',
|
|
||||||
message: {
|
|
||||||
content: [{
|
|
||||||
type: 'tool_use',
|
|
||||||
name: item.tool || item.command || 'unknown',
|
|
||||||
input: item.input || item.args || {}
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'tool_result':
|
|
||||||
// Tool result
|
|
||||||
return {
|
|
||||||
type: 'tool_result',
|
|
||||||
tool_use_id: item.tool_use_id,
|
|
||||||
content: item.output || item.result
|
|
||||||
};
|
|
||||||
|
|
||||||
case 'todo_list':
|
|
||||||
// Todo list - convert to text format
|
|
||||||
const todos = item.items || [];
|
|
||||||
const todoText = todos.map((t, i) => `${i + 1}. ${t.text || t}`).join('\n');
|
|
||||||
console.log('[CodexExecutor] Converting todo_list, items:', todos.length);
|
|
||||||
return {
|
|
||||||
type: 'assistant',
|
|
||||||
message: {
|
|
||||||
content: [{
|
|
||||||
type: 'text',
|
|
||||||
text: `**Todo List:**\n${todoText}`
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Generic text output
|
|
||||||
const text = item.text || item.content || item.aggregated_output;
|
|
||||||
if (text) {
|
|
||||||
console.log('[CodexExecutor] Converting default item type, text length:', text.length);
|
|
||||||
return {
|
|
||||||
type: 'assistant',
|
|
||||||
message: {
|
|
||||||
content: [{
|
|
||||||
type: 'text',
|
|
||||||
text: String(text)
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
console.log('[CodexExecutor] convertItemCompleted: No text content found, returning null');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Abort current execution
|
|
||||||
*/
|
|
||||||
abort() {
|
|
||||||
if (this.currentProcess) {
|
|
||||||
console.log('[CodexExecutor] Aborting current process');
|
|
||||||
this.currentProcess.kill('SIGTERM');
|
|
||||||
this.currentProcess = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if execution is in progress
|
|
||||||
* @returns {boolean} Whether execution is in progress
|
|
||||||
*/
|
|
||||||
isRunning() {
|
|
||||||
return this.currentProcess !== null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Singleton instance
|
|
||||||
const codexExecutor = new CodexExecutor();
|
|
||||||
|
|
||||||
module.exports = codexExecutor;
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const fs = require("fs/promises");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Context Manager - Handles reading, writing, and deleting context files for features
|
|
||||||
*/
|
|
||||||
class ContextManager {
|
|
||||||
/**
|
|
||||||
* Write output to feature context file
|
|
||||||
*/
|
|
||||||
async writeToContextFile(projectPath, featureId, content) {
|
|
||||||
if (!projectPath) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const featureDir = path.join(
|
|
||||||
projectPath,
|
|
||||||
".automaker",
|
|
||||||
"features",
|
|
||||||
featureId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure feature directory exists
|
|
||||||
try {
|
|
||||||
await fs.access(featureDir);
|
|
||||||
} catch {
|
|
||||||
await fs.mkdir(featureDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = path.join(featureDir, "agent-output.md");
|
|
||||||
|
|
||||||
// Append to existing file or create new one
|
|
||||||
try {
|
|
||||||
const existing = await fs.readFile(filePath, "utf-8");
|
|
||||||
await fs.writeFile(filePath, existing + content, "utf-8");
|
|
||||||
} catch {
|
|
||||||
await fs.writeFile(filePath, content, "utf-8");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[ContextManager] Failed to write to context file:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read context file for a feature
|
|
||||||
*/
|
|
||||||
async readContextFile(projectPath, featureId) {
|
|
||||||
try {
|
|
||||||
const contextPath = path.join(
|
|
||||||
projectPath,
|
|
||||||
".automaker",
|
|
||||||
"features",
|
|
||||||
featureId,
|
|
||||||
"agent-output.md"
|
|
||||||
);
|
|
||||||
const content = await fs.readFile(contextPath, "utf-8");
|
|
||||||
return content;
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`[ContextManager] No context file found for ${featureId}`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete agent context file for a feature
|
|
||||||
*/
|
|
||||||
async deleteContextFile(projectPath, featureId) {
|
|
||||||
if (!projectPath) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const contextPath = path.join(
|
|
||||||
projectPath,
|
|
||||||
".automaker",
|
|
||||||
"features",
|
|
||||||
featureId,
|
|
||||||
"agent-output.md"
|
|
||||||
);
|
|
||||||
await fs.unlink(contextPath);
|
|
||||||
console.log(
|
|
||||||
`[ContextManager] Deleted agent context for feature ${featureId}`
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
// File might not exist, which is fine
|
|
||||||
if (error.code !== "ENOENT") {
|
|
||||||
console.error("[ContextManager] Failed to delete context file:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Read the memory.md file containing lessons learned and common issues
|
|
||||||
* Returns formatted string to inject into prompts
|
|
||||||
*/
|
|
||||||
async getMemoryContent(projectPath) {
|
|
||||||
if (!projectPath) return "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const memoryPath = path.join(projectPath, ".automaker", "memory.md");
|
|
||||||
|
|
||||||
// Check if file exists
|
|
||||||
try {
|
|
||||||
await fs.access(memoryPath);
|
|
||||||
} catch {
|
|
||||||
// File doesn't exist, return empty string
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = await fs.readFile(memoryPath, "utf-8");
|
|
||||||
|
|
||||||
if (!content.trim()) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
return `
|
|
||||||
**🧠 Agent Memory - Previous Lessons Learned:**
|
|
||||||
|
|
||||||
The following memory file contains lessons learned from previous agent runs, including common issues and their solutions. Review this carefully to avoid repeating past mistakes.
|
|
||||||
|
|
||||||
<agent-memory>
|
|
||||||
${content}
|
|
||||||
</agent-memory>
|
|
||||||
|
|
||||||
**IMPORTANT:** If you encounter a new issue that took significant debugging effort to resolve, add it to the memory file at \`.automaker/memory.md\` in a concise format:
|
|
||||||
- Issue title
|
|
||||||
- Problem description (1-2 sentences)
|
|
||||||
- Solution/fix (with code example if helpful)
|
|
||||||
|
|
||||||
This helps future agent runs avoid the same pitfalls.
|
|
||||||
`;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[ContextManager] Failed to read memory file:", error);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List context files from .automaker/context/ directory and get previews
|
|
||||||
* Returns a formatted string with file names and first 50 lines of each file
|
|
||||||
*/
|
|
||||||
async getContextFilesPreview(projectPath) {
|
|
||||||
if (!projectPath) return "";
|
|
||||||
|
|
||||||
try {
|
|
||||||
const contextDir = path.join(projectPath, ".automaker", "context");
|
|
||||||
|
|
||||||
// Check if directory exists
|
|
||||||
try {
|
|
||||||
await fs.access(contextDir);
|
|
||||||
} catch {
|
|
||||||
// Directory doesn't exist, return empty string
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read directory contents
|
|
||||||
const entries = await fs.readdir(contextDir, { withFileTypes: true });
|
|
||||||
const files = entries
|
|
||||||
.filter((entry) => entry.isFile())
|
|
||||||
.map((entry) => entry.name)
|
|
||||||
.sort();
|
|
||||||
|
|
||||||
if (files.length === 0) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build preview string
|
|
||||||
const previews = [];
|
|
||||||
previews.push(`\n**📁 Context Files Available:**\n`);
|
|
||||||
previews.push(
|
|
||||||
`The following context files are available in \`.automaker/context/\` directory.`
|
|
||||||
);
|
|
||||||
previews.push(
|
|
||||||
`These files contain additional context that may be relevant to your work.`
|
|
||||||
);
|
|
||||||
previews.push(
|
|
||||||
`You can read them in full using the Read tool if needed.\n`
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const fileName of files) {
|
|
||||||
try {
|
|
||||||
const filePath = path.join(contextDir, fileName);
|
|
||||||
const content = await fs.readFile(filePath, "utf-8");
|
|
||||||
const lines = content.split("\n");
|
|
||||||
const previewLines = lines.slice(0, 50);
|
|
||||||
const preview = previewLines.join("\n");
|
|
||||||
const hasMore = lines.length > 50;
|
|
||||||
|
|
||||||
previews.push(`\n**File: ${fileName}**`);
|
|
||||||
if (hasMore) {
|
|
||||||
previews.push(
|
|
||||||
`(Showing first 50 of ${lines.length} lines - use Read tool to see full content)`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
previews.push(`\`\`\``);
|
|
||||||
previews.push(preview);
|
|
||||||
previews.push(`\`\`\`\n`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[ContextManager] Failed to read context file ${fileName}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
previews.push(`\n**File: ${fileName}** (Error reading file)\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return previews.join("\n");
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[ContextManager] Failed to list context files:", error);
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the initial git state before a feature starts executing
|
|
||||||
* This captures all files that were already modified before the AI agent started
|
|
||||||
* @param {string} projectPath - Path to the project
|
|
||||||
* @param {string} featureId - Feature ID
|
|
||||||
* @returns {Promise<{modifiedFiles: string[], untrackedFiles: string[]}>}
|
|
||||||
*/
|
|
||||||
async saveInitialGitState(projectPath, featureId) {
|
|
||||||
if (!projectPath) return { modifiedFiles: [], untrackedFiles: [] };
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { execSync } = require("child_process");
|
|
||||||
const featureDir = path.join(
|
|
||||||
projectPath,
|
|
||||||
".automaker",
|
|
||||||
"features",
|
|
||||||
featureId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure feature directory exists
|
|
||||||
try {
|
|
||||||
await fs.access(featureDir);
|
|
||||||
} catch {
|
|
||||||
await fs.mkdir(featureDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get list of modified files (both staged and unstaged)
|
|
||||||
let modifiedFiles = [];
|
|
||||||
try {
|
|
||||||
const modifiedOutput = execSync("git diff --name-only HEAD", {
|
|
||||||
cwd: projectPath,
|
|
||||||
encoding: "utf-8",
|
|
||||||
}).trim();
|
|
||||||
if (modifiedOutput) {
|
|
||||||
modifiedFiles = modifiedOutput.split("\n").filter(Boolean);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
"[ContextManager] No modified files or git error:",
|
|
||||||
error.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get list of untracked files
|
|
||||||
let untrackedFiles = [];
|
|
||||||
try {
|
|
||||||
const untrackedOutput = execSync(
|
|
||||||
"git ls-files --others --exclude-standard",
|
|
||||||
{
|
|
||||||
cwd: projectPath,
|
|
||||||
encoding: "utf-8",
|
|
||||||
}
|
|
||||||
).trim();
|
|
||||||
if (untrackedOutput) {
|
|
||||||
untrackedFiles = untrackedOutput.split("\n").filter(Boolean);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
"[ContextManager] Error getting untracked files:",
|
|
||||||
error.message
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save the initial state to a JSON file
|
|
||||||
const stateFile = path.join(featureDir, "git-state.json");
|
|
||||||
const state = {
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
modifiedFiles,
|
|
||||||
untrackedFiles,
|
|
||||||
};
|
|
||||||
|
|
||||||
await fs.writeFile(stateFile, JSON.stringify(state, null, 2), "utf-8");
|
|
||||||
console.log(
|
|
||||||
`[ContextManager] Saved initial git state for ${featureId}:`,
|
|
||||||
{
|
|
||||||
modifiedCount: modifiedFiles.length,
|
|
||||||
untrackedCount: untrackedFiles.length,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return state;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"[ContextManager] Failed to save initial git state:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return { modifiedFiles: [], untrackedFiles: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the initial git state saved before a feature started executing
|
|
||||||
* @param {string} projectPath - Path to the project
|
|
||||||
* @param {string} featureId - Feature ID
|
|
||||||
* @returns {Promise<{modifiedFiles: string[], untrackedFiles: string[], timestamp: string} | null>}
|
|
||||||
*/
|
|
||||||
async getInitialGitState(projectPath, featureId) {
|
|
||||||
if (!projectPath) return null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stateFile = path.join(
|
|
||||||
projectPath,
|
|
||||||
".automaker",
|
|
||||||
"features",
|
|
||||||
featureId,
|
|
||||||
"git-state.json"
|
|
||||||
);
|
|
||||||
const content = await fs.readFile(stateFile, "utf-8");
|
|
||||||
return JSON.parse(content);
|
|
||||||
} catch (error) {
|
|
||||||
console.log(
|
|
||||||
`[ContextManager] No initial git state found for ${featureId}`
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the git state file for a feature
|
|
||||||
* @param {string} projectPath - Path to the project
|
|
||||||
* @param {string} featureId - Feature ID
|
|
||||||
*/
|
|
||||||
async deleteGitStateFile(projectPath, featureId) {
|
|
||||||
if (!projectPath) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stateFile = path.join(
|
|
||||||
projectPath,
|
|
||||||
".automaker",
|
|
||||||
"features",
|
|
||||||
featureId,
|
|
||||||
"git-state.json"
|
|
||||||
);
|
|
||||||
await fs.unlink(stateFile);
|
|
||||||
console.log(`[ContextManager] Deleted git state file for ${featureId}`);
|
|
||||||
} catch (error) {
|
|
||||||
// File might not exist, which is fine
|
|
||||||
if (error.code !== "ENOENT") {
|
|
||||||
console.error(
|
|
||||||
"[ContextManager] Failed to delete git state file:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculate which files were changed during the AI session
|
|
||||||
* by comparing current git state with the saved initial state
|
|
||||||
* @param {string} projectPath - Path to the project
|
|
||||||
* @param {string} featureId - Feature ID
|
|
||||||
* @returns {Promise<{newFiles: string[], modifiedFiles: string[]}>}
|
|
||||||
*/
|
|
||||||
async getFilesChangedDuringSession(projectPath, featureId) {
|
|
||||||
if (!projectPath) return { newFiles: [], modifiedFiles: [] };
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { execSync } = require("child_process");
|
|
||||||
|
|
||||||
// Get initial state
|
|
||||||
const initialState = await this.getInitialGitState(
|
|
||||||
projectPath,
|
|
||||||
featureId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get current state
|
|
||||||
let currentModified = [];
|
|
||||||
try {
|
|
||||||
const modifiedOutput = execSync("git diff --name-only HEAD", {
|
|
||||||
cwd: projectPath,
|
|
||||||
encoding: "utf-8",
|
|
||||||
}).trim();
|
|
||||||
if (modifiedOutput) {
|
|
||||||
currentModified = modifiedOutput.split("\n").filter(Boolean);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ContextManager] No modified files or git error");
|
|
||||||
}
|
|
||||||
|
|
||||||
let currentUntracked = [];
|
|
||||||
try {
|
|
||||||
const untrackedOutput = execSync(
|
|
||||||
"git ls-files --others --exclude-standard",
|
|
||||||
{
|
|
||||||
cwd: projectPath,
|
|
||||||
encoding: "utf-8",
|
|
||||||
}
|
|
||||||
).trim();
|
|
||||||
if (untrackedOutput) {
|
|
||||||
currentUntracked = untrackedOutput.split("\n").filter(Boolean);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log("[ContextManager] Error getting untracked files");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!initialState) {
|
|
||||||
// No initial state - all current changes are considered from this session
|
|
||||||
console.log(
|
|
||||||
"[ContextManager] No initial state found, returning all current changes"
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
newFiles: currentUntracked,
|
|
||||||
modifiedFiles: currentModified,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate files that are new since the session started
|
|
||||||
const initialModifiedSet = new Set(initialState.modifiedFiles || []);
|
|
||||||
const initialUntrackedSet = new Set(initialState.untrackedFiles || []);
|
|
||||||
|
|
||||||
// New files = current untracked - initial untracked
|
|
||||||
const newFiles = currentUntracked.filter(
|
|
||||||
(f) => !initialUntrackedSet.has(f)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Modified files = current modified - initial modified
|
|
||||||
const modifiedFiles = currentModified.filter(
|
|
||||||
(f) => !initialModifiedSet.has(f)
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`[ContextManager] Files changed during session for ${featureId}:`,
|
|
||||||
{
|
|
||||||
newFilesCount: newFiles.length,
|
|
||||||
modifiedFilesCount: modifiedFiles.length,
|
|
||||||
newFiles,
|
|
||||||
modifiedFiles,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return { newFiles, modifiedFiles };
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
"[ContextManager] Failed to calculate changed files:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return { newFiles: [], modifiedFiles: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new ContextManager();
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,500 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const fs = require("fs/promises");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feature Loader - Handles loading and managing features from individual feature folders
|
|
||||||
* Each feature is stored in .automaker/features/{featureId}/feature.json
|
|
||||||
*/
|
|
||||||
class FeatureLoader {
|
|
||||||
/**
|
|
||||||
* Get the features directory path
|
|
||||||
*/
|
|
||||||
getFeaturesDir(projectPath) {
|
|
||||||
return path.join(projectPath, ".automaker", "features");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the path to a specific feature folder
|
|
||||||
*/
|
|
||||||
getFeatureDir(projectPath, featureId) {
|
|
||||||
return path.join(this.getFeaturesDir(projectPath), featureId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the path to a feature's feature.json file
|
|
||||||
*/
|
|
||||||
getFeatureJsonPath(projectPath, featureId) {
|
|
||||||
return path.join(
|
|
||||||
this.getFeatureDir(projectPath, featureId),
|
|
||||||
"feature.json"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the path to a feature's agent-output.md file
|
|
||||||
*/
|
|
||||||
getAgentOutputPath(projectPath, featureId) {
|
|
||||||
return path.join(
|
|
||||||
this.getFeatureDir(projectPath, featureId),
|
|
||||||
"agent-output.md"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a new feature ID
|
|
||||||
*/
|
|
||||||
generateFeatureId() {
|
|
||||||
return `feature-${Date.now()}-${Math.random()
|
|
||||||
.toString(36)
|
|
||||||
.substring(2, 11)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensure all image paths for a feature are stored within the feature directory
|
|
||||||
*/
|
|
||||||
async ensureFeatureImages(projectPath, featureId, feature) {
|
|
||||||
if (
|
|
||||||
!feature ||
|
|
||||||
!Array.isArray(feature.imagePaths) ||
|
|
||||||
feature.imagePaths.length === 0
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const featureDir = this.getFeatureDir(projectPath, featureId);
|
|
||||||
const featureImagesDir = path.join(featureDir, "images");
|
|
||||||
await fs.mkdir(featureImagesDir, { recursive: true });
|
|
||||||
|
|
||||||
const updatedImagePaths = [];
|
|
||||||
|
|
||||||
for (const entry of feature.imagePaths) {
|
|
||||||
const isStringEntry = typeof entry === "string";
|
|
||||||
const currentPathValue = isStringEntry ? entry : entry.path;
|
|
||||||
|
|
||||||
if (!currentPathValue) {
|
|
||||||
updatedImagePaths.push(entry);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let resolvedCurrentPath = currentPathValue;
|
|
||||||
if (!path.isAbsolute(resolvedCurrentPath)) {
|
|
||||||
resolvedCurrentPath = path.join(projectPath, resolvedCurrentPath);
|
|
||||||
}
|
|
||||||
resolvedCurrentPath = path.normalize(resolvedCurrentPath);
|
|
||||||
|
|
||||||
// Skip if file doesn't exist
|
|
||||||
try {
|
|
||||||
await fs.access(resolvedCurrentPath);
|
|
||||||
} catch {
|
|
||||||
console.warn(
|
|
||||||
`[FeatureLoader] Image file missing for ${featureId}: ${resolvedCurrentPath}`
|
|
||||||
);
|
|
||||||
updatedImagePaths.push(entry);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const relativeToFeatureImages = path.relative(
|
|
||||||
featureImagesDir,
|
|
||||||
resolvedCurrentPath
|
|
||||||
);
|
|
||||||
const alreadyInFeatureDir =
|
|
||||||
relativeToFeatureImages === "" ||
|
|
||||||
(!relativeToFeatureImages.startsWith("..") &&
|
|
||||||
!path.isAbsolute(relativeToFeatureImages));
|
|
||||||
|
|
||||||
let finalPath = resolvedCurrentPath;
|
|
||||||
|
|
||||||
if (!alreadyInFeatureDir) {
|
|
||||||
const originalName = path.basename(resolvedCurrentPath);
|
|
||||||
let targetPath = path.join(featureImagesDir, originalName);
|
|
||||||
|
|
||||||
// Avoid overwriting files by appending a counter if needed
|
|
||||||
let counter = 1;
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
await fs.access(targetPath);
|
|
||||||
const parsed = path.parse(originalName);
|
|
||||||
targetPath = path.join(
|
|
||||||
featureImagesDir,
|
|
||||||
`${parsed.name}-${counter}${parsed.ext}`
|
|
||||||
);
|
|
||||||
counter += 1;
|
|
||||||
} catch {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.rename(resolvedCurrentPath, targetPath);
|
|
||||||
finalPath = targetPath;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(
|
|
||||||
`[FeatureLoader] Failed to move image ${resolvedCurrentPath}: ${error.message}`
|
|
||||||
);
|
|
||||||
updatedImagePaths.push(entry);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedImagePaths.push(
|
|
||||||
isStringEntry ? finalPath : { ...entry, path: finalPath }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
feature.imagePaths = updatedImagePaths;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all features for a project
|
|
||||||
*/
|
|
||||||
async getAll(projectPath) {
|
|
||||||
try {
|
|
||||||
const featuresDir = this.getFeaturesDir(projectPath);
|
|
||||||
|
|
||||||
// Check if features directory exists
|
|
||||||
try {
|
|
||||||
await fs.access(featuresDir);
|
|
||||||
} catch {
|
|
||||||
// Directory doesn't exist, return empty array
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read all feature directories
|
|
||||||
const entries = await fs.readdir(featuresDir, { withFileTypes: true });
|
|
||||||
const featureDirs = entries.filter((entry) => entry.isDirectory());
|
|
||||||
|
|
||||||
// Load each feature
|
|
||||||
const features = [];
|
|
||||||
for (const dir of featureDirs) {
|
|
||||||
const featureId = dir.name;
|
|
||||||
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Read feature.json directly - handle ENOENT in catch block
|
|
||||||
// This avoids TOCTOU race condition from checking with fs.access first
|
|
||||||
const content = await fs.readFile(featureJsonPath, "utf-8");
|
|
||||||
const feature = JSON.parse(content);
|
|
||||||
|
|
||||||
// Validate that the feature has required fields
|
|
||||||
if (!feature.id) {
|
|
||||||
console.warn(
|
|
||||||
`[FeatureLoader] Feature ${featureId} missing required 'id' field, skipping`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
features.push(feature);
|
|
||||||
} catch (error) {
|
|
||||||
// Handle different error types appropriately
|
|
||||||
if (error.code === "ENOENT") {
|
|
||||||
// File doesn't exist - this is expected for incomplete feature directories
|
|
||||||
// Skip silently (feature.json not yet created or was removed)
|
|
||||||
continue;
|
|
||||||
} else if (error instanceof SyntaxError) {
|
|
||||||
// JSON parse error - log as warning since file exists but is malformed
|
|
||||||
console.warn(
|
|
||||||
`[FeatureLoader] Failed to parse feature.json for ${featureId}: ${error.message}`
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// Other errors - log as error
|
|
||||||
console.error(
|
|
||||||
`[FeatureLoader] Failed to load feature ${featureId}:`,
|
|
||||||
error.message || error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// Continue loading other features
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort by creation order (feature IDs contain timestamp)
|
|
||||||
features.sort((a, b) => {
|
|
||||||
const aTime = a.id ? parseInt(a.id.split("-")[1] || "0") : 0;
|
|
||||||
const bTime = b.id ? parseInt(b.id.split("-")[1] || "0") : 0;
|
|
||||||
return aTime - bTime;
|
|
||||||
});
|
|
||||||
|
|
||||||
return features;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[FeatureLoader] Failed to get all features:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a single feature by ID
|
|
||||||
*/
|
|
||||||
async get(projectPath, featureId) {
|
|
||||||
try {
|
|
||||||
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
|
|
||||||
const content = await fs.readFile(featureJsonPath, "utf-8");
|
|
||||||
return JSON.parse(content);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === "ENOENT") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
console.error(
|
|
||||||
`[FeatureLoader] Failed to get feature ${featureId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new feature
|
|
||||||
*/
|
|
||||||
async create(projectPath, featureData) {
|
|
||||||
const featureId = featureData.id || this.generateFeatureId();
|
|
||||||
const featureDir = this.getFeatureDir(projectPath, featureId);
|
|
||||||
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
|
|
||||||
|
|
||||||
// Ensure features directory exists
|
|
||||||
const featuresDir = this.getFeaturesDir(projectPath);
|
|
||||||
await fs.mkdir(featuresDir, { recursive: true });
|
|
||||||
|
|
||||||
// Create feature directory
|
|
||||||
await fs.mkdir(featureDir, { recursive: true });
|
|
||||||
|
|
||||||
// Ensure feature has an ID
|
|
||||||
const feature = { ...featureData, id: featureId };
|
|
||||||
|
|
||||||
// Move any uploaded images into the feature directory
|
|
||||||
await this.ensureFeatureImages(projectPath, featureId, feature);
|
|
||||||
|
|
||||||
// Write feature.json
|
|
||||||
await fs.writeFile(
|
|
||||||
featureJsonPath,
|
|
||||||
JSON.stringify(feature, null, 2),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`[FeatureLoader] Created feature ${featureId}`);
|
|
||||||
return feature;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a feature (partial updates supported)
|
|
||||||
*/
|
|
||||||
async update(projectPath, featureId, updates) {
|
|
||||||
try {
|
|
||||||
const feature = await this.get(projectPath, featureId);
|
|
||||||
if (!feature) {
|
|
||||||
throw new Error(`Feature ${featureId} not found`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge updates
|
|
||||||
const updatedFeature = { ...feature, ...updates };
|
|
||||||
|
|
||||||
// Move any new images into the feature directory
|
|
||||||
await this.ensureFeatureImages(projectPath, featureId, updatedFeature);
|
|
||||||
|
|
||||||
// Write back to file
|
|
||||||
const featureJsonPath = this.getFeatureJsonPath(projectPath, featureId);
|
|
||||||
await fs.writeFile(
|
|
||||||
featureJsonPath,
|
|
||||||
JSON.stringify(updatedFeature, null, 2),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`[FeatureLoader] Updated feature ${featureId}`);
|
|
||||||
return updatedFeature;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[FeatureLoader] Failed to update feature ${featureId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a feature and its entire folder
|
|
||||||
*/
|
|
||||||
async delete(projectPath, featureId) {
|
|
||||||
try {
|
|
||||||
const featureDir = this.getFeatureDir(projectPath, featureId);
|
|
||||||
await fs.rm(featureDir, { recursive: true, force: true });
|
|
||||||
console.log(`[FeatureLoader] Deleted feature ${featureId}`);
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === "ENOENT") {
|
|
||||||
// Feature doesn't exist, that's fine
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.error(
|
|
||||||
`[FeatureLoader] Failed to delete feature ${featureId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get agent output for a feature
|
|
||||||
*/
|
|
||||||
async getAgentOutput(projectPath, featureId) {
|
|
||||||
try {
|
|
||||||
const agentOutputPath = this.getAgentOutputPath(projectPath, featureId);
|
|
||||||
const content = await fs.readFile(agentOutputPath, "utf-8");
|
|
||||||
return content;
|
|
||||||
} catch (error) {
|
|
||||||
if (error.code === "ENOENT") {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
console.error(
|
|
||||||
`[FeatureLoader] Failed to get agent output for ${featureId}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Legacy methods for backward compatibility (used by backend services)
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load all features for a project (legacy API)
|
|
||||||
* Features are stored in .automaker/features/{id}/feature.json
|
|
||||||
*/
|
|
||||||
async loadFeatures(projectPath) {
|
|
||||||
return await this.getAll(projectPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update feature status (legacy API)
|
|
||||||
* Features are stored in .automaker/features/{id}/feature.json
|
|
||||||
* Creates the feature if it doesn't exist.
|
|
||||||
* @param {string} featureId - The ID of the feature to update
|
|
||||||
* @param {string} status - The new status
|
|
||||||
* @param {string} projectPath - Path to the project
|
|
||||||
* @param {Object} options - Options object for optional parameters
|
|
||||||
* @param {string} [options.summary] - Optional summary of what was done
|
|
||||||
* @param {string} [options.error] - Optional error message if feature errored
|
|
||||||
* @param {string} [options.description] - Optional detailed description
|
|
||||||
* @param {string} [options.category] - Optional category/phase
|
|
||||||
* @param {string[]} [options.steps] - Optional array of implementation steps
|
|
||||||
*/
|
|
||||||
async updateFeatureStatus(featureId, status, projectPath, options = {}) {
|
|
||||||
const { summary, error, description, category, steps } = options;
|
|
||||||
// Check if feature exists
|
|
||||||
const existingFeature = await this.get(projectPath, featureId);
|
|
||||||
|
|
||||||
if (!existingFeature) {
|
|
||||||
// Feature doesn't exist - create it with all required fields
|
|
||||||
console.log(`[FeatureLoader] Feature ${featureId} not found - creating new feature`);
|
|
||||||
const newFeature = {
|
|
||||||
id: featureId,
|
|
||||||
title: featureId.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '),
|
|
||||||
description: description || summary || '', // Use provided description, fall back to summary
|
|
||||||
category: category || "Uncategorized",
|
|
||||||
steps: steps || [],
|
|
||||||
status: status,
|
|
||||||
images: [],
|
|
||||||
imagePaths: [],
|
|
||||||
skipTests: false, // Auto-generated features should run tests by default
|
|
||||||
model: "sonnet",
|
|
||||||
thinkingLevel: "none",
|
|
||||||
summary: summary || description || '',
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
if (error !== undefined) {
|
|
||||||
newFeature.error = error;
|
|
||||||
}
|
|
||||||
await this.create(projectPath, newFeature);
|
|
||||||
console.log(
|
|
||||||
`[FeatureLoader] Created feature ${featureId}: status=${status}, category=${category || "Uncategorized"}, steps=${steps?.length || 0}${
|
|
||||||
summary ? `, summary="${summary}"` : ""
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Feature exists - update it
|
|
||||||
const updates = { status };
|
|
||||||
if (summary !== undefined) {
|
|
||||||
updates.summary = summary;
|
|
||||||
// Also update description if it's empty or not set
|
|
||||||
if (!existingFeature.description) {
|
|
||||||
updates.description = summary;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (description !== undefined) {
|
|
||||||
updates.description = description;
|
|
||||||
}
|
|
||||||
if (category !== undefined) {
|
|
||||||
updates.category = category;
|
|
||||||
}
|
|
||||||
if (steps !== undefined && Array.isArray(steps)) {
|
|
||||||
updates.steps = steps;
|
|
||||||
}
|
|
||||||
if (error !== undefined) {
|
|
||||||
updates.error = error;
|
|
||||||
} else {
|
|
||||||
// Clear error if not provided
|
|
||||||
if (existingFeature.error) {
|
|
||||||
updates.error = undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure required fields exist (for features created before this fix)
|
|
||||||
if (!existingFeature.category && !updates.category) updates.category = "Uncategorized";
|
|
||||||
if (!existingFeature.steps && !updates.steps) updates.steps = [];
|
|
||||||
if (!existingFeature.images) updates.images = [];
|
|
||||||
if (!existingFeature.imagePaths) updates.imagePaths = [];
|
|
||||||
if (existingFeature.skipTests === undefined) updates.skipTests = false;
|
|
||||||
if (!existingFeature.model) updates.model = "sonnet";
|
|
||||||
if (!existingFeature.thinkingLevel) updates.thinkingLevel = "none";
|
|
||||||
|
|
||||||
await this.update(projectPath, featureId, updates);
|
|
||||||
console.log(
|
|
||||||
`[FeatureLoader] Updated feature ${featureId}: status=${status}${
|
|
||||||
category ? `, category="${category}"` : ""
|
|
||||||
}${steps ? `, steps=${steps.length}` : ""}${
|
|
||||||
summary ? `, summary="${summary}"` : ""
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Select the next feature to implement
|
|
||||||
* Prioritizes: earlier features in the list that are not verified or waiting_approval
|
|
||||||
*/
|
|
||||||
selectNextFeature(features) {
|
|
||||||
// Find first feature that is in backlog or in_progress status
|
|
||||||
// Skip verified and waiting_approval (which needs user input)
|
|
||||||
return features.find(
|
|
||||||
(f) => f.status !== "verified" && f.status !== "waiting_approval"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update worktree info for a feature (legacy API)
|
|
||||||
* Features are stored in .automaker/features/{id}/feature.json
|
|
||||||
* @param {string} featureId - The ID of the feature to update
|
|
||||||
* @param {string} projectPath - Path to the project
|
|
||||||
* @param {string|null} worktreePath - Path to the worktree (null to clear)
|
|
||||||
* @param {string|null} branchName - Name of the feature branch (null to clear)
|
|
||||||
*/
|
|
||||||
async updateFeatureWorktree(
|
|
||||||
featureId,
|
|
||||||
projectPath,
|
|
||||||
worktreePath,
|
|
||||||
branchName
|
|
||||||
) {
|
|
||||||
const updates = {};
|
|
||||||
if (worktreePath) {
|
|
||||||
updates.worktreePath = worktreePath;
|
|
||||||
updates.branchName = branchName;
|
|
||||||
} else {
|
|
||||||
updates.worktreePath = null;
|
|
||||||
updates.branchName = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.update(projectPath, featureId, updates);
|
|
||||||
console.log(
|
|
||||||
`[FeatureLoader] Updated feature ${featureId}: worktreePath=${worktreePath}, branchName=${branchName}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new FeatureLoader();
|
|
||||||
@@ -1,379 +0,0 @@
|
|||||||
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
|
|
||||||
const promptBuilder = require("./prompt-builder");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feature Suggestions Service - Analyzes project and generates feature suggestions
|
|
||||||
*/
|
|
||||||
class FeatureSuggestionsService {
|
|
||||||
constructor() {
|
|
||||||
this.runningAnalysis = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate feature suggestions by analyzing the project
|
|
||||||
* @param {string} projectPath - Path to the project
|
|
||||||
* @param {Function} sendToRenderer - Function to send events to renderer
|
|
||||||
* @param {Object} execution - Execution context with abort controller
|
|
||||||
* @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance"
|
|
||||||
*/
|
|
||||||
async generateSuggestions(projectPath, sendToRenderer, execution, suggestionType = "features") {
|
|
||||||
console.log(
|
|
||||||
`[FeatureSuggestions] Generating ${suggestionType} suggestions for: ${projectPath}`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const abortController = new AbortController();
|
|
||||||
execution.abortController = abortController;
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
model: "claude-sonnet-4-20250514",
|
|
||||||
systemPrompt: this.getSystemPrompt(suggestionType),
|
|
||||||
maxTurns: 50,
|
|
||||||
cwd: projectPath,
|
|
||||||
allowedTools: ["Read", "Glob", "Grep", "Bash"],
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
sandbox: {
|
|
||||||
enabled: true,
|
|
||||||
autoAllowBashIfSandboxed: true,
|
|
||||||
},
|
|
||||||
abortController: abortController,
|
|
||||||
};
|
|
||||||
|
|
||||||
const prompt = this.buildAnalysisPrompt(suggestionType);
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "suggestions_progress",
|
|
||||||
content: "Starting project analysis...\n",
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentQuery = query({ prompt, options });
|
|
||||||
execution.query = currentQuery;
|
|
||||||
|
|
||||||
let fullResponse = "";
|
|
||||||
for await (const msg of currentQuery) {
|
|
||||||
if (!execution.isActive()) break;
|
|
||||||
|
|
||||||
if (msg.type === "assistant" && msg.message?.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
if (block.type === "text") {
|
|
||||||
fullResponse += block.text;
|
|
||||||
sendToRenderer({
|
|
||||||
type: "suggestions_progress",
|
|
||||||
content: block.text,
|
|
||||||
});
|
|
||||||
} else if (block.type === "tool_use") {
|
|
||||||
sendToRenderer({
|
|
||||||
type: "suggestions_tool",
|
|
||||||
tool: block.name,
|
|
||||||
input: block.input,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
execution.query = null;
|
|
||||||
execution.abortController = null;
|
|
||||||
|
|
||||||
// Parse the suggestions from the response
|
|
||||||
const suggestions = this.parseSuggestions(fullResponse);
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "suggestions_complete",
|
|
||||||
suggestions: suggestions,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
suggestions: suggestions,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AbortError || error?.name === "AbortError") {
|
|
||||||
console.log("[FeatureSuggestions] Analysis aborted");
|
|
||||||
if (execution) {
|
|
||||||
execution.abortController = null;
|
|
||||||
execution.query = null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Analysis aborted",
|
|
||||||
suggestions: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error(
|
|
||||||
"[FeatureSuggestions] Error generating suggestions:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
if (execution) {
|
|
||||||
execution.abortController = null;
|
|
||||||
execution.query = null;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse suggestions from the LLM response
|
|
||||||
* Looks for JSON array in the response
|
|
||||||
*/
|
|
||||||
parseSuggestions(response) {
|
|
||||||
try {
|
|
||||||
// Try to find JSON array in the response
|
|
||||||
// Look for ```json ... ``` blocks first
|
|
||||||
const jsonBlockMatch = response.match(/```json\s*([\s\S]*?)```/);
|
|
||||||
if (jsonBlockMatch) {
|
|
||||||
const parsed = JSON.parse(jsonBlockMatch[1].trim());
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
return this.validateSuggestions(parsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to find a raw JSON array
|
|
||||||
const jsonArrayMatch = response.match(/\[\s*\{[\s\S]*\}\s*\]/);
|
|
||||||
if (jsonArrayMatch) {
|
|
||||||
const parsed = JSON.parse(jsonArrayMatch[0]);
|
|
||||||
if (Array.isArray(parsed)) {
|
|
||||||
return this.validateSuggestions(parsed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.warn(
|
|
||||||
"[FeatureSuggestions] Could not parse suggestions from response"
|
|
||||||
);
|
|
||||||
return [];
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[FeatureSuggestions] Error parsing suggestions:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate and normalize suggestions
|
|
||||||
*/
|
|
||||||
validateSuggestions(suggestions) {
|
|
||||||
return suggestions
|
|
||||||
.filter((s) => s && typeof s === "object")
|
|
||||||
.map((s, index) => ({
|
|
||||||
id: `suggestion-${Date.now()}-${index}`,
|
|
||||||
category: s.category || "Uncategorized",
|
|
||||||
description: s.description || s.title || "No description",
|
|
||||||
steps: Array.isArray(s.steps) ? s.steps : [],
|
|
||||||
priority: typeof s.priority === "number" ? s.priority : index + 1,
|
|
||||||
reasoning: s.reasoning || "",
|
|
||||||
}))
|
|
||||||
.sort((a, b) => a.priority - b.priority);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the system prompt for feature suggestion analysis
|
|
||||||
* @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance"
|
|
||||||
*/
|
|
||||||
getSystemPrompt(suggestionType = "features") {
|
|
||||||
const basePrompt = `You are an expert software architect. Your job is to analyze a codebase and provide actionable suggestions.
|
|
||||||
|
|
||||||
You have access to file reading and search tools. Use them to understand the codebase.
|
|
||||||
|
|
||||||
When analyzing, look at:
|
|
||||||
- README files and documentation
|
|
||||||
- Package.json, cargo.toml, or similar config files for tech stack
|
|
||||||
- Source code structure and organization
|
|
||||||
- Existing code patterns and implementation styles`;
|
|
||||||
|
|
||||||
switch (suggestionType) {
|
|
||||||
case "refactoring":
|
|
||||||
return `${basePrompt}
|
|
||||||
|
|
||||||
Your specific focus is on **refactoring suggestions**. You should:
|
|
||||||
1. Identify code smells and areas that need cleanup
|
|
||||||
2. Find duplicated code that could be consolidated
|
|
||||||
3. Spot overly complex functions or classes that should be broken down
|
|
||||||
4. Look for inconsistent naming conventions or coding patterns
|
|
||||||
5. Find opportunities to improve code organization and modularity
|
|
||||||
6. Identify violations of SOLID principles or common design patterns
|
|
||||||
7. Look for dead code or unused dependencies
|
|
||||||
|
|
||||||
Prioritize suggestions by:
|
|
||||||
- Impact on maintainability
|
|
||||||
- Risk level (lower risk refactorings first)
|
|
||||||
- Complexity of the refactoring`;
|
|
||||||
|
|
||||||
case "security":
|
|
||||||
return `${basePrompt}
|
|
||||||
|
|
||||||
Your specific focus is on **security vulnerabilities and improvements**. You should:
|
|
||||||
1. Identify potential security vulnerabilities (OWASP Top 10)
|
|
||||||
2. Look for hardcoded secrets, API keys, or credentials
|
|
||||||
3. Check for proper input validation and sanitization
|
|
||||||
4. Identify SQL injection, XSS, or command injection risks
|
|
||||||
5. Review authentication and authorization patterns
|
|
||||||
6. Check for secure communication (HTTPS, encryption)
|
|
||||||
7. Look for insecure dependencies or outdated packages
|
|
||||||
8. Review error handling that might leak sensitive information
|
|
||||||
9. Check for proper session management
|
|
||||||
10. Identify insecure file handling or path traversal risks
|
|
||||||
|
|
||||||
Prioritize by severity:
|
|
||||||
- Critical: Exploitable vulnerabilities with high impact
|
|
||||||
- High: Security issues that could lead to data exposure
|
|
||||||
- Medium: Best practice violations that weaken security
|
|
||||||
- Low: Minor improvements to security posture`;
|
|
||||||
|
|
||||||
case "performance":
|
|
||||||
return `${basePrompt}
|
|
||||||
|
|
||||||
Your specific focus is on **performance issues and optimizations**. You should:
|
|
||||||
1. Identify N+1 query problems or inefficient database access
|
|
||||||
2. Look for unnecessary re-renders in React/frontend code
|
|
||||||
3. Find opportunities for caching or memoization
|
|
||||||
4. Identify large bundle sizes or unoptimized imports
|
|
||||||
5. Look for blocking operations that could be async
|
|
||||||
6. Find memory leaks or inefficient memory usage
|
|
||||||
7. Identify slow algorithms or data structure choices
|
|
||||||
8. Look for missing indexes in database schemas
|
|
||||||
9. Find opportunities for lazy loading or code splitting
|
|
||||||
10. Identify unnecessary network requests or API calls
|
|
||||||
|
|
||||||
Prioritize by:
|
|
||||||
- Impact on user experience
|
|
||||||
- Frequency of the slow path
|
|
||||||
- Ease of implementation`;
|
|
||||||
|
|
||||||
default: // "features"
|
|
||||||
return `${basePrompt}
|
|
||||||
|
|
||||||
Your specific focus is on **missing features and improvements**. You should:
|
|
||||||
1. Identify what the application does and what features it currently has
|
|
||||||
2. Look at the .automaker/app_spec.txt file if it exists
|
|
||||||
3. Generate a comprehensive list of missing features that would be valuable to users
|
|
||||||
4. Consider user experience improvements
|
|
||||||
5. Consider developer experience improvements
|
|
||||||
6. Look at common patterns in similar applications
|
|
||||||
|
|
||||||
Prioritize features by:
|
|
||||||
- Impact on users
|
|
||||||
- Alignment with project goals
|
|
||||||
- Complexity of implementation`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the prompt for analyzing the project
|
|
||||||
* @param {string} suggestionType - Type of suggestions: "features", "refactoring", "security", "performance"
|
|
||||||
*/
|
|
||||||
buildAnalysisPrompt(suggestionType = "features") {
|
|
||||||
const commonIntro = `Analyze this project and generate a list of actionable suggestions.
|
|
||||||
|
|
||||||
**Your Task:**
|
|
||||||
|
|
||||||
1. First, explore the project structure:
|
|
||||||
- Read README.md, package.json, or similar config files
|
|
||||||
- Scan the source code directory structure
|
|
||||||
- Identify the tech stack and frameworks used
|
|
||||||
- Look at existing code and how it's implemented
|
|
||||||
|
|
||||||
2. Identify what the application does:
|
|
||||||
- What is the main purpose?
|
|
||||||
- What patterns and conventions are used?
|
|
||||||
`;
|
|
||||||
|
|
||||||
const commonOutput = `
|
|
||||||
**CRITICAL: Output your suggestions as a JSON array** at the end of your response, formatted like this:
|
|
||||||
|
|
||||||
\`\`\`json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"category": "Category Name",
|
|
||||||
"description": "Clear description of the suggestion",
|
|
||||||
"steps": [
|
|
||||||
"Step 1 to implement",
|
|
||||||
"Step 2 to implement",
|
|
||||||
"Step 3 to implement"
|
|
||||||
],
|
|
||||||
"priority": 1,
|
|
||||||
"reasoning": "Why this is important"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Important Guidelines:**
|
|
||||||
- Generate at least 10-15 suggestions
|
|
||||||
- Order them by priority (1 = highest priority)
|
|
||||||
- Each suggestion should have clear, actionable steps
|
|
||||||
- Be specific about what files might need to be modified
|
|
||||||
- Consider the existing tech stack and patterns
|
|
||||||
|
|
||||||
Begin by exploring the project structure.`;
|
|
||||||
|
|
||||||
switch (suggestionType) {
|
|
||||||
case "refactoring":
|
|
||||||
return `${commonIntro}
|
|
||||||
3. Look for refactoring opportunities:
|
|
||||||
- Find code duplication across the codebase
|
|
||||||
- Identify functions or classes that are too long or complex
|
|
||||||
- Look for inconsistent patterns or naming conventions
|
|
||||||
- Find tightly coupled code that should be decoupled
|
|
||||||
- Identify opportunities to extract reusable utilities
|
|
||||||
- Look for dead code or unused exports
|
|
||||||
- Check for proper separation of concerns
|
|
||||||
|
|
||||||
Categories to use: "Code Smell", "Duplication", "Complexity", "Architecture", "Naming", "Dead Code", "Coupling", "Testing"
|
|
||||||
${commonOutput}`;
|
|
||||||
|
|
||||||
case "security":
|
|
||||||
return `${commonIntro}
|
|
||||||
3. Look for security issues:
|
|
||||||
- Check for hardcoded secrets or API keys
|
|
||||||
- Look for potential injection vulnerabilities (SQL, XSS, command)
|
|
||||||
- Review authentication and authorization code
|
|
||||||
- Check input validation and sanitization
|
|
||||||
- Look for insecure dependencies
|
|
||||||
- Review error handling for information leakage
|
|
||||||
- Check for proper HTTPS/TLS usage
|
|
||||||
- Look for insecure file operations
|
|
||||||
|
|
||||||
Categories to use: "Critical", "High", "Medium", "Low" (based on severity)
|
|
||||||
${commonOutput}`;
|
|
||||||
|
|
||||||
case "performance":
|
|
||||||
return `${commonIntro}
|
|
||||||
3. Look for performance issues:
|
|
||||||
- Find N+1 queries or inefficient database access patterns
|
|
||||||
- Look for unnecessary re-renders in React components
|
|
||||||
- Identify missing memoization opportunities
|
|
||||||
- Check bundle size and import patterns
|
|
||||||
- Look for synchronous operations that could be async
|
|
||||||
- Find potential memory leaks
|
|
||||||
- Identify slow algorithms or data structures
|
|
||||||
- Look for missing caching opportunities
|
|
||||||
- Check for unnecessary network requests
|
|
||||||
|
|
||||||
Categories to use: "Database", "Rendering", "Memory", "Bundle Size", "Caching", "Algorithm", "Network"
|
|
||||||
${commonOutput}`;
|
|
||||||
|
|
||||||
default: // "features"
|
|
||||||
return `${commonIntro}
|
|
||||||
3. Generate feature suggestions:
|
|
||||||
- Think about what's missing compared to similar applications
|
|
||||||
- Consider user experience improvements
|
|
||||||
- Consider developer experience improvements
|
|
||||||
- Think about performance, security, and reliability
|
|
||||||
- Consider testing and documentation improvements
|
|
||||||
|
|
||||||
Categories to use: "User Experience", "Performance", "Security", "Testing", "Documentation", "Developer Experience", "Accessibility", etc.
|
|
||||||
${commonOutput}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the current analysis
|
|
||||||
*/
|
|
||||||
stop() {
|
|
||||||
if (this.runningAnalysis && this.runningAnalysis.abortController) {
|
|
||||||
this.runningAnalysis.abortController.abort();
|
|
||||||
}
|
|
||||||
this.runningAnalysis = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new FeatureSuggestionsService();
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
|
|
||||||
const promptBuilder = require("./prompt-builder");
|
|
||||||
const contextManager = require("./context-manager");
|
|
||||||
const featureLoader = require("./feature-loader");
|
|
||||||
const mcpServerFactory = require("./mcp-server-factory");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feature Verifier - Handles feature verification by running tests
|
|
||||||
*/
|
|
||||||
class FeatureVerifier {
|
|
||||||
/**
|
|
||||||
* Verify feature tests (runs tests and checks if they pass)
|
|
||||||
*/
|
|
||||||
async verifyFeatureTests(feature, projectPath, sendToRenderer, execution) {
|
|
||||||
console.log(
|
|
||||||
`[FeatureVerifier] Verifying tests for: ${feature.description}`
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const verifyMsg = `\n✅ Verifying tests for: ${feature.description}\n`;
|
|
||||||
await contextManager.writeToContextFile(
|
|
||||||
projectPath,
|
|
||||||
feature.id,
|
|
||||||
verifyMsg
|
|
||||||
);
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_phase",
|
|
||||||
featureId: feature.id,
|
|
||||||
phase: "verification",
|
|
||||||
message: `Verifying tests for: ${feature.description}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
|
||||||
execution.abortController = abortController;
|
|
||||||
|
|
||||||
// Create custom MCP server with UpdateFeatureStatus tool
|
|
||||||
const featureToolsServer = mcpServerFactory.createFeatureToolsServer(
|
|
||||||
featureLoader.updateFeatureStatus.bind(featureLoader),
|
|
||||||
projectPath
|
|
||||||
);
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
model: "claude-opus-4-5-20251101",
|
|
||||||
systemPrompt: await promptBuilder.getVerificationPrompt(projectPath),
|
|
||||||
maxTurns: 1000,
|
|
||||||
cwd: projectPath,
|
|
||||||
mcpServers: {
|
|
||||||
"automaker-tools": featureToolsServer,
|
|
||||||
},
|
|
||||||
allowedTools: [
|
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"mcp__automaker-tools__UpdateFeatureStatus",
|
|
||||||
],
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
sandbox: {
|
|
||||||
enabled: true,
|
|
||||||
autoAllowBashIfSandboxed: true,
|
|
||||||
},
|
|
||||||
abortController: abortController,
|
|
||||||
};
|
|
||||||
|
|
||||||
const prompt = await promptBuilder.buildVerificationPrompt(
|
|
||||||
feature,
|
|
||||||
projectPath
|
|
||||||
);
|
|
||||||
|
|
||||||
const runningTestsMsg =
|
|
||||||
"Running Playwright tests to verify feature implementation...\n";
|
|
||||||
await contextManager.writeToContextFile(
|
|
||||||
projectPath,
|
|
||||||
feature.id,
|
|
||||||
runningTestsMsg
|
|
||||||
);
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_progress",
|
|
||||||
featureId: feature.id,
|
|
||||||
content: runningTestsMsg,
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentQuery = query({ prompt, options });
|
|
||||||
execution.query = currentQuery;
|
|
||||||
|
|
||||||
let responseText = "";
|
|
||||||
for await (const msg of currentQuery) {
|
|
||||||
// Check if this specific feature was aborted
|
|
||||||
if (!execution.isActive()) break;
|
|
||||||
|
|
||||||
if (msg.type === "assistant" && msg.message?.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
if (block.type === "text") {
|
|
||||||
responseText += block.text;
|
|
||||||
|
|
||||||
await contextManager.writeToContextFile(
|
|
||||||
projectPath,
|
|
||||||
feature.id,
|
|
||||||
block.text
|
|
||||||
);
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_progress",
|
|
||||||
featureId: feature.id,
|
|
||||||
content: block.text,
|
|
||||||
});
|
|
||||||
} else if (block.type === "tool_use") {
|
|
||||||
const toolMsg = `\n🔧 Tool: ${block.name}\n`;
|
|
||||||
await contextManager.writeToContextFile(
|
|
||||||
projectPath,
|
|
||||||
feature.id,
|
|
||||||
toolMsg
|
|
||||||
);
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_tool",
|
|
||||||
featureId: feature.id,
|
|
||||||
tool: block.name,
|
|
||||||
input: block.input,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
execution.query = null;
|
|
||||||
execution.abortController = null;
|
|
||||||
|
|
||||||
// Re-load features to check if it was marked as verified or waiting_approval (for skipTests)
|
|
||||||
const updatedFeatures = await featureLoader.loadFeatures(projectPath);
|
|
||||||
const updatedFeature = updatedFeatures.find((f) => f.id === feature.id);
|
|
||||||
// For skipTests features, waiting_approval is also considered a success
|
|
||||||
const passes =
|
|
||||||
updatedFeature?.status === "verified" ||
|
|
||||||
(updatedFeature?.skipTests &&
|
|
||||||
updatedFeature?.status === "waiting_approval");
|
|
||||||
|
|
||||||
const finalMsg = passes
|
|
||||||
? "✓ Verification successful: All tests passed\n"
|
|
||||||
: "✗ Tests failed or not all passing - feature remains in progress\n";
|
|
||||||
|
|
||||||
await contextManager.writeToContextFile(
|
|
||||||
projectPath,
|
|
||||||
feature.id,
|
|
||||||
finalMsg
|
|
||||||
);
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_progress",
|
|
||||||
featureId: feature.id,
|
|
||||||
content: finalMsg,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
passes,
|
|
||||||
message: responseText.substring(0, 500),
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AbortError || error?.name === "AbortError") {
|
|
||||||
console.log("[FeatureVerifier] Verification aborted");
|
|
||||||
if (execution) {
|
|
||||||
execution.abortController = null;
|
|
||||||
execution.query = null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
passes: false,
|
|
||||||
message: "Verification aborted",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("[FeatureVerifier] Error verifying feature:", error);
|
|
||||||
if (execution) {
|
|
||||||
execution.abortController = null;
|
|
||||||
execution.query = null;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new FeatureVerifier();
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
const { createSdkMcpServer, tool } = require("@anthropic-ai/claude-agent-sdk");
|
|
||||||
const { z } = require("zod");
|
|
||||||
const featureLoader = require("./feature-loader");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* MCP Server Factory - Creates custom MCP servers with tools
|
|
||||||
*/
|
|
||||||
class McpServerFactory {
|
|
||||||
/**
|
|
||||||
* Create a custom MCP server with the UpdateFeatureStatus tool
|
|
||||||
* This tool allows Claude Code to safely update feature status without
|
|
||||||
* directly modifying feature files, preventing race conditions
|
|
||||||
* and accidental state corruption.
|
|
||||||
*/
|
|
||||||
createFeatureToolsServer(updateFeatureStatusCallback, projectPath) {
|
|
||||||
return createSdkMcpServer({
|
|
||||||
name: "automaker-tools",
|
|
||||||
version: "1.0.0",
|
|
||||||
tools: [
|
|
||||||
tool(
|
|
||||||
"UpdateFeatureStatus",
|
|
||||||
"Create or update a feature. Use this tool to create new features with detailed information or update existing feature status. When creating features, provide comprehensive description, category, and implementation steps.",
|
|
||||||
{
|
|
||||||
featureId: z.string().describe("The ID of the feature (lowercase, hyphens for spaces). Example: 'user-authentication', 'budget-tracking'"),
|
|
||||||
status: z.enum(["backlog", "todo", "in_progress", "verified"]).describe("The status for the feature. For NEW features, ONLY use 'backlog' or 'verified'. NEVER use 'in_progress' for new features - the user will manually start them."),
|
|
||||||
summary: z.string().optional().describe("A brief summary of what was implemented/changed or what the feature does."),
|
|
||||||
description: z.string().optional().describe("A detailed description of the feature. Be comprehensive - explain what the feature does, its purpose, and key functionality."),
|
|
||||||
category: z.string().optional().describe("The category/phase for this feature. Example: 'Phase 1: Foundation', 'Phase 2: Core Logic', 'Phase 3: Polish', 'Authentication', 'UI/UX'"),
|
|
||||||
steps: z.array(z.string()).optional().describe("Array of implementation steps. Each step should be a clear, actionable task. Example: ['Set up database schema', 'Create API endpoints', 'Build UI components', 'Add validation']")
|
|
||||||
},
|
|
||||||
async (args) => {
|
|
||||||
try {
|
|
||||||
console.log(`[McpServerFactory] UpdateFeatureStatus tool called: featureId=${args.featureId}, status=${args.status}, summary=${args.summary || "(none)"}, category=${args.category || "(none)"}, steps=${args.steps?.length || 0}`);
|
|
||||||
console.log(`[Feature Creation] Creating/updating feature "${args.featureId}" with status "${args.status}"`);
|
|
||||||
|
|
||||||
// Load the feature to check skipTests flag
|
|
||||||
const features = await featureLoader.loadFeatures(projectPath);
|
|
||||||
const feature = features.find((f) => f.id === args.featureId);
|
|
||||||
|
|
||||||
if (!feature) {
|
|
||||||
console.log(`[Feature Creation] Feature ${args.featureId} not found - this is a new feature being created`);
|
|
||||||
// This is a new feature - enforce backlog status for any non-verified features
|
|
||||||
}
|
|
||||||
|
|
||||||
// If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval
|
|
||||||
let finalStatus = args.status;
|
|
||||||
// For NEW features: Convert 'todo' or 'in_progress' to 'backlog' for consistency
|
|
||||||
// New features should ALWAYS go to backlog first, user must manually start them
|
|
||||||
if (!feature && (finalStatus === "todo" || finalStatus === "in_progress")) {
|
|
||||||
console.log(`[Feature Creation] New feature ${args.featureId} - converting "${finalStatus}" to "backlog" (user must manually start features)`);
|
|
||||||
finalStatus = "backlog";
|
|
||||||
}
|
|
||||||
if (feature && args.status === "verified" && feature.skipTests === true) {
|
|
||||||
console.log(`[McpServerFactory] Feature ${args.featureId} has skipTests=true, converting verified -> waiting_approval`);
|
|
||||||
finalStatus = "waiting_approval";
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMPORTANT: Prevent agent from moving an in_progress feature back to backlog
|
|
||||||
// When a feature is being worked on, the agent should only be able to mark it as verified
|
|
||||||
// (which may be converted to waiting_approval for skipTests features)
|
|
||||||
// This prevents the agent from incorrectly putting completed work back in the backlog
|
|
||||||
if (feature && feature.status === "in_progress" && (args.status === "backlog" || args.status === "todo")) {
|
|
||||||
console.log(`[McpServerFactory] Feature ${args.featureId} is in_progress - preventing move to ${args.status}, converting to waiting_approval instead`);
|
|
||||||
finalStatus = "waiting_approval";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the provided callback to update feature status
|
|
||||||
await updateFeatureStatusCallback(
|
|
||||||
args.featureId,
|
|
||||||
finalStatus,
|
|
||||||
projectPath,
|
|
||||||
{
|
|
||||||
summary: args.summary,
|
|
||||||
description: args.description,
|
|
||||||
category: args.category,
|
|
||||||
steps: args.steps,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const statusMessage = finalStatus !== args.status
|
|
||||||
? `Successfully created/updated feature ${args.featureId} to status "${finalStatus}" (converted from "${args.status}")${args.summary ? ` - ${args.summary}` : ""}`
|
|
||||||
: `Successfully created/updated feature ${args.featureId} to status "${finalStatus}"${args.summary ? ` - ${args.summary}` : ""}`;
|
|
||||||
|
|
||||||
console.log(`[Feature Creation] ✓ ${statusMessage}`);
|
|
||||||
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: "text",
|
|
||||||
text: statusMessage
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[McpServerFactory] UpdateFeatureStatus tool error:", error);
|
|
||||||
console.error(`[Feature Creation] ✗ Failed to create/update feature ${args.featureId}: ${error.message}`);
|
|
||||||
return {
|
|
||||||
content: [{
|
|
||||||
type: "text",
|
|
||||||
text: `Failed to update feature status: ${error.message}`
|
|
||||||
}]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new McpServerFactory();
|
|
||||||
@@ -1,358 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
/**
|
|
||||||
* Standalone STDIO MCP Server for Automaker Tools
|
|
||||||
*
|
|
||||||
* This script runs as a standalone process and communicates via JSON-RPC 2.0
|
|
||||||
* over stdin/stdout. It implements the MCP protocol to expose the UpdateFeatureStatus
|
|
||||||
* tool to Codex CLI.
|
|
||||||
*
|
|
||||||
* Environment variables:
|
|
||||||
* - AUTOMAKER_PROJECT_PATH: Path to the project directory
|
|
||||||
* - AUTOMAKER_IPC_CHANNEL: IPC channel name for callback communication (optional, uses default)
|
|
||||||
*/
|
|
||||||
|
|
||||||
const readline = require('readline');
|
|
||||||
const path = require('path');
|
|
||||||
|
|
||||||
// Redirect all console.log output to stderr to avoid polluting MCP stdout
|
|
||||||
const originalConsoleLog = console.log;
|
|
||||||
console.log = (...args) => {
|
|
||||||
console.error(...args);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set up readline interface for line-by-line JSON-RPC input
|
|
||||||
// IMPORTANT: Use a separate output stream for readline to avoid interfering with JSON-RPC stdout
|
|
||||||
// We'll write JSON-RPC responses directly to stdout, not through readline
|
|
||||||
const rl = readline.createInterface({
|
|
||||||
input: process.stdin,
|
|
||||||
output: null, // Don't use stdout for readline output
|
|
||||||
terminal: false
|
|
||||||
});
|
|
||||||
|
|
||||||
let initialized = false;
|
|
||||||
let projectPath = null;
|
|
||||||
let ipcChannel = null;
|
|
||||||
|
|
||||||
// Get configuration from environment
|
|
||||||
projectPath = process.env.AUTOMAKER_PROJECT_PATH || process.cwd();
|
|
||||||
ipcChannel = process.env.AUTOMAKER_IPC_CHANNEL || 'mcp:update-feature-status';
|
|
||||||
|
|
||||||
// Load dependencies (these will be available in the Electron app context)
|
|
||||||
let featureLoader;
|
|
||||||
let electron;
|
|
||||||
|
|
||||||
// Try to load Electron IPC if available (when running from Electron app)
|
|
||||||
try {
|
|
||||||
// In Electron, we can use IPC directly
|
|
||||||
if (typeof require !== 'undefined') {
|
|
||||||
// Check if we're in Electron context
|
|
||||||
const electronModule = require('electron');
|
|
||||||
if (electronModule && electronModule.ipcMain) {
|
|
||||||
electron = electronModule;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Not in Electron context, will use alternative method
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load feature loader
|
|
||||||
// Try multiple paths since this script might be run from different contexts
|
|
||||||
try {
|
|
||||||
// First try relative path (when run from electron/services/)
|
|
||||||
featureLoader = require('./feature-loader');
|
|
||||||
} catch (e) {
|
|
||||||
try {
|
|
||||||
// Try absolute path resolution
|
|
||||||
const featureLoaderPath = path.resolve(__dirname, 'feature-loader.js');
|
|
||||||
delete require.cache[require.resolve(featureLoaderPath)];
|
|
||||||
featureLoader = require(featureLoaderPath);
|
|
||||||
} catch (e2) {
|
|
||||||
// If still fails, try from parent directory
|
|
||||||
try {
|
|
||||||
featureLoader = require(path.join(__dirname, '..', 'services', 'feature-loader'));
|
|
||||||
} catch (e3) {
|
|
||||||
console.error('[McpServerStdio] Error loading feature-loader:', e3.message);
|
|
||||||
console.error('[McpServerStdio] Tried paths:', [
|
|
||||||
'./feature-loader',
|
|
||||||
path.resolve(__dirname, 'feature-loader.js'),
|
|
||||||
path.join(__dirname, '..', 'services', 'feature-loader')
|
|
||||||
]);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send JSON-RPC response
|
|
||||||
* CRITICAL: Must write directly to stdout, not via console.log
|
|
||||||
* MCP protocol requires ONLY JSON-RPC messages on stdout
|
|
||||||
*/
|
|
||||||
function sendResponse(id, result, error = null) {
|
|
||||||
const response = {
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
id
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
response.error = error;
|
|
||||||
} else {
|
|
||||||
response.result = result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write directly to stdout with newline (MCP uses line-delimited JSON)
|
|
||||||
process.stdout.write(JSON.stringify(response) + '\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send JSON-RPC notification
|
|
||||||
* CRITICAL: Must write directly to stdout, not via console.log
|
|
||||||
*/
|
|
||||||
function sendNotification(method, params) {
|
|
||||||
const notification = {
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
method,
|
|
||||||
params
|
|
||||||
};
|
|
||||||
|
|
||||||
// Write directly to stdout with newline (MCP uses line-delimited JSON)
|
|
||||||
process.stdout.write(JSON.stringify(notification) + '\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle MCP initialize request
|
|
||||||
*/
|
|
||||||
async function handleInitialize(params, id) {
|
|
||||||
initialized = true;
|
|
||||||
|
|
||||||
sendResponse(id, {
|
|
||||||
protocolVersion: '2024-11-05',
|
|
||||||
capabilities: {
|
|
||||||
tools: {}
|
|
||||||
},
|
|
||||||
serverInfo: {
|
|
||||||
name: 'automaker-tools',
|
|
||||||
version: '1.0.0'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle tools/list request
|
|
||||||
*/
|
|
||||||
async function handleToolsList(params, id) {
|
|
||||||
sendResponse(id, {
|
|
||||||
tools: [
|
|
||||||
{
|
|
||||||
name: 'UpdateFeatureStatus',
|
|
||||||
description: 'Update the status of a feature. Use this tool instead of directly modifying feature files to safely update feature status. IMPORTANT: If the feature has skipTests=true, you should NOT mark it as verified - instead it will automatically go to waiting_approval status for manual review. Always include a summary of what was done.',
|
|
||||||
inputSchema: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
featureId: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'The ID of the feature to update'
|
|
||||||
},
|
|
||||||
status: {
|
|
||||||
type: 'string',
|
|
||||||
enum: ['backlog', 'in_progress', 'verified'],
|
|
||||||
description: 'The new status for the feature. Note: If skipTests=true, verified will be converted to waiting_approval automatically.'
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
type: 'string',
|
|
||||||
description: 'A brief summary of what was implemented/changed. This will be displayed on the Kanban card. Example: "Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx"'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
required: ['featureId', 'status']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle tools/call request
|
|
||||||
*/
|
|
||||||
async function handleToolsCall(params, id) {
|
|
||||||
const { name, arguments: args } = params;
|
|
||||||
|
|
||||||
if (name !== 'UpdateFeatureStatus') {
|
|
||||||
sendResponse(id, null, {
|
|
||||||
code: -32601,
|
|
||||||
message: `Unknown tool: ${name}`
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { featureId, status, summary } = args;
|
|
||||||
|
|
||||||
if (!featureId || !status) {
|
|
||||||
sendResponse(id, null, {
|
|
||||||
code: -32602,
|
|
||||||
message: 'Missing required parameters: featureId and status are required'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the feature to check skipTests flag
|
|
||||||
const features = await featureLoader.loadFeatures(projectPath);
|
|
||||||
const feature = features.find((f) => f.id === featureId);
|
|
||||||
|
|
||||||
if (!feature) {
|
|
||||||
sendResponse(id, null, {
|
|
||||||
code: -32602,
|
|
||||||
message: `Feature ${featureId} not found`
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If agent tries to mark as verified but feature has skipTests=true, convert to waiting_approval
|
|
||||||
let finalStatus = status;
|
|
||||||
if (status === 'verified' && feature.skipTests === true) {
|
|
||||||
finalStatus = 'waiting_approval';
|
|
||||||
}
|
|
||||||
|
|
||||||
// IMPORTANT: Prevent agent from moving an in_progress feature back to backlog
|
|
||||||
// When a feature is being worked on, the agent should only be able to mark it as verified
|
|
||||||
// (which may be converted to waiting_approval for skipTests features)
|
|
||||||
// This prevents the agent from incorrectly putting completed work back in the backlog
|
|
||||||
if (feature.status === 'in_progress' && (status === 'backlog' || status === 'todo')) {
|
|
||||||
console.log(`[McpServerStdio] Feature ${featureId} is in_progress - preventing move to ${status}, converting to waiting_approval instead`);
|
|
||||||
finalStatus = 'waiting_approval';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call the update callback via IPC or direct call
|
|
||||||
// Since we're in a separate process, we need to use IPC to communicate back
|
|
||||||
// For now, we'll call the feature loader directly since it has the update method
|
|
||||||
await featureLoader.updateFeatureStatus(featureId, finalStatus, projectPath, { summary });
|
|
||||||
|
|
||||||
const statusMessage = finalStatus !== status
|
|
||||||
? `Successfully updated feature ${featureId} to status "${finalStatus}" (converted from "${status}" because skipTests=true)${summary ? ` with summary: "${summary}"` : ''}`
|
|
||||||
: `Successfully updated feature ${featureId} to status "${finalStatus}"${summary ? ` with summary: "${summary}"` : ''}`;
|
|
||||||
|
|
||||||
sendResponse(id, {
|
|
||||||
content: [
|
|
||||||
{
|
|
||||||
type: 'text',
|
|
||||||
text: statusMessage
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[McpServerStdio] UpdateFeatureStatus error:', error);
|
|
||||||
sendResponse(id, null, {
|
|
||||||
code: -32603,
|
|
||||||
message: `Failed to update feature status: ${error.message}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle JSON-RPC request
|
|
||||||
*/
|
|
||||||
async function handleRequest(line) {
|
|
||||||
let request;
|
|
||||||
|
|
||||||
try {
|
|
||||||
request = JSON.parse(line);
|
|
||||||
} catch (e) {
|
|
||||||
sendResponse(null, null, {
|
|
||||||
code: -32700,
|
|
||||||
message: 'Parse error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate JSON-RPC 2.0 structure
|
|
||||||
if (request.jsonrpc !== '2.0') {
|
|
||||||
sendResponse(request.id || null, null, {
|
|
||||||
code: -32600,
|
|
||||||
message: 'Invalid Request'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { method, params, id } = request;
|
|
||||||
|
|
||||||
// Handle notifications (no id)
|
|
||||||
if (id === undefined) {
|
|
||||||
// Handle notifications if needed
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle requests
|
|
||||||
try {
|
|
||||||
switch (method) {
|
|
||||||
case 'initialize':
|
|
||||||
await handleInitialize(params, id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'tools/list':
|
|
||||||
if (!initialized) {
|
|
||||||
sendResponse(id, null, {
|
|
||||||
code: -32002,
|
|
||||||
message: 'Server not initialized'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await handleToolsList(params, id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'tools/call':
|
|
||||||
if (!initialized) {
|
|
||||||
sendResponse(id, null, {
|
|
||||||
code: -32002,
|
|
||||||
message: 'Server not initialized'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await handleToolsCall(params, id);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
sendResponse(id, null, {
|
|
||||||
code: -32601,
|
|
||||||
message: `Method not found: ${method}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[McpServerStdio] Error handling request:', error);
|
|
||||||
sendResponse(id, null, {
|
|
||||||
code: -32603,
|
|
||||||
message: `Internal error: ${error.message}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process stdin line by line
|
|
||||||
rl.on('line', async (line) => {
|
|
||||||
if (!line.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await handleRequest(line);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle errors
|
|
||||||
rl.on('error', (error) => {
|
|
||||||
console.error('[McpServerStdio] Readline error:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle process termination
|
|
||||||
process.on('SIGTERM', () => {
|
|
||||||
rl.close();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
process.on('SIGINT', () => {
|
|
||||||
rl.close();
|
|
||||||
process.exit(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Log startup
|
|
||||||
console.error('[McpServerStdio] Starting MCP server for automaker-tools');
|
|
||||||
console.error(`[McpServerStdio] Project path: ${projectPath}`);
|
|
||||||
console.error(`[McpServerStdio] IPC channel: ${ipcChannel}`);
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,524 +0,0 @@
|
|||||||
/**
|
|
||||||
* Model Provider Abstraction Layer
|
|
||||||
*
|
|
||||||
* This module provides an abstract interface for model providers (Claude, Codex, etc.)
|
|
||||||
* allowing the application to use different AI models through a unified API.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Base class for model providers
|
|
||||||
* Concrete implementations should extend this class
|
|
||||||
*/
|
|
||||||
class ModelProvider {
|
|
||||||
constructor(config = {}) {
|
|
||||||
this.config = config;
|
|
||||||
this.name = 'base';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get provider name
|
|
||||||
* @returns {string} Provider name
|
|
||||||
*/
|
|
||||||
getName() {
|
|
||||||
return this.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a query with the model provider
|
|
||||||
* @param {Object} options Query options
|
|
||||||
* @param {string} options.prompt The prompt to send
|
|
||||||
* @param {string} options.model The model to use
|
|
||||||
* @param {string} options.systemPrompt System prompt
|
|
||||||
* @param {string} options.cwd Working directory
|
|
||||||
* @param {number} options.maxTurns Maximum turns
|
|
||||||
* @param {string[]} options.allowedTools Allowed tools
|
|
||||||
* @param {Object} options.mcpServers MCP servers configuration
|
|
||||||
* @param {AbortController} options.abortController Abort controller
|
|
||||||
* @param {Object} options.thinking Thinking configuration
|
|
||||||
* @returns {AsyncGenerator} Async generator yielding messages
|
|
||||||
*/
|
|
||||||
async *executeQuery(options) {
|
|
||||||
throw new Error('executeQuery must be implemented by subclass');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detect if this provider's CLI/SDK is installed
|
|
||||||
* @returns {Promise<Object>} Installation status
|
|
||||||
*/
|
|
||||||
async detectInstallation() {
|
|
||||||
throw new Error('detectInstallation must be implemented by subclass');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of available models for this provider
|
|
||||||
* @returns {Array<Object>} Array of model definitions
|
|
||||||
*/
|
|
||||||
getAvailableModels() {
|
|
||||||
throw new Error('getAvailableModels must be implemented by subclass');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate provider configuration
|
|
||||||
* @returns {Object} Validation result { valid: boolean, errors: string[] }
|
|
||||||
*/
|
|
||||||
validateConfig() {
|
|
||||||
throw new Error('validateConfig must be implemented by subclass');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the full model string for a model key
|
|
||||||
* @param {string} modelKey Short model key (e.g., 'opus', 'gpt-5.1-codex')
|
|
||||||
* @returns {string} Full model string
|
|
||||||
*/
|
|
||||||
getModelString(modelKey) {
|
|
||||||
throw new Error('getModelString must be implemented by subclass');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if provider supports a specific feature
|
|
||||||
* @param {string} feature Feature name (e.g., 'thinking', 'tools', 'streaming')
|
|
||||||
* @returns {boolean} Whether the feature is supported
|
|
||||||
*/
|
|
||||||
supportsFeature(feature) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Claude Provider - Uses Anthropic Claude Agent SDK
|
|
||||||
*/
|
|
||||||
class ClaudeProvider extends ModelProvider {
|
|
||||||
constructor(config = {}) {
|
|
||||||
super(config);
|
|
||||||
this.name = 'claude';
|
|
||||||
this.sdk = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to load credentials from the app's own credentials.json file.
|
|
||||||
* This is where we store OAuth tokens and API keys that users enter in the setup wizard.
|
|
||||||
* Returns { oauthToken, apiKey } or null values if not found.
|
|
||||||
*/
|
|
||||||
loadTokenFromAppCredentials() {
|
|
||||||
try {
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const { app } = require('electron');
|
|
||||||
const credentialsPath = path.join(app.getPath('userData'), 'credentials.json');
|
|
||||||
|
|
||||||
if (!fs.existsSync(credentialsPath)) {
|
|
||||||
console.log('[ClaudeProvider] App credentials file does not exist:', credentialsPath);
|
|
||||||
return { oauthToken: null, apiKey: null };
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = fs.readFileSync(credentialsPath, 'utf-8');
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
|
|
||||||
// Check for OAuth token first (from claude setup-token), then API key
|
|
||||||
const oauthToken = parsed.anthropic_oauth_token || null;
|
|
||||||
const apiKey = parsed.anthropic || parsed.anthropic_api_key || null;
|
|
||||||
|
|
||||||
console.log('[ClaudeProvider] App credentials check - OAuth token:', !!oauthToken, ', API key:', !!apiKey);
|
|
||||||
return { oauthToken, apiKey };
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[ClaudeProvider] Failed to read app credentials:', err?.message);
|
|
||||||
return { oauthToken: null, apiKey: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Try to load a Claude OAuth token from the local CLI config (~/.claude/config.json).
|
|
||||||
* Returns the token string or null if not found.
|
|
||||||
* NOTE: Claude's credentials.json is encrypted, so we only try config.json
|
|
||||||
*/
|
|
||||||
loadTokenFromCliConfig() {
|
|
||||||
try {
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const configPath = path.join(require('os').homedir(), '.claude', 'config.json');
|
|
||||||
if (!fs.existsSync(configPath)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
||||||
const parsed = JSON.parse(raw);
|
|
||||||
// CLI config stores token as oauth_token (newer) or token (older)
|
|
||||||
return parsed.oauth_token || parsed.token || null;
|
|
||||||
} catch (err) {
|
|
||||||
console.warn('[ClaudeProvider] Failed to read CLI config token:', err?.message);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ensureAuthEnv() {
|
|
||||||
// If API key or token already present in environment, keep as-is.
|
|
||||||
if (process.env.ANTHROPIC_API_KEY || process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
||||||
console.log('[ClaudeProvider] Auth already present in environment');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 1: Try to load from app's own credentials (setup wizard)
|
|
||||||
const appCredentials = this.loadTokenFromAppCredentials();
|
|
||||||
if (appCredentials.oauthToken) {
|
|
||||||
process.env.CLAUDE_CODE_OAUTH_TOKEN = appCredentials.oauthToken;
|
|
||||||
console.log('[ClaudeProvider] Loaded CLAUDE_CODE_OAUTH_TOKEN from app credentials');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (appCredentials.apiKey) {
|
|
||||||
process.env.ANTHROPIC_API_KEY = appCredentials.apiKey;
|
|
||||||
console.log('[ClaudeProvider] Loaded ANTHROPIC_API_KEY from app credentials');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Priority 2: Try to hydrate from CLI login config (legacy)
|
|
||||||
const token = this.loadTokenFromCliConfig();
|
|
||||||
if (token) {
|
|
||||||
process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
|
|
||||||
console.log('[ClaudeProvider] Loaded CLAUDE_CODE_OAUTH_TOKEN from ~/.claude/config.json');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if CLI is installed but not logged in
|
|
||||||
try {
|
|
||||||
const claudeCliDetector = require('./claude-cli-detector');
|
|
||||||
const detection = claudeCliDetector.detectClaudeInstallation();
|
|
||||||
if (detection.installed && detection.method === 'cli') {
|
|
||||||
console.error('[ClaudeProvider] Claude CLI is installed but not authenticated. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN environment variable.');
|
|
||||||
} else {
|
|
||||||
console.error('[ClaudeProvider] No Anthropic auth found. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[ClaudeProvider] No Anthropic auth found. Use the setup wizard or set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN.');
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Lazily load the Claude SDK
|
|
||||||
*/
|
|
||||||
loadSdk() {
|
|
||||||
if (!this.sdk) {
|
|
||||||
this.sdk = require('@anthropic-ai/claude-agent-sdk');
|
|
||||||
}
|
|
||||||
return this.sdk;
|
|
||||||
}
|
|
||||||
|
|
||||||
async *executeQuery(options) {
|
|
||||||
// Ensure we have auth; fall back to app credentials or CLI login token if available.
|
|
||||||
if (!this.ensureAuthEnv()) {
|
|
||||||
// Check if CLI is installed to provide better error message
|
|
||||||
let msg = 'Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication.';
|
|
||||||
try {
|
|
||||||
const claudeCliDetector = require('./claude-cli-detector');
|
|
||||||
const detection = claudeCliDetector.detectClaudeInstallation();
|
|
||||||
if (detection.installed && detection.method === 'cli') {
|
|
||||||
msg = 'Claude CLI is installed but not authenticated. Go to Settings > Setup to provide your subscription token (from `claude setup-token`) or API key.';
|
|
||||||
} else {
|
|
||||||
msg = 'Missing Anthropic auth. Go to Settings > Setup to configure your Claude authentication, or set ANTHROPIC_API_KEY environment variable.';
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// Fallback to default message
|
|
||||||
}
|
|
||||||
console.error(`[ClaudeProvider] ${msg}`);
|
|
||||||
yield { type: 'error', error: msg };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { query } = this.loadSdk();
|
|
||||||
|
|
||||||
const sdkOptions = {
|
|
||||||
model: options.model,
|
|
||||||
systemPrompt: options.systemPrompt,
|
|
||||||
maxTurns: options.maxTurns || 1000,
|
|
||||||
cwd: options.cwd,
|
|
||||||
mcpServers: options.mcpServers,
|
|
||||||
allowedTools: options.allowedTools,
|
|
||||||
permissionMode: options.permissionMode || 'acceptEdits',
|
|
||||||
sandbox: options.sandbox,
|
|
||||||
abortController: options.abortController,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add thinking configuration if enabled
|
|
||||||
if (options.thinking) {
|
|
||||||
sdkOptions.thinking = options.thinking;
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentQuery = query({ prompt: options.prompt, options: sdkOptions });
|
|
||||||
|
|
||||||
for await (const msg of currentQuery) {
|
|
||||||
yield msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async detectInstallation() {
|
|
||||||
const claudeCliDetector = require('./claude-cli-detector');
|
|
||||||
return claudeCliDetector.getFullStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
getAvailableModels() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'haiku',
|
|
||||||
name: 'Claude Haiku',
|
|
||||||
modelString: 'claude-haiku-4-5',
|
|
||||||
provider: 'claude',
|
|
||||||
description: 'Fast and efficient for simple tasks',
|
|
||||||
tier: 'basic'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'sonnet',
|
|
||||||
name: 'Claude Sonnet',
|
|
||||||
modelString: 'claude-sonnet-4-20250514',
|
|
||||||
provider: 'claude',
|
|
||||||
description: 'Balanced performance and capabilities',
|
|
||||||
tier: 'standard'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'opus',
|
|
||||||
name: 'Claude Opus 4.5',
|
|
||||||
modelString: 'claude-opus-4-5-20251101',
|
|
||||||
provider: 'claude',
|
|
||||||
description: 'Most capable model for complex tasks',
|
|
||||||
tier: 'premium'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
validateConfig() {
|
|
||||||
const errors = [];
|
|
||||||
|
|
||||||
// Ensure auth is available (try to auto-load from app credentials or CLI config)
|
|
||||||
this.ensureAuthEnv();
|
|
||||||
|
|
||||||
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN && !process.env.ANTHROPIC_API_KEY) {
|
|
||||||
errors.push('No Claude authentication found. Go to Settings > Setup to configure your subscription token or API key.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: errors.length === 0,
|
|
||||||
errors
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getModelString(modelKey) {
|
|
||||||
const modelMap = {
|
|
||||||
haiku: 'claude-haiku-4-5',
|
|
||||||
sonnet: 'claude-sonnet-4-20250514',
|
|
||||||
opus: 'claude-opus-4-5-20251101'
|
|
||||||
};
|
|
||||||
return modelMap[modelKey] || modelMap.opus;
|
|
||||||
}
|
|
||||||
|
|
||||||
supportsFeature(feature) {
|
|
||||||
const supportedFeatures = ['thinking', 'tools', 'streaming', 'mcp'];
|
|
||||||
return supportedFeatures.includes(feature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Codex Provider - Uses OpenAI Codex CLI
|
|
||||||
*/
|
|
||||||
class CodexProvider extends ModelProvider {
|
|
||||||
constructor(config = {}) {
|
|
||||||
super(config);
|
|
||||||
this.name = 'codex';
|
|
||||||
}
|
|
||||||
|
|
||||||
async *executeQuery(options) {
|
|
||||||
const codexExecutor = require('./codex-executor');
|
|
||||||
|
|
||||||
// Validate that we're not receiving a Claude model string
|
|
||||||
if (options.model && options.model.startsWith('claude-')) {
|
|
||||||
const errorMsg = `Codex provider cannot use Claude model '${options.model}'. Codex only supports OpenAI models (gpt-5.1-codex-max, gpt-5.1-codex, gpt-5.1-codex-mini, gpt-5.1).`;
|
|
||||||
console.error(`[CodexProvider] ${errorMsg}`);
|
|
||||||
yield {
|
|
||||||
type: 'error',
|
|
||||||
error: errorMsg
|
|
||||||
};
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const executeOptions = {
|
|
||||||
prompt: options.prompt,
|
|
||||||
model: options.model,
|
|
||||||
cwd: options.cwd,
|
|
||||||
systemPrompt: options.systemPrompt,
|
|
||||||
maxTurns: options.maxTurns || 20,
|
|
||||||
allowedTools: options.allowedTools,
|
|
||||||
mcpServers: options.mcpServers, // Pass MCP servers config to executor
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
OPENAI_API_KEY: process.env.OPENAI_API_KEY
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Execute and yield results
|
|
||||||
const generator = codexExecutor.execute(executeOptions);
|
|
||||||
for await (const msg of generator) {
|
|
||||||
yield msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async detectInstallation() {
|
|
||||||
const codexCliDetector = require('./codex-cli-detector');
|
|
||||||
return codexCliDetector.getInstallationInfo();
|
|
||||||
}
|
|
||||||
|
|
||||||
getAvailableModels() {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
id: 'gpt-5.1-codex-max',
|
|
||||||
name: 'GPT-5.1 Codex Max',
|
|
||||||
modelString: 'gpt-5.1-codex-max',
|
|
||||||
provider: 'codex',
|
|
||||||
description: 'Latest flagship - deep and fast reasoning for coding',
|
|
||||||
tier: 'premium',
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gpt-5.1-codex',
|
|
||||||
name: 'GPT-5.1 Codex',
|
|
||||||
modelString: 'gpt-5.1-codex',
|
|
||||||
provider: 'codex',
|
|
||||||
description: 'Optimized for code generation',
|
|
||||||
tier: 'standard'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gpt-5.1-codex-mini',
|
|
||||||
name: 'GPT-5.1 Codex Mini',
|
|
||||||
modelString: 'gpt-5.1-codex-mini',
|
|
||||||
provider: 'codex',
|
|
||||||
description: 'Faster and cheaper option',
|
|
||||||
tier: 'basic'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'gpt-5.1',
|
|
||||||
name: 'GPT-5.1',
|
|
||||||
modelString: 'gpt-5.1',
|
|
||||||
provider: 'codex',
|
|
||||||
description: 'Broad world knowledge with strong reasoning',
|
|
||||||
tier: 'standard'
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
validateConfig() {
|
|
||||||
const errors = [];
|
|
||||||
const codexCliDetector = require('./codex-cli-detector');
|
|
||||||
const installation = codexCliDetector.detectCodexInstallation();
|
|
||||||
|
|
||||||
if (!installation.installed && !process.env.OPENAI_API_KEY) {
|
|
||||||
errors.push('Codex CLI not installed and no OPENAI_API_KEY found.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: errors.length === 0,
|
|
||||||
errors
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
getModelString(modelKey) {
|
|
||||||
// Codex models use the key directly as the model string
|
|
||||||
const modelMap = {
|
|
||||||
'gpt-5.1-codex-max': 'gpt-5.1-codex-max',
|
|
||||||
'gpt-5.1-codex': 'gpt-5.1-codex',
|
|
||||||
'gpt-5.1-codex-mini': 'gpt-5.1-codex-mini',
|
|
||||||
'gpt-5.1': 'gpt-5.1'
|
|
||||||
};
|
|
||||||
return modelMap[modelKey] || 'gpt-5.1-codex-max';
|
|
||||||
}
|
|
||||||
|
|
||||||
supportsFeature(feature) {
|
|
||||||
const supportedFeatures = ['tools', 'streaming'];
|
|
||||||
return supportedFeatures.includes(feature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Model Provider Factory
|
|
||||||
* Creates the appropriate provider based on model or provider name
|
|
||||||
*/
|
|
||||||
class ModelProviderFactory {
|
|
||||||
static providers = {
|
|
||||||
claude: ClaudeProvider,
|
|
||||||
codex: CodexProvider
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get provider for a specific model
|
|
||||||
* @param {string} modelId Model ID (e.g., 'opus', 'gpt-5.1-codex')
|
|
||||||
* @returns {ModelProvider} Provider instance
|
|
||||||
*/
|
|
||||||
static getProviderForModel(modelId) {
|
|
||||||
// Check if it's a Claude model
|
|
||||||
const claudeModels = ['haiku', 'sonnet', 'opus'];
|
|
||||||
if (claudeModels.includes(modelId)) {
|
|
||||||
return new ClaudeProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a Codex/OpenAI model
|
|
||||||
const codexModels = [
|
|
||||||
'gpt-5.1-codex-max', 'gpt-5.1-codex', 'gpt-5.1-codex-mini', 'gpt-5.1'
|
|
||||||
];
|
|
||||||
if (codexModels.includes(modelId)) {
|
|
||||||
return new CodexProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default to Claude
|
|
||||||
return new ClaudeProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get provider by name
|
|
||||||
* @param {string} providerName Provider name ('claude' or 'codex')
|
|
||||||
* @returns {ModelProvider} Provider instance
|
|
||||||
*/
|
|
||||||
static getProvider(providerName) {
|
|
||||||
const ProviderClass = this.providers[providerName];
|
|
||||||
if (!ProviderClass) {
|
|
||||||
throw new Error(`Unknown provider: ${providerName}`);
|
|
||||||
}
|
|
||||||
return new ProviderClass();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all available providers
|
|
||||||
* @returns {string[]} List of provider names
|
|
||||||
*/
|
|
||||||
static getAvailableProviders() {
|
|
||||||
return Object.keys(this.providers);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all available models across all providers
|
|
||||||
* @returns {Array<Object>} All available models
|
|
||||||
*/
|
|
||||||
static getAllModels() {
|
|
||||||
const allModels = [];
|
|
||||||
for (const providerName of this.getAvailableProviders()) {
|
|
||||||
const provider = this.getProvider(providerName);
|
|
||||||
const models = provider.getAvailableModels();
|
|
||||||
allModels.push(...models);
|
|
||||||
}
|
|
||||||
return allModels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check installation status for all providers
|
|
||||||
* @returns {Promise<Object>} Installation status for each provider
|
|
||||||
*/
|
|
||||||
static async checkAllProviders() {
|
|
||||||
const status = {};
|
|
||||||
for (const providerName of this.getAvailableProviders()) {
|
|
||||||
const provider = this.getProvider(providerName);
|
|
||||||
status[providerName] = await provider.detectInstallation();
|
|
||||||
}
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
ModelProvider,
|
|
||||||
ClaudeProvider,
|
|
||||||
CodexProvider,
|
|
||||||
ModelProviderFactory
|
|
||||||
};
|
|
||||||
@@ -1,320 +0,0 @@
|
|||||||
/**
|
|
||||||
* Model Registry - Centralized model definitions and metadata
|
|
||||||
*
|
|
||||||
* This module provides a central registry of all available models
|
|
||||||
* across different providers (Claude, Codex/OpenAI).
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Model Categories
|
|
||||||
*/
|
|
||||||
const MODEL_CATEGORIES = {
|
|
||||||
CLAUDE: 'claude',
|
|
||||||
OPENAI: 'openai',
|
|
||||||
CODEX: 'codex'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Model Tiers (capability levels)
|
|
||||||
*/
|
|
||||||
const MODEL_TIERS = {
|
|
||||||
BASIC: 'basic', // Fast, cheap, simple tasks
|
|
||||||
STANDARD: 'standard', // Balanced performance
|
|
||||||
PREMIUM: 'premium' // Most capable, complex tasks
|
|
||||||
};
|
|
||||||
|
|
||||||
const CODEX_MODEL_IDS = [
|
|
||||||
'gpt-5.1-codex-max',
|
|
||||||
'gpt-5.1-codex',
|
|
||||||
'gpt-5.1-codex-mini',
|
|
||||||
'gpt-5.1'
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* All available models with full metadata
|
|
||||||
*/
|
|
||||||
const MODELS = {
|
|
||||||
// Claude Models
|
|
||||||
haiku: {
|
|
||||||
id: 'haiku',
|
|
||||||
name: 'Claude Haiku',
|
|
||||||
modelString: 'claude-haiku-4-5',
|
|
||||||
provider: 'claude',
|
|
||||||
category: MODEL_CATEGORIES.CLAUDE,
|
|
||||||
tier: MODEL_TIERS.BASIC,
|
|
||||||
description: 'Fast and efficient for simple tasks',
|
|
||||||
capabilities: ['code', 'text', 'tools'],
|
|
||||||
maxTokens: 8192,
|
|
||||||
contextWindow: 200000,
|
|
||||||
supportsThinking: true,
|
|
||||||
requiresAuth: 'CLAUDE_CODE_OAUTH_TOKEN'
|
|
||||||
},
|
|
||||||
sonnet: {
|
|
||||||
id: 'sonnet',
|
|
||||||
name: 'Claude Sonnet',
|
|
||||||
modelString: 'claude-sonnet-4-20250514',
|
|
||||||
provider: 'claude',
|
|
||||||
category: MODEL_CATEGORIES.CLAUDE,
|
|
||||||
tier: MODEL_TIERS.STANDARD,
|
|
||||||
description: 'Balanced performance and capabilities',
|
|
||||||
capabilities: ['code', 'text', 'tools', 'analysis'],
|
|
||||||
maxTokens: 8192,
|
|
||||||
contextWindow: 200000,
|
|
||||||
supportsThinking: true,
|
|
||||||
requiresAuth: 'CLAUDE_CODE_OAUTH_TOKEN'
|
|
||||||
},
|
|
||||||
opus: {
|
|
||||||
id: 'opus',
|
|
||||||
name: 'Claude Opus 4.5',
|
|
||||||
modelString: 'claude-opus-4-5-20251101',
|
|
||||||
provider: 'claude',
|
|
||||||
category: MODEL_CATEGORIES.CLAUDE,
|
|
||||||
tier: MODEL_TIERS.PREMIUM,
|
|
||||||
description: 'Most capable model for complex tasks',
|
|
||||||
capabilities: ['code', 'text', 'tools', 'analysis', 'reasoning'],
|
|
||||||
maxTokens: 8192,
|
|
||||||
contextWindow: 200000,
|
|
||||||
supportsThinking: true,
|
|
||||||
requiresAuth: 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
||||||
default: true
|
|
||||||
},
|
|
||||||
|
|
||||||
// OpenAI GPT-5.1 Codex Models
|
|
||||||
'gpt-5.1-codex-max': {
|
|
||||||
id: 'gpt-5.1-codex-max',
|
|
||||||
name: 'GPT-5.1 Codex Max',
|
|
||||||
modelString: 'gpt-5.1-codex-max',
|
|
||||||
provider: 'codex',
|
|
||||||
category: MODEL_CATEGORIES.OPENAI,
|
|
||||||
tier: MODEL_TIERS.PREMIUM,
|
|
||||||
description: 'Latest flagship - deep and fast reasoning for coding',
|
|
||||||
capabilities: ['code', 'text', 'tools', 'reasoning'],
|
|
||||||
maxTokens: 32768,
|
|
||||||
contextWindow: 128000,
|
|
||||||
supportsThinking: false,
|
|
||||||
requiresAuth: 'OPENAI_API_KEY',
|
|
||||||
codexDefault: true
|
|
||||||
},
|
|
||||||
'gpt-5.1-codex': {
|
|
||||||
id: 'gpt-5.1-codex',
|
|
||||||
name: 'GPT-5.1 Codex',
|
|
||||||
modelString: 'gpt-5.1-codex',
|
|
||||||
provider: 'codex',
|
|
||||||
category: MODEL_CATEGORIES.OPENAI,
|
|
||||||
tier: MODEL_TIERS.STANDARD,
|
|
||||||
description: 'Optimized for code generation',
|
|
||||||
capabilities: ['code', 'text', 'tools'],
|
|
||||||
maxTokens: 32768,
|
|
||||||
contextWindow: 128000,
|
|
||||||
supportsThinking: false,
|
|
||||||
requiresAuth: 'OPENAI_API_KEY'
|
|
||||||
},
|
|
||||||
'gpt-5.1-codex-mini': {
|
|
||||||
id: 'gpt-5.1-codex-mini',
|
|
||||||
name: 'GPT-5.1 Codex Mini',
|
|
||||||
modelString: 'gpt-5.1-codex-mini',
|
|
||||||
provider: 'codex',
|
|
||||||
category: MODEL_CATEGORIES.OPENAI,
|
|
||||||
tier: MODEL_TIERS.BASIC,
|
|
||||||
description: 'Faster and cheaper option',
|
|
||||||
capabilities: ['code', 'text'],
|
|
||||||
maxTokens: 16384,
|
|
||||||
contextWindow: 128000,
|
|
||||||
supportsThinking: false,
|
|
||||||
requiresAuth: 'OPENAI_API_KEY'
|
|
||||||
},
|
|
||||||
'gpt-5.1': {
|
|
||||||
id: 'gpt-5.1',
|
|
||||||
name: 'GPT-5.1',
|
|
||||||
modelString: 'gpt-5.1',
|
|
||||||
provider: 'codex',
|
|
||||||
category: MODEL_CATEGORIES.OPENAI,
|
|
||||||
tier: MODEL_TIERS.STANDARD,
|
|
||||||
description: 'Broad world knowledge with strong reasoning',
|
|
||||||
capabilities: ['code', 'text', 'reasoning'],
|
|
||||||
maxTokens: 32768,
|
|
||||||
contextWindow: 128000,
|
|
||||||
supportsThinking: false,
|
|
||||||
requiresAuth: 'OPENAI_API_KEY'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Model Registry class for querying and managing models
|
|
||||||
*/
|
|
||||||
class ModelRegistry {
|
|
||||||
/**
|
|
||||||
* Get all registered models
|
|
||||||
* @returns {Object} All models
|
|
||||||
*/
|
|
||||||
static getAllModels() {
|
|
||||||
return MODELS;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get model by ID
|
|
||||||
* @param {string} modelId Model ID
|
|
||||||
* @returns {Object|null} Model definition or null
|
|
||||||
*/
|
|
||||||
static getModel(modelId) {
|
|
||||||
return MODELS[modelId] || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get models by provider
|
|
||||||
* @param {string} provider Provider name ('claude' or 'codex')
|
|
||||||
* @returns {Object[]} Array of models for the provider
|
|
||||||
*/
|
|
||||||
static getModelsByProvider(provider) {
|
|
||||||
return Object.values(MODELS).filter(m => m.provider === provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get models by category
|
|
||||||
* @param {string} category Category name
|
|
||||||
* @returns {Object[]} Array of models in the category
|
|
||||||
*/
|
|
||||||
static getModelsByCategory(category) {
|
|
||||||
return Object.values(MODELS).filter(m => m.category === category);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get models by tier
|
|
||||||
* @param {string} tier Tier name
|
|
||||||
* @returns {Object[]} Array of models in the tier
|
|
||||||
*/
|
|
||||||
static getModelsByTier(tier) {
|
|
||||||
return Object.values(MODELS).filter(m => m.tier === tier);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get default model for a provider
|
|
||||||
* @param {string} provider Provider name
|
|
||||||
* @returns {Object|null} Default model or null
|
|
||||||
*/
|
|
||||||
static getDefaultModel(provider = 'claude') {
|
|
||||||
const models = this.getModelsByProvider(provider);
|
|
||||||
if (provider === 'claude') {
|
|
||||||
return models.find(m => m.default) || models[0];
|
|
||||||
}
|
|
||||||
if (provider === 'codex') {
|
|
||||||
return models.find(m => m.codexDefault) || models[0];
|
|
||||||
}
|
|
||||||
return models[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get model string (full model name) for a model ID
|
|
||||||
* @param {string} modelId Model ID
|
|
||||||
* @returns {string} Full model string
|
|
||||||
*/
|
|
||||||
static getModelString(modelId) {
|
|
||||||
const model = this.getModel(modelId);
|
|
||||||
return model ? model.modelString : modelId;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine provider for a model ID
|
|
||||||
* @param {string} modelId Model ID
|
|
||||||
* @returns {string} Provider name ('claude' or 'codex')
|
|
||||||
*/
|
|
||||||
static getProviderForModel(modelId) {
|
|
||||||
const model = this.getModel(modelId);
|
|
||||||
if (model) {
|
|
||||||
return model.provider;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback detection for models not explicitly registered (keeps legacy Codex IDs working)
|
|
||||||
if (CODEX_MODEL_IDS.includes(modelId)) {
|
|
||||||
return 'codex';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'claude';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a model is a Claude model
|
|
||||||
* @param {string} modelId Model ID
|
|
||||||
* @returns {boolean} Whether it's a Claude model
|
|
||||||
*/
|
|
||||||
static isClaudeModel(modelId) {
|
|
||||||
return this.getProviderForModel(modelId) === 'claude';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a model is a Codex/OpenAI model
|
|
||||||
* @param {string} modelId Model ID
|
|
||||||
* @returns {boolean} Whether it's a Codex model
|
|
||||||
*/
|
|
||||||
static isCodexModel(modelId) {
|
|
||||||
return this.getProviderForModel(modelId) === 'codex';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get models grouped by provider for UI display
|
|
||||||
* @returns {Object} Models grouped by provider
|
|
||||||
*/
|
|
||||||
static getModelsGroupedByProvider() {
|
|
||||||
return {
|
|
||||||
claude: this.getModelsByProvider('claude'),
|
|
||||||
codex: this.getModelsByProvider('codex')
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all model IDs as an array
|
|
||||||
* @returns {string[]} Array of model IDs
|
|
||||||
*/
|
|
||||||
static getAllModelIds() {
|
|
||||||
return Object.keys(MODELS);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if model supports a specific capability
|
|
||||||
* @param {string} modelId Model ID
|
|
||||||
* @param {string} capability Capability name
|
|
||||||
* @returns {boolean} Whether the model supports the capability
|
|
||||||
*/
|
|
||||||
static modelSupportsCapability(modelId, capability) {
|
|
||||||
const model = this.getModel(modelId);
|
|
||||||
return model ? model.capabilities.includes(capability) : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if model supports extended thinking
|
|
||||||
* @param {string} modelId Model ID
|
|
||||||
* @returns {boolean} Whether the model supports thinking
|
|
||||||
*/
|
|
||||||
static modelSupportsThinking(modelId) {
|
|
||||||
const model = this.getModel(modelId);
|
|
||||||
return model ? model.supportsThinking : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get required authentication for a model
|
|
||||||
* @param {string} modelId Model ID
|
|
||||||
* @returns {string|null} Required auth env variable name
|
|
||||||
*/
|
|
||||||
static getRequiredAuth(modelId) {
|
|
||||||
const model = this.getModel(modelId);
|
|
||||||
return model ? model.requiresAuth : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if authentication is available for a model
|
|
||||||
* @param {string} modelId Model ID
|
|
||||||
* @returns {boolean} Whether auth is available
|
|
||||||
*/
|
|
||||||
static hasAuthForModel(modelId) {
|
|
||||||
const authVar = this.getRequiredAuth(modelId);
|
|
||||||
if (!authVar) return false;
|
|
||||||
return !!process.env[authVar];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
MODEL_CATEGORIES,
|
|
||||||
MODEL_TIERS,
|
|
||||||
MODELS,
|
|
||||||
ModelRegistry
|
|
||||||
};
|
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
const { query, AbortError } = require("@anthropic-ai/claude-agent-sdk");
|
|
||||||
const promptBuilder = require("./prompt-builder");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Project Analyzer - Scans codebase and updates app_spec.txt
|
|
||||||
*/
|
|
||||||
class ProjectAnalyzer {
|
|
||||||
/**
|
|
||||||
* Run the project analysis using Claude Agent SDK
|
|
||||||
*/
|
|
||||||
async runProjectAnalysis(projectPath, analysisId, sendToRenderer, execution) {
|
|
||||||
console.log(`[ProjectAnalyzer] Running project analysis for: ${projectPath}`);
|
|
||||||
|
|
||||||
try {
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_phase",
|
|
||||||
featureId: analysisId,
|
|
||||||
phase: "planning",
|
|
||||||
message: "Scanning project structure...",
|
|
||||||
});
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
|
||||||
execution.abortController = abortController;
|
|
||||||
|
|
||||||
const options = {
|
|
||||||
model: "claude-sonnet-4-20250514",
|
|
||||||
systemPrompt: promptBuilder.getProjectAnalysisSystemPrompt(),
|
|
||||||
maxTurns: 50,
|
|
||||||
cwd: projectPath,
|
|
||||||
allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
|
|
||||||
permissionMode: "acceptEdits",
|
|
||||||
sandbox: {
|
|
||||||
enabled: true,
|
|
||||||
autoAllowBashIfSandboxed: true,
|
|
||||||
},
|
|
||||||
abortController: abortController,
|
|
||||||
};
|
|
||||||
|
|
||||||
const prompt = promptBuilder.buildProjectAnalysisPrompt(projectPath);
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_progress",
|
|
||||||
featureId: analysisId,
|
|
||||||
content: "Starting project analysis...\n",
|
|
||||||
});
|
|
||||||
|
|
||||||
const currentQuery = query({ prompt, options });
|
|
||||||
execution.query = currentQuery;
|
|
||||||
|
|
||||||
let responseText = "";
|
|
||||||
for await (const msg of currentQuery) {
|
|
||||||
if (!execution.isActive()) break;
|
|
||||||
|
|
||||||
if (msg.type === "assistant" && msg.message?.content) {
|
|
||||||
for (const block of msg.message.content) {
|
|
||||||
if (block.type === "text") {
|
|
||||||
responseText += block.text;
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_progress",
|
|
||||||
featureId: analysisId,
|
|
||||||
content: block.text,
|
|
||||||
});
|
|
||||||
} else if (block.type === "tool_use") {
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_tool",
|
|
||||||
featureId: analysisId,
|
|
||||||
tool: block.name,
|
|
||||||
input: block.input,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
execution.query = null;
|
|
||||||
execution.abortController = null;
|
|
||||||
|
|
||||||
sendToRenderer({
|
|
||||||
type: "auto_mode_phase",
|
|
||||||
featureId: analysisId,
|
|
||||||
phase: "verification",
|
|
||||||
message: "Project analysis complete",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
message: "Project analyzed successfully",
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof AbortError || error?.name === "AbortError") {
|
|
||||||
console.log("[ProjectAnalyzer] Project analysis aborted");
|
|
||||||
if (execution) {
|
|
||||||
execution.abortController = null;
|
|
||||||
execution.query = null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Analysis aborted",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.error("[ProjectAnalyzer] Error in project analysis:", error);
|
|
||||||
if (execution) {
|
|
||||||
execution.abortController = null;
|
|
||||||
execution.query = null;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new ProjectAnalyzer();
|
|
||||||
@@ -1,787 +0,0 @@
|
|||||||
const contextManager = require("./context-manager");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompt Builder - Generates prompts for different agent tasks
|
|
||||||
*/
|
|
||||||
class PromptBuilder {
|
|
||||||
/**
|
|
||||||
* Build the prompt for implementing a specific feature
|
|
||||||
*/
|
|
||||||
async buildFeaturePrompt(feature, projectPath) {
|
|
||||||
const skipTestsNote = feature.skipTests
|
|
||||||
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
let imagesNote = "";
|
|
||||||
if (feature.imagePaths && feature.imagePaths.length > 0) {
|
|
||||||
const imagesList = feature.imagePaths
|
|
||||||
.map(
|
|
||||||
(img, idx) =>
|
|
||||||
` ${idx + 1}. ${img.filename} (${img.mimeType})\n Path: ${
|
|
||||||
img.path
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
imagesNote = `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read:
|
|
||||||
|
|
||||||
${imagesList}
|
|
||||||
|
|
||||||
You can use the Read tool to view these images at any time during implementation. Review them carefully before implementing.\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get context files preview
|
|
||||||
const contextFilesPreview = await contextManager.getContextFilesPreview(
|
|
||||||
projectPath
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get memory content (lessons learned from previous runs)
|
|
||||||
const memoryContent = await contextManager.getMemoryContent(projectPath);
|
|
||||||
|
|
||||||
// Build mode header for this feature
|
|
||||||
const modeHeader = feature.skipTests
|
|
||||||
? `**🔨 MODE: Manual Review (No Automated Tests)**
|
|
||||||
This feature is set for manual review - focus on clean implementation without automated tests.`
|
|
||||||
: `**🧪 MODE: Test-Driven Development (TDD)**
|
|
||||||
This feature requires automated Playwright tests to verify the implementation.`;
|
|
||||||
|
|
||||||
return `You are working on a feature implementation task.
|
|
||||||
|
|
||||||
${modeHeader}
|
|
||||||
${memoryContent}
|
|
||||||
**Current Feature to Implement:**
|
|
||||||
|
|
||||||
ID: ${feature.id}
|
|
||||||
Category: ${feature.category || "Uncategorized"}
|
|
||||||
Description: ${feature.description || feature.summary || feature.title || "No description provided"}
|
|
||||||
${skipTestsNote}${imagesNote}${contextFilesPreview}
|
|
||||||
**Steps to Complete:**
|
|
||||||
${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"}
|
|
||||||
|
|
||||||
**Your Task:**
|
|
||||||
|
|
||||||
1. Read the project files to understand the current codebase structure
|
|
||||||
2. Implement the feature according to the description and steps
|
|
||||||
${
|
|
||||||
feature.skipTests
|
|
||||||
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
|
|
||||||
: "3. Write Playwright tests to verify the feature works correctly\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification"
|
|
||||||
}
|
|
||||||
${
|
|
||||||
feature.skipTests ? "4" : "6"
|
|
||||||
}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified**
|
|
||||||
${
|
|
||||||
feature.skipTests
|
|
||||||
? "5. **DO NOT commit changes** - the user will review and commit manually"
|
|
||||||
: "7. Commit your changes with git"
|
|
||||||
}
|
|
||||||
|
|
||||||
**IMPORTANT - Updating Feature Status:**
|
|
||||||
|
|
||||||
When you have completed the feature${
|
|
||||||
feature.skipTests ? "" : " and all tests pass"
|
|
||||||
}, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
|
|
||||||
- Call the tool with: featureId="${feature.id}" and status="verified"
|
|
||||||
- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes"
|
|
||||||
- **DO NOT manually edit feature files** - this can cause race conditions
|
|
||||||
- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data
|
|
||||||
- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior
|
|
||||||
|
|
||||||
**IMPORTANT - Feature Summary (REQUIRED):**
|
|
||||||
|
|
||||||
When calling UpdateFeatureStatus, you MUST include a summary parameter that describes:
|
|
||||||
- What files were modified/created
|
|
||||||
- What functionality was added or changed
|
|
||||||
- Any notable implementation decisions
|
|
||||||
|
|
||||||
Example:
|
|
||||||
\`\`\`
|
|
||||||
UpdateFeatureStatus(featureId="${
|
|
||||||
feature.id
|
|
||||||
}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
The summary will be displayed on the Kanban card so the user can see what was done without checking the code.
|
|
||||||
|
|
||||||
**Important Guidelines:**
|
|
||||||
|
|
||||||
- Focus ONLY on implementing this specific feature
|
|
||||||
- Write clean, production-quality code
|
|
||||||
- Add proper error handling
|
|
||||||
${
|
|
||||||
feature.skipTests
|
|
||||||
? "- Skip automated testing (skipTests=true) - user will manually verify"
|
|
||||||
: "- Write comprehensive Playwright tests\n- Ensure all existing tests still pass\n- Mark the feature as passing only when all tests are green\n- **CRITICAL: Delete test files after verification** - tests accumulate and become brittle"
|
|
||||||
}
|
|
||||||
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly**
|
|
||||||
- **CRITICAL: Always include a summary when marking feature as verified**
|
|
||||||
${
|
|
||||||
feature.skipTests
|
|
||||||
? "- **DO NOT commit changes** - user will review and commit manually"
|
|
||||||
: "- Make a git commit when complete"
|
|
||||||
}
|
|
||||||
|
|
||||||
**Testing Utilities (CRITICAL):**
|
|
||||||
|
|
||||||
1. **Create/maintain tests/utils.ts** - Add helper functions for finding elements and common test operations
|
|
||||||
2. **Use utilities in tests** - Import and use helper functions instead of repeating selectors
|
|
||||||
3. **Add utilities as needed** - When you write a test, if you need a new helper, add it to utils.ts
|
|
||||||
4. **Update utilities when functionality changes** - If you modify components, update corresponding utilities
|
|
||||||
|
|
||||||
Example utilities to add:
|
|
||||||
- getByTestId(page, testId) - Find elements by data-testid
|
|
||||||
- getButtonByText(page, text) - Find buttons by text
|
|
||||||
- clickElement(page, testId) - Click an element by test ID
|
|
||||||
- fillForm(page, formData) - Fill form fields
|
|
||||||
- waitForElement(page, testId) - Wait for element to appear
|
|
||||||
|
|
||||||
This makes future tests easier to write and maintain!
|
|
||||||
|
|
||||||
**Test Deletion Policy:**
|
|
||||||
After tests pass, delete them immediately:
|
|
||||||
\`\`\`bash
|
|
||||||
rm tests/[feature-name].spec.ts
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Begin by reading the project structure and then implementing the feature.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the prompt for verifying a specific feature
|
|
||||||
*/
|
|
||||||
async buildVerificationPrompt(feature, projectPath) {
|
|
||||||
const skipTestsNote = feature.skipTests
|
|
||||||
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
let imagesNote = "";
|
|
||||||
if (feature.imagePaths && feature.imagePaths.length > 0) {
|
|
||||||
const imagesList = feature.imagePaths
|
|
||||||
.map(
|
|
||||||
(img, idx) =>
|
|
||||||
` ${idx + 1}. ${img.filename} (${img.mimeType})\n Path: ${
|
|
||||||
img.path
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
imagesNote = `\n**📎 Context Images Attached:**\nThe user has attached ${feature.imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read:
|
|
||||||
|
|
||||||
${imagesList}
|
|
||||||
|
|
||||||
You can use the Read tool to view these images at any time during implementation. Review them carefully before implementing.\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get context files preview
|
|
||||||
const contextFilesPreview = await contextManager.getContextFilesPreview(
|
|
||||||
projectPath
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get memory content (lessons learned from previous runs)
|
|
||||||
const memoryContent = await contextManager.getMemoryContent(projectPath);
|
|
||||||
|
|
||||||
// Build mode header for this feature
|
|
||||||
const modeHeader = feature.skipTests
|
|
||||||
? `**🔨 MODE: Manual Review (No Automated Tests)**
|
|
||||||
This feature is set for manual review - focus on completing implementation without automated tests.`
|
|
||||||
: `**🧪 MODE: Test-Driven Development (TDD)**
|
|
||||||
This feature requires automated Playwright tests to verify the implementation.`;
|
|
||||||
|
|
||||||
return `You are implementing and verifying a feature until it is complete and working correctly.
|
|
||||||
|
|
||||||
${modeHeader}
|
|
||||||
${memoryContent}
|
|
||||||
|
|
||||||
**Feature to Implement/Verify:**
|
|
||||||
|
|
||||||
ID: ${feature.id}
|
|
||||||
Category: ${feature.category || "Uncategorized"}
|
|
||||||
Description: ${feature.description || feature.summary || feature.title || "No description provided"}
|
|
||||||
Current Status: ${feature.status}
|
|
||||||
${skipTestsNote}${imagesNote}${contextFilesPreview}
|
|
||||||
**Steps that should be implemented:**
|
|
||||||
${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"}
|
|
||||||
|
|
||||||
**Your Task:**
|
|
||||||
|
|
||||||
1. Read the project files to understand the current implementation
|
|
||||||
2. If the feature is not fully implemented, continue implementing it
|
|
||||||
${
|
|
||||||
feature.skipTests
|
|
||||||
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
|
|
||||||
: `3. Write or update Playwright tests to verify the feature works correctly
|
|
||||||
4. Run the Playwright tests: npx playwright test tests/[feature-name].spec.ts
|
|
||||||
5. Check if all tests pass
|
|
||||||
6. **If ANY tests fail:**
|
|
||||||
- Analyze the test failures and error messages
|
|
||||||
- Fix the implementation code to make the tests pass
|
|
||||||
- Update test utilities in tests/utils.ts if needed
|
|
||||||
- Re-run the tests to verify the fixes
|
|
||||||
- **REPEAT this process until ALL tests pass**
|
|
||||||
7. **If ALL tests pass:**
|
|
||||||
- **DELETE the test file(s) for this feature** - tests are only for immediate verification`
|
|
||||||
}
|
|
||||||
${
|
|
||||||
feature.skipTests ? "4" : "8"
|
|
||||||
}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified**
|
|
||||||
${
|
|
||||||
feature.skipTests
|
|
||||||
? "5. **DO NOT commit changes** - the user will review and commit manually"
|
|
||||||
: "9. Explain what was implemented/fixed and that all tests passed\n10. Commit your changes with git"
|
|
||||||
}
|
|
||||||
|
|
||||||
**IMPORTANT - Updating Feature Status:**
|
|
||||||
|
|
||||||
When you have completed the feature${
|
|
||||||
feature.skipTests ? "" : " and all tests pass"
|
|
||||||
}, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
|
|
||||||
- Call the tool with: featureId="${feature.id}" and status="verified"
|
|
||||||
- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes"
|
|
||||||
- **DO NOT manually edit feature files** - this can cause race conditions
|
|
||||||
- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data
|
|
||||||
- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior
|
|
||||||
|
|
||||||
**IMPORTANT - Feature Summary (REQUIRED):**
|
|
||||||
|
|
||||||
When calling UpdateFeatureStatus, you MUST include a summary parameter that describes:
|
|
||||||
- What files were modified/created
|
|
||||||
- What functionality was added or changed
|
|
||||||
- Any notable implementation decisions
|
|
||||||
|
|
||||||
Example:
|
|
||||||
\`\`\`
|
|
||||||
UpdateFeatureStatus(featureId="${
|
|
||||||
feature.id
|
|
||||||
}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
The summary will be displayed on the Kanban card so the user can see what was done without checking the code.
|
|
||||||
|
|
||||||
**Testing Utilities:**
|
|
||||||
- Check if tests/utils.ts exists and is being used
|
|
||||||
- If utilities are outdated due to functionality changes, update them
|
|
||||||
- Add new utilities as needed for this feature's tests
|
|
||||||
- Ensure test utilities stay in sync with code changes
|
|
||||||
|
|
||||||
**Test Deletion Policy:**
|
|
||||||
After tests pass, delete them immediately:
|
|
||||||
\`\`\`bash
|
|
||||||
rm tests/[feature-name].spec.ts
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Important:**
|
|
||||||
${
|
|
||||||
feature.skipTests
|
|
||||||
? "- Skip automated testing (skipTests=true) - user will manually verify\n- **DO NOT commit changes** - user will review and commit manually"
|
|
||||||
: "- **CONTINUE IMPLEMENTING until all tests pass** - don't stop at the first failure\n- Only mark as verified if Playwright tests pass\n- **CRITICAL: Delete test files after they pass** - tests should not accumulate\n- Update test utilities if functionality changed\n- Make a git commit when the feature is complete\n- Be thorough and persistent in fixing issues"
|
|
||||||
}
|
|
||||||
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly**
|
|
||||||
- **CRITICAL: Always include a summary when marking feature as verified**
|
|
||||||
|
|
||||||
Begin by reading the project structure and understanding what needs to be implemented or fixed.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build prompt for resuming feature with previous context
|
|
||||||
*/
|
|
||||||
async buildResumePrompt(feature, previousContext, projectPath) {
|
|
||||||
const skipTestsNote = feature.skipTests
|
|
||||||
? `\n**⚠️ IMPORTANT - Manual Testing Mode:**\nThis feature has skipTests=true, which means:\n- DO NOT commit changes automatically\n- DO NOT mark as verified - it will automatically go to "waiting_approval" status\n- The user will manually review and commit the changes\n- Just implement the feature and mark it as verified (it will be converted to waiting_approval)\n`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// For resume, check both followUpImages and imagePaths
|
|
||||||
const imagePaths = feature.followUpImages || feature.imagePaths;
|
|
||||||
let imagesNote = "";
|
|
||||||
if (imagePaths && imagePaths.length > 0) {
|
|
||||||
const imagesList = imagePaths
|
|
||||||
.map((img, idx) => {
|
|
||||||
// Handle both FeatureImagePath objects and simple path strings
|
|
||||||
const path = typeof img === "string" ? img : img.path;
|
|
||||||
const filename =
|
|
||||||
typeof img === "string" ? path.split("/").pop() : img.filename;
|
|
||||||
const mimeType = typeof img === "string" ? "image/*" : img.mimeType;
|
|
||||||
return ` ${
|
|
||||||
idx + 1
|
|
||||||
}. ${filename} (${mimeType})\n Path: ${path}`;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
imagesNote = `\n**📎 Context Images Attached:**\nThe user has attached ${imagePaths.length} image(s) for context. These images are provided both visually (in the initial message) and as files you can read:
|
|
||||||
|
|
||||||
${imagesList}
|
|
||||||
|
|
||||||
You can use the Read tool to view these images at any time. Review them carefully.\n`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get context files preview
|
|
||||||
const contextFilesPreview = await contextManager.getContextFilesPreview(
|
|
||||||
projectPath
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get memory content (lessons learned from previous runs)
|
|
||||||
const memoryContent = await contextManager.getMemoryContent(projectPath);
|
|
||||||
|
|
||||||
// Build mode header for this feature
|
|
||||||
const modeHeader = feature.skipTests
|
|
||||||
? `**🔨 MODE: Manual Review (No Automated Tests)**
|
|
||||||
This feature is set for manual review - focus on clean implementation without automated tests.`
|
|
||||||
: `**🧪 MODE: Test-Driven Development (TDD)**
|
|
||||||
This feature requires automated Playwright tests to verify the implementation.`;
|
|
||||||
|
|
||||||
return `You are resuming work on a feature implementation that was previously started.
|
|
||||||
|
|
||||||
${modeHeader}
|
|
||||||
${memoryContent}
|
|
||||||
**Current Feature:**
|
|
||||||
|
|
||||||
ID: ${feature.id}
|
|
||||||
Category: ${feature.category || "Uncategorized"}
|
|
||||||
Description: ${feature.description || feature.summary || feature.title || "No description provided"}
|
|
||||||
${skipTestsNote}${imagesNote}${contextFilesPreview}
|
|
||||||
**Steps to Complete:**
|
|
||||||
${(feature.steps || []).map((step, i) => `${i + 1}. ${step}`).join("\n") || "No specific steps provided - implement based on description"}
|
|
||||||
|
|
||||||
**Previous Work Context:**
|
|
||||||
|
|
||||||
${previousContext || "No previous context available - this is a fresh start."}
|
|
||||||
|
|
||||||
**Your Task:**
|
|
||||||
|
|
||||||
Continue where you left off and complete the feature implementation:
|
|
||||||
|
|
||||||
1. Review the previous work context above to understand what has been done
|
|
||||||
2. Continue implementing the feature according to the description and steps
|
|
||||||
${
|
|
||||||
feature.skipTests
|
|
||||||
? "3. Test the implementation manually (no automated tests needed for skipTests features)"
|
|
||||||
: "3. Write Playwright tests to verify the feature works correctly (if not already done)\n4. Run the tests and ensure they pass\n5. **DELETE the test file(s) you created** - tests are only for immediate verification"
|
|
||||||
}
|
|
||||||
${
|
|
||||||
feature.skipTests ? "4" : "6"
|
|
||||||
}. **CRITICAL: Use the UpdateFeatureStatus tool to mark this feature as verified**
|
|
||||||
${
|
|
||||||
feature.skipTests
|
|
||||||
? "5. **DO NOT commit changes** - the user will review and commit manually"
|
|
||||||
: "7. Commit your changes with git"
|
|
||||||
}
|
|
||||||
|
|
||||||
**IMPORTANT - Updating Feature Status:**
|
|
||||||
|
|
||||||
When you have completed the feature${
|
|
||||||
feature.skipTests ? "" : " and all tests pass"
|
|
||||||
}, you MUST use the \`mcp__automaker-tools__UpdateFeatureStatus\` tool to update the feature status:
|
|
||||||
- Call the tool with: featureId="${feature.id}" and status="verified"
|
|
||||||
- **You can also include a summary parameter** to describe what was done: summary="Brief summary of changes"
|
|
||||||
- **DO NOT manually edit feature files** - this can cause race conditions
|
|
||||||
- The UpdateFeatureStatus tool safely updates the feature status without risk of corrupting other data
|
|
||||||
- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct behavior
|
|
||||||
|
|
||||||
**IMPORTANT - Feature Summary (REQUIRED):**
|
|
||||||
|
|
||||||
When calling UpdateFeatureStatus, you MUST include a summary parameter that describes:
|
|
||||||
- What files were modified/created
|
|
||||||
- What functionality was added or changed
|
|
||||||
- Any notable implementation decisions
|
|
||||||
|
|
||||||
Example:
|
|
||||||
\`\`\`
|
|
||||||
UpdateFeatureStatus(featureId="${
|
|
||||||
feature.id
|
|
||||||
}", status="verified", summary="Added dark mode toggle to settings. Modified: settings.tsx, theme-provider.tsx. Created new useTheme hook.")
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
The summary will be displayed on the Kanban card so the user can see what was done without checking the code.
|
|
||||||
|
|
||||||
**Important Guidelines:**
|
|
||||||
|
|
||||||
- Review what was already done in the previous context
|
|
||||||
- Don't redo work that's already complete - continue from where it left off
|
|
||||||
- Focus on completing any remaining tasks
|
|
||||||
${
|
|
||||||
feature.skipTests
|
|
||||||
? "- Skip automated testing (skipTests=true) - user will manually verify"
|
|
||||||
: "- Write comprehensive Playwright tests if not already done\n- Ensure all tests pass before marking as verified\n- **CRITICAL: Delete test files after verification**"
|
|
||||||
}
|
|
||||||
- **CRITICAL: Use UpdateFeatureStatus tool instead of editing feature files directly**
|
|
||||||
- **CRITICAL: Always include a summary when marking feature as verified**
|
|
||||||
${
|
|
||||||
feature.skipTests
|
|
||||||
? "- **DO NOT commit changes** - user will review and commit manually"
|
|
||||||
: "- Make a git commit when complete"
|
|
||||||
}
|
|
||||||
|
|
||||||
Begin by assessing what's been done and what remains to be completed.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build the prompt for project analysis
|
|
||||||
*/
|
|
||||||
buildProjectAnalysisPrompt(projectPath) {
|
|
||||||
return `You are analyzing a new project that was just opened in Automaker, an autonomous AI development studio.
|
|
||||||
|
|
||||||
**Your Task:**
|
|
||||||
|
|
||||||
Analyze this project's codebase and update the .automaker/app_spec.txt file with accurate information about:
|
|
||||||
|
|
||||||
1. **Project Name** - Detect the name from package.json, README, or directory name
|
|
||||||
2. **Overview** - Brief description of what the project does
|
|
||||||
3. **Technology Stack** - Languages, frameworks, libraries detected
|
|
||||||
4. **Core Capabilities** - Main features and functionality
|
|
||||||
5. **Implemented Features** - What features are already built
|
|
||||||
6. **Implementation Roadmap** - Break down remaining work into phases with individual features
|
|
||||||
|
|
||||||
**Steps to Follow:**
|
|
||||||
|
|
||||||
1. First, explore the project structure:
|
|
||||||
- Look at package.json, cargo.toml, go.mod, requirements.txt, etc. for tech stack
|
|
||||||
- Check README.md for project description
|
|
||||||
- List key directories (src, lib, components, etc.)
|
|
||||||
|
|
||||||
2. Identify the tech stack:
|
|
||||||
- Frontend framework (React, Vue, Next.js, etc.)
|
|
||||||
- Backend framework (Express, FastAPI, etc.)
|
|
||||||
- Database (if any config files exist)
|
|
||||||
- Testing framework
|
|
||||||
- Build tools
|
|
||||||
|
|
||||||
3. Update .automaker/app_spec.txt with your findings in this format:
|
|
||||||
\`\`\`xml
|
|
||||||
<project_specification>
|
|
||||||
<project_name>Detected Name</project_name>
|
|
||||||
|
|
||||||
<overview>
|
|
||||||
Clear description of what this project does based on your analysis.
|
|
||||||
</overview>
|
|
||||||
|
|
||||||
<technology_stack>
|
|
||||||
<frontend>
|
|
||||||
<framework>Framework Name</framework>
|
|
||||||
<!-- Add detected technologies -->
|
|
||||||
</frontend>
|
|
||||||
<backend>
|
|
||||||
<!-- If applicable -->
|
|
||||||
</backend>
|
|
||||||
<database>
|
|
||||||
<!-- If applicable -->
|
|
||||||
</database>
|
|
||||||
<testing>
|
|
||||||
<!-- Testing frameworks detected -->
|
|
||||||
</testing>
|
|
||||||
</technology_stack>
|
|
||||||
|
|
||||||
<core_capabilities>
|
|
||||||
<!-- List main features/capabilities you found -->
|
|
||||||
</core_capabilities>
|
|
||||||
|
|
||||||
<implemented_features>
|
|
||||||
<!-- List specific features that appear to be implemented -->
|
|
||||||
</implemented_features>
|
|
||||||
|
|
||||||
<implementation_roadmap>
|
|
||||||
<phase_1_foundation>
|
|
||||||
<!-- List foundational features to build first -->
|
|
||||||
</phase_1_foundation>
|
|
||||||
<phase_2_core_logic>
|
|
||||||
<!-- List core logic features -->
|
|
||||||
</phase_2_core_logic>
|
|
||||||
<phase_3_polish>
|
|
||||||
<!-- List polish and enhancement features -->
|
|
||||||
</phase_3_polish>
|
|
||||||
</implementation_roadmap>
|
|
||||||
</project_specification>
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
4. Ensure .automaker/context/ directory exists
|
|
||||||
|
|
||||||
5. Ensure .automaker/features/ directory exists
|
|
||||||
|
|
||||||
**Important:**
|
|
||||||
- Be concise but accurate
|
|
||||||
- Only include information you can verify from the codebase
|
|
||||||
- If unsure about something, note it as "to be determined"
|
|
||||||
- Don't make up features that don't exist
|
|
||||||
- Features are stored in .automaker/features/{id}/feature.json - each feature gets its own folder
|
|
||||||
|
|
||||||
Begin by exploring the project structure.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the system prompt for coding agent
|
|
||||||
* @param {string} projectPath - Path to the project
|
|
||||||
* @param {boolean} isTDD - Whether this is Test-Driven Development mode (skipTests=false)
|
|
||||||
*/
|
|
||||||
async getCodingPrompt(projectPath, isTDD = true) {
|
|
||||||
// Get context files preview
|
|
||||||
const contextFilesPreview = projectPath
|
|
||||||
? await contextManager.getContextFilesPreview(projectPath)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// Get memory content (lessons learned from previous runs)
|
|
||||||
const memoryContent = projectPath
|
|
||||||
? await contextManager.getMemoryContent(projectPath)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// Build mode-specific instructions
|
|
||||||
const modeHeader = isTDD
|
|
||||||
? `**🧪 MODE: Test-Driven Development (TDD)**
|
|
||||||
You are implementing features using TDD methodology. This means:
|
|
||||||
- Write Playwright tests BEFORE or alongside implementation
|
|
||||||
- Run tests frequently to verify your work
|
|
||||||
- Tests are your validation mechanism
|
|
||||||
- Delete tests after they pass (they're for immediate verification only)`
|
|
||||||
: `**🔨 MODE: Manual Review (No Automated Tests)**
|
|
||||||
You are implementing features for manual user review. This means:
|
|
||||||
- Focus on clean, working implementation
|
|
||||||
- NO automated test writing required
|
|
||||||
- User will manually verify the implementation
|
|
||||||
- DO NOT commit changes - user will review and commit`;
|
|
||||||
|
|
||||||
return `You are an AI coding agent working autonomously to implement features.
|
|
||||||
|
|
||||||
${modeHeader}
|
|
||||||
${memoryContent}
|
|
||||||
|
|
||||||
**Feature Storage:**
|
|
||||||
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
|
|
||||||
|
|
||||||
**THE ONLY WAY to update features:**
|
|
||||||
Use the mcp__automaker-tools__UpdateFeatureStatus tool with featureId, status, and summary parameters.
|
|
||||||
Do NOT manually edit feature.json files directly.
|
|
||||||
|
|
||||||
${contextFilesPreview}
|
|
||||||
|
|
||||||
Your role is to:
|
|
||||||
- Implement features exactly as specified
|
|
||||||
- Write production-quality code
|
|
||||||
- Check if feature.skipTests is true - if so, skip automated testing and don't commit
|
|
||||||
- Create comprehensive Playwright tests using testing utilities (only if skipTests is false)
|
|
||||||
- Ensure all tests pass before marking features complete (only if skipTests is false)
|
|
||||||
- **DELETE test files after successful verification** - tests are only for immediate feature verification (only if skipTests is false)
|
|
||||||
- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature files
|
|
||||||
- **Always include a summary parameter when calling UpdateFeatureStatus** - describe what was done
|
|
||||||
- Commit working code to git (only if skipTests is false - skipTests features require manual review)
|
|
||||||
- Be thorough and detail-oriented
|
|
||||||
|
|
||||||
**IMPORTANT - Manual Testing Mode (skipTests=true):**
|
|
||||||
If a feature has skipTests=true:
|
|
||||||
- DO NOT write automated tests
|
|
||||||
- DO NOT commit changes - the user will review and commit manually
|
|
||||||
- Still mark the feature as verified using UpdateFeatureStatus - it will automatically convert to "waiting_approval" for manual review
|
|
||||||
- The user will manually verify and commit the changes
|
|
||||||
|
|
||||||
**IMPORTANT - UpdateFeatureStatus Tool:**
|
|
||||||
You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When the feature is complete (and all tests pass if skipTests is false), use this tool to update the feature status:
|
|
||||||
- Call with featureId, status="verified", and summary="Description of what was done"
|
|
||||||
- **DO NOT manually edit feature files** - this can cause race conditions and restore old state
|
|
||||||
- The tool safely updates the status without corrupting other feature data
|
|
||||||
- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct
|
|
||||||
|
|
||||||
**IMPORTANT - Feature Summary (REQUIRED):**
|
|
||||||
When calling UpdateFeatureStatus, you MUST include a summary parameter that describes:
|
|
||||||
- What files were modified/created
|
|
||||||
- What functionality was added or changed
|
|
||||||
- Any notable implementation decisions
|
|
||||||
|
|
||||||
Example: summary="Added dark mode toggle. Modified: settings.tsx, theme-provider.tsx. Created useTheme hook."
|
|
||||||
|
|
||||||
The summary will be displayed on the Kanban card so the user can quickly see what was done.
|
|
||||||
|
|
||||||
**Testing Utilities (CRITICAL):**
|
|
||||||
- **Create and maintain tests/utils.ts** with helper functions for finding elements and common operations
|
|
||||||
- **Always use utilities in tests** instead of repeating selectors
|
|
||||||
- **Add new utilities as you write tests** - if you need a helper, add it to utils.ts
|
|
||||||
- **Update utilities when functionality changes** - keep helpers in sync with code changes
|
|
||||||
|
|
||||||
This makes future tests easier to write and more maintainable!
|
|
||||||
|
|
||||||
**Test Deletion Policy:**
|
|
||||||
Tests should NOT accumulate. After a feature is verified:
|
|
||||||
1. Run the tests to ensure they pass
|
|
||||||
2. Delete the test file for that feature
|
|
||||||
3. Use UpdateFeatureStatus tool to mark the feature as "verified"
|
|
||||||
|
|
||||||
This prevents test brittleness as the app changes rapidly.
|
|
||||||
|
|
||||||
You have full access to:
|
|
||||||
- Read and write files
|
|
||||||
- Run bash commands
|
|
||||||
- Execute tests
|
|
||||||
- Delete files (rm command)
|
|
||||||
- Make git commits
|
|
||||||
- Search and analyze the codebase
|
|
||||||
- **UpdateFeatureStatus tool** (mcp__automaker-tools__UpdateFeatureStatus) - Use this to update feature status
|
|
||||||
|
|
||||||
**🧠 Learning from Errors - Memory System:**
|
|
||||||
|
|
||||||
If you encounter an error or issue that:
|
|
||||||
- Took multiple attempts to debug
|
|
||||||
- Was caused by a non-obvious codebase quirk
|
|
||||||
- Required understanding something specific about this project
|
|
||||||
- Could trip up future agent runs
|
|
||||||
|
|
||||||
**ADD IT TO MEMORY** by appending to \`.automaker/memory.md\`:
|
|
||||||
|
|
||||||
\`\`\`markdown
|
|
||||||
### Issue: [Brief Title]
|
|
||||||
**Problem:** [1-2 sentence description of the issue]
|
|
||||||
**Fix:** [Concise explanation of the solution]
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Keep entries concise - focus on the essential information needed to avoid the issue in the future. This helps both you and other agents learn from mistakes.
|
|
||||||
|
|
||||||
Focus on one feature at a time and complete it fully before finishing. Always delete tests after they pass and use the UpdateFeatureStatus tool.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the system prompt for verification agent
|
|
||||||
* @param {string} projectPath - Path to the project
|
|
||||||
* @param {boolean} isTDD - Whether this is Test-Driven Development mode (skipTests=false)
|
|
||||||
*/
|
|
||||||
async getVerificationPrompt(projectPath, isTDD = true) {
|
|
||||||
// Get context files preview
|
|
||||||
const contextFilesPreview = projectPath
|
|
||||||
? await contextManager.getContextFilesPreview(projectPath)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// Get memory content (lessons learned from previous runs)
|
|
||||||
const memoryContent = projectPath
|
|
||||||
? await contextManager.getMemoryContent(projectPath)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// Build mode-specific instructions
|
|
||||||
const modeHeader = isTDD
|
|
||||||
? `**🧪 MODE: Test-Driven Development (TDD)**
|
|
||||||
You are verifying/completing features using TDD methodology. This means:
|
|
||||||
- Run Playwright tests to verify implementation
|
|
||||||
- Fix failing tests by updating code
|
|
||||||
- Tests are your validation mechanism
|
|
||||||
- Delete tests after they pass (they're for immediate verification only)`
|
|
||||||
: `**🔨 MODE: Manual Review (No Automated Tests)**
|
|
||||||
You are completing features for manual user review. This means:
|
|
||||||
- Focus on clean, working implementation
|
|
||||||
- NO automated test writing required
|
|
||||||
- User will manually verify the implementation
|
|
||||||
- DO NOT commit changes - user will review and commit`;
|
|
||||||
|
|
||||||
return `You are an AI implementation and verification agent focused on completing features and ensuring they work.
|
|
||||||
|
|
||||||
${modeHeader}
|
|
||||||
${memoryContent}
|
|
||||||
**Feature Storage:**
|
|
||||||
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
|
|
||||||
|
|
||||||
**THE ONLY WAY to update features:**
|
|
||||||
Use the mcp__automaker-tools__UpdateFeatureStatus tool with featureId, status, and summary parameters.
|
|
||||||
Do NOT manually edit feature.json files directly.
|
|
||||||
|
|
||||||
${contextFilesPreview}
|
|
||||||
|
|
||||||
Your role is to:
|
|
||||||
- **Continue implementing features until they are complete** - don't stop at the first failure
|
|
||||||
- Check if feature.skipTests is true - if so, skip automated testing and don't commit
|
|
||||||
- Write or update code to fix failing tests (only if skipTests is false)
|
|
||||||
- Run Playwright tests to verify feature implementations (only if skipTests is false)
|
|
||||||
- If tests fail, analyze errors and fix the implementation (only if skipTests is false)
|
|
||||||
- If other tests fail, verify if those tests are still accurate or should be updated or deleted (only if skipTests is false)
|
|
||||||
- Continue rerunning tests and fixing issues until ALL tests pass (only if skipTests is false)
|
|
||||||
- **DELETE test files after successful verification** - tests are only for immediate feature verification (only if skipTests is false)
|
|
||||||
- **Use the UpdateFeatureStatus tool to mark features as verified** - NEVER manually edit feature files
|
|
||||||
- **Always include a summary parameter when calling UpdateFeatureStatus** - describe what was done
|
|
||||||
- **Update test utilities (tests/utils.ts) if functionality changed** - keep helpers in sync with code (only if skipTests is false)
|
|
||||||
- Commit working code to git (only if skipTests is false - skipTests features require manual review)
|
|
||||||
|
|
||||||
**IMPORTANT - Manual Testing Mode (skipTests=true):**
|
|
||||||
If a feature has skipTests=true:
|
|
||||||
- DO NOT write automated tests
|
|
||||||
- DO NOT commit changes - the user will review and commit manually
|
|
||||||
- Still mark the feature as verified using UpdateFeatureStatus - it will automatically convert to "waiting_approval" for manual review
|
|
||||||
- The user will manually verify and commit the changes
|
|
||||||
|
|
||||||
**IMPORTANT - UpdateFeatureStatus Tool:**
|
|
||||||
You have access to the \`mcp__automaker-tools__UpdateFeatureStatus\` tool. When the feature is complete (and all tests pass if skipTests is false), use this tool to update the feature status:
|
|
||||||
- Call with featureId, status="verified", and summary="Description of what was done"
|
|
||||||
- **DO NOT manually edit feature files** - this can cause race conditions and restore old state
|
|
||||||
- The tool safely updates the status without corrupting other feature data
|
|
||||||
- **If skipTests=true, the tool will automatically convert "verified" to "waiting_approval"** - this is correct
|
|
||||||
|
|
||||||
**IMPORTANT - Feature Summary (REQUIRED):**
|
|
||||||
When calling UpdateFeatureStatus, you MUST include a summary parameter that describes:
|
|
||||||
- What files were modified/created
|
|
||||||
- What functionality was added or changed
|
|
||||||
- Any notable implementation decisions
|
|
||||||
|
|
||||||
Example: summary="Fixed login validation. Modified: auth.ts, login-form.tsx. Added password strength check."
|
|
||||||
|
|
||||||
The summary will be displayed on the Kanban card so the user can quickly see what was done.
|
|
||||||
|
|
||||||
**Testing Utilities:**
|
|
||||||
- Check if tests/utils.ts needs updates based on code changes
|
|
||||||
- If a component's selectors or behavior changed, update the corresponding utility functions
|
|
||||||
- Add new utilities as needed for the feature's tests
|
|
||||||
- Ensure utilities remain accurate and helpful for future tests
|
|
||||||
|
|
||||||
**Test Deletion Policy:**
|
|
||||||
Tests should NOT accumulate. After a feature is verified:
|
|
||||||
1. Delete the test file for that feature
|
|
||||||
2. Use UpdateFeatureStatus tool to mark the feature as "verified"
|
|
||||||
|
|
||||||
This prevents test brittleness as the app changes rapidly.
|
|
||||||
|
|
||||||
You have access to:
|
|
||||||
- Read and edit files
|
|
||||||
- Write new code or modify existing code
|
|
||||||
- Run bash commands (especially Playwright tests)
|
|
||||||
- Delete files (rm command)
|
|
||||||
- Analyze test output
|
|
||||||
- Make git commits
|
|
||||||
- **UpdateFeatureStatus tool** (mcp__automaker-tools__UpdateFeatureStatus) - Use this to update feature status
|
|
||||||
|
|
||||||
**🧠 Learning from Errors - Memory System:**
|
|
||||||
|
|
||||||
If you encounter an error or issue that:
|
|
||||||
- Took multiple attempts to debug
|
|
||||||
- Was caused by a non-obvious codebase quirk
|
|
||||||
- Required understanding something specific about this project
|
|
||||||
- Could trip up future agent runs
|
|
||||||
|
|
||||||
**ADD IT TO MEMORY** by appending to \`.automaker/memory.md\`:
|
|
||||||
|
|
||||||
\`\`\`markdown
|
|
||||||
### Issue: [Brief Title]
|
|
||||||
**Problem:** [1-2 sentence description of the issue]
|
|
||||||
**Fix:** [Concise explanation of the solution]
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
Keep entries concise - focus on the essential information needed to avoid the issue in the future. This helps both you and other agents learn from mistakes.
|
|
||||||
|
|
||||||
**CRITICAL:** Be persistent and thorough - keep iterating on the implementation until all tests pass. Don't give up after the first failure. Always delete tests after they pass, use the UpdateFeatureStatus tool with a summary, and commit your work.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get system prompt for project analysis agent
|
|
||||||
*/
|
|
||||||
getProjectAnalysisSystemPrompt() {
|
|
||||||
return `You are a project analysis agent that examines codebases to understand their structure, tech stack, and implemented features.
|
|
||||||
|
|
||||||
Your goal is to:
|
|
||||||
- Quickly scan and understand project structure
|
|
||||||
- Identify programming languages, frameworks, and libraries
|
|
||||||
- Detect existing features and capabilities
|
|
||||||
- Update the .automaker/app_spec.txt with accurate information
|
|
||||||
- Ensure all required .automaker files and directories exist
|
|
||||||
|
|
||||||
Be efficient - don't read every file, focus on:
|
|
||||||
- Configuration files (package.json, tsconfig.json, etc.)
|
|
||||||
- Main entry points
|
|
||||||
- Directory structure
|
|
||||||
- README and documentation
|
|
||||||
|
|
||||||
**Feature Storage:**
|
|
||||||
Features are stored in .automaker/features/{id}/feature.json - each feature has its own folder.
|
|
||||||
Use the UpdateFeatureStatus tool to manage features, not direct file edits.
|
|
||||||
|
|
||||||
You have access to Read, Write, Edit, Glob, Grep, and Bash tools. Use them to explore the structure and write the necessary files.`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new PromptBuilder();
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
const os = require("os");
|
|
||||||
|
|
||||||
// Prefer prebuilt to avoid native build issues.
|
|
||||||
const pty = require("@homebridge/node-pty-prebuilt-multiarch");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimal PTY helper to run CLI commands with a pseudo-terminal.
|
|
||||||
* Useful for CLIs (like Claude) that need raw mode on Windows.
|
|
||||||
*
|
|
||||||
* @param {string} command Executable path
|
|
||||||
* @param {string[]} args Arguments for the executable
|
|
||||||
* @param {Object} options Additional spawn options
|
|
||||||
* @param {(chunk: string) => void} [options.onData] Data callback
|
|
||||||
* @param {string} [options.cwd] Working directory
|
|
||||||
* @param {Object} [options.env] Extra env vars
|
|
||||||
* @param {number} [options.cols] Terminal columns
|
|
||||||
* @param {number} [options.rows] Terminal rows
|
|
||||||
* @returns {Promise<{ success: boolean, exitCode: number, signal?: number, output: string, errorOutput: string }>}
|
|
||||||
*/
|
|
||||||
function runPtyCommand(command, args = [], options = {}) {
|
|
||||||
const {
|
|
||||||
onData,
|
|
||||||
cwd = process.cwd(),
|
|
||||||
env = {},
|
|
||||||
cols = 120,
|
|
||||||
rows = 30,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
const mergedEnv = {
|
|
||||||
...process.env,
|
|
||||||
TERM: process.env.TERM || "xterm-256color",
|
|
||||||
...env,
|
|
||||||
};
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
let ptyProcess;
|
|
||||||
|
|
||||||
try {
|
|
||||||
ptyProcess = pty.spawn(command, args, {
|
|
||||||
name: os.platform() === "win32" ? "Windows.Terminal" : "xterm-color",
|
|
||||||
cols,
|
|
||||||
rows,
|
|
||||||
cwd,
|
|
||||||
env: mergedEnv,
|
|
||||||
useConpty: true,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
return reject(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let output = "";
|
|
||||||
let errorOutput = "";
|
|
||||||
|
|
||||||
ptyProcess.onData((data) => {
|
|
||||||
output += data;
|
|
||||||
if (typeof onData === "function") {
|
|
||||||
onData(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// node-pty does not emit 'error' in practice, but guard anyway
|
|
||||||
if (ptyProcess.on) {
|
|
||||||
ptyProcess.on("error", (err) => {
|
|
||||||
errorOutput += err?.message || "";
|
|
||||||
reject(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ptyProcess.onExit(({ exitCode, signal }) => {
|
|
||||||
resolve({
|
|
||||||
success: exitCode === 0,
|
|
||||||
exitCode,
|
|
||||||
signal,
|
|
||||||
output,
|
|
||||||
errorOutput,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
runPtyCommand,
|
|
||||||
};
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,569 +0,0 @@
|
|||||||
const path = require("path");
|
|
||||||
const fs = require("fs/promises");
|
|
||||||
const { exec, spawn } = require("child_process");
|
|
||||||
const { promisify } = require("util");
|
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Worktree Manager - Handles git worktrees for feature isolation
|
|
||||||
*
|
|
||||||
* This service creates isolated git worktrees for each feature, allowing:
|
|
||||||
* - Features to be worked on in isolation without affecting the main branch
|
|
||||||
* - Easy rollback/revert by simply deleting the worktree
|
|
||||||
* - Checkpointing - user can see changes in the worktree before merging
|
|
||||||
*/
|
|
||||||
class WorktreeManager {
|
|
||||||
constructor() {
|
|
||||||
// Cache for worktree info
|
|
||||||
this.worktreeCache = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the base worktree directory path
|
|
||||||
*/
|
|
||||||
getWorktreeBasePath(projectPath) {
|
|
||||||
return path.join(projectPath, ".automaker", "worktrees");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate a safe branch name from feature description
|
|
||||||
*/
|
|
||||||
generateBranchName(feature) {
|
|
||||||
// Create a slug from the description
|
|
||||||
const slug = feature.description
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9\s-]/g, "") // Remove special chars
|
|
||||||
.replace(/\s+/g, "-") // Replace spaces with hyphens
|
|
||||||
.substring(0, 40); // Limit length
|
|
||||||
|
|
||||||
// Add feature ID for uniqueness
|
|
||||||
const shortId = feature.id.replace("feature-", "").substring(0, 12);
|
|
||||||
return `feature/${shortId}-${slug}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the project is a git repository
|
|
||||||
*/
|
|
||||||
async isGitRepo(projectPath) {
|
|
||||||
try {
|
|
||||||
await execAsync("git rev-parse --is-inside-work-tree", { cwd: projectPath });
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current branch name
|
|
||||||
*/
|
|
||||||
async getCurrentBranch(projectPath) {
|
|
||||||
try {
|
|
||||||
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", { cwd: projectPath });
|
|
||||||
return stdout.trim();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[WorktreeManager] Failed to get current branch:", error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a branch exists (local or remote)
|
|
||||||
*/
|
|
||||||
async branchExists(projectPath, branchName) {
|
|
||||||
try {
|
|
||||||
await execAsync(`git rev-parse --verify ${branchName}`, { cwd: projectPath });
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List all existing worktrees
|
|
||||||
*/
|
|
||||||
async listWorktrees(projectPath) {
|
|
||||||
try {
|
|
||||||
const { stdout } = await execAsync("git worktree list --porcelain", { cwd: projectPath });
|
|
||||||
const worktrees = [];
|
|
||||||
const lines = stdout.split("\n");
|
|
||||||
|
|
||||||
let currentWorktree = null;
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith("worktree ")) {
|
|
||||||
if (currentWorktree) {
|
|
||||||
worktrees.push(currentWorktree);
|
|
||||||
}
|
|
||||||
currentWorktree = { path: line.replace("worktree ", "") };
|
|
||||||
} else if (line.startsWith("branch ") && currentWorktree) {
|
|
||||||
currentWorktree.branch = line.replace("branch refs/heads/", "");
|
|
||||||
} else if (line.startsWith("HEAD ") && currentWorktree) {
|
|
||||||
currentWorktree.head = line.replace("HEAD ", "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (currentWorktree) {
|
|
||||||
worktrees.push(currentWorktree);
|
|
||||||
}
|
|
||||||
|
|
||||||
return worktrees;
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[WorktreeManager] Failed to list worktrees:", error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a worktree for a feature
|
|
||||||
* @param {string} projectPath - Path to the main project
|
|
||||||
* @param {object} feature - Feature object with id and description
|
|
||||||
* @returns {object} - { success, worktreePath, branchName, error }
|
|
||||||
*/
|
|
||||||
async createWorktree(projectPath, feature) {
|
|
||||||
console.log(`[WorktreeManager] Creating worktree for feature: ${feature.id}`);
|
|
||||||
|
|
||||||
// Check if project is a git repo
|
|
||||||
if (!await this.isGitRepo(projectPath)) {
|
|
||||||
return { success: false, error: "Project is not a git repository" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const branchName = this.generateBranchName(feature);
|
|
||||||
const worktreeBasePath = this.getWorktreeBasePath(projectPath);
|
|
||||||
const worktreePath = path.join(worktreeBasePath, branchName.replace("feature/", ""));
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Ensure worktree directory exists
|
|
||||||
await fs.mkdir(worktreeBasePath, { recursive: true });
|
|
||||||
|
|
||||||
// Check if worktree already exists
|
|
||||||
const worktrees = await this.listWorktrees(projectPath);
|
|
||||||
const existingWorktree = worktrees.find(
|
|
||||||
w => w.path === worktreePath || w.branch === branchName
|
|
||||||
);
|
|
||||||
|
|
||||||
if (existingWorktree) {
|
|
||||||
console.log(`[WorktreeManager] Worktree already exists for feature: ${feature.id}`);
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
worktreePath: existingWorktree.path,
|
|
||||||
branchName: existingWorktree.branch,
|
|
||||||
existed: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current branch to base the new branch on
|
|
||||||
const baseBranch = await this.getCurrentBranch(projectPath);
|
|
||||||
if (!baseBranch) {
|
|
||||||
return { success: false, error: "Could not determine current branch" };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if branch already exists
|
|
||||||
const branchExists = await this.branchExists(projectPath, branchName);
|
|
||||||
|
|
||||||
if (branchExists) {
|
|
||||||
// Use existing branch
|
|
||||||
console.log(`[WorktreeManager] Using existing branch: ${branchName}`);
|
|
||||||
await execAsync(`git worktree add "${worktreePath}" ${branchName}`, { cwd: projectPath });
|
|
||||||
} else {
|
|
||||||
// Create new worktree with new branch
|
|
||||||
console.log(`[WorktreeManager] Creating new branch: ${branchName} based on ${baseBranch}`);
|
|
||||||
await execAsync(`git worktree add -b ${branchName} "${worktreePath}" ${baseBranch}`, { cwd: projectPath });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy .automaker directory to worktree (except worktrees directory itself to avoid recursion)
|
|
||||||
const automakerSrc = path.join(projectPath, ".automaker");
|
|
||||||
const automakerDst = path.join(worktreePath, ".automaker");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await fs.mkdir(automakerDst, { recursive: true });
|
|
||||||
|
|
||||||
// Note: Features are stored in .automaker/features/{id}/feature.json
|
|
||||||
// These are managed by the main project, not copied to worktrees
|
|
||||||
|
|
||||||
// Copy app_spec.txt if it exists
|
|
||||||
const appSpecSrc = path.join(automakerSrc, "app_spec.txt");
|
|
||||||
const appSpecDst = path.join(automakerDst, "app_spec.txt");
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(appSpecSrc, "utf-8");
|
|
||||||
await fs.writeFile(appSpecDst, content, "utf-8");
|
|
||||||
} catch {
|
|
||||||
// App spec might not exist yet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy categories.json if it exists
|
|
||||||
const categoriesSrc = path.join(automakerSrc, "categories.json");
|
|
||||||
const categoriesDst = path.join(automakerDst, "categories.json");
|
|
||||||
try {
|
|
||||||
const content = await fs.readFile(categoriesSrc, "utf-8");
|
|
||||||
await fs.writeFile(categoriesDst, content, "utf-8");
|
|
||||||
} catch {
|
|
||||||
// Categories might not exist yet
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn("[WorktreeManager] Failed to copy .automaker directory:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store worktree info in cache
|
|
||||||
this.worktreeCache.set(feature.id, {
|
|
||||||
worktreePath,
|
|
||||||
branchName,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
baseBranch,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`[WorktreeManager] Worktree created at: ${worktreePath}`);
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
worktreePath,
|
|
||||||
branchName,
|
|
||||||
baseBranch,
|
|
||||||
existed: false,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[WorktreeManager] Failed to create worktree:", error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get worktree info for a feature
|
|
||||||
*/
|
|
||||||
async getWorktreeInfo(projectPath, featureId) {
|
|
||||||
// Check cache first
|
|
||||||
if (this.worktreeCache.has(featureId)) {
|
|
||||||
return { success: true, ...this.worktreeCache.get(featureId) };
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan worktrees to find matching one
|
|
||||||
const worktrees = await this.listWorktrees(projectPath);
|
|
||||||
const worktreeBasePath = this.getWorktreeBasePath(projectPath);
|
|
||||||
|
|
||||||
for (const worktree of worktrees) {
|
|
||||||
// Check if this worktree is in our worktree directory
|
|
||||||
if (worktree.path.startsWith(worktreeBasePath)) {
|
|
||||||
// Check if the feature ID is in the branch name
|
|
||||||
const shortId = featureId.replace("feature-", "").substring(0, 12);
|
|
||||||
if (worktree.branch && worktree.branch.includes(shortId)) {
|
|
||||||
const info = {
|
|
||||||
worktreePath: worktree.path,
|
|
||||||
branchName: worktree.branch,
|
|
||||||
head: worktree.head,
|
|
||||||
};
|
|
||||||
this.worktreeCache.set(featureId, info);
|
|
||||||
return { success: true, ...info };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: false, error: "Worktree not found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a worktree for a feature
|
|
||||||
* This effectively reverts all changes made by the agent
|
|
||||||
*/
|
|
||||||
async removeWorktree(projectPath, featureId, deleteBranch = false) {
|
|
||||||
console.log(`[WorktreeManager] Removing worktree for feature: ${featureId}`);
|
|
||||||
|
|
||||||
const worktreeInfo = await this.getWorktreeInfo(projectPath, featureId);
|
|
||||||
if (!worktreeInfo.success) {
|
|
||||||
console.log(`[WorktreeManager] No worktree found for feature: ${featureId}`);
|
|
||||||
return { success: true, message: "No worktree to remove" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { worktreePath, branchName } = worktreeInfo;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Remove the worktree
|
|
||||||
await execAsync(`git worktree remove "${worktreePath}" --force`, { cwd: projectPath });
|
|
||||||
console.log(`[WorktreeManager] Worktree removed: ${worktreePath}`);
|
|
||||||
|
|
||||||
// Optionally delete the branch too
|
|
||||||
if (deleteBranch && branchName) {
|
|
||||||
try {
|
|
||||||
await execAsync(`git branch -D ${branchName}`, { cwd: projectPath });
|
|
||||||
console.log(`[WorktreeManager] Branch deleted: ${branchName}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`[WorktreeManager] Could not delete branch ${branchName}:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from cache
|
|
||||||
this.worktreeCache.delete(featureId);
|
|
||||||
|
|
||||||
return { success: true, removedPath: worktreePath, removedBranch: deleteBranch ? branchName : null };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[WorktreeManager] Failed to remove worktree:", error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get status of changes in a worktree
|
|
||||||
*/
|
|
||||||
async getWorktreeStatus(worktreePath) {
|
|
||||||
try {
|
|
||||||
const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd: worktreePath });
|
|
||||||
const { stdout: diffStat } = await execAsync("git diff --stat", { cwd: worktreePath });
|
|
||||||
const { stdout: commitLog } = await execAsync("git log --oneline -10", { cwd: worktreePath });
|
|
||||||
|
|
||||||
const files = statusOutput.trim().split("\n").filter(Boolean);
|
|
||||||
const commits = commitLog.trim().split("\n").filter(Boolean);
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
modifiedFiles: files.length,
|
|
||||||
files: files.slice(0, 20), // Limit to 20 files
|
|
||||||
diffStat: diffStat.trim(),
|
|
||||||
recentCommits: commits.slice(0, 5), // Last 5 commits
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[WorktreeManager] Failed to get worktree status:", error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get detailed file diff content for a worktree
|
|
||||||
* Returns unified diff format for all changes
|
|
||||||
*/
|
|
||||||
async getFileDiffs(worktreePath) {
|
|
||||||
try {
|
|
||||||
// Get both staged and unstaged diffs
|
|
||||||
const { stdout: unstagedDiff } = await execAsync("git diff --no-color", {
|
|
||||||
cwd: worktreePath,
|
|
||||||
maxBuffer: 10 * 1024 * 1024 // 10MB buffer for large diffs
|
|
||||||
});
|
|
||||||
const { stdout: stagedDiff } = await execAsync("git diff --cached --no-color", {
|
|
||||||
cwd: worktreePath,
|
|
||||||
maxBuffer: 10 * 1024 * 1024
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get list of files with their status
|
|
||||||
const { stdout: statusOutput } = await execAsync("git status --porcelain", { cwd: worktreePath });
|
|
||||||
const files = statusOutput.trim().split("\n").filter(Boolean);
|
|
||||||
|
|
||||||
// Parse file statuses
|
|
||||||
const fileStatuses = files.map(line => {
|
|
||||||
const status = line.substring(0, 2);
|
|
||||||
const filePath = line.substring(3);
|
|
||||||
return {
|
|
||||||
status: status.trim() || 'M',
|
|
||||||
path: filePath,
|
|
||||||
statusText: this.getStatusText(status)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Combine diffs
|
|
||||||
const combinedDiff = [stagedDiff, unstagedDiff].filter(Boolean).join("\n");
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
diff: combinedDiff,
|
|
||||||
files: fileStatuses,
|
|
||||||
hasChanges: files.length > 0
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[WorktreeManager] Failed to get file diffs:", error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get human-readable status text from git status code
|
|
||||||
*/
|
|
||||||
getStatusText(status) {
|
|
||||||
const statusMap = {
|
|
||||||
'M': 'Modified',
|
|
||||||
'A': 'Added',
|
|
||||||
'D': 'Deleted',
|
|
||||||
'R': 'Renamed',
|
|
||||||
'C': 'Copied',
|
|
||||||
'U': 'Updated',
|
|
||||||
'?': 'Untracked',
|
|
||||||
'!': 'Ignored'
|
|
||||||
};
|
|
||||||
const firstChar = status.charAt(0);
|
|
||||||
const secondChar = status.charAt(1);
|
|
||||||
return statusMap[firstChar] || statusMap[secondChar] || 'Changed';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get diff for a specific file in a worktree
|
|
||||||
*/
|
|
||||||
async getFileDiff(worktreePath, filePath) {
|
|
||||||
try {
|
|
||||||
// Try to get unstaged diff first, then staged if no unstaged changes
|
|
||||||
let diff = '';
|
|
||||||
try {
|
|
||||||
const { stdout } = await execAsync(`git diff --no-color -- "${filePath}"`, {
|
|
||||||
cwd: worktreePath,
|
|
||||||
maxBuffer: 5 * 1024 * 1024
|
|
||||||
});
|
|
||||||
diff = stdout;
|
|
||||||
} catch {
|
|
||||||
// File might be staged
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!diff) {
|
|
||||||
try {
|
|
||||||
const { stdout } = await execAsync(`git diff --cached --no-color -- "${filePath}"`, {
|
|
||||||
cwd: worktreePath,
|
|
||||||
maxBuffer: 5 * 1024 * 1024
|
|
||||||
});
|
|
||||||
diff = stdout;
|
|
||||||
} catch {
|
|
||||||
// File might be untracked, show the content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If still no diff, might be an untracked file - show the content
|
|
||||||
if (!diff) {
|
|
||||||
try {
|
|
||||||
const fullPath = path.join(worktreePath, filePath);
|
|
||||||
const content = await fs.readFile(fullPath, 'utf-8');
|
|
||||||
diff = `+++ ${filePath} (new file)\n${content.split('\n').map(l => '+' + l).join('\n')}`;
|
|
||||||
} catch {
|
|
||||||
diff = '(Unable to read file content)';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
diff,
|
|
||||||
filePath
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`[WorktreeManager] Failed to get diff for ${filePath}:`, error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge worktree changes back to the main branch
|
|
||||||
*/
|
|
||||||
async mergeWorktree(projectPath, featureId, options = {}) {
|
|
||||||
console.log(`[WorktreeManager] Merging worktree for feature: ${featureId}`);
|
|
||||||
|
|
||||||
const worktreeInfo = await this.getWorktreeInfo(projectPath, featureId);
|
|
||||||
if (!worktreeInfo.success) {
|
|
||||||
return { success: false, error: "Worktree not found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { branchName, worktreePath } = worktreeInfo;
|
|
||||||
const baseBranch = await this.getCurrentBranch(projectPath);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// First commit any uncommitted changes in the worktree
|
|
||||||
const { stdout: status } = await execAsync("git status --porcelain", { cwd: worktreePath });
|
|
||||||
if (status.trim()) {
|
|
||||||
// There are uncommitted changes - commit them
|
|
||||||
await execAsync("git add -A", { cwd: worktreePath });
|
|
||||||
const commitMsg = options.commitMessage || `feat: complete ${featureId}`;
|
|
||||||
await execAsync(`git commit -m "${commitMsg}"`, { cwd: worktreePath });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge the feature branch into the current branch in the main repo
|
|
||||||
if (options.squash) {
|
|
||||||
await execAsync(`git merge --squash ${branchName}`, { cwd: projectPath });
|
|
||||||
const squashMsg = options.squashMessage || `feat: ${featureId} - squashed merge`;
|
|
||||||
await execAsync(`git commit -m "${squashMsg}"`, { cwd: projectPath });
|
|
||||||
} else {
|
|
||||||
await execAsync(`git merge ${branchName} --no-ff -m "Merge ${branchName}"`, { cwd: projectPath });
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[WorktreeManager] Successfully merged ${branchName} into ${baseBranch}`);
|
|
||||||
|
|
||||||
// Optionally cleanup worktree after merge
|
|
||||||
if (options.cleanup) {
|
|
||||||
await this.removeWorktree(projectPath, featureId, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
mergedBranch: branchName,
|
|
||||||
intoBranch: baseBranch,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[WorktreeManager] Failed to merge worktree:", error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync changes from main branch to worktree (rebase or merge)
|
|
||||||
*/
|
|
||||||
async syncWorktree(projectPath, featureId, method = "rebase") {
|
|
||||||
console.log(`[WorktreeManager] Syncing worktree for feature: ${featureId}`);
|
|
||||||
|
|
||||||
const worktreeInfo = await this.getWorktreeInfo(projectPath, featureId);
|
|
||||||
if (!worktreeInfo.success) {
|
|
||||||
return { success: false, error: "Worktree not found" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { worktreePath, baseBranch } = worktreeInfo;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (method === "rebase") {
|
|
||||||
await execAsync(`git rebase ${baseBranch}`, { cwd: worktreePath });
|
|
||||||
} else {
|
|
||||||
await execAsync(`git merge ${baseBranch}`, { cwd: worktreePath });
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, method };
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[WorktreeManager] Failed to sync worktree:", error);
|
|
||||||
return { success: false, error: error.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of all feature worktrees
|
|
||||||
*/
|
|
||||||
async getAllFeatureWorktrees(projectPath) {
|
|
||||||
const worktrees = await this.listWorktrees(projectPath);
|
|
||||||
const worktreeBasePath = this.getWorktreeBasePath(projectPath);
|
|
||||||
|
|
||||||
return worktrees.filter(w =>
|
|
||||||
w.path.startsWith(worktreeBasePath) &&
|
|
||||||
w.branch &&
|
|
||||||
w.branch.startsWith("feature/")
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cleanup orphaned worktrees (worktrees without matching features)
|
|
||||||
*/
|
|
||||||
async cleanupOrphanedWorktrees(projectPath, activeFeatureIds) {
|
|
||||||
console.log("[WorktreeManager] Cleaning up orphaned worktrees...");
|
|
||||||
|
|
||||||
const worktrees = await this.getAllFeatureWorktrees(projectPath);
|
|
||||||
const cleaned = [];
|
|
||||||
|
|
||||||
for (const worktree of worktrees) {
|
|
||||||
// Extract feature ID from branch name
|
|
||||||
const branchParts = worktree.branch.replace("feature/", "").split("-");
|
|
||||||
const shortId = branchParts[0];
|
|
||||||
|
|
||||||
// Check if any active feature has this short ID
|
|
||||||
const hasMatchingFeature = activeFeatureIds.some(id => {
|
|
||||||
const featureShortId = id.replace("feature-", "").substring(0, 12);
|
|
||||||
return featureShortId === shortId;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasMatchingFeature) {
|
|
||||||
console.log(`[WorktreeManager] Removing orphaned worktree: ${worktree.path}`);
|
|
||||||
try {
|
|
||||||
await execAsync(`git worktree remove "${worktree.path}" --force`, { cwd: projectPath });
|
|
||||||
await execAsync(`git branch -D ${worktree.branch}`, { cwd: projectPath });
|
|
||||||
cleaned.push(worktree.path);
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`[WorktreeManager] Failed to cleanup worktree ${worktree.path}:`, error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true, cleaned };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = new WorktreeManager();
|
|
||||||
@@ -29,11 +29,9 @@
|
|||||||
"dev:electron:wsl:gpu": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA electron . --no-sandbox --disable-gpu-sandbox\""
|
"dev:electron:wsl:gpu": "concurrently \"next dev -p 3007\" \"wait-on http://localhost:3007 && MESA_D3D12_DEFAULT_ADAPTER_NAME=NVIDIA electron . --no-sandbox --disable-gpu-sandbox\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.1.61",
|
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -97,8 +95,7 @@
|
|||||||
"electron/**/*",
|
"electron/**/*",
|
||||||
".next/**/*",
|
".next/**/*",
|
||||||
"public/**/*",
|
"public/**/*",
|
||||||
"!node_modules/**/*",
|
"!node_modules/**/*"
|
||||||
"node_modules/@anthropic-ai/**/*"
|
|
||||||
],
|
],
|
||||||
"extraResources": [
|
"extraResources": [
|
||||||
{
|
{
|
||||||
|
|||||||
52
apps/app/src/components/delete-session-dialog.tsx
Normal file
52
apps/app/src/components/delete-session-dialog.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import { MessageSquare } from "lucide-react";
|
||||||
|
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||||
|
import type { SessionListItem } from "@/types/electron";
|
||||||
|
|
||||||
|
interface DeleteSessionDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
session: SessionListItem | null;
|
||||||
|
onConfirm: (sessionId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteSessionDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
session,
|
||||||
|
onConfirm,
|
||||||
|
}: DeleteSessionDialogProps) {
|
||||||
|
const handleConfirm = () => {
|
||||||
|
if (session) {
|
||||||
|
onConfirm(session.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DeleteConfirmDialog
|
||||||
|
open={open}
|
||||||
|
onOpenChange={onOpenChange}
|
||||||
|
onConfirm={handleConfirm}
|
||||||
|
title="Delete Session"
|
||||||
|
description="Are you sure you want to delete this session? This action cannot be undone."
|
||||||
|
confirmText="Delete Session"
|
||||||
|
testId="delete-session-dialog"
|
||||||
|
confirmTestId="confirm-delete-session"
|
||||||
|
>
|
||||||
|
{session && (
|
||||||
|
<div className="flex items-center gap-3 p-4 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
|
||||||
|
<MessageSquare className="w-5 h-5 text-brand-500" />
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<p className="font-medium text-foreground truncate">
|
||||||
|
{session.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{session.messageCount} messages
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DeleteConfirmDialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import { cn } from "@/lib/utils";
|
|||||||
import type { SessionListItem } from "@/types/electron";
|
import type { SessionListItem } from "@/types/electron";
|
||||||
import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts";
|
import { useKeyboardShortcutsConfig } from "@/hooks/use-keyboard-shortcuts";
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
import { getElectronAPI } from "@/lib/electron";
|
||||||
|
import { DeleteSessionDialog } from "@/components/delete-session-dialog";
|
||||||
|
|
||||||
// Random session name generator
|
// Random session name generator
|
||||||
const adjectives = [
|
const adjectives = [
|
||||||
@@ -113,6 +114,8 @@ export function SessionManager({
|
|||||||
const [runningSessions, setRunningSessions] = useState<Set<string>>(
|
const [runningSessions, setRunningSessions] = useState<Set<string>>(
|
||||||
new Set()
|
new Set()
|
||||||
);
|
);
|
||||||
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||||
|
const [sessionToDelete, setSessionToDelete] = useState<SessionListItem | null>(null);
|
||||||
|
|
||||||
// Check running state for all sessions
|
// Check running state for all sessions
|
||||||
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
|
const checkRunningSessions = async (sessionList: SessionListItem[]) => {
|
||||||
@@ -286,11 +289,16 @@ export function SessionManager({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete session
|
// Open delete session dialog
|
||||||
const handleDeleteSession = async (sessionId: string) => {
|
const handleDeleteSession = (session: SessionListItem) => {
|
||||||
|
setSessionToDelete(session);
|
||||||
|
setIsDeleteDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Confirm delete session
|
||||||
|
const confirmDeleteSession = async (sessionId: string) => {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
if (!api?.sessions) return;
|
if (!api?.sessions) return;
|
||||||
if (!confirm("Are you sure you want to delete this session?")) return;
|
|
||||||
|
|
||||||
const result = await api.sessions.delete(sessionId);
|
const result = await api.sessions.delete(sessionId);
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -303,6 +311,7 @@ export function SessionManager({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
setSessionToDelete(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeSessions = sessions.filter((s) => !s.isArchived);
|
const activeSessions = sessions.filter((s) => !s.isArchived);
|
||||||
@@ -315,20 +324,24 @@ export function SessionManager({
|
|||||||
<CardHeader className="pb-3">
|
<CardHeader className="pb-3">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<CardTitle>Agent Sessions</CardTitle>
|
<CardTitle>Agent Sessions</CardTitle>
|
||||||
{activeTab === "active" && (
|
<HotkeyButton
|
||||||
<HotkeyButton
|
variant="default"
|
||||||
variant="default"
|
size="sm"
|
||||||
size="sm"
|
onClick={() => {
|
||||||
onClick={handleQuickCreateSession}
|
// Switch to active tab if on archived tab
|
||||||
hotkey={shortcuts.newSession}
|
if (activeTab === "archived") {
|
||||||
hotkeyActive={false}
|
setActiveTab("active");
|
||||||
data-testid="new-session-button"
|
}
|
||||||
title={`New Session (${shortcuts.newSession})`}
|
handleQuickCreateSession();
|
||||||
>
|
}}
|
||||||
<Plus className="w-4 h-4 mr-1" />
|
hotkey={shortcuts.newSession}
|
||||||
New
|
hotkeyActive={false}
|
||||||
</HotkeyButton>
|
data-testid="new-session-button"
|
||||||
)}
|
title={`New Session (${shortcuts.newSession})`}
|
||||||
|
>
|
||||||
|
<Plus className="w-4 h-4 mr-1" />
|
||||||
|
New
|
||||||
|
</HotkeyButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Tabs
|
<Tabs
|
||||||
@@ -525,8 +538,9 @@ export function SessionManager({
|
|||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => handleDeleteSession(session.id)}
|
onClick={() => handleDeleteSession(session)}
|
||||||
className="h-7 w-7 p-0 text-destructive"
|
className="h-7 w-7 p-0 text-destructive"
|
||||||
|
data-testid={`delete-session-${session.id}`}
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3 h-3" />
|
<Trash2 className="w-3 h-3" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -552,6 +566,14 @@ export function SessionManager({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* Delete Session Confirmation Dialog */}
|
||||||
|
<DeleteSessionDialog
|
||||||
|
open={isDeleteDialogOpen}
|
||||||
|
onOpenChange={setIsDeleteDialogOpen}
|
||||||
|
session={sessionToDelete}
|
||||||
|
onConfirm={confirmDeleteSession}
|
||||||
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
88
apps/app/src/components/ui/delete-confirm-dialog.tsx
Normal file
88
apps/app/src/components/ui/delete-confirm-dialog.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { Trash2 } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { HotkeyButton } from "@/components/ui/hotkey-button";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
|
interface DeleteConfirmDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
onConfirm: () => void;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
/** Optional content to show between description and buttons (e.g., item preview card) */
|
||||||
|
children?: ReactNode;
|
||||||
|
/** Text for the confirm button. Defaults to "Delete" */
|
||||||
|
confirmText?: string;
|
||||||
|
/** Test ID for the dialog */
|
||||||
|
testId?: string;
|
||||||
|
/** Test ID for the confirm button */
|
||||||
|
confirmTestId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DeleteConfirmDialog({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onConfirm,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
children,
|
||||||
|
confirmText = "Delete",
|
||||||
|
testId = "delete-confirm-dialog",
|
||||||
|
confirmTestId = "confirm-delete-button",
|
||||||
|
}: DeleteConfirmDialogProps) {
|
||||||
|
const handleConfirm = () => {
|
||||||
|
onConfirm();
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent
|
||||||
|
className="bg-popover border-border max-w-md"
|
||||||
|
data-testid={testId}
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Trash2 className="w-5 h-5 text-destructive" />
|
||||||
|
{title}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription className="text-muted-foreground">
|
||||||
|
{description}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
|
||||||
|
<DialogFooter className="gap-2 sm:gap-2 pt-4">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
className="px-4"
|
||||||
|
data-testid="cancel-delete-button"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<HotkeyButton
|
||||||
|
variant="destructive"
|
||||||
|
onClick={handleConfirm}
|
||||||
|
data-testid={confirmTestId}
|
||||||
|
hotkey={{ key: "Enter", cmdCtrl: true }}
|
||||||
|
hotkeyActive={open}
|
||||||
|
className="px-4"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
{confirmText}
|
||||||
|
</HotkeyButton>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
|
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -211,14 +212,9 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmDelete = () => {
|
const handleConfirmDelete = () => {
|
||||||
setIsDeleteDialogOpen(false);
|
|
||||||
onDelete();
|
onDelete();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCancelDelete = () => {
|
|
||||||
setIsDeleteDialogOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Dragging logic:
|
// Dragging logic:
|
||||||
// - Backlog items can always be dragged
|
// - Backlog items can always be dragged
|
||||||
// - skipTests items can be dragged even when in_progress or verified (unless currently running)
|
// - skipTests items can be dragged even when in_progress or verified (unless currently running)
|
||||||
@@ -861,35 +857,15 @@ export const KanbanCard = memo(function KanbanCard({
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
{/* Delete Confirmation Dialog */}
|
{/* Delete Confirmation Dialog */}
|
||||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
<DeleteConfirmDialog
|
||||||
<DialogContent data-testid="delete-confirmation-dialog">
|
open={isDeleteDialogOpen}
|
||||||
<DialogHeader>
|
onOpenChange={setIsDeleteDialogOpen}
|
||||||
<DialogTitle>Delete Feature</DialogTitle>
|
onConfirm={handleConfirmDelete}
|
||||||
<DialogDescription>
|
title="Delete Feature"
|
||||||
Are you sure you want to delete this feature? This action cannot
|
description="Are you sure you want to delete this feature? This action cannot be undone."
|
||||||
be undone.
|
testId="delete-confirmation-dialog"
|
||||||
</DialogDescription>
|
confirmTestId="confirm-delete-button"
|
||||||
</DialogHeader>
|
/>
|
||||||
<DialogFooter className="mt-6">
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
onClick={handleCancelDelete}
|
|
||||||
data-testid="cancel-delete-button"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<HotkeyButton
|
|
||||||
variant="destructive"
|
|
||||||
onClick={handleConfirmDelete}
|
|
||||||
data-testid="confirm-delete-button"
|
|
||||||
hotkey={{ key: "Enter", cmdCtrl: true }}
|
|
||||||
hotkeyActive={isDeleteDialogOpen}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</HotkeyButton>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Summary Modal */}
|
{/* Summary Modal */}
|
||||||
<Dialog open={isSummaryDialogOpen} onOpenChange={setIsSummaryDialogOpen}>
|
<Dialog open={isSummaryDialogOpen} onOpenChange={setIsSummaryDialogOpen}>
|
||||||
|
|||||||
@@ -1,13 +1,5 @@
|
|||||||
import { Trash2, Folder } from "lucide-react";
|
import { Folder, Trash2 } from "lucide-react";
|
||||||
import {
|
import { DeleteConfirmDialog } from "@/components/ui/delete-confirm-dialog";
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import type { Project } from "@/lib/electron";
|
import type { Project } from "@/lib/electron";
|
||||||
|
|
||||||
interface DeleteProjectDialogProps {
|
interface DeleteProjectDialogProps {
|
||||||
@@ -26,24 +18,22 @@ export function DeleteProjectDialog({
|
|||||||
const handleConfirm = () => {
|
const handleConfirm = () => {
|
||||||
if (project) {
|
if (project) {
|
||||||
onConfirm(project.id);
|
onConfirm(project.id);
|
||||||
onOpenChange(false);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<DeleteConfirmDialog
|
||||||
<DialogContent className="bg-popover border-border max-w-md">
|
open={open}
|
||||||
<DialogHeader>
|
onOpenChange={onOpenChange}
|
||||||
<DialogTitle className="flex items-center gap-2">
|
onConfirm={handleConfirm}
|
||||||
<Trash2 className="w-5 h-5 text-destructive" />
|
title="Delete Project"
|
||||||
Delete Project
|
description="Are you sure you want to move this project to Trash?"
|
||||||
</DialogTitle>
|
confirmText="Move to Trash"
|
||||||
<DialogDescription className="text-muted-foreground">
|
testId="delete-project-dialog"
|
||||||
Are you sure you want to move this project to Trash?
|
confirmTestId="confirm-delete-project"
|
||||||
</DialogDescription>
|
>
|
||||||
</DialogHeader>
|
{project && (
|
||||||
|
<>
|
||||||
{project && (
|
|
||||||
<div className="flex items-center gap-3 p-4 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
|
<div className="flex items-center gap-3 p-4 rounded-lg bg-sidebar-accent/10 border border-sidebar-border">
|
||||||
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
|
<div className="w-10 h-10 rounded-lg bg-sidebar-accent/20 border border-sidebar-border flex items-center justify-center shrink-0">
|
||||||
<Folder className="w-5 h-5 text-brand-500" />
|
<Folder className="w-5 h-5 text-brand-500" />
|
||||||
@@ -57,27 +47,13 @@ export function DeleteProjectDialog({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
The folder will remain on disk until you permanently delete it from
|
The folder will remain on disk until you permanently delete it from
|
||||||
Trash.
|
Trash.
|
||||||
</p>
|
</p>
|
||||||
|
</>
|
||||||
<DialogFooter className="gap-2 sm:gap-0">
|
)}
|
||||||
<Button variant="ghost" onClick={() => onOpenChange(false)}>
|
</DeleteConfirmDialog>
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
onClick={handleConfirm}
|
|
||||||
data-testid="confirm-delete-project"
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
|
||||||
Move to Trash
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
|
|||||||
// Note: Some shortcuts share the same key (e.g., "N" for addFeature, newSession, addProfile)
|
// Note: Some shortcuts share the same key (e.g., "N" for addFeature, newSession, addProfile)
|
||||||
// This is intentional as they are context-specific and only active in their respective views
|
// This is intentional as they are context-specific and only active in their respective views
|
||||||
addFeature: "N", // Only active in board view
|
addFeature: "N", // Only active in board view
|
||||||
addContextFile: "F", // Only active in context view
|
addContextFile: "N", // Only active in context view
|
||||||
startNext: "G", // Only active in board view
|
startNext: "G", // Only active in board view
|
||||||
newSession: "N", // Only active in agent view
|
newSession: "N", // Only active in agent view
|
||||||
openProject: "O", // Global shortcut
|
openProject: "O", // Global shortcut
|
||||||
@@ -1215,6 +1215,20 @@ export const useAppStore = create<AppState & AppActions>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "automaker-storage",
|
name: "automaker-storage",
|
||||||
|
version: 1, // Increment when making breaking changes to persisted state
|
||||||
|
migrate: (persistedState: unknown, version: number) => {
|
||||||
|
const state = persistedState as Partial<AppState>;
|
||||||
|
|
||||||
|
// Migration from version 0 (no version) to version 1:
|
||||||
|
// - Change addContextFile shortcut from "F" to "N"
|
||||||
|
if (version === 0) {
|
||||||
|
if (state.keyboardShortcuts?.addContextFile === "F") {
|
||||||
|
state.keyboardShortcuts.addContextFile = "N";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state as AppState;
|
||||||
|
},
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
// Project management
|
// Project management
|
||||||
projects: state.projects,
|
projects: state.projects,
|
||||||
|
|||||||
3
apps/server/.gitignore
vendored
3
apps/server/.gitignore
vendored
@@ -1 +1,2 @@
|
|||||||
.env
|
.env
|
||||||
|
data
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "msg_1765523527444_d8xyk8rh9",
|
|
||||||
"role": "user",
|
|
||||||
"content": "this is a test",
|
|
||||||
"timestamp": "2025-12-12T07:12:07.444Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "msg_1765523534075_351i0rcft",
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "Hello! This is a test response. I'm working correctly and ready to help you with your software development projects.\n\nIs there something specific you'd like to work on today? I can help you with:\n\n- **Planning a new project** - Define requirements, architecture, and technical approach\n- **Writing code** - Create new files, implement features, or build components\n- **Editing existing code** - Modify, refactor, or fix bugs in your codebase\n- **Running commands** - Execute tests, builds, or other terminal commands\n- **Exploring your codebase** - Search for patterns, understand existing code structure\n\nJust let me know what you need!",
|
|
||||||
"timestamp": "2025-12-12T07:12:14.075Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "msg_1765524006202_3d2cuya6b",
|
|
||||||
"role": "user",
|
|
||||||
"content": "test",
|
|
||||||
"timestamp": "2025-12-12T07:20:06.202Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "msg_1765524011389_sdfbuai3k",
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "Hello! I'm here to help you build software. How can I assist you today?\n\nI can help you with:\n\n- **Planning** - Define requirements, discuss architecture, and design your project\n- **Coding** - Write, edit, and modify code files\n- **Debugging** - Analyze issues and fix bugs\n- **Testing** - Run tests and verify functionality\n- **Exploring** - Search and understand existing codebases\n\nWhat would you like to work on?",
|
|
||||||
"timestamp": "2025-12-12T07:20:11.389Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "msg_1765524491273_7ocghnbb5",
|
|
||||||
"role": "user",
|
|
||||||
"content": "test",
|
|
||||||
"timestamp": "2025-12-12T07:28:11.273Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "msg_1765524498237_x8b4148gc",
|
|
||||||
"role": "assistant",
|
|
||||||
"content": "Hello! I'm here to help you build software. How can I assist you today?\n\nI can help you with:\n- **Planning** - Define requirements, architecture, and technical approaches\n- **Coding** - Write, edit, and modify code files\n- **Debugging** - Find and fix issues in your codebase\n- **Testing** - Run tests and help improve test coverage\n- **Exploring** - Search and analyze your existing codebase\n\nWhat would you like to work on?",
|
|
||||||
"timestamp": "2025-12-12T07:28:18.237Z"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"msg_1765523524581_xhk6u45v2": {
|
|
||||||
"id": "msg_1765523524581_xhk6u45v2",
|
|
||||||
"name": "Bright Agent 2",
|
|
||||||
"projectPath": "/Users/webdevcody/Workspace/automaker",
|
|
||||||
"workingDirectory": "/Users/webdevcody/Workspace/automaker",
|
|
||||||
"createdAt": "2025-12-12T07:12:04.582Z",
|
|
||||||
"updatedAt": "2025-12-12T07:28:18.571Z"
|
|
||||||
},
|
|
||||||
"msg_1765525491205_xeuqv7i9v": {
|
|
||||||
"id": "msg_1765525491205_xeuqv7i9v",
|
|
||||||
"name": "Optimal Helper 52",
|
|
||||||
"projectPath": "/Users/webdevcody/Workspace/automaker",
|
|
||||||
"workingDirectory": "/Users/webdevcody/Workspace/automaker",
|
|
||||||
"createdAt": "2025-12-12T07:44:51.205Z",
|
|
||||||
"updatedAt": "2025-12-12T07:46:03.339Z"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -313,6 +313,46 @@ export class AutoModeService {
|
|||||||
// No worktree, use project path
|
// No worktree, use project path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load feature info for context
|
||||||
|
const feature = await this.loadFeature(projectPath, featureId);
|
||||||
|
|
||||||
|
// Load previous agent output if it exists
|
||||||
|
const contextPath = path.join(
|
||||||
|
projectPath,
|
||||||
|
".automaker",
|
||||||
|
"features",
|
||||||
|
featureId,
|
||||||
|
"agent-output.md"
|
||||||
|
);
|
||||||
|
let previousContext = "";
|
||||||
|
try {
|
||||||
|
previousContext = await fs.readFile(contextPath, "utf-8");
|
||||||
|
} catch {
|
||||||
|
// No previous context
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build complete prompt with feature info, previous context, and follow-up instructions
|
||||||
|
let fullPrompt = `## Follow-up on Feature Implementation
|
||||||
|
|
||||||
|
${feature ? this.buildFeaturePrompt(feature) : `**Feature ID:** ${featureId}`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (previousContext) {
|
||||||
|
fullPrompt += `
|
||||||
|
## Previous Agent Work
|
||||||
|
The following is the output from the previous implementation attempt:
|
||||||
|
|
||||||
|
${previousContext}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
fullPrompt += `
|
||||||
|
## Follow-up Instructions
|
||||||
|
${prompt}
|
||||||
|
|
||||||
|
## Task
|
||||||
|
Address the follow-up instructions above. Review the previous work and make the requested changes or fixes.`;
|
||||||
|
|
||||||
this.runningFeatures.set(featureId, {
|
this.runningFeatures.set(featureId, {
|
||||||
featureId,
|
featureId,
|
||||||
projectPath,
|
projectPath,
|
||||||
@@ -326,11 +366,14 @@ export class AutoModeService {
|
|||||||
this.emitAutoModeEvent("auto_mode_feature_start", {
|
this.emitAutoModeEvent("auto_mode_feature_start", {
|
||||||
featureId,
|
featureId,
|
||||||
projectPath,
|
projectPath,
|
||||||
feature: { id: featureId, title: "Follow-up", description: prompt.substring(0, 100) },
|
feature: feature || { id: featureId, title: "Follow-up", description: prompt.substring(0, 100) },
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.runAgent(workDir, featureId, prompt, abortController, imagePaths);
|
await this.runAgent(workDir, featureId, fullPrompt, abortController, imagePaths);
|
||||||
|
|
||||||
|
// Mark as waiting_approval for user review
|
||||||
|
await this.updateFeatureStatus(projectPath, featureId, "waiting_approval");
|
||||||
|
|
||||||
this.emitAutoModeEvent("auto_mode_feature_complete", {
|
this.emitAutoModeEvent("auto_mode_feature_complete", {
|
||||||
featureId,
|
featureId,
|
||||||
|
|||||||
109
init.sh
109
init.sh
@@ -1,31 +1,108 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Automaker - Development Environment Setup Script
|
# Automaker - Development Environment Setup and Launch Script
|
||||||
|
|
||||||
echo "=== Automaker Development Environment Setup ==="
|
set -e # Exit on error
|
||||||
|
|
||||||
# Navigate to app directory
|
echo "╔═══════════════════════════════════════════════════════╗"
|
||||||
APP_DIR="$(dirname "$0")/app"
|
echo "║ Automaker Development Environment ║"
|
||||||
|
echo "╚═══════════════════════════════════════════════════════╝"
|
||||||
|
echo ""
|
||||||
|
|
||||||
if [ ! -d "$APP_DIR" ]; then
|
# Colors for output
|
||||||
echo "Error: app directory not found"
|
GREEN='\033[0;32m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
RED='\033[0;31m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Check if node is installed
|
||||||
|
if ! command -v node &> /dev/null; then
|
||||||
|
echo -e "${RED}Error: Node.js is not installed${NC}"
|
||||||
|
echo "Please install Node.js from https://nodejs.org/"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install dependencies if node_modules doesn't exist
|
# Install dependencies if needed
|
||||||
if [ ! -d "$APP_DIR/node_modules" ]; then
|
if [ ! -d "node_modules" ]; then
|
||||||
echo "Installing dependencies..."
|
echo -e "${BLUE}Installing dependencies...${NC}"
|
||||||
npm install --prefix "$APP_DIR"
|
npm install
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Install Playwright browsers if needed
|
# Install Playwright browsers if needed
|
||||||
echo "Checking Playwright browsers..."
|
echo -e "${YELLOW}Checking Playwright browsers...${NC}"
|
||||||
npx playwright install chromium 2>/dev/null || true
|
npx playwright install chromium 2>/dev/null || true
|
||||||
|
|
||||||
# Kill any process on port 3000
|
# Kill any existing processes on required ports
|
||||||
echo "Checking port 3000..."
|
echo -e "${YELLOW}Checking for processes on ports 3007 and 3008...${NC}"
|
||||||
lsof -ti:3007 | xargs kill -9 2>/dev/null || true
|
lsof -ti:3007 | xargs kill -9 2>/dev/null || true
|
||||||
|
lsof -ti:3008 | xargs kill -9 2>/dev/null || true
|
||||||
|
|
||||||
# Start the dev server
|
# Start the backend server
|
||||||
echo "Starting Next.js development server..."
|
echo -e "${BLUE}Starting backend server on port 3008...${NC}"
|
||||||
npm run dev --prefix "$APP_DIR"
|
npm run dev:server > logs/server.log 2>&1 &
|
||||||
|
SERVER_PID=$!
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Waiting for server to be ready...${NC}"
|
||||||
|
|
||||||
|
# Wait for server health check
|
||||||
|
MAX_RETRIES=30
|
||||||
|
RETRY_COUNT=0
|
||||||
|
SERVER_READY=false
|
||||||
|
|
||||||
|
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
|
||||||
|
if curl -s http://localhost:3008/api/health > /dev/null 2>&1; then
|
||||||
|
SERVER_READY=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 1
|
||||||
|
RETRY_COUNT=$((RETRY_COUNT + 1))
|
||||||
|
echo -n "."
|
||||||
|
done
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ "$SERVER_READY" = false ]; then
|
||||||
|
echo -e "${RED}Error: Server failed to start${NC}"
|
||||||
|
echo "Check logs/server.log for details"
|
||||||
|
kill $SERVER_PID 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}✓ Server is ready!${NC}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Prompt user for application mode
|
||||||
|
echo "═══════════════════════════════════════════════════════"
|
||||||
|
echo " Select Application Mode:"
|
||||||
|
echo "═══════════════════════════════════════════════════════"
|
||||||
|
echo " 1) Web Application (Browser)"
|
||||||
|
echo " 2) Desktop Application (Electron)"
|
||||||
|
echo "═══════════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
read -p "Enter your choice (1 or 2): " choice
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Launching Web Application...${NC}"
|
||||||
|
echo "The application will be available at: ${GREEN}http://localhost:3007${NC}"
|
||||||
|
echo ""
|
||||||
|
npm run dev:web
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo ""
|
||||||
|
echo -e "${BLUE}Launching Desktop Application...${NC}"
|
||||||
|
npm run dev:electron
|
||||||
|
break
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}Invalid choice. Please enter 1 or 2.${NC}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Cleanup on exit
|
||||||
|
trap "echo 'Cleaning up...'; kill $SERVER_PID 2>/dev/null || true; exit" INT TERM EXIT
|
||||||
|
|||||||
182
package-lock.json
generated
182
package-lock.json
generated
@@ -17,11 +17,9 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "Unlicense",
|
"license": "Unlicense",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/claude-agent-sdk": "^0.1.61",
|
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
"@dnd-kit/utilities": "^3.2.2",
|
"@dnd-kit/utilities": "^3.2.2",
|
||||||
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
||||||
"@radix-ui/react-checkbox": "^1.3.3",
|
"@radix-ui/react-checkbox": "^1.3.3",
|
||||||
"@radix-ui/react-dialog": "^1.1.15",
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
"@radix-ui/react-dropdown-menu": "^2.1.16",
|
||||||
@@ -1216,22 +1214,6 @@
|
|||||||
"@hapi/hoek": "^11.0.2"
|
"@hapi/hoek": "^11.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/@homebridge/node-pty-prebuilt-multiarch": {
|
|
||||||
"version": "0.13.1",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"node-addon-api": "^7.1.0",
|
|
||||||
"prebuild-install": "^7.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18.0.0 <25.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/@homebridge/node-pty-prebuilt-multiarch/node_modules/node-addon-api": {
|
|
||||||
"version": "7.1.1",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/@humanfs/core": {
|
"apps/app/node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -3554,6 +3536,7 @@
|
|||||||
},
|
},
|
||||||
"apps/app/node_modules/base64-js": {
|
"apps/app/node_modules/base64-js": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -3580,6 +3563,7 @@
|
|||||||
},
|
},
|
||||||
"apps/app/node_modules/bl": {
|
"apps/app/node_modules/bl": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
@@ -3638,6 +3622,7 @@
|
|||||||
},
|
},
|
||||||
"apps/app/node_modules/buffer": {
|
"apps/app/node_modules/buffer": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -4351,6 +4336,7 @@
|
|||||||
},
|
},
|
||||||
"apps/app/node_modules/decompress-response": {
|
"apps/app/node_modules/decompress-response": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mimic-response": "^3.1.0"
|
"mimic-response": "^3.1.0"
|
||||||
@@ -4364,6 +4350,7 @@
|
|||||||
},
|
},
|
||||||
"apps/app/node_modules/decompress-response/node_modules/mimic-response": {
|
"apps/app/node_modules/decompress-response/node_modules/mimic-response": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
@@ -4785,6 +4772,7 @@
|
|||||||
},
|
},
|
||||||
"apps/app/node_modules/end-of-stream": {
|
"apps/app/node_modules/end-of-stream": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
@@ -5364,13 +5352,6 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/expand-template": {
|
|
||||||
"version": "2.0.3",
|
|
||||||
"license": "(MIT OR WTFPL)",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/exponential-backoff": {
|
"apps/app/node_modules/exponential-backoff": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5615,10 +5596,6 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/fs-constants": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/fs-extra": {
|
"apps/app/node_modules/fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -5748,10 +5725,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/github-from-package": {
|
|
||||||
"version": "0.0.0",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/glob": {
|
"apps/app/node_modules/glob": {
|
||||||
"version": "7.2.3",
|
"version": "7.2.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -6083,6 +6056,7 @@
|
|||||||
},
|
},
|
||||||
"apps/app/node_modules/ieee754": {
|
"apps/app/node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -7730,18 +7704,10 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/mkdirp-classic": {
|
|
||||||
"version": "0.5.3",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/ms": {
|
"apps/app/node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/napi-build-utils": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/napi-postinstall": {
|
"apps/app/node_modules/napi-postinstall": {
|
||||||
"version": "0.3.4",
|
"version": "0.3.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -8358,50 +8324,6 @@
|
|||||||
"node": "^12.20.0 || >=14"
|
"node": "^12.20.0 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/prebuild-install": {
|
|
||||||
"version": "7.1.3",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"detect-libc": "^2.0.0",
|
|
||||||
"expand-template": "^2.0.3",
|
|
||||||
"github-from-package": "0.0.0",
|
|
||||||
"minimist": "^1.2.3",
|
|
||||||
"mkdirp-classic": "^0.5.3",
|
|
||||||
"napi-build-utils": "^2.0.0",
|
|
||||||
"node-abi": "^3.3.0",
|
|
||||||
"pump": "^3.0.0",
|
|
||||||
"rc": "^1.2.7",
|
|
||||||
"simple-get": "^4.0.0",
|
|
||||||
"tar-fs": "^2.0.0",
|
|
||||||
"tunnel-agent": "^0.6.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"prebuild-install": "bin.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/prebuild-install/node_modules/node-abi": {
|
|
||||||
"version": "3.85.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"semver": "^7.3.5"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/prebuild-install/node_modules/semver": {
|
|
||||||
"version": "7.7.3",
|
|
||||||
"license": "ISC",
|
|
||||||
"bin": {
|
|
||||||
"semver": "bin/semver.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/prelude-ls": {
|
"apps/app/node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -8468,6 +8390,7 @@
|
|||||||
},
|
},
|
||||||
"apps/app/node_modules/pump": {
|
"apps/app/node_modules/pump": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"end-of-stream": "^1.1.0",
|
"end-of-stream": "^1.1.0",
|
||||||
@@ -8610,6 +8533,7 @@
|
|||||||
},
|
},
|
||||||
"apps/app/node_modules/readable-stream": {
|
"apps/app/node_modules/readable-stream": {
|
||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
@@ -8991,47 +8915,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/simple-concat": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/simple-get": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"decompress-response": "^6.0.0",
|
|
||||||
"once": "^1.3.1",
|
|
||||||
"simple-concat": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/simple-update-notifier": {
|
"apps/app/node_modules/simple-update-notifier": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -9188,6 +9071,7 @@
|
|||||||
},
|
},
|
||||||
"apps/app/node_modules/string_decoder": {
|
"apps/app/node_modules/string_decoder": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
@@ -9462,34 +9346,6 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/tar-fs": {
|
|
||||||
"version": "2.1.4",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chownr": "^1.1.1",
|
|
||||||
"mkdirp-classic": "^0.5.2",
|
|
||||||
"pump": "^3.0.0",
|
|
||||||
"tar-stream": "^2.1.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/tar-fs/node_modules/chownr": {
|
|
||||||
"version": "1.1.4",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/tar-stream": {
|
|
||||||
"version": "2.2.0",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"bl": "^4.0.3",
|
|
||||||
"end-of-stream": "^1.4.1",
|
|
||||||
"fs-constants": "^1.0.0",
|
|
||||||
"inherits": "^2.0.3",
|
|
||||||
"readable-stream": "^3.1.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/tar/node_modules/minipass": {
|
"apps/app/node_modules/tar/node_modules/minipass": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -9731,16 +9587,6 @@
|
|||||||
"json5": "lib/cli.js"
|
"json5": "lib/cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/tunnel-agent": {
|
|
||||||
"version": "0.6.0",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "^5.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/app/node_modules/tw-animate-css": {
|
"apps/app/node_modules/tw-animate-css": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
@@ -10094,6 +9940,7 @@
|
|||||||
},
|
},
|
||||||
"apps/app/node_modules/util-deprecate": {
|
"apps/app/node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"apps/app/node_modules/verror": {
|
"apps/app/node_modules/verror": {
|
||||||
@@ -12668,6 +12515,7 @@
|
|||||||
"version": "0.6.0",
|
"version": "0.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.0.0"
|
"node": ">=4.0.0"
|
||||||
@@ -12686,6 +12534,7 @@
|
|||||||
"version": "2.1.2",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||||
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -13228,6 +13077,7 @@
|
|||||||
"version": "1.3.8",
|
"version": "1.3.8",
|
||||||
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
|
||||||
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
"integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
|
||||||
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
@@ -13573,6 +13423,7 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
@@ -13881,6 +13732,7 @@
|
|||||||
"version": "1.2.8",
|
"version": "1.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||||
|
"dev": true,
|
||||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deep-extend": "^0.6.0",
|
"deep-extend": "^0.6.0",
|
||||||
@@ -14010,6 +13862,7 @@
|
|||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -14407,6 +14260,7 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
|
|||||||
5
plan.md
5
plan.md
@@ -224,10 +224,11 @@ NEXT_PUBLIC_SERVER_URL=http://localhost:3008
|
|||||||
- `apps/server/src/routes/running-agents.ts` - active agent tracking
|
- `apps/server/src/routes/running-agents.ts` - active agent tracking
|
||||||
|
|
||||||
- [x] **Phase 7**: Simplify Electron
|
- [x] **Phase 7**: Simplify Electron
|
||||||
- `apps/app/electron/main-simplified.js` - spawns server, minimal IPC
|
- `apps/app/electron/main.js` - spawns server, minimal IPC (10 handlers for native features only)
|
||||||
- `apps/app/electron/preload-simplified.js` - only native features exposed
|
- `apps/app/electron/preload.js` - only native features exposed
|
||||||
- Updated `electron.ts` to detect simplified mode
|
- Updated `electron.ts` to detect simplified mode
|
||||||
- Updated `http-api-client.ts` to use native dialogs when available
|
- Updated `http-api-client.ts` to use native dialogs when available
|
||||||
|
- Removed ~13,000 lines of dead code (obsolete services, agent-service.js, auto-mode-service.js)
|
||||||
|
|
||||||
- [x] **Phase 8**: Production ready
|
- [x] **Phase 8**: Production ready
|
||||||
- `apps/server/src/lib/auth.ts` - API key authentication middleware
|
- `apps/server/src/lib/auth.ts` - API key authentication middleware
|
||||||
|
|||||||
Reference in New Issue
Block a user