@bitkyc08/opencodex 2.64.0 → 2.65.0-preview.20260925

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 (208) hide show
  1. package/README.md +28 -25
  2. package/bin/ocx.mjs +16 -2
  3. package/gui/dist/assets/App-8NMiZxT0.css +1 -0
  4. package/gui/dist/assets/App-CsYvvpr3.js +50 -0
  5. package/gui/dist/assets/Tray-DUvc_Wul.js +1 -0
  6. package/gui/dist/assets/{index-DiBRuK-d.css → index--EWgGQvZ.css} +1 -1
  7. package/gui/dist/assets/{index-SggB6t3z.js → index-BB0iHG8-.js} +12 -12
  8. package/gui/dist/assets/tray-data-f0lTZ4sF.js +1 -0
  9. package/gui/dist/index.html +2 -2
  10. package/package.json +1 -4
  11. package/src/adapters/anthropic.ts +30 -1
  12. package/src/adapters/claude-cli/adapter.ts +218 -0
  13. package/src/adapters/claude-cli/profiles.ts +38 -0
  14. package/src/adapters/codebuddy/profiles.ts +2 -0
  15. package/src/adapters/coding-agent/profile.ts +12 -4
  16. package/src/adapters/coding-agent/turn.ts +16 -6
  17. package/src/adapters/command-code-tool-text.ts +133 -23
  18. package/src/adapters/cursor/catalog.ts +4 -8
  19. package/src/adapters/cursor/discovery.ts +10 -5
  20. package/src/adapters/cursor/effort-map.ts +2 -6
  21. package/src/adapters/cursor/envelope-echo.ts +10 -5
  22. package/src/adapters/cursor/request-builder.ts +9 -4
  23. package/src/adapters/cursor/thread-continuity.ts +53 -0
  24. package/src/adapters/cursor.ts +29 -13
  25. package/src/adapters/devin/cloud-direct/chat.ts +3 -1
  26. package/src/adapters/google-tool-schema.ts +26 -3
  27. package/src/adapters/identity.ts +214 -2
  28. package/src/adapters/openai-chat/passthrough.ts +1 -0
  29. package/src/adapters/openai-chat/serialized-tool-call-content.ts +569 -0
  30. package/src/adapters/openai-chat.ts +43 -15
  31. package/src/adapters/openai-responses/canonical-forward.ts +17 -10
  32. package/src/adapters/openai-responses/passthrough.ts +19 -4
  33. package/src/adapters/openai-responses/request-strips.ts +25 -0
  34. package/src/adapters/openai-responses/web-search.ts +22 -4
  35. package/src/adapters/qoder/profiles.ts +2 -0
  36. package/src/adapters/registry.ts +8 -0
  37. package/src/adapters/run-turn-queue.ts +7 -0
  38. package/src/bridge/response-json.ts +6 -4
  39. package/src/bridge/sse.ts +7 -5
  40. package/src/claude/alias.ts +73 -26
  41. package/src/claude/context-windows.ts +15 -6
  42. package/src/claude/desktop-3p-library.ts +3 -2
  43. package/src/claude/desktop-3p.ts +1 -1
  44. package/src/claude/desktop-first-party.ts +63 -22
  45. package/src/claude/desktop-picker-profile.ts +302 -0
  46. package/src/claude/desktop-picker.ts +365 -0
  47. package/src/claude/desktop-risk.ts +20 -0
  48. package/src/claude/gateway-cache.ts +13 -4
  49. package/src/claude/inbound.ts +5 -2
  50. package/src/claude/intercept/connect-proxy.ts +106 -29
  51. package/src/claude/intercept/local-ca.ts +76 -18
  52. package/src/claude/intercept/model-bindings.ts +145 -0
  53. package/src/claude/intercept/picker-bootstrap.ts +141 -0
  54. package/src/claude/intercept/picker-ca.ts +126 -0
  55. package/src/claude/intercept/picker-listener.ts +213 -0
  56. package/src/claude/intercept/picker-models.ts +101 -0
  57. package/src/claude/intercept/picker-runtime.ts +330 -0
  58. package/src/claude/intercept/picker-trust.ts +125 -0
  59. package/src/claude/intercept/runtime.ts +149 -2
  60. package/src/claude/model-info.ts +18 -4
  61. package/src/cli/agent.ts +16 -2
  62. package/src/cli/capabilities.ts +71 -2
  63. package/src/cli/claude-desktop.ts +319 -33
  64. package/src/cli/claude.ts +15 -7
  65. package/src/cli/codex-shim-autorestore.ts +1 -0
  66. package/src/cli/dispatch.ts +29 -23
  67. package/src/cli/effort.ts +16 -6
  68. package/src/cli/ensure-desired-integrations.ts +29 -4
  69. package/src/cli/ready.ts +1 -1
  70. package/src/cli/registry.ts +8 -0
  71. package/src/cli/restart-scope.ts +9 -1
  72. package/src/cli/runtime-api.ts +17 -0
  73. package/src/cli/system-command.ts +11 -12
  74. package/src/client/connect.ts +12 -2
  75. package/src/client/state.ts +39 -1
  76. package/src/clients/config-export.ts +57 -9
  77. package/src/codex/auth-context.ts +51 -11
  78. package/src/codex/catalog/derive-entry.ts +5 -5
  79. package/src/codex/catalog/gather-capture.ts +25 -6
  80. package/src/codex/catalog/metadata.ts +3 -3
  81. package/src/codex/catalog/parsing.ts +39 -1
  82. package/src/codex/catalog/provider-models.ts +8 -6
  83. package/src/codex/catalog/retained-sync.ts +17 -7
  84. package/src/codex/catalog/sync.ts +2 -2
  85. package/src/codex/desktop-app-restart.ts +8 -0
  86. package/src/codex/desktop-switches.ts +9 -1
  87. package/src/codex/home.ts +43 -4
  88. package/src/codex/inject/config-toml.ts +137 -0
  89. package/src/codex/inject/plan.ts +23 -0
  90. package/src/codex/inject/remove.ts +21 -1
  91. package/src/codex/inject.ts +63 -8
  92. package/src/codex/journal.ts +36 -0
  93. package/src/codex/main-account-hard-lock.ts +14 -1
  94. package/src/codex/main-account-policy-wait.ts +109 -0
  95. package/src/codex/native-profile-startup.ts +24 -6
  96. package/src/codex/native-residue.ts +28 -10
  97. package/src/codex/quota-types.ts +8 -1
  98. package/src/codex/quota.ts +49 -7
  99. package/src/codex/runtime.ts +31 -10
  100. package/src/combos/failover.ts +7 -6
  101. package/src/combos/resolve.ts +91 -14
  102. package/src/combos/types.ts +18 -1
  103. package/src/config/load-degrade.ts +2 -0
  104. package/src/config/schema/config-schema.ts +21 -2
  105. package/src/config/schema/leaf-validators.ts +6 -3
  106. package/src/config/subagent-models.ts +3 -1
  107. package/src/generated/compatibility-version.json +275 -163
  108. package/src/images/artifacts.ts +14 -0
  109. package/src/integrations/owned-refresh.ts +1 -1
  110. package/src/integrations/ownership-policy.ts +32 -1
  111. package/src/integrations/state.ts +3 -1
  112. package/src/integrations/writer.ts +9 -0
  113. package/src/lib/config-ownership.ts +3 -0
  114. package/src/lib/errors.ts +87 -0
  115. package/src/lib/package-tree-integrity.ts +1 -1
  116. package/src/lib/request-execution-budget.ts +42 -14
  117. package/src/lib/request-resend-gate.ts +4 -3
  118. package/src/lib/retry-delay.ts +7 -1
  119. package/src/lib/tool-envelope-echo-filter.ts +236 -0
  120. package/src/lib/upstream-retry.ts +20 -12
  121. package/src/oauth/index.ts +1 -0
  122. package/src/oauth/kiro-credentials.ts +14 -1
  123. package/src/oauth/login-cli.ts +1 -0
  124. package/src/providers/derive.ts +4 -0
  125. package/src/providers/fast-opt-in.ts +31 -0
  126. package/src/providers/model-rename-fields.ts +2 -0
  127. package/src/providers/provider-id-rewrite.ts +2 -0
  128. package/src/providers/quota/vendor-probes-key.ts +8 -2
  129. package/src/providers/registry/entries-core.ts +45 -2
  130. package/src/providers/registry/entries-extended.ts +99 -12
  131. package/src/providers/registry/model-ids.ts +2 -0
  132. package/src/providers/registry/types.ts +7 -1
  133. package/src/providers/resolved-model-policy.ts +11 -4
  134. package/src/providers/service-tier.ts +10 -3
  135. package/src/responses/code-mode-helper-compat.ts +3 -0
  136. package/src/responses/parser.ts +45 -3
  137. package/src/router.ts +6 -0
  138. package/src/server/auth-cors.ts +8 -0
  139. package/src/server/chat-completions.ts +3 -1
  140. package/src/server/claude-messages.ts +69 -14
  141. package/src/server/grok-upstream-envelope-echo.ts +120 -0
  142. package/src/server/index/claude-intercept-lifecycle.ts +24 -0
  143. package/src/server/index/serve-options.ts +2 -2
  144. package/src/server/index.ts +7 -0
  145. package/src/server/management/agent-settings-routes.ts +204 -112
  146. package/src/server/management/claude-desktop-picker-routes.ts +128 -0
  147. package/src/server/management/combo-routes.ts +59 -2
  148. package/src/server/management/config-routes.ts +81 -39
  149. package/src/server/management/context.ts +3 -0
  150. package/src/server/management/native-integration-routes.ts +169 -93
  151. package/src/server/management/provider-routes.ts +46 -4
  152. package/src/server/management/route-registry.ts +10 -0
  153. package/src/server/management/routing-profile-routes.ts +9 -0
  154. package/src/server/management/sidebar-routes.ts +52 -0
  155. package/src/server/management-api.ts +2 -0
  156. package/src/server/proxy-liveness.ts +38 -3
  157. package/src/server/request-log-account-rotation.ts +53 -0
  158. package/src/server/request-log.ts +3 -16
  159. package/src/server/responses/adapter-dispatch.ts +17 -1
  160. package/src/server/responses/agent-task-recovery.ts +49 -26
  161. package/src/server/responses/codex-ws-exchange.ts +18 -7
  162. package/src/server/responses/codex-ws-wire.ts +27 -4
  163. package/src/server/responses/core-combo.ts +21 -3
  164. package/src/server/responses/core-normalize.ts +7 -0
  165. package/src/server/responses/core-opaque-recovery.ts +3 -2
  166. package/src/server/responses/encrypted-payload.ts +15 -14
  167. package/src/server/responses/fetch-helpers.ts +1 -1
  168. package/src/server/responses/input-admission.ts +10 -6
  169. package/src/server/responses/native-response-control.ts +2 -2
  170. package/src/server/responses/passthrough-delivery.ts +182 -12
  171. package/src/server/responses/passthrough-dispatch.ts +102 -44
  172. package/src/server/responses/policy-fallback.ts +3 -0
  173. package/src/server/responses/policy-refusal.ts +67 -0
  174. package/src/server/responses/request-send-budget.ts +4 -0
  175. package/src/server/responses/run-turn-execution.ts +42 -11
  176. package/src/server/responses/ws-upstream.ts +2 -1
  177. package/src/server/system-env-shell.ts +0 -1
  178. package/src/server/system-env.ts +25 -4
  179. package/src/service/cli.ts +34 -3
  180. package/src/tray/assets/opencodex-tray-offline-update.ico +0 -0
  181. package/src/tray/assets/opencodex-tray-online-update.ico +0 -0
  182. package/src/tray/assets/opencodex-tray-warning-update.ico +0 -0
  183. package/src/tray/windows-tray.ps1 +188 -7
  184. package/src/tray/windows.ts +23 -2
  185. package/src/types/config.ts +52 -12
  186. package/src/types/provider.ts +22 -5
  187. package/src/types/tools.ts +11 -5
  188. package/src/types.ts +1 -0
  189. package/src/update/async-check.ts +109 -0
  190. package/src/update/badge.ts +11 -17
  191. package/src/update/check-types.ts +9 -0
  192. package/src/update/desktop-badge.ts +102 -0
  193. package/src/update/index.ts +48 -8
  194. package/src/update/install-detection.d.mts +29 -2
  195. package/src/update/install-detection.mjs +213 -7
  196. package/src/update/job.ts +17 -12
  197. package/src/update/notify.ts +16 -11
  198. package/src/update/pnpm-owner-worker.ts +12 -0
  199. package/src/update/refresh-scheduler.ts +141 -0
  200. package/src/usage/cost.ts +31 -4
  201. package/src/usage/expected-prices.ts +57 -0
  202. package/assets/download-linux.svg +0 -10
  203. package/assets/download-macos.svg +0 -10
  204. package/assets/download-windows.svg +0 -10
  205. package/gui/dist/assets/App-CpuDF3ci.js +0 -50
  206. package/gui/dist/assets/App-I5AnaSLh.css +0 -1
  207. package/gui/dist/assets/Tray-B4uEIa1O.js +0 -1
  208. package/gui/dist/assets/usage-companion-chart-DLbKOJml.js +0 -1
