codex-flow 2.1.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. codex_flow/__init__.py +28 -0
  2. codex_flow/__main__.py +9 -0
  3. codex_flow/cli.py +242 -0
  4. codex_flow/data/LICENSE +21 -0
  5. codex_flow/data/README.en.md +303 -0
  6. codex_flow/data/README.md +305 -0
  7. codex_flow/data/VERSION +1 -0
  8. codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
  9. codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
  10. codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
  11. codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
  12. codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
  13. codex_flow/data/apps/macos-overlay/README.en.md +121 -0
  14. codex_flow/data/apps/macos-overlay/README.md +123 -0
  15. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
  16. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
  17. codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
  18. codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
  19. codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
  20. codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
  21. codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
  22. codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
  23. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
  24. codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
  25. codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
  26. codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
  27. codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
  28. codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
  29. codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
  30. codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
  31. codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
  32. codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
  33. codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
  34. codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
  35. codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
  36. codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
  37. codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
  38. codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
  39. codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
  40. codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
  41. codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
  42. codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
  43. codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
  44. codex_flow/data/apps/macos-overlay/build.sh +75 -0
  45. codex_flow/data/benchmark/corpus.json +103 -0
  46. codex_flow/data/benchmark/manifest.example.json +41 -0
  47. codex_flow/data/benchmark/manifest.schema.json +137 -0
  48. codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
  49. codex_flow/data/benchmark/profiles.json +90 -0
  50. codex_flow/data/benchmark/schema.json +77 -0
  51. codex_flow/data/benchmark/tasks.json +50 -0
  52. codex_flow/data/completions/codex-flow.bash +34 -0
  53. codex_flow/data/completions/codex-flow.zsh +52 -0
  54. codex_flow/data/glama.json +6 -0
  55. codex_flow/data/install-release.ps1 +126 -0
  56. codex_flow/data/install-release.sh +155 -0
  57. codex_flow/data/install.ps1 +349 -0
  58. codex_flow/data/install.sh +362 -0
  59. codex_flow/data/policy/benchmark.toml +49 -0
  60. codex_flow/data/policy/defaults.toml +70 -0
  61. codex_flow/data/scripts/analyze-benchmark.py +510 -0
  62. codex_flow/data/scripts/benchmark-local.py +171 -0
  63. codex_flow/data/scripts/check-recommendation.py +277 -0
  64. codex_flow/data/scripts/doctor.py +449 -0
  65. codex_flow/data/scripts/generate-release-manifest.py +74 -0
  66. codex_flow/data/scripts/localization.py +192 -0
  67. codex_flow/data/scripts/manage-hooks.py +448 -0
  68. codex_flow/data/scripts/manage-instructions.py +389 -0
  69. codex_flow/data/scripts/manage-shell.py +151 -0
  70. codex_flow/data/scripts/materialize-corpus.py +193 -0
  71. codex_flow/data/scripts/menu.py +646 -0
  72. codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
  73. codex_flow/data/scripts/package-release.py +132 -0
  74. codex_flow/data/scripts/render-benchmark-report.py +292 -0
  75. codex_flow/data/scripts/run-benchmark.py +829 -0
  76. codex_flow/data/scripts/strategies/__init__.py +28 -0
  77. codex_flow/data/scripts/strategies/balanced.py +115 -0
  78. codex_flow/data/scripts/strategies/base.py +363 -0
  79. codex_flow/data/scripts/strategies/efficient.py +158 -0
  80. codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
  81. codex_flow/data/scripts/strategies/quality.py +209 -0
  82. codex_flow/data/scripts/strategies/speed.py +108 -0
  83. codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
  84. codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
  85. codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
  86. codex_flow/data/scripts/strategy_runtime.py +1091 -0
  87. codex_flow/data/scripts/telemetry.py +400 -0
  88. codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
  89. codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
  90. codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
  91. codex_flow/data/scripts/telemetry_core/common.py +421 -0
  92. codex_flow/data/scripts/telemetry_core/latency.py +593 -0
  93. codex_flow/data/scripts/telemetry_core/query.py +427 -0
  94. codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
  95. codex_flow/data/scripts/telemetry_core/render.py +460 -0
  96. codex_flow/data/scripts/telemetry_core/repair.py +223 -0
  97. codex_flow/data/scripts/ui.py +266 -0
  98. codex_flow/data/scripts/update-homebrew-formula.py +146 -0
  99. codex_flow/data/scripts/update_runtime_config.py +134 -0
  100. codex_flow/data/scripts/updater.py +1718 -0
  101. codex_flow/data/smithery.yaml +18 -0
  102. codex_flow/data/templates/agents/worker-explorer.toml +24 -0
  103. codex_flow/data/templates/agents/worker-implementer.toml +49 -0
  104. codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
  105. codex_flow/data/templates/flow-pilot-instructions.md +35 -0
  106. codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
  107. codex_flow/mcp.py +35 -0
  108. codex_flow-2.1.13.dist-info/METADATA +342 -0
  109. codex_flow-2.1.13.dist-info/RECORD +113 -0
  110. codex_flow-2.1.13.dist-info/WHEEL +5 -0
  111. codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
  112. codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
  113. codex_flow-2.1.13.dist-info/top_level.txt +1 -0
