Memory test and global enviroment variables
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
// ============================================
|
||||
|
||||
@@ -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
|
||||
// ============================================
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[]>>;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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())
|
||||
Reference in New Issue
Block a user