@@ -1,6 +1,33 @@
1
- export type DetectedInstall = "bun" | "npm" | "pnpm" | "source";
1
+ export type DetectedInstall = "bun" | "mise" | "npm" | "pnpm" | "source";
2
+
3
+ export interface MiseInstallOwner {
4
+ tool: string;
5
+ backend: string;
6
+ installPath: string;
7
+ toolRoot: string;
8
+ }
9
+
10
+ export type InstallOwnership =
11
+ | { installer: Exclude<DetectedInstall, "mise"> }
12
+ | {
13
+ installer: "mise";
14
+ owner: MiseInstallOwner | null;
15
+ error?: "metadata_unreadable" | "metadata_inconsistent";
16
+ };
17
+
18
+ export interface InstallDetectionDeps {
19
+ exists?: (path: string) => boolean;
20
+ probe?: (path: string) => "present" | "absent" | "unreadable";
21
+ readFile?: (path: string) => string;
22
+ realpath?: (path: string) => string;
23
+ }
2
24
 
3
25
  export declare function detectInstallFromPath(
4
26
  packagePath: string,
5
- deps?: { exists?: (path: string) => boolean; realpath?: (path: string) => string },
27
+ deps?: InstallDetectionDeps,
6
28
  ): DetectedInstall;
29
+
30
+ export declare function detectInstallOwnershipFromPath(
31
+ packagePath: string,
32
+ deps?: InstallDetectionDeps,
33
+ ): InstallOwnership;
@@ -1,4 +1,29 @@
1
- import { realpathSync } from "node:fs";
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
2
+
3
+ const OPENCODEX_MISE_BACKEND = "npm:@bitkyc08/opencodex";
4
+ const OPENCODEX_MISE_BACKEND_DIR = "npm-bitkyc08-opencodex";
5
+ /** mise's core Node runtime. npm -g under it is an npm install that mise did not make. */
6
+ const MISE_NODE_RUNTIME = { tool: "node", backend: "core:node" };
7
+ const OPENCODEX_PACKAGE_SEGMENT = "/node_modules/@bitkyc08/opencodex";
8
+
9
+ /**
10
+ * @typedef {{
11
+ * tool: string;
12
+ * backend: string;
13
+ * installPath: string;
14
+ * toolRoot: string;
15
+ * }} MiseInstallOwner
16
+ */
17
+
18
+ /**
19
+ * @typedef {{
20
+ * installer: "bun" | "npm" | "pnpm" | "source";
21
+ * } | {
22
+ * installer: "mise";
23
+ * owner: MiseInstallOwner | null;
24
+ * error?: "metadata_unreadable" | "metadata_inconsistent";
25
+ * }} InstallOwnership
26
+ */
2
27
 
