feat: introduce task archiving and Graphiti MCP server integration
- Added functionality to archive and unarchive tasks, allowing users to manage task visibility effectively. - Implemented Graphiti MCP server integration, enabling agents to access and manipulate knowledge graphs directly. - Enhanced project settings to include options for enabling the Graphiti MCP server and configuring its URL. - Updated the Kanban board to filter and display archived tasks, improving task management. - Introduced a changelog version suggestion feature based on task changes, streamlining release note generation. These updates enhance task management capabilities and provide a more robust integration with knowledge graph functionalities.
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
## What's New in v1.1.0
|
||||
|
||||
### New Features
|
||||
- **Follow-up Tasks**: Continue working on completed specs by adding new tasks to existing implementations. The system automatically re-enters planning mode and integrates with your existing documentation and context.
|
||||
- **Screenshot Support for Feedback**: Attach screenshots to your change requests when reviewing tasks, providing visual context for your feedback alongside text comments.
|
||||
- **Unified Task Editing**: The Edit Task dialog now includes all the same options as the New Task dialog—classification metadata, image attachments, and review settings—giving you full control when modifying tasks.
|
||||
|
||||
### Improvements
|
||||
- **Enhanced Kanban Board**: Improved visual design and interaction patterns for task cards, making it easier to scan status, understand progress, and work with tasks efficiently.
|
||||
- **Screenshot Handling**: Paste screenshots directly into task descriptions using Ctrl+V (Cmd+V on Mac) for faster documentation.
|
||||
- **Draft Auto-Save**: Task creation state is now automatically saved when you navigate away, preventing accidental loss of work-in-progress.
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed task editing to support the same comprehensive options available in new task creation
|
||||
@@ -3,6 +3,7 @@ import { EventEmitter } from 'events';
|
||||
import path from 'path';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { app } from 'electron';
|
||||
import { projectStore } from './project-store';
|
||||
|
||||
interface AgentProcess {
|
||||
taskId: string;
|
||||
@@ -79,6 +80,27 @@ export class AgentManager extends EventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get project-specific environment variables based on project settings
|
||||
*/
|
||||
private getProjectEnvVars(projectPath: string): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
|
||||
// Find project by path
|
||||
const projects = projectStore.getProjects();
|
||||
const project = projects.find((p) => p.path === projectPath);
|
||||
|
||||
if (project?.settings) {
|
||||
// Graphiti MCP integration
|
||||
if (project.settings.graphitiMcpEnabled) {
|
||||
const graphitiUrl = project.settings.graphitiMcpUrl || 'http://localhost:8000/mcp/';
|
||||
env['GRAPHITI_MCP_URL'] = graphitiUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load environment variables from auto-claude .env file
|
||||
*/
|
||||
@@ -154,8 +176,10 @@ export class AgentManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load environment variables from auto-claude .env file
|
||||
// Load environment variables from auto-claude .env file and project settings
|
||||
const autoBuildEnv = this.loadAutoBuildEnv();
|
||||
const projectEnv = this.getProjectEnvVars(projectPath);
|
||||
const combinedEnv = { ...autoBuildEnv, ...projectEnv };
|
||||
|
||||
// spec_runner.py will auto-start run.py after spec creation completes
|
||||
const args = [specRunnerPath, '--task', taskDescription, '--project-dir', projectPath];
|
||||
@@ -181,7 +205,7 @@ export class AgentManager extends EventEmitter {
|
||||
|
||||
// Note: This is spec-creation but it chains to task-execution via run.py
|
||||
// So we treat the whole thing as task-execution for status purposes
|
||||
this.spawnProcess(taskId, autoBuildSource, args, autoBuildEnv, 'task-execution');
|
||||
this.spawnProcess(taskId, autoBuildSource, args, combinedEnv, 'task-execution');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -212,8 +236,10 @@ export class AgentManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load environment variables from auto-claude .env file
|
||||
// Load environment variables from auto-claude .env file and project settings
|
||||
const autoBuildEnv = this.loadAutoBuildEnv();
|
||||
const projectEnv = this.getProjectEnvVars(projectPath);
|
||||
const combinedEnv = { ...autoBuildEnv, ...projectEnv };
|
||||
|
||||
const args = [runPath, '--spec', specId, '--project-dir', projectPath];
|
||||
|
||||
@@ -235,7 +261,7 @@ export class AgentManager extends EventEmitter {
|
||||
}
|
||||
|
||||
console.log('[AgentManager] Spawning process with args:', args);
|
||||
this.spawnProcess(taskId, autoBuildSource, args, autoBuildEnv, 'task-execution');
|
||||
this.spawnProcess(taskId, autoBuildSource, args, combinedEnv, 'task-execution');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -262,8 +288,10 @@ export class AgentManager extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load environment variables from auto-claude .env file
|
||||
// Load environment variables from auto-claude .env file and project settings
|
||||
const autoBuildEnv = this.loadAutoBuildEnv();
|
||||
const projectEnv = this.getProjectEnvVars(projectPath);
|
||||
const combinedEnv = { ...autoBuildEnv, ...projectEnv };
|
||||
|
||||
const args = [runPath, '--spec', specId, '--project-dir', projectPath, '--qa'];
|
||||
|
||||
@@ -272,7 +300,7 @@ export class AgentManager extends EventEmitter {
|
||||
args.push('--dev');
|
||||
}
|
||||
|
||||
this.spawnProcess(taskId, autoBuildSource, args, autoBuildEnv, 'qa-process');
|
||||
this.spawnProcess(taskId, autoBuildSource, args, combinedEnv, 'qa-process');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -386,14 +414,16 @@ export class AgentManager extends EventEmitter {
|
||||
const autoBuildSource = this.getAutoBuildSourcePath();
|
||||
const cwd = autoBuildSource || process.cwd();
|
||||
|
||||
// Load environment variables from auto-claude .env file
|
||||
// Load environment variables from auto-claude .env file and project settings
|
||||
const autoBuildEnv = this.loadAutoBuildEnv();
|
||||
const projectEnv = this.getProjectEnvVars(projectPath);
|
||||
const combinedEnv = { ...autoBuildEnv, ...projectEnv };
|
||||
|
||||
const childProcess = spawn(this.pythonPath, args, {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
...autoBuildEnv, // Include auto-claude .env variables (like CLAUDE_CODE_OAUTH_TOKEN)
|
||||
...combinedEnv, // Include auto-claude .env variables and project-specific env vars
|
||||
PYTHONUNBUFFERED: '1'
|
||||
}
|
||||
});
|
||||
@@ -558,7 +588,7 @@ export class AgentManager extends EventEmitter {
|
||||
*/
|
||||
private spawnRoadmapProcess(
|
||||
projectId: string,
|
||||
_projectPath: string,
|
||||
projectPath: string,
|
||||
args: string[]
|
||||
): void {
|
||||
// Kill existing process for this project if any
|
||||
@@ -568,14 +598,16 @@ export class AgentManager extends EventEmitter {
|
||||
const autoBuildSource = this.getAutoBuildSourcePath();
|
||||
const cwd = autoBuildSource || process.cwd();
|
||||
|
||||
// Load environment variables from auto-claude .env file
|
||||
// Load environment variables from auto-claude .env file and project settings
|
||||
const autoBuildEnv = this.loadAutoBuildEnv();
|
||||
const projectEnv = this.getProjectEnvVars(projectPath);
|
||||
const combinedEnv = { ...autoBuildEnv, ...projectEnv };
|
||||
|
||||
const childProcess = spawn(this.pythonPath, args, {
|
||||
cwd,
|
||||
env: {
|
||||
...process.env,
|
||||
...autoBuildEnv, // Include auto-claude .env variables (like CLAUDE_CODE_OAUTH_TOKEN)
|
||||
...combinedEnv, // Include auto-claude .env variables and project-specific env vars
|
||||
PYTHONUNBUFFERED: '1'
|
||||
}
|
||||
});
|
||||
|
||||
@@ -177,7 +177,7 @@ export class ChangelogService extends EventEmitter {
|
||||
const specsDir = path.join(projectPath, specsBaseDir || AUTO_BUILD_PATHS.SPECS_DIR);
|
||||
|
||||
return tasks
|
||||
.filter(task => task.status === 'done')
|
||||
.filter(task => task.status === 'done' && !task.metadata?.archivedAt)
|
||||
.map(task => {
|
||||
const specDir = path.join(specsDir, task.specId);
|
||||
const hasSpecs = existsSync(specDir) && existsSync(path.join(specDir, AUTO_BUILD_PATHS.SPEC_FILE));
|
||||
@@ -548,7 +548,7 @@ ${taskSummaries}
|
||||
|
||||
${request.customInstructions ? `Note: ${request.customInstructions}` : ''}
|
||||
|
||||
Generate the changelog now. Output ONLY the formatted changelog, no preamble.`;
|
||||
CRITICAL: Output ONLY the raw changelog content. Do NOT include ANY introductory text, analysis, or explanation. Start directly with the changelog heading (## or #). No "Here's the changelog" or similar phrases.`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -567,37 +567,23 @@ Generate the changelog now. Output ONLY the formatted changelog, no preamble.`;
|
||||
return `
|
||||
import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
print("SCRIPT_START", file=sys.stderr, flush=True)
|
||||
print(f"HOME={os.environ.get('HOME')}", file=sys.stderr, flush=True)
|
||||
print(f"Claude path: ${escapedClaudePath}", file=sys.stderr, flush=True)
|
||||
|
||||
prompt = """${escapedPrompt}"""
|
||||
|
||||
print(f"Prompt length: {len(prompt)}", file=sys.stderr, flush=True)
|
||||
print("Starting subprocess.run...", file=sys.stderr, flush=True)
|
||||
|
||||
# Use Claude Code CLI to generate
|
||||
# --max-turns 1: Single response (no back-and-forth needed)
|
||||
# --model haiku: Faster model for simple text generation
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['${escapedClaudePath}', '-p', prompt, '--output-format', 'text', '--max-turns', '1', '--model', 'haiku'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=180
|
||||
)
|
||||
print(f"subprocess.run completed with code {result.returncode}", file=sys.stderr, flush=True)
|
||||
# stdin=DEVNULL prevents hanging when claude checks for interactive input
|
||||
result = subprocess.run(
|
||||
['${escapedClaudePath}', '-p', prompt, '--output-format', 'text', '--model', 'haiku'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
stdin=subprocess.DEVNULL,
|
||||
timeout=300
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
print(result.stdout)
|
||||
else:
|
||||
print(f"STDERR from claude: {result.stderr}", file=sys.stderr, flush=True)
|
||||
print(result.stderr, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"Exception: {type(e).__name__}: {e}", file=sys.stderr, flush=True)
|
||||
if result.returncode == 0:
|
||||
print(result.stdout)
|
||||
else:
|
||||
print(result.stderr, file=sys.stderr)
|
||||
sys.exit(1)
|
||||
`;
|
||||
}
|
||||
@@ -610,11 +596,34 @@ except Exception as e:
|
||||
// Clean up any potential wrapper text
|
||||
let changelog = output.trim();
|
||||
|
||||
// Remove any "Here's the changelog:" or similar prefixes
|
||||
// Find where the actual changelog starts (look for markdown heading)
|
||||
// This handles cases where AI includes preamble like "I'll analyze..." or "Here's the changelog:"
|
||||
const changelogStartPatterns = [
|
||||
/^(##\s*\[[\d.]+\])/m, // Keep-a-changelog: ## [1.0.0]
|
||||
/^(##\s*What['']?s\s+New)/im, // GitHub release: ## What's New
|
||||
/^(#\s*Release\s+v?[\d.]+)/im, // Simple: # Release v1.0.0
|
||||
/^(#\s*Changelog)/im, // # Changelog
|
||||
/^(##\s*v?[\d.]+)/m // ## v1.0.0 or ## 1.0.0
|
||||
];
|
||||
|
||||
for (const pattern of changelogStartPatterns) {
|
||||
const match = changelog.match(pattern);
|
||||
if (match && match.index !== undefined) {
|
||||
// Found a changelog heading - extract from there
|
||||
changelog = changelog.substring(match.index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Additional cleanup - remove common AI preambles if they somehow remain
|
||||
const prefixes = [
|
||||
/^I['']ll\s+analyze[^#]*(?=#)/is,
|
||||
/^I['']ll\s+generate[^#]*(?=#)/is,
|
||||
/^Here['']s the changelog[:\s]*/i,
|
||||
/^The changelog[:\s]*/i,
|
||||
/^Changelog[:\s]*/i
|
||||
/^Changelog[:\s]*/i,
|
||||
/^Based on[^#]*(?=#)/is,
|
||||
/^Let me[^#]*(?=#)/is
|
||||
];
|
||||
|
||||
for (const prefix of prefixes) {
|
||||
|
||||
@@ -1913,6 +1913,40 @@ export function setupIpcHandlers(
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Task Archive Operations
|
||||
// ============================================
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.TASK_ARCHIVE,
|
||||
async (_, projectId: string, taskIds: string[], version?: string): Promise<IPCResult<boolean>> => {
|
||||
try {
|
||||
const success = projectStore.archiveTasks(projectId, taskIds, version);
|
||||
return { success, data: success };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to archive tasks'
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.TASK_UNARCHIVE,
|
||||
async (_, projectId: string, taskIds: string[]): Promise<IPCResult<boolean>> => {
|
||||
try {
|
||||
const success = projectStore.unarchiveTasks(projectId, taskIds);
|
||||
return { success, data: success };
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to unarchive tasks'
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Task Phase Logs (collapsible by phase)
|
||||
// ============================================
|
||||
@@ -4422,6 +4456,77 @@ ${issue.body || 'No description provided.'}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Create a GitHub release using the gh CLI
|
||||
*/
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.GITHUB_CREATE_RELEASE,
|
||||
async (
|
||||
_,
|
||||
projectId: string,
|
||||
version: string,
|
||||
releaseNotes: string,
|
||||
options?: { draft?: boolean; prerelease?: boolean }
|
||||
): Promise<IPCResult<{ url: string }>> => {
|
||||
const project = projectStore.getProject(projectId);
|
||||
if (!project) {
|
||||
return { success: false, error: 'Project not found' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if gh CLI is available
|
||||
try {
|
||||
execSync('which gh', { encoding: 'utf-8' });
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: 'GitHub CLI (gh) not found. Please install it: https://cli.github.com/'
|
||||
};
|
||||
}
|
||||
|
||||
// Check if user is authenticated
|
||||
try {
|
||||
execSync('gh auth status', { cwd: project.path, encoding: 'utf-8', stdio: 'pipe' });
|
||||
} catch {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Not authenticated with GitHub. Run "gh auth login" in terminal first.'
|
||||
};
|
||||
}
|
||||
|
||||
// Prepare tag name (ensure v prefix)
|
||||
const tag = version.startsWith('v') ? version : `v${version}`;
|
||||
|
||||
// Build gh release command
|
||||
const args = ['release', 'create', tag, '--title', tag, '--notes', releaseNotes];
|
||||
if (options?.draft) args.push('--draft');
|
||||
if (options?.prerelease) args.push('--prerelease');
|
||||
|
||||
// Create the release
|
||||
const output = execSync(`gh ${args.map(a => `"${a.replace(/"/g, '\\"')}"`).join(' ')}`, {
|
||||
cwd: project.path,
|
||||
encoding: 'utf-8',
|
||||
stdio: 'pipe'
|
||||
}).trim();
|
||||
|
||||
// Output is typically the release URL
|
||||
const releaseUrl = output || `https://github.com/releases/tag/${tag}`;
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { url: releaseUrl }
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : 'Failed to create release';
|
||||
// Try to extract more useful error message from stderr
|
||||
if (error && typeof error === 'object' && 'stderr' in error) {
|
||||
return { success: false, error: String(error.stderr) || errorMsg };
|
||||
}
|
||||
return { success: false, error: errorMsg };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Auto Claude Source Update Operations
|
||||
// ============================================
|
||||
@@ -5376,6 +5481,53 @@ ${idea.rationale}
|
||||
}
|
||||
);
|
||||
|
||||
ipcMain.handle(
|
||||
IPC_CHANNELS.CHANGELOG_SUGGEST_VERSION,
|
||||
async (_, projectId: string, taskIds: string[]): Promise<IPCResult<{ version: string; reason: string }>> => {
|
||||
const project = projectStore.getProject(projectId);
|
||||
if (!project) {
|
||||
return { success: false, error: 'Project not found' };
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current version from existing changelog
|
||||
const existing = changelogService.readExistingChangelog(project.path);
|
||||
const currentVersion = existing.lastVersion;
|
||||
|
||||
// Load specs for selected tasks to analyze change types
|
||||
const tasks = projectStore.getTasks(projectId);
|
||||
const devMode = project.settings.devMode ?? false;
|
||||
const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode);
|
||||
const specs = await changelogService.loadTaskSpecs(project.path, taskIds, tasks, specsBaseDir);
|
||||
|
||||
// Analyze specs and suggest version
|
||||
const suggestedVersion = changelogService.suggestVersion(specs, currentVersion);
|
||||
|
||||
// Determine reason for the suggestion
|
||||
let reason = 'patch';
|
||||
if (currentVersion) {
|
||||
const [oldMajor, oldMinor] = currentVersion.split('.').map(Number);
|
||||
const [newMajor, newMinor] = suggestedVersion.split('.').map(Number);
|
||||
if (newMajor > oldMajor) {
|
||||
reason = 'breaking';
|
||||
} else if (newMinor > oldMinor) {
|
||||
reason = 'feature';
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { version: suggestedVersion, reason }
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to suggest version'
|
||||
};
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ============================================
|
||||
// Changelog Agent Events → Renderer
|
||||
// ============================================
|
||||
|
||||
@@ -366,6 +366,79 @@ export class ProjectStore {
|
||||
|
||||
return { status: calculatedStatus, reviewReason: calculatedStatus === 'human_review' ? reviewReason : undefined };
|
||||
}
|
||||
|
||||
/**
|
||||
* Archive tasks by writing archivedAt to their metadata
|
||||
* @param projectId - Project ID
|
||||
* @param taskIds - IDs of tasks to archive
|
||||
* @param version - Version they were archived in (optional)
|
||||
*/
|
||||
archiveTasks(projectId: string, taskIds: string[], version?: string): boolean {
|
||||
const project = this.getProject(projectId);
|
||||
if (!project) return false;
|
||||
|
||||
const devMode = project.settings.devMode ?? false;
|
||||
const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode);
|
||||
const specsDir = path.join(project.path, specsBaseDir);
|
||||
|
||||
const archivedAt = new Date().toISOString();
|
||||
|
||||
for (const taskId of taskIds) {
|
||||
const specPath = path.join(specsDir, taskId);
|
||||
const metadataPath = path.join(specPath, 'task_metadata.json');
|
||||
|
||||
try {
|
||||
let metadata: TaskMetadata = {};
|
||||
if (existsSync(metadataPath)) {
|
||||
metadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
|
||||
}
|
||||
|
||||
// Add archive info
|
||||
metadata.archivedAt = archivedAt;
|
||||
if (version) {
|
||||
metadata.archivedInVersion = version;
|
||||
}
|
||||
|
||||
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
||||
} catch {
|
||||
// Continue with other tasks even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unarchive tasks by removing archivedAt from their metadata
|
||||
* @param projectId - Project ID
|
||||
* @param taskIds - IDs of tasks to unarchive
|
||||
*/
|
||||
unarchiveTasks(projectId: string, taskIds: string[]): boolean {
|
||||
const project = this.getProject(projectId);
|
||||
if (!project) return false;
|
||||
|
||||
const devMode = project.settings.devMode ?? false;
|
||||
const specsBaseDir = getSpecsDir(project.autoBuildPath, devMode);
|
||||
const specsDir = path.join(project.path, specsBaseDir);
|
||||
|
||||
for (const taskId of taskIds) {
|
||||
const specPath = path.join(specsDir, taskId);
|
||||
const metadataPath = path.join(specPath, 'task_metadata.json');
|
||||
|
||||
try {
|
||||
if (existsSync(metadataPath)) {
|
||||
const metadata: TaskMetadata = JSON.parse(readFileSync(metadataPath, 'utf-8'));
|
||||
delete metadata.archivedAt;
|
||||
delete metadata.archivedInVersion;
|
||||
writeFileSync(metadataPath, JSON.stringify(metadata, null, 2));
|
||||
}
|
||||
} catch {
|
||||
// Continue with other tasks even if one fails
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Singleton instance
|
||||
|
||||
@@ -178,6 +178,12 @@ const electronAPI: ElectronAPI = {
|
||||
listWorktrees: (projectId: string): Promise<IPCResult<import('../shared/types').WorktreeListResult>> =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.TASK_LIST_WORKTREES, projectId),
|
||||
|
||||
archiveTasks: (projectId: string, taskIds: string[], version?: string): Promise<IPCResult<boolean>> =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.TASK_ARCHIVE, projectId, taskIds, version),
|
||||
|
||||
unarchiveTasks: (projectId: string, taskIds: string[]): Promise<IPCResult<boolean>> =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.TASK_UNARCHIVE, projectId, taskIds),
|
||||
|
||||
// ============================================
|
||||
// Event Listeners (main → renderer)
|
||||
// ============================================
|
||||
@@ -557,6 +563,14 @@ const electronAPI: ElectronAPI = {
|
||||
importGitHubIssues: (projectId: string, issueNumbers: number[]): Promise<IPCResult<GitHubImportResult>> =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.GITHUB_IMPORT_ISSUES, projectId, issueNumbers),
|
||||
|
||||
createGitHubRelease: (
|
||||
projectId: string,
|
||||
version: string,
|
||||
releaseNotes: string,
|
||||
options?: { draft?: boolean; prerelease?: boolean }
|
||||
): Promise<IPCResult<{ url: string }>> =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.GITHUB_CREATE_RELEASE, projectId, version, releaseNotes, options),
|
||||
|
||||
// ============================================
|
||||
// GitHub Event Listeners
|
||||
// ============================================
|
||||
@@ -792,6 +806,12 @@ const electronAPI: ElectronAPI = {
|
||||
readExistingChangelog: (projectId: string): Promise<IPCResult<ExistingChangelog>> =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.CHANGELOG_READ_EXISTING, projectId),
|
||||
|
||||
suggestChangelogVersion: (
|
||||
projectId: string,
|
||||
taskIds: string[]
|
||||
): Promise<IPCResult<{ version: string; reason: string }>> =>
|
||||
ipcRenderer.invoke(IPC_CHANNELS.CHANGELOG_SUGGEST_VERSION, projectId, taskIds),
|
||||
|
||||
// ============================================
|
||||
// Changelog Event Listeners
|
||||
// ============================================
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -123,7 +123,7 @@ export function ChatHistorySidebar({
|
||||
</div>
|
||||
|
||||
{/* Session list */}
|
||||
<ScrollArea className="flex-1">
|
||||
<ScrollArea className="flex-1 [&>div>div]:!overflow-x-hidden">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||
@@ -247,7 +247,7 @@ function SessionItem({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'group flex cursor-pointer items-center gap-2 px-3 py-2 transition-colors hover:bg-muted',
|
||||
'group flex cursor-pointer items-center gap-2 px-2 py-2 transition-colors hover:bg-muted',
|
||||
isActive && 'bg-primary/10 hover:bg-primary/15'
|
||||
)}
|
||||
onClick={onSelect}
|
||||
@@ -276,7 +276,7 @@ function SessionItem({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
className="h-6 w-6 shrink-0 opacity-70 hover:opacity-100"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<MoreVertical className="h-3.5 w-3.5" />
|
||||
|
||||
@@ -17,9 +17,11 @@ import {
|
||||
sortableKeyboardCoordinates,
|
||||
verticalListSortingStrategy
|
||||
} from '@dnd-kit/sortable';
|
||||
import { Plus, Inbox, Loader2, Eye, CheckCircle2 } from 'lucide-react';
|
||||
import { Plus, Inbox, Loader2, Eye, CheckCircle2, Archive } from 'lucide-react';
|
||||
import { ScrollArea } from './ui/scroll-area';
|
||||
import { Button } from './ui/button';
|
||||
import { Checkbox } from './ui/checkbox';
|
||||
import { Label } from './ui/label';
|
||||
import { TaskCard } from './TaskCard';
|
||||
import { SortableTaskCard } from './SortableTaskCard';
|
||||
import { TASK_STATUS_COLUMNS, TASK_STATUS_LABELS } from '../../shared/constants';
|
||||
@@ -196,6 +198,20 @@ function DroppableColumn({ status, tasks, onTaskClick, isOver, onAddClick }: Dro
|
||||
export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardProps) {
|
||||
const [activeTask, setActiveTask] = useState<Task | null>(null);
|
||||
const [overColumnId, setOverColumnId] = useState<string | null>(null);
|
||||
const [showArchived, setShowArchived] = useState(false);
|
||||
|
||||
// Count archived tasks for display
|
||||
const archivedCount = useMemo(() => {
|
||||
return tasks.filter((t) => t.metadata?.archivedAt).length;
|
||||
}, [tasks]);
|
||||
|
||||
// Filter tasks based on archive status
|
||||
const filteredTasks = useMemo(() => {
|
||||
if (showArchived) {
|
||||
return tasks; // Show all tasks including archived
|
||||
}
|
||||
return tasks.filter((t) => !t.metadata?.archivedAt);
|
||||
}, [tasks, showArchived]);
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
@@ -217,14 +233,14 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
|
||||
done: []
|
||||
};
|
||||
|
||||
tasks.forEach((task) => {
|
||||
filteredTasks.forEach((task) => {
|
||||
if (grouped[task.status]) {
|
||||
grouped[task.status].push(task);
|
||||
}
|
||||
});
|
||||
|
||||
return grouped;
|
||||
}, [tasks]);
|
||||
}, [filteredTasks]);
|
||||
|
||||
const handleDragStart = (event: DragStartEvent) => {
|
||||
const { active } = event;
|
||||
@@ -291,34 +307,60 @@ export function KanbanBoard({ tasks, onTaskClick, onNewTaskClick }: KanbanBoardP
|
||||
};
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCorners}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="flex h-full gap-4 overflow-x-auto p-6">
|
||||
{TASK_STATUS_COLUMNS.map((status) => (
|
||||
<DroppableColumn
|
||||
key={status}
|
||||
status={status}
|
||||
tasks={tasksByStatus[status]}
|
||||
onTaskClick={onTaskClick}
|
||||
isOver={overColumnId === status}
|
||||
onAddClick={status === 'backlog' ? onNewTaskClick : undefined}
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Kanban header with filters */}
|
||||
<div className="flex items-center justify-end px-6 py-3 border-b border-border/50">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="showArchived"
|
||||
checked={showArchived}
|
||||
onCheckedChange={(checked) => setShowArchived(checked === true)}
|
||||
/>
|
||||
))}
|
||||
<Label
|
||||
htmlFor="showArchived"
|
||||
className="flex items-center gap-1.5 text-sm text-muted-foreground cursor-pointer"
|
||||
>
|
||||
<Archive className="h-3.5 w-3.5" />
|
||||
Show archived
|
||||
{archivedCount > 0 && (
|
||||
<span className="ml-1 text-xs px-1.5 py-0.5 rounded-full bg-muted">
|
||||
{archivedCount}
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Drag overlay - enhanced visual feedback */}
|
||||
<DragOverlay>
|
||||
{activeTask ? (
|
||||
<div className="drag-overlay-card">
|
||||
<TaskCard task={activeTask} onClick={() => {}} />
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
{/* Kanban columns */}
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCorners}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="flex flex-1 gap-4 overflow-x-auto p-6">
|
||||
{TASK_STATUS_COLUMNS.map((status) => (
|
||||
<DroppableColumn
|
||||
key={status}
|
||||
status={status}
|
||||
tasks={tasksByStatus[status]}
|
||||
onTaskClick={onTaskClick}
|
||||
isOver={overColumnId === status}
|
||||
onAddClick={status === 'backlog' ? onNewTaskClick : undefined}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Drag overlay - enhanced visual feedback */}
|
||||
<DragOverlay>
|
||||
{activeTask ? (
|
||||
<div className="drag-overlay-card">
|
||||
<TaskCard task={activeTask} onClick={() => {}} />
|
||||
</div>
|
||||
) : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -961,6 +961,44 @@ export function ProjectSettings({ project, open, onOpenChange }: ProjectSettings
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Graphiti MCP Server Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label className="font-normal text-foreground">Enable Agent Memory Access</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Allow agents to search and add to the knowledge graph via MCP
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={settings.graphitiMcpEnabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setSettings({ ...settings, graphitiMcpEnabled: checked })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{settings.graphitiMcpEnabled && (
|
||||
<div className="space-y-2 ml-6">
|
||||
<Label className="text-sm font-medium text-foreground">Graphiti MCP Server URL</Label>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
URL of the Graphiti MCP server (requires Docker container)
|
||||
</p>
|
||||
<Input
|
||||
placeholder="http://localhost:8000/mcp/"
|
||||
value={settings.graphitiMcpUrl || ''}
|
||||
onChange={(e) => setSettings({ ...settings, graphitiMcpUrl: e.target.value || undefined })}
|
||||
/>
|
||||
<div className="rounded-lg border border-info/30 bg-info/5 p-3">
|
||||
<p className="text-xs text-info">
|
||||
Start the MCP server with:{' '}
|
||||
<code className="px-1 bg-info/10 rounded">docker run -d -p 8000:8000 falkordb/graphiti-knowledge-graph-mcp</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
{/* LLM Provider Selection - V2 Multi-provider support */}
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium text-foreground">LLM Provider</Label>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw } from 'lucide-react';
|
||||
import { Play, Square, Clock, Zap, Target, Shield, Gauge, Palette, FileCode, Bug, Wrench, Loader2, AlertTriangle, RotateCcw, Archive } from 'lucide-react';
|
||||
import { Card, CardContent } from './ui/card';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Progress } from './ui/progress';
|
||||
@@ -133,12 +133,15 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
|
||||
|
||||
const reviewReasonInfo = task.status === 'human_review' ? getReviewReasonLabel(task.reviewReason) : null;
|
||||
|
||||
const isArchived = !!task.metadata?.archivedAt;
|
||||
|
||||
return (
|
||||
<Card
|
||||
className={cn(
|
||||
'card-surface task-card-enhanced cursor-pointer',
|
||||
isRunning && !isStuck && 'ring-2 ring-primary border-primary task-running-pulse',
|
||||
isStuck && 'ring-2 ring-warning border-warning task-stuck-pulse'
|
||||
isStuck && 'ring-2 ring-warning border-warning task-stuck-pulse',
|
||||
isArchived && 'opacity-60 hover:opacity-80'
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
@@ -172,6 +175,16 @@ export function TaskCard({ task, onClick }: TaskCardProps) {
|
||||
Incomplete
|
||||
</Badge>
|
||||
)}
|
||||
{/* Archived indicator - task has been released */}
|
||||
{task.metadata?.archivedAt && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[10px] px-1.5 py-0.5 flex items-center gap-1 bg-muted text-muted-foreground border-border"
|
||||
>
|
||||
<Archive className="h-2.5 w-2.5" />
|
||||
Archived
|
||||
</Badge>
|
||||
)}
|
||||
{/* Execution phase badge - shown when actively running */}
|
||||
{hasActiveExecution && executionPhase && !isStuck && !isIncomplete && (
|
||||
<Badge
|
||||
|
||||
@@ -113,7 +113,10 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
onReviewNeeded: true,
|
||||
sound: false
|
||||
},
|
||||
devMode: false
|
||||
devMode: false,
|
||||
// Graphiti MCP server for agent-accessible knowledge graph (enabled by default)
|
||||
graphitiMcpEnabled: true,
|
||||
graphitiMcpUrl: 'http://localhost:8000/mcp/'
|
||||
};
|
||||
|
||||
// IPC Channel names
|
||||
@@ -146,6 +149,8 @@ export const IPC_CHANNELS = {
|
||||
TASK_WORKTREE_MERGE: 'task:worktreeMerge',
|
||||
TASK_WORKTREE_DISCARD: 'task:worktreeDiscard',
|
||||
TASK_LIST_WORKTREES: 'task:listWorktrees',
|
||||
TASK_ARCHIVE: 'task:archive',
|
||||
TASK_UNARCHIVE: 'task:unarchive',
|
||||
|
||||
// Task events (main -> renderer)
|
||||
TASK_PROGRESS: 'task:progress',
|
||||
@@ -238,6 +243,7 @@ export const IPC_CHANNELS = {
|
||||
GITHUB_CHECK_CONNECTION: 'github:checkConnection',
|
||||
GITHUB_INVESTIGATE_ISSUE: 'github:investigateIssue',
|
||||
GITHUB_IMPORT_ISSUES: 'github:importIssues',
|
||||
GITHUB_CREATE_RELEASE: 'github:createRelease',
|
||||
|
||||
// GitHub events (main -> renderer)
|
||||
GITHUB_INVESTIGATION_PROGRESS: 'github:investigationProgress',
|
||||
@@ -261,6 +267,7 @@ export const IPC_CHANNELS = {
|
||||
CHANGELOG_GENERATE: 'changelog:generate',
|
||||
CHANGELOG_SAVE: 'changelog:save',
|
||||
CHANGELOG_READ_EXISTING: 'changelog:readExisting',
|
||||
CHANGELOG_SUGGEST_VERSION: 'changelog:suggestVersion',
|
||||
|
||||
// Changelog events (main -> renderer)
|
||||
CHANGELOG_GENERATION_PROGRESS: 'changelog:generationProgress',
|
||||
|
||||
@@ -23,6 +23,10 @@ export interface ProjectSettings {
|
||||
notifications: NotificationSettings;
|
||||
/** Dev mode: use dev/auto-claude/specs/ for framework development */
|
||||
devMode: boolean;
|
||||
/** Enable Graphiti MCP server for agent-accessible knowledge graph */
|
||||
graphitiMcpEnabled: boolean;
|
||||
/** Graphiti MCP server URL (default: http://localhost:8000/mcp/) */
|
||||
graphitiMcpUrl?: string;
|
||||
}
|
||||
|
||||
export interface NotificationSettings {
|
||||
@@ -204,6 +208,10 @@ export interface TaskMetadata {
|
||||
|
||||
// Review settings
|
||||
requireReviewBeforeCoding?: boolean; // Require human review of spec/plan before coding starts
|
||||
|
||||
// Archive status
|
||||
archivedAt?: string; // ISO date when task was archived
|
||||
archivedInVersion?: string; // Version in which task was archived (from changelog)
|
||||
}
|
||||
|
||||
export interface Task {
|
||||
@@ -1201,6 +1209,10 @@ export interface ElectronAPI {
|
||||
discardWorktree: (taskId: string) => Promise<IPCResult<WorktreeDiscardResult>>;
|
||||
listWorktrees: (projectId: string) => Promise<IPCResult<WorktreeListResult>>;
|
||||
|
||||
// Task archive operations
|
||||
archiveTasks: (projectId: string, taskIds: string[], version?: string) => Promise<IPCResult<boolean>>;
|
||||
unarchiveTasks: (projectId: string, taskIds: string[]) => Promise<IPCResult<boolean>>;
|
||||
|
||||
// Event listeners
|
||||
onTaskProgress: (callback: (taskId: string, plan: ImplementationPlan) => void) => () => void;
|
||||
onTaskError: (callback: (taskId: string, error: string) => void) => () => void;
|
||||
@@ -1282,6 +1294,12 @@ export interface ElectronAPI {
|
||||
checkGitHubConnection: (projectId: string) => Promise<IPCResult<GitHubSyncStatus>>;
|
||||
investigateGitHubIssue: (projectId: string, issueNumber: number) => void;
|
||||
importGitHubIssues: (projectId: string, issueNumbers: number[]) => Promise<IPCResult<GitHubImportResult>>;
|
||||
createGitHubRelease: (
|
||||
projectId: string,
|
||||
version: string,
|
||||
releaseNotes: string,
|
||||
options?: { draft?: boolean; prerelease?: boolean }
|
||||
) => Promise<IPCResult<{ url: string }>>;
|
||||
|
||||
// GitHub event listeners
|
||||
onGitHubInvestigationProgress: (
|
||||
@@ -1343,6 +1361,10 @@ export interface ElectronAPI {
|
||||
generateChangelog: (request: ChangelogGenerationRequest) => void; // Async with progress events
|
||||
saveChangelog: (request: ChangelogSaveRequest) => Promise<IPCResult<ChangelogSaveResult>>;
|
||||
readExistingChangelog: (projectId: string) => Promise<IPCResult<ExistingChangelog>>;
|
||||
suggestChangelogVersion: (
|
||||
projectId: string,
|
||||
taskIds: string[]
|
||||
) => Promise<IPCResult<{ version: string; reason: string }>>;
|
||||
|
||||
// Changelog event listeners
|
||||
onChangelogGenerationProgress: (
|
||||
|
||||
@@ -172,6 +172,29 @@ CLAUDE_CODE_OAUTH_TOKEN=your-oauth-token-here
|
||||
# Common values: nomic-embed-text=768, mxbai-embed-large=1024, all-minilm=384
|
||||
# OLLAMA_EMBEDDING_DIM=768
|
||||
|
||||
# =============================================================================
|
||||
# GRAPHITI MCP SERVER (OPTIONAL - Agent-accessible knowledge graph)
|
||||
# =============================================================================
|
||||
# Enable Graphiti MCP server for direct agent access to knowledge graph.
|
||||
# This is SEPARATE from GRAPHITI_ENABLED (Python library integration).
|
||||
#
|
||||
# With MCP server enabled, Claude agents can directly:
|
||||
# - Search for relevant context (search_nodes, search_facts)
|
||||
# - Add discoveries to the graph (add_episode)
|
||||
# - Retrieve recent sessions (get_episodes)
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. Run the Graphiti MCP server via Docker:
|
||||
# docker run -d -p 8000:8000 \
|
||||
# -e OPENAI_API_KEY=$OPENAI_API_KEY \
|
||||
# -e FALKORDB_URI=redis://host.docker.internal:6380 \
|
||||
# falkordb/graphiti-knowledge-graph-mcp
|
||||
#
|
||||
# See: https://docs.falkordb.com/agentic-memory/graphiti-mcp-server.html
|
||||
|
||||
# Graphiti MCP Server URL (setting this enables MCP integration)
|
||||
# GRAPHITI_MCP_URL=http://localhost:8000/mcp/
|
||||
|
||||
# =============================================================================
|
||||
# GRAPHITI: FalkorDB Connection Settings
|
||||
# =============================================================================
|
||||
|
||||
@@ -21,6 +21,21 @@ from auto_claude_tools import (
|
||||
)
|
||||
|
||||
|
||||
def is_graphiti_mcp_enabled() -> bool:
|
||||
"""
|
||||
Check if Graphiti MCP server integration is enabled.
|
||||
|
||||
Requires GRAPHITI_MCP_URL to be set (e.g., http://localhost:8000/mcp/)
|
||||
This is separate from GRAPHITI_ENABLED which controls the Python library integration.
|
||||
"""
|
||||
return bool(os.environ.get("GRAPHITI_MCP_URL"))
|
||||
|
||||
|
||||
def get_graphiti_mcp_url() -> str:
|
||||
"""Get the Graphiti MCP server URL."""
|
||||
return os.environ.get("GRAPHITI_MCP_URL", "http://localhost:8000/mcp/")
|
||||
|
||||
|
||||
# Puppeteer MCP tools for browser automation
|
||||
PUPPETEER_TOOLS = [
|
||||
"mcp__puppeteer__puppeteer_connect_active_tab",
|
||||
@@ -59,6 +74,16 @@ CONTEXT7_TOOLS = [
|
||||
"mcp__context7__get-library-docs",
|
||||
]
|
||||
|
||||
# Graphiti MCP tools for knowledge graph memory (when GRAPHITI_MCP_ENABLED is set)
|
||||
# See: https://docs.falkordb.com/agentic-memory/graphiti-mcp-server.html
|
||||
GRAPHITI_MCP_TOOLS = [
|
||||
"mcp__graphiti-memory__search_nodes", # Search entity summaries
|
||||
"mcp__graphiti-memory__search_facts", # Search relationships between entities
|
||||
"mcp__graphiti-memory__add_episode", # Add data to knowledge graph
|
||||
"mcp__graphiti-memory__get_episodes", # Retrieve recent episodes
|
||||
"mcp__graphiti-memory__get_entity_edge", # Get specific entity/relationship
|
||||
]
|
||||
|
||||
# Built-in tools
|
||||
BUILTIN_TOOLS = [
|
||||
"Read",
|
||||
@@ -117,11 +142,16 @@ def create_client(
|
||||
else:
|
||||
allowed_tools_list = [*BUILTIN_TOOLS]
|
||||
|
||||
# Check if Graphiti MCP is enabled
|
||||
graphiti_mcp_enabled = is_graphiti_mcp_enabled()
|
||||
|
||||
# Add external MCP tools
|
||||
allowed_tools_list.extend(PUPPETEER_TOOLS)
|
||||
allowed_tools_list.extend(CONTEXT7_TOOLS)
|
||||
if linear_enabled:
|
||||
allowed_tools_list.extend(LINEAR_TOOLS)
|
||||
if graphiti_mcp_enabled:
|
||||
allowed_tools_list.extend(GRAPHITI_MCP_TOOLS)
|
||||
|
||||
# Create comprehensive security settings
|
||||
# Note: Using relative paths ("./**") restricts access to project directory
|
||||
@@ -146,6 +176,8 @@ def create_client(
|
||||
*CONTEXT7_TOOLS,
|
||||
# Allow Linear MCP tools for project management (if enabled)
|
||||
*(LINEAR_TOOLS if linear_enabled else []),
|
||||
# Allow Graphiti MCP tools for knowledge graph memory (if enabled)
|
||||
*(GRAPHITI_MCP_TOOLS if graphiti_mcp_enabled else []),
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -163,6 +195,8 @@ def create_client(
|
||||
mcp_servers_list = ["puppeteer (browser automation)", "context7 (documentation)"]
|
||||
if linear_enabled:
|
||||
mcp_servers_list.append("linear (project management)")
|
||||
if graphiti_mcp_enabled:
|
||||
mcp_servers_list.append("graphiti-memory (knowledge graph)")
|
||||
if auto_claude_tools_enabled:
|
||||
mcp_servers_list.append(f"auto-claude ({agent_type} tools)")
|
||||
print(f" - MCP servers: {', '.join(mcp_servers_list)}")
|
||||
@@ -182,6 +216,14 @@ def create_client(
|
||||
"headers": {"Authorization": f"Bearer {linear_api_key}"}
|
||||
}
|
||||
|
||||
# Add Graphiti MCP server if enabled
|
||||
# Requires running: docker run -d -p 8000:8000 falkordb/graphiti-knowledge-graph-mcp
|
||||
if graphiti_mcp_enabled:
|
||||
mcp_servers["graphiti-memory"] = {
|
||||
"type": "http",
|
||||
"url": get_graphiti_mcp_url(),
|
||||
}
|
||||
|
||||
# Add custom auto-claude MCP server if available
|
||||
auto_claude_mcp_server = None
|
||||
if auto_claude_tools_enabled:
|
||||
|
||||
Reference in New Issue
Block a user