Merge main into massive-terminal-upgrade

Resolves merge conflicts:
- apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger
- apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions
- apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling)
- apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes
- apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
SuperComboGamer
2025-12-21 20:27:44 -05:00
393 changed files with 32473 additions and 17974 deletions

View File

@@ -2,15 +2,12 @@
* Common utilities and state for setup routes
*/
import { createLogger } from "../../lib/logger.js";
import path from "path";
import fs from "fs/promises";
import {
getErrorMessage as getErrorMessageShared,
createLogError,
} from "../common.js";
import { createLogger } from '@automaker/utils';
import path from 'path';
import fs from 'fs/promises';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
const logger = createLogger("Setup");
const logger = createLogger('Setup');
// Storage for API keys (in-memory cache) - private
const apiKeys: Record<string, string> = {};
@@ -39,22 +36,19 @@ export function getAllApiKeys(): Record<string, string> {
/**
* Helper to persist API keys to .env file
*/
export async function persistApiKeyToEnv(
key: string,
value: string
): Promise<void> {
const envPath = path.join(process.cwd(), ".env");
export async function persistApiKeyToEnv(key: string, value: string): Promise<void> {
const envPath = path.join(process.cwd(), '.env');
try {
let envContent = "";
let envContent = '';
try {
envContent = await fs.readFile(envPath, "utf-8");
envContent = await fs.readFile(envPath, 'utf-8');
} catch {
// .env file doesn't exist, we'll create it
}
// Parse existing env content
const lines = envContent.split("\n");
const lines = envContent.split('\n');
const keyRegex = new RegExp(`^${key}=`);
let found = false;
const newLines = lines.map((line) => {
@@ -70,7 +64,7 @@ export async function persistApiKeyToEnv(
newLines.push(`${key}=${value}`);
}
await fs.writeFile(envPath, newLines.join("\n"));
await fs.writeFile(envPath, newLines.join('\n'));
logger.info(`[Setup] Persisted ${key} to .env file`);
} catch (error) {
logger.error(`[Setup] Failed to persist ${key} to .env:`, error);

View File

@@ -2,20 +2,18 @@
* GET /api-keys endpoint - Get API keys status
*/
import type { Request, Response } from "express";
import { getApiKey, getErrorMessage, logError } from "../common.js";
import type { Request, Response } from 'express';
import { getApiKey, getErrorMessage, logError } from '../common.js';
export function createApiKeysHandler() {
return async (_req: Request, res: Response): Promise<void> => {
try {
res.json({
success: true,
hasAnthropicKey:
!!getApiKey("anthropic") || !!process.env.ANTHROPIC_API_KEY,
hasGoogleKey: !!getApiKey("google") || !!process.env.GOOGLE_API_KEY,
hasAnthropicKey: !!getApiKey('anthropic') || !!process.env.ANTHROPIC_API_KEY,
});
} catch (error) {
logError(error, "Get API keys failed");
logError(error, 'Get API keys failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -2,43 +2,43 @@
* POST /delete-api-key endpoint - Delete a stored API key
*/
import type { Request, Response } from "express";
import { createLogger } from "../../../lib/logger.js";
import path from "path";
import fs from "fs/promises";
import type { Request, Response } from 'express';
import { createLogger } from '@automaker/utils';
import path from 'path';
import fs from 'fs/promises';
const logger = createLogger("Setup");
const logger = createLogger('Setup');
// In-memory storage reference (imported from common.ts pattern)
// We need to modify common.ts to export a deleteApiKey function
import { setApiKey } from "../common.js";
import { setApiKey } from '../common.js';
/**
* Remove an API key from the .env file
*/
async function removeApiKeyFromEnv(key: string): Promise<void> {
const envPath = path.join(process.cwd(), ".env");
const envPath = path.join(process.cwd(), '.env');
try {
let envContent = "";
let envContent = '';
try {
envContent = await fs.readFile(envPath, "utf-8");
envContent = await fs.readFile(envPath, 'utf-8');
} catch {
// .env file doesn't exist, nothing to delete
return;
}
// Parse existing env content and remove the key
const lines = envContent.split("\n");
const lines = envContent.split('\n');
const keyRegex = new RegExp(`^${key}=`);
const newLines = lines.filter((line) => !keyRegex.test(line));
// Remove empty lines at the end
while (newLines.length > 0 && newLines[newLines.length - 1].trim() === "") {
while (newLines.length > 0 && newLines[newLines.length - 1].trim() === '') {
newLines.pop();
}
await fs.writeFile(envPath, newLines.join("\n") + (newLines.length > 0 ? "\n" : ""));
await fs.writeFile(envPath, newLines.join('\n') + (newLines.length > 0 ? '\n' : ''));
logger.info(`[Setup] Removed ${key} from .env file`);
} catch (error) {
logger.error(`[Setup] Failed to remove ${key} from .env:`, error);
@@ -54,7 +54,7 @@ export function createDeleteApiKeyHandler() {
if (!provider) {
res.status(400).json({
success: false,
error: "Provider is required",
error: 'Provider is required',
});
return;
}
@@ -63,22 +63,20 @@ export function createDeleteApiKeyHandler() {
// Map provider to env key name
const envKeyMap: Record<string, string> = {
anthropic: "ANTHROPIC_API_KEY",
google: "GOOGLE_GENERATIVE_AI_API_KEY",
openai: "OPENAI_API_KEY",
anthropic: 'ANTHROPIC_API_KEY',
};
const envKey = envKeyMap[provider];
if (!envKey) {
res.status(400).json({
success: false,
error: `Unknown provider: ${provider}`,
error: `Unknown provider: ${provider}. Only anthropic is supported.`,
});
return;
}
// Clear from in-memory storage
setApiKey(provider, "");
setApiKey(provider, '');
// Remove from environment
delete process.env[envKey];
@@ -93,14 +91,11 @@ export function createDeleteApiKeyHandler() {
message: `API key for ${provider} has been deleted`,
});
} catch (error) {
logger.error("[Setup] Delete API key error:", error);
logger.error('[Setup] Delete API key error:', error);
res.status(500).json({
success: false,
error: error instanceof Error ? error.message : "Failed to delete API key",
error: error instanceof Error ? error.message : 'Failed to delete API key',
});
}
};
}

View File

@@ -2,16 +2,11 @@
* POST /store-api-key endpoint - Store API key
*/
import type { Request, Response } from "express";
import {
setApiKey,
persistApiKeyToEnv,
getErrorMessage,
logError,
} from "../common.js";
import { createLogger } from "../../../lib/logger.js";
import type { Request, Response } from 'express';
import { setApiKey, persistApiKeyToEnv, getErrorMessage, logError } from '../common.js';
import { createLogger } from '@automaker/utils';
const logger = createLogger("Setup");
const logger = createLogger('Setup');
export function createStoreApiKeyHandler() {
return async (req: Request, res: Response): Promise<void> => {
@@ -22,28 +17,29 @@ export function createStoreApiKeyHandler() {
};
if (!provider || !apiKey) {
res
.status(400)
.json({ success: false, error: "provider and apiKey required" });
res.status(400).json({ success: false, error: 'provider and apiKey required' });
return;
}
setApiKey(provider, apiKey);
// Also set as environment variable and persist to .env
if (provider === "anthropic" || provider === "anthropic_oauth_token") {
if (provider === 'anthropic' || provider === 'anthropic_oauth_token') {
// Both API key and OAuth token use ANTHROPIC_API_KEY
process.env.ANTHROPIC_API_KEY = apiKey;
await persistApiKeyToEnv("ANTHROPIC_API_KEY", apiKey);
logger.info("[Setup] Stored API key as ANTHROPIC_API_KEY");
} else if (provider === "google") {
process.env.GOOGLE_API_KEY = apiKey;
await persistApiKeyToEnv("GOOGLE_API_KEY", apiKey);
await persistApiKeyToEnv('ANTHROPIC_API_KEY', apiKey);
logger.info('[Setup] Stored API key as ANTHROPIC_API_KEY');
} else {
res.status(400).json({
success: false,
error: `Unsupported provider: ${provider}. Only anthropic is supported.`,
});
return;
}
res.json({ success: true });
} catch (error) {
logError(error, "Store API key failed");
logError(error, 'Store API key failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};

View File

@@ -3,50 +3,50 @@
* Supports verifying either CLI auth or API key auth independently
*/
import type { Request, Response } from "express";
import { query } from "@anthropic-ai/claude-agent-sdk";
import { createLogger } from "../../../lib/logger.js";
import { getApiKey } from "../common.js";
import type { Request, Response } from 'express';
import { query } from '@anthropic-ai/claude-agent-sdk';
import { createLogger } from '@automaker/utils';
import { getApiKey } from '../common.js';
const logger = createLogger("Setup");
const logger = createLogger('Setup');
// Known error patterns that indicate auth failure
const AUTH_ERROR_PATTERNS = [
"OAuth token revoked",
"Please run /login",
"please run /login",
"token revoked",
"invalid_api_key",
"authentication_error",
"unauthorized",
"not authenticated",
"authentication failed",
"invalid api key",
"api key is invalid",
'OAuth token revoked',
'Please run /login',
'please run /login',
'token revoked',
'invalid_api_key',
'authentication_error',
'unauthorized',
'not authenticated',
'authentication failed',
'invalid api key',
'api key is invalid',
];
// Patterns that indicate billing/credit issues - should FAIL verification
const BILLING_ERROR_PATTERNS = [
"credit balance is too low",
"credit balance too low",
"insufficient credits",
"insufficient balance",
"no credits",
"out of credits",
"billing",
"payment required",
"add credits",
'credit balance is too low',
'credit balance too low',
'insufficient credits',
'insufficient balance',
'no credits',
'out of credits',
'billing',
'payment required',
'add credits',
];
// Patterns that indicate rate/usage limits - should FAIL verification
// Users need to wait or upgrade their plan
const RATE_LIMIT_PATTERNS = [
"limit reached",
"rate limit",
"rate_limit",
"resets", // Only valid if it's a temporary reset, not a billing issue
"/upgrade",
"extra-usage",
'limit reached',
'rate limit',
'rate_limit',
'resets', // Only valid if it's a temporary reset, not a billing issue
'/upgrade',
'extra-usage',
];
function isRateLimitError(text: string): boolean {
@@ -55,43 +55,33 @@ function isRateLimitError(text: string): boolean {
if (isBillingError(text)) {
return false;
}
return RATE_LIMIT_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
return RATE_LIMIT_PATTERNS.some((pattern) => lowerText.includes(pattern.toLowerCase()));
}
function isBillingError(text: string): boolean {
const lowerText = text.toLowerCase();
return BILLING_ERROR_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
return BILLING_ERROR_PATTERNS.some((pattern) => lowerText.includes(pattern.toLowerCase()));
}
function containsAuthError(text: string): boolean {
const lowerText = text.toLowerCase();
return AUTH_ERROR_PATTERNS.some((pattern) =>
lowerText.includes(pattern.toLowerCase())
);
return AUTH_ERROR_PATTERNS.some((pattern) => lowerText.includes(pattern.toLowerCase()));
}
export function createVerifyClaudeAuthHandler() {
return async (req: Request, res: Response): Promise<void> => {
try {
// Get the auth method from the request body
const { authMethod } = req.body as { authMethod?: "cli" | "api_key" };
const { authMethod } = req.body as { authMethod?: 'cli' | 'api_key' };
logger.info(
`[Setup] Verifying Claude authentication using method: ${
authMethod || "auto"
}`
);
logger.info(`[Setup] Verifying Claude authentication using method: ${authMethod || 'auto'}`);
// Create an AbortController with a 30-second timeout
const abortController = new AbortController();
const timeoutId = setTimeout(() => abortController.abort(), 30000);
let authenticated = false;
let errorMessage = "";
let errorMessage = '';
let receivedAnyContent = false;
// Save original env values
@@ -99,25 +89,23 @@ export function createVerifyClaudeAuthHandler() {
try {
// Configure environment based on auth method
if (authMethod === "cli") {
if (authMethod === 'cli') {
// For CLI verification, remove any API key so it uses CLI credentials only
delete process.env.ANTHROPIC_API_KEY;
logger.info(
"[Setup] Cleared API key environment for CLI verification"
);
} else if (authMethod === "api_key") {
logger.info('[Setup] Cleared API key environment for CLI verification');
} else if (authMethod === 'api_key') {
// For API key verification, ensure we're using the stored API key
const storedApiKey = getApiKey("anthropic");
const storedApiKey = getApiKey('anthropic');
if (storedApiKey) {
process.env.ANTHROPIC_API_KEY = storedApiKey;
logger.info("[Setup] Using stored API key for verification");
logger.info('[Setup] Using stored API key for verification');
} else {
// Check env var
if (!process.env.ANTHROPIC_API_KEY) {
res.json({
success: true,
authenticated: false,
error: "No API key configured. Please enter an API key first.",
error: 'No API key configured. Please enter an API key first.',
});
return;
}
@@ -128,7 +116,7 @@ export function createVerifyClaudeAuthHandler() {
const stream = query({
prompt: "Reply with only the word 'ok'",
options: {
model: "claude-sonnet-4-20250514",
model: 'claude-sonnet-4-20250514',
maxTurns: 1,
allowedTools: [],
abortController,
@@ -141,50 +129,50 @@ export function createVerifyClaudeAuthHandler() {
for await (const msg of stream) {
const msgStr = JSON.stringify(msg);
allMessages.push(msgStr);
logger.info("[Setup] Stream message:", msgStr.substring(0, 500));
logger.info('[Setup] Stream message:', msgStr.substring(0, 500));
// Check for billing errors FIRST - these should fail verification
if (isBillingError(msgStr)) {
logger.error("[Setup] Found billing error in message");
logger.error('[Setup] Found billing error in message');
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
'Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com';
authenticated = false;
break;
}
// Check if any part of the message contains auth errors
if (containsAuthError(msgStr)) {
logger.error("[Setup] Found auth error in message");
if (authMethod === "cli") {
logger.error('[Setup] Found auth error in message');
if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
errorMessage = 'API key is invalid or has been revoked.';
}
break;
}
// Check specifically for assistant messages with text content
if (msg.type === "assistant" && (msg as any).message?.content) {
if (msg.type === 'assistant' && (msg as any).message?.content) {
const content = (msg as any).message.content;
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === "text" && block.text) {
if (block.type === 'text' && block.text) {
const text = block.text;
logger.info("[Setup] Assistant text:", text);
logger.info('[Setup] Assistant text:', text);
if (containsAuthError(text)) {
if (authMethod === "cli") {
if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
errorMessage = 'API key is invalid or has been revoked.';
}
break;
}
// Valid text response that's not an error
if (text.toLowerCase().includes("ok") || text.length > 0) {
if (text.toLowerCase().includes('ok') || text.length > 0) {
receivedAnyContent = true;
}
}
@@ -193,34 +181,30 @@ export function createVerifyClaudeAuthHandler() {
}
// Check for result messages
if (msg.type === "result") {
if (msg.type === 'result') {
const resultStr = JSON.stringify(msg);
// First check for billing errors - these should FAIL verification
if (isBillingError(resultStr)) {
logger.error(
"[Setup] Billing error detected - insufficient credits"
);
logger.error('[Setup] Billing error detected - insufficient credits');
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
'Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com';
authenticated = false;
break;
}
// Check if it's a rate limit error - should FAIL verification
else if (isRateLimitError(resultStr)) {
logger.warn(
"[Setup] Rate limit detected - treating as unverified"
);
logger.warn('[Setup] Rate limit detected - treating as unverified');
errorMessage =
"Rate limit reached. Please wait a while before trying again or upgrade your plan.";
'Rate limit reached. Please wait a while before trying again or upgrade your plan.';
authenticated = false;
break;
} else if (containsAuthError(resultStr)) {
if (authMethod === "cli") {
if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
errorMessage = 'API key is invalid or has been revoked.';
}
} else {
// Got a result without errors
@@ -236,60 +220,48 @@ export function createVerifyClaudeAuthHandler() {
authenticated = true;
} else {
// No content received - might be an issue
logger.warn("[Setup] No content received from stream");
logger.warn("[Setup] All messages:", allMessages.join("\n"));
errorMessage =
"No response received from Claude. Please check your authentication.";
logger.warn('[Setup] No content received from stream');
logger.warn('[Setup] All messages:', allMessages.join('\n'));
errorMessage = 'No response received from Claude. Please check your authentication.';
}
} catch (error: unknown) {
const errMessage =
error instanceof Error ? error.message : String(error);
const errMessage = error instanceof Error ? error.message : String(error);
logger.error("[Setup] Claude auth verification exception:", errMessage);
logger.error('[Setup] Claude auth verification exception:', errMessage);
// Check for billing errors FIRST - these always fail
if (isBillingError(errMessage)) {
authenticated = false;
errorMessage =
"Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com";
'Credit balance is too low. Please add credits to your Anthropic account at console.anthropic.com';
}
// Check for rate limit in exception - should FAIL verification
else if (isRateLimitError(errMessage)) {
authenticated = false;
errorMessage =
"Rate limit reached. Please wait a while before trying again or upgrade your plan.";
logger.warn(
"[Setup] Rate limit in exception - treating as unverified"
);
'Rate limit reached. Please wait a while before trying again or upgrade your plan.';
logger.warn('[Setup] Rate limit in exception - treating as unverified');
}
// If we already determined auth was successful, keep it
else if (authenticated) {
logger.info("[Setup] Auth already confirmed, ignoring exception");
logger.info('[Setup] Auth already confirmed, ignoring exception');
}
// Check for auth-related errors in exception
else if (containsAuthError(errMessage)) {
if (authMethod === "cli") {
if (authMethod === 'cli') {
errorMessage =
"CLI authentication failed. Please run 'claude login' in your terminal to authenticate.";
} else {
errorMessage = "API key is invalid or has been revoked.";
errorMessage = 'API key is invalid or has been revoked.';
}
} else if (
errMessage.includes("abort") ||
errMessage.includes("timeout")
) {
errorMessage = "Verification timed out. Please try again.";
} else if (
errMessage.includes("exit") &&
errMessage.includes("code 1")
) {
} else if (errMessage.includes('abort') || errMessage.includes('timeout')) {
errorMessage = 'Verification timed out. Please try again.';
} else if (errMessage.includes('exit') && errMessage.includes('code 1')) {
// Process exited with code 1 but we might have gotten rate limit info in the stream
// Check if we received any content that indicated auth worked
if (receivedAnyContent && !errorMessage) {
authenticated = true;
logger.info(
"[Setup] Process exit 1 but content received - auth valid"
);
logger.info('[Setup] Process exit 1 but content received - auth valid');
} else if (!errorMessage) {
errorMessage = errMessage;
}
@@ -301,13 +273,13 @@ export function createVerifyClaudeAuthHandler() {
// Restore original environment
if (originalAnthropicKey !== undefined) {
process.env.ANTHROPIC_API_KEY = originalAnthropicKey;
} else if (authMethod === "cli") {
} else if (authMethod === 'cli') {
// If we cleared it and there was no original, keep it cleared
delete process.env.ANTHROPIC_API_KEY;
}
}
logger.info("[Setup] Verification result:", {
logger.info('[Setup] Verification result:', {
authenticated,
errorMessage,
authMethod,
@@ -319,11 +291,11 @@ export function createVerifyClaudeAuthHandler() {
error: errorMessage || undefined,
});
} catch (error) {
logger.error("[Setup] Verify Claude auth endpoint error:", error);
logger.error('[Setup] Verify Claude auth endpoint error:', error);
res.status(500).json({
success: false,
authenticated: false,
error: error instanceof Error ? error.message : "Verification failed",
error: error instanceof Error ? error.message : 'Verification failed',
});
}
};