3
28
  /**
4
29
  * Infer the package manager from the path of the running package.
@@ -14,7 +39,30 @@ import { realpathSync } from "node:fs";
14
39
  * virtual store.
15
40
  */
16
41
  export function detectInstallFromPath(packagePath, deps = {}) {
17
- const exists = deps.exists;
42
+ return detectInstallOwnershipFromPath(packagePath, deps).installer;
43
+ }
44
+
45
+ /**
46
+ * Infer the outer owner of the running package.
47
+ *
48
+ * mise's npm backend deliberately contains an ordinary npm/aube installation, so
49
+ * package-manager layout alone reports npm. The adjacent backend record is the
50
+ * stronger ownership signal: it identifies the mise alias and canonical backend,
51
+ * while containment proves that the running package belongs to that installation.
52
+ *
53
+ * @param {string} packagePath
54
+ * @param {{
55
+ * exists?: (path: string) => boolean;
56
+ * probe?: (path: string) => "present" | "absent" | "unreadable";
57
+ * readFile?: (path: string) => string;
58
+ * realpath?: (path: string) => string;
59
+ * }} deps
60
+ * @returns {InstallOwnership}
61
+ */
62
+ export function detectInstallOwnershipFromPath(packagePath, deps = {}) {
63
+ const exists = deps.exists ?? existsSync;
64
+ const probe = deps.probe ?? probeMetadata;
65
+ const readFile = deps.readFile ?? (path => readFileSync(path, "utf8"));
18
66
  const candidates = [String(packagePath)];
19
67
  try {
20
68
  const resolved = (deps.realpath ?? realpathSync)(String(packagePath));
@@ -24,17 +72,175 @@ export function detectInstallFromPath(packagePath, deps = {}) {
24
72
  // realpath. The lexical path still carries the evidence when it is available.
25
73
  }
26
74
 
27
- let sawNodeModules = false;
75
+ let detectedManager = "source";
76
+ /** @type {MiseInstallOwner[]} */
77
+ const miseOwners = [];
78
+ /** @type {"metadata_unreadable" | "metadata_inconsistent" | undefined} */
79
+ let miseError;
28
80
  for (const candidate of candidates) {
81
+ const mise = detectMiseOwner(candidate, { probe, readFile });
82
+ if (mise.recognized) {
83
+ if (mise.owner) miseOwners.push(mise.owner);
84
+ else miseError = mise.error;
85
+ }
29
86
  const detected = detectInstallCandidate(candidate, exists);
30
- if (detected === "pnpm" || detected === "bun") return detected;
31
- if (detected === "npm") sawNodeModules = true;
87
+ if (detected === "pnpm" || detected === "bun") detectedManager = detected;
88
+ else if (detected === "npm" && detectedManager === "source") detectedManager = "npm";
89
+ }
90
+ // Any recognized ownership error on either spelling takes precedence over every verified
91
+ // owner. Keeping a command from the other candidate could authorize mutation across a
92
+ // lexical/resolved-path mismatch, so fail closed without recovery guidance.
93
+ if (miseError) return { installer: "mise", owner: null, error: miseError };
94
+ const miseOwner = miseOwners.at(-1);
95
+ if (miseOwner) {
96
+ // The lexical and resolved spellings of one install differ when an ancestor such as the
97
+ // mise data directory is a symlink (macOS /var -> /private/var). Both still name the same
98
+ // physical tool directory and the same backend file, so compare canonical directories;
99
+ // any other disagreement stays inconsistent.
100
+ const realpath = deps.realpath ?? realpathSync;
101
+ const consistent = miseOwners.every(owner =>
102
+ owner.tool === miseOwner.tool
103
+ && owner.backend === miseOwner.backend
104
+ && sameDirectory(owner.toolRoot, miseOwner.toolRoot, realpath)
105
+ );
106
+ return consistent
107
+ ? { installer: "mise", owner: miseOwner }
108
+ : { installer: "mise", owner: null, error: "metadata_inconsistent" };
109
+ }
110
+ return { installer: detectedManager };
111
+ }
112
+
113
+ function parseBackendMetadata(content) {
114
+ const fields = new Map();
115
+ for (const line of String(content).split(/\r?\n/)) {
116
+ const match = /^\s*(short|full)\s*=\s*("(?:[^"\\]|\\.)*"|'[^']*')\s*(?:#.*)?$/.exec(line);
117
+ if (!match) continue;
118
+ if (fields.has(match[1])) return null;
119
+ try {
120
+ fields.set(
121
+ match[1],
122
+ match[2].startsWith('"') ? JSON.parse(match[2]) : match[2].slice(1, -1),
123
+ );
124
+ } catch {
125
+ return null;
126
+ }
32
127
  }
33
- return sawNodeModules ? "npm" : "source";
128
+ const tool = fields.get("short");
129
+ const backend = fields.get("full");
130
+ return typeof tool === "string" && typeof backend === "string"
131
+ ? { tool, backend }
132
+ : null;
133
+ }
134
+
135
+ function detectMiseOwner(packagePath, deps) {
136
+ const windowsPath = /^[A-Za-z]:[\\/]/.test(String(packagePath))
137
+ || String(packagePath).startsWith("\\\\");
138
+ const normalized = (windowsPath ? String(packagePath).replaceAll("\\", "/") : String(packagePath))
139
+ .replace(/\/+$/, "");
140
+ const lower = normalized.toLowerCase();
141
+ let marker = -1;
142
+ let installPath;
143
+ let toolRoot;
144
+ let metadataPath;
145
+ while ((marker = lower.indexOf("/node_modules/", marker + 1)) >= 1) {
146
+ installPath = normalized.slice(0, marker);
147
+ const slash = installPath.lastIndexOf("/");
148
+ if (slash < 1) continue;
149
+ toolRoot = installPath.slice(0, slash);
150
+ metadataPath = `${toolRoot}/.mise.backend.toml`;
151
+ const metadataState = deps.probe(metadataPath);
152
+ if (metadataState === "present") break;
153
+ if (metadataState === "unreadable") {
154
+ return { recognized: true, owner: null, error: "metadata_unreadable" };
155
+ }
156
+ metadataPath = undefined;
157
+ }
158
+ if (!metadataPath || !installPath || !toolRoot) return { recognized: false };
159
+
160
+ let metadata;
161
+ try {
162
+ metadata = parseBackendMetadata(deps.readFile(metadataPath));
163
+ } catch {
164
+ return { recognized: true, owner: null, error: "metadata_unreadable" };
165
+ }
166
+ const toolDir = toolRoot.slice(toolRoot.lastIndexOf("/") + 1);
167
+ // On Windows, npm -g under a mise-managed Node writes the package straight into
168
+ // <mise>/installs/node/<version>/node_modules, so the adjacent record is Node's own
169
+ // (short = "node", full = "core:node"), not a statement about OpenCodex. Only that exact
170
+ // runtime record with the package directly in the runtime's global node_modules is an
171
+ // npm install; any other backend or layout stays fail-closed below.
172
+ if (
173
+ metadata
174
+ && metadata.tool === MISE_NODE_RUNTIME.tool
175
+ && metadata.backend === MISE_NODE_RUNTIME.backend
176
+ && samePath(toolDir, MISE_NODE_RUNTIME.tool, windowsPath)
177
+ && isRuntimeGlobalPackage(normalized, installPath, windowsPath)
178
+ ) {
179
+ return { recognized: false };
180
+ }
181
+ const expectedToolDir = metadata?.tool === OPENCODEX_MISE_BACKEND
182
+ ? OPENCODEX_MISE_BACKEND_DIR
183
+ : metadata?.tool;
184
+ if (
185
+ !metadata
186
+ || metadata.backend !== OPENCODEX_MISE_BACKEND
187
+ || (metadata.tool !== OPENCODEX_MISE_BACKEND
188
+ && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(metadata.tool))
189
+ || !samePath(expectedToolDir, toolDir, windowsPath)
190
+ ) {
191
+ return { recognized: true, owner: null, error: "metadata_inconsistent" };
192
+ }
193
+ return {
194
+ recognized: true,
195
+ owner: {
196
+ tool: metadata.tool,
197
+ backend: metadata.backend,
198
+ installPath,
199
+ toolRoot,
200
+ },
201
+ };
202
+ }
203
+
204
+ /**
205
+ * True when the package sits directly in the runtime's global node_modules:
206
+ * <toolRoot>/<version>/node_modules/@bitkyc08/opencodex[/...].
207
+ */
208
+ function isRuntimeGlobalPackage(packagePath, installPath, windowsPath) {
209
+ const rest = packagePath.slice(installPath.length);
210
+ const probe = windowsPath ? rest.toLowerCase() : rest;
211
+ return probe === OPENCODEX_PACKAGE_SEGMENT || probe.startsWith(`${OPENCODEX_PACKAGE_SEGMENT}/`);
212
+ }
213
+
214
+ function probeMetadata(path) {
215
+ try {
216
+ statSync(path);
217
+ return "present";
218
+ } catch (error) {
219
+ const code = error && typeof error === "object" && "code" in error
220
+ ? error.code
221
+ : undefined;
222
+ return code === "ENOENT" || code === "ENOTDIR" ? "absent" : "unreadable";
223
+ }
224
+ }
225
+
226
+ function sameDirectory(left, right, realpath) {
227
+ if (samePath(left, right)) return true;
228
+ try {
229
+ return samePath(realpath(left), realpath(right));
230
+ } catch {
231
+ return false;
232
+ }
233
+ }
234
+
235
+ function samePath(left, right, windows = /^[A-Za-z]:\//.test(left) && /^[A-Za-z]:\//.test(right)) {
236
+ return windows ? left.toLowerCase() === right.toLowerCase() : left === right;
34
237
  }
