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,646 @@
1
+ #!/usr/bin/env python3
2
+ """Interactive management console for codex-flow."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ import subprocess
8
+ import sys
9
+ import unicodedata
10
+ from pathlib import Path
11
+ from typing import Any
12
+
13
+ try:
14
+ from . import telemetry
15
+ except ImportError:
16
+ import telemetry # type: ignore
17
+
18
+ try:
19
+ from localization import resolve_language, tr
20
+ except ImportError:
21
+ from scripts.localization import resolve_language, tr # type: ignore
22
+
23
+ try:
24
+ from updater import cached_status as cached_update_status, update_menu_label as updater_menu_label
25
+ except ImportError:
26
+ from scripts.updater import ( # type: ignore
27
+ cached_status as cached_update_status,
28
+ update_menu_label as updater_menu_label,
29
+ )
30
+
31
+ ROOT_DIR = Path(__file__).resolve().parents[1]
32
+ CODEX_HOME = Path(os.environ.get("CODEX_HOME", Path.home() / ".codex"))
33
+ STATE_DIR = CODEX_HOME / "codex-flow"
34
+ LANG = resolve_language(CODEX_HOME / "codex-flow.toml")
35
+
36
+
37
+ def T(en: str, zh: str) -> str:
38
+ return tr(en, zh, lang=LANG)
39
+
40
+
41
+ class ConsoleStyle:
42
+ def __init__(self) -> None:
43
+ self.enabled = (
44
+ sys.stdout.isatty()
45
+ and os.environ.get("NO_COLOR") != "1"
46
+ and os.environ.get("TERM") != "dumb"
47
+ )
48
+ if self.enabled:
49
+ self.BOLD = "\033[1m"
50
+ self.DIM = "\033[2m"
51
+ self.CYAN = "\033[36m"
52
+ self.GREEN = "\033[32m"
53
+ self.YELLOW = "\033[33m"
54
+ self.RED = "\033[31m"
55
+ self.MAGENTA = "\033[35m"
56
+ self.RESET = "\033[0m"
57
+ else:
58
+ self.BOLD = self.DIM = self.CYAN = self.GREEN = ""
59
+ self.YELLOW = self.RED = self.MAGENTA = self.RESET = ""
60
+
61
+
62
+ style = ConsoleStyle()
63
+
64
+
65
+ def display_width(s: str) -> int:
66
+ return sum(2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1 for ch in s)
67
+
68
+
69
+ def truncate_display(s: str, max_width: int) -> str:
70
+ cur = 0
71
+ res: list[str] = []
72
+ for ch in s:
73
+ width = 2 if unicodedata.east_asian_width(ch) in ("W", "F") else 1
74
+ if cur + width > max_width:
75
+ break
76
+ res.append(ch)
77
+ cur += width
78
+ return "".join(res)
79
+
80
+
81
+ def pad_display(s: str, target_width: int, align: str = "left") -> str:
82
+ pad = max(0, target_width - display_width(s))
83
+ return (" " * pad + s) if align == "right" else (s + " " * pad)
84
+
85
+
86
+ def print_banner(version: str = "1.0.0") -> None:
87
+ print(f"\n{style.DIM}╭────────────────────────────────────────────────────────────────────╮{style.RESET}")
88
+ title = T(f"🚀 codex-flow Console (v{version})", f"🚀 codex-flow 控制台 (v{version})")
89
+ pad_len = max(0, 66 - display_width(title))
90
+ left_pad = pad_len // 2
91
+ right_pad = pad_len - left_pad
92
+ print(f"{style.DIM}│{style.RESET}{' ' * left_pad}{style.BOLD}{style.CYAN}{title}{style.RESET}{' ' * right_pad}{style.DIM}│{style.RESET}")
93
+ sub = T(
94
+ "FlowPilot orchestration · deterministic telemetry · local benchmark validation",
95
+ "FlowPilot 智能编排 · 确定性任务遥测 · 本地 Benchmark 验证",
96
+ )
97
+ sub_pad = max(0, 66 - display_width(sub))
98
+ sub_l = sub_pad // 2
99
+ sub_r = sub_pad - sub_l
100
+ print(f"{style.DIM}│{style.RESET}{' ' * sub_l}{style.DIM}{sub}{style.RESET}{' ' * sub_r}{style.DIM}│{style.RESET}")
101
+ print(f"{style.DIM}╰────────────────────────────────────────────────────────────────────╯{style.RESET}\n")
102
+
103
+
104
+ def get_version() -> str:
105
+ for ver_file in (ROOT_DIR / "VERSION", STATE_DIR / "version"):
106
+ if ver_file.exists():
107
+ try:
108
+ return ver_file.read_text(encoding="utf-8").strip()
109
+ except OSError:
110
+ pass
111
+ return "1.0.0"
112
+
113
+
114
+ def pause_prompt(prompt: str | None = None) -> None:
115
+ prompt = prompt or T("Press Enter to return...", "按 Enter 键返回...")
116
+ try:
117
+ input(f"\n{style.DIM}{prompt}{style.RESET}")
118
+ except (KeyboardInterrupt, EOFError):
119
+ pass
120
+
121
+
122
+ def handle_show_last() -> None:
123
+ print(f"\n{style.BOLD}{T('📊 Latest task telemetry summary:', '📊 最新任务遥测摘要:')}{style.RESET}")
124
+ ret = telemetry.show_last(as_json=False)
125
+ if ret == 0:
126
+ pause_prompt()
127
+
128
+
129
+ def select_project_interactive(
130
+ title: str | None = None,
131
+ allow_all: bool = True,
132
+ all_label: str | None = None,
133
+ limit: int = 10,
134
+ ) -> str | None | False:
135
+ title = title or T("Select a project to analyze", "请选择要统计的项目")
136
+ all_label = all_label or T("All projects (full statistics)", "全部项目 (全量统计)")
137
+ try:
138
+ all_stats = telemetry.aggregate_project_stats(days=30)
139
+ except Exception:
140
+ all_stats = {}
141
+
142
+ projects = all_stats.get("projects") or {}
143
+ total_count = len(projects)
144
+ sorted_projs = sorted(
145
+ projects.items(),
146
+ key=lambda item: (item[1].get("tokens", 0), item[1].get("runs", 0)),
147
+ reverse=True,
148
+ )
149
+ if limit and len(sorted_projs) > limit:
150
+ sorted_projs = sorted_projs[:limit]
151
+
152
+ if not sorted_projs:
153
+ print(f"\n{style.DIM}{T('📁 No project history yet', '📁 暂无历史项目记录')}{style.RESET}")
154
+ try:
155
+ prompt = T("Enter project name (Enter for all): ", "请输入项目名称 (直接回车统计全量): ")
156
+ p_in = input(f"{style.CYAN}{prompt}{style.RESET}").strip()
157
+ return p_in if p_in else None
158
+ except (KeyboardInterrupt, EOFError):
159
+ return False
160
+
161
+ title_suffix = T(f" (top {len(sorted_projs)})", f" (前 {len(sorted_projs)} 个)") if total_count > len(sorted_projs) else ""
162
+ print(f"\n{style.BOLD}📁 {title}{title_suffix}:{style.RESET}")
163
+ if allow_all:
164
+ default = T("(Enter by default)", "(直接回车默认)")
165
+ print(f" [{style.CYAN}0{style.RESET}] 🌐 {all_label} {style.DIM}{default}{style.RESET}")
166
+ for idx, (p_name, p_info) in enumerate(sorted_projs, 1):
167
+ runs_cnt = p_info.get("runs", 0)
168
+ tok_cnt = telemetry.fmt_tokens(p_info.get("tokens", 0)) or "0"
169
+ unit = T("tasks", "个任务")
170
+ print(f" [{style.CYAN}{idx}{style.RESET}] {p_name} {style.DIM}({runs_cnt} {unit} · {tok_cnt} tokens){style.RESET}")
171
+ if total_count > len(sorted_projs):
172
+ text = T(f"✍️ Enter another project name ({total_count} total)", f"✍️ 手动输入其他项目名称 (共 {total_count} 个项目)")
173
+ else:
174
+ text = T("✍️ Enter project name manually", "✍️ 手动输入项目名称")
175
+ print(f" [{style.CYAN}m{style.RESET}] {text}")
176
+ print(f" [{style.CYAN}q{style.RESET}] 🔙 {T('Back', '返回')}")
177
+
178
+ try:
179
+ prompt_hint = f"0-{len(sorted_projs)}/m/q" if allow_all else f"1-{len(sorted_projs)}/m/q"
180
+ choice = input(f"\n{style.CYAN}{T('Select', '请选择')} [{prompt_hint}]: {style.RESET}").strip()
181
+ except (KeyboardInterrupt, EOFError):
182
+ return False
183
+
184
+ if choice == "":
185
+ return None if allow_all else False
186
+ if choice.lower() in ("0", "all") and allow_all:
187
+ return None
188
+ if choice.lower() in ("q", "back", "exit"):
189
+ return False
190
+ if choice.lower() in ("m", "manual"):
191
+ try:
192
+ prompt = T("Enter project name (Enter for all): ", "请输入项目名称 (直接回车为全量): ")
193
+ p_in = input(f"{style.CYAN}{prompt}{style.RESET}").strip()
194
+ return p_in if p_in else None
195
+ except (KeyboardInterrupt, EOFError):
196
+ return False
197
+ if choice.isdigit():
198
+ idx = int(choice)
199
+ if idx == 0 and allow_all:
200
+ return None
201
+ if 1 <= idx <= len(sorted_projs):
202
+ return sorted_projs[idx - 1][0]
203
+ print(f"{style.RED}❌ {T(f'Index out of range [1-{len(sorted_projs)}]', f'序号超出范围 [1-{len(sorted_projs)}]')}{style.RESET}")
204
+ pause_prompt()
205
+ return False
206
+ return choice
207
+
208
+
209
+ def handle_show_history() -> None:
210
+ limit = 10
211
+ project: str | None = None
212
+ today = False
213
+ while True:
214
+ runs = telemetry.list_runs(limit=limit, project=project, today=today)
215
+ print(telemetry.render_run_list(runs, project=project, today=today))
216
+ print(f"{style.BOLD}{T('Quick actions:', '快捷操作:')}{style.RESET}")
217
+ if runs:
218
+ print(f" [{style.CYAN}1-{len(runs)}{style.RESET}] {T('View task card details', '查看对应序号的任务卡片详情')}")
219
+ current_project = project or T("All", "全部")
220
+ print(f" [{style.CYAN}p{style.RESET}] {T('Filter by project', '按项目名称过滤')} ({T('current', '当前')}: {current_project})")
221
+ print(f" [{style.CYAN}t{style.RESET}] {T('Toggle today only', '切换仅看今天')} ({T('current', '当前')}: {T('yes', '是') if today else T('no', '否')})")
222
+ print(f" [{style.CYAN}s{style.RESET}] {T('Project aggregate stats', '查看项目聚合统计')} (Stats)")
223
+ print(f" [{style.CYAN}x{style.RESET}] {T('Repair history telemetry', '回填修复历史遥测')} (Repair)")
224
+ print(f" [{style.CYAN}r{style.RESET}] {T('Refresh', '刷新列表')}")
225
+ print(f" [{style.CYAN}0{style.RESET}] {T('Back to main menu', '返回主菜单')}")
226
+ try:
227
+ choice = input(f"\n{style.CYAN}{T('Enter option', '请输入选项')}: {style.RESET}").strip()
228
+ except (KeyboardInterrupt, EOFError):
229
+ break
230
+ if choice in ("0", "q", "exit", "back"):
231
+ break
232
+ if choice == "r":
233
+ continue
234
+ if choice.lower() in ("x", "repair"):
235
+ handle_repair_history()
236
+ continue
237
+ if choice == "t":
238
+ today = not today
239
+ continue
240
+ if choice == "p":
241
+ selected = select_project_interactive(
242
+ title=T("Select a project to filter", "请选择要过滤的项目"),
243
+ allow_all=True,
244
+ all_label=T("All projects (clear filter)", "全部项目 (清除过滤)"),
245
+ )
246
+ if selected is not False:
247
+ project = selected
248
+ continue
249
+ if choice == "s":
250
+ handle_show_stats(project)
251
+ continue
252
+ if choice.isdigit():
253
+ idx = int(choice)
254
+ if 1 <= idx <= len(runs):
255
+ selected = runs[idx - 1]
256
+ telemetry.enrich_run_metadata(selected)
257
+ print(telemetry.render_summary(selected))
258
+ pause_prompt(T("Press Enter to return to history...", "按 Enter 键返回历史列表..."))
259
+ else:
260
+ print(f"{style.RED}❌ {T(f'Index out of range [1-{len(runs)}]', f'序号超出范围 [1-{len(runs)}]')}{style.RESET}")
261
+ pause_prompt()
262
+ else:
263
+ print(f"{style.RED}❌ {T('Invalid option', '无效选项')}: {choice}{style.RESET}")
264
+
265
+
266
+ def handle_show_stats(default_project: str | None = None) -> None:
267
+ target_project = default_project
268
+ if target_project is None:
269
+ selected = select_project_interactive()
270
+ if selected is False:
271
+ return
272
+ target_project = selected
273
+ stats = telemetry.aggregate_project_stats(project=target_project, days=30)
274
+ print(telemetry.render_project_stats(stats))
275
+ pause_prompt()
276
+
277
+
278
+ def handle_show_status() -> None:
279
+ print(f"\n{style.BOLD}{T('🎯 Effective FlowPilot policy:', '🎯 当前 FlowPilot 策略状态:')}{style.RESET}\n")
280
+ if os.name != "nt":
281
+ bin_script = ROOT_DIR / "bin" / "codex-flow"
282
+ subprocess.run(["bash", str(bin_script), "status"] if bin_script.exists() else ["codex-flow", "status"])
283
+ else:
284
+ subprocess.run(["codex-flow.cmd", "status"], shell=True)
285
+ pause_prompt()
286
+
287
+
288
+ def handle_run_doctor() -> None:
289
+ print(f"\n{style.BOLD}{T('🩺 Running diagnostics...', '🩺 正在运行系统诊断检查...')}{style.RESET}\n")
290
+ if os.name != "nt":
291
+ doc_script = ROOT_DIR / "scripts" / "doctor"
292
+ subprocess.run(["bash", str(doc_script)] if doc_script.exists() else ["codex-flow", "doctor"])
293
+ else:
294
+ subprocess.run(["codex-flow.cmd", "doctor"], shell=True)
295
+ pause_prompt()
296
+
297
+
298
+ def handle_run_benchmark() -> None:
299
+ print(f"\n{style.BOLD}{T('⚡ Local quick benchmark:', '⚡ 本地快速 Benchmark 评测:')}{style.RESET}")
300
+ print(style.DIM + T(
301
+ "Runs 6 representative tasks with the locally authenticated Codex to evaluate routing and model behavior.",
302
+ "将调用本地登录的 Codex 执行 6 个典型任务以测量当前模型与编排效果。",
303
+ ) + style.RESET)
304
+ try:
305
+ confirm = input(f"{style.YELLOW}{T('Start now? [y/N]: ', '确定开始执行吗?[y/N]: ')}{style.RESET}").strip().lower()
306
+ except (KeyboardInterrupt, EOFError):
307
+ return
308
+ if confirm in ("y", "yes", "是"):
309
+ bench_script = ROOT_DIR / "scripts" / "benchmark-local.py"
310
+ subprocess.run([sys.executable, str(bench_script), "quick"] if bench_script.exists() else ["codex-flow", "benchmark-local", "quick"])
311
+ pause_prompt()
312
+
313
+
314
+ def handle_update() -> None:
315
+ print(f"\n{style.BOLD}{T('🔄 Checking for codex-flow updates...', '🔄 正在检查并更新 codex-flow...')}{style.RESET}\n")
316
+ if os.name != "nt":
317
+ bin_script = ROOT_DIR / "bin" / "codex-flow"
318
+ subprocess.run(["bash", str(bin_script), "update"] if bin_script.exists() else ["codex-flow", "update"])
319
+ else:
320
+ subprocess.run(["codex-flow.cmd", "update"], shell=True)
321
+ pause_prompt()
322
+
323
+
324
+ def get_source_dir() -> Path:
325
+ source_file = STATE_DIR / "source"
326
+ if source_file.exists():
327
+ try:
328
+ p = Path(source_file.read_text(encoding="utf-8").strip())
329
+ if p.exists():
330
+ return p
331
+ except OSError:
332
+ pass
333
+ return ROOT_DIR
334
+
335
+
336
+ def get_overlay_bin() -> Path | None:
337
+ src = get_source_dir()
338
+ candidates = [
339
+ STATE_DIR / "bin" / "FlowPilot",
340
+ STATE_DIR / "bin" / "codex-flow-overlay",
341
+ src / "apps" / "macos-overlay" / "bin" / "FlowPilot",
342
+ src / "apps" / "macos-overlay" / "bin" / "codex-flow-overlay",
343
+ ]
344
+ return next((c for c in candidates if c.exists() and os.access(str(c), os.X_OK)), None)
345
+
346
+
347
+ def launch_overlay_daemon(overlay_bin: Path) -> None:
348
+ kwargs = {
349
+ "stdin": subprocess.DEVNULL,
350
+ "stdout": subprocess.DEVNULL,
351
+ "stderr": subprocess.DEVNULL,
352
+ }
353
+ if sys.platform != "win32":
354
+ kwargs["start_new_session"] = True
355
+ subprocess.Popen([str(overlay_bin), "start"], **kwargs)
356
+
357
+
358
+ def run_overlay_build() -> bool:
359
+ src = get_source_dir()
360
+ build_script = src / "apps" / "macos-overlay" / "build.sh"
361
+ if not build_script.exists():
362
+ print(f"\n{style.RED}❌ {T('Build script not found', '未找到构建脚本')}: {build_script}{style.RESET}")
363
+ return False
364
+ print(f"\n{style.BOLD}{T('🔨 Building FlowPilot native macOS overlay...', '🔨 正在编译 FlowPilot macOS 原生悬浮窗...')}{style.RESET}")
365
+ print(f"{style.DIM}{T('(Native SwiftUI compilation, typically takes ~30-50s)', '(基于 SwiftUI 原生编译,通常耗时约 30-50 秒)')}{style.RESET}\n")
366
+ try:
367
+ res = subprocess.run(["bash", str(build_script)])
368
+ return res.returncode == 0
369
+ except Exception as exc:
370
+ print(f"{style.RED}❌ {T('Build execution error', '执行构建失败')}: {exc}{style.RESET}")
371
+ return False
372
+
373
+
374
+ def run_showcase_generation() -> bool:
375
+ src = get_source_dir()
376
+ sources_dir = src / "apps" / "macos-overlay" / "Sources"
377
+ showcase_script = src / "scripts" / "generate_showcase.swift"
378
+ output_bin = src / "bin" / "generate_showcase"
379
+
380
+ if not sources_dir.exists():
381
+ print(f"\n{style.RED}❌ {T('Overlay source directory not found', '未找到浮窗源码目录')}: {sources_dir}{style.RESET}")
382
+ return False
383
+ if not showcase_script.exists():
384
+ print(f"\n{style.RED}❌ {T('Showcase generator not found', '未找到 Showcase 生成脚本')}: {showcase_script}{style.RESET}")
385
+ return False
386
+
387
+ swift_files = sorted(
388
+ p for p in sources_dir.rglob("*.swift")
389
+ if p.name != "main.swift"
390
+ )
391
+ if not swift_files:
392
+ print(f"\n{style.RED}❌ {T('No Swift source files found for showcase generation.', '未找到可用于生成 Showcase 的 Swift 源码。')}{style.RESET}")
393
+ return False
394
+
395
+ output_bin.parent.mkdir(parents=True, exist_ok=True)
396
+ compile_cmd = [
397
+ "swiftc",
398
+ "-framework", "Cocoa",
399
+ "-framework", "SwiftUI",
400
+ "-framework", "Combine",
401
+ *[str(path) for path in swift_files],
402
+ str(showcase_script),
403
+ "-o", str(output_bin),
404
+ ]
405
+
406
+ print(f"\n{style.BOLD}{T('🖼 Rebuilding showcase and promotional images...', '🖼 正在重新生成 Showcase / 宣传图...')}{style.RESET}")
407
+ print(f"{style.DIM}{T('Compiling the native SwiftUI showcase renderer first.', '先编译原生 SwiftUI Showcase 渲染器。')}{style.RESET}\n")
408
+ try:
409
+ compile_res = subprocess.run(compile_cmd, cwd=src)
410
+ if compile_res.returncode != 0:
411
+ print(f"\n{style.RED}❌ {T('Showcase renderer build failed. Please check the logs above.', 'Showcase 渲染器编译失败,请检查上方日志。')}{style.RESET}")
412
+ return False
413
+
414
+ generate_res = subprocess.run([str(output_bin)], cwd=src)
415
+ if generate_res.returncode != 0:
416
+ print(f"\n{style.RED}❌ {T('Showcase generation failed. Please check the logs above.', 'Showcase 图片生成失败,请检查上方日志。')}{style.RESET}")
417
+ return False
418
+ except FileNotFoundError as exc:
419
+ print(f"\n{style.RED}❌ {T('Required command not found', '缺少必要命令')}: {exc}{style.RESET}")
420
+ return False
421
+ except Exception as exc:
422
+ print(f"\n{style.RED}❌ {T('Showcase generation error', '执行 Showcase 生成失败')}: {exc}{style.RESET}")
423
+ return False
424
+
425
+ print(f"\n{style.GREEN}✨ {T('Showcase generation completed.', 'Showcase / 宣传图生成完成。')}{style.RESET}")
426
+ print(f"{style.DIM}{T('Updated:', '已更新:')} docs/assets/screenshots/ · docs/assets/promo/{style.RESET}")
427
+ return True
428
+
429
+
430
+ def handle_generate_showcase() -> None:
431
+ run_showcase_generation()
432
+ pause_prompt()
433
+
434
+
435
+ def handle_manage_overlay() -> None:
436
+ if sys.platform != "darwin":
437
+ print(f"\n{style.YELLOW}⚠️ {T('The native FlowPilot floating window is available only on macOS.', 'FlowPilot 原生悬浮窗仅支持 macOS 系统。')}{style.RESET}")
438
+ pause_prompt()
439
+ return
440
+
441
+ while True:
442
+ overlay_bin = get_overlay_bin()
443
+ has_bin = overlay_bin is not None and overlay_bin.exists() and os.access(str(overlay_bin), os.X_OK)
444
+
445
+ is_running = False
446
+ if has_bin:
447
+ try:
448
+ status_res = subprocess.run([str(overlay_bin), "status"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
449
+ is_running = status_res.returncode == 0
450
+ except Exception:
451
+ is_running = False
452
+
453
+ print(f"\n{style.BOLD}🪟 {T('macOS Native Floating Widget:', 'macOS 原生悬浮窗管理 (Native Floating Widget):')}{style.RESET}")
454
+ if not has_bin:
455
+ print(f" {T('Status', '当前状态')}: {style.YELLOW}⚠️ {T('Not built yet (binary not found)', '尚未编译构建 (未找到可执行文件)')}{style.RESET}")
456
+ elif is_running:
457
+ print(f" {T('Status', '当前状态')}: {style.GREEN}● {T('running (hover 0.4s to expand Summary)', '正在运行 (Hover 0.4s 展开 Summary)')}{style.RESET}")
458
+ else:
459
+ print(f" {T('Status', '当前状态')}: {style.DIM}○ {T('stopped (ready to start)', '未运行 (就绪)')}{style.RESET}")
460
+
461
+ print(f"\n [{style.CYAN}1{style.RESET}] {T('⏹ Stop widget', '⏹ 停止浮窗') if is_running else T('🚀 Start widget', '🚀 启动浮窗')}")
462
+ if is_running:
463
+ print(f" [{style.CYAN}2{style.RESET}] {T('🔨 Rebuild and restart widget', '🔨 重新编译并重启浮窗')}")
464
+ else:
465
+ print(f" [{style.CYAN}2{style.RESET}] {T('🔨 Build and start widget', '🔨 编译并启动浮窗')}")
466
+ print(f" [{style.CYAN}3{style.RESET}] {T('📦 Build widget only', '📦 编译浮窗 (仅编译构建)')}")
467
+
468
+ if is_running:
469
+ print(f" [{style.CYAN}4{style.RESET}] {T('🔄 Toggle expanded / collapsed', '🔄 切换展开 / 折叠 (Toggle)')}")
470
+ print(f" [{style.CYAN}5{style.RESET}] {T('⚡ Push latest data and expand', '⚡️ 发送最新数据并展开 (Push Last Summary)')}")
471
+
472
+ print(f" [{style.CYAN}0{style.RESET}] 🔙 {T('Back to main menu', '返回主菜单')}\n")
473
+
474
+ try:
475
+ choice = input(f"{style.CYAN}{T('Select action', '请选择操作')}: {style.RESET}").strip()
476
+ except (KeyboardInterrupt, EOFError):
477
+ break
478
+
479
+ if choice in ("0", "q", "exit", "back"):
480
+ break
481
+
482
+ if choice == "1":
483
+ if is_running:
484
+ try:
485
+ subprocess.run([str(overlay_bin), "stop"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
486
+ subprocess.run(["pkill", "-f", "FlowPilot.*start|codex-flow-overlay.*start|bin/FlowPilot"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
487
+ except Exception:
488
+ pass
489
+ print(f"{style.YELLOW}{T('Widget stopped.', '浮窗已停止。')}{style.RESET}")
490
+ pause_prompt()
491
+ else:
492
+ if not has_bin:
493
+ print(f"\n{style.YELLOW}💡 {T('The floating window has not been built yet.', 'FlowPilot 原生悬浮窗尚未编译。')}{style.RESET}")
494
+ try:
495
+ ask_build = input(f"{style.CYAN}{T('Would you like to build and start it now? [Y/n]: ', '是否立即编译并启动浮窗?[Y/n]: ')}{style.RESET}").strip().lower()
496
+ except (KeyboardInterrupt, EOFError):
497
+ ask_build = "n"
498
+ if ask_build in ("", "y", "yes", "是"):
499
+ if run_overlay_build():
500
+ overlay_bin = get_overlay_bin()
501
+ if overlay_bin and overlay_bin.exists():
502
+ launch_overlay_daemon(overlay_bin)
503
+ print(f"{style.GREEN}{T('Widget started. It appears near the top-right and expands after a 0.4s hover.', '浮窗已启动!默认在屏幕右上角圆形悬浮,光标停留 0.4 秒展开。')}{style.RESET}")
504
+ else:
505
+ print(f"{style.RED}{T('Binary not found after build.', '编译成功但未找到可执行文件。')}{style.RESET}")
506
+ else:
507
+ print(f"{style.RED}{T('Build failed. Please check the logs above.', '编译失败,请检查上方构建日志。')}{style.RESET}")
508
+ pause_prompt()
509
+ else:
510
+ try:
511
+ launch_overlay_daemon(overlay_bin)
512
+ print(f"{style.GREEN}{T('Widget started. It appears near the top-right and expands after a 0.4s hover.', '浮窗已启动!默认在屏幕右上角圆形悬浮,光标停留 0.4 秒展开。')}{style.RESET}")
513
+ except Exception as exc:
514
+ print(f"{style.RED}{T('Start failed', '启动失败')}: {exc}{style.RESET}")
515
+ pause_prompt()
516
+
517
+ elif choice == "2":
518
+ if is_running and overlay_bin:
519
+ try:
520
+ subprocess.run([str(overlay_bin), "stop"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
521
+ subprocess.run(["pkill", "-f", "FlowPilot.*start|codex-flow-overlay.*start|bin/FlowPilot"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
522
+ except Exception:
523
+ pass
524
+ if run_overlay_build():
525
+ overlay_bin = get_overlay_bin()
526
+ if overlay_bin and overlay_bin.exists():
527
+ try:
528
+ launch_overlay_daemon(overlay_bin)
529
+ print(f"\n{style.GREEN}✨ {T('Build succeeded and widget started! It appears near the top-right.', '✨ 编译完成并已成功启动浮窗!默认在屏幕右上角圆形悬浮,光标停留 0.4 秒展开。')}{style.RESET}")
530
+ except Exception as exc:
531
+ print(f"\n{style.RED}{T('Start failed', '启动失败')}: {exc}{style.RESET}")
532
+ else:
533
+ print(f"\n{style.RED}{T('Binary not found after build.', '编译成功但未找到可执行文件。')}{style.RESET}")
534
+ else:
535
+ print(f"\n{style.RED}{T('Build failed. Please check the logs above.', '编译失败,请检查上方构建日志。')}{style.RESET}")
536
+ pause_prompt()
537
+
538
+ elif choice == "3":
539
+ if is_running and overlay_bin:
540
+ print(f"{style.DIM}{T('Stopping running widget before rebuild...', '重新编译前正在停止运行中的浮窗...')}{style.RESET}")
541
+ try:
542
+ subprocess.run([str(overlay_bin), "stop"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
543
+ subprocess.run(["pkill", "-f", "FlowPilot.*start|codex-flow-overlay.*start|bin/FlowPilot"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
544
+ except Exception:
545
+ pass
546
+ if run_overlay_build():
547
+ print(f"\n{style.GREEN}✨ {T('Widget build succeeded! You can now select [1] to start it.', '✨ 浮窗编译构建成功!可选择 [1] 启动浮窗。')}{style.RESET}")
548
+ else:
549
+ print(f"\n{style.RED}❌ {T('Build failed. Please check the logs above.', '编译失败,请检查上方构建日志。')}{style.RESET}")
550
+ pause_prompt()
551
+
552
+ elif choice == "4" and is_running:
553
+ try:
554
+ subprocess.run([str(overlay_bin), "toggle"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
555
+ except Exception:
556
+ pass
557
+
558
+ elif choice == "5" and is_running:
559
+ try:
560
+ subprocess.run([str(overlay_bin), "update"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
561
+ print(f"{style.GREEN}{T('Latest Summary pushed and expanded.', '已向浮窗推送最新 Summary 并展开。')}{style.RESET}")
562
+ except Exception as exc:
563
+ print(f"{style.RED}{T('Push failed', '推送失败')}: {exc}{style.RESET}")
564
+ pause_prompt()
565
+
566
+ else:
567
+ print(f"{style.RED}❌ {T('Invalid option', '无效选项')}{style.RESET}")
568
+ pause_prompt()
569
+
570
+
571
+ def handle_repair_history() -> None:
572
+ print(f"\n{style.BOLD}🛠️ {T('Telemetry History Repair & Backfill', '历史遥测数据修复与回填')}:{style.RESET}")
573
+ print(f" [{style.CYAN}1{style.RESET}] 🔍 {T('Dry-Run (scan and stats without modifying files)', '演练模式 (仅扫描统计,不修改任何文件)')}")
574
+ print(f" [{style.CYAN}2{style.RESET}] ✍️ {T('Repair (backfill missing fields and write atomically)', '正式回填 (补齐缺失字段并原子写入)')}")
575
+ print(f" [{style.CYAN}0{style.RESET}] 🔙 {T('Back to main menu', '返回主菜单')}")
576
+ try:
577
+ choice = input(f"\n{style.CYAN}{T('Select mode [1-2/0]', '请选择模式 [1-2/0]')}: {style.RESET}").strip()
578
+ except (KeyboardInterrupt, EOFError):
579
+ return
580
+ if choice in ("0", "q", "exit", "back", ""):
581
+ return
582
+ if choice == "1":
583
+ print()
584
+ telemetry.repair_history(dry_run=True, verbose=True)
585
+ pause_prompt()
586
+ elif choice == "2":
587
+ print()
588
+ telemetry.repair_history(dry_run=False, verbose=True)
589
+ pause_prompt()
590
+ else:
591
+ print(f"{style.RED}❌ {T('Invalid option', '无效选项')}{style.RESET}")
592
+ pause_prompt()
593
+
594
+
595
+ def main_menu() -> int:
596
+ version = get_version()
597
+ while True:
598
+ print_banner(version)
599
+ update_state = cached_update_status(trigger_background=True)
600
+ update_label = updater_menu_label(LANG, update_state)
601
+ items = [
602
+ ("1", T("🪟 macOS native floating widget", "🪟 macOS 原生悬浮窗"), "overlay widget"),
603
+ ("2", T("📊 Latest task card", "📊 查看最新任务卡片"), "usage last"),
604
+ ("3", T("📜 Task history", "📜 浏览历史任务列表"), "usage list"),
605
+ ("4", T("📈 Project aggregate statistics", "📈 项目聚合统计分析"), "usage stats"),
606
+ ("5", T("🎯 Effective policy", "🎯 查看生效策略配置"), "status"),
607
+ ("6", T("🩺 Diagnostics", "🩺 运行系统诊断检查"), "doctor"),
608
+ ("7", T("⚡ Local quick Benchmark", "⚡ 本地快速 Benchmark"), "benchmark-local quick"),
609
+ ("8", update_label, "update"),
610
+ ("9", T("🛠️ Repair telemetry history", "🛠️ 修复历史遥测数据"), "telemetry repair"),
611
+ ("10", T("🖼 Regenerate Showcase / promotional images", "🖼 重新生成 Showcase / 宣传图"), "showcase"),
612
+ ]
613
+ for key, label, command in items:
614
+ print(f" [{style.CYAN}{key}{style.RESET}] {label} {style.DIM}({command}){style.RESET}")
615
+ print(f" [{style.CYAN}0{style.RESET}] 🚪 {T('Exit', '退出')}\n")
616
+ try:
617
+ choice = input(f"{style.CYAN}{T('Enter option [0-10]', '请输入选项 [0-10]')}: {style.RESET}").strip()
618
+ except (KeyboardInterrupt, EOFError):
619
+ print("\n" + T("👋 Exited codex-flow console.", "👋 已退出 codex-flow 控制台。"))
620
+ return 0
621
+ if choice in ("0", "q", "exit"):
622
+ print(T("👋 Exited codex-flow console.", "👋 已退出 codex-flow 控制台。"))
623
+ return 0
624
+ handlers = {
625
+ "1": handle_manage_overlay,
626
+ "2": handle_show_last,
627
+ "3": handle_show_history,
628
+ "4": handle_show_stats,
629
+ "5": handle_show_status,
630
+ "6": handle_run_doctor,
631
+ "7": handle_run_benchmark,
632
+ "8": handle_update,
633
+ "9": handle_repair_history,
634
+ "10": handle_generate_showcase,
635
+ }
636
+ handler = handlers.get(choice)
637
+ if handler:
638
+ handler()
639
+ else:
640
+ print(f"{style.RED}❌ {T('Invalid option; enter 0-10', '无效选项,请输入 0-10')}{style.RESET}")
641
+ pause_prompt()
642
+ return 0
643
+
644
+
645
+ if __name__ == "__main__":
646
+ raise SystemExit(main_menu())