@oneciel-ai/ciel-runtime 0.1.0 → 0.1.1
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.
- package/ciel-runtime-menu.py +204 -22
- package/ciel-runtime-tool-guard.py +29 -24
- package/ciel_runtime.py +6023 -2515
- package/ciel_runtime_support/agent_router.py +97 -0
- package/ciel_runtime_support/agy_cli.py +248 -0
- package/ciel_runtime_support/claude_router.py +479 -0
- package/ciel_runtime_support/codex_app_server.py +430 -0
- package/ciel_runtime_support/codex_cli.py +303 -0
- package/ciel_runtime_support/codex_router.py +80 -0
- package/docs/AGY-CLI-Research.md +62 -0
- package/docs/Architecture.md +23 -0
- package/docs/CLI-Reference.md +1 -1
- package/docs/Home.md +1 -1
- package/docs/Installation.md +9 -5
- package/docs/MCP-Channels.md +2 -4
- package/docs/Module-Map.md +29 -1
- package/docs/Providers.md +3 -2
- package/docs/Router.md +1 -1
- package/docs/Test-Suite.md +1 -0
- package/package.json +2 -3
- package/npm-bin/postinstall.js +0 -46
package/ciel-runtime-menu.py
CHANGED
|
@@ -207,14 +207,19 @@ CTL = str(Path.home() / ".local/bin/ciel-runtimectl")
|
|
|
207
207
|
CONFIG = Path.home() / ".config/ciel-runtime/config.json"
|
|
208
208
|
NCP_ENV = Path.home() / ".config/nvd-claude-proxy/.env"
|
|
209
209
|
PROVIDERS = [
|
|
210
|
-
("
|
|
211
|
-
("
|
|
212
|
-
("
|
|
210
|
+
("agy:native", "AGY"),
|
|
211
|
+
("agy:routed", "AGY Routed"),
|
|
212
|
+
("anthropic:routed", "Anthropic routed"),
|
|
213
|
+
("anthropic:native", "Claude Native"),
|
|
214
|
+
("codex:native", "Codex Native"),
|
|
215
|
+
("codex:routed", "Codex routed"),
|
|
213
216
|
("deepseek", "DeepSeek.com"),
|
|
214
|
-
("vllm", "vLLM"),
|
|
215
217
|
("lm-studio", "LM Studio"),
|
|
216
218
|
("nvidia-hosted", "Nvidia Hosted"),
|
|
219
|
+
("ollama", "Ollama"),
|
|
220
|
+
("ollama-cloud", "Ollama Cloud"),
|
|
217
221
|
("self-hosted-nim", "Self Hosted NIM"),
|
|
222
|
+
("vllm", "vLLM"),
|
|
218
223
|
]
|
|
219
224
|
APP_NAME = "Ciel Runtime"
|
|
220
225
|
|
|
@@ -233,6 +238,10 @@ def app_title() -> str:
|
|
|
233
238
|
|
|
234
239
|
|
|
235
240
|
CREDITS = "Credits: One Ciel LLC"
|
|
241
|
+
PRELAUNCH_CANCEL = 10
|
|
242
|
+
PRELAUNCH_LAUNCH_CODEX = 11
|
|
243
|
+
PRELAUNCH_LAUNCH_CLAUDE = 12
|
|
244
|
+
PRELAUNCH_LAUNCH_AGY = 13
|
|
236
245
|
LANGUAGES = {
|
|
237
246
|
"en": "English",
|
|
238
247
|
"ko": "한국어",
|
|
@@ -251,6 +260,8 @@ UI_TEXT = {
|
|
|
251
260
|
"provider_options": "Provider options",
|
|
252
261
|
"test": "Test compatibility",
|
|
253
262
|
"launch": "Launch Claude Code",
|
|
263
|
+
"launch_agy": "Launch AGY",
|
|
264
|
+
"launch_codex": "Launch Codex",
|
|
254
265
|
"quit": "Quit",
|
|
255
266
|
"title": "ciel-runtime pre-launch",
|
|
256
267
|
"select_language": "Enter selects language. Up/Down moves inside submenu. Esc closes submenu.",
|
|
@@ -261,6 +272,8 @@ UI_TEXT = {
|
|
|
261
272
|
"select_provider_options": "Enter applies this provider option. Custom input accepts KEY=VALUE or unset:KEY.",
|
|
262
273
|
"test_result": "Compatibility result is shown inline. Esc closes the result. Enter runs the test again.",
|
|
263
274
|
"help_launch": "Enter launches Claude Code with the selected provider and model.",
|
|
275
|
+
"help_launch_agy": "Enter launches AGY with the selected provider mode.",
|
|
276
|
+
"help_launch_codex": "Enter launches Codex with the selected provider mode.",
|
|
264
277
|
"help_test": "Enter tests current provider/model with a minimal Claude Code tool request.",
|
|
265
278
|
"help_language": "Enter expands language submenu inline.",
|
|
266
279
|
"help_provider": "Enter expands provider submenu inline.",
|
|
@@ -288,6 +301,8 @@ UI_TEXT = {
|
|
|
288
301
|
"provider_options": "프로바이더 옵션",
|
|
289
302
|
"test": "호환성 테스트",
|
|
290
303
|
"launch": "Claude Code 실행",
|
|
304
|
+
"launch_agy": "AGY 실행",
|
|
305
|
+
"launch_codex": "Codex 실행",
|
|
291
306
|
"quit": "종료",
|
|
292
307
|
"title": "ciel-runtime 실행 전 설정",
|
|
293
308
|
"select_language": "Enter로 언어를 선택합니다. 위/아래로 이동, Esc로 닫기.",
|
|
@@ -298,6 +313,8 @@ UI_TEXT = {
|
|
|
298
313
|
"select_provider_options": "Enter로 프로바이더 옵션을 적용합니다. 직접 입력은 KEY=VALUE 또는 unset:KEY를 받습니다.",
|
|
299
314
|
"test_result": "호환성 결과가 메뉴 안에 표시됩니다. Esc로 닫고 Enter로 다시 테스트합니다.",
|
|
300
315
|
"help_launch": "선택한 프로바이더와 모델로 Claude Code를 실행합니다.",
|
|
316
|
+
"help_launch_agy": "선택한 프로바이더 모드로 AGY를 실행합니다.",
|
|
317
|
+
"help_launch_codex": "선택한 프로바이더 모드로 Codex를 실행합니다.",
|
|
301
318
|
"help_test": "현재 프로바이더/모델에 최소 Claude Code 도구 요청을 보내 호환성을 확인합니다.",
|
|
302
319
|
"help_language": "언어 선택 메뉴를 펼칩니다.",
|
|
303
320
|
"help_provider": "프로바이더 선택 메뉴를 펼칩니다.",
|
|
@@ -325,6 +342,8 @@ UI_TEXT = {
|
|
|
325
342
|
"provider_options": "プロバイダーオプション",
|
|
326
343
|
"test": "互換性テスト",
|
|
327
344
|
"launch": "Claude Codeを起動",
|
|
345
|
+
"launch_agy": "AGYを起動",
|
|
346
|
+
"launch_codex": "Codexを起動",
|
|
328
347
|
"quit": "終了",
|
|
329
348
|
"title": "ciel-runtime 起動前設定",
|
|
330
349
|
"select_language": "Enterで言語を選択します。上下で移動、Escで閉じます。",
|
|
@@ -335,6 +354,8 @@ UI_TEXT = {
|
|
|
335
354
|
"select_provider_options": "Enterでプロバイダーオプションを適用します。手入力はKEY=VALUEまたはunset:KEYです。",
|
|
336
355
|
"test_result": "互換性結果はメニュー内に表示されます。Escで閉じ、Enterで再テストします。",
|
|
337
356
|
"help_launch": "選択したプロバイダーとモデルでClaude Codeを起動します。",
|
|
357
|
+
"help_launch_agy": "選択したプロバイダーモードでAGYを起動します。",
|
|
358
|
+
"help_launch_codex": "選択したプロバイダーモードでCodexを起動します。",
|
|
338
359
|
"help_test": "現在のプロバイダー/モデルへ最小のClaude Codeツール要求を送り互換性を確認します。",
|
|
339
360
|
"help_language": "言語選択メニューを展開します。",
|
|
340
361
|
"help_provider": "プロバイダー選択メニューを展開します。",
|
|
@@ -362,6 +383,8 @@ UI_TEXT = {
|
|
|
362
383
|
"provider_options": "提供商选项",
|
|
363
384
|
"test": "兼容性测试",
|
|
364
385
|
"launch": "启动 Claude Code",
|
|
386
|
+
"launch_agy": "启动 AGY",
|
|
387
|
+
"launch_codex": "启动 Codex",
|
|
365
388
|
"quit": "退出",
|
|
366
389
|
"title": "ciel-runtime 启动前设置",
|
|
367
390
|
"select_language": "按 Enter 选择语言。上下移动,Esc 关闭。",
|
|
@@ -372,6 +395,8 @@ UI_TEXT = {
|
|
|
372
395
|
"select_provider_options": "按 Enter 应用提供商选项。手动输入支持 KEY=VALUE 或 unset:KEY。",
|
|
373
396
|
"test_result": "兼容性结果会在菜单内显示。Esc 关闭,Enter 重新测试。",
|
|
374
397
|
"help_launch": "使用所选提供商和模型启动 Claude Code。",
|
|
398
|
+
"help_launch_agy": "使用所选提供商模式启动 AGY。",
|
|
399
|
+
"help_launch_codex": "使用所选提供商模式启动 Codex。",
|
|
375
400
|
"help_test": "向当前提供商/模型发送最小 Claude Code 工具请求以检查兼容性。",
|
|
376
401
|
"help_language": "展开语言选择菜单。",
|
|
377
402
|
"help_provider": "展开提供商选择菜单。",
|
|
@@ -681,6 +706,14 @@ def api_key_status(provider: str, pcfg: dict) -> str:
|
|
|
681
706
|
return "API key: set (NVIDIA)" if meaningful_key(read_env_file(NCP_ENV).get("NVIDIA_API_KEY")) else "API key: missing (NVIDIA required)"
|
|
682
707
|
if provider == "anthropic":
|
|
683
708
|
return "API key: set (Anthropic)" if meaningful_key(pcfg.get("api_key")) else "API key: not set (use API key or Claude login)"
|
|
709
|
+
if provider == "agy":
|
|
710
|
+
if pcfg.get("route_through_router"):
|
|
711
|
+
return "API key: not set (uses native AGY Google sign-in/keyring)"
|
|
712
|
+
return "API key: not set (uses native AGY Google sign-in/config)"
|
|
713
|
+
if provider == "codex":
|
|
714
|
+
if pcfg.get("route_through_router"):
|
|
715
|
+
return "API key: not set (uses native Codex login/auth headers)"
|
|
716
|
+
return "API key: optional fallback (uses native Codex login/config)"
|
|
684
717
|
if provider == "ollama-cloud":
|
|
685
718
|
return "API key: set (Ollama Cloud)" if meaningful_key(pcfg.get("api_key")) else "API key: missing (Ollama Cloud required)"
|
|
686
719
|
if provider == "deepseek":
|
|
@@ -712,6 +745,14 @@ def probe_base_url(provider: str, pcfg: dict) -> str:
|
|
|
712
745
|
return f"Base URL: placeholder ({base})"
|
|
713
746
|
if provider == "nvidia-hosted":
|
|
714
747
|
return f"Base URL: NVIDIA hosted ({base}); local router http://127.0.0.1:8799 starts on launch"
|
|
748
|
+
if provider == "codex":
|
|
749
|
+
if pcfg.get("route_through_router"):
|
|
750
|
+
return "Base URL: Codex routed through local router (/backend-api/codex)"
|
|
751
|
+
return "Base URL: native Codex config (not overridden)"
|
|
752
|
+
if provider == "agy":
|
|
753
|
+
if pcfg.get("route_through_router"):
|
|
754
|
+
return "Base URL: AGY routed uses native upstream; Ciel routes channel/PTY wake only"
|
|
755
|
+
return "Base URL: native AGY config (not overridden)"
|
|
715
756
|
if provider == "deepseek":
|
|
716
757
|
return f"Base URL: DeepSeek Anthropic API configured ({base})"
|
|
717
758
|
path = "/api/tags" if provider in ("ollama", "ollama-cloud") else "/v1/models"
|
|
@@ -759,7 +800,36 @@ def preflight_checks() -> list[str]:
|
|
|
759
800
|
return lines
|
|
760
801
|
|
|
761
802
|
|
|
803
|
+
def provider_from_choice(value: str) -> str:
|
|
804
|
+
return str(value or "").split(":", 1)[0]
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def provider_choice_for_cfg(cfg: dict) -> str:
|
|
808
|
+
provider = cfg.get("current_provider", "nvidia-hosted")
|
|
809
|
+
pcfg = cfg.get("providers", {}).get(provider, {})
|
|
810
|
+
if provider == "anthropic":
|
|
811
|
+
return "anthropic:routed" if bool(pcfg.get("route_through_router", False)) else "anthropic:native"
|
|
812
|
+
if provider == "agy":
|
|
813
|
+
return "agy:routed" if bool(pcfg.get("route_through_router", False)) else "agy:native"
|
|
814
|
+
if provider == "codex":
|
|
815
|
+
return "codex:routed" if bool(pcfg.get("route_through_router", False)) else "codex:native"
|
|
816
|
+
return provider
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
def claude_launch_enabled(provider: str) -> bool:
|
|
820
|
+
return provider not in ("agy", "codex")
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def agy_launch_enabled(provider: str) -> bool:
|
|
824
|
+
return provider == "agy"
|
|
825
|
+
|
|
826
|
+
|
|
827
|
+
def codex_launch_enabled(provider: str) -> bool:
|
|
828
|
+
return provider == "codex"
|
|
829
|
+
|
|
830
|
+
|
|
762
831
|
def provider_preview_checks(provider: str) -> list[str]:
|
|
832
|
+
provider = provider_from_choice(provider)
|
|
763
833
|
cfg = load_cfg()
|
|
764
834
|
pcfg = cfg.get("providers", {}).get(provider, {})
|
|
765
835
|
lang = current_language()
|
|
@@ -800,7 +870,7 @@ def is_ollama_provider(provider: str) -> bool:
|
|
|
800
870
|
|
|
801
871
|
|
|
802
872
|
def has_provider_options(provider: str) -> bool:
|
|
803
|
-
return provider in ("vllm", "lm-studio", "nvidia-hosted", "self-hosted-nim", "ollama", "ollama-cloud", "deepseek")
|
|
873
|
+
return provider in ("anthropic", "agy", "codex", "vllm", "lm-studio", "nvidia-hosted", "self-hosted-nim", "ollama", "ollama-cloud", "deepseek")
|
|
804
874
|
|
|
805
875
|
|
|
806
876
|
def ollama_ctx_text(pcfg: dict) -> str:
|
|
@@ -852,11 +922,21 @@ def provider_options_summary(provider: str, pcfg: dict) -> str:
|
|
|
852
922
|
return "; ".join(parts)
|
|
853
923
|
|
|
854
924
|
|
|
925
|
+
def provider_menu_label(provider: str, pcfg: dict) -> str:
|
|
926
|
+
if provider == "anthropic":
|
|
927
|
+
return "Anthropic routed" if bool(pcfg.get("route_through_router", False)) else "Claude Native"
|
|
928
|
+
if provider == "agy":
|
|
929
|
+
return "AGY Routed" if bool(pcfg.get("route_through_router", False)) else "AGY"
|
|
930
|
+
if provider == "codex":
|
|
931
|
+
return "Codex routed" if bool(pcfg.get("route_through_router", False)) else "Codex Native"
|
|
932
|
+
return provider
|
|
933
|
+
|
|
934
|
+
|
|
855
935
|
def main_items() -> list[tuple[str, str]]:
|
|
856
936
|
provider, pcfg = current_provider_cfg()
|
|
857
937
|
lang = current_language()
|
|
858
|
-
model = pcfg.get("current_model", "unset")
|
|
859
|
-
advisor_model = "Claude Code native /advisor" if provider == "anthropic" else (pcfg.get("advisor_model") or "off")
|
|
938
|
+
model = "AGY default" if provider == "agy" and not pcfg.get("current_model") else ("Codex default" if provider == "codex" and not pcfg.get("current_model") else pcfg.get("current_model", "unset"))
|
|
939
|
+
advisor_model = "Claude Code native /advisor" if provider == "anthropic" else ("AGY native" if provider == "agy" else ("Codex native" if provider == "codex" else (pcfg.get("advisor_model") or "off")))
|
|
860
940
|
base = pcfg.get("base_url", "unset")
|
|
861
941
|
rows: list[tuple[str, str]] = []
|
|
862
942
|
|
|
@@ -864,7 +944,7 @@ def main_items() -> list[tuple[str, str]]:
|
|
|
864
944
|
rows.append((key, f"{len(rows)}. {label}"))
|
|
865
945
|
|
|
866
946
|
add("language", f"{t('language')} [{LANGUAGES.get(lang, lang)}]")
|
|
867
|
-
add("provider", f"{t('provider')} [{provider}]")
|
|
947
|
+
add("provider", f"{t('provider')} [{provider_menu_label(provider, pcfg)}]")
|
|
868
948
|
add("api-key", t("api_key"))
|
|
869
949
|
add("base-url", f"{t('base_url')} [{base}]")
|
|
870
950
|
add("model", f"{t('model')} [{model}]")
|
|
@@ -874,13 +954,29 @@ def main_items() -> list[tuple[str, str]]:
|
|
|
874
954
|
if has_provider_options(provider):
|
|
875
955
|
add("provider-options", f"{t('provider_options')} [{provider_options_summary(provider, pcfg)}]")
|
|
876
956
|
add("test", t("test"))
|
|
877
|
-
|
|
957
|
+
launch_label = t("launch")
|
|
958
|
+
if not claude_launch_enabled(provider):
|
|
959
|
+
family = "AGY" if provider == "agy" else "Codex" if provider == "codex" else provider_menu_label(provider, pcfg)
|
|
960
|
+
launch_label += f" [disabled: {family} provider selected]"
|
|
961
|
+
add("launch", launch_label)
|
|
962
|
+
launch_codex_label = t("launch_codex")
|
|
963
|
+
if not codex_launch_enabled(provider):
|
|
964
|
+
launch_codex_label += " [disabled: select Codex provider]"
|
|
965
|
+
add("launch-codex", launch_codex_label)
|
|
966
|
+
launch_agy_label = t("launch_agy")
|
|
967
|
+
if not agy_launch_enabled(provider):
|
|
968
|
+
launch_agy_label += " [disabled: select AGY provider]"
|
|
969
|
+
add("launch-agy", launch_agy_label)
|
|
878
970
|
rows.append(("quit", t("quit")))
|
|
879
971
|
return rows
|
|
880
972
|
|
|
881
973
|
|
|
882
974
|
def settings_ready_except_api_key() -> bool:
|
|
883
975
|
provider, pcfg = current_provider_cfg()
|
|
976
|
+
if provider == "codex":
|
|
977
|
+
return True
|
|
978
|
+
if provider == "agy":
|
|
979
|
+
return True
|
|
884
980
|
base = pcfg.get("base_url", "")
|
|
885
981
|
model = pcfg.get("current_model", "")
|
|
886
982
|
return bool(provider and base and model and "your-" not in base)
|
|
@@ -888,6 +984,8 @@ def settings_ready_except_api_key() -> bool:
|
|
|
888
984
|
def default_base_url(provider: str) -> str:
|
|
889
985
|
return {
|
|
890
986
|
"anthropic": "https://api.anthropic.com",
|
|
987
|
+
"codex": "https://api.openai.com",
|
|
988
|
+
"agy": "https://antigravity.google",
|
|
891
989
|
"ollama": "http://your-ollama:11434",
|
|
892
990
|
"ollama-cloud": "https://ollama.com",
|
|
893
991
|
"deepseek": "https://api.deepseek.com/anthropic",
|
|
@@ -915,6 +1013,8 @@ def help_for_action(action: str, sub_kind: str | None = None) -> str:
|
|
|
915
1013
|
return t("test_result")
|
|
916
1014
|
return {
|
|
917
1015
|
"launch": t("help_launch"),
|
|
1016
|
+
"launch-agy": t("help_launch_agy"),
|
|
1017
|
+
"launch-codex": t("help_launch_codex"),
|
|
918
1018
|
"test": t("help_test"),
|
|
919
1019
|
"language": t("help_language"),
|
|
920
1020
|
"provider": t("help_provider"),
|
|
@@ -942,14 +1042,15 @@ def get_models_for_current_provider() -> tuple[list[tuple[str, str]], str]:
|
|
|
942
1042
|
|
|
943
1043
|
def build_provider_submenu() -> dict:
|
|
944
1044
|
cfg = load_cfg()
|
|
945
|
-
current = cfg
|
|
1045
|
+
current = provider_choice_for_cfg(cfg)
|
|
946
1046
|
items = []
|
|
947
1047
|
idx = 0
|
|
948
1048
|
for i, (key, label) in enumerate(PROVIDERS):
|
|
949
1049
|
if key == current:
|
|
950
1050
|
idx = i
|
|
951
|
-
|
|
952
|
-
|
|
1051
|
+
provider = provider_from_choice(key)
|
|
1052
|
+
base = cfg.get("providers", {}).get(provider, {}).get("base_url", "")
|
|
1053
|
+
items.append({"value": key, "label": f"{label:<16} {key:<17} {base}", "current": key == current})
|
|
953
1054
|
return {"kind": "provider", "parent": "provider", "items": items, "idx": idx, "offset": 0}
|
|
954
1055
|
|
|
955
1056
|
|
|
@@ -969,9 +1070,10 @@ def build_api_key_submenu() -> dict:
|
|
|
969
1070
|
items = []
|
|
970
1071
|
idx = 0
|
|
971
1072
|
for i, (key, label) in enumerate(PROVIDERS):
|
|
972
|
-
|
|
1073
|
+
provider = provider_from_choice(key)
|
|
1074
|
+
if provider == current:
|
|
973
1075
|
idx = i
|
|
974
|
-
items.append({"value":
|
|
1076
|
+
items.append({"value": provider, "label": f"{label:<16} {provider:<15}", "current": provider == current})
|
|
975
1077
|
return {"kind": "api-key", "parent": "api-key", "items": items, "idx": idx, "offset": 0}
|
|
976
1078
|
|
|
977
1079
|
def build_model_submenu() -> tuple[dict | None, list[str]]:
|
|
@@ -1301,6 +1403,10 @@ def build_provider_options_submenu() -> dict:
|
|
|
1301
1403
|
("__edit_max_output__", f"Edit max_output_tokens [{max_output}]", False),
|
|
1302
1404
|
("__edit_timeout__", f"Edit timeout ms [{timeout}]", False),
|
|
1303
1405
|
]
|
|
1406
|
+
if provider in ("anthropic", "codex"):
|
|
1407
|
+
routed = bool(pcfg.get("route_through_router", False))
|
|
1408
|
+
choices.insert(0, ("route_through_router=true", "routed through ciel-runtime router", routed))
|
|
1409
|
+
choices.insert(1, ("route_through_router=false", "native direct mode", not routed))
|
|
1304
1410
|
if provider in ("lm-studio", "nvidia-hosted", "self-hosted-nim", "ollama", "ollama-cloud"):
|
|
1305
1411
|
choices.append(("__edit_rate_limit__", f"Edit rate_limit_rpm [{pcfg.get('rate_limit_rpm', 40)}]", False))
|
|
1306
1412
|
choices.append(("rate_limit_status=true", "rate_limit_status on", bool(pcfg.get("rate_limit_status", True))))
|
|
@@ -1542,6 +1648,12 @@ def index_for_action(action: str) -> int:
|
|
|
1542
1648
|
return next((i for i, (key, _) in enumerate(items) if key == action), 0)
|
|
1543
1649
|
|
|
1544
1650
|
|
|
1651
|
+
def default_launch_action() -> str:
|
|
1652
|
+
if agy_launch_enabled(current_provider()):
|
|
1653
|
+
return "launch-agy"
|
|
1654
|
+
return "launch-codex" if codex_launch_enabled(current_provider()) else "launch"
|
|
1655
|
+
|
|
1656
|
+
|
|
1545
1657
|
def add(stdscr, y: int, x: int, text: str, style: str = "") -> None:
|
|
1546
1658
|
_write_safe(y, max(0, x), text, style)
|
|
1547
1659
|
|
|
@@ -1615,7 +1727,9 @@ def render(stdscr, idx: int, sub: dict | None, notice: list[str], checks: list[s
|
|
|
1615
1727
|
break
|
|
1616
1728
|
if i == idx and (sub is None or sub.get("readonly")):
|
|
1617
1729
|
style = _style(reverse=True, bold=True)
|
|
1618
|
-
elif key
|
|
1730
|
+
elif key in ("launch", "launch-codex", "launch-agy") and "disabled:" in label:
|
|
1731
|
+
style = _style(dim=True)
|
|
1732
|
+
elif key in ("launch", "launch-codex", "launch-agy"):
|
|
1619
1733
|
style = cp(2) + _style(bold=True)
|
|
1620
1734
|
elif key == "test":
|
|
1621
1735
|
style = cp(3) + _style(bold=True)
|
|
@@ -1701,7 +1815,7 @@ def render(stdscr, idx: int, sub: dict | None, notice: list[str], checks: list[s
|
|
|
1701
1815
|
|
|
1702
1816
|
def main() -> int:
|
|
1703
1817
|
init_colors()
|
|
1704
|
-
idx = index_for_action(
|
|
1818
|
+
idx = index_for_action(default_launch_action()) if settings_ready_except_api_key() else 0
|
|
1705
1819
|
sub: dict | None = None
|
|
1706
1820
|
notice: list[str] = []
|
|
1707
1821
|
checks = preflight_checks()
|
|
@@ -1718,7 +1832,7 @@ def main() -> int:
|
|
|
1718
1832
|
else:
|
|
1719
1833
|
notice = [t("test_failed")]
|
|
1720
1834
|
checks = preflight_checks()
|
|
1721
|
-
idx = index_for_action(
|
|
1835
|
+
idx = index_for_action(default_launch_action() if ok else "model")
|
|
1722
1836
|
|
|
1723
1837
|
while True:
|
|
1724
1838
|
items = main_items()
|
|
@@ -1743,6 +1857,10 @@ def main() -> int:
|
|
|
1743
1857
|
action = items[idx][0]
|
|
1744
1858
|
if action == "launch":
|
|
1745
1859
|
provider, pcfg = current_provider_cfg()
|
|
1860
|
+
if not claude_launch_enabled(provider):
|
|
1861
|
+
notice = [f"Launch Claude Code is disabled while {provider_menu_label(provider, pcfg)} provider is selected."]
|
|
1862
|
+
sub = None
|
|
1863
|
+
continue
|
|
1746
1864
|
if launch_requires_api_key(provider, pcfg):
|
|
1747
1865
|
label = dict(PROVIDERS).get(provider, provider)
|
|
1748
1866
|
notice = [
|
|
@@ -1752,7 +1870,39 @@ def main() -> int:
|
|
|
1752
1870
|
idx = index_for_action("api-key")
|
|
1753
1871
|
sub = None
|
|
1754
1872
|
continue
|
|
1755
|
-
return
|
|
1873
|
+
return PRELAUNCH_LAUNCH_CLAUDE
|
|
1874
|
+
if action == "launch-agy":
|
|
1875
|
+
provider, pcfg = current_provider_cfg()
|
|
1876
|
+
if not agy_launch_enabled(provider):
|
|
1877
|
+
notice = ["Launch AGY is disabled until you select AGY or AGY Routed as the provider."]
|
|
1878
|
+
sub = None
|
|
1879
|
+
continue
|
|
1880
|
+
if launch_requires_api_key(provider, pcfg):
|
|
1881
|
+
label = dict(PROVIDERS).get(provider, provider)
|
|
1882
|
+
notice = [
|
|
1883
|
+
f"Launch blocked: {label} requires an API key.",
|
|
1884
|
+
"Opening API key setup.",
|
|
1885
|
+
]
|
|
1886
|
+
idx = index_for_action("api-key")
|
|
1887
|
+
sub = None
|
|
1888
|
+
continue
|
|
1889
|
+
return PRELAUNCH_LAUNCH_AGY
|
|
1890
|
+
if action == "launch-codex":
|
|
1891
|
+
provider, pcfg = current_provider_cfg()
|
|
1892
|
+
if not codex_launch_enabled(provider):
|
|
1893
|
+
notice = ["Launch Codex is disabled until you select Codex or Codex routed as the provider."]
|
|
1894
|
+
sub = None
|
|
1895
|
+
continue
|
|
1896
|
+
if launch_requires_api_key(provider, pcfg):
|
|
1897
|
+
label = dict(PROVIDERS).get(provider, provider)
|
|
1898
|
+
notice = [
|
|
1899
|
+
f"Launch blocked: {label} requires an API key.",
|
|
1900
|
+
"Opening API key setup.",
|
|
1901
|
+
]
|
|
1902
|
+
idx = index_for_action("api-key")
|
|
1903
|
+
sub = None
|
|
1904
|
+
continue
|
|
1905
|
+
return PRELAUNCH_LAUNCH_CODEX
|
|
1756
1906
|
if action == "test":
|
|
1757
1907
|
code, out = run_test_with_animation(idx, checks)
|
|
1758
1908
|
apply_test_result(code, out)
|
|
@@ -1806,7 +1956,8 @@ def main() -> int:
|
|
|
1806
1956
|
notice = (out.strip().splitlines() or [item["value"]])[:2]
|
|
1807
1957
|
checks = preflight_checks()
|
|
1808
1958
|
sub = None
|
|
1809
|
-
|
|
1959
|
+
provider = current_provider()
|
|
1960
|
+
idx = index_for_action(default_launch_action() if provider == "codex" else "model")
|
|
1810
1961
|
elif sub["kind"] == "api-key":
|
|
1811
1962
|
row = row_by_action.get("__sub_selected__", row_by_action.get("api-key", 10))
|
|
1812
1963
|
key = inline_secret_prompt(None, f"API key for {item['value']}: ", row)
|
|
@@ -1944,7 +2095,7 @@ def main() -> int:
|
|
|
1944
2095
|
continue
|
|
1945
2096
|
|
|
1946
2097
|
if ch in ("KEY_ESC", "q"):
|
|
1947
|
-
return
|
|
2098
|
+
return PRELAUNCH_CANCEL
|
|
1948
2099
|
if ch in ("KEY_UP", "k"):
|
|
1949
2100
|
notice = []
|
|
1950
2101
|
idx = (idx - 1) % len(items)
|
|
@@ -1959,6 +2110,37 @@ def main() -> int:
|
|
|
1959
2110
|
action = items[idx][0]
|
|
1960
2111
|
if action == "launch":
|
|
1961
2112
|
provider, pcfg = current_provider_cfg()
|
|
2113
|
+
if not claude_launch_enabled(provider):
|
|
2114
|
+
notice = [f"Launch Claude Code is disabled while {provider_menu_label(provider, pcfg)} provider is selected."]
|
|
2115
|
+
continue
|
|
2116
|
+
if launch_requires_api_key(provider, pcfg):
|
|
2117
|
+
label = dict(PROVIDERS).get(provider, provider)
|
|
2118
|
+
notice = [
|
|
2119
|
+
f"Launch blocked: {label} requires an API key.",
|
|
2120
|
+
"Opening API key setup.",
|
|
2121
|
+
]
|
|
2122
|
+
idx = index_for_action("api-key")
|
|
2123
|
+
continue
|
|
2124
|
+
return PRELAUNCH_LAUNCH_CLAUDE
|
|
2125
|
+
if action == "launch-agy":
|
|
2126
|
+
provider, pcfg = current_provider_cfg()
|
|
2127
|
+
if not agy_launch_enabled(provider):
|
|
2128
|
+
notice = ["Launch AGY is disabled until you select AGY or AGY Routed as the provider."]
|
|
2129
|
+
continue
|
|
2130
|
+
if launch_requires_api_key(provider, pcfg):
|
|
2131
|
+
label = dict(PROVIDERS).get(provider, provider)
|
|
2132
|
+
notice = [
|
|
2133
|
+
f"Launch blocked: {label} requires an API key.",
|
|
2134
|
+
"Opening API key setup.",
|
|
2135
|
+
]
|
|
2136
|
+
idx = index_for_action("api-key")
|
|
2137
|
+
continue
|
|
2138
|
+
return PRELAUNCH_LAUNCH_AGY
|
|
2139
|
+
if action == "launch-codex":
|
|
2140
|
+
provider, pcfg = current_provider_cfg()
|
|
2141
|
+
if not codex_launch_enabled(provider):
|
|
2142
|
+
notice = ["Launch Codex is disabled until you select Codex or Codex routed as the provider."]
|
|
2143
|
+
continue
|
|
1962
2144
|
if launch_requires_api_key(provider, pcfg):
|
|
1963
2145
|
label = dict(PROVIDERS).get(provider, provider)
|
|
1964
2146
|
notice = [
|
|
@@ -1967,13 +2149,13 @@ def main() -> int:
|
|
|
1967
2149
|
]
|
|
1968
2150
|
idx = index_for_action("api-key")
|
|
1969
2151
|
continue
|
|
1970
|
-
return
|
|
2152
|
+
return PRELAUNCH_LAUNCH_CODEX
|
|
1971
2153
|
if action == "test":
|
|
1972
2154
|
code, out = run_test_with_animation(idx, checks)
|
|
1973
2155
|
apply_test_result(code, out)
|
|
1974
2156
|
continue
|
|
1975
2157
|
if action == "quit":
|
|
1976
|
-
return
|
|
2158
|
+
return PRELAUNCH_CANCEL
|
|
1977
2159
|
if action == "language":
|
|
1978
2160
|
sub = build_language_submenu()
|
|
1979
2161
|
notice = []
|
|
@@ -59,6 +59,7 @@ GLOB_KEYS = {"pattern", "path"}
|
|
|
59
59
|
GREP_KEYS = {"pattern", "path", "glob", "type", "output_mode", "-A", "-B", "-C", "head_limit", "multiline"}
|
|
60
60
|
LS_KEYS = {"path", "ignore"}
|
|
61
61
|
TASKLIST_KEYS: set[str] = set()
|
|
62
|
+
TRANSCRIPT_TAIL_BYTES = 1024 * 1024
|
|
62
63
|
TASKUPDATE_KEYS = {
|
|
63
64
|
"taskId",
|
|
64
65
|
"subject",
|
|
@@ -313,16 +314,34 @@ def has_in_progress_task(session_id: str | None) -> bool:
|
|
|
313
314
|
return False
|
|
314
315
|
|
|
315
316
|
|
|
316
|
-
def
|
|
317
|
-
if not transcript_path:
|
|
318
|
-
return
|
|
317
|
+
def transcript_tail_lines(transcript_path: str | None, max_lines: int) -> list[str]:
|
|
318
|
+
if not transcript_path or max_lines <= 0:
|
|
319
|
+
return []
|
|
319
320
|
path = Path(transcript_path)
|
|
320
321
|
if not path.exists():
|
|
321
|
-
return
|
|
322
|
+
return []
|
|
322
323
|
try:
|
|
323
|
-
|
|
324
|
+
size = path.stat().st_size
|
|
325
|
+
offset = max(0, size - TRANSCRIPT_TAIL_BYTES)
|
|
326
|
+
with path.open("rb") as f:
|
|
327
|
+
if offset:
|
|
328
|
+
f.seek(offset)
|
|
329
|
+
data = f.read(TRANSCRIPT_TAIL_BYTES)
|
|
324
330
|
except Exception:
|
|
325
|
-
return
|
|
331
|
+
return []
|
|
332
|
+
if offset:
|
|
333
|
+
newline = data.find(b"\n")
|
|
334
|
+
if newline >= 0:
|
|
335
|
+
data = data[newline + 1 :]
|
|
336
|
+
try:
|
|
337
|
+
text = data.decode("utf-8", errors="ignore")
|
|
338
|
+
except Exception:
|
|
339
|
+
return []
|
|
340
|
+
return text.splitlines()[-max_lines:]
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def transcript_latest_user_text(transcript_path: str | None) -> str:
|
|
344
|
+
lines = transcript_tail_lines(transcript_path, 80)
|
|
326
345
|
for line in reversed(lines):
|
|
327
346
|
try:
|
|
328
347
|
data = json.loads(line)
|
|
@@ -352,19 +371,13 @@ def latest_user_is_channel_wake(transcript_path: str | None) -> bool:
|
|
|
352
371
|
return (
|
|
353
372
|
"[ciel-runtime external channel message" in text
|
|
354
373
|
or "[ciel-runtime channel inbox]" in text
|
|
374
|
+
or "[external channel input]" in text
|
|
375
|
+
or text.startswith("[external input pending]")
|
|
355
376
|
)
|
|
356
377
|
|
|
357
378
|
|
|
358
379
|
def transcript_plan_mode_active(transcript_path: str | None) -> bool:
|
|
359
|
-
|
|
360
|
-
return False
|
|
361
|
-
path = Path(transcript_path)
|
|
362
|
-
if not path.exists():
|
|
363
|
-
return False
|
|
364
|
-
try:
|
|
365
|
-
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()[-240:]
|
|
366
|
-
except Exception:
|
|
367
|
-
return False
|
|
380
|
+
lines = transcript_tail_lines(transcript_path, 240)
|
|
368
381
|
active = False
|
|
369
382
|
tool_names_by_id: dict[str, str] = {}
|
|
370
383
|
for line in lines:
|
|
@@ -429,15 +442,7 @@ def message_has_tool_use(message: dict[str, Any]) -> bool:
|
|
|
429
442
|
|
|
430
443
|
|
|
431
444
|
def transcript_latest_turn(transcript_path: str | None) -> dict[str, Any]:
|
|
432
|
-
|
|
433
|
-
return {}
|
|
434
|
-
path = Path(transcript_path)
|
|
435
|
-
if not path.exists():
|
|
436
|
-
return {}
|
|
437
|
-
try:
|
|
438
|
-
lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()[-160:]
|
|
439
|
-
except Exception:
|
|
440
|
-
return {}
|
|
445
|
+
lines = transcript_tail_lines(transcript_path, 160)
|
|
441
446
|
|
|
442
447
|
latest_assistant: dict[str, Any] | None = None
|
|
443
448
|
latest_assistant_index = -1
|