#if canImport(Darwin)import Darwin#elseimport Glibc#endifimport Foundation /// Executes an interactive CLI inside a pseudo-terminal and returns all captured text./// Keeps it minimal so we can reuse for Codex and Claude without tmux.public struct TTYCommandRunner { public struct Result: Sendable { public let text: String } public struct Options: Sendable { public var rows: UInt16 = 50 public var cols: UInt16 = 160 public var timeout: TimeInterval = 20.0 /// Stop early once output has been idle for this long (only for non-Codex flows). /// Useful for interactive TUIs that render once and then wait for input indefinitely. public var idleTimeout: TimeInterval? public var workingDirectory: URL? public var extraArgs: [String] = [] public var initialDelay: TimeInterval = 0.4 public var sendEnterEvery: TimeInterval? public var sendOnSubstrings: [String: String] public var stopOnURL: Bool public var stopOnSubstrings: [String] public var settleAfterStop: TimeInterval public init( rows: UInt16 = 50, cols: UInt16 = 160, timeout: TimeInterval = 20.0, idleTimeout: TimeInterval? = nil, workingDirectory: URL? = nil, extraArgs: [String] = [], initialDelay: TimeInterval = 0.4, sendEnterEvery: TimeInterval? = nil, sendOnSubstrings: [String: String] = [:], stopOnURL: Bool = false, stopOnSubstrings: [String] = [], settleAfterStop: TimeInterval = 0.25) { self.rows = rows self.cols = cols self.timeout = timeout self.idleTimeout = idleTimeout self.workingDirectory = workingDirectory self.extraArgs = extraArgs self.initialDelay = initialDelay self.sendEnterEvery = sendEnterEvery self.sendOnSubstrings = sendOnSubstrings self.stopOnURL = stopOnURL self.stopOnSubstrings = stopOnSubstrings self.settleAfterStop = settleAfterStop } } public enum Error: Swift.Error, LocalizedError, Sendable { case binaryNotFound(String) case launchFailed(String) case timedOut public var errorDescription: String? { switch self { case let .binaryNotFound(bin): "Missing CLI '\(bin)'. Install it (e.g. npm i -g @openai/codex) or add it to PATH." case let .launchFailed(msg): "Failed to launch process: \(msg)" case .timedOut: "PTY command timed out." } } } public init() {} struct RollingBuffer: Sendable { private let maxNeedle: Int private var tail = Data() init(maxNeedle: Int) { self.maxNeedle = max(0, maxNeedle) } mutating func append(_ data: Data) -> Data { guard !data.isEmpty else { return Data() } var combined = Data() combined.reserveCapacity(self.tail.count + data.count) combined.append(self.tail) combined.append(data) if self.maxNeedle > 1 { if combined.count >= self.maxNeedle - 1 { self.tail = combined.suffix(self.maxNeedle - 1) } else { self.tail = combined } } else { self.tail.removeAll(keepingCapacity: true) } return combined } mutating func reset() { self.tail.removeAll(keepingCapacity: true) } } static func lowercasedASCII(_ data: Data) -> Data { guard !data.isEmpty else { return data } var out = Data(count: data.count) out.withUnsafeMutableBytes { dest in data.withUnsafeBytes { source in let src = source.bindMemory(to: UInt8.self) let dst = dest.bindMemory(to: UInt8.self) for idx in 0..<src.count { var byte = src[idx] if byte >= 65, byte <= 90 { byte += 32 } dst[idx] = byte } } } return out } static func locateBundledHelper(_ name: String) -> String? { let fm = FileManager.default func isExecutable(_ path: String) -> Bool { fm.isExecutableFile(atPath: path) } if let override = ProcessInfo.processInfo.environment["CODEXBAR_HELPER_\(name.uppercased())"], isExecutable(override) { return override } func candidate(inAppBundleURL appURL: URL) -> String? { let path = appURL .appendingPathComponent("Contents", isDirectory: true) .appendingPathComponent("Helpers", isDirectory: true) .appendingPathComponent(name, isDirectory: false) .path return isExecutable(path) ? path : nil } let mainURL = Bundle.main.bundleURL if mainURL.pathExtension == "app", let found = candidate(inAppBundleURL: mainURL) { return found } if let argv0 = CommandLine.arguments.first { var url = URL(fileURLWithPath: argv0) if !argv0.hasPrefix("/") { url = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent(argv0) } var probe = url for _ in 0..<6 { let parent = probe.deletingLastPathComponent() if parent.pathExtension == "app", let found = candidate(inAppBundleURL: parent) { return found } if parent.path == probe.path { break } probe = parent } } return nil } // swiftlint:disable function_body_length // swiftlint:disable:next cyclomatic_complexity public func run( binary: String, send script: String, options: Options = Options(), onURLDetected: (@Sendable () -> Void)? = nil) throws -> Result { let resolved: String if FileManager.default.isExecutableFile(atPath: binary) { resolved = binary } else if let hit = Self.which(binary) { resolved = hit } else { throw Error.binaryNotFound(binary) } var primaryFD: Int32 = -1 var secondaryFD: Int32 = -1 var win = winsize(ws_row: options.rows, ws_col: options.cols, ws_xpixel: 0, ws_ypixel: 0) guard openpty(&primaryFD, &secondaryFD, nil, nil, &win) == 0 else { throw Error.launchFailed("openpty failed") } // Make primary side non-blocking so read loops don't hang when no data is available. _ = fcntl(primaryFD, F_SETFL, O_NONBLOCK) let primaryHandle = FileHandle(fileDescriptor: primaryFD, closeOnDealloc: true) let secondaryHandle = FileHandle(fileDescriptor: secondaryFD, closeOnDealloc: true) func writeAllToPrimary(_ data: Data) throws { try data.withUnsafeBytes { rawBytes in guard let baseAddress = rawBytes.baseAddress else { return } var offset = 0 var retries = 0 while offset < rawBytes.count { let written = write(primaryFD, baseAddress.advanced(by: offset), rawBytes.count - offset) if written > 0 { offset += written retries = 0 continue } if written == 0 { break } let err = errno if err == EAGAIN || err == EWOULDBLOCK { retries += 1 if retries > 200 { throw Error.launchFailed("write to PTY would block") } usleep(5000) continue } throw Error.launchFailed("write to PTY failed: \(String(cString: strerror(err)))") } } } let proc = Process() let resolvedURL = URL(fileURLWithPath: resolved) if resolvedURL.lastPathComponent == "claude", let watchdog = Self.locateBundledHelper("CodexBarClaudeWatchdog") { proc.executableURL = URL(fileURLWithPath: watchdog) proc.arguments = ["--", resolved] + options.extraArgs } else { proc.executableURL = resolvedURL proc.arguments = options.extraArgs } proc.standardInput = secondaryHandle proc.standardOutput = secondaryHandle proc.standardError = secondaryHandle // Use login-shell PATH when available, but keep the caller’s environment (HOME, LANG, etc.) so // the CLIs can find their auth/config files. var env = Self.enrichedEnvironment() if let workingDirectory = options.workingDirectory { proc.currentDirectoryURL = workingDirectory env["PWD"] = workingDirectory.path } proc.environment = env var cleanedUp = false var didLaunch = false var processGroup: pid_t? // Always tear down the PTY child (and its process group) even if we throw early // while bootstrapping the CLI (e.g. when it prompts for login/telemetry). func cleanup() { guard !cleanedUp else { return } cleanedUp = true if didLaunch, proc.isRunning { let exitData = Data("/exit\n".utf8) try? writeAllToPrimary(exitData) } try? primaryHandle.close() try? secondaryHandle.close() guard didLaunch else { return } if proc.isRunning { proc.terminate() } if let pgid = processGroup { kill(-pgid, SIGTERM) } let waitDeadline = Date().addingTimeInterval(2.0) while proc.isRunning, Date() < waitDeadline { usleep(100_000) } if proc.isRunning { if let pgid = processGroup { kill(-pgid, SIGKILL) } kill(proc.processIdentifier, SIGKILL) } if didLaunch { proc.waitUntilExit() } } // Ensure the PTY process is always torn down, even when we throw early (e.g. login prompt). defer { cleanup() } try proc.run() didLaunch = true // Isolate the child into its own process group so descendant helpers can be // terminated together. If this fails (e.g. process already exec'ed), we // continue and fall back to single-PID termination. let pid = proc.processIdentifier if setpgid(pid, pid) == 0 { processGroup = pid } func send(_ text: String) throws { guard let data = text.data(using: .utf8) else { return } try writeAllToPrimary(data) } let deadline = Date().addingTimeInterval(options.timeout) let trimmed = script.trimmingCharacters(in: .whitespacesAndNewlines) let isCodex = (binary == "codex") let isCodexStatus = isCodex && trimmed == "/status" var buffer = Data() func readChunk() -> Data { var appended = Data() while true { var tmp = [UInt8](repeating: 0, count: 8192) let n = read(primaryFD, &tmp, tmp.count) if n > 0 { let slice = tmp.prefix(n) buffer.append(contentsOf: slice) appended.append(contentsOf: slice) continue } break } return appended } func firstLink(in data: Data) -> String? { guard let s = String(data: data, encoding: .utf8) else { return nil } let pattern = #"https?://[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]+"# guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil } let range = NSRange(s.startIndex..<s.endIndex, in: s) guard let match = regex.firstMatch(in: s, range: range), let r = Range(match.range, in: s) else { return nil } var url = String(s[r]) while let last = url.unicodeScalars.last, CharacterSet(charactersIn: ".,;:)]}>\"'").contains(last) { url.unicodeScalars.removeLast() } return url } let cursorQuery = Data([0x1B, 0x5B, 0x36, 0x6E]) usleep(UInt32(options.initialDelay * 1_000_000)) // Generic path for non-Codex (e.g. Claude /login) if !isCodex { if !trimmed.isEmpty { try send(trimmed) try send("\r") } let stopNeedles = options.stopOnSubstrings.map { Data($0.utf8) } let sendNeedles = options.sendOnSubstrings.map { ( needle: Data($0.key.utf8), needleString: $0.key, keys: Data($0.value.utf8)) } let urlNeedles = [Data("https://".utf8), Data("http://".utf8)] let needleLengths = stopNeedles.map(\.count) + sendNeedles.map(\.needle.count) + urlNeedles.map(\.count) + [cursorQuery.count] let maxNeedle = needleLengths.max() ?? cursorQuery.count var scanBuffer = RollingBuffer(maxNeedle: maxNeedle) var nextCursorCheckAt = Date(timeIntervalSince1970: 0) var lastEnter = Date() var stoppedEarly = false var urlSeen = false var triggeredSends = Set<Data>() var recentText = "" var lastOutputAt = Date() while Date() < deadline { let newData = readChunk() if !newData.isEmpty { lastOutputAt = Date() if let chunkText = String(bytes: newData, encoding: .utf8) { recentText += chunkText if recentText.count > 8192 { recentText.removeFirst(recentText.count - 8192) } } } let scanData = scanBuffer.append(newData) if Date() >= nextCursorCheckAt, !scanData.isEmpty, scanData.range(of: cursorQuery) != nil { try? send("\u{1b}[1;1R") nextCursorCheckAt = Date().addingTimeInterval(1.0) } if !sendNeedles.isEmpty { let recentTextCollapsed = recentText.replacingOccurrences(of: "\r", with: "") for item in sendNeedles where !triggeredSends.contains(item.needle) { let matched = scanData.range(of: item.needle) != nil || recentText.contains(item.needleString) || recentTextCollapsed.contains(item.needleString) if matched { if let keysString = String(data: item.keys, encoding: .utf8) { try? send(keysString) } else { try? writeAllToPrimary(item.keys) } triggeredSends.insert(item.needle) } } } if urlNeedles.contains(where: { scanData.range(of: $0) != nil }) { urlSeen = true if urlSeen { onURLDetected?() } if options.stopOnURL { stoppedEarly = true break } } if !stopNeedles.isEmpty, stopNeedles.contains(where: { scanData.range(of: $0) != nil }) { stoppedEarly = true break } if let idleTimeout = options.idleTimeout, !buffer.isEmpty, Date().timeIntervalSince(lastOutputAt) >= idleTimeout { stoppedEarly = true break } if !urlSeen, let every = options.sendEnterEvery, Date().timeIntervalSince(lastEnter) >= every { try? send("\r") lastEnter = Date() } if !proc.isRunning { break } usleep(60000) } if stoppedEarly { let settle = max(0, min(options.settleAfterStop, deadline.timeIntervalSinceNow)) if settle > 0 { let settleDeadline = Date().addingTimeInterval(settle) while Date() < settleDeadline { let newData = readChunk() let scanData = scanBuffer.append(newData) if Date() >= nextCursorCheckAt, !scanData.isEmpty, scanData.range(of: cursorQuery) != nil { try? send("\u{1b}[1;1R") nextCursorCheckAt = Date().addingTimeInterval(1.0) } usleep(50000) } } } let text = String(data: buffer, encoding: .utf8) ?? "" guard !text.isEmpty else { throw Error.timedOut } return Result(text: text) } // Codex-specific behavior (/status and update handling) let delayInitialSend = isCodexStatus if !delayInitialSend { try send(script) try send("\r") usleep(150_000) try send("\r") try send("\u{1b}") } var skippedCodexUpdate = false var sentScript = !delayInitialSend var updateSkipAttempts = 0 var lastEnter = Date(timeIntervalSince1970: 0) var scriptSentAt: Date? = sentScript ? Date() : nil var resendStatusRetries = 0 var enterRetries = 0 var sawCodexStatus = false var sawCodexUpdatePrompt = false let statusMarkers = [ "Credits:", "5h limit", "5-hour limit", "Weekly limit", ].map { Data($0.utf8) } let updateNeedles = ["Update available!", "Run bun install -g @openai/codex", "0.60.1 ->"] let updateNeedlesLower = updateNeedles.map { Data($0.lowercased().utf8) } let statusNeedleLengths = statusMarkers.map(\.count) let updateNeedleLengths = updateNeedlesLower.map(\.count)Lines 1–500 of 679. Load more
#if canImport(Darwin) import Darwin #else import Glibc #endif import Foundation /// Executes an interactive CLI inside a pseudo-terminal and returns all captured text. /// Keeps it minimal so we can reuse for Codex and Claude without tmux. public struct TTYCommandRunner { public struct Result: Sendable { public let text: String } public struct Options: Sendable { public var rows: UInt16 = 50 public var cols: UInt16 = 160 public var timeout: TimeInterval = 20.0 /// Stop early once output has been idle for this long (only for non-Codex flows). /// Useful for interactive TUIs that render once and then wait for input indefinitely. public var idleTimeout: TimeInterval? public var workingDirectory: URL? public var extraArgs: [String] = [] public var initialDelay: TimeInterval = 0.4 public var sendEnterEvery: TimeInterval? public var sendOnSubstrings: [String: String] public var stopOnURL: Bool public var stopOnSubstrings: [String] public var settleAfterStop: TimeInterval public init( rows: UInt16 = 50, cols: UInt16 = 160, timeout: TimeInterval = 20.0, idleTimeout: TimeInterval? = nil, workingDirectory: URL? = nil, extraArgs: [String] = [], initialDelay: TimeInterval = 0.4, sendEnterEvery: TimeInterval? = nil, sendOnSubstrings: [String: String] = [:], stopOnURL: Bool = false, stopOnSubstrings: [String] = [], settleAfterStop: TimeInterval = 0.25) { self.rows = rows self.cols = cols self.timeout = timeout self.idleTimeout = idleTimeout self.workingDirectory = workingDirectory self.extraArgs = extraArgs self.initialDelay = initialDelay self.sendEnterEvery = sendEnterEvery self.sendOnSubstrings = sendOnSubstrings self.stopOnURL = stopOnURL self.stopOnSubstrings = stopOnSubstrings self.settleAfterStop = settleAfterStop } } public enum Error: Swift.Error, LocalizedError, Sendable { case binaryNotFound(String) case launchFailed(String) case timedOut public var errorDescription: String? { switch self { case let .binaryNotFound(bin): "Missing CLI '\(bin)'. Install it (e.g. npm i -g @openai/codex) or add it to PATH." case let .launchFailed(msg): "Failed to launch process: \(msg)" case .timedOut: "PTY command timed out." } } } public init() {} struct RollingBuffer: Sendable { private let maxNeedle: Int private var tail = Data() init(maxNeedle: Int) { self.maxNeedle = max(0, maxNeedle) } mutating func append(_ data: Data) -> Data { guard !data.isEmpty else { return Data() } var combined = Data() combined.reserveCapacity(self.tail.count + data.count) combined.append(self.tail) combined.append(data) if self.maxNeedle > 1 { if combined.count >= self.maxNeedle - 1 { self.tail = combined.suffix(self.maxNeedle - 1) } else { self.tail = combined } } else { self.tail.removeAll(keepingCapacity: true) } return combined } mutating func reset() { self.tail.removeAll(keepingCapacity: true) } } static func lowercasedASCII(_ data: Data) -> Data { guard !data.isEmpty else { return data } var out = Data(count: data.count) out.withUnsafeMutableBytes { dest in data.withUnsafeBytes { source in let src = source.bindMemory(to: UInt8.self) let dst = dest.bindMemory(to: UInt8.self) for idx in 0..<src.count { var byte = src[idx] if byte >= 65, byte <= 90 { byte += 32 } dst[idx] = byte } } } return out } static func locateBundledHelper(_ name: String) -> String? { let fm = FileManager.default func isExecutable(_ path: String) -> Bool { fm.isExecutableFile(atPath: path) } if let override = ProcessInfo.processInfo.environment["CODEXBAR_HELPER_\(name.uppercased())"], isExecutable(override) { return override } func candidate(inAppBundleURL appURL: URL) -> String? { let path = appURL .appendingPathComponent("Contents", isDirectory: true) .appendingPathComponent("Helpers", isDirectory: true) .appendingPathComponent(name, isDirectory: false) .path return isExecutable(path) ? path : nil } let mainURL = Bundle.main.bundleURL if mainURL.pathExtension == "app", let found = candidate(inAppBundleURL: mainURL) { return found } if let argv0 = CommandLine.arguments.first { var url = URL(fileURLWithPath: argv0) if !argv0.hasPrefix("/") { url = URL(fileURLWithPath: FileManager.default.currentDirectoryPath).appendingPathComponent(argv0) } var probe = url for _ in 0..<6 { let parent = probe.deletingLastPathComponent() if parent.pathExtension == "app", let found = candidate(inAppBundleURL: parent) { return found } if parent.path == probe.path { break } probe = parent } } return nil } // swiftlint:disable function_body_length // swiftlint:disable:next cyclomatic_complexity public func run( binary: String, send script: String, options: Options = Options(), onURLDetected: (@Sendable () -> Void)? = nil) throws -> Result { let resolved: String if FileManager.default.isExecutableFile(atPath: binary) { resolved = binary } else if let hit = Self.which(binary) { resolved = hit } else { throw Error.binaryNotFound(binary) } var primaryFD: Int32 = -1 var secondaryFD: Int32 = -1 var win = winsize(ws_row: options.rows, ws_col: options.cols, ws_xpixel: 0, ws_ypixel: 0) guard openpty(&primaryFD, &secondaryFD, nil, nil, &win) == 0 else { throw Error.launchFailed("openpty failed") } // Make primary side non-blocking so read loops don't hang when no data is available. _ = fcntl(primaryFD, F_SETFL, O_NONBLOCK) let primaryHandle = FileHandle(fileDescriptor: primaryFD, closeOnDealloc: true) let secondaryHandle = FileHandle(fileDescriptor: secondaryFD, closeOnDealloc: true) func writeAllToPrimary(_ data: Data) throws { try data.withUnsafeBytes { rawBytes in guard let baseAddress = rawBytes.baseAddress else { return } var offset = 0 var retries = 0 while offset < rawBytes.count { let written = write(primaryFD, baseAddress.advanced(by: offset), rawBytes.count - offset) if written > 0 { offset += written retries = 0 continue } if written == 0 { break } let err = errno if err == EAGAIN || err == EWOULDBLOCK { retries += 1 if retries > 200 { throw Error.launchFailed("write to PTY would block") } usleep(5000) continue } throw Error.launchFailed("write to PTY failed: \(String(cString: strerror(err)))") } } } let proc = Process() let resolvedURL = URL(fileURLWithPath: resolved) if resolvedURL.lastPathComponent == "claude", let watchdog = Self.locateBundledHelper("CodexBarClaudeWatchdog") { proc.executableURL = URL(fileURLWithPath: watchdog) proc.arguments = ["--", resolved] + options.extraArgs } else { proc.executableURL = resolvedURL proc.arguments = options.extraArgs } proc.standardInput = secondaryHandle proc.standardOutput = secondaryHandle proc.standardError = secondaryHandle // Use login-shell PATH when available, but keep the caller’s environment (HOME, LANG, etc.) so // the CLIs can find their auth/config files. var env = Self.enrichedEnvironment() if let workingDirectory = options.workingDirectory { proc.currentDirectoryURL = workingDirectory env["PWD"] = workingDirectory.path } proc.environment = env var cleanedUp = false var didLaunch = false var processGroup: pid_t? // Always tear down the PTY child (and its process group) even if we throw early // while bootstrapping the CLI (e.g. when it prompts for login/telemetry). func cleanup() { guard !cleanedUp else { return } cleanedUp = true if didLaunch, proc.isRunning { let exitData = Data("/exit\n".utf8) try? writeAllToPrimary(exitData) } try? primaryHandle.close() try? secondaryHandle.close() guard didLaunch else { return } if proc.isRunning { proc.terminate() } if let pgid = processGroup { kill(-pgid, SIGTERM) } let waitDeadline = Date().addingTimeInterval(2.0) while proc.isRunning, Date() < waitDeadline { usleep(100_000) } if proc.isRunning { if let pgid = processGroup { kill(-pgid, SIGKILL) } kill(proc.processIdentifier, SIGKILL) } if didLaunch { proc.waitUntilExit() } } // Ensure the PTY process is always torn down, even when we throw early (e.g. login prompt). defer { cleanup() } try proc.run() didLaunch = true // Isolate the child into its own process group so descendant helpers can be // terminated together. If this fails (e.g. process already exec'ed), we // continue and fall back to single-PID termination. let pid = proc.processIdentifier if setpgid(pid, pid) == 0 { processGroup = pid } func send(_ text: String) throws { guard let data = text.data(using: .utf8) else { return } try writeAllToPrimary(data) } let deadline = Date().addingTimeInterval(options.timeout) let trimmed = script.trimmingCharacters(in: .whitespacesAndNewlines) let isCodex = (binary == "codex") let isCodexStatus = isCodex && trimmed == "/status" var buffer = Data() func readChunk() -> Data { var appended = Data() while true { var tmp = [UInt8](repeating: 0, count: 8192) let n = read(primaryFD, &tmp, tmp.count) if n > 0 { let slice = tmp.prefix(n) buffer.append(contentsOf: slice) appended.append(contentsOf: slice) continue } break } return appended } func firstLink(in data: Data) -> String? { guard let s = String(data: data, encoding: .utf8) else { return nil } let pattern = #"https?://[A-Za-z0-9._~:/?#\[\]@!$&'()*+,;=%-]+"# guard let regex = try? NSRegularExpression(pattern: pattern) else { return nil } let range = NSRange(s.startIndex..<s.endIndex, in: s) guard let match = regex.firstMatch(in: s, range: range), let r = Range(match.range, in: s) else { return nil } var url = String(s[r]) while let last = url.unicodeScalars.last, CharacterSet(charactersIn: ".,;:)]}>\"'").contains(last) { url.unicodeScalars.removeLast() } return url } let cursorQuery = Data([0x1B, 0x5B, 0x36, 0x6E]) usleep(UInt32(options.initialDelay * 1_000_000)) // Generic path for non-Codex (e.g. Claude /login) if !isCodex { if !trimmed.isEmpty { try send(trimmed) try send("\r") } let stopNeedles = options.stopOnSubstrings.map { Data($0.utf8) } let sendNeedles = options.sendOnSubstrings.map { ( needle: Data($0.key.utf8), needleString: $0.key, keys: Data($0.value.utf8)) } let urlNeedles = [Data("https://".utf8), Data("http://".utf8)] let needleLengths = stopNeedles.map(\.count) + sendNeedles.map(\.needle.count) + urlNeedles.map(\.count) + [cursorQuery.count] let maxNeedle = needleLengths.max() ?? cursorQuery.count var scanBuffer = RollingBuffer(maxNeedle: maxNeedle) var nextCursorCheckAt = Date(timeIntervalSince1970: 0) var lastEnter = Date() var stoppedEarly = false var urlSeen = false var triggeredSends = Set<Data>() var recentText = "" var lastOutputAt = Date() while Date() < deadline { let newData = readChunk() if !newData.isEmpty { lastOutputAt = Date() if let chunkText = String(bytes: newData, encoding: .utf8) { recentText += chunkText if recentText.count > 8192 { recentText.removeFirst(recentText.count - 8192) } } } let scanData = scanBuffer.append(newData) if Date() >= nextCursorCheckAt, !scanData.isEmpty, scanData.range(of: cursorQuery) != nil { try? send("\u{1b}[1;1R") nextCursorCheckAt = Date().addingTimeInterval(1.0) } if !sendNeedles.isEmpty { let recentTextCollapsed = recentText.replacingOccurrences(of: "\r", with: "") for item in sendNeedles where !triggeredSends.contains(item.needle) { let matched = scanData.range(of: item.needle) != nil || recentText.contains(item.needleString) || recentTextCollapsed.contains(item.needleString) if matched { if let keysString = String(data: item.keys, encoding: .utf8) { try? send(keysString) } else { try? writeAllToPrimary(item.keys) } triggeredSends.insert(item.needle) } } } if urlNeedles.contains(where: { scanData.range(of: $0) != nil }) { urlSeen = true if urlSeen { onURLDetected?() } if options.stopOnURL { stoppedEarly = true break } } if !stopNeedles.isEmpty, stopNeedles.contains(where: { scanData.range(of: $0) != nil }) { stoppedEarly = true break } if let idleTimeout = options.idleTimeout, !buffer.isEmpty, Date().timeIntervalSince(lastOutputAt) >= idleTimeout { stoppedEarly = true break } if !urlSeen, let every = options.sendEnterEvery, Date().timeIntervalSince(lastEnter) >= every { try? send("\r") lastEnter = Date() } if !proc.isRunning { break } usleep(60000) } if stoppedEarly { let settle = max(0, min(options.settleAfterStop, deadline.timeIntervalSinceNow)) if settle > 0 { let settleDeadline = Date().addingTimeInterval(settle) while Date() < settleDeadline { let newData = readChunk() let scanData = scanBuffer.append(newData) if Date() >= nextCursorCheckAt, !scanData.isEmpty, scanData.range(of: cursorQuery) != nil { try? send("\u{1b}[1;1R") nextCursorCheckAt = Date().addingTimeInterval(1.0) } usleep(50000) } } } let text = String(data: buffer, encoding: .utf8) ?? "" guard !text.isEmpty else { throw Error.timedOut } return Result(text: text) } // Codex-specific behavior (/status and update handling) let delayInitialSend = isCodexStatus if !delayInitialSend { try send(script) try send("\r") usleep(150_000) try send("\r") try send("\u{1b}") } var skippedCodexUpdate = false var sentScript = !delayInitialSend var updateSkipAttempts = 0 var lastEnter = Date(timeIntervalSince1970: 0) var scriptSentAt: Date? = sentScript ? Date() : nil var resendStatusRetries = 0 var enterRetries = 0 var sawCodexStatus = false var sawCodexUpdatePrompt = false let statusMarkers = [ "Credits:", "5h limit", "5-hour limit", "Weekly limit", ].map { Data($0.utf8) } let updateNeedles = ["Update available!", "Run bun install -g @openai/codex", "0.60.1 ->"] let updateNeedlesLower = updateNeedles.map { Data($0.lowercased().utf8) } let statusNeedleLengths = statusMarkers.map(\.count) let updateNeedleLengths = updateNeedlesLower.map(\.count)