import Foundation public struct AgentSession: Codable, Equatable, Sendable, Identifiable { public enum Provider: String, Codable, Sendable { case codex case claude } public enum Source: String, Codable, Sendable { case cli case desktopApp case ide case unknown } public enum State: String, Codable, Sendable { case active case idle } public var id: String public var provider: Provider public var source: Source public var state: State public var pid: Int32? public var cwd: String? public var projectName: String? public var startedAt: Date? public var lastActivityAt: Date? public var transcriptPath: String? public var host: String public init( id: String, provider: Provider, source: Source, state: State, pid: Int32?, cwd: String?, projectName: String?, startedAt: Date?, lastActivityAt: Date?, transcriptPath: String?, host: String) { self.id = id self.provider = provider self.source = source self.state = state self.pid = pid self.cwd = cwd self.projectName = projectName self.startedAt = startedAt self.lastActivityAt = lastActivityAt self.transcriptPath = transcriptPath self.host = host } } public struct SessionScanConfig: Equatable, Sendable { public var activeWindow: TimeInterval public var fileOnlyWindow: TimeInterval public init(activeWindow: TimeInterval = 120, fileOnlyWindow: TimeInterval = 30 * 60) { self.activeWindow = activeWindow self.fileOnlyWindow = fileOnlyWindow } public func state(lastActivityAt: Date?, now: Date, hasLiveProcess: Bool) -> AgentSession.State { guard let lastActivityAt else { return hasLiveProcess ? .active : .idle } return now.timeIntervalSince(lastActivityAt) <= self.activeWindow ? .active : .idle } } public struct AgentProcessRecord: Equatable, Sendable { public let pid: Int32 public let ppid: Int32 public let startedAt: Date? public let command: String public init(pid: Int32, ppid: Int32, startedAt: Date?, command: String) { self.pid = pid self.ppid = ppid self.startedAt = startedAt self.command = command } public var executableBasename: String { let firstToken = self.command.split(whereSeparator: \ .isWhitespace).first.map(String.init) ?? "" let firstBasename = URL(fileURLWithPath: firstToken).lastPathComponent if firstBasename == "disclaimer" { return firstBasename } if self.command.contains("Application Support/Claude/claude-code/claude") { return "claude" } return firstBasename } } public enum AgentPSOutputParser { public static func parse(_ output: String) -> [AgentProcessRecord] { let formatter = DateFormatter() formatter.locale = Locale(identifier: "en_US_POSIX") formatter.dateFormat = "EEE MMM d HH:mm:ss yyyy" return output.split(whereSeparator: \ .isNewline).compactMap { rawLine -> AgentProcessRecord? in let fields = rawLine.split(maxSplits: 7, omittingEmptySubsequences: true, whereSeparator: \ .isWhitespace) guard fields.count == 8, let pid = Int32(fields[0]), let ppid = Int32(fields[1]) else { return nil } let dateText = fields[2...6].joined(separator: " ") return AgentProcessRecord( pid: pid, ppid: ppid, startedAt: formatter.date(from: dateText), command: String(fields[7])) } } public static func agentProcesses(from records: [AgentProcessRecord]) -> [AgentProcessRecord] { let candidates = records.filter { record in let basename = record.executableBasename.lowercased() if basename == "codex" { let arguments = self.arguments(record.command) return self.isCodexAgentExecutable(record.command) && !arguments.contains("app-server") && !arguments.contains("--help") && !arguments.contains("--version") } if basename == "claude" { return self.isClaudeAgentExecutable(record.command) && !self.isObviousClaudeHelper(record.command) } return basename == "disclaimer" && record.command.contains("claude") } let recordsByPID = Dictionary(uniqueKeysWithValues: candidates.map { ($0.pid, $0) }) return candidates.filter { record in guard record.executableBasename.lowercased() == "disclaimer" else { return true } return !candidates.contains { child in child.ppid == record.pid && child.executableBasename.lowercased() == "claude" && self.normalizedClaudeArguments(child.command) == self.normalizedClaudeArguments(record.command) } && recordsByPID[record.ppid] == nil } } public static func provider(for record: AgentProcessRecord) -> AgentSession.Provider? { let basename = record.executableBasename.lowercased() if basename == "codex" { return .codex } if basename == "claude" || basename == "disclaimer" { return .claude } return nil } public static func source(for record: AgentProcessRecord) -> AgentSession.Source { guard self.provider(for: record) == .claude else { return .cli } return record.command.contains("Application Support/Claude/claude-code") ? .desktopApp : .cli } public static func hasCodexAppServer(in records: [AgentProcessRecord]) -> Bool { records.contains { record in record.executableBasename.lowercased() == "codex" && self.isCodexAgentExecutable(record.command) && self.arguments(record.command).contains("app-server") } } private static func arguments(_ command: String) -> [String] { command.split(whereSeparator: \ .isWhitespace).dropFirst().map(String.init) } private static func normalizedClaudeArguments(_ command: String) -> [String] { let arguments = self.arguments(command) if let index = arguments.firstIndex(where: { URL(fileURLWithPath: $0).lastPathComponent == "claude" }) { return Array(arguments.suffix(from: arguments.index(after: index))) } return arguments } private static func isObviousClaudeHelper(_ command: String) -> Bool { let lowercased = command.lowercased() return lowercased.contains("--version") || lowercased.contains("--help") || lowercased.contains("claude-code-acp") } private static func isCodexAgentExecutable(_ command: String) -> Bool { let lowercased = command.lowercased() guard lowercased.contains(".app/") else { return true } return lowercased.hasPrefix("/applications/codex.app/contents/resources/codex ") || lowercased.hasPrefix("/applications/codex.app/contents/resources/codex\t") } private static func isClaudeAgentExecutable(_ command: String) -> Bool { let lowercased = command.lowercased() return !lowercased.contains(".app/") || lowercased.contains("application support/claude/claude-code/claude") } } public enum LSOFCWDOutputParser { public static func parse(_ output: String) -> [Int32: String] { var result: [Int32: String] = [:] var currentPID: Int32? for line in output.split(whereSeparator: \ .isNewline).map(String.init) { guard let prefix = line.first else { continue } switch prefix { case "p": currentPID = Int32(line.dropFirst()) case "n": if let currentPID { result[currentPID] = String(line.dropFirst()) } default: continue } } return result } } public enum ClaudeSessionProjectMapper { public struct Transcript: Equatable, Sendable { public let url: URL public let modifiedAt: Date public init(url: URL, modifiedAt: Date) { self.url = url self.modifiedAt = modifiedAt } } public static func escapedCWD(_ cwd: String) -> String { String(cwd.unicodeScalars.map { scalar in let value = scalar.value let isASCIIAlphanumeric = (48...57).contains(value) || (65...90).contains(value) || (97...122).contains(value) return isASCIIAlphanumeric ? Character(scalar) : "-" }) } public static func projectDirectories( cwd: String, homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser, fileManager: FileManager = .default) -> [URL] { let standardRoot = homeDirectory .appendingPathComponent(".claude", isDirectory: true) .appendingPathComponent("projects", isDirectory: true) return ([standardRoot] + ClaudeDesktopProjectsLocator.roots( homeDirectory: homeDirectory, fileManager: fileManager)) .map { $0.appendingPathComponent(self.escapedCWD(cwd), isDirectory: true) } } public static func newestTranscript( cwd: String, homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser, fileManager: FileManager = .default) -> (url: URL, modifiedAt: Date)? { self.transcripts(cwd: cwd, homeDirectory: homeDirectory, fileManager: fileManager).first .map { (url: $0.url, modifiedAt: $0.modifiedAt) } } public static func transcripts( cwd: String, homeDirectory: URL = FileManager.default.homeDirectoryForCurrentUser, fileManager: FileManager = .default) -> [Transcript] { self.projectDirectories(cwd: cwd, homeDirectory: homeDirectory, fileManager: fileManager) .flatMap { directory -> [(URL, Date)] in guard let files = try? fileManager.contentsOfDirectory( at: directory, includingPropertiesForKeys: [.contentModificationDateKey], options: [.skipsHiddenFiles]) else { return [] } return files.compactMap { file in guard file.pathExtension == "jsonl", let modifiedAt = try? file.resourceValues( forKeys: [.contentModificationDateKey]).contentModificationDate else { return nil } return (file, modifiedAt) } } .sorted { $0.1 > $1.1 } .map { Transcript(url: $0.0, modifiedAt: $0.1) } } } public enum AgentSessionCorrelation { public static func newestProcessesFirst(_ processes: [AgentProcessRecord]) -> [AgentProcessRecord] { processes.sorted { lhs, rhs in let lhsDate = lhs.startedAt ?? .distantPast let rhsDate = rhs.startedAt ?? .distantPast if lhsDate != rhsDate { return lhsDate > rhsDate } return lhs.pid > rhs.pid } } public static func assignClaudeTranscripts( processes: [AgentProcessRecord], cwdByPID: [Int32: String], transcriptsByCWD: [String: [ClaudeSessionProjectMapper.Transcript]]) -> [Int32: ClaudeSessionProjectMapper.Transcript] { var assigned: [Int32: ClaudeSessionProjectMapper.Transcript] = [:] var usedPaths = Set() let unambiguousPIDs = self.unambiguousProcessIDs(processes: processes, cwdByPID: cwdByPID) for process in self.newestProcessesFirst(processes) { guard unambiguousPIDs.contains(process.pid), let cwd = cwdByPID[process.pid], let candidates = transcriptsByCWD[cwd] else { continue } let candidate = candidates.first { transcript in guard !usedPaths.contains(transcript.url.path) else { return false } guard let startedAt = process.startedAt else { return true } return transcript.modifiedAt >= startedAt } if let candidate { assigned[process.pid] = candidate usedPaths.insert(candidate.url.path) } } return assigned } public static func unambiguousProcessIDs( processes: [AgentProcessRecord], cwdByPID: [Int32: String]) -> Set { let grouped = Dictionary(grouping: processes.compactMap { process -> (Int32, String)? in guard let cwd = cwdByPID[process.pid] else { return nil } return (process.pid, URL(fileURLWithPath: cwd).standardizedFileURL.path) }, by: { $0.1 }) return Set(grouped.values.compactMap { records in records.count == 1 ? records[0].0 : nil }) } public static func codexWorkingDirectoriesMatch(_ lhs: String?, _ rhs: String?) -> Bool { guard let lhs, let rhs else { return false } return URL(fileURLWithPath: lhs).standardizedFileURL.path == URL(fileURLWithPath: rhs).standardizedFileURL.path } public static func fileOnlyCodexSource( metadataSource: AgentSession.Source, appServerPresent: Bool) -> AgentSession.Source { metadataSource == .unknown && appServerPresent ? .desktopApp : metadataSource } } public struct CodexRolloutMetadata: Equatable, Sendable { public let sessionID: String public let cwd: String? public let originator: String? public let source: String? public init(sessionID: String, cwd: String?, originator: String?, source: String?) { self.sessionID = sessionID self.cwd = cwd self.originator = originator self.source = source } public var sessionSource: AgentSession.Source { let value = [self.originator, self.source] .compactMap { $0?.lowercased() } .joined(separator: " ") if value.contains("desktop") || value.contains("app-server") { return .desktopApp } if value.contains("ide") || value.contains("vscode") || value.contains("cursor") || value.contains("zed") { return .ide } if value.contains("codex_exec") || value.contains("exec") || value.contains("cli") { return .cli } return .unknown } } public enum CodexRolloutFirstLineParser { public static func parse(_ line: String) -> CodexRolloutMetadata? { guard let data = line.data(using: .utf8), let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any], object["type"] as? String == "session_meta", let payload = object["payload"] as? [String: Any], let sessionID = (payload["session_id"] as? String) ?? (payload["id"] as? String) else { return nil } return CodexRolloutMetadata( sessionID: sessionID, cwd: payload["cwd"] as? String, originator: payload["originator"] as? String, source: payload["source"] as? String) } public static func read(from url: URL) -> CodexRolloutMetadata? { guard let handle = try? FileHandle(forReadingFrom: url) else { return nil } defer { try? handle.close() } var data = Data() while data.count < 256 * 1024 { guard let chunk = try? handle.read(upToCount: 4096), !chunk.isEmpty else { break } if let newline = chunk.firstIndex(of: 0x0A) { data.append(chunk.prefix(upTo: newline)) break } data.append(chunk) } guard let line = String(data: data, encoding: .utf8) else { return nil } return self.parse(line) } public static func makeSession( metadata: CodexRolloutMetadata, transcriptURL: URL, modifiedAt: Date, pid: Int32? = nil, startedAt: Date? = nil, host: String, config: SessionScanConfig = SessionScanConfig(), now: Date = Date()) -> AgentSession? { guard pid != nil || now.timeIntervalSince(modifiedAt) <= config.fileOnlyWindow else { return nil } let cwd = metadata.cwd return AgentSession( id: metadata.sessionID, provider: .codex, source: metadata.sessionSource, state: config.state(lastActivityAt: modifiedAt, now: now, hasLiveProcess: pid != nil), pid: pid, cwd: cwd, projectName: cwd.map { URL(fileURLWithPath: $0).lastPathComponent }, startedAt: startedAt, lastActivityAt: modifiedAt, transcriptPath: transcriptURL.path, host: host) } }