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,352 @@
1
+ import SwiftUI
2
+
3
+ public struct FlowPilotUpdateView: View {
4
+ @ObservedObject private var service = FlowPilotUpdateService.shared
5
+
6
+ public init() {}
7
+
8
+ public var body: some View {
9
+ VStack(alignment: .leading, spacing: 10) {
10
+ headerSection
11
+
12
+ ScrollView(.vertical, showsIndicators: true) {
13
+ VStack(alignment: .leading, spacing: 10) {
14
+ versionCard
15
+ autoUpdateCard
16
+ releaseNotesSection
17
+ feedbackMessageSection
18
+ }
19
+ .padding(.horizontal, 1)
20
+ }
21
+ .frame(maxHeight: 230)
22
+
23
+ if service.isRestartRequired {
24
+ restartSection
25
+ }
26
+
27
+ Divider()
28
+ .opacity(0.35)
29
+
30
+ actionButtons
31
+ }
32
+ .padding(14)
33
+ .frame(width: 330)
34
+ .onAppear {
35
+ service.refreshFromDisk()
36
+ service.requestCachedCheck()
37
+ }
38
+ }
39
+
40
+ private var headerSection: some View {
41
+ HStack(spacing: 8) {
42
+ ZStack {
43
+ Circle()
44
+ .fill(Color.cyan.opacity(0.16))
45
+ .frame(width: 34, height: 34)
46
+ Image(systemName: service.isRestartRequired ? "arrow.clockwise.circle.fill" : "arrow.down.circle.fill")
47
+ .font(.system(size: 17, weight: .semibold))
48
+ .foregroundColor(service.isRestartRequired ? .orange : .cyan)
49
+ }
50
+ VStack(alignment: .leading, spacing: 2) {
51
+ Text(L("Software Update", "软件更新"))
52
+ .font(.system(size: 13, weight: .bold, design: .rounded))
53
+ Text(service.statusText)
54
+ .font(.system(size: 9.5, weight: .medium))
55
+ .foregroundColor(.secondary)
56
+ .lineLimit(2)
57
+ }
58
+ Spacer(minLength: 4)
59
+ }
60
+ }
61
+
62
+ private var versionCard: some View {
63
+ VStack(spacing: 6) {
64
+ versionRow(
65
+ title: L("Current", "当前版本"),
66
+ value: service.snapshot.currentVersion.map { "v\($0)" } ?? "—"
67
+ )
68
+ versionRow(
69
+ title: L("Latest", "最新版本"),
70
+ value: service.snapshot.latestVersion.map { "v\($0)" } ?? "—"
71
+ )
72
+ versionRow(
73
+ title: L("Channel", "更新通道"),
74
+ value: service.snapshot.channel?.capitalized ?? "Stable"
75
+ )
76
+ }
77
+ .padding(9)
78
+ .background(
79
+ RoundedRectangle(cornerRadius: 9)
80
+ .fill(Color.primary.opacity(0.045))
81
+ )
82
+ }
83
+
84
+ private var autoUpdateCard: some View {
85
+ HStack(spacing: 8) {
86
+ VStack(alignment: .leading, spacing: 2) {
87
+ Text(L("Automatic Updates", "自动更新"))
88
+ .font(.system(size: 10, weight: .bold))
89
+ Text(L("Automatically download, install, and restart", "发现新版本时自动下载安装并重启"))
90
+ .font(.system(size: 8))
91
+ .foregroundColor(.secondary)
92
+ }
93
+ Spacer()
94
+ if service.isChangingAutoUpdate {
95
+ ProgressView().controlSize(.mini)
96
+ }
97
+ Toggle("", isOn: Binding(
98
+ get: { service.isAutoUpdateEnabled },
99
+ set: { service.setAutoUpdateEnabled($0) }
100
+ ))
101
+ .labelsHidden()
102
+ .toggleStyle(SleekSwitchToggleStyle(tint: .cyan, width: 32, height: 18))
103
+ .disabled(
104
+ service.isChangingAutoUpdate
105
+ || service.isInstalling
106
+ || service.isRestartingFlowPilot
107
+ || service.isAutoRestartScheduled
108
+ )
109
+ }
110
+ .padding(8)
111
+ .background(
112
+ RoundedRectangle(cornerRadius: 8)
113
+ .fill(Color.primary.opacity(0.035))
114
+ )
115
+ }
116
+
117
+ @ViewBuilder
118
+ private var releaseNotesSection: some View {
119
+ if let notes = service.snapshot.releaseNotes, !notes.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
120
+ VStack(alignment: .leading, spacing: 5) {
121
+ HStack {
122
+ Text(L("What's New", "更新内容"))
123
+ .font(.system(size: 10, weight: .bold))
124
+ Spacer()
125
+ if let urlString = service.snapshot.releaseURL, let url = URL(string: urlString) {
126
+ Link(destination: url) {
127
+ HStack(spacing: 2) {
128
+ Text(L("Changelog", "更新日志"))
129
+ Image(systemName: "arrow.up.right")
130
+ }
131
+ .font(.system(size: 8.5, weight: .medium))
132
+ .foregroundColor(.cyan)
133
+ }
134
+ }
135
+ }
136
+
137
+ ScrollView(.vertical, showsIndicators: true) {
138
+ Text(notes)
139
+ .font(.system(size: 9.5, weight: .regular))
140
+ .foregroundColor(.secondary)
141
+ .frame(maxWidth: .infinity, alignment: .leading)
142
+ .textSelection(.enabled)
143
+ .padding(8)
144
+ }
145
+ .frame(height: 96)
146
+ .background(
147
+ RoundedRectangle(cornerRadius: 8)
148
+ .fill(Color.primary.opacity(0.035))
149
+ )
150
+ .overlay(
151
+ RoundedRectangle(cornerRadius: 8)
152
+ .stroke(Color.primary.opacity(0.06), lineWidth: 1)
153
+ )
154
+ }
155
+ }
156
+ }
157
+
158
+ @ViewBuilder
159
+ private var feedbackMessageSection: some View {
160
+ if let error = service.actionError, !error.isEmpty {
161
+ HStack(alignment: .top, spacing: 6) {
162
+ Image(systemName: "exclamationmark.circle.fill")
163
+ .font(.system(size: 9.5))
164
+ .foregroundColor(.orange)
165
+ .padding(.top, 1)
166
+ ScrollView(.vertical, showsIndicators: true) {
167
+ Text(error)
168
+ .font(.system(size: 9, weight: .medium))
169
+ .foregroundColor(.orange)
170
+ .frame(maxWidth: .infinity, alignment: .leading)
171
+ }
172
+ .frame(maxHeight: 52)
173
+ }
174
+ .padding(7)
175
+ .background(
176
+ RoundedRectangle(cornerRadius: 7)
177
+ .fill(Color.orange.opacity(0.08))
178
+ )
179
+ } else if let message = service.actionMessage, !message.isEmpty {
180
+ Text(message)
181
+ .font(.system(size: 9, weight: .medium))
182
+ .foregroundColor(.secondary)
183
+ .lineLimit(3)
184
+ .fixedSize(horizontal: false, vertical: true)
185
+ }
186
+ }
187
+
188
+ @ViewBuilder
189
+ private var restartSection: some View {
190
+ VStack(alignment: .leading, spacing: 7) {
191
+ HStack(alignment: .top, spacing: 6) {
192
+ Image(systemName: "exclamationmark.triangle.fill")
193
+ .font(.system(size: 9))
194
+ .foregroundColor(.orange)
195
+ Text(restartExplanation)
196
+ .font(.system(size: 9, weight: .medium))
197
+ .foregroundColor(.secondary)
198
+ .fixedSize(horizontal: false, vertical: true)
199
+ }
200
+
201
+ HStack(spacing: 6) {
202
+ if service.isFlowPilotRestartRequired {
203
+ Button {
204
+ service.restartFlowPilot()
205
+ } label: {
206
+ HStack(spacing: 4) {
207
+ if service.isRestartingFlowPilot || service.isAutoRestartScheduled {
208
+ ProgressView().controlSize(.mini)
209
+ } else {
210
+ Image(systemName: "arrow.clockwise")
211
+ }
212
+ Text(service.isAutoRestartScheduled ? L("Restarting…", "正在重启…") : L("Restart FlowPilot", "重启 FlowPilot"))
213
+ }
214
+ .font(.system(size: 9.5, weight: .semibold))
215
+ }
216
+ .buttonStyle(.borderedProminent)
217
+ .disabled(
218
+ service.isRestartingFlowPilot
219
+ || service.isAutoRestartScheduled
220
+ || service.isAcknowledgingRestart
221
+ || service.isInstalling
222
+ || service.isChecking
223
+ )
224
+ }
225
+
226
+ if service.isCodexRestartRequired {
227
+ Button {
228
+ service.acknowledgeRestart()
229
+ } label: {
230
+ HStack(spacing: 4) {
231
+ if service.isAcknowledgingRestart {
232
+ ProgressView().controlSize(.mini)
233
+ } else {
234
+ Image(systemName: "checkmark.circle")
235
+ }
236
+ Text(L("I've restarted Codex", "我已重启 Codex"))
237
+ }
238
+ .font(.system(size: 9.5, weight: .semibold))
239
+ }
240
+ .buttonStyle(.bordered)
241
+ .disabled(
242
+ service.isAcknowledgingRestart
243
+ || service.isRestartingFlowPilot
244
+ || service.isAutoRestartScheduled
245
+ || service.isInstalling
246
+ || service.isChecking
247
+ )
248
+ }
249
+ }
250
+ }
251
+ .padding(8)
252
+ .background(
253
+ RoundedRectangle(cornerRadius: 8)
254
+ .fill(Color.orange.opacity(0.07))
255
+ )
256
+ }
257
+
258
+ private var actionButtons: some View {
259
+ HStack(spacing: 8) {
260
+ Button {
261
+ service.checkNow()
262
+ } label: {
263
+ HStack(spacing: 4) {
264
+ if service.isChecking {
265
+ ProgressView().controlSize(.mini)
266
+ } else {
267
+ Image(systemName: "arrow.clockwise")
268
+ }
269
+ Text(L("Check Again", "重新检查"))
270
+ }
271
+ .font(.system(size: 10, weight: .semibold))
272
+ .frame(maxWidth: .infinity, minHeight: 28)
273
+ }
274
+ .buttonStyle(.bordered)
275
+ .disabled(
276
+ service.isChecking
277
+ || service.isInstalling
278
+ || service.isAcknowledgingRestart
279
+ || service.isRestartingFlowPilot
280
+ || service.isAutoRestartScheduled
281
+ )
282
+
283
+ Button {
284
+ service.installUpdate()
285
+ } label: {
286
+ HStack(spacing: 4) {
287
+ if service.isInstalling {
288
+ ProgressView().controlSize(.mini)
289
+ } else {
290
+ Image(systemName: "arrow.down.circle.fill")
291
+ }
292
+ Text(service.isInstalling ? L("Updating…", "更新中…") : L("Update Now", "立即更新"))
293
+ }
294
+ .font(.system(size: 10, weight: .bold))
295
+ .frame(maxWidth: .infinity, minHeight: 28)
296
+ }
297
+ .buttonStyle(.borderedProminent)
298
+ .disabled(
299
+ service.isInstalling
300
+ || service.isChecking
301
+ || service.isAcknowledgingRestart
302
+ || service.isRestartingFlowPilot
303
+ || service.isAutoRestartScheduled
304
+ || service.snapshot.updateAvailable != true
305
+ || service.snapshot.artifactAvailable == false
306
+ )
307
+ }
308
+ }
309
+
310
+ private var restartExplanation: String {
311
+ if service.isFlowPilotRestartRequired && service.isCodexRestartRequired {
312
+ if let reason = service.codexRestartReason, !reason.isEmpty {
313
+ return L(
314
+ "The new files are installed (\(reason)). Restart FlowPilot to load the new app binary, and fully restart Codex to activate updated configurations.",
315
+ "新文件已经安装(\(reason))。请重启 FlowPilot 载入新的 App 程序,并完整重启 Codex 以激活新配置。"
316
+ )
317
+ }
318
+ return L(
319
+ "The new files are installed. Restart FlowPilot to load the new app binary, and fully restart Codex to activate updated FlowPilot policy and hook snapshots.",
320
+ "新文件已经安装。请重启 FlowPilot 载入新的 App 程序,并完整重启 Codex 以激活新的 FlowPilot 策略和 Hook 快照。"
321
+ )
322
+ }
323
+ if service.isFlowPilotRestartRequired {
324
+ return L(
325
+ "The updated FlowPilot binary is installed. Restart FlowPilot to load it.",
326
+ "新的 FlowPilot 程序已经安装。请重启 FlowPilot 以载入新版本。"
327
+ )
328
+ }
329
+ if let reason = service.codexRestartReason, !reason.isEmpty {
330
+ return L(
331
+ "Components updated (\(reason)). Fully restart Codex to activate updated configurations.",
332
+ "组件已更新(\(reason))。请完整重启 Codex 以激活新配置。"
333
+ )
334
+ }
335
+ return L(
336
+ "Fully restart Codex to activate updated FlowPilot policy and hook snapshots.",
337
+ "请完整重启 Codex,以激活新的 FlowPilot 策略和 Hook 快照。"
338
+ )
339
+ }
340
+
341
+ private func versionRow(title: String, value: String) -> some View {
342
+ HStack {
343
+ Text(title)
344
+ .font(.system(size: 9.5, weight: .medium))
345
+ .foregroundColor(.secondary)
346
+ Spacer()
347
+ Text(value)
348
+ .font(.system(size: 10, weight: .bold, design: .rounded))
349
+ .lineLimit(1)
350
+ }
351
+ }
352
+ }
@@ -0,0 +1,340 @@
1
+ import Cocoa
2
+ import SwiftUI
3
+ import Darwin
4
+
5
+ class AppDelegate: NSObject, NSApplicationDelegate {
6
+ let instanceLock: FlowPilotInstanceLock
7
+ var state: OverlayState!
8
+ var windowController: OverlayWindowController!
9
+ var watcher: TelemetryWatcher!
10
+ var ipcServer: IPCService.Server!
11
+
12
+ init(instanceLock: FlowPilotInstanceLock) {
13
+ self.instanceLock = instanceLock
14
+ super.init()
15
+ }
16
+
17
+ func applicationDidFinishLaunching(_ notification: Notification) {
18
+ NSApp.setActivationPolicy(.accessory)
19
+
20
+ state = OverlayState()
21
+ windowController = OverlayWindowController(state: state)
22
+ watcher = TelemetryWatcher(state: state)
23
+ ipcServer = IPCService.Server(state: state)
24
+
25
+ // The singleton lock is acquired before NSApplication starts. If the
26
+ // IPC endpoint cannot be established, terminate through the normal
27
+ // lifecycle so `applicationWillTerminate` releases the lock too.
28
+ guard ipcServer.isRunning else {
29
+ writeStandardError(L(
30
+ "FlowPilot could not start its IPC server.\n",
31
+ "FlowPilot 无法启动 IPC 服务。\n"
32
+ ))
33
+ NSApp.terminate(nil)
34
+ return
35
+ }
36
+ }
37
+
38
+ func applicationWillTerminate(_ notification: Notification) {
39
+ watcher?.stopWatching()
40
+ ipcServer?.stop()
41
+ // Release only after IPC shutdown has been requested. Keeping the
42
+ // lock through socket teardown prevents a replacement from racing
43
+ // this process's cancellation handler.
44
+ instanceLock.release()
45
+ }
46
+ }
47
+
48
+ func printUsage() {
49
+ print(L(
50
+ """
51
+ FlowPilot - Native macOS Assistant & Telemetry Widget
52
+
53
+ Usage:
54
+ FlowPilot [command] [options]
55
+
56
+ Commands:
57
+ start, --daemon Start/Restart the FlowPilot floating daemon (default)
58
+ restart Restart the running FlowPilot daemon with fresh build
59
+ stop, quit Stop running FlowPilot daemon
60
+ status [--json] Check if FlowPilot daemon is currently active
61
+ autostart [action] Login launch: status|enable|disable
62
+ toggle Toggle between circular bubble and expanded summary
63
+ expand Expand summary window
64
+ collapse Collapse to circular bubble
65
+ tab <inspector|hist|stat> Switch active tab in expanded view
66
+ show <#|id|session> Inspect specific task in FlowPilot
67
+ stats [days] Open analytics dashboard with N days (default: 30)
68
+ history, list Open history task timeline
69
+ update [path|json] Push telemetry run update and expand
70
+ help, -h Show this help message
71
+ """,
72
+ """
73
+ FlowPilot - macOS 原生助手与遥测悬浮窗
74
+
75
+ 用法:
76
+ FlowPilot [命令] [选项]
77
+
78
+ 命令:
79
+ start, --daemon 启动/重启 FlowPilot 悬浮窗(默认)
80
+ restart 使用最新构建重启 FlowPilot
81
+ stop, quit 停止 FlowPilot
82
+ status [--json] 查看 FlowPilot 是否正在运行
83
+ autostart [操作] 登录启动:status|enable|disable
84
+ toggle 在悬浮球与展开摘要之间切换
85
+ expand 展开摘要窗口
86
+ collapse 收起为悬浮球
87
+ tab <inspector|hist|stat> 切换展开视图中的标签页
88
+ show <#|id|session> 查看指定任务
89
+ stats [days] 打开 N 天统计面板(默认:30)
90
+ history, list 打开历史任务列表
91
+ update [path|json] 推送遥测任务更新并展开
92
+ help, -h 显示帮助
93
+ """
94
+ ))
95
+ }
96
+
97
+ func printAutostartStatus(_ status: FlowPilotAutostartStatus, json: Bool) {
98
+ if json {
99
+ let object: [String: Any] = [
100
+ "enabled": status.enabled,
101
+ "registered": status.plistExists,
102
+ "launchdLoaded": status.launchdLoaded,
103
+ "executable": status.executablePath
104
+ ]
105
+ if let data = try? JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]),
106
+ let text = String(data: data, encoding: .utf8) {
107
+ print(text)
108
+ }
109
+ return
110
+ }
111
+
112
+ if status.enabled {
113
+ let detail = status.launchdLoaded
114
+ ? L("launchd loaded in this login session", "当前登录会话已由 launchd 加载")
115
+ : L("registered for the next login", "已注册,将在下次登录时启动")
116
+ print(L("● Login launch enabled · \(detail)", "● 登录启动已开启 · \(detail)"))
117
+ } else {
118
+ print(L("○ Login launch disabled", "○ 登录启动已关闭"))
119
+ }
120
+ }
121
+
122
+ func printStatusFailureJSON(_ message: String) {
123
+ let object: [String: Any] = ["running": false, "error": message]
124
+ if let data = try? JSONSerialization.data(withJSONObject: object, options: [.sortedKeys]),
125
+ let text = String(data: data, encoding: .utf8) {
126
+ print(text)
127
+ }
128
+ }
129
+
130
+ func writeStandardError(_ message: String) {
131
+ guard let data = message.data(using: .utf8) else { return }
132
+ FileHandle.standardError.write(data)
133
+ }
134
+
135
+ func acquireFlowPilotInstanceLock(isExplicitRestart: Bool) -> FlowPilotInstanceLock? {
136
+ let lock: FlowPilotInstanceLock
137
+ do {
138
+ lock = try FlowPilotInstanceLock()
139
+ } catch {
140
+ writeStandardError(L(
141
+ "FlowPilot could not open its instance lock: \(error.localizedDescription)\n",
142
+ "FlowPilot 无法打开实例锁:\(error.localizedDescription)\n"
143
+ ))
144
+ return nil
145
+ }
146
+
147
+ let isLaunchAgentStart = FlowPilotInstanceLock.isLaunchAgentStart
148
+ let timeoutNanos = UInt64(FlowPilotInstanceLock.manualHandoffTimeout * 1_000_000_000)
149
+ let now = DispatchTime.now().uptimeNanoseconds
150
+ let deadline = UInt64.max - now < timeoutNanos ? UInt64.max : now + timeoutNanos
151
+ var ownsLock = false
152
+
153
+ // A starter may race either a current lock-aware build or a legacy build
154
+ // that owns only the IPC socket. Do not proceed until this process owns
155
+ // the durable lock and no previous IPC owner is still answering.
156
+ while true {
157
+ let statusCheck = IPCService.sendCommand("status")
158
+ if statusCheck.success {
159
+ if isLaunchAgentStart && !isExplicitRestart {
160
+ // RunAtLoad must never preempt a live manual/restart owner,
161
+ // including an older build that predates the instance lock.
162
+ return nil
163
+ }
164
+ // An explicit restart is an active handoff even when its caller
165
+ // inherited the launchd marker. Ask the current IPC owner to
166
+ // terminate, then keep polling until its durable lock is released.
167
+ _ = IPCService.sendCommand("quit")
168
+ } else if ownsLock {
169
+ return lock
170
+ }
171
+
172
+ if !ownsLock {
173
+ do {
174
+ try lock.acquire(timeout: 0)
175
+ ownsLock = true
176
+ } catch let error as FlowPilotInstanceLock.LockError {
177
+ switch error {
178
+ case .timedOut:
179
+ if isLaunchAgentStart && !isExplicitRestart {
180
+ // A loaded LaunchAgent may race a manual start before
181
+ // that owner has created IPC. Treat contention as a
182
+ // successful no-op because KeepAlive is disabled.
183
+ return nil
184
+ }
185
+ default:
186
+ writeStandardError(L(
187
+ "FlowPilot could not acquire its instance lock: \(error.localizedDescription)\n",
188
+ "FlowPilot 无法获取实例锁:\(error.localizedDescription)\n"
189
+ ))
190
+ return nil
191
+ }
192
+ } catch {
193
+ writeStandardError(L(
194
+ "FlowPilot could not acquire its instance lock: \(error.localizedDescription)\n",
195
+ "FlowPilot 无法获取实例锁:\(error.localizedDescription)\n"
196
+ ))
197
+ return nil
198
+ }
199
+ }
200
+
201
+ if DispatchTime.now().uptimeNanoseconds >= deadline {
202
+ writeStandardError(L(
203
+ "FlowPilot restart timed out waiting for the previous instance to exit.\n",
204
+ "FlowPilot 重启等待旧实例退出超时。\n"
205
+ ))
206
+ return nil
207
+ }
208
+
209
+ usleep(10_000)
210
+ }
211
+ }
212
+
213
+ let args = Array(CommandLine.arguments.dropFirst())
214
+
215
+ if args.isEmpty || args[0] == "start" || args[0] == "--daemon" || args[0] == "restart" {
216
+ // Detach from controlling terminal session and ignore hangup signals when running as daemon
217
+ _ = setsid()
218
+ signal(SIGHUP, SIG_IGN)
219
+
220
+ let isExplicitRestart = args.first == "restart"
221
+ guard let instanceLock = acquireFlowPilotInstanceLock(isExplicitRestart: isExplicitRestart) else {
222
+ // A LaunchAgent-created start is an expected no-op when another
223
+ // process owns the singleton lock. Explicit restarts must report a
224
+ // failed handoff instead, even if they inherited the launchd marker.
225
+ exit(FlowPilotInstanceLock.isLaunchAgentStart && !isExplicitRestart ? 0 : 1)
226
+ }
227
+
228
+ let app = NSApplication.shared
229
+ let delegate = AppDelegate(instanceLock: instanceLock)
230
+ app.delegate = delegate
231
+ app.run()
232
+ } else {
233
+ let cmd = args[0]
234
+ switch cmd {
235
+ case "status":
236
+ let wantsJSON = args.contains("--json")
237
+ let res = IPCService.sendCommand("status")
238
+ if res.success {
239
+ if wantsJSON {
240
+ print(res.response)
241
+ } else {
242
+ print(L("● FlowPilot is RUNNING: \(res.response)", "● FlowPilot 正在运行:\(res.response)"))
243
+ }
244
+ exit(0)
245
+ } else {
246
+ if wantsJSON {
247
+ printStatusFailureJSON(res.response)
248
+ } else {
249
+ print(L("○ FlowPilot is NOT running.", "○ FlowPilot 未运行。"))
250
+ }
251
+ exit(1)
252
+ }
253
+ case "autostart":
254
+ let action = args.count > 1 ? args[1].lowercased() : "status"
255
+ let wantsJSON = args.contains("--json")
256
+ do {
257
+ switch action {
258
+ case "status":
259
+ printAutostartStatus(FlowPilotAutostartService.status(), json: wantsJSON)
260
+ case "enable", "on":
261
+ let status = try FlowPilotAutostartService.enable()
262
+ printAutostartStatus(status, json: wantsJSON)
263
+ case "disable", "off":
264
+ let status = try FlowPilotAutostartService.disable()
265
+ printAutostartStatus(status, json: wantsJSON)
266
+ default:
267
+ print(L(
268
+ "Usage: FlowPilot autostart status|enable|disable [--json]",
269
+ "用法:FlowPilot autostart status|enable|disable [--json]"
270
+ ))
271
+ exit(2)
272
+ }
273
+ exit(0)
274
+ } catch {
275
+ if wantsJSON {
276
+ let object: [String: Any] = ["error": error.localizedDescription]
277
+ if let data = try? JSONSerialization.data(withJSONObject: object),
278
+ let text = String(data: data, encoding: .utf8) {
279
+ print(text)
280
+ }
281
+ } else {
282
+ print(L("Autostart update failed: \(error.localizedDescription)", "登录启动设置失败:\(error.localizedDescription)"))
283
+ }
284
+ exit(1)
285
+ }
286
+ case "toggle":
287
+ let res = IPCService.sendCommand("toggle")
288
+ print(res.response)
289
+ exit(res.success ? 0 : 1)
290
+ case "expand":
291
+ let res = IPCService.sendCommand("expand")
292
+ print(res.response)
293
+ exit(res.success ? 0 : 1)
294
+ case "collapse":
295
+ let res = IPCService.sendCommand("collapse")
296
+ print(res.response)
297
+ exit(res.success ? 0 : 1)
298
+ case "tab":
299
+ let target = args.dropFirst().joined(separator: " ")
300
+ let res = IPCService.sendCommand("tab \(target)")
301
+ print(res.response)
302
+ exit(res.success ? 0 : 1)
303
+ case "show":
304
+ let target = args.dropFirst().joined(separator: " ")
305
+ let res = IPCService.sendCommand("show \(target)")
306
+ print(res.response)
307
+ exit(res.success ? 0 : 1)
308
+ case "stats", "summary":
309
+ let target = args.dropFirst().joined(separator: " ")
310
+ let res = IPCService.sendCommand("stats \(target)")
311
+ print(res.response)
312
+ exit(res.success ? 0 : 1)
313
+ case "history", "list":
314
+ let res = IPCService.sendCommand("history")
315
+ print(res.response)
316
+ exit(res.success ? 0 : 1)
317
+ case "stop", "quit":
318
+ let res = IPCService.sendCommand("quit")
319
+ print(res.response)
320
+ exit(res.success ? 0 : 1)
321
+ case "update":
322
+ let payload = args.dropFirst().joined(separator: " ")
323
+ if payload.isEmpty {
324
+ let res = IPCService.sendCommand("update")
325
+ print(res.response)
326
+ exit(res.success ? 0 : 1)
327
+ } else {
328
+ let res = IPCService.sendCommand("update \(payload)")
329
+ print(res.response)
330
+ exit(res.success ? 0 : 1)
331
+ }
332
+ case "help", "-h", "--help":
333
+ printUsage()
334
+ exit(0)
335
+ default:
336
+ print(L("Unknown command: \(cmd)", "未知命令:\(cmd)"))
337
+ printUsage()
338
+ exit(1)
339
+ }
340
+ }