We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/steipete/Peekaboo'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
import Commander
import Darwin
import Dispatch
import Foundation
import Logging
import PeekabooAgentRuntime
import PeekabooCore
import PeekabooFoundation
import Spinner
import Tachikoma
import TauTUI
// Temporary session info struct until PeekabooAgentService implements session management
// Test: Icon notifications are now working
struct AgentSessionInfo: Codable {
let id: String
let task: String
let created: Date
let lastModified: Date
let messageCount: Int
}
// Simple debug logging check
private var isDebugLoggingEnabled: Bool {
// Check if verbose mode is enabled via log level
if let logLevel = ProcessInfo.processInfo.environment["PEEKABOO_LOG_LEVEL"]?.lowercased() {
return logLevel == "debug" || logLevel == "trace"
}
// Check if agent is in verbose mode
if ProcessInfo.processInfo.arguments.contains("-v") ||
ProcessInfo.processInfo.arguments.contains("--verbose") {
return true
}
return false
}
private func aiDebugPrint(_ message: String) {
if isDebugLoggingEnabled {
print(message)
}
}
/// Output modes for agent execution with progressive enhancement
enum OutputMode {
case minimal // CI/pipes - no colors, simple text
case compact // Basic colors and icons (legacy default)
case enhanced // Rich formatting with progress indicators
case quiet // Only final result
case verbose // Full JSON debug information
}
/// Get icon for tool name in compact mode
func iconForTool(_ toolName: String) -> String {
AgentDisplayTokens.icon(for: toolName)
}
/// AI Agent command that uses new Chat Completions API architecture
@available(macOS 14.0, *)
struct AgentCommand: RuntimeOptionsConfigurable {
static let commandDescription = CommandDescription(
commandName: "agent",
abstract: "Execute complex automation tasks using the Peekaboo agent",
discussion: """
Launches the autonomous Peekaboo operator so it can interpret a natural-language goal,
choose tools (see, click, type, etc.), and report progress back to you. Supports resuming
previous sessions, dry-run planning, audio input, and JSON/quiet output modes for CI.
""",
usageExamples: [
CommandUsageExample(
command: "peekaboo agent \"Prepare the TestFlight build for review\"",
description: "Start a brand-new session with a natural-language brief."
),
CommandUsageExample(
command: "peekaboo agent --resume",
description: "Resume the most recent session without retyping the task."
),
CommandUsageExample(
command: "peekaboo agent --resume-session SESSION_ID --max-steps 12",
description: "Resume a known session while capping the step budget."
)
]
)
@Argument(help: "Natural language description of the task to perform (optional when using --resume)")
var task: String?
@Flag(name: .customLong("debug-terminal"), help: "Show detailed terminal detection info")
var debugTerminal = false
@Flag(names: [.short("q"), .long], help: "Quiet mode - only show final result")
var quiet = false
@Flag(name: .long, help: "Dry run - show planned steps without executing")
var dryRun = false
@Option(name: .long, help: "Maximum number of steps the agent can take")
var maxSteps: Int?
@Option(name: .long, help: "Queue mode for queued prompts: one-at-a-time (default) or all")
var queueMode: String?
@Option(name: .long, help: "AI model to use (allowed: gpt-5.1, claude-opus-4-5, or gemini-3-flash)")
var model: String?
@Flag(name: .long, help: "Resume the most recent session (use with task argument)")
var resume = false
@Option(name: .long, help: "Resume a specific session by ID")
var resumeSession: String?
@Flag(name: .long, help: "List available sessions")
var listSessions = false
@Flag(name: .long, help: "Disable session caching (always create new session)")
var noCache = false
@Flag(name: .long, help: "Enable audio input mode (record from microphone)")
var audio = false
@Option(name: .long, help: "Audio input file path (instead of microphone)")
var audioFile: String?
@Flag(name: .long, help: "Use real-time audio streaming (OpenAI only)")
var realtime = false
@Flag(name: .long, help: "Force simple output mode (no colors or rich formatting)")
var simple = false
@Flag(name: .long, help: "Disable colors in output")
var noColor = false
@Flag(name: .long, help: "Start an interactive chat session")
var chat = false
/// Computed property for output mode with smart detection and progressive enhancement
private var outputMode: OutputMode {
// Explicit user overrides first
if self.quiet { return .quiet }
if self.verbose || self.debugTerminal { return .verbose }
if self.simple { return .minimal }
if self.noColor { return .minimal }
// Check for environment-based forced modes
if let forcedMode = TerminalDetector.shouldForceOutputMode() {
return forcedMode
}
// Smart detection based on terminal capabilities
let capabilities = TerminalDetector.detectCapabilities()
return capabilities.recommendedOutputMode
}
@RuntimeStorage private var runtime: CommandRuntime?
var runtimeOptions = CommandRuntimeOptions()
private var resolvedRuntime: CommandRuntime {
guard let runtime else {
preconditionFailure("CommandRuntime must be configured before accessing runtime resources")
}
return runtime
}
@MainActor
var services: any PeekabooServiceProviding {
self.resolvedRuntime.services
}
private var logger: Logger {
self.resolvedRuntime.logger
}
var jsonOutput: Bool { self.runtime?.configuration.jsonOutput ?? self.runtimeOptions.jsonOutput }
var verbose: Bool { self.runtime?.configuration.verbose ?? self.runtimeOptions.verbose }
}
@MainActor
private final class TerminalModeGuard {
private let fd: Int32
private var original = termios()
private var active = false
init?(fd: Int32 = STDIN_FILENO) {
guard isatty(fd) == 1 else { return nil }
guard tcgetattr(fd, &self.original) == 0 else { return nil }
var raw = self.original
cfmakeraw(&raw)
raw.c_lflag |= tcflag_t(ISIG) // keep signals like Ctrl+C enabled
guard tcsetattr(fd, TCSANOW, &raw) == 0 else { return nil }
self.fd = fd
self.active = true
}
var fileDescriptor: Int32 { self.fd }
func restore() {
guard self.active else { return }
_ = tcsetattr(self.fd, TCSANOW, &self.original)
self.active = false
}
@MainActor
deinit {
self.restore()
}
}
final class EscapeKeyMonitor {
private var source: (any DispatchSourceRead)?
private var terminalGuard: TerminalModeGuard?
private let handler: @Sendable () async -> Void
private let queue = DispatchQueue(label: "peekaboo.escape.monitor")
init(handler: @escaping @Sendable () async -> Void) {
self.handler = handler
}
func start() {
guard self.source == nil else { return }
guard let termGuard = TerminalModeGuard() else { return }
let fd = termGuard.fileDescriptor
let handler = self.handler
let source = DispatchSource.makeReadSource(fileDescriptor: fd, queue: self.queue)
source.setEventHandler {
var buffer = [UInt8](repeating: 0, count: 16)
let count = read(fd, &buffer, buffer.count)
guard count > 0 else { return }
if buffer[..<count].contains(0x1B) {
Task.detached(priority: .userInitiated) {
await handler()
}
}
}
source.setCancelHandler {
termGuard.restore()
}
source.resume()
self.source = source
self.terminalGuard = termGuard
}
func stop() {
self.source?.cancel()
self.source = nil
self.terminalGuard = nil
}
}
@available(macOS 14.0, *)
extension AgentCommand {
@MainActor
mutating func run() async throws {
let runtime = await CommandRuntime.makeDefaultAsync()
try await self.run(using: runtime)
}
@MainActor
mutating func run(using runtime: CommandRuntime) async throws {
self.runtime = runtime
do {
try await self.runInternal(runtime: runtime)
} catch let error as DecodingError {
aiDebugPrint("DEBUG: Caught DecodingError in run(): \(error)")
throw error
} catch let error as NSError {
aiDebugPrint("DEBUG: Caught NSError in run(): \(error)")
aiDebugPrint("DEBUG: Domain: \(error.domain)")
aiDebugPrint("DEBUG: Code: \(error.code)")
aiDebugPrint("DEBUG: UserInfo: \(error.userInfo)")
throw error
} catch {
aiDebugPrint("DEBUG: Caught unknown error in run(): \(error)")
throw error
}
}
@MainActor
mutating func runInternal(runtime: CommandRuntime) async throws {
if self.isAgentDisabled() {
self.emitAgentUnavailableMessage()
return
}
let services = runtime.services
guard let agentService = services.agent else {
self.emitAgentUnavailableMessage()
return
}
let terminalCapabilities = TerminalDetector.detectCapabilities()
if self.debugTerminal {
self.printTerminalDetectionDebug(terminalCapabilities, actualMode: self.outputMode)
}
if self.listSessions {
try await self.showSessions(agentService)
return
}
guard self.hasConfiguredAIProvider(configuration: services.configuration) else {
self.emitAgentUnavailableMessage()
return
}
let shouldSuppressMCPLogs = !self.verbose && !self.debugTerminal
self.configureLogging(suppressingMCPLogs: shouldSuppressMCPLogs)
guard let peekabooAgent = agentService as? PeekabooAgentService else {
throw PeekabooError.commandFailed("Agent service not properly initialized")
}
let requestedModel: LanguageModel?
do {
requestedModel = try self.validatedModelSelection()
} catch {
self.printAgentExecutionError(error.localizedDescription)
return
}
guard await self.ensureAgentHasCredentials(peekabooAgent, requestedModel: requestedModel) else {
return
}
let chatPolicy = AgentChatLaunchPolicy()
let chatContext = AgentChatLaunchContext(
chatFlag: self.chat,
hasTaskInput: self.hasTaskInput,
listSessions: self.listSessions,
normalizedTaskInput: self.normalizedTaskInput,
capabilities: terminalCapabilities
)
let queueMode: QueueMode
do {
queueMode = try self.resolvedQueueMode()
} catch {
self.printAgentExecutionError(error.localizedDescription)
return
}
switch chatPolicy.strategy(for: chatContext) {
case .helpOnly:
self.printNonInteractiveChatHelp()
return
case let .interactive(initialPrompt):
try await self.runChatLoop(
peekabooAgent,
requestedModel: requestedModel,
initialPrompt: initialPrompt,
capabilities: terminalCapabilities,
queueMode: queueMode
)
return
case .none:
break
}
if try await self.handleSessionResumption(
peekabooAgent,
requestedModel: requestedModel,
maxSteps: self.maxSteps ?? 100,
queueMode: queueMode
) {
return
}
guard let executionTask = try await self.buildExecutionTask() else {
return
}
_ = try await self.executeAgentTask(
peekabooAgent,
task: executionTask,
requestedModel: requestedModel,
maxSteps: self.maxSteps ?? 100,
queueMode: queueMode
)
}
private func isAgentDisabled() -> Bool {
let value = ProcessInfo.processInfo.environment["PEEKABOO_DISABLE_AGENT"]?.lowercased()
return value == "1" || value == "true"
}
private func configureLogging(suppressingMCPLogs: Bool) {
if suppressingMCPLogs {
LoggingSystem.bootstrap { label in
var handler = StreamLogHandler.standardOutput(label: label)
if label.hasPrefix("tachikoma.mcp") {
handler.logLevel = .critical // hide MCP init chatter unless --verbose
} else {
handler.logLevel = .info
}
return handler
}
} else {
LoggingSystem.bootstrap(StreamLogHandler.standardOutput)
}
}
private func ensureAgentHasCredentials(
_ peekabooAgent: PeekabooAgentService,
requestedModel: LanguageModel?
) async -> Bool {
if let requestedModel {
if self.hasCredentials(for: requestedModel) {
return true
}
let providerName = self.providerDisplayName(for: requestedModel)
let envVar = self.providerEnvironmentVariable(for: requestedModel)
self.printAgentExecutionError(
"Missing API key for \(providerName). Set \(envVar) and retry."
)
return false
}
let hasCredential = await peekabooAgent.maskedApiKey != nil
if !hasCredential {
self.emitAgentUnavailableMessage()
}
return hasCredential
}
private func handleSessionResumption(
_ agentService: PeekabooAgentService,
requestedModel: LanguageModel?,
maxSteps: Int,
queueMode: QueueMode
) async throws -> Bool {
if let sessionId = self.resumeSession {
guard let continuationTask = self.task else {
self.printMissingTaskError(
message: "Task argument required when resuming session",
usage: "Usage: peekaboo agent --resume-session <session-id> \"<continuation-task>\""
)
return true
}
try await self.resumeAgentSession(
agentService,
request: ResumeAgentSessionRequest(
sessionId: sessionId,
task: continuationTask,
requestedModel: requestedModel,
maxSteps: maxSteps,
queueMode: queueMode
)
)
return true
}
if self.resume {
guard let continuationTask = self.task else {
self.printMissingTaskError(
message: "Task argument required when resuming",
usage: "Usage: peekaboo agent --resume \"<continuation-task>\""
)
return true
}
let sessions = try await agentService.listSessions()
if let mostRecent = sessions.first {
try await self.resumeAgentSession(
agentService,
request: ResumeAgentSessionRequest(
sessionId: mostRecent.id,
task: continuationTask,
requestedModel: requestedModel,
maxSteps: maxSteps,
queueMode: queueMode
)
)
} else {
if self.jsonOutput {
let error = ["success": false, "error": "No sessions found to resume"] as [String: Any]
let jsonData = try JSONSerialization.data(withJSONObject: error, options: .prettyPrinted)
print(String(data: jsonData, encoding: .utf8) ?? "{}")
} else {
print("\(TerminalColor.red)Error: No sessions found to resume\(TerminalColor.reset)")
}
}
return true
}
return false
}
private struct ResumeAgentSessionRequest {
let sessionId: String
let task: String
let requestedModel: LanguageModel?
let maxSteps: Int
let queueMode: QueueMode
}
func printMissingTaskError(message: String, usage: String) {
if self.jsonOutput {
let error = ["success": false, "error": message] as [String: Any]
if let jsonData = try? JSONSerialization.data(withJSONObject: error, options: .prettyPrinted),
let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
} else {
print("{\"success\":false,\"error\":\"\(message)\"}")
}
} else {
print("\(TerminalColor.red)Error: \(message)\(TerminalColor.reset)")
if !usage.isEmpty {
print(usage)
}
}
}
/// Render the agent execution result using either JSON output or a rich CLI transcript.
@MainActor
func displayResult(_ result: AgentExecutionResult, delegate: AgentOutputDelegate? = nil) {
if self.jsonOutput {
let response = [
"success": true,
"result": [
"content": result.content,
"sessionId": result.sessionId as Any,
"toolCalls": result.messages.flatMap { message in
message.content.compactMap { content in
if case let .toolCall(toolCall) = content {
return [
"id": toolCall.id,
"name": toolCall.name,
"arguments": String(describing: toolCall.arguments)
]
}
return nil
}
},
"metadata": [
"executionTime": result.metadata.executionTime,
"toolCallCount": result.metadata.toolCallCount,
"modelName": result.metadata.modelName
],
"usage": result.usage.map { usage in
[
"inputTokens": usage.inputTokens,
"outputTokens": usage.outputTokens,
"totalTokens": usage.totalTokens
]
} as Any
]
] as [String: Any]
if let jsonData = try? JSONSerialization.data(withJSONObject: response, options: .prettyPrinted) {
print(String(data: jsonData, encoding: .utf8) ?? "{}")
}
} else if self.outputMode == .quiet {
// Quiet mode - only show final result
print(result.content)
}
delegate?.showFinalSummaryIfNeeded(result)
}
// MARK: - Session Management
@MainActor
func showSessions(_ agentService: any AgentServiceProtocol) async throws {
guard let peekabooService = agentService as? PeekabooAgentService else {
throw PeekabooError.commandFailed("Agent service not properly initialized")
}
let sessionSummaries = try await peekabooService.listSessions()
let sessions = sessionSummaries.map { summary in
AgentSessionInfo(
id: summary.id,
task: summary.summary ?? "Unknown task",
created: summary.createdAt,
lastModified: summary.lastAccessedAt,
messageCount: summary.messageCount
)
}
guard !sessions.isEmpty else {
self.printNoAgentSessions()
return
}
if self.jsonOutput {
self.printSessionsJSON(sessions)
} else {
self.printSessionsList(sessions)
}
}
private func printNoAgentSessions() {
if self.jsonOutput {
let response = ["success": true, "sessions": []] as [String: Any]
let jsonData = try? JSONSerialization.data(withJSONObject: response, options: .prettyPrinted)
print(String(data: jsonData ?? Data(), encoding: .utf8) ?? "{}")
} else {
print("No agent sessions found.")
}
}
private func printSessionsJSON(_ sessions: [AgentSessionInfo]) {
let sessionData = sessions.map { session in
[
"id": session.id,
"createdAt": ISO8601DateFormatter().string(from: session.created),
"updatedAt": ISO8601DateFormatter().string(from: session.lastModified),
"messageCount": session.messageCount
]
}
let response = ["success": true, "sessions": sessionData] as [String: Any]
if let jsonData = try? JSONSerialization.data(withJSONObject: response, options: .prettyPrinted) {
print(String(data: jsonData, encoding: .utf8) ?? "{}")
}
}
private func printSessionsList(_ sessions: [AgentSessionInfo]) {
let headerLine = [
"\(TerminalColor.cyan)\(TerminalColor.bold)Agent Sessions:\(TerminalColor.reset)",
"\n"
].joined()
print(headerLine)
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .short
for (index, session) in sessions.prefix(10).indexed() {
self.printSessionLine(index: index, session: session, dateFormatter: dateFormatter)
if index < sessions.count - 1 {
print()
}
}
if sessions.count > 10 {
print([
"\n",
"\(TerminalColor.dim)... and \(sessions.count - 10) more sessions\(TerminalColor.reset)"
].joined())
}
let resumeHintLine = [
"\n",
"\(TerminalColor.dim)To resume: peekaboo agent --resume <session-id>",
" \"<continuation>\"\(TerminalColor.reset)"
].joined()
print(resumeHintLine)
}
private func printSessionLine(index: Int, session: AgentSessionInfo, dateFormatter: DateFormatter) {
let timeAgo = formatTimeAgo(session.lastModified)
let sessionLine = [
"\(TerminalColor.blue)\(index + 1).\(TerminalColor.reset)",
" ",
"\(TerminalColor.bold)\(session.id.prefix(8))\(TerminalColor.reset)"
].joined()
print(sessionLine)
print(" Messages: \(session.messageCount)")
print(" Last activity: \(timeAgo)")
}
private func resumeAgentSession(
_ agentService: PeekabooAgentService,
request: ResumeAgentSessionRequest
) async throws {
if !self.jsonOutput {
let resumingLine = [
"\(TerminalColor.cyan)\(TerminalColor.bold)",
"\(AgentDisplayTokens.Status.info)",
" Resuming session \(request.sessionId.prefix(8))...",
"\(TerminalColor.reset)",
"\n"
].joined()
print(resumingLine)
}
let outputDelegate = self.makeDisplayDelegate(for: request.task)
let streamingDelegate = self.makeStreamingDelegate(using: outputDelegate)
do {
let result = try await agentService.continueSession(
sessionId: request.sessionId,
userMessage: request.task,
model: request.requestedModel,
maxSteps: request.maxSteps,
dryRun: self.dryRun,
queueMode: request.queueMode,
eventDelegate: streamingDelegate
)
self.displayResult(result, delegate: outputDelegate)
} catch {
self.printAgentExecutionError("Failed to resume session: \(error.localizedDescription)")
throw error
}
}
func makeDisplayDelegate(for task: String) -> AgentOutputDelegate? {
guard !self.jsonOutput, !self.quiet else { return nil }
return AgentOutputDelegate(outputMode: self.outputMode, jsonOutput: self.jsonOutput, task: task)
}
func makeStreamingDelegate(using displayDelegate: AgentOutputDelegate?) -> (any AgentEventDelegate)? {
if let displayDelegate {
return displayDelegate
}
if self.jsonOutput || self.quiet {
return SilentAgentEventDelegate()
}
return nil
}
final class SilentAgentEventDelegate: AgentEventDelegate {
func agentDidEmitEvent(_ event: AgentEvent) {}
}
func printAgentExecutionError(_ message: String) {
if self.jsonOutput {
let error: [String: Any] = ["success": false, "error": message]
if let jsonData = try? JSONSerialization.data(withJSONObject: error, options: .prettyPrinted),
let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
} else {
print("{\"success\":false,\"error\":\"\(message)\"}")
}
} else {
print("\(TerminalColor.red)Error: \(message)\(TerminalColor.reset)")
}
}
func executeAgentTask(
_ agentService: PeekabooAgentService,
task: String,
requestedModel: LanguageModel?,
maxSteps: Int,
queueMode: QueueMode
) async throws -> AgentExecutionResult {
let outputDelegate = self.makeDisplayDelegate(for: task)
let streamingDelegate = self.makeStreamingDelegate(using: outputDelegate)
do {
let result = try await agentService.executeTask(
task,
maxSteps: maxSteps,
sessionId: nil,
model: requestedModel,
dryRun: self.dryRun,
queueMode: queueMode,
eventDelegate: streamingDelegate,
verbose: self.verbose
)
self.displayResult(result, delegate: outputDelegate)
let duration = String(format: "%.2f", result.metadata.executionTime)
let sessionId = result.sessionId ?? "none"
let finalTokens = result.usage?.totalTokens ?? 0
let status = result.metadata.context["status"] ?? "completed"
AutomationEventLogger.log(
.agent,
"result status=\(status) task='\(task)' model=\(result.metadata.modelName) duration=\(duration)s "
+ "tools=\(result.metadata.toolCallCount) dry_run=\(self.dryRun) "
+ "session=\(sessionId) tokens=\(finalTokens)"
)
return result
} catch {
self.printAgentExecutionError("Agent execution failed: \(error.localizedDescription)")
throw error
}
}
private var normalizedTaskInput: String? {
guard let task else { return nil }
let trimmed = task.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
private var hasTaskInput: Bool {
self.normalizedTaskInput != nil || self.audio || self.audioFile != nil
}
var resolvedMaxSteps: Int { self.maxSteps ?? 100 }
private func resolvedQueueMode() throws -> QueueMode {
guard let raw = self.queueMode?.trimmingCharacters(in: .whitespacesAndNewlines), !raw.isEmpty else {
return .oneAtATime
}
switch raw.lowercased() {
case "one", "one-at-a-time", "single", "sequential", "1":
return .oneAtATime
case "all", "batch", "together":
return .all
default:
throw PeekabooError.invalidInput("Invalid queue mode '\(raw)'. Use one-at-a-time or all.")
}
}
func printChatWelcome(sessionId: String?, modelDescription: String, queueMode: QueueMode) {
guard !self.quiet else { return }
let header = [
TerminalColor.cyan,
TerminalColor.bold,
"Interactive agent chat",
TerminalColor.reset,
" – model: ",
modelDescription,
" • queue: ",
queueMode == .all ? "all" : "one-at-a-time"
].joined()
print(header)
if let sessionId {
print("\(TerminalColor.dim)Resuming session \(sessionId.prefix(8))\(TerminalColor.reset)")
} else {
print("\(TerminalColor.dim)A new session will be created on the first prompt\(TerminalColor.reset)")
}
print()
}
func printChatHelpIntro() {
guard !self.quiet else { return }
print("Type /help for chat commands (Ctrl+C to exit).")
self.printChatHelpMenu()
}
func printChatHelpMenu() {
guard !self.quiet else { return }
self.chatHelpLines.forEach { print($0) }
}
private var chatHelpText: String {
"""
Chat commands:
• Type any prompt and press Return to run it.
• /help Show this menu again.
• Esc Cancel the active run (if one is in progress).
• Ctrl+C Cancel when running; exit immediately when idle.
• Ctrl+D Exit when idle (EOF).
"""
}
var chatHelpLines: [String] {
self.chatHelpText
.split(separator: "\n", omittingEmptySubsequences: false)
.map(String.init)
}
private func printCapabilityFlag(_ label: String, supported: Bool, detail: String? = nil) {
let status = supported ? AgentDisplayTokens.Status.success : AgentDisplayTokens.Status.failure
let detailSuffix = detail.map { " (\($0))" } ?? ""
print(" • \(label): \(status)\(detailSuffix)")
}
/// Print detailed terminal detection debugging information
func printTerminalDetectionDebug(_ capabilities: TerminalCapabilities, actualMode: OutputMode) {
// Print detailed terminal detection debugging information
print("\n" + String(repeating: "=", count: 60))
print("\(TerminalColor.bold)\(TerminalColor.cyan)TERMINAL DETECTION DEBUG (-vv)\(TerminalColor.reset)")
print(String(repeating: "=", count: 60))
// Basic terminal info
print("[term] \(TerminalColor.bold)Terminal Type:\(TerminalColor.reset) \(capabilities.termType ?? "unknown")")
print(
"[size] \(TerminalColor.bold)Dimensions:\(TerminalColor.reset) \(capabilities.width)x\(capabilities.height)"
)
// Capability flags
print("\(AgentDisplayTokens.Status.running) \(TerminalColor.bold)Capabilities:\(TerminalColor.reset)")
self.printCapabilityFlag("Interactive", supported: capabilities.isInteractive, detail: "isatty check")
self.printCapabilityFlag("Colors", supported: capabilities.supportsColors, detail: "ANSI support")
self.printCapabilityFlag("True Color", supported: capabilities.supportsTrueColor, detail: "24-bit")
print(" • Dimensions: \(capabilities.width)x\(capabilities.height)")
// Environment info
print("[env] \(TerminalColor.bold)Environment:\(TerminalColor.reset)")
self.printCapabilityFlag("CI Environment", supported: capabilities.isCI)
self.printCapabilityFlag("Piped Output", supported: capabilities.isPiped)
// Environment variables
let env = ProcessInfo.processInfo.environment
print("\(AgentDisplayTokens.Status.running) \(TerminalColor.bold)Environment Variables:\(TerminalColor.reset)")
print(" • TERM: \(env["TERM"] ?? "not set")")
print(" • COLORTERM: \(env["COLORTERM"] ?? "not set")")
print(" • NO_COLOR: \(env["NO_COLOR"] != nil ? "set" : "not set")")
print(" • FORCE_COLOR: \(env["FORCE_COLOR"] ?? "not set")")
print(" • PEEKABOO_OUTPUT_MODE: \(env["PEEKABOO_OUTPUT_MODE"] ?? "not set")")
// Recommended vs actual mode
let recommendedMode = capabilities.recommendedOutputMode
print("[focus] \(TerminalColor.bold)Recommended Mode:\(TerminalColor.reset) \(recommendedMode.description)")
print("[focus] \(TerminalColor.bold)Actual Mode:\(TerminalColor.reset) \(actualMode.description)")
if recommendedMode != actualMode {
let modeOverrideLine = [
"\(AgentDisplayTokens.Status.warning) ",
"\(TerminalColor.yellow)Mode Override Detected\(TerminalColor.reset)",
" - explicit flag or environment variable used"
].joined()
print(modeOverrideLine)
}
// Show decision logic
if !capabilities.isInteractive || capabilities.isCI || capabilities.isPiped {
print(" → Minimal mode (non-interactive/CI/piped)")
} else if capabilities.supportsColors {
print(" → Enhanced mode (colors available)")
} else {
print(" → Compact mode (basic terminal)")
}
print(String(repeating: "=", count: 60) + "\n")
}
private func hasConfiguredAIProvider(configuration: PeekabooCore.ConfigurationManager) -> Bool {
let hasOpenAI = configuration.getOpenAIAPIKey()?.isEmpty == false
let hasAnthropic = configuration.getAnthropicAPIKey()?.isEmpty == false
let hasGemini = configuration.getGeminiAPIKey()?.isEmpty == false
return hasOpenAI || hasAnthropic || hasGemini
}
private func emitAgentUnavailableMessage() {
if self.jsonOutput {
let error = [
"success": false,
"error": "Agent service not available. Please set OPENAI_API_KEY, ANTHROPIC_API_KEY, or GEMINI_API_KEY."
] as [String: Any]
if let jsonData = try? JSONSerialization.data(withJSONObject: error, options: .prettyPrinted),
let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
} else {
print("{\"success\":false,\"error\":\"Agent service not available\"}")
}
} else {
let errorPrefix = [
"\(TerminalColor.red)Error: Agent service not available.",
" Please set OPENAI_API_KEY, ANTHROPIC_API_KEY, or GEMINI_API_KEY."
].joined()
let errorMessageLine = [errorPrefix, "\(TerminalColor.reset)"].joined()
print(errorMessageLine)
}
}
// MARK: - Model Parsing
func parseModelString(_ modelString: String) -> LanguageModel? {
let trimmed = modelString.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else { return nil }
guard let parsed = LanguageModel.parse(from: trimmed) else {
return nil
}
switch parsed {
case let .openai(model):
if Self.supportedOpenAIInputs.contains(model) {
return .openai(.gpt51)
}
case let .anthropic(model):
if Self.supportedAnthropicInputs.contains(model) {
return .anthropic(.opus45)
}
case let .google(model):
if Self.supportedGoogleInputs.contains(model) {
return .google(.gemini3Flash)
}
default:
break
}
return nil
}
func validatedModelSelection() throws -> LanguageModel? {
guard let modelString = self.model else { return nil }
guard let parsed = self.parseModelString(modelString) else {
throw PeekabooError.invalidInput(
"Unsupported model '\(modelString)'. Allowed values: \(Self.allowedModelList)"
)
}
return parsed
}
private static let supportedOpenAIInputs: Set<LanguageModel.OpenAI> = [
.gpt51,
.gpt5,
.gpt5Pro,
.gpt5Mini,
.gpt5Nano,
.gpt5Thinking,
.gpt5ThinkingMini,
.gpt5ThinkingNano,
.gpt5ChatLatest,
.gpt4o,
.gpt4oMini,
.gpt4oRealtime,
.o4Mini,
]
private static let supportedAnthropicInputs: Set<LanguageModel.Anthropic> = [
.sonnet45,
.sonnet4,
.sonnet4Thinking,
.opus45,
.opus4,
.opus4Thinking,
]
private static let supportedGoogleInputs: Set<LanguageModel.Google> = [
.gemini3Flash,
]
private static var allowedModelList: String {
let openAIModels = Self.supportedOpenAIInputs.map(\.modelId)
let anthropicModels = Self.supportedAnthropicInputs.map(\.modelId)
let googleModels = Self.supportedGoogleInputs.map(\.userFacingModelId)
return (openAIModels + anthropicModels + googleModels).sorted().joined(separator: ", ")
}
@MainActor
private func hasCredentials(for model: LanguageModel) -> Bool {
let configuration = self.services.configuration
switch model {
case .openai:
return configuration.getOpenAIAPIKey()?.isEmpty == false
case .anthropic:
return configuration.getAnthropicAPIKey()?.isEmpty == false
case .google:
return configuration.getGeminiAPIKey()?.isEmpty == false
default:
return false
}
}
private func providerDisplayName(for model: LanguageModel) -> String {
switch model {
case .openai:
"OpenAI"
case .anthropic:
"Anthropic"
case .google:
"Google"
default:
"the selected provider"
}
}
private func providerEnvironmentVariable(for model: LanguageModel) -> String {
switch model {
case .openai:
"OPENAI_API_KEY"
case .anthropic:
"ANTHROPIC_API_KEY"
case .google:
"GEMINI_API_KEY"
default:
"provider API key"
}
}
}
extension AgentCommand: ParsableCommand {}
extension AgentCommand: AsyncRuntimeCommand {}