@@ -0,0 +1,153 @@
1
+ import Foundation
2
+ import Darwin
3
+
4
+ /// Process-lifetime singleton lock for the native FlowPilot overlay.
5
+ ///
6
+ /// The lock file is deliberately never removed. `flock` tracks ownership by
7
+ /// open file description, so a process crash releases the lock while keeping a
8
+ /// stable path for future processes to open. The descriptor is marked
9
+ /// close-on-exec so helper processes cannot accidentally keep the overlay
10
+ /// alive after the app exits.
11
+ public final class FlowPilotInstanceLock {
12
+ public static let launchAgentEnvironmentKey = "CODEX_FLOW_LAUNCHD_START"
13
+ public static let manualHandoffTimeout: TimeInterval = 5.0
14
+
15
+ public static var lockPath: String {
16
+ let home = FileManager.default.homeDirectoryForCurrentUser
17
+ let codexHome = ProcessInfo.processInfo.environment["CODEX_HOME"]
18
+ ?? home.appendingPathComponent(".codex").path
19
+ let directory = URL(fileURLWithPath: codexHome).appendingPathComponent("codex-flow")
20
+ try? FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
21
+ return directory.appendingPathComponent("overlay.lock").path
22
+ }
23
+
24
+ public static var isLaunchAgentStart: Bool {
25
+ guard let raw = ProcessInfo.processInfo.environment[launchAgentEnvironmentKey]?
26
+ .trimmingCharacters(in: .whitespacesAndNewlines)
27
+ .lowercased() else {
28
+ return false
29
+ }
30
+ return raw == "1" || raw == "true" || raw == "yes"
31
+ }
32
+
33
+ public enum LockError: Error, LocalizedError {
34
+ case openFailed(path: String, errno: Int32)
35
+ case acquisitionFailed(path: String, errno: Int32)
36
+ case timedOut(path: String, timeout: TimeInterval)
37
+ case closed
38
+
39
+ public var errorDescription: String? {
40
+ switch self {
41
+ case let .openFailed(path, errorNumber):
42
+ return "Could not open FlowPilot instance lock at \(path): \(Self.message(for: errorNumber))"
43
+ case let .acquisitionFailed(path, errorNumber):
44
+ return "Could not acquire FlowPilot instance lock at \(path): \(Self.message(for: errorNumber))"
45
+ case let .timedOut(path, timeout):
46
+ return "Timed out after \(String(format: "%.1f", timeout))s waiting for FlowPilot instance lock at \(path)."
47
+ case .closed:
48
+ return "FlowPilot instance lock is closed."
49
+ }
50
+ }
51
+
52
+ private static func message(for errorNumber: Int32) -> String {
53
+ guard let message = strerror(errorNumber) else {
54
+ return "errno \(errorNumber)"
55
+ }
56
+ return String(cString: message)
57
+ }
58
+ }
59
+
60
+ private let path: String
61
+ private var fileDescriptor: Int32
62
+ private var ownsLock = false
63
+
64
+ public init(path: String = FlowPilotInstanceLock.lockPath) throws {
65
+ self.path = path
66
+
67
+ let directory = URL(fileURLWithPath: path).deletingLastPathComponent()
68
+ try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
69
+
70
+ let mode = mode_t(0o600)
71
+ let descriptor = open(path, O_CREAT | O_RDWR | O_CLOEXEC, mode)
72
+ guard descriptor >= 0 else {
73
+ throw LockError.openFailed(path: path, errno: errno)
74
+ }
75
+
76
+ self.fileDescriptor = descriptor
77
+ // `open(..., 0600)` only applies when the file is created. Tighten an
78
+ // existing lock left by an older build as well, without replacing its
79
+ // inode or disturbing an active flock owner.
80
+ if fchmod(descriptor, mode) != 0 {
81
+ let errorNumber = errno
82
+ close(descriptor)
83
+ throw LockError.openFailed(path: path, errno: errorNumber)
84
+ }
85
+ }
86
+
87
+ /// Acquire the exclusive lock, retrying transient contention until the
88
+ /// monotonic timeout expires. A zero timeout performs one non-blocking
89
+ /// attempt, which is used to decide whether a manual handoff is needed.
90
+ public func acquire(timeout: TimeInterval) throws {
91
+ guard fileDescriptor >= 0 else { throw LockError.closed }
92
+
93
+ if ownsLock { return }
94
+ let deadline = Self.monotonicDeadline(after: timeout)
95
+
96
+ while true {
97
+ if flock(fileDescriptor, LOCK_EX | LOCK_NB) == 0 {
98
+ ownsLock = true
99
+ return
100
+ }
101
+
102
+ let errorNumber = errno
103
+ guard errorNumber == EINTR || errorNumber == EWOULDBLOCK || errorNumber == EAGAIN else {
104
+ throw LockError.acquisitionFailed(path: path, errno: errorNumber)
105
+ }
106
+
107
+ let current = Self.monotonicNow()
108
+ if current >= deadline {
109
+ throw LockError.timedOut(path: path, timeout: max(0, timeout))
110
+ }
111
+
112
+ // A short bounded sleep avoids a busy loop while keeping handoff
113
+ // responsive. If usleep is interrupted, the outer loop retries
114
+ // and re-checks the same monotonic deadline.
115
+ let remaining = deadline - current
116
+ let micros = min(UInt64(10_000), max(UInt64(1), remaining / 1_000))
117
+ _ = usleep(UInt32(min(micros, UInt64(UInt32.max))))
118
+ }
119
+ }
120
+
121
+ /// Release ownership and close the descriptor. The lock file itself is
122
+ /// intentionally retained for the next process to open.
123
+ public func release() {
124
+ guard fileDescriptor >= 0 else { return }
125
+ if ownsLock {
126
+ _ = flock(fileDescriptor, LOCK_UN)
127
+ ownsLock = false
128
+ }
129
+ close(fileDescriptor)
130
+ fileDescriptor = -1
131
+ }
132
+
133
+ deinit {
134
+ release()
135
+ }
136
+
137
+ private static func monotonicNow() -> UInt64 {
138
+ DispatchTime.now().uptimeNanoseconds
139
+ }
140
+
141
+ private static func monotonicDeadline(after timeout: TimeInterval) -> UInt64 {
142
+ let now = monotonicNow()
143
+ guard timeout.isFinite, timeout > 0 else {
144
+ return timeout.isFinite ? now : UInt64.max
145
+ }
146
+
147
+ let requested = timeout * 1_000_000_000
148
+ let delta = requested >= Double(UInt64.max)
149
+ ? UInt64.max
150
+ : UInt64(max(1, requested.rounded(.up)))
151
+ return UInt64.max - now < delta ? UInt64.max : now + delta
152
+ }
153
+ }
@@ -0,0 +1,298 @@
1
+ import Foundation
2
+ import Cocoa
3
+ import Darwin
4
+
5
+ public class IPCService {
6
+ public static var socketPath: String {
7
+ let home = FileManager.default.homeDirectoryForCurrentUser
8
+ let codexHome = ProcessInfo.processInfo.environment["CODEX_HOME"] ?? home.appendingPathComponent(".codex").path
9
+ let dir = URL(fileURLWithPath: codexHome).appendingPathComponent("codex-flow")
10
+ try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
11
+ return dir.appendingPathComponent("overlay.sock").path
12
+ }
13
+
14
+ // MARK: - Server Implementation
15
+ public class Server {
16
+ public let state: OverlayState
17
+ private var serverSource: DispatchSourceRead?
18
+ private var serverSocket: Int32 = -1
19
+ private let path: String
20
+ private var boundSocketFileNumber: UInt64?
21
+ public private(set) var isRunning = false
22
+
23
+ public init(state: OverlayState, socketPath: String = IPCService.socketPath) {
24
+ self.state = state
25
+ self.path = socketPath
26
+ start()
27
+ }
28
+
29
+ public func start() {
30
+ guard serverSource == nil, serverSocket < 0 else { return }
31
+ unlink(path)
32
+
33
+ serverSocket = socket(AF_UNIX, SOCK_STREAM, 0)
34
+ guard serverSocket >= 0 else { return }
35
+
36
+ var addr = sockaddr_un()
37
+ addr.sun_family = sa_family_t(AF_UNIX)
38
+ let pathMax = MemoryLayout.size(ofValue: addr.sun_path)
39
+
40
+ withUnsafeMutablePointer(to: &addr.sun_path) { ptr in
41
+ let rawPtr = UnsafeMutableRawPointer(ptr).assumingMemoryBound(to: CChar.self)
42
+ _ = path.withCString { cstr in
43
+ strncpy(rawPtr, cstr, pathMax - 1)
44
+ }
45
+ }
46
+
47
+ let addrLen = socklen_t(MemoryLayout<sockaddr_un>.size)
48
+ let bindResult = withUnsafePointer(to: &addr) { ptr in
49
+ ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
50
+ bind(serverSocket, sa, addrLen)
51
+ }
52
+ }
53
+
54
+ guard bindResult == 0 else {
55
+ close(serverSocket)
56
+ serverSocket = -1
57
+ return
58
+ }
59
+
60
+ // Remember the filesystem socket node we actually bound. During a
61
+ // restart an older process can finish after a newer process has
62
+ // rebound the same path; only the owner of the current node may
63
+ // unlink it during shutdown.
64
+ boundSocketFileNumber = socketFileNumber(at: path)
65
+
66
+ guard listen(serverSocket, 5) == 0 else {
67
+ let expected = boundSocketFileNumber
68
+ close(serverSocket)
69
+ serverSocket = -1
70
+ if let expected,
71
+ self.socketFileNumber(at: path) == expected {
72
+ unlink(path)
73
+ }
74
+ boundSocketFileNumber = nil
75
+ return
76
+ }
77
+
78
+ let source = DispatchSource.makeReadSource(fileDescriptor: serverSocket, queue: .main)
79
+ source.setEventHandler { [weak self] in
80
+ self?.acceptConnection()
81
+ }
82
+ source.setCancelHandler { [weak self] in
83
+ guard let self else { return }
84
+ self.isRunning = false
85
+ if self.serverSocket >= 0 {
86
+ close(self.serverSocket)
87
+ self.serverSocket = -1
88
+ }
89
+ if let expected = self.boundSocketFileNumber,
90
+ self.socketFileNumber(at: self.path) == expected {
91
+ unlink(self.path)
92
+ }
93
+ }
94
+ source.resume()
95
+ self.serverSource = source
96
+ self.isRunning = true
97
+ }
98
+
99
+ private func socketFileNumber(at path: String) -> UInt64? {
100
+ guard let attributes = try? FileManager.default.attributesOfItem(atPath: path),
101
+ let number = attributes[.systemFileNumber] as? NSNumber else {
102
+ return nil
103
+ }
104
+ return number.uint64Value
105
+ }
106
+
107
+ private func acceptConnection() {
108
+ var clientAddr = sockaddr_un()
109
+ var clientAddrLen = socklen_t(MemoryLayout<sockaddr_un>.size)
110
+ let clientSocket = withUnsafeMutablePointer(to: &clientAddr) { ptr in
111
+ ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
112
+ accept(serverSocket, sa, &clientAddrLen)
113
+ }
114
+ }
115
+
116
+ guard clientSocket >= 0 else { return }
117
+ var noSigPipe: Int32 = 1
118
+ _ = setsockopt(
119
+ clientSocket,
120
+ SOL_SOCKET,
121
+ SO_NOSIGPIPE,
122
+ &noSigPipe,
123
+ socklen_t(MemoryLayout<Int32>.size)
124
+ )
125
+
126
+ var buffer = [UInt8](repeating: 0, count: 4096)
127
+ let bytesRead = read(clientSocket, &buffer, buffer.count)
128
+ if bytesRead > 0 {
129
+ let data = Data(buffer[0..<bytesRead])
130
+ if let command = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) {
131
+ let response = handleCommand(command)
132
+ if let resData = response.data(using: .utf8) {
133
+ _ = resData.withUnsafeBytes { raw in
134
+ write(clientSocket, raw.baseAddress, resData.count)
135
+ }
136
+ }
137
+ }
138
+ }
139
+ close(clientSocket)
140
+ }
141
+
142
+ private func handleCommand(_ cmd: String) -> String {
143
+ if cmd == "status" {
144
+ return "{\"running\": true, \"pid\": \(getpid()), \"isExpanded\": \(state.isExpanded), \"isPinned\": \(state.isPinned), \"activeTab\": \"\(state.activeTab.rawValue)\"}\n"
145
+ } else if cmd == "toggle" {
146
+ state.toggle()
147
+ return "{\"ok\": true, \"action\": \"toggle\", \"isExpanded\": \(state.isExpanded)}\n"
148
+ } else if cmd == "expand" {
149
+ state.expand()
150
+ return "{\"ok\": true, \"action\": \"expand\"}\n"
151
+ } else if cmd == "collapse" {
152
+ state.collapse()
153
+ return "{\"ok\": true, \"action\": \"collapse\"}\n"
154
+ } else if cmd.hasPrefix("tab") {
155
+ let target = cmd.dropFirst("tab".count).trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
156
+ if target.contains("hist") {
157
+ state.selectTab(.history)
158
+ state.expand()
159
+ return "{\"ok\": true, \"tab\": \"History\"}\n"
160
+ } else if target.contains("stat") || target.contains("analyt") {
161
+ state.selectTab(.analytics)
162
+ state.expand()
163
+ return "{\"ok\": true, \"tab\": \"Analytics\"}\n"
164
+ } else {
165
+ state.selectTab(.inspector)
166
+ state.expand()
167
+ return "{\"ok\": true, \"tab\": \"Inspector\"}\n"
168
+ }
169
+ } else if cmd.hasPrefix("show") {
170
+ let target = cmd.dropFirst("show".count).trimmingCharacters(in: .whitespacesAndNewlines)
171
+ if let run = TelemetryQueryEngine.shared.fetchRun(identifier: target) {
172
+ state.inspect(run: run)
173
+ state.expand()
174
+ return "{\"ok\": true, \"inspected\": \"\(run.sessionTitle)\"}\n"
175
+ }
176
+ return "{\"ok\": false, \"error\": \"run not found for '\(target)'\"}\n"
177
+ } else if cmd.hasPrefix("stats") {
178
+ let parts = cmd.dropFirst("stats".count).trimmingCharacters(in: .whitespacesAndNewlines)
179
+ if let d = Int(parts), d > 0 {
180
+ state.statsDays = d
181
+ }
182
+ state.selectTab(.analytics)
183
+ state.expand()
184
+ return "{\"ok\": true, \"action\": \"showing_stats\", \"days\": \(state.statsDays)}\n"
185
+ } else if cmd.hasPrefix("history") || cmd.hasPrefix("list") {
186
+ state.selectTab(.history)
187
+ state.expand()
188
+ return "{\"ok\": true, \"action\": \"showing_history\"}\n"
189
+ } else if cmd.hasPrefix("update") {
190
+ let payload = cmd.dropFirst("update".count).trimmingCharacters(in: .whitespacesAndNewlines)
191
+ if payload.isEmpty {
192
+ let home = FileManager.default.homeDirectoryForCurrentUser
193
+ let codexHome = ProcessInfo.processInfo.environment["CODEX_HOME"] ?? home.appendingPathComponent(".codex").path
194
+ let lastUrl = URL(fileURLWithPath: codexHome).appendingPathComponent("codex-flow").appendingPathComponent("telemetry").appendingPathComponent("last.json")
195
+ if let data = try? Data(contentsOf: lastUrl),
196
+ var run = try? JSONDecoder().decode(TaskRun.self, from: data) {
197
+ TelemetryQueryEngine.shared.enrichRunIfNeeded(&run)
198
+ state.update(run: run)
199
+ state.expand()
200
+ return "{\"ok\": true, \"updatedFrom\": \"last.json\"}\n"
201
+ }
202
+ state.expand()
203
+ return "{\"ok\": true, \"action\": \"expanded\"}\n"
204
+ } else {
205
+ if let fileData = try? Data(contentsOf: URL(fileURLWithPath: payload)),
206
+ var run = try? JSONDecoder().decode(TaskRun.self, from: fileData) {
207
+ TelemetryQueryEngine.shared.enrichRunIfNeeded(&run)
208
+ state.update(run: run)
209
+ state.expand()
210
+ return "{\"ok\": true, \"updatedFrom\": \"file\"}\n"
211
+ } else if let json = payload.data(using: .utf8),
212
+ var run = try? JSONDecoder().decode(TaskRun.self, from: json) {
213
+ TelemetryQueryEngine.shared.enrichRunIfNeeded(&run)
214
+ state.update(run: run)
215
+ state.expand()
216
+ return "{\"ok\": true, \"updatedFrom\": \"json\"}\n"
217
+ }
218
+ }
219
+ return "{\"ok\": false, \"error\": \"invalid payload\"}\n"
220
+ } else if cmd == "quit" || cmd == "stop" {
221
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
222
+ NSApplication.shared.terminate(nil)
223
+ }
224
+ return "{\"ok\": true, \"action\": \"quitting\"}\n"
225
+ }
226
+ return "{\"ok\": false, \"error\": \"unknown command\"}\n"
227
+ }
228
+
229
+ public func stop() {
230
+ serverSource?.cancel()
231
+ serverSource = nil
232
+ // The dispatch source owns final socket teardown. Mark the
233
+ // service stopped immediately so AppDelegate can release the
234
+ // singleton lock only after this cancellation has been issued.
235
+ isRunning = false
236
+ }
237
+ }
238
+
239
+ // MARK: - Client Implementation
240
+ public static func sendCommand(_ cmd: String, socketPath: String = IPCService.socketPath) -> (success: Bool, response: String) {
241
+ guard FileManager.default.fileExists(atPath: socketPath) else {
242
+ return (false, L("Overlay is not running (socket not found).", "悬浮窗未运行(未找到 socket)。"))
243
+ }
244
+
245
+ let clientSocket = socket(AF_UNIX, SOCK_STREAM, 0)
246
+ guard clientSocket >= 0 else {
247
+ return (false, L("Failed to create client socket.", "创建客户端 socket 失败。"))
248
+ }
249
+ defer { close(clientSocket) }
250
+ var noSigPipe: Int32 = 1
251
+ _ = setsockopt(
252
+ clientSocket,
253
+ SOL_SOCKET,
254
+ SO_NOSIGPIPE,
255
+ &noSigPipe,
256
+ socklen_t(MemoryLayout<Int32>.size)
257
+ )
258
+
259
+ var addr = sockaddr_un()
260
+ addr.sun_family = sa_family_t(AF_UNIX)
261
+ let pathMax = MemoryLayout.size(ofValue: addr.sun_path)
262
+
263
+ withUnsafeMutablePointer(to: &addr.sun_path) { ptr in
264
+ let rawPtr = UnsafeMutableRawPointer(ptr).assumingMemoryBound(to: CChar.self)
265
+ _ = socketPath.withCString { cstr in
266
+ strncpy(rawPtr, cstr, pathMax - 1)
267
+ }
268
+ }
269
+
270
+ let addrLen = socklen_t(MemoryLayout<sockaddr_un>.size)
271
+ let connectResult = withUnsafePointer(to: &addr) { ptr in
272
+ ptr.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
273
+ connect(clientSocket, sa, addrLen)
274
+ }
275
+ }
276
+
277
+ guard connectResult == 0 else {
278
+ return (false, L("Failed to connect to overlay socket.", "连接悬浮窗 socket 失败。"))
279
+ }
280
+
281
+ guard let data = cmd.data(using: .utf8) else {
282
+ return (false, L("Encoding error.", "编码错误。"))
283
+ }
284
+
285
+ _ = data.withUnsafeBytes { raw in
286
+ write(clientSocket, raw.baseAddress, data.count)
287
+ }
288
+
289
+ var buffer = [UInt8](repeating: 0, count: 4096)
290
+ let bytesRead = read(clientSocket, &buffer, buffer.count)
291
+ if bytesRead > 0 {
292
+ let response = String(data: Data(buffer[0..<bytesRead]), encoding: .utf8) ?? ""
293
+ return (true, response.trimmingCharacters(in: .whitespacesAndNewlines))
294
+ }
295
+
296
+ return (true, "OK")
297
+ }
298
+ }