feat: CxIDE v1 — native macOS SwiftUI IDE with agentic AI assistant
- SwiftUI macOS app with C++17 code analysis engine (ObjC++ bridge) - Agentic AI loop: LLM plans → tool calls → execution → feedback loop - 15 agent tools: file ops, terminal, git, xcode build, code intel - 7 persistent terminal tools with background session management - Chat sidebar with agent step rendering and auto-apply - NVIDIA NIM API integration (Llama 3.3 70B default) - OpenAI tool_calls format with prompt-based fallback - Code editor with syntax highlighting and multi-tab support - File tree, console view, terminal view - Git integration and workspace management
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
// ProjectTools.swift
|
||||
// CxSwiftAgent — Project Management MCP Tools
|
||||
//
|
||||
// 5 tools: project_detect, project_run, project_deps, project_config, project_create
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ProjectTools {
|
||||
static func register(on server: MCPServer, config: AgentConfig, memory: AgentMemory) {
|
||||
|
||||
// ── project_detect ────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"project_detect",
|
||||
description: "Detect project type, language, build system, and key files.",
|
||||
inputSchema: [
|
||||
"type": "object",
|
||||
"properties": [
|
||||
"path": ["type": "string", "description": "Project root (default: workspace root)."],
|
||||
] as [String: Any],
|
||||
] as [String: Any],
|
||||
annotations: ToolAnnotations(readOnlyHint: true)
|
||||
) { args in
|
||||
let root = config.resolvePath(args["path"] as? String ?? ".") ?? config.workspaceRoot
|
||||
let fm = FileManager.default
|
||||
|
||||
var info: [String: Any] = ["path": root]
|
||||
var languages: [String] = []
|
||||
var buildSystems: [String] = []
|
||||
var keyFiles: [String] = []
|
||||
|
||||
let detectors: [(String, String, String)] = [
|
||||
("Package.swift", "swift", "spm"),
|
||||
("Makefile", "", "make"),
|
||||
("CMakeLists.txt", "c++", "cmake"),
|
||||
("package.json", "javascript", "npm"),
|
||||
("composer.json", "php", "composer"),
|
||||
("Cargo.toml", "rust", "cargo"),
|
||||
("go.mod", "go", "go-modules"),
|
||||
("pyproject.toml", "python", "pip"),
|
||||
("requirements.txt", "python", "pip"),
|
||||
("Gemfile", "ruby", "bundler"),
|
||||
("build.gradle", "java", "gradle"),
|
||||
("pom.xml", "java", "maven"),
|
||||
("cpanfile", "perl", "cpanm"),
|
||||
]
|
||||
|
||||
for (file, lang, build) in detectors {
|
||||
let path = (root as NSString).appendingPathComponent(file)
|
||||
if fm.fileExists(atPath: path) {
|
||||
keyFiles.append(file)
|
||||
if !lang.isEmpty && !languages.contains(lang) { languages.append(lang) }
|
||||
if !buildSystems.contains(build) { buildSystems.append(build) }
|
||||
}
|
||||
}
|
||||
|
||||
// Xcode detection
|
||||
if let items = try? fm.contentsOfDirectory(atPath: root) {
|
||||
for item in items {
|
||||
if item.hasSuffix(".xcodeproj") || item.hasSuffix(".xcworkspace") {
|
||||
keyFiles.append(item)
|
||||
if !languages.contains("swift") { languages.append("swift") }
|
||||
if !buildSystems.contains("xcode") { buildSystems.append("xcode") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info["languages"] = languages
|
||||
info["build_systems"] = buildSystems
|
||||
info["key_files"] = keyFiles
|
||||
info["type"] = languages.first ?? "unknown"
|
||||
|
||||
return ok(JSON.serialize(info, pretty: true))
|
||||
}
|
||||
|
||||
// ── project_run ───────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"project_run",
|
||||
description: "Run project build/test/lint commands based on detected project type.",
|
||||
inputSchema: [
|
||||
"type": "object",
|
||||
"required": ["action"],
|
||||
"properties": [
|
||||
"action": ["type": "string", "description": "Action: build, test, lint, run, clean."],
|
||||
"path": ["type": "string", "description": "Project root (default: workspace root)."],
|
||||
] as [String: Any],
|
||||
] as [String: Any]
|
||||
) { args in
|
||||
let root = config.resolvePath(args["path"] as? String ?? ".") ?? config.workspaceRoot
|
||||
let action = args["action"] as? String ?? "build"
|
||||
let fm = FileManager.default
|
||||
|
||||
// Detect project type and run appropriate command
|
||||
if fm.fileExists(atPath: (root as NSString).appendingPathComponent("Package.swift")) {
|
||||
switch action {
|
||||
case "build": return runCmd("swift", ["build"], in: root)
|
||||
case "test": return runCmd("swift", ["test"], in: root)
|
||||
case "clean": return runCmd("swift", ["package", "clean"], in: root)
|
||||
case "run": return runCmd("swift", ["run"], in: root)
|
||||
default: return err("Unknown action: \(action)")
|
||||
}
|
||||
} else if fm.fileExists(atPath: (root as NSString).appendingPathComponent("package.json")) {
|
||||
switch action {
|
||||
case "build": return runCmd("npm", ["run", "build"], in: root)
|
||||
case "test": return runCmd("npm", ["test"], in: root)
|
||||
case "lint": return runCmd("npm", ["run", "lint"], in: root)
|
||||
case "run": return runCmd("npm", ["start"], in: root)
|
||||
default: return err("Unknown action: \(action)")
|
||||
}
|
||||
} else if fm.fileExists(atPath: (root as NSString).appendingPathComponent("Makefile")) {
|
||||
switch action {
|
||||
case "build": return runCmd("make", [], in: root)
|
||||
case "test": return runCmd("make", ["test"], in: root)
|
||||
case "clean": return runCmd("make", ["clean"], in: root)
|
||||
default: return err("Unknown action: \(action)")
|
||||
}
|
||||
} else if fm.fileExists(atPath: (root as NSString).appendingPathComponent("composer.json")) {
|
||||
switch action {
|
||||
case "test": return runCmd("vendor/bin/phpunit", [], in: root)
|
||||
case "lint": return runCmd("vendor/bin/phpcs", ["src/"], in: root)
|
||||
default: return err("Unknown action: \(action)")
|
||||
}
|
||||
}
|
||||
|
||||
// Check for Xcode project
|
||||
let xcodeProjects = (try? fm.contentsOfDirectory(atPath: root))?.filter { $0.hasSuffix(".xcodeproj") } ?? []
|
||||
if let xcodeProj = xcodeProjects.first {
|
||||
let fullProj = (root as NSString).appendingPathComponent(xcodeProj)
|
||||
switch action {
|
||||
case "build": return runCmd("/usr/bin/xcodebuild", ["build", "-project", fullProj, "-quiet"], in: root)
|
||||
case "test": return runCmd("/usr/bin/xcodebuild", ["test", "-project", fullProj, "-quiet"], in: root)
|
||||
case "clean": return runCmd("/usr/bin/xcodebuild", ["clean", "-project", fullProj, "-quiet"], in: root)
|
||||
case "run": return runCmd("/usr/bin/xcodebuild", ["build", "-project", fullProj, "-quiet"], in: root)
|
||||
default: return err("Unknown action: \(action)")
|
||||
}
|
||||
}
|
||||
|
||||
return err("Cannot detect project type at \(root)")
|
||||
}
|
||||
|
||||
// ── project_deps ──────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"project_deps",
|
||||
description: "List project dependencies from manifest files.",
|
||||
inputSchema: [
|
||||
"type": "object",
|
||||
"properties": [
|
||||
"path": ["type": "string", "description": "Project root (default: workspace root)."],
|
||||
] as [String: Any],
|
||||
] as [String: Any],
|
||||
annotations: ToolAnnotations(readOnlyHint: true)
|
||||
) { args in
|
||||
let root = config.resolvePath(args["path"] as? String ?? ".") ?? config.workspaceRoot
|
||||
let fm = FileManager.default
|
||||
|
||||
// Try Package.swift
|
||||
let spmPath = (root as NSString).appendingPathComponent("Package.swift")
|
||||
if fm.fileExists(atPath: spmPath) {
|
||||
return runCmd("swift", ["package", "show-dependencies"], in: root)
|
||||
}
|
||||
|
||||
// Try package.json
|
||||
let npmPath = (root as NSString).appendingPathComponent("package.json")
|
||||
if fm.fileExists(atPath: npmPath) {
|
||||
if let content = try? String(contentsOfFile: npmPath, encoding: .utf8) {
|
||||
return ok(content)
|
||||
}
|
||||
}
|
||||
|
||||
// Try composer.json
|
||||
let composerPath = (root as NSString).appendingPathComponent("composer.json")
|
||||
if fm.fileExists(atPath: composerPath) {
|
||||
if let content = try? String(contentsOfFile: composerPath, encoding: .utf8) {
|
||||
return ok(content)
|
||||
}
|
||||
}
|
||||
|
||||
return err("No dependency manifest found")
|
||||
}
|
||||
|
||||
// ── project_config ────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"project_config",
|
||||
description: "Read or list project configuration files.",
|
||||
inputSchema: [
|
||||
"type": "object",
|
||||
"properties": [
|
||||
"path": ["type": "string", "description": "Project root (default: workspace root)."],
|
||||
"file": ["type": "string", "description": "Specific config file to read."],
|
||||
] as [String: Any],
|
||||
] as [String: Any],
|
||||
annotations: ToolAnnotations(readOnlyHint: true)
|
||||
) { args in
|
||||
let root = config.resolvePath(args["path"] as? String ?? ".") ?? config.workspaceRoot
|
||||
let fm = FileManager.default
|
||||
|
||||
if let file = args["file"] as? String {
|
||||
let filePath = (root as NSString).appendingPathComponent(file)
|
||||
guard let content = try? String(contentsOfFile: filePath, encoding: .utf8) else {
|
||||
return err("Cannot read \(file)")
|
||||
}
|
||||
return ok(content)
|
||||
}
|
||||
|
||||
// List config files
|
||||
let configFiles = [
|
||||
".env", ".env.example", ".editorconfig", ".gitignore",
|
||||
".swiftlint.yml", ".swift-format", "tsconfig.json",
|
||||
".eslintrc.json", "phpunit.xml", "pyproject.toml",
|
||||
"Dockerfile", "docker-compose.yml", ".github/workflows",
|
||||
]
|
||||
|
||||
var found: [String] = []
|
||||
for file in configFiles {
|
||||
let path = (root as NSString).appendingPathComponent(file)
|
||||
if fm.fileExists(atPath: path) {
|
||||
found.append(file)
|
||||
}
|
||||
}
|
||||
|
||||
return ok(found.isEmpty ? "No config files found" : "Config files:\n" + found.map { " \($0)" }.joined(separator: "\n"))
|
||||
}
|
||||
|
||||
// ── project_create ────────────────────────────────────────────
|
||||
server.registerTool(
|
||||
"project_create",
|
||||
description: "Create a new project from template (Swift Package, Node, etc.).",
|
||||
inputSchema: [
|
||||
"type": "object",
|
||||
"required": ["name", "type"],
|
||||
"properties": [
|
||||
"name": ["type": "string", "description": "Project name."],
|
||||
"type": ["type": "string", "description": "Project type: swift-package, swift-executable, node, python."],
|
||||
"path": ["type": "string", "description": "Parent directory (default: workspace root)."],
|
||||
] as [String: Any],
|
||||
] as [String: Any],
|
||||
annotations: ToolAnnotations(destructiveHint: true)
|
||||
) { args in
|
||||
if config.sandboxMode { return err("Project creation disabled in sandbox mode") }
|
||||
|
||||
guard let name = args["name"] as? String, !name.isEmpty else {
|
||||
return err("Missing project name")
|
||||
}
|
||||
let projectType = args["type"] as? String ?? "swift-executable"
|
||||
let parent = config.resolvePath(args["path"] as? String ?? ".") ?? config.workspaceRoot
|
||||
let projectPath = (parent as NSString).appendingPathComponent(name)
|
||||
|
||||
let fm = FileManager.default
|
||||
try? fm.createDirectory(atPath: projectPath, withIntermediateDirectories: true)
|
||||
|
||||
switch projectType {
|
||||
case "swift-package", "swift-executable":
|
||||
let pkgType = projectType == "swift-package" ? "library" : "executable"
|
||||
return runCmd("swift", ["package", "init", "--type", pkgType, "--name", name], in: projectPath)
|
||||
|
||||
case "node":
|
||||
return runCmd("npm", ["init", "-y"], in: projectPath)
|
||||
|
||||
default:
|
||||
return err("Unknown project type: \(projectType). Supported: swift-package, swift-executable, node, python")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private static func runCmd(_ executable: String, _ args: [String], in directory: String) -> [[String: Any]] {
|
||||
let process = Process()
|
||||
let exePath: String
|
||||
if executable.hasPrefix("/") || executable.hasPrefix(".") {
|
||||
exePath = executable
|
||||
} else if executable.contains("/") {
|
||||
exePath = (directory as NSString).appendingPathComponent(executable)
|
||||
} else {
|
||||
exePath = "/usr/bin/\(executable)"
|
||||
}
|
||||
|
||||
process.executableURL = URL(fileURLWithPath: exePath)
|
||||
process.arguments = args
|
||||
process.currentDirectoryURL = URL(fileURLWithPath: directory)
|
||||
process.environment = ProcessInfo.processInfo.environment
|
||||
|
||||
let pipe = Pipe()
|
||||
let errPipe = Pipe()
|
||||
process.standardOutput = pipe
|
||||
process.standardError = errPipe
|
||||
|
||||
do {
|
||||
try process.run()
|
||||
process.waitUntilExit()
|
||||
|
||||
let outData = pipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
|
||||
let output = String(data: outData, encoding: .utf8) ?? ""
|
||||
let errOutput = String(data: errData, encoding: .utf8) ?? ""
|
||||
|
||||
if process.terminationStatus != 0 {
|
||||
return err("Exit \(process.terminationStatus): \(errOutput.isEmpty ? output : errOutput)")
|
||||
}
|
||||
return ok(output.isEmpty ? "(success)" : output)
|
||||
} catch {
|
||||
return err("Failed to run \(executable): \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
private static func ok(_ text: String) -> [[String: Any]] {
|
||||
[["type": "text", "text": text]]
|
||||
}
|
||||
|
||||
private static func err(_ message: String) -> [[String: Any]] {
|
||||
[["type": "text", "text": "Error: \(message)"]]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user