@cortexkit/opencode-openai-auth 0.3.2 → 0.3.4

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 (41) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/commands.d.ts +3 -1
  3. package/dist/core/accounts.d.ts +6 -0
  4. package/dist/core/cachekeep.d.ts +16 -0
  5. package/dist/core/oauth.d.ts +1 -1
  6. package/dist/core/quota-manager.d.ts +33 -1
  7. package/dist/{index-gwmhhrrc.js → index-1xmasv0x.js} +1 -1
  8. package/dist/{index-899gpxdd.js → index-5s67238f.js} +1 -1
  9. package/dist/{index-53j7bdev.js → index-e7m7qbne.js} +1 -1
  10. package/dist/{index-7w467pz8.js → index-k5zwf3j1.js} +1 -1
  11. package/dist/{index-qp2nja20.js → index-mrg2fms9.js} +2 -2
  12. package/dist/{index-jn8xd8fy.js → index-qdh7pt5a.js} +1 -1
  13. package/dist/index-y8d01tgn.js +92 -0
  14. package/dist/index.js +68 -62
  15. package/dist/{quota-normalize-dxftsgep.js → quota-normalize-dfdpwwm3.js} +1 -1
  16. package/dist/rpc/port-file.js +1 -1
  17. package/dist/rpc/rpc-client.js +1 -1
  18. package/dist/rpc/rpc-dir.js +1 -1
  19. package/dist/sidebar-state.js +1 -1
  20. package/dist/{token-28n5g707.js → token-01dcf92a.js} +1 -1
  21. package/dist/token-util-rkqd82q9.js +1 -0
  22. package/dist/tui-preferences.js +1 -1
  23. package/dist/version.d.ts +1 -1
  24. package/dist/ws-pool.d.ts +11 -0
  25. package/dist/ws.d.ts +1 -1
  26. package/package.json +6 -4
  27. package/src/tui/command-dialogs.tsx +77 -0
  28. package/src/tui/entry.mjs +37 -0
  29. package/src/tui-compiled/logger.ts +161 -0
  30. package/src/tui-compiled/rpc/port-file.ts +70 -0
  31. package/src/tui-compiled/rpc/protocol.ts +31 -0
  32. package/src/tui-compiled/rpc/rpc-client.ts +78 -0
  33. package/src/tui-compiled/rpc/rpc-dir.ts +22 -0
  34. package/src/tui-compiled/sidebar-state.ts +316 -0
  35. package/src/tui-compiled/tui/command-dialogs.tsx +640 -0
  36. package/src/tui-compiled/tui-preferences.ts +328 -0
  37. package/src/tui-compiled/tui.tsx +1041 -0
  38. package/src/tui-compiled/util/open-url.ts +30 -0
  39. package/src/tui.tsx +16 -13
  40. package/dist/index-z21wp7ac.js +0 -92
  41. package/dist/token-util-gmr2b13z.js +0 -1
