massive overhaul of auth system logic, and subtle fixes to UI

This commit is contained in:
trueheads
2025-12-12 15:59:21 -06:00
parent 0bb774375e
commit 62082fbaf5
11 changed files with 373 additions and 112 deletions

View File

@@ -39,30 +39,30 @@ const PORT = parseInt(process.env.PORT || "3008", 10);
const DATA_DIR = process.env.DATA_DIR || "./data";
// Check for required environment variables
// Claude Agent SDK supports EITHER OAuth token (subscription) OR API key (pay-per-use)
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
if (!hasAnthropicKey) {
if (!hasAnthropicKey && !hasOAuthToken) {
console.warn(`
╔═══════════════════════════════════════════════════════════════════════╗
║ ⚠️ WARNING: ANTHROPIC_API_KEY not set
║ ⚠️ WARNING: No Claude authentication configured
║ ║
║ The Claude Agent SDK requires ANTHROPIC_API_KEY to function. ║
${
hasOAuthToken
? " You have CLAUDE_CODE_OAUTH_TOKEN set - this is for CLI auth only."
: ""
}
║ The Claude Agent SDK requires authentication to function.
║ ║
Set your API key:
Option 1 - Subscription (OAuth Token):
║ export CLAUDE_CODE_OAUTH_TOKEN="your-oauth-token" ║
║ ║
║ Option 2 - Pay-per-use (API Key): ║
║ export ANTHROPIC_API_KEY="sk-ant-..." ║
║ ║
║ Or add to apps/server/.env:
║ ANTHROPIC_API_KEY=sk-ant-... ║
║ Or use the setup wizard in Settings to configure authentication.
╚═══════════════════════════════════════════════════════════════════════╝
`);
} else if (hasOAuthToken) {
console.log("[Server] ✓ CLAUDE_CODE_OAUTH_TOKEN detected (subscription auth)");
} else {
console.log("[Server] ✓ ANTHROPIC_API_KEY detected");
console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)");
}
// Initialize security

View File

