### Initialize and Start Hook Server Source: https://context7.com/wxtsky/codeisland/llms.txt Sets up the HookServer to listen on a Unix socket. Cleans up any existing socket file and configures network parameters. Handles new connections and updates the socket's file permissions upon becoming ready. ```swift // HookServer listens on Unix socket at /tmp/codeisland-.sock @MainActor class HookServer { private let appState: AppState nonisolated static var socketPath: String { SocketPath.path } private var listener: NWListener? func start() { // Clean up stale socket unlink(HookServer.socketPath) let params = NWParameters() params.defaultProtocolStack.transportProtocol = NWProtocolTCP.Options() params.requiredLocalEndpoint = NWEndpoint.unix(path: HookServer.socketPath) listener = try? NWListener(using: params) listener?.newConnectionHandler = { [weak self] connection in Task { @MainActor in self?.handleConnection(connection) } } listener?.stateUpdateHandler = { state in if case .ready = state { chmod(HookServer.socketPath, 0o700) // Restrict to current user } } listener?.start(queue: .main) } // Process incoming requests private func processRequest(data: Data, connection: NWConnection) { guard let event = HookEvent(from: data) else { sendResponse(connection: connection, data: Data("{\"error\":\"parse_failed\"}".utf8)) return } // Auto-approve safe internal tools let autoApproveTools: Set = [ "TaskCreate", "TodoRead", "TodoWrite", "EnterPlanMode", "ExitPlanMode" ] if event.eventName == "PermissionRequest" { if let toolName = event.toolName, autoApproveTools.contains(toolName) { let response = #"{"hookSpecificOutput":{"decision":{"behavior":"allow"}}}"# sendResponse(connection: connection, data: Data(response.utf8)) return } // Handle permission request with UI Task { let responseBody = await withCheckedContinuation { continuation in appState.handlePermissionRequest(event, continuation: continuation) } self.sendResponse(connection: connection, data: responseBody) } } else { // Regular events: process and respond immediately appState.handleEvent(event) sendResponse(connection: connection, data: Data("{}".utf8)) } } } ``` -------------------------------- ### Install CodeIsland via Homebrew Source: https://github.com/wxtsky/codeisland/blob/main/README.zh-CN.md Use Homebrew to install the CodeIsland application on macOS. ```bash brew tap wxtsky/tap brew install --cask codeisland ``` -------------------------------- ### Define and Manage CLI Hook Configurations Source: https://context7.com/wxtsky/codeisland/llms.txt Defines the CLIConfig structure and provides methods for installing, enabling, and disabling hooks for various AI tools. ```swift // CLI configuration structure struct CLIConfig { let name: String // Display name let source: String // Hook source identifier let configPath: String // Path to config file relative to home let configKey: String // JSON key containing hooks let format: HookFormat // .claude, .nested, .flat, .copilot let events: [(String, Int, Bool)] // (eventName, timeout, async) } // Supported CLIs with their configurations static let allCLIs: [CLIConfig] = [ // Claude Code CLIConfig( name: "Claude Code", source: "claude", configPath: ".claude/settings.json", configKey: "hooks", format: .claude, events: [ ("UserPromptSubmit", 5, true), ("PreToolUse", 5, false), ("PostToolUse", 5, true), ("PermissionRequest", 86400, false), ("Stop", 5, true), ("SessionStart", 5, false), ("SessionEnd", 5, true), ("Notification", 86400, false), ] ), // Codex CLIConfig( name: "Codex", source: "codex", configPath: ".codex/hooks.json", configKey: "hooks", format: .nested, events: [ ("SessionStart", 5, false), ("UserPromptSubmit", 5, false), ("PreToolUse", 5, false), ("PostToolUse", 5, false), ] ), // Cursor CLIConfig( name: "Cursor", source: "cursor", configPath: ".cursor/hooks.json", configKey: "hooks", format: .flat, events: [ ("beforeSubmitPrompt", 5, false), ("beforeShellExecution", 5, false), ("afterFileEdit", 5, false), ("stop", 5, false), ] ), ] // Install hooks for all enabled CLIs static func install() -> Bool { let fm = FileManager.default let hookDir = NSHomeDirectory() + "/.claude/hooks" try? fm.createDirectory(atPath: hookDir, withIntermediateDirectories: true) installHookScript(fm: fm) installBridgeBinary(fm: fm) for cli in allCLIs where isEnabled(source: cli.source) { if cli.source == "claude" { installClaudeHooks(cli: cli, fm: fm) } else { installExternalHooks(cli: cli, fm: fm) } } return true } // Check if a CLI is enabled by user static func isEnabled(source: String) -> Bool { let key = "cli_enabled_\(source)" if UserDefaults.standard.object(forKey: key) == nil { return true } return UserDefaults.standard.bool(forKey: key) } // Toggle CLI hooks on/off static func setEnabled(source: String, enabled: Bool) -> Bool { UserDefaults.standard.set(enabled, forKey: "cli_enabled_\(source)") if enabled { // Install hooks guard let cli = allCLIs.first(where: { $0.source == source }) else { return false } return installExternalHooks(cli: cli, fm: FileManager.default) } else { // Uninstall hooks if let cli = allCLIs.first(where: { $0.source == source }) { uninstallHooks(cli: cli, fm: FileManager.default) } return true } } ``` -------------------------------- ### JavaScript OpenCode Plugin for Event Forwarding Source: https://context7.com/wxtsky/codeisland/llms.txt This JavaScript code is an OpenCode plugin that forwards events to the CodeIsland bridge via a Unix socket. It includes functions to send data and wait for responses, handling session start and permission requests. ```javascript // OpenCode plugin for CodeIsland import { connect } from "net"; import { getuid } from "process"; const SOCKET = `/tmp/codeisland-${getuid()}.sock`; function sendToSocket(json) { return new Promise((resolve) => { const sock = connect({ path: SOCKET }, () => { sock.write(JSON.stringify(json)); sock.end(); resolve(true); }); sock.on("error", () => resolve(false)); sock.setTimeout(3000, () => { sock.destroy(); resolve(false); }); }); } function sendAndWaitResponse(json, timeoutMs = 300000) { return new Promise((resolve) => { const sock = connect({ path: SOCKET }, () => { sock.write(JSON.stringify(json)); sock.end(); }); let buf = ""; sock.on("data", (data) => { buf += data.toString(); }); sock.on("end", () => { try { resolve(JSON.parse(buf)); } catch { resolve(null); } }); sock.on("error", () => resolve(null)); }); } export default async ({ client, serverUrl }) => { function base(sessionId, extra) { return { session_id: sessionId, _source: "opencode", _ppid: process.pid, _env: collectEnv(), ...extra }; } function mapEvent(ev) { if (ev.type === "session.created" && ev.properties.info) { return base(`opencode-${ev.properties.info.id}`, { hook_event_name: "SessionStart", cwd: ev.properties.info.directory }); } if (ev.type === "permission.asked" && ev.properties.id) { return base(`opencode-${ev.properties.sessionID}`, { hook_event_name: "PermissionRequest", tool_name: ev.properties.permission, _opencode_request_id: ev.properties.id }); } return null; } return { "event": async ({ event }) => { const mapped = mapEvent(event); if (!mapped) return; if (mapped.hook_event_name === "PermissionRequest") { // Wait for user response from CodeIsland const response = await sendAndWaitResponse(mapped); const behavior = response?.hookSpecificOutput?.decision?.behavior; const reply = behavior === "allow" ? "once" : "reject"; // Reply to OpenCode await fetch(`http://localhost:${serverPort}/permission/${mapped._opencode_request_id}/reply`, { method: "POST", body: JSON.stringify({ reply }) }); return; } await sendToSocket(mapped); } }; }; ``` -------------------------------- ### Build CodeIsland from Source Source: https://github.com/wxtsky/codeisland/blob/main/README.md Commands to clone the repository and build the application for development or release. ```bash git clone https://github.com/wxtsky/CodeIsland.git cd CodeIsland # Development (debug build + launch) swift build && open .build/debug/CodeIsland.app # Release (universal binary: Apple Silicon + Intel) ./build.sh open .build/release/CodeIsland.app ``` -------------------------------- ### Build CodeIsland from Source Source: https://github.com/wxtsky/codeisland/blob/main/README.zh-CN.md Commands to clone, build, and run the project from source code. ```bash git clone https://github.com/wxtsky/CodeIsland.git cd CodeIsland # 开发模式(debug 构建 + 启动) swift build && open .build/debug/CodeIsland.app # 发布模式(通用二进制:Apple Silicon + Intel) ./build.sh open .build/release/CodeIsland.app ``` -------------------------------- ### Handle Permission Request in AppState Source: https://context7.com/wxtsky/codeisland/llms.txt Manages incoming permission requests by updating session status, queuing the request, and displaying an approval card if it's the first in the queue. Requires the `HookEvent` and a `CheckedContinuation` to resume with user response data. ```swift @MainActor @Observable final class AppState { var sessions: [String: SessionSnapshot] = [: ] var permissionQueue: [PermissionRequest] = [] var questionQueue: [QuestionRequest] = [] var surface: IslandSurface = .collapsed func handlePermissionRequest(_ event: HookEvent, continuation: CheckedContinuation) { let sessionId = event.sessionId ?? "default" sessions[sessionId]?.status = .waitingApproval sessions[sessionId]?.currentTool = event.toolName let request = PermissionRequest(event: event, continuation: continuation) permissionQueue.append(request) if permissionQueue.count == 1 { activeSessionId = sessionId surface = .approvalCard(sessionId: sessionId) SoundManager.shared.handleEvent("PermissionRequest") } } func approvePermission(always: Bool = false) { guard !permissionQueue.isEmpty else { return } let pending = permissionQueue.removeFirst() let responseData: Data if always { let toolName = pending.event.toolName ?? "" let obj: [String: Any] = [ "hookSpecificOutput": [ "hookEventName": "PermissionRequest", "decision": [ "behavior": "allow", "updatedPermissions": [[ "type": "addRules", "rules": [["toolName": toolName, "ruleContent": "*"]], "behavior": "allow" ]] ] ] ] responseData = try! JSONSerialization.data(withJSONObject: obj) } else { responseData = Data(#"{"hookSpecificOutput":{"decision":{"behavior":"allow"}}}"#.utf8) } pending.continuation.resume(returning: responseData) sessions[pending.event.sessionId ?? "default"]?.status = .running showNextPending() } func denyPermission() { guard !permissionQueue.isEmpty else { return } let pending = permissionQueue.removeFirst() let response = #"{"hookSpecificOutput":{"decision":{"behavior":"deny"}}}"# pending.continuation.resume(returning: Data(response.utf8)) showNextPending() } func answerQuestion(_ answer: String) { guard !questionQueue.isEmpty else { return } let pending = questionQueue.removeFirst() let obj: [String: Any] = [ "hookSpecificOutput": [ "hookEventName": "Notification", "answer": answer ] ] let responseData = try! JSONSerialization.data(withJSONObject: obj) pending.continuation.resume(returning: responseData) sessions[pending.event.sessionId ?? "default"]?.status = .processing showNextPending() } } ``` -------------------------------- ### Discover and Monitor Claude Sessions Source: https://context7.com/wxtsky/codeisland/llms.txt Functions for identifying active Claude sessions by scanning process directories and monitoring CLI process exit events using DispatchSource. ```swift // Find active Claude sessions private nonisolated static func findActiveClaudeSessions() -> [DiscoveredSession] { let claudePids = findClaudePids() guard !claudePids.isEmpty else { return [] } let home = FileManager.default.homeDirectoryForCurrentUser.path var results: [DiscoveredSession] = [] for pid in claudePids { guard let cwd = getCwd(for: pid), !cwd.isEmpty else { continue } let projectDir = cwd.claudeProjectDirEncoded() let projectPath = "\(home)/.claude/projects/\(projectDir)" // Find most recent transcript file guard let files = try? FileManager.default.contentsOfDirectory(atPath: projectPath) else { continue } var bestFile: String? var bestDate = Date.distantPast for file in files where file.hasSuffix(".jsonl") { let fullPath = "\(projectPath)/\(file)" if let attrs = try? FileManager.default.attributesOfItem(atPath: fullPath), let modified = attrs[.modificationDate] as? Date, modified > bestDate { bestDate = modified bestFile = file } } guard let file = bestFile else { continue } let sessionId = String(file.dropLast(6)) let (model, messages) = readRecentFromTranscript(path: "\(projectPath)/\(file)") results.append(DiscoveredSession( sessionId: sessionId, cwd: cwd, model: model, pid: pid, modifiedAt: bestDate, recentMessages: messages )) } return results } // Monitor CLI process for exit private func monitorProcess(sessionId: String, pid: pid_t) { let source = DispatchSource.makeProcessSource( identifier: pid, eventMask: .exit, queue: .main ) source.setEventHandler { [weak self] in Task { @MainActor in self?.handleProcessExit(sessionId: sessionId) } } source.resume() processMonitors[sessionId] = source } // Get CWD of a process private nonisolated static func getCwd(for pid: pid_t) -> String? { var pathInfo = proc_vnodepathinfo() let ret = proc_pidinfo(pid, PROC_PIDVNODEPATHINFO, 0, &pathInfo, Int32(MemoryLayout.size)) guard ret > 0 else { return nil } return withUnsafePointer(to: pathInfo.pvi_cdir.vip_path) { $0.withMemoryRebound(to: CChar.self, capacity: Int(MAXPATHLEN)) { String(cString: $0) } } } ``` -------------------------------- ### Swift Bridge Binary for Event Forwarding Source: https://context7.com/wxtsky/codeisland/llms.txt This Swift code acts as a bridge, reading JSON events from stdin, enriching them with terminal environment information, and forwarding them to a Unix socket. It handles permission requests by waiting for a response. ```swift // Bridge binary main entry point let socketPath = SocketPath.path // Parse command line arguments var sourceTag: String? = nil if let idx = args.firstIndex(of: "--source"), idx + 1 < args.count { sourceTag = args[idx + 1] } // Read and parse input JSON let input = FileHandle.standardInput.readDataToEndOfFile() guard !input.isEmpty, var json = try? JSONSerialization.jsonObject(with: input) as? [String: Any] else { exit(0) } // Validate session_id guard let sessionId = json["session_id"] as? String, !sessionId.isEmpty else { exit(0) } // Enrich with terminal environment if let termApp = ProcessInfo.processInfo.environment["TERM_PROGRAM"] { json["_term_app"] = termApp } if let iterm = ProcessInfo.processInfo.environment["ITERM_SESSION_ID"] { if let colonIdx = iterm.firstIndex(of: ":") { json["_iterm_session"] = String(iterm[iterm.index(after: colonIdx)...]) } } if let kitty = ProcessInfo.processInfo.environment["KITTY_WINDOW_ID"] { json["_kitty_window"] = kitty } if let tmux = ProcessInfo.processInfo.environment["TMUX"] { json["_tmux"] = tmux } json["_ppid"] = getppid() if let source = sourceTag { json["_source"] = source } // Connect and send guard let enriched = try? JSONSerialization.data(withJSONObject: json), let sock = connectSocket(socketPath) else { exit(0) } sendAll(sock, data: enriched) shutdown(sock, SHUT_WR) // For permission requests, wait for response let isPermission = (json["hook_event_name"] as? String) == "PermissionRequest" if isPermission { let response = recvAll(sock) FileHandle.standardOutput.write(response) } close(sock) ``` -------------------------------- ### CodeIsland Architecture Flow Source: https://github.com/wxtsky/codeisland/blob/main/README.zh-CN.md Visual representation of the event flow from AI tools to the CodeIsland UI. ```text AI 工具 (Claude/Codex/Gemini/Cursor/...) → 触发 Hook 事件 → codeisland-bridge(原生 Swift 二进制,约 86KB) → Unix socket → /tmp/codeisland-.sock → CodeIsland 接收事件 → 实时更新 UI ``` -------------------------------- ### Define Session State with SessionSnapshot Source: https://context7.com/wxtsky/codeisland/llms.txt The SessionSnapshot struct tracks the state of AI coding sessions, including status, tool usage, terminal information, and recent messages. It supports various AI sources and provides computed properties for display and model name. ```swift public struct SessionSnapshot { public static let supportedSources: Set = [ "claude", "codex", "gemini", "cursor", "copilot", "qoder", "droid", "codebuddy", "opencode" ] public var status: AgentStatus = .idle public var currentTool: String? public var toolDescription: String? public var lastActivity: Date = Date() public var cwd: String? public var model: String? public var toolHistory: [ToolHistoryEntry] = [] public var subagents: [String: SubagentState] = [: ] public var recentMessages: [ChatMessage] = [] // Terminal info for window activation public var termApp: String? public var itermSessionId: String? public var ttyPath: String? public var kittyWindowId: String? public var tmuxPane: String? public var cliPid: pid_t? public var source: String = "claude" // Computed properties public var displayName: String { if let cwd = cwd { return (cwd as NSString).lastPathComponent } return "Session" } public var shortModelName: String? { guard let model = model else { return nil } let lower = model.lowercased() if lower.contains("opus") { return "opus" } if lower.contains("sonnet") { return "sonnet" } if lower.contains("haiku") { return "haiku" } return String(model.prefix(8)) } public var isNativeAppMode: Bool { guard let bid = termBundleId else { return false } guard let expectedSource = Self.appBundleSources[bid] else { return false } return source == expectedSource } } public enum AgentStatus { case idle case processing case running case waitingApproval case waitingQuestion } ``` -------------------------------- ### Normalize and Parse AI Tool Events Source: https://context7.com/wxtsky/codeisland/llms.txt The EventNormalizer maps CLI-specific event names to internal PascalCase, while the HookEvent struct handles JSON deserialization from incoming Unix socket data. ```swift // Event normalization maps CLI-specific names to internal PascalCase public enum EventNormalizer { public static func normalize(_ name: String) -> String { switch name { // Cursor (camelCase) case "beforeSubmitPrompt": return "UserPromptSubmit" case "beforeShellExecution": return "PreToolUse" case "afterShellExecution": return "PostToolUse" case "stop": return "Stop" // Gemini case "BeforeTool": return "PreToolUse" case "AfterTool": return "PostToolUse" // GitHub Copilot CLI case "sessionStart": return "SessionStart" case "preToolUse": return "PreToolUse" default: return name } } } // Core HookEvent structure parsed from incoming JSON public struct HookEvent { public let eventName: String public let sessionId: String? public let toolName: String? public let agentId: String? public let toolInput: [String: Any]? public let rawJSON: [String: Any] public init?(from data: Data) { guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let eventName = json["hook_event_name"] as? String else { return nil } self.eventName = eventName self.sessionId = json["session_id"] as? String self.toolName = json["tool_name"] as? String self.toolInput = json["tool_input"] as? [String: Any] self.agentId = json["agent_id"] as? String self.rawJSON = json } } ``` -------------------------------- ### Reduce Events and Update Session State Source: https://context7.com/wxtsky/codeisland/llms.txt The reduceEvent function processes hook events to update session states and returns side effects for the caller to execute. It handles various event types like user prompts, tool usage, and session lifecycle changes. ```swift // Pure reducer: mutates sessions, returns side effects public func reduceEvent( sessions: inout [String: SessionSnapshot], event: HookEvent, maxHistory: Int ) -> [SideEffect] { let sessionId = event.sessionId ?? "default" let eventName = EventNormalizer.normalize(event.eventName) var effects: [SideEffect] = [] if sessions[sessionId] == nil { sessions[sessionId] = SessionSnapshot() } extractMetadata(into: &sessions, sessionId: sessionId, event: event) switch eventName { case "UserPromptSubmit": sessions[sessionId]?.status = .processing sessions[sessionId]?.currentTool = nil if let prompt = event.rawJSON["prompt"] as? String { sessions[sessionId]?.lastUserPrompt = prompt sessions[sessionId]?.addRecentMessage(ChatMessage(isUser: true, text: prompt)) } case "PreToolUse": sessions[sessionId]?.status = .running sessions[sessionId]?.currentTool = event.toolName sessions[sessionId]?.toolDescription = event.toolDescription case "PostToolUse": if let tool = sessions[sessionId]?.currentTool { sessions[sessionId]?.recordTool(tool, description: nil, success: true, agentType: nil, maxHistory: maxHistory) } sessions[sessionId]?.status = .processing sessions[sessionId]?.currentTool = nil case "Stop": sessions[sessionId]?.status = .idle sessions[sessionId]?.currentTool = nil if let msg = event.rawJSON["last_assistant_message"] as? String { sessions[sessionId]?.lastAssistantMessage = msg sessions[sessionId]?.addRecentMessage(ChatMessage(isUser: false, text: msg)) } effects.append(.enqueueCompletion(sessionId: sessionId)) case "SessionStart": effects.append(.stopMonitor(sessionId: sessionId)) sessions[sessionId] = SessionSnapshot(startTime: Date()) effects.append(.tryMonitorSession(sessionId: sessionId)) case "SessionEnd": effects.append(.removeSession(sessionId: sessionId)) return effects default: break } sessions[sessionId]?.lastActivity = Date() effects.append(.playSound(eventName)) if sessions[sessionId]?.status != .idle { effects.append(.setActiveSession(sessionId: sessionId)) } return effects } // Side effect enum public enum SideEffect: Equatable { case playSound(String) case tryMonitorSession(sessionId: String) case stopMonitor(sessionId: String) case removeSession(sessionId: String) case enqueueCompletion(sessionId: String) case setActiveSession(sessionId: String?) } ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.