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.
- codex_flow/__init__.py +28 -0
- codex_flow/__main__.py +9 -0
- codex_flow/cli.py +242 -0
- codex_flow/data/LICENSE +21 -0
- codex_flow/data/README.en.md +303 -0
- codex_flow/data/README.md +305 -0
- codex_flow/data/VERSION +1 -0
- codex_flow/data/apps/chatgpt-mcp/README.md +86 -0
- codex_flow/data/apps/chatgpt-mcp/__init__.py +1 -0
- codex_flow/data/apps/chatgpt-mcp/adapter.py +458 -0
- codex_flow/data/apps/chatgpt-mcp/server.py +358 -0
- codex_flow/data/apps/chatgpt-mcp/widget.html +927 -0
- codex_flow/data/apps/macos-overlay/README.en.md +121 -0
- codex_flow/data/apps/macos-overlay/README.md +123 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayRuntimeState.swift +126 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayScreenGeometry.swift +82 -0
- codex_flow/data/apps/macos-overlay/Sources/Controllers/OverlayWindowController.swift +1052 -0
- codex_flow/data/apps/macos-overlay/Sources/Localization.swift +197 -0
- codex_flow/data/apps/macos-overlay/Sources/Models/TelemetryData.swift +1557 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/AccountSnapshotService.swift +1101 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/FlowPilotInstanceLock.swift +153 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/IPCServer.swift +298 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryQueryEngine.swift +800 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/TelemetryWatcher.swift +135 -0
- codex_flow/data/apps/macos-overlay/Sources/Services/UpdateService.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AccountView.swift +610 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AnalyticsView.swift +566 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/AutostartView.swift +293 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/BubbleView.swift +317 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HistoryView.swift +1124 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/HoverRevealText.swift +165 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/InspectorSkillsToolsView.swift +121 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/LogoView.swift +182 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SleekSwitch.swift +117 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/StrategyModeView.swift +561 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/SummaryView.swift +1273 -0
- codex_flow/data/apps/macos-overlay/Sources/Views/UpdateView.swift +352 -0
- codex_flow/data/apps/macos-overlay/Sources/main.swift +340 -0
- codex_flow/data/apps/macos-overlay/Tests/OverlayScreenGeometryTests.swift +163 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryPhase1ContractTests.swift +357 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQueryEngineConcurrencyTests.swift +221 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryQuotaSelectionTests.swift +158 -0
- codex_flow/data/apps/macos-overlay/Tests/TelemetryWorkerTokenTests.swift +122 -0
- codex_flow/data/apps/macos-overlay/build.sh +75 -0
- codex_flow/data/benchmark/corpus.json +103 -0
- codex_flow/data/benchmark/manifest.example.json +41 -0
- codex_flow/data/benchmark/manifest.schema.json +137 -0
- codex_flow/data/benchmark/prices/gpt-5.6-2026-08-30.json +5 -0
- codex_flow/data/benchmark/profiles.json +90 -0
- codex_flow/data/benchmark/schema.json +77 -0
- codex_flow/data/benchmark/tasks.json +50 -0
- codex_flow/data/completions/codex-flow.bash +34 -0
- codex_flow/data/completions/codex-flow.zsh +52 -0
- codex_flow/data/glama.json +6 -0
- codex_flow/data/install-release.ps1 +126 -0
- codex_flow/data/install-release.sh +155 -0
- codex_flow/data/install.ps1 +349 -0
- codex_flow/data/install.sh +362 -0
- codex_flow/data/policy/benchmark.toml +49 -0
- codex_flow/data/policy/defaults.toml +70 -0
- codex_flow/data/scripts/analyze-benchmark.py +510 -0
- codex_flow/data/scripts/benchmark-local.py +171 -0
- codex_flow/data/scripts/check-recommendation.py +277 -0
- codex_flow/data/scripts/doctor.py +449 -0
- codex_flow/data/scripts/generate-release-manifest.py +74 -0
- codex_flow/data/scripts/localization.py +192 -0
- codex_flow/data/scripts/manage-hooks.py +448 -0
- codex_flow/data/scripts/manage-instructions.py +389 -0
- codex_flow/data/scripts/manage-shell.py +151 -0
- codex_flow/data/scripts/materialize-corpus.py +193 -0
- codex_flow/data/scripts/menu.py +646 -0
- codex_flow/data/scripts/migrations/0001_update_settings.py +80 -0
- codex_flow/data/scripts/package-release.py +132 -0
- codex_flow/data/scripts/render-benchmark-report.py +292 -0
- codex_flow/data/scripts/run-benchmark.py +829 -0
- codex_flow/data/scripts/strategies/__init__.py +28 -0
- codex_flow/data/scripts/strategies/balanced.py +115 -0
- codex_flow/data/scripts/strategies/base.py +363 -0
- codex_flow/data/scripts/strategies/efficient.py +158 -0
- codex_flow/data/scripts/strategies/lifecycle_runtime.py +590 -0
- codex_flow/data/scripts/strategies/quality.py +209 -0
- codex_flow/data/scripts/strategies/speed.py +108 -0
- codex_flow/data/scripts/strategies/task_budget_runtime.py +644 -0
- codex_flow/data/scripts/strategies/task_phase_runtime.py +341 -0
- codex_flow/data/scripts/strategies/work_unit_runtime.py +421 -0
- codex_flow/data/scripts/strategy_runtime.py +1091 -0
- codex_flow/data/scripts/telemetry.py +400 -0
- codex_flow/data/scripts/telemetry_core/__init__.py +192 -0
- codex_flow/data/scripts/telemetry_core/app_server.py +1192 -0
- codex_flow/data/scripts/telemetry_core/collector.py +1247 -0
- codex_flow/data/scripts/telemetry_core/common.py +421 -0
- codex_flow/data/scripts/telemetry_core/latency.py +593 -0
- codex_flow/data/scripts/telemetry_core/query.py +427 -0
- codex_flow/data/scripts/telemetry_core/quota_ledger.py +598 -0
- codex_flow/data/scripts/telemetry_core/render.py +460 -0
- codex_flow/data/scripts/telemetry_core/repair.py +223 -0
- codex_flow/data/scripts/ui.py +266 -0
- codex_flow/data/scripts/update-homebrew-formula.py +146 -0
- codex_flow/data/scripts/update_runtime_config.py +134 -0
- codex_flow/data/scripts/updater.py +1718 -0
- codex_flow/data/smithery.yaml +18 -0
- codex_flow/data/templates/agents/worker-explorer.toml +24 -0
- codex_flow/data/templates/agents/worker-implementer.toml +49 -0
- codex_flow/data/templates/agents/worker-reviewer.toml +25 -0
- codex_flow/data/templates/flow-pilot-instructions.md +35 -0
- codex_flow/data/templates/skills/flow-pilot/SKILL.md +577 -0
- codex_flow/mcp.py +35 -0
- codex_flow-2.1.13.dist-info/METADATA +342 -0
- codex_flow-2.1.13.dist-info/RECORD +113 -0
- codex_flow-2.1.13.dist-info/WHEEL +5 -0
- codex_flow-2.1.13.dist-info/entry_points.txt +3 -0
- codex_flow-2.1.13.dist-info/licenses/LICENSE +21 -0
- 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
|
+
}
|