@@ -107,12 +107,14 @@ export function createSetupRoutes(): Router {
}
// Check authentication - detect all possible auth methods
// Note: apiKeys.anthropic_oauth_token stores OAuth tokens from subscription auth
// apiKeys.anthropic stores direct API keys for pay-per-use
let auth = {
authenticated: false,
method: "none" as string,
hasCredentialsFile: false,
hasToken: false,
hasStoredOAuthToken: false,
hasStoredOAuthToken: !!apiKeys.anthropic_oauth_token,
hasStoredApiKey: !!apiKeys.anthropic,
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
hasEnvOAuthToken: !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
@@ -199,9 +201,17 @@ export function createSetupRoutes(): Router {
auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var
}
// In-memory stored API key (from settings UI)
// In-memory stored OAuth token (from setup wizard - subscription auth)
if (!auth.authenticated && apiKeys.anthropic_oauth_token) {
auth.authenticated = true;
auth.oauthTokenValid = true;
auth.method = "oauth_token"; // Stored OAuth token from setup wizard
}
// In-memory stored API key (from settings UI - pay-per-use)
if (!auth.authenticated && apiKeys.anthropic) {
auth.authenticated = true;
auth.apiKeyValid = true;
auth.method = "api_key"; // Manually stored API key
}
@@ -393,9 +403,19 @@ export function createSetupRoutes(): Router {
apiKeys[provider] = apiKey;
// Also set as environment variable and persist to .env
if (provider === "anthropic" || provider === "anthropic_oauth_token") {
// IMPORTANT: OAuth tokens and API keys must be stored separately
// - OAuth tokens (subscription auth) -> CLAUDE_CODE_OAUTH_TOKEN
// - API keys (pay-per-use) -> ANTHROPIC_API_KEY
if (provider === "anthropic_oauth_token") {
// OAuth token from claude setup-token (subscription-based auth)
process.env.CLAUDE_CODE_OAUTH_TOKEN = apiKey;
await persistApiKeyToEnv("CLAUDE_CODE_OAUTH_TOKEN", apiKey);
console.log("[Setup] Stored OAuth token as CLAUDE_CODE_OAUTH_TOKEN");
} else if (provider === "anthropic") {
// Direct API key (pay-per-use)
process.env.ANTHROPIC_API_KEY = apiKey;
await persistApiKeyToEnv("ANTHROPIC_API_KEY", apiKey);
console.log("[Setup] Stored API key as ANTHROPIC_API_KEY");
} else if (provider === "openai") {
process.env.OPENAI_API_KEY = apiKey;
await persistApiKeyToEnv("OPENAI_API_KEY", apiKey);

View File

@@ -11,11 +11,28 @@ import type { EventEmitter } from "../lib/events.js";
let isRunning = false;
let currentAbortController: AbortController | null = null;
// Helper to log authentication status
function logAuthStatus(context: string): void {
const hasOAuthToken = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
console.log(`[SpecRegeneration] ${context} - Auth Status:`);
console.log(`[SpecRegeneration] CLAUDE_CODE_OAUTH_TOKEN: ${hasOAuthToken ? 'SET (' + process.env.CLAUDE_CODE_OAUTH_TOKEN?.substring(0, 20) + '...)' : 'NOT SET'}`);
console.log(`[SpecRegeneration] ANTHROPIC_API_KEY: ${hasApiKey ? 'SET (' + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + '...)' : 'NOT SET'}`);
if (!hasOAuthToken && !hasApiKey) {
console.error(`[SpecRegeneration] ⚠️ WARNING: No authentication configured! SDK will fail.`);
}
}
export function createSpecRegenerationRoutes(events: EventEmitter): Router {
const router = Router();
// Create project spec from overview
router.post("/create", async (req: Request, res: Response) => {
console.log("[SpecRegeneration] ========== /create endpoint called ==========");
console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2));
try {
const { projectPath, projectOverview, generateFeatures } = req.body as {
projectPath: string;
@@ -23,7 +40,13 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
generateFeatures?: boolean;
};
console.log(`[SpecRegeneration] Parsed params:`);
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
console.log(`[SpecRegeneration] projectOverview length: ${projectOverview?.length || 0} chars`);
console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`);
if (!projectPath || !projectOverview) {
console.error("[SpecRegeneration] Missing required parameters");
res.status(400).json({
success: false,
error: "projectPath and projectOverview required",
@@ -32,12 +55,16 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
}
if (isRunning) {
console.warn("[SpecRegeneration] Generation already running, rejecting request");
res.json({ success: false, error: "Spec generation already running" });
return;
}
logAuthStatus("Before starting generation");
isRunning = true;
currentAbortController = new AbortController();
console.log("[SpecRegeneration] Starting background generation task...");
// Start generation in background
generateSpec(
@@ -48,19 +75,27 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
generateFeatures
)
.catch((error) => {
console.error("[SpecRegeneration] Error:", error);
console.error("[SpecRegeneration] ❌ Generation failed with error:");
console.error("[SpecRegeneration] Error name:", error?.name);
console.error("[SpecRegeneration] Error message:", error?.message);
console.error("[SpecRegeneration] Error stack:", error?.stack);
console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
events.emit("spec-regeneration:event", {
type: "spec_error",
error: error.message,
error: error.message || String(error),
});
})
.finally(() => {
console.log("[SpecRegeneration] Generation task finished (success or error)");
isRunning = false;
currentAbortController = null;
});
console.log("[SpecRegeneration] Returning success response (generation running in background)");
res.json({ success: true });
} catch (error) {
console.error("[SpecRegeneration] ❌ Route handler exception:");
console.error("[SpecRegeneration] Error:", error);
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
@@ -68,13 +103,21 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
// Generate from project definition
router.post("/generate", async (req: Request, res: Response) => {
console.log("[SpecRegeneration] ========== /generate endpoint called ==========");
console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2));
try {
const { projectPath, projectDefinition } = req.body as {
projectPath: string;
projectDefinition: string;
};
console.log(`[SpecRegeneration] Parsed params:`);
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
console.log(`[SpecRegeneration] projectDefinition length: ${projectDefinition?.length || 0} chars`);
if (!projectPath || !projectDefinition) {
console.error("[SpecRegeneration] Missing required parameters");
res.status(400).json({
success: false,
error: "projectPath and projectDefinition required",
@@ -83,12 +126,16 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
}
if (isRunning) {
console.warn("[SpecRegeneration] Generation already running, rejecting request");
res.json({ success: false, error: "Spec generation already running" });
return;
}
logAuthStatus("Before starting generation");
isRunning = true;
currentAbortController = new AbortController();
console.log("[SpecRegeneration] Starting background generation task...");
generateSpec(
projectPath,
@@ -98,19 +145,27 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
false
)
.catch((error) => {
console.error("[SpecRegeneration] Error:", error);
console.error("[SpecRegeneration] ❌ Generation failed with error:");
console.error("[SpecRegeneration] Error name:", error?.name);
console.error("[SpecRegeneration] Error message:", error?.message);
console.error("[SpecRegeneration] Error stack:", error?.stack);
console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
events.emit("spec-regeneration:event", {
type: "spec_error",
error: error.message,
error: error.message || String(error),
});
})
.finally(() => {
console.log("[SpecRegeneration] Generation task finished (success or error)");
isRunning = false;
currentAbortController = null;
});
console.log("[SpecRegeneration] Returning success response (generation running in background)");
res.json({ success: true });
} catch (error) {
console.error("[SpecRegeneration] ❌ Route handler exception:");
console.error("[SpecRegeneration] Error:", error);
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
@@ -118,37 +173,55 @@ export function createSpecRegenerationRoutes(events: EventEmitter): Router {
// Generate features from existing spec
router.post("/generate-features", async (req: Request, res: Response) => {
console.log("[SpecRegeneration] ========== /generate-features endpoint called ==========");
console.log("[SpecRegeneration] Request body:", JSON.stringify(req.body, null, 2));
try {
const { projectPath } = req.body as { projectPath: string };
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
if (!projectPath) {
console.error("[SpecRegeneration] Missing projectPath parameter");
res.status(400).json({ success: false, error: "projectPath required" });
return;
}
if (isRunning) {
console.warn("[SpecRegeneration] Generation already running, rejecting request");
res.json({ success: false, error: "Generation already running" });
return;
}
logAuthStatus("Before starting feature generation");
isRunning = true;
currentAbortController = new AbortController();
console.log("[SpecRegeneration] Starting background feature generation task...");
generateFeaturesFromSpec(projectPath, events, currentAbortController)
.catch((error) => {
console.error("[SpecRegeneration] Error:", error);
console.error("[SpecRegeneration] ❌ Feature generation failed with error:");
console.error("[SpecRegeneration] Error name:", error?.name);
console.error("[SpecRegeneration] Error message:", error?.message);
console.error("[SpecRegeneration] Error stack:", error?.stack);
console.error("[SpecRegeneration] Full error object:", JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
events.emit("spec-regeneration:event", {
type: "features_error",
error: error.message,
error: error.message || String(error),
});
})
.finally(() => {
console.log("[SpecRegeneration] Feature generation task finished (success or error)");
isRunning = false;
currentAbortController = null;
});
console.log("[SpecRegeneration] Returning success response (generation running in background)");
res.json({ success: true });
} catch (error) {
console.error("[SpecRegeneration] ❌ Route handler exception:");
console.error("[SpecRegeneration] Error:", error);
const message = error instanceof Error ? error.message : "Unknown error";
res.status(500).json({ success: false, error: message });
}
@@ -188,6 +261,11 @@ async function generateSpec(
abortController: AbortController,
generateFeatures?: boolean
) {
console.log("[SpecRegeneration] ========== generateSpec() started ==========");
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
console.log(`[SpecRegeneration] projectOverview length: ${projectOverview.length} chars`);
console.log(`[SpecRegeneration] generateFeatures: ${generateFeatures}`);
const prompt = `You are helping to define a software project specification.
Project Overview:
@@ -214,6 +292,8 @@ Also generate a list of features to implement. For each feature provide:
Format your response as markdown. Be specific and actionable.`;
console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`);
events.emit("spec-regeneration:event", {
type: "spec_progress",
content: "Starting spec generation...\n",
@@ -228,38 +308,78 @@ Format your response as markdown. Be specific and actionable.`;
abortController,
};
const stream = query({ prompt, options });
let responseText = "";
console.log("[SpecRegeneration] SDK Options:", JSON.stringify(options, null, 2));
console.log("[SpecRegeneration] Calling Claude Agent SDK query()...");
// Log auth status right before the SDK call
logAuthStatus("Right before SDK query()");
for await (const msg of stream) {
if (msg.type === "assistant" && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText = block.text;
events.emit("spec-regeneration:event", {
type: "spec_progress",
content: block.text,
});
} else if (block.type === "tool_use") {
events.emit("spec-regeneration:event", {
type: "spec_tool",
tool: block.name,
input: block.input,
});
}
}
} else if (msg.type === "result" && msg.subtype === "success") {
responseText = msg.result || responseText;
}
let stream;
try {
stream = query({ prompt, options });
console.log("[SpecRegeneration] query() returned stream successfully");
} catch (queryError) {
console.error("[SpecRegeneration] ❌ query() threw an exception:");
console.error("[SpecRegeneration] Error:", queryError);
throw queryError;
}
let responseText = "";
let messageCount = 0;
console.log("[SpecRegeneration] Starting to iterate over stream...");
try {
for await (const msg of stream) {
messageCount++;
console.log(`[SpecRegeneration] Stream message #${messageCount}:`, JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2));
if (msg.type === "assistant" && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText = block.text;
console.log(`[SpecRegeneration] Text block received (${block.text.length} chars)`);
events.emit("spec-regeneration:event", {
type: "spec_progress",
content: block.text,
});
} else if (block.type === "tool_use") {
console.log(`[SpecRegeneration] Tool use: ${block.name}`);
events.emit("spec-regeneration:event", {
type: "spec_tool",
tool: block.name,
input: block.input,
});
}
}
} else if (msg.type === "result" && (msg as any).subtype === "success") {
console.log("[SpecRegeneration] Received success result");
responseText = (msg as any).result || responseText;
} else if (msg.type === "error") {
console.error("[SpecRegeneration] ❌ Received error message from stream:");
console.error("[SpecRegeneration] Error message:", JSON.stringify(msg, null, 2));
}
}
} catch (streamError) {
console.error("[SpecRegeneration] ❌ Error while iterating stream:");
console.error("[SpecRegeneration] Stream error:", streamError);
throw streamError;
}
console.log(`[SpecRegeneration] Stream iteration complete. Total messages: ${messageCount}`);
console.log(`[SpecRegeneration] Response text length: ${responseText.length} chars`);
// Save spec
const specDir = path.join(projectPath, ".automaker");
const specPath = path.join(specDir, "project-spec.md");
const specPath = path.join(specDir, "app_spec.txt");
console.log(`[SpecRegeneration] Saving spec to: ${specPath}`);
await fs.mkdir(specDir, { recursive: true });
await fs.writeFile(specPath, responseText);
console.log("[SpecRegeneration] Spec saved successfully");
events.emit("spec-regeneration:event", {
type: "spec_complete",
specPath,
@@ -268,8 +388,11 @@ Format your response as markdown. Be specific and actionable.`;
// If generate features was requested, parse and create them
if (generateFeatures) {
console.log("[SpecRegeneration] Starting feature generation...");
await parseAndCreateFeatures(projectPath, responseText, events);
}
console.log("[SpecRegeneration] ========== generateSpec() completed ==========");
}
async function generateFeaturesFromSpec(
@@ -277,13 +400,20 @@ async function generateFeaturesFromSpec(
events: EventEmitter,
abortController: AbortController
) {
console.log("[SpecRegeneration] ========== generateFeaturesFromSpec() started ==========");
console.log(`[SpecRegeneration] projectPath: ${projectPath}`);
// Read existing spec
const specPath = path.join(projectPath, ".automaker", "project-spec.md");
const specPath = path.join(projectPath, ".automaker", "app_spec.txt");
let spec: string;
console.log(`[SpecRegeneration] Reading spec from: ${specPath}`);
try {
spec = await fs.readFile(specPath, "utf-8");
} catch {
console.log(`[SpecRegeneration] Spec loaded successfully (${spec.length} chars)`);
} catch (readError) {
console.error("[SpecRegeneration] ❌ Failed to read spec file:", readError);
events.emit("spec-regeneration:event", {
type: "features_error",
error: "No project spec found. Generate spec first.",
@@ -320,6 +450,8 @@ Format as JSON:
Generate 5-15 features that build on each other logically.`;
console.log(`[SpecRegeneration] Prompt length: ${prompt.length} chars`);
events.emit("spec-regeneration:event", {
type: "features_progress",
content: "Analyzing spec and generating features...\n",
@@ -334,26 +466,62 @@ Generate 5-15 features that build on each other logically.`;
abortController,
};
const stream = query({ prompt, options });
let responseText = "";
console.log("[SpecRegeneration] SDK Options:", JSON.stringify(options, null, 2));
console.log("[SpecRegeneration] Calling Claude Agent SDK query() for features...");
logAuthStatus("Right before SDK query() for features");
for await (const msg of stream) {
if (msg.type === "assistant" && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText = block.text;
events.emit("spec-regeneration:event", {
type: "features_progress",
content: block.text,
});
}
}
} else if (msg.type === "result" && msg.subtype === "success") {
responseText = msg.result || responseText;
}
let stream;
try {
stream = query({ prompt, options });
console.log("[SpecRegeneration] query() returned stream successfully");
} catch (queryError) {
console.error("[SpecRegeneration] ❌ query() threw an exception:");
console.error("[SpecRegeneration] Error:", queryError);
throw queryError;
}
let responseText = "";
let messageCount = 0;
console.log("[SpecRegeneration] Starting to iterate over feature stream...");
try {
for await (const msg of stream) {
messageCount++;
console.log(`[SpecRegeneration] Feature stream message #${messageCount}:`, JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2));
if (msg.type === "assistant" && msg.message.content) {
for (const block of msg.message.content) {
if (block.type === "text") {
responseText = block.text;
console.log(`[SpecRegeneration] Feature text block received (${block.text.length} chars)`);
events.emit("spec-regeneration:event", {
type: "features_progress",
content: block.text,
});
}
}
} else if (msg.type === "result" && (msg as any).subtype === "success") {
console.log("[SpecRegeneration] Received success result for features");
responseText = (msg as any).result || responseText;
} else if (msg.type === "error") {
console.error("[SpecRegeneration] ❌ Received error message from feature stream:");
console.error("[SpecRegeneration] Error message:", JSON.stringify(msg, null, 2));
}
}
} catch (streamError) {
console.error("[SpecRegeneration] ❌ Error while iterating feature stream:");
console.error("[SpecRegeneration] Stream error:", streamError);
throw streamError;
}
console.log(`[SpecRegeneration] Feature stream complete. Total messages: ${messageCount}`);
console.log(`[SpecRegeneration] Feature response length: ${responseText.length} chars`);
await parseAndCreateFeatures(projectPath, responseText, events);
console.log("[SpecRegeneration] ========== generateFeaturesFromSpec() completed ==========");
}
async function parseAndCreateFeatures(
@@ -361,20 +529,31 @@ async function parseAndCreateFeatures(
content: string,
events: EventEmitter
) {
console.log("[SpecRegeneration] ========== parseAndCreateFeatures() started ==========");
console.log(`[SpecRegeneration] Content length: ${content.length} chars`);
try {
// Extract JSON from response
console.log("[SpecRegeneration] Extracting JSON from response...");
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
if (!jsonMatch) {
console.error("[SpecRegeneration] ❌ No valid JSON found in response");
console.error("[SpecRegeneration] Content preview:", content.substring(0, 500));
throw new Error("No valid JSON found in response");
}
console.log(`[SpecRegeneration] JSON match found (${jsonMatch[0].length} chars)`);
const parsed = JSON.parse(jsonMatch[0]);
console.log(`[SpecRegeneration] Parsed ${parsed.features?.length || 0} features`);
const featuresDir = path.join(projectPath, ".automaker", "features");
await fs.mkdir(featuresDir, { recursive: true });
const createdFeatures: Array<{ id: string; title: string }> = [];
for (const feature of parsed.features) {
console.log(`[SpecRegeneration] Creating feature: ${feature.id}`);
const featureDir = path.join(featuresDir, feature.id);
await fs.mkdir(featureDir, { recursive: true });
@@ -398,15 +577,21 @@ async function parseAndCreateFeatures(
createdFeatures.push({ id: feature.id, title: feature.title });
}
console.log(`[SpecRegeneration] ✓ Created ${createdFeatures.length} features successfully`);
events.emit("spec-regeneration:event", {
type: "features_complete",
features: createdFeatures,
count: createdFeatures.length,
});
} catch (error) {
console.error("[SpecRegeneration] ❌ parseAndCreateFeatures() failed:");
console.error("[SpecRegeneration] Error:", error);
events.emit("spec-regeneration:event", {
type: "features_error",
error: (error as Error).message,
});
}
console.log("[SpecRegeneration] ========== parseAndCreateFeatures() completed ==========");
}