@bitkyc08/opencodex 2.36.0 → 2.38.0

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 (70) hide show
  1. package/bin/ocx.mjs +69 -10
  2. package/gui/dist/assets/index-C14iCj_Q.js +112 -0
  3. package/gui/dist/assets/index-D7PIz7_g.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/aside.svg +3 -0
  6. package/gui/dist/provider-icons/deepseek-harness.svg +3 -0
  7. package/gui/dist/provider-icons/oh-my-pi.svg +11 -0
  8. package/gui/dist/provider-icons/openclaw.svg +54 -0
  9. package/gui/dist/provider-icons/prime-agent.svg +21 -0
  10. package/gui/dist/provider-icons/zcode.svg +219 -0
  11. package/package.json +1 -1
  12. package/src/adapters/cursor/protobuf-request.ts +4 -1
  13. package/src/adapters/cursor/tool-definitions.ts +36 -4
  14. package/src/adapters/kiro-constants.ts +36 -2
  15. package/src/adapters/kiro.ts +13 -2
  16. package/src/cli/capabilities.ts +14 -0
  17. package/src/cli/claude.ts +12 -0
  18. package/src/cli/codex-cli-update.ts +96 -0
  19. package/src/cli/codex-shim-autorestore.ts +3 -0
  20. package/src/cli/export-command.ts +18 -17
  21. package/src/cli/help.ts +2 -2
  22. package/src/cli/index.ts +3 -2
  23. package/src/cli/launcher-context.ts +53 -2
  24. package/src/cli/opencode.ts +126 -33
  25. package/src/cli/registry.ts +16 -10
  26. package/src/cli/system-command.ts +6 -1
  27. package/src/clients/config-export.ts +293 -28
  28. package/src/codex/account-store.ts +10 -4
  29. package/src/codex/autostart-health.ts +3 -3
  30. package/src/codex/catalog/provider-fetch.ts +20 -1
  31. package/src/codex/catalog/sync.ts +4 -3
  32. package/src/codex/cli-install-provenance.ts +795 -0
  33. package/src/codex/convergence.ts +4 -3
  34. package/src/codex/credential-mutation-epoch.ts +11 -0
  35. package/src/codex/main-account.ts +2 -0
  36. package/src/codex/model-entitlements.ts +489 -30
  37. package/src/codex/native-profile-manager.ts +4 -0
  38. package/src/codex/reset-credit-operation-ledger.ts +1411 -0
  39. package/src/codex/reset-credit-recovery.ts +20 -2
  40. package/src/codex/shim.ts +204 -18
  41. package/src/codex/user-identity.ts +2 -1
  42. package/src/config/paths.ts +18 -3
  43. package/src/config.ts +23 -0
  44. package/src/generated/compatibility-version.json +84 -48
  45. package/src/integrations/registry.ts +112 -0
  46. package/src/integrations/state.ts +67 -5
  47. package/src/integrations/writer.ts +25 -9
  48. package/src/lib/bounded-subprocess.ts +36 -0
  49. package/src/lib/strict-semver.ts +47 -0
  50. package/src/lib/windows-elevation.ts +32 -1
  51. package/src/lib/windows-secret-acl.ts +47 -25
  52. package/src/lib/windows-service-mutation-lock.ts +133 -0
  53. package/src/lib/windows-user-principal.ts +15 -17
  54. package/src/responses/spill-store.ts +334 -29
  55. package/src/responses/state.ts +488 -7
  56. package/src/server/index.ts +4 -3
  57. package/src/server/lifecycle.ts +5 -1
  58. package/src/server/management/model-rows.ts +11 -2
  59. package/src/server/management/provider-routes.ts +4 -0
  60. package/src/server/management/system-restart.ts +5 -5
  61. package/src/server/management-api.ts +7 -2
  62. package/src/server/startup-action-control.ts +3 -2
  63. package/src/service.ts +594 -33
  64. package/src/sidecar/candidates.ts +1 -1
  65. package/src/update/codex-cli-update-launch-policy.d.mts +18 -0
  66. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  67. package/src/update/index.ts +3 -2
  68. package/src/update/job.ts +10 -11
  69. package/gui/dist/assets/index-Cy7Z_pl0.css +0 -1
  70. package/gui/dist/assets/index-DO8liQVL.js +0 -112
