@bitkyc08/opencodex 2.6.28-preview.20260707 → 2.6.31-preview.20260707
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/gui/dist/assets/{index-CkV5xFA8.js → index-CWujz83O.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +22 -7
- package/src/cli/doctor.ts +89 -1
- package/src/codex/home.ts +68 -13
- package/src/codex/shim.ts +74 -10
- package/src/lib/gcp-adc.ts +13 -2
- package/src/oauth/anthropic.ts +2 -2
- package/src/oauth/local-token-detect.ts +36 -5
- package/src/service.ts +4 -0
|
@@ -12,4 +12,4 @@ Error generating stack: `+e.message+`
|
|
|
12
12
|
-d '{
|
|
13
13
|
"model": "gpt-5.4",
|
|
14
14
|
"input": "Hello, world!"
|
|
15
|
-
}'`})]})]})}var xt=`opencodex-api-token`,St=!1,Ct=null;function wt(e){try{let t=e instanceof Request?e.url:String(e);return new URL(t,window.location.href).pathname}catch{return null}}function Tt(e){let t=wt(e);return!!t&&(t.startsWith(`/api/`)||t.startsWith(`/v1/`))}function Et(){try{return sessionStorage.getItem(xt)?.trim()||null}catch{return null}}function Dt(e){try{sessionStorage.setItem(xt,e)}catch{}}function Ot(){try{sessionStorage.removeItem(xt)}catch{}}function kt(e,t,n){let r=new Headers(t?.headers??(e instanceof Request?e.headers:void 0));return r.set(`X-OpenCodex-API-Key`,n),e instanceof Request?[new Request(e,{headers:r}),t?{...t,headers:r}:void 0]:[e,{...t,headers:r}]}async function At(){return Ct||(Ct=Promise.resolve().then(()=>window.prompt(`OpenCodex API token`)?.trim()||null).finally(()=>{Ct=null}),Ct)}function jt(){if(St)return;St=!0;let e=window.fetch.bind(window);window.fetch=async(t,n)=>{if(!Tt(t))return e(t,n);let r=Et(),[i,a]=r?kt(t,n,r):[t,n],o=await e(i,a);if(o.status!==401)return o;r&&Ot();let s=await At();if(!s)return o;Dt(s);let[c,l]=kt(t,n,s),u=await e(c,l);return u.status===401&&Ot(),u}}jt();var Mt=new Set([`dashboard`,`providers`,`models`,`subagents`,`logs`,`usage`,`codex-auth`,`api`]);function Nt(){let e=location.hash.replace(/^#\/?/,``);return Mt.has(e)?e:`dashboard`}var Pt=``,Ft=`ocx-theme`,It=[{id:`dashboard`,tkey:`nav.dashboard`,Icon:w},{id:`providers`,tkey:`nav.providers`,Icon:T},{id:`models`,tkey:`nav.models`,Icon:E},{id:`subagents`,tkey:`nav.subagents`,Icon:ee},{id:`logs`,tkey:`nav.logs`,Icon:D},{id:`usage`,tkey:`nav.usage`,Icon:te},{id:`codex-auth`,tkey:`nav.codexAuth`,Icon:L},{id:`api`,tkey:`nav.api`,Icon:de}],Lt={light:ce,dark:le,system:ue},Rt={light:`theme.light`,dark:`theme.dark`,system:`theme.system`};function zt(e){if(!e||typeof e!=`object`||!(`version`in e))return null;let t=e.version;return typeof t==`string`&&t.length>0?t:null}function Bt(){let e=localStorage.getItem(Ft);return e===`light`||e===`dark`?e:`system`}function Vt(){let[e,t]=(0,_.useState)(Nt),n=e=>{location.hash=e,t(e)},[r,i]=(0,_.useState)(Bt),[a,o]=(0,_.useState)(null),{locale:s,setLocale:c}=be(),l=xe();(0,_.useEffect)(()=>{let e=()=>t(Nt());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),(0,_.useEffect)(()=>{let e=document.documentElement;r===`system`?(e.removeAttribute(`data-theme`),localStorage.removeItem(Ft)):(e.setAttribute(`data-theme`,r),localStorage.setItem(Ft,r))},[r]),(0,_.useEffect)(()=>{let e=!1,t=async()=>{try{let t=await fetch(`${Pt}/healthz`);if(!t.ok)return;let n=zt(await t.json());!e&&n&&o(n)}catch{}};t();let n=setInterval(t,3e4);return()=>{e=!0,clearInterval(n)}},[]);let u=()=>i(e=>e===`light`?`dark`:e===`dark`?`system`:`light`),d=Lt[r],f=a??`2.6.
|
|
15
|
+
}'`})]})]})}var xt=`opencodex-api-token`,St=!1,Ct=null;function wt(e){try{let t=e instanceof Request?e.url:String(e);return new URL(t,window.location.href).pathname}catch{return null}}function Tt(e){let t=wt(e);return!!t&&(t.startsWith(`/api/`)||t.startsWith(`/v1/`))}function Et(){try{return sessionStorage.getItem(xt)?.trim()||null}catch{return null}}function Dt(e){try{sessionStorage.setItem(xt,e)}catch{}}function Ot(){try{sessionStorage.removeItem(xt)}catch{}}function kt(e,t,n){let r=new Headers(t?.headers??(e instanceof Request?e.headers:void 0));return r.set(`X-OpenCodex-API-Key`,n),e instanceof Request?[new Request(e,{headers:r}),t?{...t,headers:r}:void 0]:[e,{...t,headers:r}]}async function At(){return Ct||(Ct=Promise.resolve().then(()=>window.prompt(`OpenCodex API token`)?.trim()||null).finally(()=>{Ct=null}),Ct)}function jt(){if(St)return;St=!0;let e=window.fetch.bind(window);window.fetch=async(t,n)=>{if(!Tt(t))return e(t,n);let r=Et(),[i,a]=r?kt(t,n,r):[t,n],o=await e(i,a);if(o.status!==401)return o;r&&Ot();let s=await At();if(!s)return o;Dt(s);let[c,l]=kt(t,n,s),u=await e(c,l);return u.status===401&&Ot(),u}}jt();var Mt=new Set([`dashboard`,`providers`,`models`,`subagents`,`logs`,`usage`,`codex-auth`,`api`]);function Nt(){let e=location.hash.replace(/^#\/?/,``);return Mt.has(e)?e:`dashboard`}var Pt=``,Ft=`ocx-theme`,It=[{id:`dashboard`,tkey:`nav.dashboard`,Icon:w},{id:`providers`,tkey:`nav.providers`,Icon:T},{id:`models`,tkey:`nav.models`,Icon:E},{id:`subagents`,tkey:`nav.subagents`,Icon:ee},{id:`logs`,tkey:`nav.logs`,Icon:D},{id:`usage`,tkey:`nav.usage`,Icon:te},{id:`codex-auth`,tkey:`nav.codexAuth`,Icon:L},{id:`api`,tkey:`nav.api`,Icon:de}],Lt={light:ce,dark:le,system:ue},Rt={light:`theme.light`,dark:`theme.dark`,system:`theme.system`};function zt(e){if(!e||typeof e!=`object`||!(`version`in e))return null;let t=e.version;return typeof t==`string`&&t.length>0?t:null}function Bt(){let e=localStorage.getItem(Ft);return e===`light`||e===`dark`?e:`system`}function Vt(){let[e,t]=(0,_.useState)(Nt),n=e=>{location.hash=e,t(e)},[r,i]=(0,_.useState)(Bt),[a,o]=(0,_.useState)(null),{locale:s,setLocale:c}=be(),l=xe();(0,_.useEffect)(()=>{let e=()=>t(Nt());return window.addEventListener(`hashchange`,e),()=>window.removeEventListener(`hashchange`,e)},[]),(0,_.useEffect)(()=>{let e=document.documentElement;r===`system`?(e.removeAttribute(`data-theme`),localStorage.removeItem(Ft)):(e.setAttribute(`data-theme`,r),localStorage.setItem(Ft,r))},[r]),(0,_.useEffect)(()=>{let e=!1,t=async()=>{try{let t=await fetch(`${Pt}/healthz`);if(!t.ok)return;let n=zt(await t.json());!e&&n&&o(n)}catch{}};t();let n=setInterval(t,3e4);return()=>{e=!0,clearInterval(n)}},[]);let u=()=>i(e=>e===`light`?`dark`:e===`dark`?`system`:`light`),d=Lt[r],f=a??`2.6.31-preview.20260707`,p=me.find(e=>e.code===s)?.name??`English`,m=()=>{let e=me.map(e=>e.code);c(e[(e.indexOf(s)+1)%e.length])},[h,g]=(0,_.useState)(!1);return(0,S.jsxs)(`div`,{className:`app`,children:[(0,S.jsxs)(`aside`,{className:`sidebar`,children:[(0,S.jsxs)(`div`,{className:`brand`,children:[(0,S.jsx)(`span`,{className:`brand-logo`,role:`img`,"aria-label":`opencodex logo`}),(0,S.jsx)(`span`,{className:`name`,children:`opencodex`}),(0,S.jsxs)(`span`,{className:`ver`,children:[`v`,f]})]}),(0,S.jsx)(`nav`,{children:It.map(({id:t,tkey:r,Icon:i})=>(0,S.jsxs)(`button`,{className:`nav-item${e===t?` active`:``}`,"data-page":t,onClick:()=>n(t),"aria-current":e===t?`page`:void 0,children:[(0,S.jsx)(i,{}),` `,l(r)]},t))}),(0,S.jsxs)(`div`,{className:`sidebar-foot`,children:[(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:m,"aria-label":`${l(`lang.label`)}: ${p}`,title:`${l(`lang.label`)}: ${p}`,children:[(0,S.jsx)(de,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:p})]}),(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle`,onClick:u,"aria-label":`${l(`theme.label`)}: ${l(Rt[r])}`,title:`${l(`theme.label`)}: ${l(Rt[r])}`,children:[(0,S.jsx)(d,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:l(Rt[r])})]}),(0,S.jsxs)(`button`,{type:`button`,className:`theme-toggle stop-toggle`,onClick:async()=>{if(confirm(l(`dash.stopConfirm`))){g(!0);try{await fetch(`${Pt}/api/stop`,{method:`POST`})}catch{}}},disabled:h,"aria-label":l(`dash.stop`),title:l(`dash.stop`),children:[(0,S.jsx)(F,{}),` `,(0,S.jsx)(`span`,{className:`mode`,children:l(h?`dash.stopping`:`dash.stop`)})]}),(0,S.jsxs)(`a`,{className:`sidebar-link`,href:`https://github.com/lidge-jun/opencodex`,target:`_blank`,rel:`noreferrer`,children:[(0,S.jsx)(P,{}),` `,l(`common.github`)]})]})]}),(0,S.jsx)(`main`,{className:`main`,children:(0,S.jsxs)(`div`,{className:`main-inner`,children:[e===`dashboard`&&(0,S.jsx)(Oe,{apiBase:Pt}),e===`providers`&&(0,S.jsx)(We,{apiBase:Pt}),e===`models`&&(0,S.jsx)(Ye,{apiBase:Pt}),e===`subagents`&&(0,S.jsx)(Xe,{apiBase:Pt}),e===`logs`&&(0,S.jsx)(ot,{apiBase:Pt}),e===`usage`&&(0,S.jsx)(pt,{apiBase:Pt}),e===`codex-auth`&&(0,S.jsx)(ht,{apiBase:Pt}),e===`api`&&(0,S.jsx)(bt,{apiBase:Pt})]})})]})}v.createRoot(document.getElementById(`root`)).render((0,S.jsx)(_.StrictMode,{children:(0,S.jsx)(ye,{children:(0,S.jsx)(Vt,{})})}));
|
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-CWujz83O.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-ByGC8-Bm.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bitkyc08/opencodex",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.31-preview.20260707",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -331,9 +331,16 @@ function buildToolNameTransforms(provider: OcxProviderConfig): { toWire: (name:
|
|
|
331
331
|
function toAnthropicToolResult(msg: OcxToolResultMessage): Record<string, unknown> {
|
|
332
332
|
// Anthropic tool_result accepts a string OR content blocks — render images natively
|
|
333
333
|
// (e.g. Codex view_image output) instead of dropping them.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
334
|
+
let content: string | unknown[];
|
|
335
|
+
if (typeof msg.content === "string") {
|
|
336
|
+
// Anthropic rejects tool_result with empty text content blocks.
|
|
337
|
+
content = msg.content || "(empty tool output)";
|
|
338
|
+
} else {
|
|
339
|
+
const parts = (msg.content as OcxContentPart[])
|
|
340
|
+
.map(toAnthropicContentPart)
|
|
341
|
+
.filter(p => !((p as { type?: string }).type === "text" && !(p as { text?: string }).text));
|
|
342
|
+
content = parts.length > 0 ? parts : "(empty tool output)";
|
|
343
|
+
}
|
|
337
344
|
return {
|
|
338
345
|
type: "tool_result",
|
|
339
346
|
tool_use_id: msg.toolCallId,
|
|
@@ -370,9 +377,16 @@ function messagesToAnthropicFormat(
|
|
|
370
377
|
switch (msg.role) {
|
|
371
378
|
case "user":
|
|
372
379
|
case "developer": {
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
380
|
+
let content: string | unknown[];
|
|
381
|
+
if (typeof msg.content === "string") {
|
|
382
|
+
// Anthropic rejects empty string text content blocks.
|
|
383
|
+
content = msg.content || "(empty)";
|
|
384
|
+
} else {
|
|
385
|
+
const parts = (msg.content as OcxContentPart[])
|
|
386
|
+
.map(toAnthropicContentPart)
|
|
387
|
+
.filter(p => !((p as { type?: string }).type === "text" && !(p as { text?: string }).text));
|
|
388
|
+
content = parts.length > 0 ? parts : "(empty)";
|
|
389
|
+
}
|
|
376
390
|
messages.push({ role: "user", content });
|
|
377
391
|
break;
|
|
378
392
|
}
|
|
@@ -382,7 +396,8 @@ function messagesToAnthropicFormat(
|
|
|
382
396
|
const toolUseIds: string[] = [];
|
|
383
397
|
for (const part of aMsg.content) {
|
|
384
398
|
if (part.type === "text") {
|
|
385
|
-
|
|
399
|
+
const text = (part as OcxTextContent).text;
|
|
400
|
+
if (text) content.push({ type: "text", text });
|
|
386
401
|
} else if (part.type === "thinking") {
|
|
387
402
|
const t = part as OcxThinkingContent;
|
|
388
403
|
// Redacted blocks replay verbatim FIRST (they preceded the visible thinking block
|
package/src/cli/doctor.ts
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
* networking. See devlog/_plan/260630_wsl-account-autoswitch/30_*.
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
|
+
import { homedir } from "node:os";
|
|
11
12
|
import { join } from "node:path";
|
|
12
13
|
import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, resolveEnvValue } from "../config";
|
|
13
14
|
import { readCodexTokens } from "../codex/auth-collision";
|
|
14
|
-
import { resolveCodexHomeDir as resolveCodexHomeDirImpl } from "../codex/home";
|
|
15
|
+
import { resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
16
|
+
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
15
17
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
16
18
|
export { resolveCodexHomeDir } from "../codex/home";
|
|
17
19
|
|
|
@@ -70,6 +72,70 @@ function readMounts(): string | null {
|
|
|
70
72
|
}
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
export type WslDualInstallDiagnostic = {
|
|
76
|
+
wsl: boolean;
|
|
77
|
+
automountRoot: string;
|
|
78
|
+
effectiveCodexHome: string;
|
|
79
|
+
effectiveIsWindowsMount: boolean;
|
|
80
|
+
linuxCodexConfigured: boolean;
|
|
81
|
+
windowsCodexHomes: string[];
|
|
82
|
+
dualInstall: boolean;
|
|
83
|
+
interopCodexOnPath: string | null;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type WslDualInstallDeps = CodexHomeDeps & {
|
|
87
|
+
pathValue?: string;
|
|
88
|
+
effectiveCodexHome?: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* WSL + Windows dual-install visibility: which `.codex` home each side owns,
|
|
93
|
+
* whether the effective home sits on a Windows mount, and whether the `codex`
|
|
94
|
+
* on PATH is actually the Windows launcher reached through drive interop.
|
|
95
|
+
* Read-only; hints are printed by runDoctor, never applied.
|
|
96
|
+
*/
|
|
97
|
+
export function collectWslDualInstall(deps: WslDualInstallDeps = {}): WslDualInstallDiagnostic {
|
|
98
|
+
const wsl = isWslRuntime(deps);
|
|
99
|
+
const effectiveCodexHome = deps.effectiveCodexHome ?? resolveCodexHomeDirImpl(deps);
|
|
100
|
+
if (!wsl) {
|
|
101
|
+
return {
|
|
102
|
+
wsl: false,
|
|
103
|
+
automountRoot: "/mnt",
|
|
104
|
+
effectiveCodexHome,
|
|
105
|
+
effectiveIsWindowsMount: false,
|
|
106
|
+
linuxCodexConfigured: false,
|
|
107
|
+
windowsCodexHomes: [],
|
|
108
|
+
dualInstall: false,
|
|
109
|
+
interopCodexOnPath: null,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const automountRoot = wslAutomountRoot(deps);
|
|
113
|
+
const exists = deps.existsSync ?? existsSync;
|
|
114
|
+
const home = (deps.homedir ?? homedir)();
|
|
115
|
+
const linuxCodexConfigured = !!home && exists(join(home, ".codex", "config.toml"));
|
|
116
|
+
const windowsCodexHomes = listWslWindowsCodexHomes(deps);
|
|
117
|
+
const onPath = findCodexOnPath({
|
|
118
|
+
pathValue: deps.pathValue ?? process.env.PATH,
|
|
119
|
+
wsl: false, // scan everything; classify interop ourselves
|
|
120
|
+
posixPaths: true, // WSL PATH entries are POSIX regardless of the host running doctor
|
|
121
|
+
automountRoot,
|
|
122
|
+
// When a fake fs is injected (tests), the real lstat/readFile would miss its
|
|
123
|
+
// synthetic paths; treat every injected hit as a plain non-shim file.
|
|
124
|
+
...(deps.existsSync ? { exists: deps.existsSync, isShimFile: () => false, isDirectory: () => false } : {}),
|
|
125
|
+
});
|
|
126
|
+
const interopCodexOnPath = onPath && isWindowsInteropDir(onPath, automountRoot) ? onPath : null;
|
|
127
|
+
return {
|
|
128
|
+
wsl,
|
|
129
|
+
automountRoot,
|
|
130
|
+
effectiveCodexHome,
|
|
131
|
+
effectiveIsWindowsMount: isWindowsInteropDir(effectiveCodexHome, automountRoot),
|
|
132
|
+
linuxCodexConfigured,
|
|
133
|
+
windowsCodexHomes,
|
|
134
|
+
dualInstall: linuxCodexConfigured && windowsCodexHomes.length > 0,
|
|
135
|
+
interopCodexOnPath,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
73
139
|
const PROXY_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY"] as const;
|
|
74
140
|
|
|
75
141
|
export type ProxyEnvRow = { key: string; present: boolean };
|
|
@@ -307,6 +373,21 @@ export async function runDoctor(): Promise<void> {
|
|
|
307
373
|
console.log(` -- ${pending.pendingRows} thread(s) still tagged opencodex, ${pending.backupEntries} backup manifest entr${pending.backupEntries === 1 ? "y" : "ies"}`);
|
|
308
374
|
}
|
|
309
375
|
|
|
376
|
+
const dual = collectWslDualInstall();
|
|
377
|
+
if (dual.wsl) {
|
|
378
|
+
console.log("\nWSL Codex installs");
|
|
379
|
+
console.log(` ${dual.linuxCodexConfigured ? "ok " : "-- "} Linux ~/.codex/config.toml`);
|
|
380
|
+
if (dual.windowsCodexHomes.length > 0) {
|
|
381
|
+
for (const winHome of dual.windowsCodexHomes) console.log(` ok Windows ${winHome}`);
|
|
382
|
+
} else {
|
|
383
|
+
console.log(" -- no Windows-profile .codex detected under /mnt/c/Users");
|
|
384
|
+
}
|
|
385
|
+
console.log(` effective CODEX_HOME: ${dual.effectiveCodexHome}${dual.effectiveIsWindowsMount ? " (Windows mount)" : ""}`);
|
|
386
|
+
if (dual.interopCodexOnPath) {
|
|
387
|
+
console.log(` -- codex on PATH is the Windows launcher via interop: ${dual.interopCodexOnPath}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
310
391
|
// Hints, not fixes.
|
|
311
392
|
const hints: string[] = [];
|
|
312
393
|
const anyDrvfs = paths.some(p => detectFsType(p.path, mounts).isDrvfs || detectFsType(p.path, mounts).isMntDrive);
|
|
@@ -325,6 +406,13 @@ export async function runDoctor(): Promise<void> {
|
|
|
325
406
|
if (pending.failed || pending.pendingRows > 0 || pending.backupEntries > 0) {
|
|
326
407
|
hints.push("Legacy chat threads are still tagged opencodex (or the DB was locked). The running proxy retries the migration automatically; to force it now, close the Codex app and run 'ocx sync'.");
|
|
327
408
|
}
|
|
409
|
+
if (dual.dualInstall && !dual.effectiveIsWindowsMount) {
|
|
410
|
+
hints.push(`Codex is installed on BOTH WSL and Windows. Each side keeps its own ~/.codex (logins, config, catalog are separate); ocx here manages the Linux one. To share a single home, set CODEX_HOME=${dual.windowsCodexHomes[0] ?? `${dual.automountRoot}/c/Users/<you>/.codex`} in WSL (drvfs file locking is less reliable).`);
|
|
411
|
+
hints.push("localhost is one-way in WSL2 NAT mode: Windows-side codex reaches this WSL proxy via localhost (localhostForwarding, on by default), but a Windows-side proxy is NOT reachable from WSL via localhost — use networkingMode=mirrored in .wslconfig for both directions.");
|
|
412
|
+
}
|
|
413
|
+
if (dual.interopCodexOnPath) {
|
|
414
|
+
hints.push("The `codex` found on PATH is the Windows launcher reached through WSL interop; ocx will not shim it (a WSL shim breaks Windows invocations). Install codex inside WSL (npm i -g @openai/codex) or run 'ocx ensure' from Windows.");
|
|
415
|
+
}
|
|
328
416
|
if (hints.length > 0) {
|
|
329
417
|
console.log("\nHints");
|
|
330
418
|
for (const h of hints) console.log(` - ${h}`);
|
package/src/codex/home.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join, resolve } from "node:path";
|
|
3
|
+
import { join, posix, resolve } from "node:path";
|
|
4
4
|
import { expandUserPath } from "../config";
|
|
5
5
|
|
|
6
6
|
export type CodexHomeDeps = {
|
|
@@ -10,18 +10,64 @@ export type CodexHomeDeps = {
|
|
|
10
10
|
procVersion?: string | null;
|
|
11
11
|
homedir?: () => string;
|
|
12
12
|
usersRoot?: string;
|
|
13
|
+
/** Raw /etc/wsl.conf content override (tests); null means "no file". */
|
|
14
|
+
wslConf?: string | null;
|
|
13
15
|
existsSync?: (path: string) => boolean;
|
|
14
16
|
readdirSync?: (path: string) => string[];
|
|
15
17
|
statSync?: typeof statSync;
|
|
16
18
|
realpathSync?: (path: string) => string;
|
|
17
19
|
};
|
|
18
20
|
|
|
19
|
-
function windowsUserProfileToWslPath(value: string | undefined): string | null {
|
|
21
|
+
function windowsUserProfileToWslPath(value: string | undefined, automountRoot = DEFAULT_WSL_AUTOMOUNT_ROOT): string | null {
|
|
20
22
|
if (!value) return null;
|
|
21
23
|
const normalized = value.replaceAll("\\", "/");
|
|
22
24
|
const match = normalized.match(/^([A-Za-z]):\/Users\/([^/]+)$/);
|
|
23
25
|
if (!match) return null;
|
|
24
|
-
|
|
26
|
+
const root = normalizeAutomountRoot(automountRoot);
|
|
27
|
+
return `${root === "/" ? "" : root}/${match[1]!.toLowerCase()}/Users/${match[2]}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_WSL_AUTOMOUNT_ROOT = "/mnt";
|
|
31
|
+
|
|
32
|
+
function normalizeAutomountRoot(value: string): string {
|
|
33
|
+
const trimmed = value.replace(/\/+$/, "");
|
|
34
|
+
return trimmed === "" ? "/" : trimmed;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function readWslConf(): string | null {
|
|
38
|
+
try {
|
|
39
|
+
return readFileSync("/etc/wsl.conf", "utf8");
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Windows drive mount root inside WSL: `[automount] root` from /etc/wsl.conf,
|
|
47
|
+
* default `/mnt` (https://learn.microsoft.com/en-us/windows/wsl/wsl-config).
|
|
48
|
+
* Returns a path without a trailing slash (or `/` itself).
|
|
49
|
+
*/
|
|
50
|
+
export function wslAutomountRoot(deps: CodexHomeDeps = {}): string {
|
|
51
|
+
const content = deps.wslConf !== undefined ? deps.wslConf : readWslConf();
|
|
52
|
+
if (!content) return DEFAULT_WSL_AUTOMOUNT_ROOT;
|
|
53
|
+
let section = "";
|
|
54
|
+
for (const rawLine of content.split("\n")) {
|
|
55
|
+
const line = rawLine.replace(/[#;].*$/, "").trim();
|
|
56
|
+
if (!line) continue;
|
|
57
|
+
const sect = line.match(/^\[(.+)\]$/);
|
|
58
|
+
if (sect) {
|
|
59
|
+
section = sect[1]!.trim().toLowerCase();
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (section !== "automount") continue;
|
|
63
|
+
const kv = line.match(/^root\s*=\s*(.+)$/i);
|
|
64
|
+
if (kv) {
|
|
65
|
+
const value = kv[1]!.trim().replace(/^["']|["']$/g, "");
|
|
66
|
+
if (!value.startsWith("/")) return DEFAULT_WSL_AUTOMOUNT_ROOT;
|
|
67
|
+
return normalizeAutomountRoot(value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return DEFAULT_WSL_AUTOMOUNT_ROOT;
|
|
25
71
|
}
|
|
26
72
|
|
|
27
73
|
function readProcVersion(): string | null {
|
|
@@ -40,23 +86,24 @@ export function isWslRuntime(deps: CodexHomeDeps = {}): boolean {
|
|
|
40
86
|
return /microsoft|wsl/i.test(version);
|
|
41
87
|
}
|
|
42
88
|
|
|
43
|
-
|
|
44
|
-
|
|
89
|
+
/** All Windows-profile `.codex` homes (with config.toml) visible from WSL, resolved real paths. */
|
|
90
|
+
export function listWslWindowsCodexHomes(deps: CodexHomeDeps = {}): string[] {
|
|
91
|
+
if (!isWslRuntime(deps)) return [];
|
|
45
92
|
const exists = deps.existsSync ?? existsSync;
|
|
46
93
|
const stat = deps.statSync ?? statSync;
|
|
47
94
|
const readdir = deps.readdirSync ?? readdirSync;
|
|
48
95
|
const realpath = deps.realpathSync ?? realpathSync.native;
|
|
49
|
-
const
|
|
50
|
-
|
|
51
|
-
|
|
96
|
+
const automountRoot = wslAutomountRoot(deps);
|
|
97
|
+
// WSL mount paths are POSIX by definition; keep separators stable on any host.
|
|
98
|
+
const usersRoot = deps.usersRoot ?? posix.join(automountRoot, "c", "Users");
|
|
99
|
+
if (!exists(usersRoot)) return [];
|
|
52
100
|
|
|
53
|
-
const explicitProfile = windowsUserProfileToWslPath(env.USERPROFILE);
|
|
54
101
|
const candidates = [];
|
|
55
102
|
try {
|
|
56
103
|
for (const user of readdir(usersRoot)) {
|
|
57
104
|
if (user === "Default" || user === "Default User" || user === "Public" || user === "All Users") continue;
|
|
58
|
-
const home = join(usersRoot, user, ".codex");
|
|
59
|
-
const config = join(home, "config.toml");
|
|
105
|
+
const home = posix.join(usersRoot, user, ".codex");
|
|
106
|
+
const config = posix.join(home, "config.toml");
|
|
60
107
|
if (!exists(config)) continue;
|
|
61
108
|
try {
|
|
62
109
|
if (stat(home).isDirectory()) candidates.push(realpath(home));
|
|
@@ -65,11 +112,19 @@ export function findWslWindowsCodexHome(deps: CodexHomeDeps = {}): string | null
|
|
|
65
112
|
}
|
|
66
113
|
}
|
|
67
114
|
} catch {
|
|
68
|
-
return
|
|
115
|
+
return [];
|
|
69
116
|
}
|
|
117
|
+
return candidates;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function findWslWindowsCodexHome(deps: CodexHomeDeps = {}): string | null {
|
|
121
|
+
const env = deps.env ?? process.env;
|
|
122
|
+
const candidates = listWslWindowsCodexHomes(deps);
|
|
123
|
+
if (candidates.length === 0) return null;
|
|
70
124
|
|
|
125
|
+
const explicitProfile = windowsUserProfileToWslPath(env.USERPROFILE, wslAutomountRoot(deps));
|
|
71
126
|
if (explicitProfile) {
|
|
72
|
-
const explicitHome = join(explicitProfile, ".codex");
|
|
127
|
+
const explicitHome = posix.join(explicitProfile, ".codex");
|
|
73
128
|
const match = candidates.find(candidate => candidate === explicitHome || candidate.endsWith(`/${explicitProfile.split("/").pop()}/.codex`));
|
|
74
129
|
if (match) return match;
|
|
75
130
|
}
|
package/src/codex/shim.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { delimiter, dirname, extname, join } from "node:path";
|
|
1
|
+
import { delimiter, dirname, extname, join, posix } from "node:path";
|
|
2
2
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { getConfigDir } from "../config";
|
|
4
4
|
import { durableBunPath } from "../lib/bun-runtime";
|
|
5
5
|
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
6
6
|
import { windowsEnvIndirectBatchValue } from "../lib/win-paths";
|
|
7
|
+
import { isWslRuntime, wslAutomountRoot } from "./home";
|
|
7
8
|
|
|
8
9
|
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
9
10
|
let lastShimDiscoveryError: string | null = null;
|
|
11
|
+
/** Last human-readable reason discovery returned null (exposed for doctor/tests). */
|
|
12
|
+
export function lastCodexDiscoveryError(): string | null {
|
|
13
|
+
return lastShimDiscoveryError;
|
|
14
|
+
}
|
|
10
15
|
const CODEX_INTERNAL_COMMANDS = [
|
|
11
16
|
"app-server",
|
|
12
17
|
"archive",
|
|
@@ -66,18 +71,77 @@ function isShim(path: string): boolean {
|
|
|
66
71
|
}
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
74
|
+
/**
|
|
75
|
+
* A PATH entry that reaches Windows through WSL drive interop
|
|
76
|
+
* (`<automount-root>/<drive>/...`; root defaults to /mnt, configurable via
|
|
77
|
+
* /etc/wsl.conf [automount] root).
|
|
78
|
+
*/
|
|
79
|
+
export function isWindowsInteropDir(dir: string, automountRoot = "/mnt"): boolean {
|
|
80
|
+
const root = automountRoot.replace(/\/+$/, "");
|
|
81
|
+
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
82
|
+
return new RegExp(`^${escaped}/[a-z](/|$)`, "i").test(dir);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type CodexPathScanDeps = {
|
|
86
|
+
pathValue?: string;
|
|
87
|
+
wsl?: boolean;
|
|
88
|
+
/** Treat PATH entries as POSIX paths (WSL context). Defaults to wsl || non-win32. */
|
|
89
|
+
posixPaths?: boolean;
|
|
90
|
+
automountRoot?: string;
|
|
91
|
+
exists?: (path: string) => boolean;
|
|
92
|
+
isShimFile?: (path: string) => boolean;
|
|
93
|
+
isDirectory?: (path: string) => boolean;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function realIsDirectory(path: string): boolean {
|
|
97
|
+
try {
|
|
98
|
+
return lstatSync(path).isDirectory();
|
|
99
|
+
} catch {
|
|
100
|
+
return true; // unreadable -> treat as unusable
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function findCodexOnPath(deps: CodexPathScanDeps = {}): string | null {
|
|
105
|
+
lastShimDiscoveryError = null;
|
|
106
|
+
const exists = deps.exists ?? existsSync;
|
|
107
|
+
const shimFile = deps.isShimFile ?? isShim;
|
|
108
|
+
const isDir = deps.isDirectory ?? realIsDirectory;
|
|
109
|
+
const wsl = deps.wsl ?? (process.platform === "linux" && isWslRuntime());
|
|
110
|
+
const usePosix = deps.posixPaths ?? (wsl || process.platform !== "win32");
|
|
111
|
+
const joinPath = usePosix ? posix.join : join;
|
|
112
|
+
const pathSep = usePosix ? ":" : delimiter;
|
|
113
|
+
const automountRoot = deps.automountRoot ?? (wsl ? wslAutomountRoot() : "/mnt");
|
|
114
|
+
// Windows npm prefixes ship codex.exe/codex.cmd next to the extensionless sh launcher.
|
|
115
|
+
const interopNames = ["codex", "codex.exe", "codex.cmd", "codex.ps1"];
|
|
116
|
+
let skippedInterop: string | null = null;
|
|
117
|
+
|
|
118
|
+
for (const dir of (deps.pathValue ?? process.env.PATH ?? "").split(pathSep).filter(Boolean)) {
|
|
119
|
+
if (wsl && isWindowsInteropDir(dir, automountRoot)) {
|
|
120
|
+
// A Windows-side codex reached through WSL PATH interop: a Unix shim written
|
|
121
|
+
// here would embed WSL-only paths and break every Windows-side invocation.
|
|
122
|
+
if (!skippedInterop) {
|
|
123
|
+
for (const name of interopNames) {
|
|
124
|
+
const path = joinPath(dir, name);
|
|
125
|
+
if (exists(path) && !shimFile(path) && !isDir(path)) { skippedInterop = path; break; }
|
|
126
|
+
}
|
|
78
127
|
}
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
// Interop dirs carry Windows launcher names even when the scan is not skipping them.
|
|
131
|
+
const names = isWindowsInteropDir(dir, automountRoot) ? interopNames : commandNames("codex");
|
|
132
|
+
for (const name of names) {
|
|
133
|
+
const path = joinPath(dir, name);
|
|
134
|
+
if (!exists(path) || shimFile(path)) continue;
|
|
135
|
+
if (!isDir(path)) return path;
|
|
79
136
|
}
|
|
80
137
|
}
|
|
138
|
+
|
|
139
|
+
if (skippedInterop) {
|
|
140
|
+
lastShimDiscoveryError =
|
|
141
|
+
`Found a Windows codex at ${skippedInterop} via WSL PATH interop, but no Linux-side codex. ` +
|
|
142
|
+
"Refusing to shim a Windows launcher from WSL (a WSL shim breaks Windows invocations). " +
|
|
143
|
+
"Install codex inside WSL (npm i -g @openai/codex), or run 'ocx ensure' from Windows to shim the Windows side.";
|
|
144
|
+
}
|
|
81
145
|
return null;
|
|
82
146
|
}
|
|
83
147
|
|
package/src/lib/gcp-adc.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Direct WebCrypto + REST implementation (no `google-auth-library`). Sources, in priority order:
|
|
5
5
|
* 1. `GOOGLE_APPLICATION_CREDENTIALS` env → file with `type: "service_account"` (RS256 JWT
|
|
6
6
|
* exchange) or `type: "authorized_user"` (refresh-token exchange).
|
|
7
|
-
* 2.
|
|
7
|
+
* 2. gcloud user ADC (`CLOUDSDK_CONFIG`, else `%APPDATA%\gcloud` on Windows, else
|
|
8
|
+
* `~/.config/gcloud`) `application_default_credentials.json` (authorized_user flow).
|
|
8
9
|
* 3. GCE / Cloud Run metadata server.
|
|
9
10
|
*
|
|
10
11
|
* Tokens are cached per source key and refreshed `GOOGLE_VERTEX_REFRESH_SKEW_MS` (default 60s)
|
|
@@ -65,8 +66,18 @@ function getRefreshSkewMs(): number {
|
|
|
65
66
|
return Number.isFinite(raw) && raw > 0 ? raw : 60_000;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/** gcloud config dir, matching the SDK's own resolution order. */
|
|
70
|
+
function gcloudConfigDir(): string {
|
|
71
|
+
const explicit = process.env.CLOUDSDK_CONFIG?.trim();
|
|
72
|
+
if (explicit) return explicit;
|
|
73
|
+
if (process.platform === "win32" && process.env.APPDATA) {
|
|
74
|
+
return path.join(process.env.APPDATA, "gcloud");
|
|
75
|
+
}
|
|
76
|
+
return path.join(os.homedir(), ".config", "gcloud");
|
|
77
|
+
}
|
|
78
|
+
|
|
68
79
|
function userAdcPath(): string {
|
|
69
|
-
return path.join(
|
|
80
|
+
return path.join(gcloudConfigDir(), "application_default_credentials.json");
|
|
70
81
|
}
|
|
71
82
|
|
|
72
83
|
/**
|
package/src/oauth/anthropic.ts
CHANGED
|
@@ -137,8 +137,8 @@ export async function loginAnthropic(
|
|
|
137
137
|
} else if (importLocal === "only") {
|
|
138
138
|
throw new Error(
|
|
139
139
|
process.platform === "darwin"
|
|
140
|
-
? "No Claude Code token found
|
|
141
|
-
: "Claude Code
|
|
140
|
+
? "No Claude Code token found (Keychain or ~/.claude/.credentials.json). Run 'ocx login anthropic' for browser OAuth."
|
|
141
|
+
: "No Claude Code token found (~/.claude/.credentials.json). Run 'ocx login anthropic' for browser OAuth.",
|
|
142
142
|
);
|
|
143
143
|
}
|
|
144
144
|
}
|
|
@@ -39,8 +39,14 @@ export function detectGrokCliToken(): OAuthCredentials | null {
|
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
function
|
|
42
|
+
/** Claude Code config dir: `CLAUDE_CONFIG_DIR` override, else `~/.claude`. */
|
|
43
|
+
function claudeConfigDir(): string {
|
|
44
|
+
const explicit = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
45
|
+
return explicit ? explicit : join(homedir(), ".claude");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Read the Claude Code OAuth credential from the macOS Keychain (darwin only). */
|
|
49
|
+
function readClaudeKeychain(): string | null {
|
|
44
50
|
if (process.platform !== "darwin") return null;
|
|
45
51
|
try {
|
|
46
52
|
return execSync(`security find-generic-password -s "${CLAUDE_KEYCHAIN_SERVICE}" -w`, {
|
|
@@ -51,9 +57,28 @@ function readClaudeSecureStorage(): string | null {
|
|
|
51
57
|
}
|
|
52
58
|
}
|
|
53
59
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Read the Claude Code credential file (`<config-dir>/.credentials.json`).
|
|
62
|
+
* Claude Code writes this on Linux/Windows (and on macOS when the Keychain is
|
|
63
|
+
* unavailable); it carries the same `claudeAiOauth` payload as the Keychain item.
|
|
64
|
+
* Exported for tests.
|
|
65
|
+
*/
|
|
66
|
+
export function readClaudeCredentialsFile(): string | null {
|
|
67
|
+
const path = join(claudeConfigDir(), ".credentials.json");
|
|
68
|
+
try {
|
|
69
|
+
if (!existsSync(path)) return null;
|
|
70
|
+
return readFileSync(path, "utf8").trim();
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Keychain first on macOS, then the cross-platform credentials file. */
|
|
77
|
+
function readClaudeSecureStorage(): string | null {
|
|
78
|
+
return readClaudeKeychain() ?? readClaudeCredentialsFile();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function parseClaudeOauthPayload(raw: string): OAuthCredentials | null {
|
|
57
82
|
try {
|
|
58
83
|
const data = JSON.parse(raw) as { claudeAiOauth?: { accessToken?: string; refreshToken?: string; expiresAt?: number } };
|
|
59
84
|
const o = data.claudeAiOauth;
|
|
@@ -63,3 +88,9 @@ export function detectClaudeCodeToken(): OAuthCredentials | null {
|
|
|
63
88
|
return null;
|
|
64
89
|
}
|
|
65
90
|
}
|
|
91
|
+
|
|
92
|
+
export function detectClaudeCodeToken(): OAuthCredentials | null {
|
|
93
|
+
const raw = readClaudeSecureStorage();
|
|
94
|
+
if (!raw) return null;
|
|
95
|
+
return parseClaudeOauthPayload(raw);
|
|
96
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { dirname, join, resolve } from "node:path";
|
|
|
12
12
|
import { expandUserPath, getConfigDir, readPid, removePid, removeRuntimePort } from "./config";
|
|
13
13
|
import { loadConfig } from "./config";
|
|
14
14
|
import { restoreNativeCodex } from "./codex/inject";
|
|
15
|
+
import { isWslRuntime } from "./codex/home";
|
|
15
16
|
import { durableBunPath, durableBunRuntime } from "./lib/bun-runtime";
|
|
16
17
|
import { isProcessAlive, stopProxy } from "./lib/process-control";
|
|
17
18
|
import { serviceApiTokenFilePath } from "./lib/service-secrets";
|
|
@@ -572,6 +573,9 @@ function platformOps(): ServiceOps | null {
|
|
|
572
573
|
}
|
|
573
574
|
if (!isSystemd() && !existsSync(unitPath())) {
|
|
574
575
|
console.error("systemd not found. Run 'ocx start' under your process supervisor.");
|
|
576
|
+
if (isWslRuntime()) {
|
|
577
|
+
console.error("WSL detected: enable systemd by adding [boot] systemd=true to /etc/wsl.conf, then run 'wsl --shutdown' from Windows and reopen the distro (WSL 0.67.6+).");
|
|
578
|
+
}
|
|
575
579
|
process.exit(1);
|
|
576
580
|
}
|
|
577
581
|
return { install: installSystemd, start: startSystemd, stop: stopSystemd, status: statusSystemd, uninstall: uninstallSystemd };
|