Welcome back! In Chapter 3: Safety & Abort Mechanism (Esc Hotkey), we implemented a "Kill Switch" to stop the AI if it starts behaving erratically.
We now have the Brain (MCP Server), the Hands (Executor), and the Emergency Brake (Safety). However, these parts are currently floating around separately. We need a central hub to connect them all to the specific environment we are running in (the Command Line Interface).
Think of the core Computer Use library as a Universal Travel Appliance (like a hair dryer). It knows how to blow air, but it doesn't fit into the wall socket of every country.
The Host Adapter is the Travel Adapter.
Imagine the application is a plane. You don't want two different pilots (Executors) fighting over the controls, or two different black boxes (Loggers) recording data in different formats.
The Goal: We need a Singleton (a single, unique instance) that acts as the source of truth for the entire application lifecycle. Whenever any part of the app asks, "How do I click the mouse?" or "Am I allowed to record the screen?", they ask the Host Adapter.
DebugLogger that translates generic log messages into the format our CLI tools understand.The usage is designed to be incredibly simple: just ask for it.
Anywhere in your code (like in the MCP Server or the Executor), you can call this function:
// From any file in the project
import { getComputerUseHostAdapter } from './hostAdapter'
// Get the one-and-only instance
const adapter = getComputerUseHostAdapter()
Explanation: This function guarantees you get the initialized adapter. If it doesn't exist yet, it creates it.
Once you have the adapter, you can access the tools we built in previous chapters.
// Example: Using the adapter to check permissions
async function startSession() {
const adapter = getComputerUseHostAdapter()
// Ask the adapter: "Are we allowed to see the screen?"
const permissions = await adapter.ensureOsPermissions()
if (!permissions.granted) {
console.error("Please grant Screen Recording permissions!")
}
}
Explanation: The generic code doesn't need to know how to check macOS permissions; it just asks the adapter to do it.
How does hostAdapter.ts ensure there is only one instance and that everything is wired up correctly?
Let's look at how we build this "Universal Adapter" in hostAdapter.ts.
First, we define how logging should work in this specific CLI environment. We implement a Logger interface that routes messages to our debug file.
// From hostAdapter.ts
class DebugLogger implements Logger {
info(message: string, ...args: unknown[]): void {
// Redirect generic info logs to our specific debug tool
logForDebugging(format(message, ...args), { level: 'info' })
}
error(message: string, ...args: unknown[]): void {
logForDebugging(format(message, ...args), { level: 'error' })
}
// ... other log levels (debug, warn) follow the same pattern
}
Explanation: The core library just calls logger.info("..."). This class catches that call and sends it to logForDebugging, which writes to our CLI's output stream.
This is the most important part of the file. It ensures we only build the adapter once.
// From hostAdapter.ts
let cached: ComputerUseHostAdapter | undefined
export function getComputerUseHostAdapter(): ComputerUseHostAdapter {
// 1. If we already built it, return it immediately!
if (cached) return cached
// 2. Otherwise, build the object
cached = {
serverName: COMPUTER_USE_MCP_SERVER_NAME,
logger: new DebugLogger(),
// ... (continued below)
Explanation: The variable cached sits outside the function. It remembers the result between calls.
We connect the Executor we built in Chapter 2 and configure its settings (Gates).
// From hostAdapter.ts
// Connect the Executor (Hands)
executor: createCliExecutor({
// Check feature flags (Gates) for animation preferences
getMouseAnimationEnabled: () => getChicagoSubGates().mouseAnimation,
getHideBeforeActionEnabled: () => getChicagoSubGates().hideBeforeAction,
}),
// Connect the Feature Flags
isDisabled: () => !getChicagoEnabled(),
getSubGates: getChicagoSubGates,
Explanation: We pass functions (callbacks) for settings. This allows us to change settings like mouseAnimation on the fly without restarting the adapter.
Finally, the adapter provides a way to check if the OS is happy.
// From hostAdapter.ts
ensureOsPermissions: async () => {
// Load the native Swift module
const cu = requireComputerUseSwift()
// Check macOS specific permissions
const accessibility = cu.tcc.checkAccessibility()
const screenRecording = cu.tcc.checkScreenRecording()
// Return a simple Summary
return accessibility && screenRecording
? { granted: true }
: { granted: false, accessibility, screenRecording }
},
Explanation: This abstracts the complex Swift calls into a simple granted: true/false result.
cropRawPatchYou might notice this property in the code:
// From hostAdapter.ts
cropRawPatch: () => null,
Explanation: Some environments verify clicks by decoding images pixel-by-pixel. This is heavy and slow. In our CLI adapter, we return null to say, "Skip the pixel validation, trust the coordinates." This keeps our adapter fast and lightweight.
In this chapter, we built the Host Adapter, the central nervous system of our application.
Now that we have a central adapter, we face a new problem. Since this is an async environment, what happens if the user tries to type something while the AI is moving the mouse? We need to prevent collisions.
Next Chapter: Session Locking (Concurrency Control)
Generated by Code IQ