@@ -0,0 +1,219 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
3
+ <svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
4
+ viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
5
+ <style type="text/css">
6
+ .st0{opacity:0.3;fill:#E2E4E7;}
7
+ .st1{opacity:0.8;fill:#E2E4E7;stroke:#FFFFFF;stroke-width:5;stroke-miterlimit:10;}
8
+ .st2{fill:url(#SVGID_1_);}
9
+ .st3{fill:none;stroke:#E0E4E9;stroke-width:0.25;stroke-miterlimit:10;}
10
+ .st4{fill:none;}
11
+ .st5{fill:#9DA1A5;}
12
+ .st6{fill-rule:evenodd;clip-rule:evenodd;fill:none;}
13
+ .st7{fill-rule:evenodd;clip-rule:evenodd;fill:#DFE2E7;}
14
+ .st8{fill-rule:evenodd;clip-rule:evenodd;fill:#CDD4DA;}
15
+ .st9{fill-rule:evenodd;clip-rule:evenodd;fill:#B3BCC7;}
16
+ .st10{fill-rule:evenodd;clip-rule:evenodd;fill:#9DAAB7;}
17
+ .st11{fill-rule:evenodd;clip-rule:evenodd;fill:#8698A8;}
18
+ .st12{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_2_);}
19
+ .st13{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_3_);}
20
+ .st14{fill:#1F63EC;}
21
+ .st15{fill:#2D2D2D;}
22
+ .st16{fill:none;stroke:#E0E4E9;stroke-width:0.5;stroke-miterlimit:10;}
23
+ .st17{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_4_);}
24
+ .st18{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_5_);}
25
+ .st19{fill:none;stroke:#677380;stroke-width:0.5;stroke-miterlimit:10;}
26
+ .st20{fill:none;stroke:url(#SVGID_6_);stroke-width:2;stroke-miterlimit:10;}
27
+ .st21{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_7_);}
28
+ .st22{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_8_);}
29
+ .st23{fill:#FFFFFF;}
30
+ .st24{fill-rule:evenodd;clip-rule:evenodd;fill:#2D2D2D;}
31
+ .st25{clip-path:url(#SVGID_10_);}
32
+ .st26{clip-path:url(#SVGID_12_);}
33
+ .st27{fill:url(#SVGID_13_);}
34
+ .st28{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_14_);}
35
+ .st29{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_15_);}
36
+ .st30{clip-path:url(#SVGID_17_);}
37
+ .st31{clip-path:url(#SVGID_19_);}
38
+ .st32{fill:url(#SVGID_20_);}
39
+ .st33{fill:none;stroke:url(#SVGID_21_);stroke-width:2;stroke-miterlimit:10;}
40
+ .st34{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_22_);}
41
+ .st35{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_23_);}
42
+ .st36{clip-path:url(#SVGID_25_);}
43
+ .st37{clip-path:url(#SVGID_27_);}
44
+ .st38{fill:url(#SVGID_28_);}
45
+ .st39{clip-path:url(#SVGID_30_);}
46
+ .st40{clip-path:url(#SVGID_32_);}
47
+ .st41{fill:url(#SVGID_33_);}
48
+ .st42{fill-rule:evenodd;clip-rule:evenodd;fill:#126EF6;}
49
+ .st43{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
50
+ .st44{clip-path:url(#SVGID_35_);}
51
+ .st45{clip-path:url(#SVGID_37_);}
52
+ .st46{fill:url(#SVGID_38_);}
53
+ .st47{fill-rule:evenodd;clip-rule:evenodd;fill:#9DA1A5;}
54
+ .st48{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_39_);}
55
+ .st49{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_40_);}
56
+ .st50{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_41_);}
57
+ .st51{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_42_);}
58
+ .st52{fill:none;stroke:url(#SVGID_43_);stroke-width:2;stroke-miterlimit:10;}
59
+ .st53{fill-rule:evenodd;clip-rule:evenodd;fill:none;stroke:#E0E4E9;stroke-width:0.5;stroke-miterlimit:10;}
60
+ .st54{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_44_);}
61
+ .st55{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_45_);}
62
+ .st56{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_46_);}
63
+ .st57{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_47_);}
64
+ .st58{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_48_);}
65
+ .st59{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_49_);}
66
+ .st60{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_50_);}
67
+ .st61{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_51_);}
68
+ .st62{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_52_);}
69
+ .st63{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_53_);}
70
+ .st64{clip-path:url(#SVGID_55_);}
71
+ .st65{clip-path:url(#SVGID_57_);}
72
+ .st66{fill:url(#SVGID_58_);}
73
+ .st67{clip-path:url(#SVGID_60_);}
74
+ .st68{clip-path:url(#SVGID_62_);}
75
+ .st69{fill:url(#SVGID_63_);}
76
+ .st70{fill:none;stroke:url(#SVGID_64_);stroke-width:2;stroke-miterlimit:10;}
77
+ .st71{clip-path:url(#SVGID_66_);}
78
+ .st72{clip-path:url(#SVGID_68_);}
79
+ .st73{fill:url(#SVGID_69_);}
80
+ .st74{clip-path:url(#SVGID_71_);}
81
+ .st75{clip-path:url(#SVGID_73_);}
82
+ .st76{fill:url(#SVGID_74_);}
83
+ .st77{clip-path:url(#SVGID_76_);}
84
+ .st78{clip-path:url(#SVGID_78_);}
85
+ .st79{fill:url(#SVGID_79_);}
86
+ .st80{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_80_);}
87
+ .st81{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_81_);}
88
+ .st82{clip-path:url(#SVGID_83_);}
89
+ .st83{clip-path:url(#SVGID_85_);}
90
+ .st84{fill:url(#SVGID_86_);}
91
+ .st85{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_87_);}
92
+ .st86{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_88_);}
93
+ .st87{clip-path:url(#SVGID_90_);}
94
+ .st88{clip-path:url(#SVGID_92_);}
95
+ .st89{fill:url(#SVGID_93_);}
96
+ .st90{fill:none;stroke:url(#SVGID_94_);stroke-width:2;stroke-miterlimit:10;}
97
+ .st91{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_95_);}
98
+ .st92{fill-rule:evenodd;clip-rule:evenodd;fill:url(#SVGID_96_);}
99
+ .st93{clip-path:url(#SVGID_98_);}
100
+ .st94{clip-path:url(#SVGID_100_);}
101
+ .st95{fill:url(#SVGID_101_);}
102
+ .st96{clip-path:url(#SVGID_103_);}
103
+ .st97{clip-path:url(#SVGID_105_);}
104
+ .st98{fill:url(#SVGID_106_);}
105
+ .st99{clip-path:url(#SVGID_108_);}
106
+ .st100{clip-path:url(#SVGID_110_);}
107
+ .st101{fill:url(#SVGID_111_);}
108
+ .st102{fill:#FFFFFF;stroke:#B3BCC7;stroke-width:0.275;stroke-miterlimit:10;}
109
+ .st103{clip-path:url(#SVGID_113_);}
110
+ .st104{fill:#FDD138;}
111
+ .st105{fill:#FCA62F;}
112
+ .st106{fill:#FB7927;}
113
+ .st107{fill:#F44B22;}
114
+ .st108{fill:#D81915;}
115
+ .st109{fill:#2D2D2D;stroke:#FFFFFF;stroke-width:0.3354;stroke-miterlimit:10;}
116
+ .st110{fill:none;stroke:#65727F;stroke-width:2;stroke-miterlimit:10;}
117
+ .st111{fill:none;stroke:#65727F;stroke-width:0.75;stroke-miterlimit:10;}
118
+ .st112{fill:url(#SVGID_114_);}
119
+ .st113{fill:#D06C50;}
120
+ .st114{fill:#2D2D2D;stroke:#B3BCC7;stroke-width:0.275;stroke-miterlimit:10;}
121
+ .st115{opacity:0.2;}
122
+ .st116{fill:none;stroke:#677380;stroke-width:0.3564;stroke-miterlimit:10;}
123
+ .st117{fill:none;stroke:#677380;stroke-width:0.3564;stroke-miterlimit:10;stroke-dasharray:1.0212,1.0212;}
124
+ .st118{fill:none;stroke:#677380;stroke-width:0.3564;stroke-miterlimit:10;stroke-dasharray:1.0205,1.0205;}
125
+ .st119{opacity:0.2;fill:none;}
126
+ .st120{fill:none;stroke:#677380;stroke-width:0.3689;stroke-miterlimit:10;}
127
+ .st121{fill:none;stroke:#677380;stroke-width:0.3689;stroke-miterlimit:10;stroke-dasharray:1.0509,1.0509;}
128
+ .st122{opacity:0.3;fill:#1F63EC;}
129
+ .st123{fill:#2D2D2D;stroke:#FFFFFF;stroke-width:0.3162;stroke-miterlimit:10;}
130
+ .st124{fill:#FFFFFF;stroke:#B3BCC7;stroke-width:0.3162;stroke-miterlimit:10;}
131
+ .st125{clip-path:url(#SVGID_118_);}
132
+ .st126{fill:url(#SVGID_119_);}
133
+ .st127{fill:none;stroke:#DFE2E7;stroke-width:0.75;stroke-miterlimit:10;}
134
+ .st128{fill:#9DA1A5;stroke:#FFFFFF;stroke-miterlimit:10;}
135
+ .st129{fill:url(#SVGID_120_);}
136
+ .st130{fill:none;stroke:#677380;stroke-width:0.75;stroke-miterlimit:10;}
137
+ .st131{opacity:0.4;}
138
+ .st132{clip-path:url(#SVGID_122_);}
139
+ .st133{clip-path:url(#SVGID_124_);}
140
+ .st134{fill:url(#SVGID_125_);}
141
+ .st135{fill:none;stroke:#8392A3;stroke-width:0.35;stroke-miterlimit:10;}
142
+ .st136{fill:none;stroke:#8392A3;stroke-width:0.35;stroke-miterlimit:10;stroke-dasharray:0.9951,0.9951;}
143
+ .st137{fill:none;stroke:#8392A3;stroke-width:0.35;stroke-miterlimit:10;stroke-dasharray:1.004,1.004;}
144
+ .st138{fill:none;stroke:url(#SVGID_126_);stroke-width:1.5;stroke-miterlimit:10;}
145
+ .st139{fill:url(#SVGID_127_);}
146
+ .st140{fill:none;stroke:#DDE0E4;stroke-width:0.35;stroke-miterlimit:10;}
147
+ .st141{fill:#2D2D2D;stroke:#A9B3BE;stroke-width:0.275;stroke-miterlimit:10;}
148
+ .st142{fill-rule:evenodd;clip-rule:evenodd;fill:#126EF4;}
149
+ .st143{fill:#FFFFFF;stroke:#B1BAC4;stroke-width:0.275;stroke-miterlimit:10;}
150
+ .st144{fill:#CE6C50;}
151
+ .st145{fill:#5B5B5B;}
152
+ .st146{fill:#8392A3;}
153
+ .st147{fill:none;stroke:url(#SVGID_128_);stroke-width:1.5;stroke-miterlimit:10;}
154
+ .st148{fill:url(#SVGID_129_);}
155
+ .st149{fill:none;stroke:#B5BDC4;stroke-width:0.7;stroke-miterlimit:10;}
156
+ .st150{opacity:0.6;fill:none;stroke:#78838E;stroke-width:0.35;stroke-miterlimit:10;}
157
+ .st151{opacity:0.2;fill:none;stroke:#8392A3;stroke-width:0.35;stroke-miterlimit:10;stroke-dasharray:1,1;}
158
+ .st152{fill:none;stroke:#DDE0E4;stroke-width:0.75;stroke-miterlimit:10;}
159
+ .st153{fill:none;stroke:#8392A3;stroke-width:0.5;stroke-miterlimit:10;}
160
+ .st154{opacity:0.2;fill:none;stroke:#677380;stroke-width:0.3564;stroke-miterlimit:10;stroke-dasharray:1.0182,1.0182;}
161
+ .st155{fill:none;stroke:#DDE0E4;stroke-width:0.765;stroke-miterlimit:10;}
162
+ .st156{fill:url(#SVGID_130_);}
163
+ .st157{fill:url(#SVGID_131_);}
164
+ .st158{fill:#B1BAC4;}
165
+ .st159{fill:#CBD1D8;}
166
+ .st160{fill:#0B1B2B;}
167
+ .st161{fill:#91D119;}
168
+ .st162{opacity:0.7;}
169
+ .st163{fill:#FFFFFF;stroke:#000000;stroke-width:0.4418;stroke-miterlimit:10;}
170
+ .st164{fill:none;stroke:#939CAA;stroke-width:0.2209;stroke-miterlimit:10;}
171
+ .st165{fill:none;stroke:#FFFFFF;stroke-width:3.0924;stroke-miterlimit:10;}
172
+ .st166{fill:url(#SVGID_132_);}
173
+ .st167{fill:none;stroke:url(#SVGID_133_);stroke-width:1.714;stroke-miterlimit:10;}
174
+ .st168{fill:url(#SVGID_134_);}
175
+ .st169{fill:url(#SVGID_135_);}
176
+ .st170{fill:url(#SVGID_136_);}
177
+ .st171{fill:url(#SVGID_137_);}
178
+ .st172{fill:url(#SVGID_138_);}
179
+ .st173{fill:url(#SVGID_139_);}
180
+ .st174{fill:url(#SVGID_140_);}
181
+ .st175{fill:url(#SVGID_141_);}
182
+ .st176{fill:url(#SVGID_142_);}
183
+ .st177{fill:url(#SVGID_143_);}
184
+ .st178{fill:url(#SVGID_144_);}
185
+ .st179{fill:none;stroke:#1F63EC;stroke-width:4;stroke-miterlimit:10;}
186
+ .st180{fill:none;stroke:#0B1B2B;stroke-width:4;stroke-miterlimit:10;}
187
+ .st181{fill:none;stroke:#677380;stroke-width:0.3989;stroke-miterlimit:10;}
188
+ .st182{fill:none;stroke:#677380;stroke-width:0.3989;stroke-miterlimit:10;stroke-dasharray:1.14,1.14;}
189
+ .st183{fill:#257AF1;}
190
+ .st184{opacity:0.3;fill:#FFFFFF;}
191
+ .st185{fill:none;stroke:#98A5B2;stroke-width:4;stroke-miterlimit:10;}
192
+ .st186{fill:none;stroke:#65727F;stroke-width:0.3989;stroke-miterlimit:10;}
193
+ .st187{fill:none;stroke:#65727F;stroke-width:0.3989;stroke-miterlimit:10;stroke-dasharray:1.14,1.14;}
194
+ .st188{fill:none;stroke:#DDDFE4;stroke-width:0.75;stroke-miterlimit:10;}
195
+ .st189{fill:#9A9EA2;}
196
+ .st190{fill-rule:evenodd;clip-rule:evenodd;fill:#3267AC;}
197
+ .st191{fill:#FFFFFF;stroke:#AFB8C3;stroke-width:0.275;stroke-miterlimit:10;}
198
+ .st192{fill:#C5694E;}
199
+ .st193{fill:#8192A2;}
200
+ .st194{fill:#2D2D2D;stroke:#FFFFFF;stroke-width:0.6317;stroke-miterlimit:10;}
201
+ </style>
202
+ <g id="图层_2">
203
+ </g>
204
+ <g id="图层_1">
205
+ <path class="st194" d="M24.51,28.51H5.49c-2.21,0-4-1.79-4-4V5.49c0-2.21,1.79-4,4-4h19.03c2.21,0,4,1.79,4,4v19.03
206
+ C28.51,26.72,26.72,28.51,24.51,28.51z"/>
207
+ <g>
208
+ <g>
209
+ <g>
210
+ <g>
211
+ <path class="st23" d="M15.47,7.1l-1.3,1.85c-0.2,0.29-0.54,0.47-0.9,0.47h-7.1V7.09C6.16,7.1,15.47,7.1,15.47,7.1z"/>
212
+ <polygon class="st23" points="24.3,7.1 13.14,22.91 5.7,22.91 16.86,7.1 "/>
213
+ <path class="st23" d="M14.53,22.91l1.31-1.86c0.2-0.29,0.54-0.47,0.9-0.47h7.09v2.33H14.53z"/>
214
+ </g>
215
+ </g>
216
+ </g>
217
+ </g>
218
+ </g>
219
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.36.0",
3
+ "version": "2.38.0",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -56,6 +56,7 @@ import {
56
56
  cursorToolsForActivePrompt,
57
57
  buildCursorToolGuidanceSystemNote,
58
58
  buildCursorToolDefinitions,
59
+ cursorToolWireName,
59
60
  cursorRequestHasShellAlias,
60
61
  CURSOR_SHELL_ALIAS_SYSTEM_NOTE,
61
62
  OCX_RESPONSES_TOOL_PROVIDER,
@@ -1091,7 +1092,9 @@ function toolCallStep(
1091
1092
  ): Uint8Array {
1092
1093
  const args: Record<string, Uint8Array> = {};
1093
1094
  for (const [key, value] of Object.entries(part.arguments ?? {})) args[key] = argBytes(value);
1094
- const toolName = namespacedToolName(part.namespace, part.name);
1095
+ // Replay the same provider-isolated identity advertised in this request. Returned calls are
1096
+ // restored to the client name, so transcript parts carry the client name again on the next turn.
1097
+ const toolName = cursorToolWireName(part);
1095
1098
  const decodedResult = result ? decodeResultParts(result) : undefined;
1096
1099
  const serialize = (maxImages: number): Uint8Array => toBinary(ConversationStepSchema, create(ConversationStepSchema, {
1097
1100
  message: {
@@ -12,6 +12,7 @@ export const CODEX_SHELL_COMMAND_TOOL = "shell_command";
12
12
  export const CODEX_UNIFIED_EXEC_TOOL = "exec";
13
13
  export const CODEX_WAIT_TOOL = "wait";
14
14
  export const CODEX_APPLY_PATCH_TOOL = "apply_patch";
15
+ export const CODEX_TOOL_SEARCH_TOOL = "tool_search";
15
16
  export const CURSOR_EDIT_FILE_TOOL = "edit_file";
16
17
  export const CURSOR_MULTI_EDIT_TOOL = "multi_edit";
17
18
  export const CURSOR_STRUCTURED_EDIT_TOOLS = [CURSOR_EDIT_FILE_TOOL, CURSOR_MULTI_EDIT_TOOL] as const;
@@ -169,8 +170,10 @@ function cursorToolChoiceMatches(
169
170
  }
170
171
  return tool.name === choiceName || cursorToolWireName(tool) === choiceName;
171
172
  }
172
- if (tool.name === choiceName || cursorToolWireName(tool) === choiceName) return true;
173
- return cursorToolChoiceAliases(tool).includes(choiceName);
173
+ if (tool.name === choiceName) return true;
174
+ if (cursorToolChoiceAliases(tool).includes(choiceName)) return true;
175
+ return cursorToolWireName(tool) === choiceName
176
+ && !catalog.some(candidate => candidate.name === choiceName);
174
177
  }
175
178
 
176
179
  export function isBareCodexShellBridgeTool(tool: Pick<OcxTool, "namespace" | "name">): boolean {
@@ -320,10 +323,39 @@ export function cursorRequestAdvertisesStructuredEdits(
320
323
  return cursorStructuredEditTools(tools, toolChoice).length > 0;
321
324
  }
322
325
 
326
+ const CURSOR_CLIENT_TOOL_WIRE_PREFIX = "ocx_client_";
327
+ const CURSOR_PROXY_OWNED_BARE_TOOL_NAMES = new Set([
328
+ CODEX_UNIFIED_EXEC_TOOL,
329
+ CODEX_WAIT_TOOL,
330
+ CODEX_EXEC_COMMAND_TOOL,
331
+ CODEX_SHELL_COMMAND_TOOL,
332
+ CODEX_APPLY_PATCH_TOOL,
333
+ CURSOR_EDIT_FILE_TOOL,
334
+ CURSOR_MULTI_EDIT_TOOL,
335
+ CODEX_TOOL_SEARCH_TOOL,
336
+ ]);
337
+
338
+ /** Avoid collisions with Cursor's private bare-tool namespace. */
339
+ function isCursorBareClientToolWireAliased(
340
+ tool: Pick<OcxTool, "namespace" | "name">,
341
+ ): boolean {
342
+ return !tool.namespace
343
+ && !CURSOR_PROXY_OWNED_BARE_TOOL_NAMES.has(tool.name);
344
+ }
345
+
323
346
  export function cursorToolWireName(tool: Pick<OcxTool, "namespace" | "name">): string {
347
+ if (isCursorBareClientToolWireAliased(tool)) {
348
+ return `${CURSOR_CLIENT_TOOL_WIRE_PREFIX}${tool.name}`;
349
+ }
324
350
  return namespacedToolName(tool.namespace, tool.name);
325
351
  }
326
352
 
353
+ function clientSemanticToolNameFromCursorWire(name: string): string {
354
+ return name.startsWith(CURSOR_CLIENT_TOOL_WIRE_PREFIX)
355
+ ? name.slice(CURSOR_CLIENT_TOOL_WIRE_PREFIX.length)
356
+ : name;
357
+ }
358
+
327
359
  /**
328
360
  * Cursor's harness shows MCP tools to the model as `mcp_<providerIdentifier>_<toolName>`; models
329
361
  * sometimes call that display name verbatim instead of the advertised short name (live 20:41/21:00
@@ -591,7 +623,7 @@ function quotedNames(names: readonly string[]): string {
591
623
  }
592
624
 
593
625
  function advertisedCoversNeighbor(wireNames: readonly string[], neighbor: (typeof NEIGHBOR_AGENT_TOOL_NAMES)[number]): boolean {
594
- const advertised = new Set(wireNames.map(name => name.toLowerCase()));
626
+ const advertised = new Set(wireNames.map(name => clientSemanticToolNameFromCursorWire(name).toLowerCase()));
595
627
  if (advertised.has(neighbor.toLowerCase())) return true;
596
628
  return NEIGHBOR_AGENT_TOOL_ALIASES[neighbor].some(alias => advertised.has(alias.toLowerCase()));
597
629
  }
@@ -602,7 +634,7 @@ function unavailableNeighborAgentToolNames(wireNames: readonly string[]): string
602
634
 
603
635
  function discoveryToolLabel(wireNames: readonly string[]): string | undefined {
604
636
  const labels: string[] = [];
605
- if (wireNames.includes("tool_search")) labels.push("`tool_search`");
637
+ if (wireNames.includes(CODEX_TOOL_SEARCH_TOOL)) labels.push(`\`${CODEX_TOOL_SEARCH_TOOL}\``);
606
638
  if (wireNames.some(name => name.startsWith("mcp__"))) labels.push("MCP");
607
639
  if (wireNames.some(name => /resource/i.test(name))) labels.push("resource discovery");
608
640
  return labels.length > 0 ? labels.join(", ") : undefined;
@@ -16,8 +16,24 @@ export const KIRO_BUILDER_ID_SERVICE_PROFILE_ARN =
16
16
  "arn:aws:codewhisperer:us-east-1:638616132270:profile/AAAACCCCXXXX";
17
17
  export const KIRO_CONTINUATION_MESSAGE =
18
18
  "Continue from the prior conversation. Do not quote or mention this instruction.";
19
+ /**
20
+ * Sent when a `required`-mode turn ended without calling the completion tool.
21
+ *
22
+ * This is the one instruction the model reads at the exact moment it failed to complete, so its
23
+ * wording decides the next move. It used to end with "Do not ask the user for another task or emit
24
+ * another progress-only message", which was written to stop soliciting NEW work but reads as a
25
+ * blanket ban on asking anything. Combined with a contract that only described "still working" and
26
+ * "fully done", a model holding a genuine question had no endorsed move left except to keep
27
+ * working — so it wrote the question as prose and answered it itself in the same inference. The
28
+ * refusal is now scoped to a new task, and the blocked-on-user case is routed to the channel that
29
+ * already terminates the turn correctly.
30
+ *
31
+ * The condition names a decision, information, AND clarification on purpose. A model blocked on a
32
+ * missing account id or an ambiguous path is stuck exactly as hard as one blocked on a choice, and
33
+ * "decision" alone would leave that common class of question with no endorsed move again.
34
+ */
19
35
  export const KIRO_COMPLETION_RETRY_MESSAGE =
20
- `Continue the existing task without quoting this instruction. If the task is complete, call ${KIRO_COMPLETION_TOOL_NAME} now with the complete final answer. Otherwise issue the next real tool call now. Do not ask the user for another task or emit another progress-only message.`;
36
+ `Continue the existing task without quoting this instruction. If the task is complete, call ${KIRO_COMPLETION_TOOL_NAME} now with the complete final answer. If you cannot continue until the user supplies a decision, information, or a clarification that only they can give, call ${KIRO_COMPLETION_TOOL_NAME} now with that question as the answer. Otherwise issue the next real tool call now. Do not solicit a new task and do not emit another progress-only message.`;
21
37
 
22
38
  export const KIRO_TOOL_RESULT_CARRIER_MESSAGE = "The requested tool result is attached.";
23
39
  export const KIRO_EMPTY_TOOL_RESULT_MESSAGE = "The tool completed without textual output.";
@@ -34,8 +50,26 @@ export const KIRO_EMPTY_TOOL_RESULT_MESSAGE = "The tool completed without textua
34
50
  export const KIRO_ANSWER_DELIVERED_MESSAGE =
35
51
  "The previous final answer was delivered to the user and that task is closed. No new request has been made yet. Do not repeat, revise, or continue that work; wait for the user's next instruction.";
36
52
 
53
+ /**
54
+ * The prose half of the `required`-mode completion contract.
55
+ *
56
+ * The closing clause exists because the earlier contract described two states — still working, and
57
+ * fully done — for a model that has three. Prose does not end the turn, the completion tool means
58
+ * the task is finished, and a real tool call continues the turn, so "I need a decision before I can
59
+ * go on" was not expressible. Measured outcome: a model with a pending question emitted the question
60
+ * and an overriding answer as one merged message, then called another tool 4ms later in the same
61
+ * inference, which reads to the user as an agent working past its own final answer.
62
+ *
63
+ * The clause is unconditional on purpose. It names only the completion tool, which is always
64
+ * advertised whenever this instruction is injected, so it can never point at something the model
65
+ * cannot call — the failure mode `0325a5afd` fixed for the shared catalog nudge.
66
+ *
67
+ * Its condition is deliberately wider than a decision: information and clarification are named too,
68
+ * because "what is the account id" and "which of these paths did you mean" block progress the same
69
+ * way a choice does, and a narrower trigger would silently exclude them.
70
+ */
37
71
  export const KIRO_COMPLETION_INSTRUCTIONS =
38
- `When tools are available, ordinary assistant text is mid-task commentary and does not end the turn. Continue using tools after progress updates. When the task is fully complete and no more tool calls are needed, call ${KIRO_COMPLETION_TOOL_NAME} exactly once with the complete user-facing final answer in \`answer\`. Do not provide the final answer as ordinary assistant text.`;
72
+ `When tools are available, ordinary assistant text is mid-task commentary and does not end the turn. Continue using tools after progress updates. When the task is fully complete and no more tool calls are needed, call ${KIRO_COMPLETION_TOOL_NAME} exactly once with the complete user-facing final answer in \`answer\`. Do not provide the final answer as ordinary assistant text. This completion tool is not an ordinary work tool. When the task is complete, call it instead of emitting answer-shaped ordinary assistant text. The call is terminal and is the exception to generic tool-result counting: it is complete when issued, ends the turn, returns no tool result, and no text or tool call may follow it. If you cannot continue until the user supplies a decision, information, or a clarification that only they can give, that question is your final answer: call ${KIRO_COMPLETION_TOOL_NAME} with the question and stop. Do not write the question as ordinary text and then answer it yourself.`;
39
73
 
40
74
  export type KiroCompletionMode = "disabled" | "required" | "text_fallback";
41
75
 
@@ -469,14 +469,25 @@ function kiroCompletionTool(): Record<string, unknown> {
469
469
  return {
470
470
  toolSpecification: {
471
471
  name: KIRO_COMPLETION_TOOL_NAME,
472
- description: "Finish the task and return the complete user-facing final answer. Call only when no more work or tool calls are needed.",
472
+ // The shared tool-catalog nudge enumerates this name next to ordinary tools and tells every
473
+ // listed name to count a call only after its tool result returns. Nothing returns a result
474
+ // here: a valid call becomes the turn's terminal. Left undescribed, the model reads one more
475
+ // deferrable work tool and keeps calling tools with a finished answer already written as
476
+ // commentary. So the description states the distinction, the obligation, and the terminality
477
+ // where the model is actually choosing between tools.
478
+ //
479
+ // It also has to name the blocked-on-user state, for the same reason the prose contract does.
480
+ // This is the surface the model reads while CHOOSING; if it admits only "fully complete", a
481
+ // model holding a question that blocks progress reads this tool as unavailable and keeps
482
+ // working instead, which is the measured defect. The two surfaces must not disagree.
483
+ description: "Terminal completion channel, not an ordinary work tool. When the task is fully complete and no more work or tool calls are needed, you must call this tool exactly once instead of providing the final answer as ordinary assistant text. Call it the same way when you cannot continue until the user supplies a decision, information, or a clarification that only they can give: the question itself is the answer. Put the complete user-facing final answer in `answer`. The call is complete when issued: it ends the turn, returns no tool result, and no text or tool call may follow it.",
473
484
  inputSchema: {
474
485
  json: {
475
486
  type: "object",
476
487
  properties: {
477
488
  answer: {
478
489
  type: "string",
479
- description: "The complete final answer to show the user.",
490
+ description: "The complete final answer to show the user, or the blocking question you need the user to answer before you can continue.",
480
491
  },
481
492
  },
482
493
  required: ["answer"],
@@ -418,6 +418,20 @@ export const CAPABILITIES: readonly Capability[] = [
418
418
  "The GUI reads this state directly; without a verb an agent could not tell whether the Codex app-server was reachable at all.",
419
419
  ],
420
420
  },
421
+ {
422
+ command: ["system", "codex-cli-update", "check"],
423
+ summary: "Inspect a configured Codex CLI candidate and its ownership provenance.",
424
+ routes: [],
425
+ flags: [{ name: "--json", value: "boolean", summary: "Emit the redacted provenance report as JSON." }],
426
+ mutates: false,
427
+ json: "envelope",
428
+ details: [
429
+ "Proof-bound published-launcher context authenticates the configured candidate snapshot, not successful Codex execution; this check does not attest or admit a selected runtime.",
430
+ "On Windows this first slice performs no candidate or configuration filesystem I/O: only a proof-captured absolute environment candidate can receive lexical app-bundle or version-manager labels; every other Windows candidate fails closed.",
431
+ "Makes no package-registry request.",
432
+ "Does not execute Codex or npm, install or repair software, control a process, or write configuration or cache state.",
433
+ ],
434
+ },
421
435
  {
422
436
  command: ["system", "codex-restart"],
423
437
  summary: "Restart the Codex app-server.",
package/src/cli/claude.ts CHANGED
@@ -132,6 +132,18 @@ export function buildClaudeEnv(
132
132
  const replacement = `http://127.0.0.1:${port}`;
133
133
  console.error(`⚠ Replacing stale opencodex ANTHROPIC_BASE_URL ${parsed.origin} with ${replacement}.`);
134
134
  env.ANTHROPIC_BASE_URL = replacement;
135
+ // The credentials in this environment were paired with the destination we just
136
+ // replaced. An admission secret minted by that other proxy is not valid here, and
137
+ // leaving it in place makes Claude Code authenticate as a host-managed provider
138
+ // instead of using its own subscription OAuth — the launch then bypasses the
139
+ // subscription-preserving default below. Only OUR OWN admission forms are dropped:
140
+ // a genuine user `sk-ant-` credential is upstream auth that native passthrough
141
+ // needs (server/claude-messages.ts), so it must survive the destination rewrite.
142
+ for (const slot of ["ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY"] as const) {
143
+ const value = env[slot]?.trim();
144
+ if (!value) continue;
145
+ if (value === PROXY_MARKER || isProxyAdmissionSecret(value, config)) delete env[slot];
146
+ }
135
147
  }
136
148
  } catch {
137
149
  // Preserve user-provided values that are not parseable URLs.
@@ -0,0 +1,96 @@
1
+ import {
2
+ inspectCodexCliInstall,
3
+ type CodexCliInstallProvenanceDeps,
4
+ type CodexCliInstallReport,
5
+ } from "../codex/cli-install-provenance";
6
+ import { CliUsageError, isJsonOption, printData, runCliAction } from "./runtime-api";
7
+ import { trustedNodeLauncherContext } from "./launcher-context";
8
+
9
+ export const CODEX_CLI_UPDATE_USAGE = `Usage:
10
+ ocx system codex-cli-update check [--json]`;
11
+
12
+ export type ParsedCodexCliUpdateArgs = Readonly<{
13
+ json: boolean;
14
+ }>;
15
+
16
+ export interface CodexCliUpdateCommandDeps {
17
+ readonly inspectInstall?: (deps: CodexCliInstallProvenanceDeps) => Promise<CodexCliInstallReport>;
18
+ }
19
+
20
+ function installSummary(report: CodexCliInstallReport): string[] {
21
+ return [
22
+ `candidate: ${report.candidateAvailable ? "yes" : "no"}`,
23
+ `candidate-source: ${report.candidateSource ?? "unavailable"}`,
24
+ `selection-attested: ${report.selectionAttested ? "yes" : "no"}`,
25
+ `provenance: ${report.provenance}`,
26
+ `managed: ${report.managed ? "yes" : "no"}`,
27
+ `reason: ${report.reason}`,
28
+ `candidate-version: ${report.candidateVersion ?? "unavailable"}`,
29
+ `package-version: ${report.packageVersion ?? "unavailable"}`,
30
+ `version-evidence: ${report.versionEvidence.kind}`,
31
+ `location: ${report.location ?? "unavailable"}`,
32
+ `shim: ${report.shim.status}${report.shim.backingKind ? `/${report.shim.backingKind}` : ""}`,
33
+ ];
34
+ }
35
+
36
+ export function parseCodexCliUpdateArgs(argv: readonly string[]): ParsedCodexCliUpdateArgs {
37
+ // `--json` is accepted in any argv position CLI-wide, so remove it before positional
38
+ // validation. Requiring `check` at index 0 first would reject `--json check`, which
39
+ // automation that puts output flags ahead of the subcommand legitimately produces.
40
+ let json = false;
41
+ const positional: string[] = [];
42
+ for (const token of argv) {
43
+ if (isJsonOption(token)) {
44
+ if (json) throw new CliUsageError("--json may be specified only once", CODEX_CLI_UPDATE_USAGE);
45
+ json = true;
46
+ continue;
47
+ }
48
+ positional.push(token);
49
+ }
50
+ if (positional[0] !== "check") {
51
+ throw new CliUsageError("codex-cli-update action must be check", CODEX_CLI_UPDATE_USAGE);
52
+ }
53
+ if (positional.length > 1) {
54
+ throw new CliUsageError("unsupported codex-cli-update argument", CODEX_CLI_UPDATE_USAGE);
55
+ }
56
+ return Object.freeze({ json });
57
+ }
58
+
59
+ export async function handleCodexCliUpdateCommand(
60
+ argv: readonly string[],
61
+ deps: CodexCliUpdateCommandDeps = {},
62
+ ): Promise<number> {
63
+ let parsed: ParsedCodexCliUpdateArgs;
64
+ try {
65
+ parsed = parseCodexCliUpdateArgs(argv);
66
+ } catch (error) {
67
+ if (error instanceof CliUsageError) {
68
+ console.error(`Error: ${error.message}`);
69
+ console.error(error.usage ?? CODEX_CLI_UPDATE_USAGE);
70
+ return 2;
71
+ }
72
+ throw error;
73
+ }
74
+ return runCliAction(async () => {
75
+ const trustedInspectionEnv = trustedNodeLauncherContext()?.codexCliInspectionEnv;
76
+ const inspectionDeps: CodexCliInstallProvenanceDeps = trustedInspectionEnv
77
+ && trustedInspectionEnv.managerRoots !== null ? {
78
+ env: {
79
+ ...trustedInspectionEnv.managerRoots,
80
+ CODEX_CLI_PATH: trustedInspectionEnv.codexCliPath ?? undefined,
81
+ PATH: trustedInspectionEnv.path ?? undefined,
82
+ PATHEXT: trustedInspectionEnv.pathExt ?? undefined,
83
+ },
84
+ configDir: trustedInspectionEnv.configDir,
85
+ // This is a fresh one-shot CLI process. Its proof-bound launcher snapshot
86
+ // supplies configured candidate evidence, not selected-runtime admission.
87
+ } : {
88
+ // Direct Bun/source launches have no pre-dotenv proof. Do not inspect
89
+ // ambient or persisted candidate state at all.
90
+ env: { PATH: "" },
91
+ configDir: ".",
92
+ };
93
+ const report = await (deps.inspectInstall ?? inspectCodexCliInstall)(inspectionDeps);
94
+ printData(report, parsed.json, installSummary(report));
95
+ });
96
+ }
@@ -19,6 +19,9 @@ export function skipsCodexShimAutoRestore(command: string | undefined, args: str
19
19
  if (command === "uninstall" || command === "remove") return true;
20
20
  // `lab` is read-only inspection; it must not trigger shim side effects.
21
21
  if (command === "lab") return true;
22
+ // The entire updater-inspection namespace is zero-effect, including malformed
23
+ // or future actions. A later `apply` implementation must own its preflight.
24
+ if (command === "system" && args[1] === "codex-cli-update") return true;
22
25
  return command === "codex-shim" && ["install", "uninstall", "remove"].includes(args[1] ?? "");
23
26
  }
24
27
 
@@ -80,28 +80,24 @@ function hasContextLimit(model: ExportModel): boolean {
80
80
  *
81
81
  * `opencodeCatalogFromProxyRows` owns the visibility rules (drop `disabled`, drop dupes,
82
82
  * drop native under Codex Direct) — the export core does none of that, so a row filtered
83
- * here is the only thing keeping a disabled model out of a client's picker. Modalities are
84
- * re-joined by `namespaced` because the launcher's catalog type does not carry them.
83
+ * here is the only thing keeping a disabled model out of a client's picker. It also carries
84
+ * the effort ladder, so the ladder a client receives comes from the same filtered, deduped
85
+ * row as the model itself: a second lookup over the raw rows would let a hidden or disabled
86
+ * duplicate donate its ladder to the visible entry.
87
+ *
88
+ * Only modalities are re-joined by `namespaced`, because the catalog type does not carry them.
85
89
  */
86
90
  export function exportModelsFromProxyRows(
87
91
  rows: readonly ExportProxyModelRow[],
88
92
  config: OcxConfig,
89
93
  ): ExportModel[] {
90
- const metadata = new Map<string, Pick<ExportModel, "inputModalities" | "reasoningEfforts" | "defaultReasoningEffort">>();
94
+ const modalities = new Map<string, string[]>();
91
95
  for (const row of rows) {
92
96
  const namespaced = row.namespaced?.trim();
93
- if (!namespaced || metadata.has(namespaced)) continue;
94
- metadata.set(namespaced, {
95
- ...(Array.isArray(row.inputModalities) && row.inputModalities.length > 0
96
- ? { inputModalities: [...row.inputModalities] }
97
- : {}),
98
- ...(Array.isArray(row.reasoningEfforts) && row.reasoningEfforts.length > 0
99
- ? { reasoningEfforts: [...row.reasoningEfforts] }
100
- : {}),
101
- ...(typeof row.defaultReasoningEffort === "string" && row.defaultReasoningEffort.length > 0
102
- ? { defaultReasoningEffort: row.defaultReasoningEffort }
103
- : {}),
104
- });
97
+ if (!namespaced || modalities.has(namespaced)) continue;
98
+ if (Array.isArray(row.inputModalities) && row.inputModalities.length > 0) {
99
+ modalities.set(namespaced, [...row.inputModalities]);
100
+ }
105
101
  }
106
102
  return opencodeCatalogFromProxyRows(rows, config).map(entry => {
107
103
  const model: ExportModel = {
@@ -112,7 +108,12 @@ export function exportModelsFromProxyRows(
112
108
  if (entry.native) model.native = true;
113
109
  if (entry.displayName) model.displayName = entry.displayName;
114
110
  if (entry.contextWindow !== undefined) model.contextWindow = entry.contextWindow;
115
- Object.assign(model, metadata.get(entry.namespaced));
111
+ if (entry.reasoningEfforts && entry.reasoningEfforts.length > 0) {
112
+ model.reasoningEfforts = [...entry.reasoningEfforts];
113
+ }
114
+ if (entry.defaultReasoningEffort) model.defaultReasoningEffort = entry.defaultReasoningEffort;
115
+ const input = modalities.get(entry.namespaced);
116
+ if (input) model.inputModalities = [...input];
116
117
  return model;
117
118
  });
118
119
  }
@@ -203,7 +204,7 @@ export async function handleExportCommand(argv: string[], deps: ExportCommandDep
203
204
  "",
204
205
  ...(out !== undefined ? [`Wrote ${out}`] : []),
205
206
  `Destination: ${spec.destination(process.env)}`,
206
- "Merge this provider block into that file; do not replace it.",
207
+ "Merge this generated configuration into that file; do not replace it.",
207
208
  `Before launching: ${spec.exportHint}`,
208
209
  `${models.length} model${models.length === 1 ? "" : "s"}; ${degraded} omit context limits (the client applies its own defaults).`,
209
210
  ]);
package/src/cli/help.ts CHANGED
@@ -73,10 +73,10 @@ Usage:
73
73
  ocx memory [--json] Alias of ocx observe memory
74
74
  ocx api-key <sub> Alias of ocx access key
75
75
  ocx access <sub> External API keys and endpoint information
76
- ocx export --client <id> Print a client config wired to the running proxy (11 clients)
76
+ ocx export --client <id> Print a client config wired to the running proxy (12 clients)
77
77
  ocx integration client <sub> Enable, disable, inspect or roll back a client integration
78
78
  ocx grok <sub> Grok Build model selection and apply
79
- ocx system <sub> Runtime settings, startup, sync, and updates
79
+ ocx system <sub> Runtime settings, startup, sync, OpenCodex updates, and Codex CLI inspection
80
80
  ocx config <sub> Validated configuration show/get/set/import/export
81
81
  ocx lab <sub> Read-only Compatibility Lab projection inspection
82
82
  ocx claude [args...] Launch Claude Code wired to the proxy (model discovery on)
package/src/cli/index.ts CHANGED
@@ -363,11 +363,12 @@ async function handleStart(options: { block?: boolean } = {}) {
363
363
  shutdownStartedAt = now;
364
364
  console.log("\nšŸ›‘ Shutting down opencodex proxy...");
365
365
  void (async () => {
366
+ let shutdownSucceeded = false;
366
367
  try {
367
- await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
368
+ shutdownSucceeded = await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
368
369
  } finally {
369
370
  const restored = syncCleanup(); // idempotent (cleaned-guard); also re-run by process.on("exit")
370
- process.exit(restored ? 0 : 1);
371
+ process.exit(restored && shutdownSucceeded ? 0 : 1);
371
372
  }
372
373
  })();
373
374
  };