import { z } from "zod";
import {
ToolContext,
ApplicationListData,
WindowListData,
ApplicationInfo,
WindowInfo,
TargetApplicationInfo,
SwiftCliResponse,
ToolResponse,
} from "../types/index.js";
import { parseAIProviders, getProviderStatus } from "../utils/ai-providers.js";
import { executeSwiftCli, execPeepIt } from "../utils/peepit-cli.js";
import { generateServerStatusString } from "../utils/server-status.js";
import fs from "fs/promises";
import path from "path";
import { existsSync, accessSync, constants } from "fs";
import os from "os";
import { fileURLToPath } from "url";
import { Logger } from "pino";
export const listToolSchema = z
.object({
item_type: z.preprocess(
(val) => {
// Convert empty string to undefined
if (val === "" || val === null) {
return undefined;
}
return val;
},
z
.enum(["running_applications", "application_windows", "server_status"])
.optional(),
)
.describe(
"Specifies the type of items to list. If omitted or empty, it defaults to 'application_windows' if 'app' is provided, otherwise 'running_applications'. Valid options are:\n" +
"- `running_applications`: Lists all currently running applications.\n" +
"- `application_windows`: Lists open windows for a specific application. Requires the `app` parameter.\n" +
"- `server_status`: Returns information about the PeepIt MCP server.",
),
app: z
.string()
.optional()
.describe(
"Required when `item_type` is `application_windows`. " +
"Specifies the target application by its name (e.g., \"Safari\", \"TextEdit\"), bundle ID, or process ID (e.g., \"PID:663\"). " +
"Fuzzy matching is used for names, so partial names may work.",
),
include_window_details: z.preprocess(
(val) => {
// Handle empty string or null/undefined
if (val === "" || val === null || val === undefined) {
return undefined;
}
// If it's already an array, return as-is
if (Array.isArray(val)) {
return val;
}
// If it's a string that looks like JSON, try to parse it
if (typeof val === "string") {
try {
const parsed = JSON.parse(val);
if (Array.isArray(parsed)) {
return parsed;
}
} catch {
// Not valid JSON, treat as single item
}
// If it's a comma-separated string, split it
if (val.includes(",")) {
return val.split(",").map(s => s.trim());
}
// Single string value, wrap in array
return [val.trim()];
}
return val;
},
z
.array(z.enum(["off_screen", "bounds", "ids"]))
.optional()
.describe(
"Optional, only applicable when `item_type` is `application_windows`. " +
"Specifies additional details to include for each window. Provide an array of strings. Example: `[\"bounds\", \"ids\"]`.\n" +
"- `ids`: Include window ID.\n" +
"- `bounds`: Include window position and size (x, y, width, height).\n" +
"- `off_screen`: Indicate if the window is currently off-screen.",
),
),
})
.refine(
(data) =>
data.item_type !== "application_windows" ||
(data.app !== undefined && data.app.trim() !== ""),
{
message: "For 'application_windows', 'app' identifier is required.",
path: ["app"],
},
)
.refine(
(data) =>
!data.include_window_details ||
data.include_window_details.length === 0 ||
data.item_type === "application_windows" ||
(data.app !== undefined && data.app.trim() !== ""),
{
message:
"'include_window_details' is only applicable when 'item_type' is 'application_windows' or when 'app' is provided.",
path: ["include_window_details"],
},
)
.refine(
(data) =>
data.item_type !== "server_status" ||
(data.app === undefined &&
(data.include_window_details === undefined || data.include_window_details.length === 0)),
{
message:
"'app' and 'include_window_details' not applicable for 'server_status'.",
path: ["item_type"],
},
)
.describe(
"Lists various system items, providing situational awareness. " +
"The `item_type` is optional and will be inferred if omitted (defaults to 'application_windows' if 'app' is provided, else 'running_applications'). " +
"App identifier uses fuzzy matching for convenience.",
);
export type ListToolInput = z.infer<typeof listToolSchema>;
export async function listToolHandler(
input: ListToolInput,
context: ToolContext,
): Promise<ToolResponse> {
const { logger } = context;
try {
logger.debug({ input }, "Processing peepit.list tool call");
// Handle server_status directly without calling Swift CLI
if (input.item_type === "server_status") {
// Get package version and root directory
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const packageRootDir = path.resolve(__dirname, "../..");
const packageJsonPath = path.join(packageRootDir, "package.json");
const packageJson = JSON.parse(
await fs.readFile(packageJsonPath, "utf-8"),
);
const version = packageJson.version || "[unknown]";
return await handleServerStatus(version, packageRootDir, logger);
}
// Build Swift CLI arguments
const args = buildSwiftCliArgs(input);
logger.debug({ args }, "Swift CLI arguments built");
// Execute Swift CLI
const swiftResponse = await executeSwiftCli(args, logger, { timeout: 15000 });
if (!swiftResponse.success) {
logger.error({ error: swiftResponse.error }, "Swift CLI returned error");
const errorMessage = swiftResponse.error?.message || "Unknown error";
const errorDetails = swiftResponse.error?.details;
const fullErrorMessage = errorDetails
? `${errorMessage}\n${errorDetails}`
: errorMessage;
return {
content: [
{
type: "text" as const,
text: `List operation failed: ${fullErrorMessage}`,
},
],
isError: true,
_meta: {
backend_error_code: swiftResponse.error?.code,
},
};
}
// Check if data is null or undefined
if (!swiftResponse.data) {
logger.error("Swift CLI reported success but no data was returned.");
return {
content: [
{
type: "text" as const,
text: "List operation failed: Invalid response from list utility (no data).",
},
],
isError: true,
_meta: {
backend_error_code: "INVALID_RESPONSE_NO_DATA",
},
};
}
// Process the response based on item type
let effective_item_type: string;
if (input.item_type && typeof input.item_type === "string" && input.item_type.trim() !== "") {
effective_item_type = input.item_type.trim();
} else if (input.app) {
effective_item_type = "application_windows";
} else {
effective_item_type = "running_applications";
}
if (effective_item_type === "running_applications") {
return handleApplicationsList(
swiftResponse.data as ApplicationListData,
swiftResponse,
);
} else if (effective_item_type === "application_windows") {
return handleWindowsList(
swiftResponse.data as WindowListData,
input,
swiftResponse,
);
}
// Fallback
return {
content: [
{
type: "text" as const,
text: "List operation completed with unknown item type.",
},
],
};
} catch (error) {
logger.error({ error }, "Unexpected error in list tool handler");
return {
content: [
{
type: "text" as const,
text: `Unexpected error: ${error instanceof Error ? error.message : "Unknown error"}`,
},
],
isError: true,
};
}
}
async function handleServerStatus(
version: string,
packageRootDir: string,
logger: Logger,
): Promise<ToolResponse> {
const statusSections: string[] = [];
// 1. Server version and AI providers
statusSections.push(generateServerStatusString(version));
// 2. Native Binary Status
statusSections.push("\n## Native Binary (Swift CLI) Status");
const cliPath = process.env.PEEPIT_CLI_PATH || path.join(packageRootDir, "peepit");
let cliStatus = "❌ Not found";
let cliVersion = "Unknown";
let cliExecutable = false;
if (existsSync(cliPath)) {
try {
accessSync(cliPath, constants.X_OK);
cliExecutable = true;
// Try to get CLI version
const versionResult = await execPeepIt(
["--version"],
packageRootDir,
{ expectSuccess: false },
);
if (versionResult.success && versionResult.data) {
cliVersion = versionResult.data.trim();
cliStatus = "✅ Found and executable";
} else {
cliStatus = "⚠️ Found but version check failed";
}
} catch (_error) {
cliStatus = "⚠️ Found but not executable";
}
}
statusSections.push(`- Location: ${cliPath}`);
statusSections.push(`- Status: ${cliStatus}`);
statusSections.push(`- Version: ${cliVersion}`);
statusSections.push(`- Executable: ${cliExecutable ? "Yes" : "No"}`);
// 3. Permissions Status
statusSections.push("\n## System Permissions");
if (cliExecutable) {
try {
const permissionsResult = await execPeepIt(
["list", "server_status", "--json-output"],
packageRootDir,
{ expectSuccess: false },
);
if (permissionsResult.success && permissionsResult.data) {
const status = JSON.parse(permissionsResult.data);
if (status.data?.permissions) {
const perms = status.data.permissions;
statusSections.push(`- Screen Recording: ${perms.screen_recording ? "✅ Granted" : "❌ Not granted"}`);
statusSections.push(`- Accessibility: ${perms.accessibility ? "✅ Granted" : "❌ Not granted"}`);
} else {
statusSections.push("- Unable to determine permissions status");
}
} else {
statusSections.push("- Unable to check permissions (CLI error)");
}
} catch (error) {
statusSections.push(`- Unable to check permissions: ${error}`);
}
} else {
statusSections.push("- Unable to check permissions (CLI not available)");
}
// 4. AI Provider Status
statusSections.push("\n## AI Provider Status");
const aiProvidersEnv = process.env.PEEPIT_AI_PROVIDERS;
if (!aiProvidersEnv || !aiProvidersEnv.trim()) {
statusSections.push("❌ No AI providers configured");
statusSections.push("Configure PEEPIT_AI_PROVIDERS environment variable to enable image analysis");
} else {
const providers = parseAIProviders(aiProvidersEnv);
if (providers.length === 0) {
statusSections.push("❌ Invalid AI provider configuration");
statusSections.push(`Raw config: ${aiProvidersEnv}`);
} else {
statusSections.push(`Found ${providers.length} configured provider${providers.length !== 1 ? "s" : ""}:`);
for (const provider of providers) {
statusSections.push(`\n### ${provider.provider}/${provider.model}`);
try {
const status = await getProviderStatus(provider, logger);
if (status.available) {
statusSections.push("✅ **Available and working**");
if (status.details?.modelList && status.details.modelList.length > 0) {
const modelCount = status.details.modelList.length;
statusSections.push(`- Found ${modelCount} available model${modelCount !== 1 ? "s" : ""}`);
}
} else {
statusSections.push("❌ **Not available**");
if (status.error) {
statusSections.push(`- Error: ${status.error}`);
}
// Provide specific troubleshooting info
if (status.details) {
const details = status.details;
if (provider.provider.toLowerCase() === "openai") {
if (!details.apiKeyPresent) {
statusSections.push("- Missing: Set OPENAI_API_KEY environment variable");
} else if (!details.serverReachable) {
statusSections.push("- Network issue: Cannot reach OpenAI API");
} else if (details.apiKeyPresent && !status.available) {
statusSections.push("- Invalid API key or insufficient permissions");
}
} else if (provider.provider.toLowerCase() === "ollama") {
if (!details.serverReachable) {
statusSections.push("- Ollama server not running or not reachable");
statusSections.push("- Start with: ollama serve");
} else if (!details.modelAvailable) {
statusSections.push(`- Model '${provider.model}' not installed`);
statusSections.push(`- Install with: ollama pull ${provider.model}`);
if (details.modelList && details.modelList.length > 0) {
statusSections.push(`- Available models: ${details.modelList.slice(0, 5).join(", ")}${details.modelList.length > 5 ? "..." : ""}`);
}
}
}
}
}
} catch (error) {
statusSections.push("❌ **Status check failed**");
statusSections.push(`- Error: ${error instanceof Error ? error.message : "Unknown error"}`);
}
}
}
}
// 5. Environment Configuration
statusSections.push("\n## Environment Configuration");
const logFile = process.env.PEEPIT_LOG_FILE || path.join(os.homedir(), "Library/Logs/peepit-mcp.log");
const logLevel = process.env.PEEPIT_LOG_LEVEL || "info";
const consoleLogging = process.env.PEEPIT_CONSOLE_LOGGING === "true";
const aiProviders = process.env.PEEPIT_AI_PROVIDERS || "None configured";
const customCliPath = process.env.PEEPIT_CLI_PATH;
const defaultSavePath = process.env.PEEPIT_DEFAULT_SAVE_PATH || "Not set";
statusSections.push(`- Log File: ${logFile}`);
// Check log file accessibility
try {
const logDir = path.dirname(logFile);
await fs.access(logDir, constants.W_OK);
statusSections.push(" Status: ✅ Directory writable");
} catch (_error) {
statusSections.push(" Status: ❌ Directory not writable");
}
statusSections.push(`- Log Level: ${logLevel}`);
statusSections.push(`- Console Logging: ${consoleLogging ? "Enabled" : "Disabled"}`);
statusSections.push(`- AI Providers: ${aiProviders}`);
statusSections.push(`- Custom CLI Path: ${customCliPath || "Not set (using default)"}`);
statusSections.push(`- Default Save Path: ${defaultSavePath}`);
// 6. Configuration Issues
statusSections.push("\n## Configuration Issues");
const issues: string[] = [];
if (!cliExecutable) {
issues.push("❌ Swift CLI not found or not executable");
}
if (cliVersion !== version && cliVersion !== "Unknown") {
issues.push(`⚠️ Version mismatch: Server ${version} vs CLI ${cliVersion}`);
}
if (!aiProviders || aiProviders === "None configured") {
issues.push("⚠️ No AI providers configured (analysis features will be limited)");
}
// Check if log directory is writable
try {
const logDir = path.dirname(logFile);
await fs.access(logDir, constants.W_OK);
} catch {
issues.push(`❌ Log directory not writable: ${path.dirname(logFile)}`);
}
if (issues.length === 0) {
statusSections.push("✅ No configuration issues detected");
} else {
issues.forEach(issue => statusSections.push(issue));
}
// 7. System Information
statusSections.push("\n## System Information");
statusSections.push(`- Platform: ${os.platform()}`);
statusSections.push(`- Architecture: ${os.arch()}`);
statusSections.push(`- OS Version: ${os.release()}`);
statusSections.push(`- Node.js Version: ${process.version}`);
const fullStatus = statusSections.join("\n");
logger.info({ status: fullStatus }, "Server status info generated");
return {
content: [
{
type: "text" as const,
text: fullStatus,
},
],
};
}
export function buildSwiftCliArgs(input: ListToolInput): string[] {
const args: string[] = ["list"];
// Determine item type with defensive checks
let itemType: string;
if (input.item_type && typeof input.item_type === "string" && input.item_type.trim() !== "") {
itemType = input.item_type.trim();
} else if (input.app) {
itemType = "application_windows";
} else {
itemType = "running_applications";
}
// Add appropriate subcommand
switch (itemType) {
case "running_applications":
args.push("apps");
break;
case "application_windows":
args.push("windows");
if (input.app && input.app.trim()) {
args.push("--app", input.app.trim());
}
if (input.include_window_details && input.include_window_details.length > 0) {
const details = input.include_window_details.filter(d => d && d.trim()).join(",");
if (details) {
args.push("--include-details", details);
}
}
break;
case "server_status":
args.push("server_status");
break;
default:
// Fallback to apps if unknown type
args.push("apps");
break;
}
// Filter out any undefined or empty values
return args.filter(arg => arg !== undefined && arg !== null && arg !== "");
}
function handleApplicationsList(
data: ApplicationListData,
swiftResponse: SwiftCliResponse,
): ToolResponse & { application_list: ApplicationInfo[] } {
const apps = data.applications || [];
let summary = `Found ${apps.length} running application${apps.length !== 1 ? "s" : ""}`;
if (apps.length > 0) {
summary += ":\n\n";
apps.forEach((app, index) => {
summary += `${index + 1}. ${app.app_name}`;
if (app.bundle_id) {
summary += ` (${app.bundle_id})`;
}
summary += ` - PID: ${app.pid}`;
if (app.is_active) {
summary += " [ACTIVE]";
}
summary += ` - Windows: ${app.window_count}\n`;
});
}
// Add messages from Swift CLI if any
if (swiftResponse.messages?.length) {
summary += `\nMessages: ${swiftResponse.messages.join("; ")}`;
}
return {
content: [
{
type: "text" as const,
text: summary,
},
],
application_list: apps,
};
}
function handleWindowsList(
data: WindowListData,
input: ListToolInput,
swiftResponse: SwiftCliResponse,
): ToolResponse & {
window_list?: WindowInfo[];
target_application_info?: TargetApplicationInfo;
} {
const windows = data.windows || [];
const appInfo = data.target_application_info;
// Validate required fields
if (!appInfo) {
return {
content: [
{
type: "text" as const,
text: "List operation failed: Invalid response from list utility (missing application info).",
},
],
isError: true,
_meta: {
backend_error_code: "INVALID_RESPONSE_MISSING_APP_INFO",
},
};
}
let summary = `Found ${windows.length} window${windows.length !== 1 ? "s" : ""} for application: ${appInfo.app_name}`;
if (appInfo.bundle_id) {
summary += ` (${appInfo.bundle_id})`;
}
summary += ` - PID: ${appInfo.pid}`;
if (windows.length > 0) {
summary += "\n\nWindows:\n";
windows.forEach((window, index) => {
summary += `${index + 1}. "${window.window_title}"`;
if (window.window_id !== undefined) {
summary += ` [ID: ${window.window_id}]`;
}
if (window.is_on_screen !== undefined) {
summary += window.is_on_screen ? " [ON-SCREEN]" : " [OFF-SCREEN]";
}
if (window.bounds) {
summary += ` [${window.bounds.x},${window.bounds.y} ${window.bounds.width}×${window.bounds.height}]`;
}
summary += "\n";
});
}
// Add messages from Swift CLI if any
if (swiftResponse.messages?.length) {
summary += `\nMessages: ${swiftResponse.messages.join("; ")}`;
}
return {
content: [
{
type: "text" as const,
text: summary,
},
],
window_list: windows,
target_application_info: appInfo,
};
}