35
238
 
36
239
  function detectInstallCandidate(packagePath, exists) {
37
- const normalized = String(packagePath).replaceAll("\\", "/");
240
+ const path = String(packagePath);
241
+ const normalized = /^[A-Za-z]:[\\/]/.test(path) || path.startsWith("\\\\")
242
+ ? path.replaceAll("\\", "/")
243
+ : path;
38
244
  const segments = normalized.split("/").filter(Boolean);
39
245
  // Windows paths are case-insensitive. Treating the structural marker this way also
40
246
  // keeps a preserved-symlink path from being downgraded merely because its casing came
package/src/update/job.ts CHANGED
@@ -34,19 +34,22 @@ import {
34
34
  import { isServiceInstalled, isServiceViable, readServiceBackend, stopWindows } from "../service";
35
35
  import { runUpdateRestartWithOwnershipLease, type ServiceOwnershipResolution } from "./restart-ownership";
36
36
  import {
37
- type Channel,
38
- type Installer,
37
+ type Channel, type Installer,
39
38
  PKG,
40
39
  checkUpdatePackageIntegrity,
41
40
  currentVersion,
42
41
  defaultUpdateTag,
43
42
  detectInstall,
43
+ detectInstallOwnership,
44
44
  latestVersion,
45
+ miseUpdateCommand,
45
46
  updateCommand,
46
47
  updateCommandStr,
47
48
  resolveCurrentPnpmGlobalOwner,
48
49
  resolvePnpmActiveLauncher,
49
50
  } from "./index";
51
+ import type { UpdateCheckDeps } from "./check-types";
52
+ export type { UpdateCheckDeps } from "./check-types";
50
53
  import type { PnpmGlobalOwner } from "./pnpm-global-install.mjs";
51
54
  import { isNewer } from "./notify";
52
55
  import { isRealBunBinary } from "../lib/bun-binary-validator.mjs";
@@ -114,12 +117,6 @@ export class UpdateJobError extends Error {
114
117
  }
115
118
  }
116
119
 
117
- export interface UpdateCheckDeps {
118
- currentVersion: () => string;
119
- detectInstall: () => Installer;
120
- latestVersion: (tag: Channel) => string | null;
121
- }
122
-
123
120
  interface UpdateWorkerProcess {
124
121
  pid?: number;
125
122
  unref(): void;
@@ -136,7 +133,9 @@ export interface StartUpdateJobDeps {
136
133
  const defaultCheckDeps: UpdateCheckDeps = {
137
134
  currentVersion,
138
135
  detectInstall,
136
+ detectInstallOwnership,
139
137
  latestVersion,
138
+ miseUpdateCommand,
140
139
  };
141
140
 
142
141
  function nodeBin(): string {
@@ -489,16 +488,22 @@ export function checkForUpdate(
489
488
  deps: UpdateCheckDeps = defaultCheckDeps,
490
489
  ): UpdateCheckResult {
491
490
  const current = deps.currentVersion();
492
- const installer = deps.detectInstall();
491
+ const ownership = deps.detectInstallOwnership?.();
492
+ const installer = ownership?.installer ?? deps.detectInstall();
493
493
  const channel = requestedChannel ?? normalizeUpdateChannel(null, current);
494
494
  const latest = installer === "source" ? null : deps.latestVersion(channel);
495
495
  const updateAvailable = !!latest && isNewer(latest, current, channel);
496
496
  let reason: string | undefined;
497
- let command = installer === "source" ? manualSourceCommand() : updateExecutionCommand(installer, channel).display;
497
+ let command = installer === "source"
498
+ ? manualSourceCommand()
499
+ : installer === "mise"
500
+ ? (ownership && deps.miseUpdateCommand?.(ownership)) ?? ""
501
+ : updateExecutionCommand(installer, channel).display;
498
502
 
499
503
  if (installer === "source") {
500
504
  reason = "source_checkout";
501
- command = manualSourceCommand();
505
+ } else if (installer === "mise") {
506
+ reason = command ? "externally_managed" : "external_ownership_invalid";
502
507
  } else if (!latest) {
503
508
  reason = "latest_unavailable";
504
509
  } else if (!updateAvailable) {
@@ -511,7 +516,7 @@ export function checkForUpdate(
511
516
  channel,
512
517
  installer,
513
518
  updateAvailable,
514
- canUpdate: installer !== "source" && updateAvailable,
519
+ canUpdate: installer !== "source" && installer !== "mise" && updateAvailable,
515
520
  command,
516
521
  releaseNotesUrl: RELEASE_NOTES_URL,
517
522
  ...(reason ? { reason } : {}),
@@ -17,7 +17,8 @@ import {
17
17
  } from "./index";
18
18
 
19
19
  const VERSION_FILENAME = "version.json";
20
- const REFRESH_INTERVAL_MS = 20 * 60 * 60 * 1000; // 20h, matching codex-rs
20
+ export const REFRESH_INTERVAL_MS = 20 * 60 * 60 * 1000; // 20h, matching codex-rs
21
+ export const CACHE_MAX_AGE_MS = 40 * 60 * 60 * 1000;
21
22
  const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/latest";
22
23
 
23
24
  export interface VersionCache {
@@ -62,6 +63,17 @@ export function writeVersionCache(cache: VersionCache): void {
62
63
  }
63
64
  }
64
65
 
66
+ export function writeFreshVersionCache(channel: Channel, latest: string, nowMs = Date.now()): void {
67
+ const previous = readVersionCache(channel);
68
+ writeVersionCache({
69
+ latest_version: latest,
70
+ last_checked_at: new Date(nowMs).toISOString(),
71
+ dismissed_version: previous?.latest_version === latest && previous.dismissed_version === latest
72
+ ? latest : undefined,
73
+ tag: channel,
74
+ });
75
+ }
76
+
65
77
  function parseStable(v: string): [number, number, number] | null {
66
78
  const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
67
79
  if (!m) return null;
@@ -139,7 +151,8 @@ export function interactiveGuardOk(): boolean {
139
151
  * the one-time star prompt has already run (first-run yield, O1).
140
152
  */
141
153
  export function shouldConsider(): { channel: Channel; current: string } | null {
142
- if (detectInstall() === "source") return null;
154
+ const installer = detectInstall();
155
+ if (installer === "source" || installer === "mise") return null;
143
156
  const current = currentVersion();
144
157
  if (current === "?" || isSourceBuildVersion(current)) return null;
145
158
  if (!interactiveGuardOk()) return null;
@@ -195,14 +208,7 @@ export function triggerBackgroundRefreshIfStale(channel: Channel, cache: Version
195
208
  */
196
209
  export async function refreshVersionCache(channel: Channel): Promise<void> {
197
210
  const latest = latestVersion(channel);
198
- if (!latest) return; // do not dirty the cache or advance the timestamp
199
- const prev = readVersionCache(channel);
200
- writeVersionCache({
201
- latest_version: latest,
202
- last_checked_at: new Date().toISOString(),
203
- dismissed_version: prev?.dismissed_version,
204
- tag: channel,
205
- });
211
+ if (latest) writeFreshVersionCache(channel, latest);
206
212
  }
207
213
 
208
214
  /** Persist a dismissal so this exact version stops prompting. */
@@ -241,7 +247,6 @@ export async function maybeShowUpdatePrompt(): Promise<void> {
241
247
  const { channel, current } = eligible;
242
248
 
243
249
  const cache = readVersionCache(channel);
244
- triggerBackgroundRefreshIfStale(channel, cache);
245
250
 
246
251
  const latest = getUpgradeVersionForPopup(cache, current, channel);
247
252
  if (!latest) return;
@@ -0,0 +1,12 @@
1
+ import { resolveCurrentPnpmGlobalOwner } from "./index";
2
+
3
+ onmessage = event => {
4
+ try {
5
+ const invoked = typeof event.data === "string" ? event.data : "";
6
+ if (!invoked) { postMessage(null); return; }
7
+ const result = resolveCurrentPnpmGlobalOwner(invoked);
8
+ postMessage(result.ok ? result.owner : null);
9
+ } catch {
10
+ postMessage(null);
11
+ }
12
+ };
@@ -0,0 +1,141 @@
1
+ import { checkForUpdate, type UpdateCheckResult } from "./job";
2
+ import { currentVersion, defaultUpdateTag, detectInstall, detectInstallOwnership, miseUpdateCommand, type Channel, type Installer } from "./index";
3
+ import { isSourceBuildVersion, readVersionCache, REFRESH_INTERVAL_MS, writeFreshVersionCache } from "./notify";
4
+ import { latestVersionAsync } from "./async-check";
5
+
6
+ export const STALENESS_TICK_MS = 60 * 60 * 1000;
7
+ export const RETRY_BASE_MS = 60_000;
8
+ export const RETRY_CAP_MS = STALENESS_TICK_MS;
9
+
10
+ export interface RefreshDeps {
11
+ now: () => number;
12
+ lookup: (channel: Channel, installer: Installer) => Promise<string | null>;
13
+ current: () => string;
14
+ install: () => Installer;
15
+ ownership: typeof detectInstallOwnership;
16
+ guidance: typeof miseUpdateCommand;
17
+ read: typeof readVersionCache;
18
+ write: typeof writeFreshVersionCache;
19
+ setTimer: typeof setTimeout;
20
+ clearTimer: typeof clearTimeout;
21
+ disabled: () => boolean;
22
+ }
23
+
24
+ const defaults: RefreshDeps = {
25
+ now: Date.now,
26
+ lookup: latestVersionAsync,
27
+ current: currentVersion,
28
+ install: detectInstall,
29
+ ownership: detectInstallOwnership,
30
+ guidance: miseUpdateCommand,
31
+ read: readVersionCache,
32
+ write: writeFreshVersionCache,
33
+ setTimer: setTimeout,
34
+ clearTimer: clearTimeout,
35
+ disabled: () => process.env.OCX_DISABLE_UPDATE_CHECK === "1",
36
+ };
37
+
38
+ export function createRefreshScheduler(deps: RefreshDeps = defaults) {
39
+ const inFlight = new Map<Channel, { task: Promise<string | null>; epoch: number; markExplicit: () => void }>();
40
+ // Flight order per channel: a flight started earlier never overwrites a cache entry that a
41
+ // later-started flight already wrote (an explicit caller can keep an old flight writable
42
+ // across stop/start while the new listener's flight finishes first).
43
+ const lastWrittenFlight = new Map<Channel, number>();
44
+ let flightSeq = 0;
45
+ let timer: ReturnType<typeof setTimeout> | undefined;
46
+ let running = false;
47
+ let starts = 0;
48
+ let failures = 0;
49
+ let retryAt = 0;
50
+ let generation = 0;
51
+
52
+ const eligible = () => {
53
+ const version = deps.current();
54
+ const installer = deps.install();
55
+ return !deps.disabled() && version !== "?" && !isSourceBuildVersion(version)
56
+ && installer !== "source" && installer !== "mise";
57
+ };
58
+ const channel = () => defaultUpdateTag(deps.current());
59
+ const stale = (tag: Channel) => {
60
+ const checked = Date.parse(deps.read(tag)?.last_checked_at ?? "");
61
+ return !Number.isFinite(checked) || checked > deps.now() || deps.now() - checked >= REFRESH_INTERVAL_MS;
62
+ };
63
+ const schedule = (delay: number) => {
64
+ if (!running) return;
65
+ if (timer) deps.clearTimer(timer);
66
+ timer = deps.setTimer(() => { timer = undefined; void tick(); }, delay);
67
+ timer.unref?.();
68
+ };
69
+ const lookup = (tag: Channel, automatic: boolean): Promise<string | null> => {
70
+ const existing = inFlight.get(tag);
71
+ // An automatic tick joins only a flight of its own generation. A flight left over from a
72
+ // stopped listener cannot write for the new one, so joining it would push the next check an
73
+ // hour out with nothing written. Explicit callers may join any flight: their interest writes.
74
+ if (existing && (!automatic || existing.epoch === generation)) {
75
+ if (!automatic) existing.markExplicit();
76
+ return existing.task;
77
+ }
78
+ const epoch = generation;
79
+ const seq = ++flightSeq;
80
+ let explicitInterest = !automatic;
81
+ const task = Promise.resolve().then(() => deps.lookup(tag, deps.install())).then(latest => {
82
+ if (latest && (explicitInterest || (automatic && running && epoch === generation))
83
+ && seq > (lastWrittenFlight.get(tag) ?? 0)) {
84
+ lastWrittenFlight.set(tag, seq);
85
+ deps.write(tag, latest, deps.now());
86
+ }
87
+ if (automatic && running && epoch === generation) {
88
+ failures = latest ? 0 : failures + 1;
89
+ retryAt = latest ? 0 : deps.now() + Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** Math.min(failures - 1, 6));
90
+ }
91
+ return latest;
92
+ }).catch(() => {
93
+ if (automatic && running && epoch === generation) {
94
+ failures += 1;
95
+ retryAt = deps.now() + Math.min(RETRY_CAP_MS, RETRY_BASE_MS * 2 ** Math.min(failures - 1, 6));
96
+ }
97
+ return null;
98
+ }).finally(() => { if (inFlight.get(tag)?.task === task) inFlight.delete(tag); });
99
+ inFlight.set(tag, { task, epoch, markExplicit: () => { explicitInterest = true; } });
100
+ return task;
101
+ };
102
+ const tick = async () => {
103
+ if (!running || !eligible()) return;
104
+ const tag = channel();
105
+ if (stale(tag) && deps.now() >= retryAt) await lookup(tag, true);
106
+ schedule(retryAt > deps.now() ? Math.min(STALENESS_TICK_MS, retryAt - deps.now()) : STALENESS_TICK_MS);
107
+ };
108
+ return {
109
+ start() {
110
+ starts += 1;
111
+ if (starts !== 1 || !eligible()) return;
112
+ running = true;
113
+ schedule(0);
114
+ },
115
+ stop() {
116
+ if (starts === 0) return;
117
+ starts -= 1;
118
+ if (starts !== 0) return;
119
+ running = false;
120
+ generation += 1;
121
+ if (timer) deps.clearTimer(timer);
122
+ timer = undefined;
123
+ },
124
+ async check(tag: Channel): Promise<UpdateCheckResult> {
125
+ const installer = deps.install();
126
+ const latest = installer === "source" || installer === "mise" ? null : await lookup(tag, false);
127
+ return checkForUpdate(tag, {
128
+ currentVersion: deps.current,
129
+ detectInstall: deps.install,
130
+ detectInstallOwnership: deps.ownership,
131
+ miseUpdateCommand: deps.guidance,
132
+ latestVersion: () => latest,
133
+ });
134
+ },
135
+ };
136
+ }
137
+
138
+ export const packageRefresh = createRefreshScheduler();
139
+
140
+ export function startPackageRefresh(): void { packageRefresh.start(); }
141
+ export function stopPackageRefresh(): void { packageRefresh.stop(); }
package/src/usage/cost.ts CHANGED
@@ -25,6 +25,9 @@ import {
25
25
  findExpectedPriceOverlay,
26
26
  findVerifiedPriceOverride,
27
27
  findPriorityPricingRule,
28
+ cursorFastPriceMultiplier,
29
+ cursorFastPriceSource,
30
+ cursorFastPriceSupported,
28
31
  findContextTier,
29
32
  isLongContext,
30
33
  type Cost4,
@@ -257,6 +260,7 @@ function resolveMatchedPriceExact(
257
260
  // operator's explicit price is authoritative for the ~$ estimate.
258
261
  const userOverlay = userOverlayMatch(provider, modelId, userOverlays);
259
262
  if (userOverlay) return userOverlay;
263
+ if (!cursorFastPriceSupported(provider, modelId)) return null;
260
264
  const verifiedOverride = overlays === EXPECTED_PRICE_OVERLAYS
261
265
  ? findVerifiedPriceOverride(provider, modelId)
262
266
  : undefined;
@@ -264,11 +268,12 @@ function resolveMatchedPriceExact(
264
268
  return {
265
269
  provider,
266
270
  modelId,
267
- cost4: verifiedOverride.cost4,
271
+ cost4: multiplyCursorFastCost(verifiedOverride.cost4, provider, modelId),
268
272
  source: "expected",
269
273
  sourceRef: verifiedOverride.source,
270
274
  verifiedAt: verifiedOverride.verifiedAt,
271
275
  status: verifiedOverride.status,
276
+ ...cursorFastPriceProvenance(provider, modelId),
272
277
  };
273
278
  }
274
279
  const metadataProvider = resolveMetadataProvider(provider);
@@ -280,28 +285,50 @@ function resolveMatchedPriceExact(
280
285
  provider,
281
286
  modelId,
282
287
  jawcodeProvider: metadataProvider,
283
- cost4: bundled.cost,
288
+ cost4: multiplyCursorFastCost(bundled.cost, provider, modelId),
284
289
  source: "jawcode",
285
290
  status: "verified",
291
+ ...cursorFastPriceProvenance(provider, modelId),
286
292
  };
287
293
  }
288
294
  const overlay = findExpectedPriceOverlay(provider, modelId, overlays);
289
295
  if (!overlay || !validCost4(overlay.cost4) || !hasNonZeroCost(overlay.cost4)) {
290
- return options.allowModelLevelFallback === false ? null : resolveModelLevelPrice(provider, modelId);
296
+ if (options.allowModelLevelFallback === false) return null;
297
+ const fallback = resolveModelLevelPrice(provider, modelId);
298
+ return fallback
299
+ ? { ...fallback, cost4: multiplyCursorFastCost(fallback.cost4, provider, modelId), ...cursorFastPriceProvenance(provider, modelId) }
300
+ : null;
291
301
  }
292
302
  if (overlay.status === "unverified") return null;
293
303
  return {
294
304
  provider,
295
305
  modelId,
296
306
  ...(metadataProvider ? { jawcodeProvider: metadataProvider } : {}),
297
- cost4: overlay.cost4,
307
+ cost4: multiplyCursorFastCost(overlay.cost4, provider, modelId),
298
308
  source: "expected",
299
309
  sourceRef: overlay.source,
300
310
  verifiedAt: overlay.verifiedAt,
301
311
  status: overlay.status,
312
+ ...cursorFastPriceProvenance(provider, modelId),
302
313
  };
303
314
  }
304
315
 
316
+ function multiplyCursorFastCost(cost4: Cost4, provider: string, modelId: string): Cost4 {
317
+ const multiplier = cursorFastPriceMultiplier(provider, modelId);
318
+ if (multiplier === 1) return cost4;
319
+ return {
320
+ input: cost4.input * multiplier,
321
+ output: cost4.output * multiplier,
322
+ cacheRead: cost4.cacheRead * multiplier,
323
+ cacheWrite: cost4.cacheWrite * multiplier,
324
+ };
325
+ }
326
+
327
+ function cursorFastPriceProvenance(provider: string, modelId: string): Pick<MatchedPrice, "sourceRef" | "status"> | Record<never, never> {
328
+ const sourceRef = cursorFastPriceSource(provider, modelId);
329
+ return sourceRef ? { sourceRef, status: "verified" } : {};
330
+ }
331
+
305
332
  /** User-configured overlay match; explicit zero rates are authoritative too. */
306
333
  function userOverlayMatch(
307
334
  provider: string,