Memory test and global enviroment variables

This commit is contained in:
AndyMik90
2025-12-11 21:28:15 +01:00
parent dec16284f1
commit bc5ad38e05
16 changed files with 2099 additions and 121 deletions
+1
View File
@@ -15,6 +15,7 @@
"package:mac": "electron-vite build && electron-builder --mac",
"package:win": "electron-vite build && electron-builder --win",
"package:linux": "electron-vite build && electron-builder --linux",
"start:packaged": "open dist/mac-arm64/Auto\\ Claude.app || open dist/mac/Auto\\ Claude.app",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
+20 -7
View File
@@ -73,11 +73,14 @@ export class AgentManager extends EventEmitter {
private loadAutoBuildEnv(): Record<string, string> {
const autoBuildSource = this.getAutoBuildSourcePath();
if (!autoBuildSource) {
console.log('[loadAutoBuildEnv] No auto-build source path found');
return {};
}
const envPath = path.join(autoBuildSource, '.env');
console.log('[loadAutoBuildEnv] Looking for .env at:', envPath);
if (!existsSync(envPath)) {
console.log('[loadAutoBuildEnv] .env file does not exist');
return {};
}
@@ -230,17 +233,17 @@ export class AgentManager extends EventEmitter {
const args = [ideationRunnerPath, '--project', projectPath];
// Add enabled types
// Add enabled types as comma-separated list
if (config.enabledTypes.length > 0) {
args.push('--types', ...config.enabledTypes);
args.push('--types', config.enabledTypes.join(','));
}
// Add context flags
if (config.includeRoadmapContext) {
args.push('--include-roadmap');
// Add context flags (script uses --no-roadmap/--no-kanban negative flags)
if (!config.includeRoadmapContext) {
args.push('--no-roadmap');
}
if (config.includeKanbanContext) {
args.push('--include-kanban');
if (!config.includeKanbanContext) {
args.push('--no-kanban');
}
// Add max ideas per type
@@ -299,11 +302,18 @@ export class AgentManager extends EventEmitter {
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.length > 0) {
console.log('[Ideation]', trimmed);
this.emit('ideation-log', projectId, trimmed);
}
}
};
console.log('[Ideation] Starting ideation process with args:', args);
console.log('[Ideation] CWD:', cwd);
console.log('[Ideation] Python path:', this.pythonPath);
console.log('[Ideation] Env vars loaded:', Object.keys(autoBuildEnv));
console.log('[Ideation] Has CLAUDE_CODE_OAUTH_TOKEN:', !!autoBuildEnv['CLAUDE_CODE_OAUTH_TOKEN']);
// Handle stdout
childProcess.stdout?.on('data', (data: Buffer) => {
const log = data.toString();
@@ -356,6 +366,7 @@ export class AgentManager extends EventEmitter {
// Handle stderr - also emit as logs
childProcess.stderr?.on('data', (data: Buffer) => {
const log = data.toString();
console.error('[Ideation STDERR]', log);
emitLogs(log);
this.emit('ideation-progress', projectId, {
phase: progressPhase,
@@ -366,6 +377,7 @@ export class AgentManager extends EventEmitter {
// Handle process exit
childProcess.on('exit', (code: number | null) => {
console.log('[Ideation] Process exited with code:', code);
this.processes.delete(projectId);
if (code === 0) {
@@ -381,6 +393,7 @@ export class AgentManager extends EventEmitter {
// Handle process error
childProcess.on('error', (err: Error) => {
console.error('[Ideation] Process error:', err.message);
this.processes.delete(projectId);
this.emit('ideation-error', projectId, err.message);
});
+312 -77
View File
@@ -40,7 +40,9 @@ import type {
IdeationSession,
IdeationConfig,
IdeationGenerationStatus,
IdeationStatus
IdeationStatus,
SourceEnvConfig,
SourceEnvCheckResult
} from '../shared/types';
import { projectStore } from './project-store';
import { fileWatcher } from './file-watcher';
@@ -494,16 +496,26 @@ export function setupIpcHandlers(
ipcMain.handle(
IPC_CHANNELS.SETTINGS_GET,
async (): Promise<IPCResult<AppSettings>> => {
let settings = { ...DEFAULT_APP_SETTINGS };
if (existsSync(settingsPath)) {
try {
const content = readFileSync(settingsPath, 'utf-8');
const settings = JSON.parse(content);
return { success: true, data: { ...DEFAULT_APP_SETTINGS, ...settings } };
settings = { ...settings, ...JSON.parse(content) };
} catch {
return { success: true, data: DEFAULT_APP_SETTINGS as AppSettings };
// Use defaults
}
}
return { success: true, data: DEFAULT_APP_SETTINGS as AppSettings };
// If no manual autoBuildPath is set, try to auto-detect
if (!settings.autoBuildPath) {
const detectedPath = detectAutoBuildSourcePath();
if (detectedPath) {
settings.autoBuildPath = detectedPath;
}
}
return { success: true, data: settings as AppSettings };
}
);
@@ -1479,89 +1491,111 @@ ${existingVars['GRAPHITI_DATABASE'] ? `GRAPHITI_DATABASE=${existingVars['GRAPHIT
const envPath = path.join(project.path, project.autoBuildPath, '.env');
// Load global settings for fallbacks
let globalSettings: AppSettings = { ...DEFAULT_APP_SETTINGS };
if (existsSync(settingsPath)) {
try {
const content = readFileSync(settingsPath, 'utf-8');
globalSettings = { ...globalSettings, ...JSON.parse(content) };
} catch {
// Use defaults
}
}
// Default config
const config: ProjectEnvConfig = {
claudeAuthStatus: 'not_configured',
linearEnabled: false,
githubEnabled: false,
graphitiEnabled: false,
enableFancyUi: true
enableFancyUi: true,
claudeTokenIsGlobal: false,
openaiKeyIsGlobal: false
};
if (!existsSync(envPath)) {
return { success: true, data: config };
// Parse project-specific .env if it exists
let vars: Record<string, string> = {};
if (existsSync(envPath)) {
try {
const content = readFileSync(envPath, 'utf-8');
vars = parseEnvFile(content);
} catch {
// Continue with empty vars
}
}
try {
const content = readFileSync(envPath, 'utf-8');
const vars = parseEnvFile(content);
// Parse values into config
if (vars['CLAUDE_CODE_OAUTH_TOKEN']) {
config.claudeOAuthToken = vars['CLAUDE_CODE_OAUTH_TOKEN'];
config.claudeAuthStatus = 'token_set';
}
if (vars['AUTO_BUILD_MODEL']) {
config.autoBuildModel = vars['AUTO_BUILD_MODEL'];
}
if (vars['LINEAR_API_KEY']) {
config.linearEnabled = true;
config.linearApiKey = vars['LINEAR_API_KEY'];
}
if (vars['LINEAR_TEAM_ID']) {
config.linearTeamId = vars['LINEAR_TEAM_ID'];
}
if (vars['LINEAR_PROJECT_ID']) {
config.linearProjectId = vars['LINEAR_PROJECT_ID'];
}
if (vars['LINEAR_REALTIME_SYNC']?.toLowerCase() === 'true') {
config.linearRealtimeSync = true;
}
// GitHub config
if (vars['GITHUB_TOKEN']) {
config.githubEnabled = true;
config.githubToken = vars['GITHUB_TOKEN'];
}
if (vars['GITHUB_REPO']) {
config.githubRepo = vars['GITHUB_REPO'];
}
if (vars['GITHUB_AUTO_SYNC']?.toLowerCase() === 'true') {
config.githubAutoSync = true;
}
if (vars['GRAPHITI_ENABLED']?.toLowerCase() === 'true') {
config.graphitiEnabled = true;
}
if (vars['OPENAI_API_KEY']) {
config.openaiApiKey = vars['OPENAI_API_KEY'];
}
if (vars['GRAPHITI_FALKORDB_HOST']) {
config.graphitiFalkorDbHost = vars['GRAPHITI_FALKORDB_HOST'];
}
if (vars['GRAPHITI_FALKORDB_PORT']) {
config.graphitiFalkorDbPort = parseInt(vars['GRAPHITI_FALKORDB_PORT'], 10);
}
if (vars['GRAPHITI_FALKORDB_PASSWORD']) {
config.graphitiFalkorDbPassword = vars['GRAPHITI_FALKORDB_PASSWORD'];
}
if (vars['GRAPHITI_DATABASE']) {
config.graphitiDatabase = vars['GRAPHITI_DATABASE'];
}
if (vars['ENABLE_FANCY_UI']?.toLowerCase() === 'false') {
config.enableFancyUi = false;
}
return { success: true, data: config };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to read .env file'
};
// Claude OAuth Token: project-specific takes precedence, then global
if (vars['CLAUDE_CODE_OAUTH_TOKEN']) {
config.claudeOAuthToken = vars['CLAUDE_CODE_OAUTH_TOKEN'];
config.claudeAuthStatus = 'token_set';
config.claudeTokenIsGlobal = false;
} else if (globalSettings.globalClaudeOAuthToken) {
config.claudeOAuthToken = globalSettings.globalClaudeOAuthToken;
config.claudeAuthStatus = 'token_set';
config.claudeTokenIsGlobal = true;
}
if (vars['AUTO_BUILD_MODEL']) {
config.autoBuildModel = vars['AUTO_BUILD_MODEL'];
}
if (vars['LINEAR_API_KEY']) {
config.linearEnabled = true;
config.linearApiKey = vars['LINEAR_API_KEY'];
}
if (vars['LINEAR_TEAM_ID']) {
config.linearTeamId = vars['LINEAR_TEAM_ID'];
}
if (vars['LINEAR_PROJECT_ID']) {
config.linearProjectId = vars['LINEAR_PROJECT_ID'];
}
if (vars['LINEAR_REALTIME_SYNC']?.toLowerCase() === 'true') {
config.linearRealtimeSync = true;
}
// GitHub config
if (vars['GITHUB_TOKEN']) {
config.githubEnabled = true;
config.githubToken = vars['GITHUB_TOKEN'];
}
if (vars['GITHUB_REPO']) {
config.githubRepo = vars['GITHUB_REPO'];
}
if (vars['GITHUB_AUTO_SYNC']?.toLowerCase() === 'true') {
config.githubAutoSync = true;
}
if (vars['GRAPHITI_ENABLED']?.toLowerCase() === 'true') {
config.graphitiEnabled = true;
}
// OpenAI API Key: project-specific takes precedence, then global
if (vars['OPENAI_API_KEY']) {
config.openaiApiKey = vars['OPENAI_API_KEY'];
config.openaiKeyIsGlobal = false;
} else if (globalSettings.globalOpenAIApiKey) {
config.openaiApiKey = globalSettings.globalOpenAIApiKey;
config.openaiKeyIsGlobal = true;
}
if (vars['GRAPHITI_FALKORDB_HOST']) {
config.graphitiFalkorDbHost = vars['GRAPHITI_FALKORDB_HOST'];
}
if (vars['GRAPHITI_FALKORDB_PORT']) {
config.graphitiFalkorDbPort = parseInt(vars['GRAPHITI_FALKORDB_PORT'], 10);
}
if (vars['GRAPHITI_FALKORDB_PASSWORD']) {
config.graphitiFalkorDbPassword = vars['GRAPHITI_FALKORDB_PASSWORD'];
}
if (vars['GRAPHITI_DATABASE']) {
config.graphitiDatabase = vars['GRAPHITI_DATABASE'];
}
if (vars['ENABLE_FANCY_UI']?.toLowerCase() === 'false') {
config.enableFancyUi = false;
}
return { success: true, data: config };
}
);
@@ -2791,6 +2825,207 @@ ${issue.body || 'No description provided.'}
}
);
// ============================================
// Auto Claude Source Environment Operations
// ============================================
/**
* Parse an .env file content into a key-value object
*/
const parseSourceEnvFile = (content: string): Record<string, string> => {
const vars: Record<string, string> = {};
for (const line of content.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
const key = trimmed.substring(0, eqIndex).trim();
let value = trimmed.substring(eqIndex + 1).trim();
// Remove quotes if present
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
vars[key] = value;
}
}
return vars;
};
ipcMain.handle(
IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_GET,
async (): Promise<IPCResult<SourceEnvConfig>> => {
try {
const sourcePath = getEffectiveSourcePath();
if (!sourcePath) {
return {
success: true,
data: {
hasClaudeToken: false,
envExists: false,
sourcePath: undefined
}
};
}
const envPath = path.join(sourcePath, '.env');
const envExists = existsSync(envPath);
if (!envExists) {
return {
success: true,
data: {
hasClaudeToken: false,
envExists: false,
sourcePath
}
};
}
const content = readFileSync(envPath, 'utf-8');
const vars = parseSourceEnvFile(content);
const hasToken = !!vars['CLAUDE_CODE_OAUTH_TOKEN'];
return {
success: true,
data: {
hasClaudeToken: hasToken,
claudeOAuthToken: hasToken ? vars['CLAUDE_CODE_OAUTH_TOKEN'] : undefined,
envExists: true,
sourcePath
}
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to get source env'
};
}
}
);
ipcMain.handle(
IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_UPDATE,
async (_, config: { claudeOAuthToken?: string }): Promise<IPCResult> => {
try {
const sourcePath = getEffectiveSourcePath();
if (!sourcePath) {
return {
success: false,
error: 'Auto-Claude source path not found. Please configure it in App Settings.'
};
}
const envPath = path.join(sourcePath, '.env');
// Read existing content or start fresh
let existingContent = '';
const existingVars: Record<string, string> = {};
if (existsSync(envPath)) {
existingContent = readFileSync(envPath, 'utf-8');
Object.assign(existingVars, parseSourceEnvFile(existingContent));
}
// Update the token
if (config.claudeOAuthToken !== undefined) {
existingVars['CLAUDE_CODE_OAUTH_TOKEN'] = config.claudeOAuthToken;
}
// Rebuild the .env file preserving comments and structure
const lines = existingContent.split('\n');
const processedKeys = new Set<string>();
const outputLines: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) {
outputLines.push(line);
continue;
}
const eqIndex = trimmed.indexOf('=');
if (eqIndex > 0) {
const key = trimmed.substring(0, eqIndex).trim();
if (key in existingVars) {
outputLines.push(`${key}=${existingVars[key]}`);
processedKeys.add(key);
} else {
outputLines.push(line);
}
} else {
outputLines.push(line);
}
}
// Add any new keys that weren't in the original file
for (const [key, value] of Object.entries(existingVars)) {
if (!processedKeys.has(key)) {
outputLines.push(`${key}=${value}`);
}
}
writeFileSync(envPath, outputLines.join('\n'));
return { success: true };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to update source env'
};
}
}
);
ipcMain.handle(
IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_CHECK_TOKEN,
async (): Promise<IPCResult<SourceEnvCheckResult>> => {
try {
const sourcePath = getEffectiveSourcePath();
if (!sourcePath) {
return {
success: true,
data: {
hasToken: false,
sourcePath: undefined,
error: 'Auto-Claude source path not found'
}
};
}
const envPath = path.join(sourcePath, '.env');
if (!existsSync(envPath)) {
return {
success: true,
data: {
hasToken: false,
sourcePath,
error: '.env file does not exist'
}
};
}
const content = readFileSync(envPath, 'utf-8');
const vars = parseSourceEnvFile(content);
const hasToken = !!vars['CLAUDE_CODE_OAUTH_TOKEN'] && vars['CLAUDE_CODE_OAUTH_TOKEN'].length > 0;
return {
success: true,
data: {
hasToken,
sourcePath
}
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to check source token'
};
}
}
);
// ============================================
// Ideation Operations
// ============================================
+15
View File
@@ -40,6 +40,8 @@ import type {
IdeationGenerationStatus,
AutoBuildSourceUpdateCheck,
AutoBuildSourceUpdateProgress,
SourceEnvConfig,
SourceEnvCheckResult,
ChangelogTask,
TaskSpecContent,
ChangelogGenerationRequest,
@@ -595,6 +597,19 @@ const electronAPI: ElectronAPI = {
};
},
// ============================================
// Auto-Build Source Environment Operations
// ============================================
getSourceEnv: (): Promise<IPCResult<SourceEnvConfig>> =>
ipcRenderer.invoke(IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_GET),
updateSourceEnv: (config: { claudeOAuthToken?: string }): Promise<IPCResult> =>
ipcRenderer.invoke(IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_UPDATE, config),
checkSourceToken: (): Promise<IPCResult<SourceEnvCheckResult>> =>
ipcRenderer.invoke(IPC_CHANNELS.AUTOBUILD_SOURCE_ENV_CHECK_TOKEN),
// ============================================
// Changelog Operations
// ============================================
+59
View File
@@ -295,6 +295,65 @@ export function App() {
onOpenChange={setIsProjectSettingsOpen}
/>
)}
{/* Initialize Auto Claude Dialog */}
<Dialog open={showInitDialog} onOpenChange={setShowInitDialog}>
<DialogContent>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Download className="h-5 w-5" />
Initialize Auto Claude
</DialogTitle>
<DialogDescription>
This project doesn't have Auto Claude initialized. Would you like to set it up now?
</DialogDescription>
</DialogHeader>
<div className="py-4">
<div className="rounded-lg bg-muted p-4 text-sm">
<p className="font-medium mb-2">This will:</p>
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
<li>Create a <code className="text-xs bg-background px-1 py-0.5 rounded">.auto-claude</code> folder in your project</li>
<li>Copy the Auto Claude framework files</li>
<li>Set up the specs directory for your tasks</li>
</ul>
</div>
{!settings.autoBuildPath && (
<div className="mt-4 rounded-lg border border-warning/50 bg-warning/10 p-4 text-sm">
<div className="flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-warning mt-0.5 shrink-0" />
<div>
<p className="font-medium text-warning">Source path not configured</p>
<p className="text-muted-foreground mt-1">
Please set the Auto Claude source path in App Settings before initializing.
</p>
</div>
</div>
</div>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={handleSkipInit} disabled={isInitializing}>
Skip
</Button>
<Button
onClick={handleInitialize}
disabled={isInitializing || !settings.autoBuildPath}
>
{isInitializing ? (
<>
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
Initializing...
</>
) : (
<>
<Download className="mr-2 h-4 w-4" />
Initialize
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</TooltipProvider>
);
@@ -10,7 +10,11 @@ import {
RefreshCw,
CheckCircle2,
AlertCircle,
CloudDownload
CloudDownload,
Key,
Eye,
EyeOff,
Info
} from 'lucide-react';
import {
Dialog,
@@ -60,6 +64,10 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
const [isDownloadingUpdate, setIsDownloadingUpdate] = useState(false);
const [downloadProgress, setDownloadProgress] = useState<AutoBuildSourceUpdateProgress | null>(null);
// Password visibility toggles for global API keys
const [showGlobalClaudeToken, setShowGlobalClaudeToken] = useState(false);
const [showGlobalOpenAIKey, setShowGlobalOpenAIKey] = useState(false);
// Load settings on mount
useEffect(() => {
loadSettings();
@@ -298,6 +306,78 @@ export function AppSettingsDialog({ open, onOpenChange }: AppSettingsDialogProps
<Separator />
{/* Global API Keys */}
<section className="space-y-4">
<div className="flex items-center gap-2">
<Key className="h-4 w-4" />
<h3 className="text-sm font-semibold text-foreground">Global API Keys</h3>
</div>
<div className="rounded-lg bg-info/10 border border-info/30 p-3">
<div className="flex items-start gap-2">
<Info className="h-4 w-4 text-info flex-shrink-0 mt-0.5" />
<p className="text-xs text-muted-foreground">
Set API keys here to use them across all projects. Individual projects can override these in their settings.
</p>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="globalClaudeToken" className="text-sm font-medium text-foreground">
Claude OAuth Token
</Label>
<div className="relative">
<Input
id="globalClaudeToken"
type={showGlobalClaudeToken ? 'text' : 'password'}
placeholder="Enter your Claude OAuth token..."
value={settings.globalClaudeOAuthToken || ''}
onChange={(e) =>
setSettings({ ...settings, globalClaudeOAuthToken: e.target.value || undefined })
}
className="pr-10 font-mono text-sm"
/>
<button
type="button"
onClick={() => setShowGlobalClaudeToken(!showGlobalClaudeToken)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showGlobalClaudeToken ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-xs text-muted-foreground">
Get your token by running <code className="px-1 py-0.5 bg-muted rounded font-mono">claude setup-token</code>
</p>
</div>
<div className="space-y-2">
<Label htmlFor="globalOpenAIKey" className="text-sm font-medium text-foreground">
OpenAI API Key
</Label>
<div className="relative">
<Input
id="globalOpenAIKey"
type={showGlobalOpenAIKey ? 'text' : 'password'}
placeholder="sk-..."
value={settings.globalOpenAIApiKey || ''}
onChange={(e) =>
setSettings({ ...settings, globalOpenAIApiKey: e.target.value || undefined })
}
className="pr-10 font-mono text-sm"
/>
<button
type="button"
onClick={() => setShowGlobalOpenAIKey(!showGlobalOpenAIKey)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showGlobalOpenAIKey ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
</button>
</div>
<p className="text-xs text-muted-foreground">
Required for Graphiti memory backend (embeddings)
</p>
</div>
</section>
<Separator />
{/* Auto Claude Framework Updates */}
<section className="space-y-4">
<h3 className="text-sm font-semibold text-foreground">Auto Claude Framework</h3>
@@ -0,0 +1,333 @@
import { useState, useEffect } from 'react';
import {
AlertCircle,
Key,
Loader2,
CheckCircle2,
ExternalLink,
Copy,
Eye,
EyeOff,
Info
} from 'lucide-react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from './ui/dialog';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Label } from './ui/label';
import {
Tooltip,
TooltipContent,
TooltipTrigger
} from './ui/tooltip';
interface EnvConfigModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
onConfigured?: () => void;
title?: string;
description?: string;
}
export function EnvConfigModal({
open,
onOpenChange,
onConfigured,
title = 'Claude Authentication Required',
description = 'A Claude Code OAuth token is required to use AI features like Ideation and Roadmap generation.'
}: EnvConfigModalProps) {
const [token, setToken] = useState('');
const [showToken, setShowToken] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isChecking, setIsChecking] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [sourcePath, setSourcePath] = useState<string | null>(null);
const [hasExistingToken, setHasExistingToken] = useState(false);
// Check current token status when modal opens
useEffect(() => {
const checkToken = async () => {
if (!open) return;
setIsChecking(true);
setError(null);
setSuccess(false);
try {
const result = await window.electronAPI.checkSourceToken();
if (result.success && result.data) {
setSourcePath(result.data.sourcePath || null);
setHasExistingToken(result.data.hasToken);
if (result.data.hasToken) {
// Token exists, show success state
setSuccess(true);
}
} else {
setError(result.error || 'Failed to check token status');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsChecking(false);
}
};
checkToken();
}, [open]);
const handleSave = async () => {
if (!token.trim()) {
setError('Please enter a token');
return;
}
setIsSaving(true);
setError(null);
try {
const result = await window.electronAPI.updateSourceEnv({
claudeOAuthToken: token.trim()
});
if (result.success) {
setSuccess(true);
setHasExistingToken(true);
setToken(''); // Clear the input
// Notify parent that configuration is complete
setTimeout(() => {
onConfigured?.();
onOpenChange(false);
}, 1500);
} else {
setError(result.error || 'Failed to save token');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsSaving(false);
}
};
const handleCopyCommand = () => {
navigator.clipboard.writeText('claude setup-token');
};
const handleOpenDocs = () => {
// Open the Claude Code documentation for getting a token
window.open('https://docs.anthropic.com/en/docs/claude-code', '_blank');
};
const handleClose = () => {
if (!isSaving) {
setToken('');
setError(null);
setSuccess(false);
onOpenChange(false);
}
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2 text-foreground">
<Key className="h-5 w-5" />
{title}
</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
{/* Loading state */}
{isChecking && (
<div className="flex items-center justify-center py-8">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)}
{/* Success state */}
{!isChecking && success && (
<div className="py-4">
<div className="rounded-lg bg-success/10 border border-success/30 p-4 flex items-center gap-3">
<CheckCircle2 className="h-5 w-5 text-success flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium text-success">
Token configured successfully
</p>
<p className="text-xs text-success/80 mt-1">
You can now use AI features like Ideation and Roadmap generation.
</p>
</div>
</div>
</div>
)}
{/* Configuration form */}
{!isChecking && !success && (
<div className="py-4 space-y-4">
{/* Error banner */}
{error && (
<div className="rounded-lg bg-destructive/10 border border-destructive/30 p-3 flex items-start gap-2">
<AlertCircle className="h-4 w-4 text-destructive flex-shrink-0 mt-0.5" />
<p className="text-sm text-destructive">{error}</p>
</div>
)}
{/* Info about getting a token */}
<div className="rounded-lg bg-info/10 border border-info/30 p-4">
<div className="flex items-start gap-3">
<Info className="h-5 w-5 text-info flex-shrink-0 mt-0.5" />
<div className="flex-1 space-y-2">
<p className="text-sm text-foreground font-medium">
How to get a Claude Code OAuth token:
</p>
<ol className="text-sm text-muted-foreground space-y-1 list-decimal list-inside">
<li>Install Claude Code CLI if you haven't already</li>
<li>
Run{' '}
<code className="px-1.5 py-0.5 bg-muted rounded font-mono text-xs">
claude setup-token
</code>
{' '}
<button
onClick={handleCopyCommand}
className="inline-flex items-center text-info hover:text-info/80"
>
<Copy className="h-3 w-3 ml-1" />
</button>
</li>
<li>Copy the token and paste it below</li>
</ol>
<button
onClick={handleOpenDocs}
className="text-sm text-info hover:text-info/80 flex items-center gap-1"
>
<ExternalLink className="h-3 w-3" />
View documentation
</button>
</div>
</div>
</div>
{/* Token input */}
<div className="space-y-2">
<Label htmlFor="token" className="text-sm font-medium text-foreground">
Claude Code OAuth Token
</Label>
<div className="relative">
<Input
id="token"
type={showToken ? 'text' : 'password'}
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="Enter your token..."
className="pr-10 font-mono text-sm"
disabled={isSaving}
/>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setShowToken(!showToken)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
{showToken ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</button>
</TooltipTrigger>
<TooltipContent>
{showToken ? 'Hide token' : 'Show token'}
</TooltipContent>
</Tooltip>
</div>
<p className="text-xs text-muted-foreground">
The token will be saved to{' '}
<code className="px-1 py-0.5 bg-muted rounded font-mono">
{sourcePath ? `${sourcePath}/.env` : 'auto-claude/.env'}
</code>
</p>
</div>
{/* Existing token info */}
{hasExistingToken && (
<div className="rounded-lg bg-muted/50 p-3">
<p className="text-sm text-muted-foreground">
A token is already configured. Enter a new token above to replace it.
</p>
</div>
)}
</div>
)}
<DialogFooter>
<Button variant="outline" onClick={handleClose} disabled={isSaving}>
{success ? 'Close' : 'Cancel'}
</Button>
{!success && (
<Button onClick={handleSave} disabled={!token.trim() || isSaving}>
{isSaving ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
<>
<Key className="mr-2 h-4 w-4" />
Save Token
</>
)}
</Button>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
/**
* Hook to check if the Claude token is configured
* Returns { hasToken, isLoading, checkToken }
*/
export function useClaudeTokenCheck() {
const [hasToken, setHasToken] = useState<boolean | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const checkToken = async () => {
setIsLoading(true);
setError(null);
try {
const result = await window.electronAPI.checkSourceToken();
if (result.success && result.data) {
setHasToken(result.data.hasToken);
} else {
setHasToken(false);
setError(result.error || 'Failed to check token');
}
} catch (err) {
setHasToken(false);
setError(err instanceof Error ? err.message : 'Unknown error');
} finally {
setIsLoading(false);
}
};
useEffect(() => {
checkToken();
}, []);
return { hasToken, isLoading, error, checkToken };
}
@@ -29,7 +29,8 @@ import {
Database,
Wifi,
Box,
HardDrive
HardDrive,
Code2
} from 'lucide-react';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
@@ -51,6 +52,7 @@ import {
DialogHeader,
DialogTitle
} from './ui/dialog';
import { EnvConfigModal, useClaudeTokenCheck } from './EnvConfigModal';
import {
useIdeationStore,
loadIdeation,
@@ -75,7 +77,9 @@ import {
UIUX_CATEGORY_LABELS,
DOCUMENTATION_CATEGORY_LABELS,
SECURITY_CATEGORY_LABELS,
PERFORMANCE_CATEGORY_LABELS
PERFORMANCE_CATEGORY_LABELS,
CODE_QUALITY_CATEGORY_LABELS,
CODE_QUALITY_SEVERITY_COLORS
} from '../../shared/constants';
import type {
Idea,
@@ -86,7 +90,8 @@ import type {
HighValueFeatureIdea,
DocumentationGapIdea,
SecurityHardeningIdea,
PerformanceOptimizationIdea
PerformanceOptimizationIdea,
CodeQualityIdea
} from '../../shared/types';
interface IdeationProps {
@@ -107,6 +112,8 @@ const TypeIcon = ({ type }: { type: IdeationType }) => {
return <Shield className="h-4 w-4" />;
case 'performance_optimizations':
return <Gauge className="h-4 w-4" />;
case 'code_quality':
return <Code2 className="h-4 w-4" />;
default:
return <Lightbulb className="h-4 w-4" />;
}
@@ -119,7 +126,8 @@ const ALL_IDEATION_TYPES: IdeationType[] = [
'high_value_features',
'documentation_gaps',
'security_hardening',
'performance_optimizations'
'performance_optimizations',
'code_quality'
];
// Type guard functions for new types
@@ -131,6 +139,10 @@ function isSecurityHardeningIdea(idea: Idea): idea is SecurityHardeningIdea {
return idea.type === 'security_hardening';
}
function isCodeQualityIdea(idea: Idea): idea is CodeQualityIdea {
return idea.type === 'code_quality';
}
function isPerformanceOptimizationIdea(idea: Idea): idea is PerformanceOptimizationIdea {
return idea.type === 'performance_optimizations';
}
@@ -217,6 +229,11 @@ export function Ideation({ projectId }: IdeationProps) {
const [activeTab, setActiveTab] = useState<string>('all');
const [showConfigDialog, setShowConfigDialog] = useState(false);
const [showDismissed, setShowDismissed] = useState(false);
const [showEnvConfigModal, setShowEnvConfigModal] = useState(false);
const [pendingAction, setPendingAction] = useState<'generate' | 'refresh' | null>(null);
// Check if Claude token is configured
const { hasToken, isLoading: isCheckingToken, checkToken } = useClaudeTokenCheck();
// Set up IPC listeners and load ideation on mount
useEffect(() => {
@@ -225,14 +242,37 @@ export function Ideation({ projectId }: IdeationProps) {
return cleanup;
}, [projectId]);
const handleGenerate = () => {
const handleGenerate = async () => {
// Check token before generating
if (hasToken === false) {
setPendingAction('generate');
setShowEnvConfigModal(true);
return;
}
generateIdeation(projectId);
};
const handleRefresh = () => {
const handleRefresh = async () => {
// Check token before refreshing
if (hasToken === false) {
setPendingAction('refresh');
setShowEnvConfigModal(true);
return;
}
refreshIdeation(projectId);
};
// Handle when env config is complete - execute pending action
const handleEnvConfigured = () => {
checkToken(); // Re-check the token
if (pendingAction === 'generate') {
generateIdeation(projectId);
} else if (pendingAction === 'refresh') {
refreshIdeation(projectId);
}
setPendingAction(null);
};
const handleConvertToTask = async (idea: Idea) => {
const result = await window.electronAPI.convertIdeaToTask(projectId, idea.id);
if (result.success) {
@@ -315,11 +355,28 @@ export function Ideation({ projectId }: IdeationProps) {
</div>
</div>
<Button onClick={handleGenerate} size="lg">
<Button onClick={handleGenerate} size="lg" disabled={isCheckingToken}>
<Sparkles className="h-4 w-4 mr-2" />
Generate Ideas
</Button>
{/* Show warning if token is missing */}
{hasToken === false && !isCheckingToken && (
<p className="mt-3 text-sm text-muted-foreground">
<AlertCircle className="h-4 w-4 inline-block mr-1 text-warning" />
Claude token not configured. You'll be prompted to enter it when generating.
</p>
)}
</Card>
{/* Environment Configuration Modal */}
<EnvConfigModal
open={showEnvConfigModal}
onOpenChange={setShowEnvConfigModal}
onConfigured={handleEnvConfigured}
title="Claude Authentication Required"
description="A Claude Code OAuth token is required to generate AI-powered feature ideas."
/>
</div>
);
}
@@ -540,6 +597,15 @@ export function Ideation({ projectId }: IdeationProps) {
</DialogFooter>
</DialogContent>
</Dialog>
{/* Environment Configuration Modal */}
<EnvConfigModal
open={showEnvConfigModal}
onOpenChange={setShowEnvConfigModal}
onConfigured={handleEnvConfigured}
title="Claude Authentication Required"
description="A Claude Code OAuth token is required to generate AI-powered feature ideas."
/>
</div>
);
}
@@ -605,6 +671,11 @@ function IdeaCard({ idea, onClick, onConvert, onDismiss }: IdeaCardProps) {
{(idea as PerformanceOptimizationIdea).impact} impact
</Badge>
)}
{isCodeQualityIdea(idea) && (
<Badge variant="outline" className={CODE_QUALITY_SEVERITY_COLORS[(idea as CodeQualityIdea).severity]}>
{(idea as CodeQualityIdea).severity}
</Badge>
)}
</div>
<h3 className={`font-medium ${isDismissed ? 'line-through' : ''}`}>
{idea.title}
@@ -730,6 +801,10 @@ function IdeaDetailPanel({ idea, onClose, onConvert, onDismiss }: IdeaDetailPane
{isPerformanceOptimizationIdea(idea) && (
<PerformanceOptimizationDetails idea={idea as PerformanceOptimizationIdea} />
)}
{isCodeQualityIdea(idea) && (
<CodeQualityDetails idea={idea as CodeQualityIdea} />
)}
</div>
{/* Actions */}
@@ -1257,3 +1332,159 @@ function PerformanceOptimizationDetails({ idea }: { idea: PerformanceOptimizatio
</>
);
}
function CodeQualityDetails({ idea }: { idea: CodeQualityIdea }) {
return (
<>
{/* Metrics */}
<div className="grid grid-cols-2 gap-2">
<Card className="p-3 text-center">
<div className={`text-lg font-semibold ${CODE_QUALITY_SEVERITY_COLORS[idea.severity]}`}>
{idea.severity}
</div>
<div className="text-xs text-muted-foreground">Severity</div>
</Card>
<Card className="p-3 text-center">
<div className={`text-lg font-semibold ${IDEATION_EFFORT_COLORS[idea.estimatedEffort]}`}>
{idea.estimatedEffort}
</div>
<div className="text-xs text-muted-foreground">Effort</div>
</Card>
</div>
{/* Category */}
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<Code2 className="h-4 w-4" />
Category
</h3>
<Badge variant="outline">
{CODE_QUALITY_CATEGORY_LABELS[idea.category]}
</Badge>
</div>
{/* Breaking Change Warning */}
{idea.breakingChange && (
<div className="rounded-lg bg-destructive/10 border border-destructive/30 p-3">
<div className="flex items-center gap-2">
<AlertTriangle className="h-4 w-4 text-destructive" />
<span className="text-sm font-medium text-destructive">Breaking Change</span>
</div>
<p className="text-xs text-muted-foreground mt-1">
This refactoring may break existing code or tests.
</p>
</div>
)}
{/* Current State */}
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<AlertCircle className="h-4 w-4" />
Current State
</h3>
<p className="text-sm text-muted-foreground">{idea.currentState}</p>
</div>
{/* Proposed Change */}
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<TrendingUp className="h-4 w-4 text-success" />
Proposed Change
</h3>
<p className="text-sm text-muted-foreground whitespace-pre-line">{idea.proposedChange}</p>
</div>
{/* Code Example */}
{idea.codeExample && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<FileCode className="h-4 w-4" />
Code Example
</h3>
<pre className="text-xs font-mono bg-muted/50 p-3 rounded-lg overflow-x-auto">
{idea.codeExample}
</pre>
</div>
)}
{/* Metrics (if available) */}
{idea.metrics && (
<div>
<h3 className="text-sm font-medium mb-2">Metrics</h3>
<div className="grid grid-cols-2 gap-2">
{idea.metrics.lineCount && (
<Card className="p-2 text-center">
<div className="text-sm font-semibold">{idea.metrics.lineCount}</div>
<div className="text-xs text-muted-foreground">Lines</div>
</Card>
)}
{idea.metrics.complexity && (
<Card className="p-2 text-center">
<div className="text-sm font-semibold">{idea.metrics.complexity}</div>
<div className="text-xs text-muted-foreground">Complexity</div>
</Card>
)}
{idea.metrics.duplicateLines && (
<Card className="p-2 text-center">
<div className="text-sm font-semibold">{idea.metrics.duplicateLines}</div>
<div className="text-xs text-muted-foreground">Duplicate Lines</div>
</Card>
)}
{idea.metrics.testCoverage !== undefined && (
<Card className="p-2 text-center">
<div className="text-sm font-semibold">{idea.metrics.testCoverage}%</div>
<div className="text-xs text-muted-foreground">Test Coverage</div>
</Card>
)}
</div>
</div>
)}
{/* Affected Files */}
{idea.affectedFiles.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<FileCode className="h-4 w-4" />
Affected Files
</h3>
<ul className="space-y-1">
{idea.affectedFiles.map((file, i) => (
<li key={i} className="text-sm font-mono text-muted-foreground">
{file}
</li>
))}
</ul>
</div>
)}
{/* Best Practice */}
{idea.bestPractice && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<BookOpen className="h-4 w-4" />
Best Practice
</h3>
<p className="text-sm text-muted-foreground">{idea.bestPractice}</p>
</div>
)}
{/* Prerequisites */}
{idea.prerequisites && idea.prerequisites.length > 0 && (
<div>
<h3 className="text-sm font-medium mb-2 flex items-center gap-2">
<Clock className="h-4 w-4" />
Prerequisites
</h3>
<ul className="space-y-1">
{idea.prerequisites.map((prereq, i) => (
<li key={i} className="text-sm text-muted-foreground flex items-start gap-2">
<span className="text-muted-foreground">•</span>
{prereq}
</li>
))}
</ul>
</div>
)}
</>
);
}
@@ -17,7 +17,8 @@ import {
ChevronUp,
Import,
Radio,
Github
Github,
Globe
} from 'lucide-react';
import { LinearTaskImportModal } from './LinearTaskImportModal';
import {
@@ -517,18 +518,35 @@ export function ProjectSettings({ project, open, onOpenChange }: ProjectSettings
{/* Manual OAuth Token */}
<div className="space-y-2">
<Label className="text-sm font-medium text-foreground">
OAuth Token (Manual)
</Label>
<p className="text-xs text-muted-foreground">
Or paste a token directly if you have one from <code className="px-1 bg-muted rounded">claude setup-token</code>
</p>
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-foreground">
OAuth Token {envConfig.claudeTokenIsGlobal ? '(Override)' : ''}
</Label>
{envConfig.claudeTokenIsGlobal && (
<span className="flex items-center gap-1 text-xs text-info">
<Globe className="h-3 w-3" />
Using global token
</span>
)}
</div>
{envConfig.claudeTokenIsGlobal ? (
<p className="text-xs text-muted-foreground">
Using token from App Settings. Enter a project-specific token below to override.
</p>
) : (
<p className="text-xs text-muted-foreground">
Paste a token from <code className="px-1 bg-muted rounded">claude setup-token</code>
</p>
)}
<div className="relative">
<Input
type={showClaudeToken ? 'text' : 'password'}
placeholder="your-oauth-token-here"
value={envConfig.claudeOAuthToken || ''}
onChange={(e) => updateEnvConfig({ claudeOAuthToken: e.target.value })}
placeholder={envConfig.claudeTokenIsGlobal ? 'Enter to override global token...' : 'your-oauth-token-here'}
value={envConfig.claudeTokenIsGlobal ? '' : (envConfig.claudeOAuthToken || '')}
onChange={(e) => updateEnvConfig({
claudeOAuthToken: e.target.value || undefined,
// When user enters a value, it's no longer global
})}
className="pr-10"
/>
<button
@@ -943,16 +961,32 @@ export function ProjectSettings({ project, open, onOpenChange }: ProjectSettings
</div>
<div className="space-y-2">
<Label className="text-sm font-medium text-foreground">OpenAI API Key</Label>
<p className="text-xs text-muted-foreground">
Required for Graphiti embeddings
</p>
<div className="flex items-center justify-between">
<Label className="text-sm font-medium text-foreground">
OpenAI API Key {envConfig.openaiKeyIsGlobal ? '(Override)' : ''}
</Label>
{envConfig.openaiKeyIsGlobal && (
<span className="flex items-center gap-1 text-xs text-info">
<Globe className="h-3 w-3" />
Using global key
</span>
)}
</div>
{envConfig.openaiKeyIsGlobal ? (
<p className="text-xs text-muted-foreground">
Using key from App Settings. Enter a project-specific key below to override.
</p>
) : (
<p className="text-xs text-muted-foreground">
Required for Graphiti embeddings
</p>
)}
<div className="relative">
<Input
type={showOpenAIKey ? 'text' : 'password'}
placeholder="sk-xxxxxxxx"
value={envConfig.openaiApiKey || ''}
onChange={(e) => updateEnvConfig({ openaiApiKey: e.target.value })}
placeholder={envConfig.openaiKeyIsGlobal ? 'Enter to override global key...' : 'sk-xxxxxxxx'}
value={envConfig.openaiKeyIsGlobal ? '' : (envConfig.openaiApiKey || '')}
onChange={(e) => updateEnvConfig({ openaiApiKey: e.target.value || undefined })}
className="pr-10"
/>
<button
@@ -11,3 +11,4 @@ export * from './Ideation';
export * from './GitHubIssues';
export * from './Changelog';
export * from './WelcomeScreen';
export * from './EnvConfigModal';
+48 -10
View File
@@ -40,22 +40,27 @@ export const CHUNK_STATUS_COLORS: Record<string, string> = {
// Default app settings
export const DEFAULT_APP_SETTINGS = {
theme: 'system' as const,
defaultModel: 'sonnet',
defaultModel: 'opus',
defaultParallelism: 1,
pythonPath: undefined as string | undefined,
autoBuildPath: undefined as string | undefined,
autoUpdateAutoBuild: true,
notifications: {
onTaskComplete: true,
onTaskFailed: true,
onReviewNeeded: true,
sound: false
}
},
// Global API keys (used as defaults for all projects)
globalClaudeOAuthToken: undefined as string | undefined,
globalOpenAIApiKey: undefined as string | undefined
};
// Default project settings
export const DEFAULT_PROJECT_SETTINGS = {
parallelEnabled: false,
maxWorkers: 2,
model: 'sonnet',
model: 'opus',
memoryBackend: 'file' as const,
linearSync: false,
notifications: {
@@ -177,6 +182,11 @@ export const IPC_CHANNELS = {
AUTOBUILD_SOURCE_VERSION: 'autobuild:source:version',
AUTOBUILD_SOURCE_PROGRESS: 'autobuild:source:progress',
// Auto Claude source environment configuration
AUTOBUILD_SOURCE_ENV_GET: 'autobuild:source:env:get',
AUTOBUILD_SOURCE_ENV_UPDATE: 'autobuild:source:env:update',
AUTOBUILD_SOURCE_ENV_CHECK_TOKEN: 'autobuild:source:env:checkToken',
// Changelog operations
CHANGELOG_GET_DONE_TASKS: 'changelog:getDoneTasks',
CHANGELOG_LOAD_TASK_SPECS: 'changelog:loadTaskSpecs',
@@ -241,9 +251,9 @@ export const ROADMAP_IMPACT_COLORS: Record<string, string> = {
// Models available for selection
export const AVAILABLE_MODELS = [
{ value: 'sonnet', label: 'Claude Sonnet' },
{ value: 'opus', label: 'Claude Opus' },
{ value: 'haiku', label: 'Claude Haiku' }
{ value: 'opus', label: 'Claude Opus 4.5' },
{ value: 'sonnet', label: 'Claude Sonnet 4' },
{ value: 'haiku', label: 'Claude Haiku 3.5' }
] as const;
// Memory backends
@@ -263,7 +273,8 @@ export const IDEATION_TYPE_LABELS: Record<string, string> = {
high_value_features: 'High-Value Features',
documentation_gaps: 'Documentation',
security_hardening: 'Security',
performance_optimizations: 'Performance'
performance_optimizations: 'Performance',
code_quality: 'Code Quality'
};
export const IDEATION_TYPE_DESCRIPTIONS: Record<string, string> = {
@@ -272,7 +283,8 @@ export const IDEATION_TYPE_DESCRIPTIONS: Record<string, string> = {
high_value_features: 'Strategic features that provide significant value to target users',
documentation_gaps: 'Missing or outdated documentation that needs attention',
security_hardening: 'Security vulnerabilities and hardening opportunities',
performance_optimizations: 'Performance bottlenecks and optimization opportunities'
performance_optimizations: 'Performance bottlenecks and optimization opportunities',
code_quality: 'Refactoring opportunities, large files, code smells, and best practice violations'
};
// Ideation type colors
@@ -282,7 +294,8 @@ export const IDEATION_TYPE_COLORS: Record<string, string> = {
high_value_features: 'bg-primary/10 text-primary border-primary/30',
documentation_gaps: 'bg-amber-500/10 text-amber-500 border-amber-500/30',
security_hardening: 'bg-destructive/10 text-destructive border-destructive/30',
performance_optimizations: 'bg-purple-500/10 text-purple-400 border-purple-500/30'
performance_optimizations: 'bg-purple-500/10 text-purple-400 border-purple-500/30',
code_quality: 'bg-cyan-500/10 text-cyan-400 border-cyan-500/30'
};
// Ideation type icons (Lucide icon names)
@@ -292,7 +305,8 @@ export const IDEATION_TYPE_ICONS: Record<string, string> = {
high_value_features: 'Target',
documentation_gaps: 'BookOpen',
security_hardening: 'Shield',
performance_optimizations: 'Gauge'
performance_optimizations: 'Gauge',
code_quality: 'Code2'
};
// Ideation status colors
@@ -368,6 +382,30 @@ export const PERFORMANCE_CATEGORY_LABELS: Record<string, string> = {
caching: 'Caching'
};
// Code quality category labels
export const CODE_QUALITY_CATEGORY_LABELS: Record<string, string> = {
large_files: 'Large Files',
code_smells: 'Code Smells',
complexity: 'High Complexity',
duplication: 'Code Duplication',
naming: 'Naming Conventions',
structure: 'File Structure',
linting: 'Linting Issues',
testing: 'Test Coverage',
types: 'Type Safety',
dependencies: 'Dependency Issues',
dead_code: 'Dead Code',
git_hygiene: 'Git Hygiene'
};
// Code quality severity colors
export const CODE_QUALITY_SEVERITY_COLORS: Record<string, string> = {
suggestion: 'bg-info/10 text-info',
minor: 'bg-warning/10 text-warning',
major: 'bg-orange-500/10 text-orange-500',
critical: 'bg-destructive/10 text-destructive'
};
// Default ideation config
export const DEFAULT_IDEATION_CONFIG = {
enabledTypes: ['low_hanging_fruit', 'ui_ux_improvements', 'high_value_features'] as const,
+53 -2
View File
@@ -135,6 +135,9 @@ export interface AppSettings {
autoBuildPath?: string;
autoUpdateAutoBuild: boolean;
notifications: NotificationSettings;
// Global API keys (used as defaults for all projects)
globalClaudeOAuthToken?: string;
globalOpenAIApiKey?: string;
}
// Auto Claude Initialization Types
@@ -380,7 +383,8 @@ export type IdeationType =
| 'high_value_features'
| 'documentation_gaps'
| 'security_hardening'
| 'performance_optimizations';
| 'performance_optimizations'
| 'code_quality';
export type IdeationStatus = 'draft' | 'selected' | 'converted' | 'dismissed';
export type IdeationGenerationPhase = 'idle' | 'analyzing' | 'discovering' | 'generating' | 'complete' | 'error';
@@ -465,13 +469,34 @@ export interface PerformanceOptimizationIdea extends IdeaBase {
estimatedEffort: 'trivial' | 'small' | 'medium' | 'large';
}
export interface CodeQualityIdea extends IdeaBase {
type: 'code_quality';
category: 'large_files' | 'code_smells' | 'complexity' | 'duplication' | 'naming' | 'structure' | 'linting' | 'testing' | 'types' | 'dependencies' | 'dead_code' | 'git_hygiene';
severity: 'suggestion' | 'minor' | 'major' | 'critical';
affectedFiles: string[]; // Files that need refactoring
currentState: string; // Description of the current problematic state
proposedChange: string; // What should be done
codeExample?: string; // Example of problematic code (if applicable)
bestPractice?: string; // Reference to best practice being violated
metrics?: {
lineCount?: number; // For large files
complexity?: number; // Cyclomatic complexity if applicable
duplicateLines?: number; // For duplication issues
testCoverage?: number; // Current test coverage percentage
};
estimatedEffort: 'trivial' | 'small' | 'medium' | 'large';
breakingChange: boolean; // Whether this refactoring could break existing code
prerequisites?: string[]; // Things that should be done first
}
export type Idea =
| LowHangingFruitIdea
| UIUXImprovementIdea
| HighValueFeatureIdea
| DocumentationGapIdea
| SecurityHardeningIdea
| PerformanceOptimizationIdea;
| PerformanceOptimizationIdea
| CodeQualityIdea;
export interface IdeationSession {
id: string;
@@ -508,6 +533,8 @@ export interface ProjectEnvConfig {
// Claude Authentication
claudeOAuthToken?: string;
claudeAuthStatus: 'authenticated' | 'token_set' | 'not_configured';
// Indicates if the Claude token is from global settings (not project-specific)
claudeTokenIsGlobal?: boolean;
// Model Override
autoBuildModel?: string;
@@ -528,6 +555,8 @@ export interface ProjectEnvConfig {
// Graphiti Memory Integration
graphitiEnabled: boolean;
openaiApiKey?: string;
// Indicates if the OpenAI key is from global settings (not project-specific)
openaiKeyIsGlobal?: boolean;
graphitiFalkorDbHost?: string;
graphitiFalkorDbPort?: number;
graphitiFalkorDbPassword?: string;
@@ -537,6 +566,23 @@ export interface ProjectEnvConfig {
enableFancyUi: boolean;
}
// Auto-Claude Source Environment Configuration (for auto-claude repo .env)
export interface SourceEnvConfig {
// Claude Authentication (required for ideation, roadmap generation, etc.)
hasClaudeToken: boolean;
claudeOAuthToken?: string;
// Source path info
sourcePath?: string;
envExists: boolean;
}
export interface SourceEnvCheckResult {
hasToken: boolean;
sourcePath?: string;
error?: string;
}
// ============================================
// Linear Integration Types
// ============================================
@@ -921,6 +967,11 @@ export interface ElectronAPI {
callback: (progress: AutoBuildSourceUpdateProgress) => void
) => () => void;
// Auto Claude source environment operations
getSourceEnv: () => Promise<IPCResult<SourceEnvConfig>>;
updateSourceEnv: (config: { claudeOAuthToken?: string }) => Promise<IPCResult>;
checkSourceToken: () => Promise<IPCResult<SourceEnvCheckResult>>;
// Changelog operations
getChangelogDoneTasks: (projectId: string) => Promise<IPCResult<ChangelogTask[]>>;
loadTaskSpecs: (projectId: string, taskIds: string[]) => Promise<IPCResult<TaskSpecContent[]>>;
+168
View File
@@ -0,0 +1,168 @@
{
"base_commands": [
".",
"[",
"[[",
"ag",
"awk",
"basename",
"bash",
"bc",
"break",
"cat",
"cd",
"chmod",
"clear",
"cmp",
"column",
"comm",
"command",
"continue",
"cp",
"curl",
"cut",
"date",
"df",
"diff",
"dig",
"dirname",
"du",
"echo",
"egrep",
"env",
"eval",
"exec",
"exit",
"expand",
"export",
"expr",
"false",
"fd",
"fgrep",
"file",
"find",
"fmt",
"fold",
"gawk",
"gh",
"git",
"grep",
"gunzip",
"gzip",
"head",
"help",
"host",
"iconv",
"id",
"jobs",
"join",
"jq",
"kill",
"killall",
"less",
"let",
"ln",
"ls",
"lsof",
"man",
"mkdir",
"mktemp",
"more",
"mv",
"nl",
"paste",
"pgrep",
"ping",
"pkill",
"popd",
"printenv",
"printf",
"ps",
"pushd",
"pwd",
"read",
"readlink",
"realpath",
"reset",
"return",
"rev",
"rg",
"rm",
"rmdir",
"sed",
"seq",
"set",
"sh",
"shuf",
"sleep",
"sort",
"source",
"split",
"stat",
"tail",
"tar",
"tee",
"test",
"time",
"timeout",
"touch",
"tr",
"tree",
"true",
"type",
"uname",
"unexpand",
"uniq",
"unset",
"unzip",
"watch",
"wc",
"wget",
"whereis",
"which",
"whoami",
"xargs",
"yes",
"yq",
"zip",
"zsh"
],
"stack_commands": [
"ipython",
"jupyter",
"notebook",
"pdb",
"pip",
"pip3",
"pipx",
"pudb",
"python",
"python3"
],
"script_commands": [],
"custom_commands": [],
"detected_stack": {
"languages": [
"python"
],
"package_managers": [
"pip"
],
"frameworks": [],
"databases": [],
"infrastructure": [],
"cloud_providers": [],
"code_quality_tools": [],
"version_managers": []
},
"custom_scripts": {
"npm_scripts": [],
"make_targets": [],
"poetry_scripts": [],
"cargo_aliases": [],
"shell_scripts": []
},
"project_dir": "/Users/andremikalsen/Documents/Coding/autonomous-coding/auto-claude",
"created_at": "2025-12-11T21:19:19.074875",
"project_hash": "ef656cc9bb3236b96381a0dd319201e9"
}
+3
View File
@@ -64,6 +64,7 @@ IDEATION_TYPES = [
"documentation_gaps",
"security_hardening",
"performance_optimizations",
"code_quality",
]
IDEATION_TYPE_LABELS = {
@@ -73,6 +74,7 @@ IDEATION_TYPE_LABELS = {
"documentation_gaps": "Documentation Gaps",
"security_hardening": "Security Hardening",
"performance_optimizations": "Performance Optimizations",
"code_quality": "Code Quality & Refactoring",
}
IDEATION_TYPE_PROMPTS = {
@@ -82,6 +84,7 @@ IDEATION_TYPE_PROMPTS = {
"documentation_gaps": "ideation_documentation.md",
"security_hardening": "ideation_security.md",
"performance_optimizations": "ideation_performance.md",
"code_quality": "ideation_code_quality.md",
}
@@ -0,0 +1,275 @@
# Code Quality & Refactoring Ideation Agent
You are a senior software architect and code quality expert. Your task is to analyze a codebase and identify refactoring opportunities, code smells, best practice violations, and areas that could benefit from improved code quality.
## Context
You have access to:
- Project index with file structure and file sizes
- Source code across the project
- Package manifest (package.json, requirements.txt, etc.)
- Configuration files (ESLint, Prettier, tsconfig, etc.)
- Git history (if available)
- Memory context from previous sessions (if available)
## Your Mission
Identify code quality issues across these categories:
### 1. Large Files
- Files exceeding 500-800 lines that should be split
- Component files over 400 lines
- Monolithic components/modules
- "God objects" with too many responsibilities
- Single files handling multiple concerns
### 2. Code Smells
- Duplicated code blocks
- Long methods/functions (>50 lines)
- Deep nesting (>3 levels)
- Too many parameters (>4)
- Primitive obsession
- Feature envy
- Inappropriate intimacy between modules
### 3. High Complexity
- Cyclomatic complexity issues
- Complex conditionals that need simplification
- Overly clever code that's hard to understand
- Functions doing too many things
### 4. Code Duplication
- Copy-pasted code blocks
- Similar logic that could be abstracted
- Repeated patterns that should be utilities
- Near-duplicate components
### 5. Naming Conventions
- Inconsistent naming styles
- Unclear/cryptic variable names
- Abbreviations that hurt readability
- Names that don't reflect purpose
### 6. File Structure
- Poor folder organization
- Inconsistent module boundaries
- Circular dependencies
- Misplaced files
- Missing index/barrel files
### 7. Linting Issues
- Missing ESLint/Prettier configuration
- Inconsistent code formatting
- Unused variables/imports
- Missing or inconsistent rules
### 8. Test Coverage
- Missing unit tests for critical logic
- Components without test files
- Untested edge cases
- Missing integration tests
### 9. Type Safety
- Missing TypeScript types
- Excessive `any` usage
- Incomplete type definitions
- Runtime type mismatches
### 10. Dependency Issues
- Unused dependencies
- Duplicate dependencies
- Outdated dev tooling
- Missing peer dependencies
### 11. Dead Code
- Unused functions/components
- Commented-out code blocks
- Unreachable code paths
- Deprecated features not removed
### 12. Git Hygiene
- Large commits that should be split
- Missing commit message standards
- Lack of branch naming conventions
- Missing pre-commit hooks
## Analysis Process
1. **File Size Analysis**
- Identify files over 500-800 lines (context-dependent)
- Find components with too many exports
- Check for monolithic modules
2. **Pattern Detection**
- Search for duplicated code blocks
- Find similar function signatures
- Identify repeated error handling patterns
3. **Complexity Metrics**
- Estimate cyclomatic complexity
- Count nesting levels
- Measure function lengths
4. **Config Review**
- Check for linting configuration
- Review TypeScript strictness
- Assess test setup
5. **Structure Analysis**
- Map module dependencies
- Check for circular imports
- Review folder organization
## Output Format
Write your findings to `{output_dir}/code_quality_ideas.json`:
```json
{
"ideas": [
{
"id": "cq-001",
"type": "code_quality",
"title": "Split large API handler file into domain modules",
"description": "The file src/api/handlers.ts has grown to 1200 lines and handles multiple unrelated domains (users, products, orders). This violates single responsibility and makes the code hard to navigate and maintain.",
"rationale": "Very large files increase cognitive load, make code reviews harder, and often lead to merge conflicts. Smaller, focused modules are easier to test, maintain, and reason about.",
"category": "large_files",
"severity": "major",
"affectedFiles": ["src/api/handlers.ts"],
"currentState": "Single 1200-line file handling users, products, and orders API logic",
"proposedChange": "Split into src/api/users/handlers.ts, src/api/products/handlers.ts, src/api/orders/handlers.ts with shared utilities in src/api/utils/",
"codeExample": "// Current:\nexport function handleUserCreate() { ... }\nexport function handleProductList() { ... }\nexport function handleOrderSubmit() { ... }\n\n// Proposed:\n// users/handlers.ts\nexport function handleCreate() { ... }",
"bestPractice": "Single Responsibility Principle - each module should have one reason to change",
"metrics": {
"lineCount": 1200,
"complexity": null,
"duplicateLines": null,
"testCoverage": null
},
"estimatedEffort": "medium",
"breakingChange": false,
"prerequisites": ["Ensure test coverage before refactoring"]
},
{
"id": "cq-002",
"type": "code_quality",
"title": "Extract duplicated form validation logic",
"description": "Similar validation logic is duplicated across 5 form components. Each validates email, phone, and required fields with slightly different implementations.",
"rationale": "Code duplication leads to bugs when fixes are applied inconsistently and increases maintenance burden.",
"category": "duplication",
"severity": "minor",
"affectedFiles": [
"src/components/UserForm.tsx",
"src/components/ContactForm.tsx",
"src/components/SignupForm.tsx",
"src/components/ProfileForm.tsx",
"src/components/CheckoutForm.tsx"
],
"currentState": "5 forms each implementing their own validation with 15-20 lines of similar code",
"proposedChange": "Create src/lib/validation.ts with reusable validators (validateEmail, validatePhone, validateRequired) and a useFormValidation hook",
"codeExample": "// Current (repeated in 5 files):\nconst validateEmail = (v) => /^[^@]+@[^@]+\\.[^@]+$/.test(v);\n\n// Proposed:\nimport { validators, useFormValidation } from '@/lib/validation';\nconst { errors, validate } = useFormValidation({\n email: validators.email,\n phone: validators.phone\n});",
"bestPractice": "DRY (Don't Repeat Yourself) - extract common logic into reusable utilities",
"metrics": {
"lineCount": null,
"complexity": null,
"duplicateLines": 85,
"testCoverage": null
},
"estimatedEffort": "small",
"breakingChange": false,
"prerequisites": null
}
],
"metadata": {
"filesAnalyzed": 156,
"largeFilesFound": 8,
"duplicateBlocksFound": 12,
"lintingConfigured": true,
"testsPresent": true,
"generatedAt": "2024-12-11T10:00:00Z"
}
}
```
## Severity Classification
| Severity | Description | Examples |
|----------|-------------|----------|
| critical | Blocks development, causes bugs | Circular deps, type errors |
| major | Significant maintainability impact | Large files, high complexity |
| minor | Should be addressed but not urgent | Duplication, naming issues |
| suggestion | Nice to have improvements | Style consistency, docs |
## Guidelines
- **Prioritize Impact**: Focus on issues that most affect maintainability and developer experience
- **Provide Clear Refactoring Steps**: Each finding should include how to fix it
- **Consider Breaking Changes**: Flag refactorings that might break existing code or tests
- **Identify Prerequisites**: Note if something else should be done first
- **Be Realistic About Effort**: Accurately estimate the work required
- **Include Code Examples**: Show before/after when helpful
- **Consider Trade-offs**: Sometimes "imperfect" code is acceptable for good reasons
## Categories Explained
| Category | Focus | Common Issues |
|----------|-------|---------------|
| large_files | File size & scope | >300 line files, monoliths |
| code_smells | Design problems | Long methods, deep nesting |
| complexity | Cognitive load | Complex conditionals, many branches |
| duplication | Repeated code | Copy-paste, similar patterns |
| naming | Readability | Unclear names, inconsistency |
| structure | Organization | Folder structure, circular deps |
| linting | Code style | Missing config, inconsistent format |
| testing | Test coverage | Missing tests, uncovered paths |
| types | Type safety | Missing types, excessive `any` |
| dependencies | Package management | Unused, outdated, duplicates |
| dead_code | Unused code | Commented code, unreachable paths |
| git_hygiene | Version control | Commit practices, hooks |
## Common Patterns to Flag
### Large File Indicators
```
# Files to investigate (use judgment - context matters)
- Component files > 400-500 lines
- Utility/service files > 600-800 lines
- Test files > 800 lines (often acceptable if well-organized)
- Single-purpose modules > 1000 lines (definite split candidate)
```
### Code Smell Patterns
```javascript
// Long parameter list (>4 params)
function createUser(name, email, phone, address, city, state, zip, country) { }
// Deep nesting (>3 levels)
if (a) { if (b) { if (c) { if (d) { ... } } } }
// Feature envy - method uses more from another class
class Order {
getCustomerDiscount() {
return this.customer.level * this.customer.years * this.customer.purchases;
}
}
```
### Duplication Signals
```javascript
// Near-identical functions
function validateUserEmail(email) { return /regex/.test(email); }
function validateContactEmail(email) { return /regex/.test(email); }
function validateOrderEmail(email) { return /regex/.test(email); }
```
### Type Safety Issues
```typescript
// Excessive any usage
const data: any = fetchData();
const result: any = process(data as any);
// Missing return types
function calculate(a, b) { return a + b; } // Should have : number
```
Remember: Code quality improvements should make code easier to understand, test, and maintain. Focus on changes that provide real value to the development team, not arbitrary rules.
+441
View File
@@ -0,0 +1,441 @@
#!/usr/bin/env python3
"""
Test Script for Graphiti Memory Integration
============================================
This script tests the hybrid memory layer (graph + semantic search) to verify
data is being saved and retrieved correctly from FalkorDB.
Usage:
# Set environment variables first (or in .env file):
export GRAPHITI_ENABLED=true
export OPENAI_API_KEY=sk-...
export GRAPHITI_FALKORDB_HOST=localhost
export GRAPHITI_FALKORDB_PORT=6380
# Run the test:
python auto-claude/test_graphiti_memory.py
"""
import asyncio
import json
import os
import sys
from datetime import datetime, timezone
from pathlib import Path
# Add auto-claude to path
sys.path.insert(0, str(Path(__file__).parent))
# Load .env file
from dotenv import load_dotenv
env_file = Path(__file__).parent / ".env"
if env_file.exists():
load_dotenv(env_file)
print(f"Loaded .env from {env_file}")
from graphiti_config import (
GraphitiConfig,
is_graphiti_enabled,
get_graphiti_status,
)
def print_header(title: str):
"""Print a section header."""
print("\n" + "=" * 60)
print(f" {title}")
print("=" * 60 + "\n")
def print_result(label: str, value: str, success: bool = True):
"""Print a result line."""
status = "✅" if success else "❌"
print(f" {status} {label}: {value}")
async def test_connection():
"""Test basic FalkorDB connection."""
print_header("1. Testing FalkorDB Connection")
config = GraphitiConfig.from_env()
print(f" Host: {config.falkordb_host}")
print(f" Port: {config.falkordb_port}")
print(f" Database: {config.database}")
print()
try:
from graphiti_core import Graphiti
from graphiti_core.driver.falkordb_driver import FalkorDriver
# Try to connect
driver = FalkorDriver(
host=config.falkordb_host,
port=config.falkordb_port,
password=config.falkordb_password or None,
database=config.database,
)
graphiti = Graphiti(graph_driver=driver)
# Try building indices
print(" Building indices...")
await graphiti.build_indices_and_constraints()
print_result("Connection", "SUCCESS", True)
await graphiti.close()
return True
except Exception as e:
print_result("Connection", f"FAILED: {e}", False)
return False
async def test_save_episode():
"""Test saving an episode to the graph."""
print_header("2. Testing Episode Save")
config = GraphitiConfig.from_env()
try:
from graphiti_core import Graphiti
from graphiti_core.driver.falkordb_driver import FalkorDriver
from graphiti_core.nodes import EpisodeType
# Connect
driver = FalkorDriver(
host=config.falkordb_host,
port=config.falkordb_port,
password=config.falkordb_password or None,
database=config.database,
)
graphiti = Graphiti(graph_driver=driver)
await graphiti.build_indices_and_constraints()
# Create test episode
test_data = {
"type": "test_episode",
"timestamp": datetime.now(timezone.utc).isoformat(),
"test_field": "Hello from test script!",
"test_number": 42,
"test_list": ["item1", "item2", "item3"],
}
episode_name = f"test_episode_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
group_id = "graphiti_test_group"
print(f" Episode name: {episode_name}")
print(f" Group ID: {group_id}")
print(f" Data: {json.dumps(test_data, indent=4)}")
print()
# Save the episode
print(" Saving episode...")
await graphiti.add_episode(
name=episode_name,
episode_body=json.dumps(test_data),
source=EpisodeType.text,
source_description="Test episode from test_graphiti_memory.py",
reference_time=datetime.now(timezone.utc),
group_id=group_id,
)
print_result("Episode Save", "SUCCESS", True)
await graphiti.close()
return episode_name, group_id
except Exception as e:
print_result("Episode Save", f"FAILED: {e}", False)
import traceback
traceback.print_exc()
return None, None
async def test_search(group_id: str):
"""Test semantic search."""
print_header("3. Testing Semantic Search")
if not group_id:
print(" ⚠️ Skipping - no group_id from previous test")
return
config = GraphitiConfig.from_env()
try:
from graphiti_core import Graphiti
from graphiti_core.driver.falkordb_driver import FalkorDriver
# Connect
driver = FalkorDriver(
host=config.falkordb_host,
port=config.falkordb_port,
password=config.falkordb_password or None,
database=config.database,
)
graphiti = Graphiti(graph_driver=driver)
# Search for the test data
query = "test episode hello"
print(f" Query: \"{query}\"")
print(f" Group ID: {group_id}")
print()
print(" Searching...")
results = await graphiti.search(
query=query,
group_ids=[group_id],
num_results=10,
)
print(f" Found {len(results)} results:")
for i, result in enumerate(results):
print(f"\n Result {i+1}:")
# Print available attributes
for attr in ['fact', 'content', 'uuid', 'name', 'score']:
if hasattr(result, attr):
val = getattr(result, attr)
if val:
print(f" {attr}: {str(val)[:100]}...")
if results:
print_result("Search", f"SUCCESS - Found {len(results)} results", True)
else:
print_result("Search", "WARNING - No results found", False)
await graphiti.close()
except Exception as e:
print_result("Search", f"FAILED: {e}", False)
import traceback
traceback.print_exc()
async def test_graphiti_memory_class():
"""Test the GraphitiMemory wrapper class."""
print_header("4. Testing GraphitiMemory Class")
try:
from graphiti_memory import GraphitiMemory
# Create a temporary spec directory for testing
test_spec_dir = Path("/tmp/graphiti_test_spec")
test_spec_dir.mkdir(parents=True, exist_ok=True)
test_project_dir = Path("/tmp/graphiti_test_project")
test_project_dir.mkdir(parents=True, exist_ok=True)
print(f" Spec dir: {test_spec_dir}")
print(f" Project dir: {test_project_dir}")
print()
# Create memory instance
memory = GraphitiMemory(test_spec_dir, test_project_dir)
print(f" Is enabled: {memory.is_enabled}")
print(f" Group ID: {memory.group_id}")
print()
if not memory.is_enabled:
print_result("GraphitiMemory", "Graphiti not enabled/configured", False)
return
# Initialize
print(" Initializing...")
init_result = await memory.initialize()
print(f" Initialized: {init_result}")
if not init_result:
print_result("GraphitiMemory Init", "Failed to initialize", False)
return
# Test save_session_insights
print("\n Testing save_session_insights...")
insights = {
"chunks_completed": ["chunk-test-1", "chunk-test-2"],
"discoveries": {
"files_understood": {"test.py": "Test file purpose"},
"patterns_found": ["Pattern: Using async/await"],
"gotchas_encountered": ["Gotcha: Need to handle edge case"],
},
"what_worked": ["Using the GraphitiMemory class"],
"what_failed": [],
"recommendations_for_next_session": ["Continue testing"],
}
save_result = await memory.save_session_insights(session_num=1, insights=insights)
print_result("save_session_insights", "SUCCESS" if save_result else "FAILED", save_result)
# Test save_pattern
print("\n Testing save_pattern...")
pattern_result = await memory.save_pattern("Test pattern: Always validate inputs before processing")
print_result("save_pattern", "SUCCESS" if pattern_result else "FAILED", pattern_result)
# Test save_gotcha
print("\n Testing save_gotcha...")
gotcha_result = await memory.save_gotcha("Gotcha: FalkorDB requires Redis protocol on port 6380")
print_result("save_gotcha", "SUCCESS" if gotcha_result else "FAILED", gotcha_result)
# Test save_codebase_discoveries
print("\n Testing save_codebase_discoveries...")
discoveries = {
"graphiti_memory.py": "Manages graph-based persistent memory",
"graphiti_config.py": "Configuration for FalkorDB connection",
}
discovery_result = await memory.save_codebase_discoveries(discoveries)
print_result("save_codebase_discoveries", "SUCCESS" if discovery_result else "FAILED", discovery_result)
# Test get_relevant_context (semantic search)
print("\n Testing get_relevant_context (waiting for embedding processing)...")
await asyncio.sleep(2) # Give time for embeddings
context = await memory.get_relevant_context("graphiti memory session insights")
print(f" Found {len(context)} context items:")
for item in context[:3]:
print(f" - Type: {item.get('type', 'unknown')}")
print(f" Content: {str(item.get('content', ''))[:80]}...")
print_result("get_relevant_context", f"Found {len(context)} items", len(context) > 0)
# Get status summary
print("\n Status summary:")
status = memory.get_status_summary()
for key, value in status.items():
print(f" {key}: {value}")
await memory.close()
print_result("GraphitiMemory", "All tests completed", True)
except ImportError as e:
print_result("GraphitiMemory", f"Import error: {e}", False)
except Exception as e:
print_result("GraphitiMemory", f"FAILED: {e}", False)
import traceback
traceback.print_exc()
async def test_raw_falkordb():
"""Test raw FalkorDB operations to see what's in the database."""
print_header("5. Raw FalkorDB Query (Debug)")
config = GraphitiConfig.from_env()
try:
import redis
from falkordb import FalkorDB
# Connect using FalkorDB client
db = FalkorDB(
host=config.falkordb_host,
port=config.falkordb_port,
password=config.falkordb_password or None,
)
# List all graphs
graphs = db.list_graphs()
print(f" Available graphs: {graphs}")
# Query the main graph
graph_name = config.database
print(f"\n Querying graph: {graph_name}")
graph = db.select_graph(graph_name)
# Count nodes
result = graph.query("MATCH (n) RETURN count(n) as count")
node_count = result.result_set[0][0] if result.result_set else 0
print(f" Total nodes: {node_count}")
# Count edges
result = graph.query("MATCH ()-[r]->() RETURN count(r) as count")
edge_count = result.result_set[0][0] if result.result_set else 0
print(f" Total edges: {edge_count}")
# Get node labels
result = graph.query("MATCH (n) RETURN DISTINCT labels(n)")
labels = [r[0] for r in result.result_set] if result.result_set else []
print(f" Node labels: {labels}")
# Get sample nodes
print("\n Sample nodes:")
result = graph.query("MATCH (n) RETURN n LIMIT 5")
for i, row in enumerate(result.result_set or []):
print(f" {i+1}. {row}")
# Get episode nodes specifically
print("\n Episode nodes:")
result = graph.query("MATCH (n:Episode) RETURN n.name, n.source_description LIMIT 5")
for i, row in enumerate(result.result_set or []):
print(f" {i+1}. name={row[0]}, desc={row[1]}")
# Get entity nodes
print("\n Entity nodes:")
result = graph.query("MATCH (n:Entity) RETURN n.name, n.summary LIMIT 5")
for i, row in enumerate(result.result_set or []):
print(f" {i+1}. name={row[0]}, summary={str(row[1])[:50]}...")
print_result("Raw FalkorDB", f"Graph has {node_count} nodes, {edge_count} edges", True)
except ImportError as e:
print_result("Raw FalkorDB", f"Import error (install falkordb): {e}", False)
except Exception as e:
print_result("Raw FalkorDB", f"FAILED: {e}", False)
import traceback
traceback.print_exc()
async def main():
"""Run all tests."""
print("\n" + "=" * 60)
print(" GRAPHITI MEMORY TEST SUITE")
print("=" * 60)
# Check configuration first
print_header("0. Configuration Check")
config = GraphitiConfig.from_env()
status = get_graphiti_status()
print_result("GRAPHITI_ENABLED", str(config.enabled), config.enabled)
print_result("OPENAI_API_KEY set", "Yes" if config.openai_api_key else "No", bool(config.openai_api_key))
print_result("FalkorDB host", config.falkordb_host, True)
print_result("FalkorDB port", str(config.falkordb_port), True)
print_result("Database", config.database, True)
print_result("is_graphiti_enabled()", str(is_graphiti_enabled()), is_graphiti_enabled())
if not is_graphiti_enabled():
print("\n ⚠️ Graphiti is not enabled!")
print(" Make sure to set these environment variables:")
print(" export GRAPHITI_ENABLED=true")
print(" export OPENAI_API_KEY=sk-...")
print()
if status.get("reason"):
print(f" Reason: {status['reason']}")
return
# Run tests
conn_ok = await test_connection()
if conn_ok:
episode_name, group_id = await test_save_episode()
# Wait a bit for embeddings to process
if episode_name:
print("\n Waiting 3 seconds for embedding processing...")
await asyncio.sleep(3)
await test_search(group_id)
await test_graphiti_memory_class()
await test_raw_falkordb()
print_header("TEST SUMMARY")
print(" Tests completed. Check the results above for any failures.")
print()
if __name__ == "__main__":
asyncio.run(main())