@@ -1 +1 @@
1
- import{c as a,d as b,e as c,f as d}from"./index-d9hq0tmc.js";import"./index-53j7bdev.js";export{a as toResetIso,c as normalizeWsFrame,d as normalizeWham,b as normalizeQuotaHeaders};
1
+ import{c as a,d as b,e as c,f as d}from"./index-d9hq0tmc.js";import"./index-e7m7qbne.js";export{a as toResetIso,c as normalizeWsFrame,d as normalizeWham,b as normalizeQuotaHeaders};
@@ -1 +1 @@
1
- import{ca as a,da as b}from"../index-7w467pz8.js";import"../index-53j7bdev.js";export{a as writePortFile,b as discoverPortFile};
1
+ import{da as a,ea as b}from"../index-k5zwf3j1.js";import"../index-e7m7qbne.js";export{a as writePortFile,b as discoverPortFile};
@@ -1 +1 @@
1
- import{da as c}from"../index-7w467pz8.js";import"../index-53j7bdev.js";async function p(n,r,l,o,i){let t=await c(n,r);if(l?.(t),!t)return null;let e=new AbortController,s=setTimeout(()=>e.abort(),2000);try{let a=await fetch(`http://127.0.0.1:${t.port}/rpc/${o}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${t.token}`},body:JSON.stringify(i),signal:e.signal});if(!a.ok)return null;return await a.json()}catch{return null}finally{clearTimeout(s)}}function y(n,r,l){let o=!1,i=(t)=>{if(o)return;o=!0,l?.(t)};return{async pending(t,e){return(await p(n,r,i,"pending-notifications",{lastReceivedId:t,sessionId:e}))?.messages??[]},async apply(t){return await p(n,r,i,"apply",{...t})??{text:"apply failed",knobs:{}}}}}export{y as createRpcClient};
1
+ import{ea as c}from"../index-k5zwf3j1.js";import"../index-e7m7qbne.js";async function p(n,r,l,o,i){let t=await c(n,r);if(l?.(t),!t)return null;let e=new AbortController,s=setTimeout(()=>e.abort(),2000);try{let a=await fetch(`http://127.0.0.1:${t.port}/rpc/${o}`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${t.token}`},body:JSON.stringify(i),signal:e.signal});if(!a.ok)return null;return await a.json()}catch{return null}finally{clearTimeout(s)}}function y(n,r,l){let o=!1,i=(t)=>{if(o)return;o=!0,l?.(t)};return{async pending(t,e){return(await p(n,r,i,"pending-notifications",{lastReceivedId:t,sessionId:e}))?.messages??[]},async apply(t){return await p(n,r,i,"apply",{...t})??{text:"apply failed",knobs:{}}}}}export{y as createRpcClient};
@@ -1 +1 @@
1
- import{ea as a,fa as b}from"../index-gwmhhrrc.js";import"../index-53j7bdev.js";export{a as tmpdir,b as getRpcDir};
1
+ import{fa as a,ga as b}from"../index-1xmasv0x.js";import"../index-e7m7qbne.js";export{a as tmpdir,b as getRpcDir};
@@ -1 +1 @@
1
- import{Q as a,R as b,S as c,T as d,U as e,V as f,W as g,X as h,Y as i,Z as j,_ as k}from"./index-qp2nja20.js";import"./index-jn8xd8fy.js";import"./index-53j7bdev.js";export{e as setSidebarState,g as resolveActiveAccount,c as normalizeSidebarState,a as getSidebarStateFile,d as getSidebarState,h as getCollapsedQuotaSummary,f as drainSidebarWrites,k as computeQuotaPacing,j as SEVEN_DAY_MS,i as FIVE_HOUR_MS,b as DEFAULT_SIDEBAR_STATE};
1
+ import{$ as k,R as a,S as b,T as c,U as d,V as e,W as f,X as g,Y as h,Z as i,_ as j}from"./index-mrg2fms9.js";import"./index-qdh7pt5a.js";import"./index-e7m7qbne.js";export{e as setSidebarState,g as resolveActiveAccount,c as normalizeSidebarState,a as getSidebarStateFile,d as getSidebarState,h as getCollapsedQuotaSummary,f as drainSidebarWrites,k as computeQuotaPacing,j as SEVEN_DAY_MS,i as FIVE_HOUR_MS,b as DEFAULT_SIDEBAR_STATE};
@@ -1 +1 @@
1
- import{a as U,b as V}from"./index-899gpxdd.js";import{ha as R}from"./index-53j7bdev.js";var E=R((M,Q)=>{var{defineProperty:K,getOwnPropertyDescriptor:W,getOwnPropertyNames:X}=Object,Y=Object.prototype.hasOwnProperty,Z=(q,v)=>{for(var z in v)K(q,z,{get:v[z],enumerable:!0})},$=(q,v,z,F)=>{if(v&&typeof v==="object"||typeof v==="function"){for(let G of X(v))if(!Y.call(q,G)&&G!==z)K(q,G,{get:()=>v[G],enumerable:!(F=W(v,G))||F.enumerable})}return q},A=(q)=>$(K({},"__esModule",{value:!0}),q),L={};Z(L,{refreshToken:()=>D});Q.exports=A(L);var H=U(),B=V();async function D(){let{projectId:q,teamId:v}=(0,B.findProjectInfo)(),z=(0,B.loadToken)(q);if(!z||(0,B.isExpired)((0,B.getTokenPayload)(z.token))){let F=await(0,B.getVercelCliToken)();if(!F)throw new H.VercelOidcTokenError("Failed to refresh OIDC token: Log in to Vercel CLI and link your project with `vc link`");if(!q)throw new H.VercelOidcTokenError("Failed to refresh OIDC token: Try re-linking your project with `vc link`");if(z=await(0,B.getVercelOidcToken)(F,q,v),!z)throw new H.VercelOidcTokenError("Failed to refresh OIDC token");(0,B.saveToken)(z,q)}process.env.VERCEL_OIDC_TOKEN=z.token;return}});export default E();
1
+ import{a as U,b as V}from"./index-5s67238f.js";import{ia as R}from"./index-e7m7qbne.js";var E=R((M,Q)=>{var{defineProperty:K,getOwnPropertyDescriptor:W,getOwnPropertyNames:X}=Object,Y=Object.prototype.hasOwnProperty,Z=(q,v)=>{for(var z in v)K(q,z,{get:v[z],enumerable:!0})},$=(q,v,z,F)=>{if(v&&typeof v==="object"||typeof v==="function"){for(let G of X(v))if(!Y.call(q,G)&&G!==z)K(q,G,{get:()=>v[G],enumerable:!(F=W(v,G))||F.enumerable})}return q},A=(q)=>$(K({},"__esModule",{value:!0}),q),L={};Z(L,{refreshToken:()=>D});Q.exports=A(L);var H=U(),B=V();async function D(){let{projectId:q,teamId:v}=(0,B.findProjectInfo)(),z=(0,B.loadToken)(q);if(!z||(0,B.isExpired)((0,B.getTokenPayload)(z.token))){let F=await(0,B.getVercelCliToken)();if(!F)throw new H.VercelOidcTokenError("Failed to refresh OIDC token: Log in to Vercel CLI and link your project with `vc link`");if(!q)throw new H.VercelOidcTokenError("Failed to refresh OIDC token: Try re-linking your project with `vc link`");if(z=await(0,B.getVercelOidcToken)(F,q,v),!z)throw new H.VercelOidcTokenError("Failed to refresh OIDC token");(0,B.saveToken)(z,q)}process.env.VERCEL_OIDC_TOKEN=z.token;return}});export default E();
@@ -0,0 +1 @@
1
+ import{b as a}from"./index-5s67238f.js";import"./index-e7m7qbne.js";export default a();
@@ -1,4 +1,4 @@
1
- import{ga as P,ha as C,ja as _}from"./index-53j7bdev.js";var g=C((T,L)=>{(function(Q){if(typeof L==="object"&&typeof L.exports==="object"){var J=Q(_,T);if(J!==void 0)L.exports=J}else if(typeof define==="function"&&define.amd)define(["require","exports","./impl/format","./impl/edit","./impl/scanner","./impl/parser"],Q)})(function(Q,J){Object.defineProperty(J,"__esModule",{value:!0}),J.applyEdits=J.modify=J.format=J.printParseErrorCode=J.ParseErrorCode=J.stripComments=J.visit=J.getNodeValue=J.getNodePath=J.findNodeAtOffset=J.findNodeAtLocation=J.parseTree=J.parse=J.getLocation=J.SyntaxKind=J.ScanError=J.createScanner=void 0;let X=Q("./impl/format"),Z=Q("./impl/edit"),G=Q("./impl/scanner"),$=Q("./impl/parser");J.createScanner=G.createScanner;var W;(function(z){z[z.None=0]="None",z[z.UnexpectedEndOfComment=1]="UnexpectedEndOfComment",z[z.UnexpectedEndOfString=2]="UnexpectedEndOfString",z[z.UnexpectedEndOfNumber=3]="UnexpectedEndOfNumber",z[z.InvalidUnicode=4]="InvalidUnicode",z[z.InvalidEscapeCharacter=5]="InvalidEscapeCharacter",z[z.InvalidCharacter=6]="InvalidCharacter"})(W||(J.ScanError=W={}));var H;(function(z){z[z.OpenBraceToken=1]="OpenBraceToken",z[z.CloseBraceToken=2]="CloseBraceToken",z[z.OpenBracketToken=3]="OpenBracketToken",z[z.CloseBracketToken=4]="CloseBracketToken",z[z.CommaToken=5]="CommaToken",z[z.ColonToken=6]="ColonToken",z[z.NullKeyword=7]="NullKeyword",z[z.TrueKeyword=8]="TrueKeyword",z[z.FalseKeyword=9]="FalseKeyword",z[z.StringLiteral=10]="StringLiteral",z[z.NumericLiteral=11]="NumericLiteral",z[z.LineCommentTrivia=12]="LineCommentTrivia",z[z.BlockCommentTrivia=13]="BlockCommentTrivia",z[z.LineBreakTrivia=14]="LineBreakTrivia",z[z.Trivia=15]="Trivia",z[z.Unknown=16]="Unknown",z[z.EOF=17]="EOF"})(H||(J.SyntaxKind=H={})),J.getLocation=$.getLocation,J.parse=$.parse,J.parseTree=$.parseTree,J.findNodeAtLocation=$.findNodeAtLocation,J.findNodeAtOffset=$.findNodeAtOffset,J.getNodePath=$.getNodePath,J.getNodeValue=$.getNodeValue,J.visit=$.visit,J.stripComments=$.stripComments;var q;(function(z){z[z.InvalidSymbol=1]="InvalidSymbol",z[z.InvalidNumberFormat=2]="InvalidNumberFormat",z[z.PropertyNameExpected=3]="PropertyNameExpected",z[z.ValueExpected=4]="ValueExpected",z[z.ColonExpected=5]="ColonExpected",z[z.CommaExpected=6]="CommaExpected",z[z.CloseBraceExpected=7]="CloseBraceExpected",z[z.CloseBracketExpected=8]="CloseBracketExpected",z[z.EndOfFileExpected=9]="EndOfFileExpected",z[z.InvalidCommentToken=10]="InvalidCommentToken",z[z.UnexpectedEndOfComment=11]="UnexpectedEndOfComment",z[z.UnexpectedEndOfString=12]="UnexpectedEndOfString",z[z.UnexpectedEndOfNumber=13]="UnexpectedEndOfNumber",z[z.InvalidUnicode=14]="InvalidUnicode",z[z.InvalidEscapeCharacter=15]="InvalidEscapeCharacter",z[z.InvalidCharacter=16]="InvalidCharacter"})(q||(J.ParseErrorCode=q={}));function D(z){switch(z){case 1:return"InvalidSymbol";case 2:return"InvalidNumberFormat";case 3:return"PropertyNameExpected";case 4:return"ValueExpected";case 5:return"ColonExpected";case 6:return"CommaExpected";case 7:return"CloseBraceExpected";case 8:return"CloseBracketExpected";case 9:return"EndOfFileExpected";case 10:return"InvalidCommentToken";case 11:return"UnexpectedEndOfComment";case 12:return"UnexpectedEndOfString";case 13:return"UnexpectedEndOfNumber";case 14:return"InvalidUnicode";case 15:return"InvalidEscapeCharacter";case 16:return"InvalidCharacter"}return"<unknown ParseErrorCode>"}J.printParseErrorCode=D;function m(z,U,j){return X.format(z,U,j)}J.format=m;function f(z,U,j,A){return Z.setProperty(z,U,j,A)}J.modify=f;function u(z,U){let j=U.slice(0).sort((I,Y)=>{let F=I.offset-Y.offset;if(F===0)return I.length-Y.length;return F}),A=z.length;for(let I=j.length-1;I>=0;I--){let Y=j[I];if(Y.offset+Y.length<=A)z=Z.applyEdit(z,Y);else throw Error("Overlapping edit");A=Y.offset}return z}J.applyEdits=u})});var M=P(g(),1);import{watch as K}from"node:fs";import{mkdir as c,readFile as O,rename as y,writeFile as S}from"node:fs/promises";import{homedir as E}from"node:os";import{basename as l,dirname as v,join as k}from"node:path";var p="OPENCODE_TUI_PREFERENCES_FILE",x="tui-preferences.jsonc";function w(){let Q=process.env[p];if(Q)return Q;let J=process.env.OPENCODE_CONFIG_DIR||k(process.env.XDG_CONFIG_HOME||k(E(),".config"),"opencode");return k(J,x)}function N(Q){return typeof Q==="object"&&Q!==null&&!Array.isArray(Q)}async function Xz(){try{let Q=await O(w(),"utf8"),J=[],X=M.parse(Q,J,{allowTrailingComma:!0});if(J.length>0)return{};return N(X)?X:{}}catch{return{}}}var i="openai-auth",s=160,b={forceToTop:!1,order:s,startCollapsed:!1,rememberCollapsed:!0,collapsed:null,pollMs:1500,refreshDebounceMs:200,header:{label:"OPENAI",showVersion:!0},sections:{quota:!0,fallbackAccounts:!0,routing:!0,health:!0,pacing:!0},appearance:{barWidth:10,barFilledChar:"█",barEmptyChar:"░",warnThreshold:50,errorThreshold:80}};function V(Q,J){return typeof Q==="boolean"?Q:J}function B(Q,J,X,Z){if(typeof Q!=="number"||!Number.isFinite(Q))return J;return Math.min(Math.max(Math.round(Q),X),Z)}function d(Q,J,X){if(typeof Q!=="string"||Q.length===0)return J;return Q.slice(0,X)}function h(Q,J){if(typeof Q!=="string")return J;return[...Q][0]??J}function Zz(Q){let J=Q[i];if(!N(J))return structuredClone(b);let X=b,Z=N(J.header)?J.header:{},G=N(J.sections)?J.sections:{},$=N(J.appearance)?J.appearance:{},W=B($.warnThreshold,X.appearance.warnThreshold,0,99),H=Math.max(B($.errorThreshold,X.appearance.errorThreshold,0,100),Math.min(W+1,100));return{forceToTop:V(J.forceToTop,X.forceToTop),order:B(J.order,X.order,-1e4,1e4),startCollapsed:V(J.startCollapsed,X.startCollapsed),rememberCollapsed:V(J.rememberCollapsed,X.rememberCollapsed),collapsed:typeof J.collapsed==="boolean"?J.collapsed:null,pollMs:B(J.pollMs,X.pollMs,500,30000),refreshDebounceMs:B(J.refreshDebounceMs,X.refreshDebounceMs,50,5000),header:{label:d(Z.label,X.header.label,20),showVersion:V(Z.showVersion,X.header.showVersion)},sections:{quota:V(G.quota,X.sections.quota),fallbackAccounts:V(G.fallbackAccounts,X.sections.fallbackAccounts),routing:V(G.routing,X.sections.routing),health:V(G.health,X.sections.health),pacing:V(G.pacing,X.sections.pacing)},appearance:{barWidth:B($.barWidth,X.appearance.barWidth,4,40),barFilledChar:h($.barFilledChar,X.appearance.barFilledChar),barEmptyChar:h($.barEmptyChar,X.appearance.barEmptyChar),warnThreshold:W,errorThreshold:H}}}var n=-1e5;function $z(Q,J,X){let Z=Q[J];if(!N(Z))return X;if(Z.forceToTop===!0)return n+Object.keys(Q).indexOf(J);return B(Z.order,X,-1e4,1e4)}var t=`// Shared preferences for opencode TUI plugins.
1
+ import{ha as P,ia as C,ka as _}from"./index-e7m7qbne.js";var g=C((T,L)=>{(function(Q){if(typeof L==="object"&&typeof L.exports==="object"){var J=Q(_,T);if(J!==void 0)L.exports=J}else if(typeof define==="function"&&define.amd)define(["require","exports","./impl/format","./impl/edit","./impl/scanner","./impl/parser"],Q)})(function(Q,J){Object.defineProperty(J,"__esModule",{value:!0}),J.applyEdits=J.modify=J.format=J.printParseErrorCode=J.ParseErrorCode=J.stripComments=J.visit=J.getNodeValue=J.getNodePath=J.findNodeAtOffset=J.findNodeAtLocation=J.parseTree=J.parse=J.getLocation=J.SyntaxKind=J.ScanError=J.createScanner=void 0;let X=Q("./impl/format"),Z=Q("./impl/edit"),G=Q("./impl/scanner"),$=Q("./impl/parser");J.createScanner=G.createScanner;var W;(function(z){z[z.None=0]="None",z[z.UnexpectedEndOfComment=1]="UnexpectedEndOfComment",z[z.UnexpectedEndOfString=2]="UnexpectedEndOfString",z[z.UnexpectedEndOfNumber=3]="UnexpectedEndOfNumber",z[z.InvalidUnicode=4]="InvalidUnicode",z[z.InvalidEscapeCharacter=5]="InvalidEscapeCharacter",z[z.InvalidCharacter=6]="InvalidCharacter"})(W||(J.ScanError=W={}));var H;(function(z){z[z.OpenBraceToken=1]="OpenBraceToken",z[z.CloseBraceToken=2]="CloseBraceToken",z[z.OpenBracketToken=3]="OpenBracketToken",z[z.CloseBracketToken=4]="CloseBracketToken",z[z.CommaToken=5]="CommaToken",z[z.ColonToken=6]="ColonToken",z[z.NullKeyword=7]="NullKeyword",z[z.TrueKeyword=8]="TrueKeyword",z[z.FalseKeyword=9]="FalseKeyword",z[z.StringLiteral=10]="StringLiteral",z[z.NumericLiteral=11]="NumericLiteral",z[z.LineCommentTrivia=12]="LineCommentTrivia",z[z.BlockCommentTrivia=13]="BlockCommentTrivia",z[z.LineBreakTrivia=14]="LineBreakTrivia",z[z.Trivia=15]="Trivia",z[z.Unknown=16]="Unknown",z[z.EOF=17]="EOF"})(H||(J.SyntaxKind=H={})),J.getLocation=$.getLocation,J.parse=$.parse,J.parseTree=$.parseTree,J.findNodeAtLocation=$.findNodeAtLocation,J.findNodeAtOffset=$.findNodeAtOffset,J.getNodePath=$.getNodePath,J.getNodeValue=$.getNodeValue,J.visit=$.visit,J.stripComments=$.stripComments;var q;(function(z){z[z.InvalidSymbol=1]="InvalidSymbol",z[z.InvalidNumberFormat=2]="InvalidNumberFormat",z[z.PropertyNameExpected=3]="PropertyNameExpected",z[z.ValueExpected=4]="ValueExpected",z[z.ColonExpected=5]="ColonExpected",z[z.CommaExpected=6]="CommaExpected",z[z.CloseBraceExpected=7]="CloseBraceExpected",z[z.CloseBracketExpected=8]="CloseBracketExpected",z[z.EndOfFileExpected=9]="EndOfFileExpected",z[z.InvalidCommentToken=10]="InvalidCommentToken",z[z.UnexpectedEndOfComment=11]="UnexpectedEndOfComment",z[z.UnexpectedEndOfString=12]="UnexpectedEndOfString",z[z.UnexpectedEndOfNumber=13]="UnexpectedEndOfNumber",z[z.InvalidUnicode=14]="InvalidUnicode",z[z.InvalidEscapeCharacter=15]="InvalidEscapeCharacter",z[z.InvalidCharacter=16]="InvalidCharacter"})(q||(J.ParseErrorCode=q={}));function D(z){switch(z){case 1:return"InvalidSymbol";case 2:return"InvalidNumberFormat";case 3:return"PropertyNameExpected";case 4:return"ValueExpected";case 5:return"ColonExpected";case 6:return"CommaExpected";case 7:return"CloseBraceExpected";case 8:return"CloseBracketExpected";case 9:return"EndOfFileExpected";case 10:return"InvalidCommentToken";case 11:return"UnexpectedEndOfComment";case 12:return"UnexpectedEndOfString";case 13:return"UnexpectedEndOfNumber";case 14:return"InvalidUnicode";case 15:return"InvalidEscapeCharacter";case 16:return"InvalidCharacter"}return"<unknown ParseErrorCode>"}J.printParseErrorCode=D;function m(z,U,j){return X.format(z,U,j)}J.format=m;function f(z,U,j,A){return Z.setProperty(z,U,j,A)}J.modify=f;function u(z,U){let j=U.slice(0).sort((I,Y)=>{let F=I.offset-Y.offset;if(F===0)return I.length-Y.length;return F}),A=z.length;for(let I=j.length-1;I>=0;I--){let Y=j[I];if(Y.offset+Y.length<=A)z=Z.applyEdit(z,Y);else throw Error("Overlapping edit");A=Y.offset}return z}J.applyEdits=u})});var M=P(g(),1);import{watch as K}from"node:fs";import{mkdir as c,readFile as O,rename as y,writeFile as S}from"node:fs/promises";import{homedir as E}from"node:os";import{basename as l,dirname as v,join as k}from"node:path";var p="OPENCODE_TUI_PREFERENCES_FILE",x="tui-preferences.jsonc";function w(){let Q=process.env[p];if(Q)return Q;let J=process.env.OPENCODE_CONFIG_DIR||k(process.env.XDG_CONFIG_HOME||k(E(),".config"),"opencode");return k(J,x)}function N(Q){return typeof Q==="object"&&Q!==null&&!Array.isArray(Q)}async function Xz(){try{let Q=await O(w(),"utf8"),J=[],X=M.parse(Q,J,{allowTrailingComma:!0});if(J.length>0)return{};return N(X)?X:{}}catch{return{}}}var i="openai-auth",s=160,b={forceToTop:!1,order:s,startCollapsed:!1,rememberCollapsed:!0,collapsed:null,pollMs:1500,refreshDebounceMs:200,header:{label:"OPENAI",showVersion:!0},sections:{quota:!0,fallbackAccounts:!0,routing:!0,health:!0,pacing:!0},appearance:{barWidth:10,barFilledChar:"█",barEmptyChar:"░",warnThreshold:50,errorThreshold:80}};function V(Q,J){return typeof Q==="boolean"?Q:J}function B(Q,J,X,Z){if(typeof Q!=="number"||!Number.isFinite(Q))return J;return Math.min(Math.max(Math.round(Q),X),Z)}function d(Q,J,X){if(typeof Q!=="string"||Q.length===0)return J;return Q.slice(0,X)}function h(Q,J){if(typeof Q!=="string")return J;return[...Q][0]??J}function Zz(Q){let J=Q[i];if(!N(J))return structuredClone(b);let X=b,Z=N(J.header)?J.header:{},G=N(J.sections)?J.sections:{},$=N(J.appearance)?J.appearance:{},W=B($.warnThreshold,X.appearance.warnThreshold,0,99),H=Math.max(B($.errorThreshold,X.appearance.errorThreshold,0,100),Math.min(W+1,100));return{forceToTop:V(J.forceToTop,X.forceToTop),order:B(J.order,X.order,-1e4,1e4),startCollapsed:V(J.startCollapsed,X.startCollapsed),rememberCollapsed:V(J.rememberCollapsed,X.rememberCollapsed),collapsed:typeof J.collapsed==="boolean"?J.collapsed:null,pollMs:B(J.pollMs,X.pollMs,500,30000),refreshDebounceMs:B(J.refreshDebounceMs,X.refreshDebounceMs,50,5000),header:{label:d(Z.label,X.header.label,20),showVersion:V(Z.showVersion,X.header.showVersion)},sections:{quota:V(G.quota,X.sections.quota),fallbackAccounts:V(G.fallbackAccounts,X.sections.fallbackAccounts),routing:V(G.routing,X.sections.routing),health:V(G.health,X.sections.health),pacing:V(G.pacing,X.sections.pacing)},appearance:{barWidth:B($.barWidth,X.appearance.barWidth,4,40),barFilledChar:h($.barFilledChar,X.appearance.barFilledChar),barEmptyChar:h($.barEmptyChar,X.appearance.barEmptyChar),warnThreshold:W,errorThreshold:H}}}var n=-1e5;function $z(Q,J,X){let Z=Q[J];if(!N(Z))return X;if(Z.forceToTop===!0)return n+Object.keys(Q).indexOf(J);return B(Z.order,X,-1e4,1e4)}var t=`// Shared preferences for opencode TUI plugins.
2
2
  // One top-level key per plugin (short name). See each plugin's README for
3
3
  // its supported settings. This file is safe to hand-edit; plugins update
4
4
  // individual keys in place and preserve comments.
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const PackageVersion = "0.3.2";
1
+ export declare const PackageVersion = "0.3.4";
package/dist/ws-pool.d.ts CHANGED
@@ -20,6 +20,17 @@ export interface CreateWebSocketFetchOptions {
20
20
  * shared mutable global.
21
21
  */
22
22
  onQuota?: (s: Record<string, unknown>, accessToken: string, accountId: string | undefined, servedChatgptAccountId: string | undefined) => void;
23
+ /**
24
+ * Fires when a response.failed frame carries a rate_limit_reached_type —
25
+ * mid-stream quota exhaustion the reactive HTTP-status fallback cannot see
26
+ * on the WebSocket transport (ws.ts synthesizes status:200 at upgrade,
27
+ * before generation runs).
28
+ *
29
+ * Receives the window label plus the per-request internal quota account key
30
+ * captured at send time, same as onQuota, so the signal is attributed to
31
+ * the connection's own account rather than a shared mutable global.
32
+ */
33
+ onRateLimitReached?: (window: string, accountId: string | undefined) => void;
23
34
  }
24
35
  interface PoolEntry {
25
36
  socket?: WebSocket;
package/dist/ws.d.ts CHANGED
@@ -30,7 +30,7 @@ export interface StreamResponsesWebSocketOptions {
30
30
  onAbort?: (error: Error) => void;
31
31
  /** Push per-turn quota from a codex.rate_limits in-band frame. */
32
32
  onQuota?: (s: Record<string, unknown>) => void;
33
- /** Display+classify only: called when response.failed carries a rate_limit_reached_type. */
33
+ /** Called when response.failed carries a rate_limit_reached_type — the only mid-stream quota-exhaustion signal on this transport. */
34
34
  onRateLimitReached?: (window: string) => void;
35
35
  }
36
36
  export interface WrappedError {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-openai-auth",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "./tui": {
22
22
  "types": "./dist/tui.d.ts",
23
- "import": "./src/tui.tsx"
23
+ "import": "./src/tui/entry.mjs"
24
24
  },
25
25
  "./tui-prefs": {
26
26
  "types": "./dist/tui-preferences.d.ts",
@@ -37,7 +37,8 @@
37
37
  "files": [
38
38
  "dist",
39
39
  "src/tui.tsx",
40
- "src/tui/command-dialogs.tsx",
40
+ "src/tui",
41
+ "src/tui-compiled",
41
42
  "src/sidebar-state.ts",
42
43
  "src/tui-preferences.ts",
43
44
  "src/logger.ts",
@@ -47,7 +48,8 @@
47
48
  "LICENSE"
48
49
  ],
49
50
  "scripts": {
50
- "build": "rm -rf dist && bun build src/index.ts src/cli.ts src/sidebar-state.ts src/tui-preferences.ts src/rpc/rpc-client.ts src/rpc/port-file.ts src/rpc/protocol.ts src/rpc/rpc-dir.ts --outdir dist --target node --format esm --splitting --external @opencode-ai/plugin --minify && tsc -p tsconfig.build.json --emitDeclarationOnly",
51
+ "build": "rm -rf dist && bun build src/index.ts src/cli.ts src/sidebar-state.ts src/tui-preferences.ts src/rpc/rpc-client.ts src/rpc/port-file.ts src/rpc/protocol.ts src/rpc/rpc-dir.ts --outdir dist --target node --format esm --splitting --external @opencode-ai/plugin --minify && tsc -p tsconfig.build.json --emitDeclarationOnly && bun run build:tui",
52
+ "build:tui": "bun scripts/build-tui.ts",
51
53
  "build:dev": "rm -rf dist && tsc -p tsconfig.build.json",
52
54
  "test": "bun test src/tests",
53
55
  "typecheck": "tsc",
@@ -20,6 +20,12 @@ export function buildCachekeepDialogOptions(payload: OpenDialogPayload) {
20
20
  const maxSubagentIdleMs = Number(
21
21
  payload.knobs.maxSubagentIdleMs ?? 30 * 60 * 1000,
22
22
  )
23
+ const windowKnob = payload.knobs.window as
24
+ | { startHour: number; endHour: number }
25
+ | undefined
26
+ const windowLabel = windowKnob
27
+ ? `${String(windowKnob.startHour).padStart(2, '0')}-${String(windowKnob.endHour).padStart(2, '0')}`
28
+ : 'always'
23
29
  const lastWarm = lastWarmAt
24
30
  ? `${Math.ceil((generatedAt - lastWarmAt) / 1000)}s ago`
25
31
  : 'none yet'
@@ -30,6 +36,7 @@ export function buildCachekeepDialogOptions(payload: OpenDialogPayload) {
30
36
  `timer ${running ? 'armed' : 'idle'}`,
31
37
  `${tracked} tracked`,
32
38
  `last warm ${lastWarm}`,
39
+ `window ${windowLabel}`,
33
40
  `${idleWindow}m idle cap`,
34
41
  `subagent idle ${subIdleWindow}m`,
35
42
  ].filter((part) => part.length > 0)
@@ -54,6 +61,20 @@ export function buildCachekeepDialogOptions(payload: OpenDialogPayload) {
54
61
  ? `Warm subagent sessions (${subIdleWindow}m idle cap). Disable to skip subagents.`
55
62
  : `Skip subagent sessions. Enable to warm them too (${subIdleWindow}m idle cap).`,
56
63
  },
64
+ {
65
+ title: windowKnob ? `Warm window: ${windowLabel}` : 'Set warm window…',
66
+ value: 'set_window',
67
+ description: windowKnob
68
+ ? `Currently only warming between ${windowLabel} local hours — pick to change`
69
+ : 'Restrict capture + warm to a clock-hour window, e.g. 9-18 or 22-6',
70
+ },
71
+ {
72
+ title: 'Clear warm window',
73
+ value: 'clear_window',
74
+ description: windowKnob
75
+ ? 'Remove the clock-hour restriction — return to always-warm (within idle caps)'
76
+ : 'No clock-hour window is currently set',
77
+ },
57
78
  {
58
79
  title: 'Refresh status',
59
80
  value: 'refresh',
@@ -76,6 +97,44 @@ function showText(api: TuiPluginApi, text: string) {
76
97
  ))
77
98
  }
78
99
 
100
+ function showSetCachekeepWindowPrompt(
101
+ api: TuiPluginApi,
102
+ apply: ApplyFn,
103
+ state: OpenDialogPayload,
104
+ render: (state: OpenDialogPayload) => void,
105
+ ) {
106
+ const DialogPrompt = api.ui.DialogPrompt
107
+ const knobWindow = state.knobs.window as
108
+ | { startHour: number; endHour: number }
109
+ | undefined
110
+ const seed = knobWindow ? `${knobWindow.startHour}-${knobWindow.endHour}` : ''
111
+ api.ui.dialog.setSize('xlarge')
112
+ api.ui.dialog.replace(() => (
113
+ <DialogPrompt
114
+ title='Cachekeep warm window'
115
+ description={() => <text>{state.text}</text>}
116
+ placeholder='HH-HH (e.g. 9-18 or 22-6)'
117
+ value={seed}
118
+ onConfirm={(value: string) => {
119
+ const trimmed = value.trim()
120
+ if (!trimmed) {
121
+ render(state)
122
+ return
123
+ }
124
+ void apply('openai-cachekeep', trimmed).then((r) => {
125
+ api.ui.toast({ message: r.text })
126
+ render({
127
+ command: 'openai-cachekeep',
128
+ text: r.text,
129
+ knobs: r.knobs,
130
+ })
131
+ })
132
+ }}
133
+ onCancel={() => render(state)}
134
+ />
135
+ ))
136
+ }
137
+
79
138
  export function openCommandDialog(
80
139
  api: TuiPluginApi,
81
140
  payload: OpenDialogPayload,
@@ -250,6 +309,24 @@ export function openCommandDialog(
250
309
  api.ui.dialog.clear()
251
310
  return
252
311
  }
312
+ if (option.value === 'set_window') {
313
+ showSetCachekeepWindowPrompt(api, apply, state, render)
314
+ return
315
+ }
316
+ if (option.value === 'clear_window') {
317
+ // The option label is `clear_window`, but executeCachekeepCommand
318
+ // matches on `window clear` — forward the canonical form or the
319
+ // command falls through to the usage-text branch and the window
320
+ // is never cleared.
321
+ void apply('openai-cachekeep', 'window clear').then((r) => {
322
+ render({
323
+ command: 'openai-cachekeep',
324
+ text: r.text,
325
+ knobs: r.knobs,
326
+ })
327
+ })
328
+ return
329
+ }
253
330
  const args =
254
331
  option.value === 'refresh' ? 'status' : String(option.value)
255
332
  void apply('openai-cachekeep', args).then((r) => {
@@ -0,0 +1,37 @@
1
+ // Prefer the host OpenTUI runtime registry when it exists. OpenTUI 0.4.x
2
+ // registers these virtual modules process-wide, allowing the precompiled TUI to
3
+ // share the host's single Solid/OpenTUI runtime when loaded from node_modules.
4
+ const runtimeProbe = `opentui:runtime-module:${encodeURIComponent('@opentui/solid')}`
5
+
6
+ function isMissingRuntimeRegistry(error) {
7
+ const message = error instanceof Error ? error.message : String(error)
8
+ return (
9
+ /Cannot find|Could not resolve|Module not found|Unable to resolve/.test(
10
+ message,
11
+ ) && message.includes('opentui:runtime-module:')
12
+ )
13
+ }
14
+
15
+ let mod
16
+ try {
17
+ await import(runtimeProbe)
18
+ } catch (error) {
19
+ if (!isMissingRuntimeRegistry(error)) {
20
+ console.error('OpenAI Auth TUI runtime registry probe failed', error)
21
+ throw error
22
+ }
23
+ // Older hosts and bare Bun do not provide the virtual registry. Their source
24
+ // loader still applies the Solid transform, so retain the raw TSX fallback.
25
+ mod = await import('../tui.tsx')
26
+ }
27
+
28
+ if (!mod) {
29
+ try {
30
+ mod = await import('../tui-compiled/tui.tsx')
31
+ } catch (error) {
32
+ console.error('OpenAI Auth compiled TUI failed to load', error)
33
+ throw error
34
+ }
35
+ }
36
+
37
+ export default mod.default
@@ -0,0 +1,161 @@
1
+ import {
2
+ appendFileSync,
3
+ chmodSync,
4
+ existsSync,
5
+ renameSync,
6
+ statSync,
7
+ } from 'node:fs'
8
+ import { tmpdir } from 'node:os'
9
+ import { join } from 'node:path'
10
+
11
+ type Level = 'error' | 'warn' | 'info' | 'debug' | 'trace'
12
+ const ORDER: Record<Level, number> = {
13
+ error: 0,
14
+ warn: 1,
15
+ info: 2,
16
+ debug: 3,
17
+ trace: 4,
18
+ }
19
+ const isTestEnv = process.env.NODE_ENV === 'test'
20
+ const MAX_BYTES = 5 * 1024 * 1024
21
+
22
+ function logFile(): string {
23
+ return (
24
+ process.env.OPENCODE_OPENAI_AUTH_LOG_FILE ??
25
+ join(tmpdir(), 'opencode-openai-auth.log')
26
+ )
27
+ }
28
+ let runtimeLevel: Level | undefined
29
+ export function setLogLevel(l: Level | undefined) {
30
+ if (l === undefined || l in ORDER) runtimeLevel = l
31
+ }
32
+ function configuredLevel(): Level {
33
+ if (runtimeLevel) return runtimeLevel
34
+ const env = process.env.OPENCODE_OPENAI_AUTH_LOG_LEVEL as Level | undefined
35
+ if (env && env in ORDER) return env
36
+ return 'info'
37
+ }
38
+
39
+ const SECRET_KEY_EXACT =
40
+ /^(authorization|x-api-key|cookie|set-cookie|refresh|access|token)$/i
41
+ function isSecretKey(key: string): boolean {
42
+ if (SECRET_KEY_EXACT.test(key)) return true
43
+ const k = key.toLowerCase().replace(/[-_]/g, '')
44
+ // Only the unambiguous ChatGPT stable id (chatgpt-account-id / chatgptAccountId)
45
+ // is sensitive. A bare `accountId` field is overloaded — most diagnostic logs put
46
+ // the INTERNAL account id/key ('main' or a fallback id) there, which is safe and
47
+ // needed for debugging — so it is intentionally NOT redacted by name. The genuine
48
+ // ChatGPT id is kept out of logs at the source instead.
49
+ if (k === 'chatgptaccountid') return true
50
+ if (k.includes('apikey')) return true
51
+ if (k.endsWith('secret') || k.endsWith('password')) return true
52
+ if (k.endsWith('token') && !k.endsWith('tokens')) return true
53
+ return false
54
+ }
55
+ const TOKEN_VALUE = /\b(Bearer\s+[\w.-]+|sk-[\w-]+|eyJ[\w.-]+)\b/g
56
+ export function redact(value: unknown): unknown {
57
+ return redactInner(value, new WeakSet<object>())
58
+ }
59
+ function redactInner(value: unknown, seen: WeakSet<object>): unknown {
60
+ if (typeof value === 'string')
61
+ return value.replace(TOKEN_VALUE, '***REDACTED***')
62
+ if (Array.isArray(value)) {
63
+ if (seen.has(value)) return '[Circular]'
64
+ seen.add(value)
65
+ const arr = value.map((v) => redactInner(v, seen))
66
+ seen.delete(value)
67
+ return arr
68
+ }
69
+ if (value && typeof value === 'object') {
70
+ if (seen.has(value)) return '[Circular]'
71
+ seen.add(value)
72
+ const out: Record<string, unknown> = {}
73
+ for (const [k, v] of Object.entries(value))
74
+ out[k] = isSecretKey(k) ? '***REDACTED***' : redactInner(v, seen)
75
+ seen.delete(value)
76
+ return out
77
+ }
78
+ return value
79
+ }
80
+
81
+ let buffer: string[] = []
82
+ let timer: ReturnType<typeof setTimeout> | undefined
83
+ const ROTATE_KEEP = 3
84
+ function chmodPrivate(path: string) {
85
+ try {
86
+ chmodSync(path, 0o600)
87
+ } catch {
88
+ /* never throw */
89
+ }
90
+ }
91
+ function rotateIfNeeded() {
92
+ try {
93
+ const f = logFile()
94
+ if (!(existsSync(f) && statSync(f).size > MAX_BYTES)) return
95
+ for (let i = ROTATE_KEEP - 1; i >= 1; i--) {
96
+ if (existsSync(`${f}.${i}`)) {
97
+ const rotated = `${f}.${i + 1}`
98
+ renameSync(`${f}.${i}`, rotated)
99
+ chmodPrivate(rotated)
100
+ }
101
+ }
102
+ const rotated = `${f}.1`
103
+ renameSync(f, rotated)
104
+ chmodPrivate(rotated)
105
+ } catch {
106
+ /* never throw */
107
+ }
108
+ }
109
+ function flush() {
110
+ if (!buffer.length) return
111
+ const text = buffer.join('')
112
+ buffer = []
113
+ try {
114
+ rotateIfNeeded()
115
+ const file = logFile()
116
+ if (existsSync(file)) chmodPrivate(file)
117
+ appendFileSync(file, text, { encoding: 'utf8', mode: 0o600 })
118
+ } catch {
119
+ /* never throw */
120
+ }
121
+ }
122
+ function schedule() {
123
+ if (!timer)
124
+ timer = setTimeout(() => {
125
+ timer = undefined
126
+ flush()
127
+ }, 500)
128
+ }
129
+ if (!isTestEnv) process.on('exit', flush)
130
+
131
+ function safeSerialize(data: unknown): string {
132
+ try {
133
+ return ` ${JSON.stringify(redact(data))}`
134
+ } catch {
135
+ return ' [unserializable]'
136
+ }
137
+ }
138
+
139
+ function emit(channel: string, level: Level, message: string, data?: unknown) {
140
+ if (ORDER[level] > ORDER[configuredLevel()]) return
141
+ const line =
142
+ `[${new Date().toISOString()}] ${level.toUpperCase()} [${channel}] ${message}` +
143
+ (data === undefined ? '' : safeSerialize(data)) +
144
+ '\n'
145
+ buffer.push(line)
146
+ if (buffer.length >= 50) flush()
147
+ else schedule()
148
+ }
149
+
150
+ export function createLogger(channel: string) {
151
+ return {
152
+ error: (m: string, d?: unknown) => emit(channel, 'error', m, d),
153
+ warn: (m: string, d?: unknown) => emit(channel, 'warn', m, d),
154
+ info: (m: string, d?: unknown) => emit(channel, 'info', m, d),
155
+ debug: (m: string, d?: unknown) => emit(channel, 'debug', m, d),
156
+ trace: (m: string, d?: unknown) => emit(channel, 'trace', m, d),
157
+ }
158
+ }
159
+ export async function flushForTest() {
160
+ flush()
161
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ mkdir,
3
+ readdir,
4
+ readFile,
5
+ rename,
6
+ unlink,
7
+ writeFile,
8
+ } from 'node:fs/promises'
9
+ import { join } from 'node:path'
10
+
11
+ export interface PortFileEntry {
12
+ port: number
13
+ token: string
14
+ pid: number
15
+ startedAt: number
16
+ }
17
+
18
+ function pidAlive(pid: number): boolean {
19
+ try {
20
+ process.kill(pid, 0)
21
+ return true
22
+ } catch (error) {
23
+ return (error as NodeJS.ErrnoException).code === 'EPERM'
24
+ }
25
+ }
26
+
27
+ export async function writePortFile(
28
+ dir: string,
29
+ entry: { port: number; token: string; pid: number },
30
+ ): Promise<string> {
31
+ await mkdir(dir, { recursive: true })
32
+ const full: PortFileEntry = { ...entry, startedAt: Date.now() }
33
+ const target = join(dir, `port-${entry.pid}.json`)
34
+ const tmp = `${target}.${process.pid}.tmp`
35
+ await writeFile(tmp, JSON.stringify(full), { encoding: 'utf8', mode: 0o600 })
36
+ await rename(tmp, target)
37
+ return target
38
+ }
39
+
40
+ export async function discoverPortFile(
41
+ dir: string,
42
+ expectedPid?: number,
43
+ ): Promise<PortFileEntry | null> {
44
+ let names: string[]
45
+ try {
46
+ names = await readdir(dir)
47
+ } catch {
48
+ return null
49
+ }
50
+ const live: PortFileEntry[] = []
51
+ for (const name of names) {
52
+ if (!name.startsWith('port-') || !name.endsWith('.json')) continue
53
+ try {
54
+ const parsed = JSON.parse(
55
+ await readFile(join(dir, name), 'utf8'),
56
+ ) as PortFileEntry
57
+ if (Number.isFinite(parsed.port)) {
58
+ if (pidAlive(parsed.pid)) live.push(parsed)
59
+ else unlink(join(dir, name)).catch(() => {})
60
+ }
61
+ } catch {}
62
+ }
63
+ if (live.length === 0) return null
64
+ const candidates =
65
+ expectedPid !== undefined && expectedPid >= 1
66
+ ? live.filter((entry) => entry.pid === expectedPid)
67
+ : []
68
+ const entries = candidates.length > 0 ? candidates : live
69
+ return entries.sort((a, b) => b.startedAt - a.startedAt)[0] ?? null
70
+ }
@@ -0,0 +1,31 @@
1
+ export type CommandModalName =
2
+ | 'openai-quota'
3
+ | 'openai-account'
4
+ | 'openai-routing'
5
+ | 'openai-killswitch'
6
+ | 'openai-dump'
7
+ | 'openai-logging'
8
+ | 'openai-cachekeep'
9
+
10
+ export interface OpenDialogPayload {
11
+ command: CommandModalName
12
+ text: string
13
+ knobs: Record<string, unknown>
14
+ }
15
+
16
+ export interface RpcNotification {
17
+ id: number
18
+ type: 'open-dialog'
19
+ payload: OpenDialogPayload
20
+ sessionId?: string
21
+ }
22
+
23
+ export interface ApplyRequest {
24
+ command: CommandModalName
25
+ arguments: string
26
+ }
27
+
28
+ export interface ApplyResult {
29
+ text: string
30
+ knobs: Record<string, unknown>
31
+ }
@@ -0,0 +1,78 @@
1
+ import { discoverPortFile, type PortFileEntry } from './port-file'
2
+ import type { ApplyRequest, ApplyResult, RpcNotification } from './protocol'
3
+
4
+ export interface RpcClient {
5
+ pending: (
6
+ lastReceivedId: number,
7
+ sessionId?: string,
8
+ ) => Promise<RpcNotification[]>
9
+ apply: (request: ApplyRequest) => Promise<ApplyResult>
10
+ }
11
+
12
+ async function call<T>(
13
+ dir: string,
14
+ expectedPid: number | undefined,
15
+ onSelected: ((entry: PortFileEntry | null) => void) | undefined,
16
+ method: string,
17
+ params: Record<string, unknown>,
18
+ ): Promise<T | null> {
19
+ const entry = await discoverPortFile(dir, expectedPid)
20
+ onSelected?.(entry)
21
+ if (!entry) return null
22
+ const controller = new AbortController()
23
+ const timer = setTimeout(() => controller.abort(), 2_000)
24
+ try {
25
+ const res = await fetch(`http://127.0.0.1:${entry.port}/rpc/${method}`, {
26
+ method: 'POST',
27
+ headers: {
28
+ 'content-type': 'application/json',
29
+ authorization: `Bearer ${entry.token}`,
30
+ },
31
+ body: JSON.stringify(params),
32
+ signal: controller.signal,
33
+ })
34
+ if (!res.ok) return null
35
+ return (await res.json()) as T
36
+ } catch {
37
+ return null
38
+ } finally {
39
+ clearTimeout(timer)
40
+ }
41
+ }
42
+
43
+ export function createRpcClient(
44
+ dir: string,
45
+ expectedPid?: number,
46
+ onSelected?: (entry: PortFileEntry | null) => void,
47
+ ): RpcClient {
48
+ let reportedSelection = false
49
+ const reportSelected = (entry: PortFileEntry | null) => {
50
+ if (reportedSelection) return
51
+ reportedSelection = true
52
+ onSelected?.(entry)
53
+ }
54
+ return {
55
+ async pending(lastReceivedId, sessionId) {
56
+ const out = await call<{ messages: RpcNotification[] }>(
57
+ dir,
58
+ expectedPid,
59
+ reportSelected,
60
+ 'pending-notifications',
61
+ { lastReceivedId, sessionId },
62
+ )
63
+ return out?.messages ?? []
64
+ },
65
+ async apply(request) {
66
+ const out = await call<ApplyResult>(
67
+ dir,
68
+ expectedPid,
69
+ reportSelected,
70
+ 'apply',
71
+ {
72
+ ...request,
73
+ },
74
+ )
75
+ return out ?? { text: 'apply failed', knobs: {} }
76
+ },
77
+ }
78
+ }
@@ -0,0 +1,22 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { homedir, tmpdir } from 'node:os'
3
+ import { join, resolve } from 'node:path'
4
+
5
+ const RPC_DIR_ENV = 'OPENCODE_OPENAI_AUTH_RPC_DIR'
6
+
7
+ // Both processes must resolve the SAME dir from the SAME project directory.
8
+ export function getRpcDir(projectDirectory: string): string {
9
+ const override = process.env[RPC_DIR_ENV]?.trim()
10
+ // A relative override is anchored to projectDirectory (shared by both processes)
11
+ // so server and TUI halves always resolve the same dir. An absolute override is
12
+ // used as-is (resolve(base, absolute) returns the absolute path unchanged).
13
+ if (override) return resolve(projectDirectory, override)
14
+ const hash = createHash('sha256')
15
+ .update(projectDirectory)
16
+ .digest('hex')
17
+ .slice(0, 16)
18
+ const base = process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state')
19
+ return join(base, 'cortexkit', 'openai-auth', 'rpc', hash)
20
+ }
21
+
22
+ export { tmpdir }