@cortexkit/opencode-openai-auth 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +8 -4
  2. package/dist/cli.js +1 -1
  3. package/dist/codex-http.d.ts +1 -1
  4. package/dist/commands.d.ts +6 -0
  5. package/dist/core/accounts.d.ts +6 -1
  6. package/dist/core/background-quota-refresh.d.ts +46 -0
  7. package/dist/core/cachekeep.d.ts +4 -0
  8. package/dist/core/oauth.d.ts +1 -1
  9. package/dist/core/quota-manager.d.ts +18 -15
  10. package/dist/core/refresh-all-quota.d.ts +12 -1
  11. package/dist/core/sticky-routing.d.ts +43 -0
  12. package/dist/dump.d.ts +1 -0
  13. package/dist/{index-x1c39tzz.js → index-6rnw2cgs.js} +1 -1
  14. package/dist/index-89mh4ahn.js +89 -0
  15. package/dist/index-rfgyxxxq.js +1 -0
  16. package/dist/index-s5fbqqwx.js +3 -0
  17. package/dist/index-vrdc90yd.js +6 -0
  18. package/dist/index-wnxfzjfr.js +2 -0
  19. package/dist/{index-jpx7h5gc.js → index-wpd7mg3p.js} +1 -1
  20. package/dist/index-zkzb0twr.js +2 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +101 -83
  23. package/dist/{quota-normalize-02mn21p4.js → quota-normalize-n02dfdx0.js} +1 -1
  24. package/dist/raw-ws-bun.d.ts +2 -0
  25. package/dist/raw-ws-node.d.ts +2 -0
  26. package/dist/raw-ws-upgrade.d.ts +2 -0
  27. package/dist/rpc/port-file.d.ts +4 -0
  28. package/dist/rpc/port-file.js +1 -1
  29. package/dist/rpc/protocol.d.ts +1 -0
  30. package/dist/rpc/rpc-client.js +1 -1
  31. package/dist/rpc/rpc-dir.d.ts +6 -0
  32. package/dist/rpc/rpc-dir.js +1 -1
  33. package/dist/rpc/rpc-server.d.ts +2 -0
  34. package/dist/sidebar-state.d.ts +46 -5
  35. package/dist/sidebar-state.js +1 -1
  36. package/dist/{token-dab1ama0.js → token-5hdnazxp.js} +1 -1
  37. package/dist/token-util-3d2nj45d.js +1 -0
  38. package/dist/tui-preferences.js +1 -1
  39. package/dist/tui.d.ts +9 -0
  40. package/dist/version.d.ts +1 -1
  41. package/dist/ws-pool.d.ts +2 -11
  42. package/dist/ws.d.ts +7 -2
  43. package/package.json +2 -2
  44. package/src/rpc/port-file.ts +115 -8
  45. package/src/rpc/protocol.ts +1 -0
  46. package/src/rpc/rpc-dir.ts +31 -7
  47. package/src/rpc/rpc-server.ts +18 -2
  48. package/src/sidebar-state.ts +771 -23
  49. package/src/tui/command-dialogs.tsx +25 -3
  50. package/src/tui-compiled/rpc/port-file.ts +115 -8
  51. package/src/tui-compiled/rpc/protocol.ts +1 -0
  52. package/src/tui-compiled/rpc/rpc-dir.ts +31 -7
  53. package/src/tui-compiled/sidebar-state.ts +771 -23
  54. package/src/tui-compiled/tui/command-dialogs.tsx +17 -4
  55. package/src/tui-compiled/tui.tsx +50 -15
  56. package/src/tui.tsx +57 -19
  57. package/dist/index-72cas5ht.js +0 -7
  58. package/dist/index-dc7za61x.js +0 -89
  59. package/dist/index-hjgzqz1c.js +0 -1
  60. package/dist/index-qnd9x5jh.js +0 -2
  61. package/dist/index-wpk440dz.js +0 -2
  62. package/dist/token-util-41n40m0y.js +0 -1
@@ -1 +1 @@
1
- import{c as a,d as b,e as c,f as d,g as e}from"./index-2ng03cby.js";import"./index-jpx7h5gc.js";export{a as toResetIso,d as normalizeWsFrame,e as normalizeWham,b as normalizeQuotaHeaders,c as isCompleteQuotaHeaderFrame};
1
+ import{c as a,d as b,e as c,f as d,g as e}from"./index-2ng03cby.js";import"./index-wpd7mg3p.js";export{a as toResetIso,d as normalizeWsFrame,e as normalizeWham,b as normalizeQuotaHeaders,c as isCompleteQuotaHeaderFrame};
@@ -7,6 +7,7 @@ export declare class RawWebSocket {
7
7
  private listeners;
8
8
  private rxBuffer;
9
9
  private handshakeDone;
10
+ private upgradeFailureEmitted;
10
11
  private expectedAccept;
11
12
  private fragOpcode;
12
13
  private fragChunks;
@@ -21,6 +22,7 @@ export declare class RawWebSocket {
21
22
  private emit;
22
23
  private connect;
23
24
  private onData;
25
+ private emitRejectedUpgrade;
24
26
  private drainFrames;
25
27
  private handleFrame;
26
28
  send(data: string): void;
@@ -7,6 +7,7 @@ export declare class RawWebSocket {
7
7
  private listeners;
8
8
  private rxBuffer;
9
9
  private handshakeDone;
10
+ private upgradeFailureEmitted;
10
11
  private expectedAccept;
11
12
  private fragOpcode;
12
13
  private fragChunks;
@@ -20,6 +21,7 @@ export declare class RawWebSocket {
20
21
  private emit;
21
22
  private connect;
22
23
  private onData;
24
+ private emitRejectedUpgrade;
23
25
  private drainFrames;
24
26
  private handleFrame;
25
27
  send(data: string): void;
@@ -0,0 +1,2 @@
1
+ export declare function rejectedUpgradeStatus(statusLine: string): number | undefined;
2
+ export declare function rejectedUpgradeEvent(buffer: Uint8Array, headerEnd: number, finalizePartial?: boolean): Record<string, unknown> | undefined;
@@ -8,5 +8,9 @@ export declare function writePortFile(dir: string, entry: {
8
8
  port: number;
9
9
  token: string;
10
10
  pid: number;
11
+ }, options?: {
12
+ secureDir?: boolean;
13
+ beforeWrite?: () => void | Promise<void>;
11
14
  }): Promise<string>;
15
+ export declare function sweepRpcState(root: string, activeDir: string): Promise<void>;
12
16
  export declare function discoverPortFile(dir: string, expectedPid?: number): Promise<PortFileEntry | null>;
@@ -1 +1 @@
1
- import{na as a,oa as b}from"../index-qnd9x5jh.js";import"../index-jpx7h5gc.js";export{a as writePortFile,b as discoverPortFile};
1
+ import{ua as a,va as b,wa as c}from"../index-zkzb0twr.js";import"../index-s5fbqqwx.js";import"../index-wpd7mg3p.js";export{a as writePortFile,b as sweepRpcState,c as discoverPortFile};
@@ -13,6 +13,7 @@ export interface RpcNotification {
13
13
  export interface ApplyRequest {
14
14
  command: CommandModalName;
15
15
  arguments: string;
16
+ sessionId?: string;
16
17
  }
17
18
  export interface ApplyResult {
18
19
  text: string;
@@ -1 +1 @@
1
- import{oa as c}from"../index-qnd9x5jh.js";import"../index-jpx7h5gc.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{wa as c}from"../index-zkzb0twr.js";import"../index-s5fbqqwx.js";import"../index-wpd7mg3p.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,3 +1,9 @@
1
1
  import { tmpdir } from 'node:os';
2
+ export interface RpcDirResolution {
3
+ dir: string;
4
+ secureDir: boolean;
5
+ sweepRoot?: string;
6
+ }
2
7
  export declare function getRpcDir(projectDirectory: string): string;
8
+ export declare function resolveRpcDir(projectDirectory: string): Promise<RpcDirResolution>;
3
9
  export { tmpdir };
@@ -1 +1 @@
1
- import{pa as a,qa as b}from"../index-hjgzqz1c.js";import"../index-jpx7h5gc.js";export{a as tmpdir,b as getRpcDir};
1
+ import{Aa as a,Ba as b,Ca as c}from"../index-rfgyxxxq.js";import"../index-wpd7mg3p.js";export{a as tmpdir,c as resolveRpcDir,b as getRpcDir};
@@ -7,6 +7,8 @@ export interface RpcServerHandle {
7
7
  }
8
8
  export interface RpcServerOptions {
9
9
  dir: string;
10
+ secureDir?: boolean;
11
+ sweepRoot?: string;
10
12
  drain: typeof drainNotifications;
11
13
  apply: (request: ApplyRequest) => Promise<ApplyResult>;
12
14
  timeoutMs?: number;
@@ -1,10 +1,12 @@
1
1
  export interface QuotaWindow {
2
2
  usedPercent: number;
3
3
  remainingPercent: number;
4
+ checkedAt?: number;
4
5
  resetsAt?: string;
5
6
  windowMinutes?: number;
6
7
  }
7
8
  export interface AccountQuota {
9
+ checkedAt?: number;
8
10
  primary?: QuotaWindow;
9
11
  secondary?: QuotaWindow;
10
12
  resetCreditsAvailable?: number;
@@ -21,6 +23,8 @@ export declare function getPresentQuotaWindows(quota: AccountQuota | null): Pres
21
23
  export interface SidebarAccountState {
22
24
  id: string;
23
25
  label: string | undefined;
26
+ /** ChatGPT identity of the account this quota belongs to. */
27
+ accountId?: string;
24
28
  quota: AccountQuota | null;
25
29
  killed: boolean;
26
30
  enabled: boolean;
@@ -32,9 +36,32 @@ export interface ActiveRoutingEntry {
32
36
  updatedAt: number;
33
37
  }
34
38
  export type ActiveRoutingMap = Record<string, ActiveRoutingEntry>;
39
+ export interface StickyAssignment {
40
+ accountId: string;
41
+ assignedAt: number;
42
+ lastSeenAt: number;
43
+ inputBytes: number;
44
+ quotaCheckedAt?: number;
45
+ }
46
+ export type StickyAssignmentMap = Record<string, StickyAssignment>;
47
+ export interface StickyAssignmentChoice {
48
+ accountId: string;
49
+ quotaCheckedAt?: number;
50
+ }
51
+ export interface ResolveStickyAssignmentInput {
52
+ sessionId: string;
53
+ requestBytes: number;
54
+ now: number;
55
+ validPinnedAccountIds: readonly string[];
56
+ excludeAccountIds?: readonly string[];
57
+ quotaCheckedAtByAccount: Readonly<Record<string, number | undefined>>;
58
+ choose: (pendingBytes: ReadonlyMap<string, number>) => StickyAssignmentChoice | undefined;
59
+ }
35
60
  export interface SidebarState {
36
61
  main: {
37
62
  quota: AccountQuota | null;
63
+ /** ChatGPT identity of the main account this quota belongs to. */
64
+ mainAccountId?: string;
38
65
  killed: boolean;
39
66
  quotaBackedOff?: boolean;
40
67
  quotaBackoffUntil?: number;
@@ -48,10 +75,14 @@ export interface SidebarState {
48
75
  /** Machine-global routing mode and compatibility value for older readers. */
49
76
  route: string;
50
77
  activeRouting?: ActiveRoutingMap;
78
+ stickyAssignments?: StickyAssignmentMap;
51
79
  planType?: string;
52
80
  credits?: number;
53
81
  lastUpdated: number;
54
82
  }
83
+ export declare const STICKY_ASSIGNMENT_MAX_AGE_MS: number;
84
+ export declare const STICKY_ASSIGNMENT_MAX_ENTRIES = 256;
85
+ export declare function hashSidebarSessionId(sessionId: string): string;
55
86
  export declare function getSidebarStateFile(): string;
56
87
  export declare const DEFAULT_SIDEBAR_STATE: SidebarState;
57
88
  /**
@@ -64,19 +95,27 @@ export declare const DEFAULT_SIDEBAR_STATE: SidebarState;
64
95
  * the I/O boundary, so a bad file can never crash the host TUI.
65
96
  */
66
97
  export declare function normalizeSidebarState(raw: unknown): SidebarState;
67
- export declare function getSidebarState(): Promise<SidebarState>;
98
+ export declare function getSidebarState(stateFile?: string): Promise<SidebarState>;
68
99
  export declare const ACTIVE_ROUTING_MAX_AGE_MS: number;
69
100
  export declare const ACTIVE_ROUTING_MAX_ENTRIES = 128;
70
101
  export type SidebarRoutingAccount = {
71
102
  id: string;
72
103
  enabled?: boolean;
104
+ killed?: boolean;
73
105
  };
74
- export declare function isUsableRoutingEntry(entry: ActiveRoutingEntry, accounts: readonly SidebarRoutingAccount[], now?: number): boolean;
106
+ export declare function isUsableRoutingEntry(entry: ActiveRoutingEntry, accounts: readonly SidebarRoutingAccount[] | undefined, now?: number): boolean;
107
+ export declare function exhaustedQuotaResetAt(quota: AccountQuota | null | undefined, now?: number): {
108
+ resetsAt: string;
109
+ resetAtMs: number;
110
+ } | undefined;
111
+ export declare function isQuotaExhausted(quota: AccountQuota | null | undefined, now?: number): boolean;
112
+ export declare function resolveSessionStickyAccount(state: SidebarState, sessionId: string | undefined, now?: number): string | undefined;
75
113
  export declare function resolveSessionSidebarRouting(state: SidebarState, sessionId?: string, now?: number): {
76
114
  activeId: string;
77
115
  route: string;
78
116
  };
79
- export declare function pruneActiveRouting(activeRouting: ActiveRoutingMap | undefined, accounts: readonly SidebarRoutingAccount[], now?: number, removedSessionId?: string): ActiveRoutingMap | undefined;
117
+ export declare function pruneActiveRouting(activeRouting: ActiveRoutingMap | undefined, accounts: readonly SidebarRoutingAccount[] | undefined, now?: number, removedSessionId?: string): ActiveRoutingMap | undefined;
118
+ export declare function pruneStickyAssignments(assignments: StickyAssignmentMap | undefined, validAccountIds: ReadonlySet<string> | undefined, now?: number, removedSessionHash?: string): StickyAssignmentMap | undefined;
80
119
  interface SidebarMergeHooks {
81
120
  beforeRecheck?: () => void | Promise<void>;
82
121
  }
@@ -98,9 +137,11 @@ export type SidebarMachineState = Pick<SidebarState, 'main' | 'fallbacks' | 'pla
98
137
  export declare function setSidebarMachineState(machineState: SidebarMachineState, file?: string, hooks?: SidebarMergeHooks): Promise<void>;
99
138
  export declare function upsertSidebarActiveRouting(input: {
100
139
  sessionId: string;
101
- } & ActiveRoutingEntry, accounts: readonly SidebarRoutingAccount[], file?: string, hooks?: SidebarMergeHooks): Promise<void>;
140
+ } & ActiveRoutingEntry, accounts: readonly SidebarRoutingAccount[] | undefined, file?: string, hooks?: SidebarMergeHooks): Promise<void>;
102
141
  export declare function setSidebarLegacyRouting(input: ActiveRoutingEntry, file?: string): Promise<void>;
103
- export declare function removeSidebarActiveRouting(sessionId: string, accounts: readonly SidebarRoutingAccount[], file?: string, hooks?: SidebarMergeHooks): Promise<void>;
142
+ export declare function removeSidebarActiveRouting(sessionId: string, accounts: readonly SidebarRoutingAccount[] | undefined, file?: string, hooks?: SidebarMergeHooks): Promise<void>;
143
+ export declare function resolveSidebarStickyAssignment(input: ResolveStickyAssignmentInput, file?: string, hooks?: SidebarMergeHooks): Promise<StickyAssignment | undefined>;
144
+ export declare function clearSidebarStickyAssignment(sessionId: string, file?: string): Promise<boolean>;
104
145
  /**
105
146
  * Await all pending sidebar writes. Tests call this before restoring env
106
147
  * vars in teardown so no in-flight write can re-resolve getSidebarStateFile()
@@ -1 +1 @@
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,aa as l,ba as m,ca as n,da as o,ea as p,fa as q,ga as r,ha as s,ia as t}from"./index-wpk440dz.js";import"./index-72cas5ht.js";import"./index-jpx7h5gc.js";export{n as upsertSidebarActiveRouting,l as setSidebarState,m as setSidebarMachineState,o as setSidebarLegacyRouting,j as resolveSessionSidebarRouting,r as resolveActiveAccount,p as removeSidebarActiveRouting,k as pruneActiveRouting,e as normalizeSidebarState,i as isUsableRoutingEntry,c as getSidebarStateFile,f as getSidebarState,b as getPresentQuotaWindows,s as getCollapsedQuotaSummary,a as formatWindowLabel,q as drainSidebarWrites,t as computeQuotaPacing,d as DEFAULT_SIDEBAR_STATE,h as ACTIVE_ROUTING_MAX_ENTRIES,g as ACTIVE_ROUTING_MAX_AGE_MS};
1
+ import{$ as j,S as a,T as b,U as c,V as d,W as e,X as f,Y as g,Z as h,_ as i,aa as k,ba as l,ca as m,da as n,ea as o,fa as p,ga as q,ha as r,ia as s,ja as t,ka as u,la as v,ma as w,na as x,oa as y,pa as z,qa as A,ra as B,sa as C}from"./index-wnxfzjfr.js";import"./index-vrdc90yd.js";import"./index-s5fbqqwx.js";import"./index-wpd7mg3p.js";export{u as upsertSidebarActiveRouting,s as setSidebarState,t as setSidebarMachineState,v as setSidebarLegacyRouting,x as resolveSidebarStickyAssignment,o as resolveSessionStickyAccount,p as resolveSessionSidebarRouting,A as resolveActiveAccount,w as removeSidebarActiveRouting,r as pruneStickyAssignments,q as pruneActiveRouting,h as normalizeSidebarState,l as isUsableRoutingEntry,n as isQuotaExhausted,e as hashSidebarSessionId,f as getSidebarStateFile,i as getSidebarState,b as getPresentQuotaWindows,B as getCollapsedQuotaSummary,a as formatWindowLabel,m as exhaustedQuotaResetAt,z as drainSidebarWrites,C as computeQuotaPacing,y as clearSidebarStickyAssignment,d as STICKY_ASSIGNMENT_MAX_ENTRIES,c as STICKY_ASSIGNMENT_MAX_AGE_MS,g as DEFAULT_SIDEBAR_STATE,k as ACTIVE_ROUTING_MAX_ENTRIES,j as ACTIVE_ROUTING_MAX_AGE_MS};
@@ -1 +1 @@
1
- import{a as U,b as V}from"./index-x1c39tzz.js";import{sa as R}from"./index-jpx7h5gc.js";var E=R((M,Q)=>{var{defineProperty:H,getOwnPropertyDescriptor:W,getOwnPropertyNames:X}=Object,Y=Object.prototype.hasOwnProperty,Z=(z,q)=>{for(var B in q)H(z,B,{get:q[B],enumerable:!0})},$=(z,q,B,F)=>{if(q&&typeof q==="object"||typeof q==="function"){for(let v of X(q))if(!Y.call(z,v)&&v!==B)H(z,v,{get:()=>q[v],enumerable:!(F=W(q,v))||F.enumerable})}return z},A=(z)=>$(H({},"__esModule",{value:!0}),z),L={};Z(L,{refreshToken:()=>D});Q.exports=A(L);var K=U(),G=V();async function D(z){let q=z?.project,B=z?.team;if(!q&&!B){let v=(0,G.findProjectInfo)();q=v.projectId,B=v.teamId}else if(!q||!B){let v=(0,G.findProjectInfo)();q=q??v.projectId,B=B??v.teamId}if(!q)throw new K.VercelOidcTokenError("Failed to refresh OIDC token: No project specified. Try re-linking your project with `vc link`");let F=(0,G.loadToken)(q);if(!F||(0,G.isExpired)((0,G.getTokenPayload)(F.token),z?.expirationBufferMs)){let v=await(0,G.getVercelToken)({expirationBufferMs:z?.expirationBufferMs});if(F=await(0,G.getVercelOidcToken)(v,q,B),!F)throw new K.VercelOidcTokenError("Failed to refresh OIDC token");(0,G.saveToken)(F,q)}process.env.VERCEL_OIDC_TOKEN=F.token;return}});export default E();
1
+ import{a as U,b as V}from"./index-6rnw2cgs.js";import{Ea as R}from"./index-wpd7mg3p.js";var E=R((M,Q)=>{var{defineProperty:H,getOwnPropertyDescriptor:W,getOwnPropertyNames:X}=Object,Y=Object.prototype.hasOwnProperty,Z=(z,q)=>{for(var B in q)H(z,B,{get:q[B],enumerable:!0})},$=(z,q,B,F)=>{if(q&&typeof q==="object"||typeof q==="function"){for(let v of X(q))if(!Y.call(z,v)&&v!==B)H(z,v,{get:()=>q[v],enumerable:!(F=W(q,v))||F.enumerable})}return z},A=(z)=>$(H({},"__esModule",{value:!0}),z),L={};Z(L,{refreshToken:()=>D});Q.exports=A(L);var K=U(),G=V();async function D(z){let q=z?.project,B=z?.team;if(!q&&!B){let v=(0,G.findProjectInfo)();q=v.projectId,B=v.teamId}else if(!q||!B){let v=(0,G.findProjectInfo)();q=q??v.projectId,B=B??v.teamId}if(!q)throw new K.VercelOidcTokenError("Failed to refresh OIDC token: No project specified. Try re-linking your project with `vc link`");let F=(0,G.loadToken)(q);if(!F||(0,G.isExpired)((0,G.getTokenPayload)(F.token),z?.expirationBufferMs)){let v=await(0,G.getVercelToken)({expirationBufferMs:z?.expirationBufferMs});if(F=await(0,G.getVercelOidcToken)(v,q,B),!F)throw new K.VercelOidcTokenError("Failed to refresh OIDC token");(0,G.saveToken)(F,q)}process.env.VERCEL_OIDC_TOKEN=F.token;return}});export default E();
@@ -0,0 +1 @@
1
+ import{b as a}from"./index-6rnw2cgs.js";import"./index-wpd7mg3p.js";export default a();
@@ -1,4 +1,4 @@
1
- import{ra as P,sa as C,ua as _}from"./index-jpx7h5gc.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{Da as P,Ea as C,Ga as _}from"./index-wpd7mg3p.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/tui.d.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import type { TuiPluginModule } from '@opencode-ai/plugin/tui';
3
+ import type { ApplyRequest, CommandModalName } from './rpc/protocol.js';
3
4
  import { type AccountQuota, type QuotaPacing, type QuotaWindow, type SidebarState } from './sidebar-state.js';
5
+ export declare function buildApplyRequest(command: CommandModalName, arguments_: string, sessionId?: string): ApplyRequest;
6
+ type Tone = 'ok' | 'warn' | 'err' | 'muted' | 'accent' | 'text';
4
7
  export declare function formatResetIn(resetsAt: string | undefined): string;
5
8
  export interface QuotaDisplayRow {
6
9
  key: 'primary' | 'secondary';
@@ -20,6 +23,12 @@ export declare function getAccountMetadataRows(resetCredits: number | undefined)
20
23
  }>;
21
24
  export declare function readStateFromFile(): Promise<SidebarState>;
22
25
  export declare function resolveQuotaDialogActiveId(state: SidebarState, sessionId: string | undefined, now?: number): string | undefined;
26
+ export interface RoutingDisplayRow {
27
+ label: string;
28
+ value: string;
29
+ tone: Tone;
30
+ }
31
+ export declare function buildRoutingRowsForDisplay(state: SidebarState, sessionId: string | undefined, now?: number): RoutingDisplayRow[];
23
32
  declare const plugin: TuiPluginModule & {
24
33
  id: string;
25
34
  };
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const PackageVersion = "0.4.2";
1
+ export declare const PackageVersion = "0.5.0";
package/dist/ws-pool.d.ts CHANGED
@@ -20,17 +20,8 @@ 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
+ /** Receives quota exhaustion plus the account identity captured at send time. */
24
+ onRateLimitReached?: (window: string, accountId: string | undefined, resetAt?: number) => void;
34
25
  }
35
26
  interface PoolEntry {
36
27
  socket?: WebSocket;
package/dist/ws.d.ts CHANGED
@@ -30,8 +30,8 @@ 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
- /** Called when response.failed carries a rate_limit_reached_type — the only mid-stream quota-exhaustion signal on this transport. */
34
- onRateLimitReached?: (window: string) => void;
33
+ /** Called when the transport reports quota exhaustion for the current connection. */
34
+ onRateLimitReached?: (window: string, resetAt?: number) => void;
35
35
  }
36
36
  export interface WrappedError {
37
37
  status: number;
@@ -41,6 +41,11 @@ export interface WrappedError {
41
41
  export declare function toWebSocketUrl(url: string): string;
42
42
  export declare function normalizeHeaders(headers: HeadersInit | undefined): Record<string, string>;
43
43
  export declare function isAbortError(error: unknown): error is DOMException;
44
+ export declare function isUpgradeFailure(error: unknown): boolean;
44
45
  export declare function connectResponsesWebSocket(options: ConnectResponsesWebSocketOptions): Promise<WebSocket>;
45
46
  export declare function streamResponsesWebSocket(options: StreamResponsesWebSocketOptions): Response;
47
+ export declare function parseRateLimitSignal(value: unknown): {
48
+ window: string;
49
+ resetAt?: number;
50
+ } | undefined;
46
51
  export * as OpenAIWebSocket from './ws';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-openai-auth",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "repository": {
@@ -63,7 +63,7 @@
63
63
  "dependencies": {
64
64
  "@opentui/core": ">=0.4.5",
65
65
  "@opentui/solid": ">=0.4.5",
66
- "ai": "^7.0.34",
66
+ "ai": "^7.0.48",
67
67
  "jsonc-parser": "^3.3.1",
68
68
  "solid-js": "1.9.12"
69
69
  }
@@ -1,12 +1,18 @@
1
+ import type { Dirent } from 'node:fs'
1
2
  import {
3
+ chmod,
2
4
  mkdir,
3
5
  readdir,
4
6
  readFile,
5
7
  rename,
8
+ rmdir,
6
9
  unlink,
7
10
  writeFile,
8
11
  } from 'node:fs/promises'
9
- import { join } from 'node:path'
12
+ import { join, resolve } from 'node:path'
13
+ import { createLogger } from '../logger'
14
+
15
+ const log = createLogger('rpc')
10
16
 
11
17
  export interface PortFileEntry {
12
18
  port: number
@@ -24,17 +30,118 @@ function pidAlive(pid: number): boolean {
24
30
  }
25
31
  }
26
32
 
33
+ function isManagedRpcStateDir(name: string): boolean {
34
+ return /^(?:openai-auth-)?[0-9a-f]{16}$/.test(name)
35
+ }
36
+
37
+ function isUsablePortFileEntry(value: unknown): value is PortFileEntry {
38
+ return (
39
+ value !== null &&
40
+ typeof value === 'object' &&
41
+ !Array.isArray(value) &&
42
+ typeof (value as { pid?: unknown }).pid === 'number' &&
43
+ Number.isFinite((value as { pid: number }).pid) &&
44
+ typeof (value as { port?: unknown }).port === 'number' &&
45
+ Number.isFinite((value as { port: number }).port)
46
+ )
47
+ }
48
+
49
+ async function removeCorruptPortFile(portFile: string): Promise<void> {
50
+ log.debug('rpc corrupt port file', { pid: process.pid, portFile })
51
+ await unlink(portFile).catch(() => {})
52
+ }
53
+
27
54
  export async function writePortFile(
28
55
  dir: string,
29
56
  entry: { port: number; token: string; pid: number },
57
+ options: {
58
+ secureDir?: boolean
59
+ beforeWrite?: () => void | Promise<void>
60
+ } = {},
30
61
  ): 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
62
+ // The directory can be removed by another project's sweep between our
63
+ // mkdir and the first writeFile/rename, so the whole create-then-rename
64
+ // unit is retried once on ENOENT. The retry recreates the directory; a
65
+ // persistent ENOENT (e.g. permission, read-only parent) will surface on
66
+ // the second attempt — failing fast beats an unbounded loop.
67
+ const writeOnce = async (): Promise<string> => {
68
+ await mkdir(dir, {
69
+ recursive: true,
70
+ mode: options.secureDir ? 0o700 : undefined,
71
+ })
72
+ if (options.secureDir) await chmod(dir, 0o700)
73
+ await options.beforeWrite?.()
74
+ const full: PortFileEntry = { ...entry, startedAt: Date.now() }
75
+ const target = join(dir, `port-${entry.pid}.json`)
76
+ const tmp = `${target}.${process.pid}.tmp`
77
+ await writeFile(tmp, JSON.stringify(full), {
78
+ encoding: 'utf8',
79
+ mode: 0o600,
80
+ })
81
+ await rename(tmp, target)
82
+ return target
83
+ }
84
+ try {
85
+ return await writeOnce()
86
+ } catch (error) {
87
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
88
+ return await writeOnce()
89
+ }
90
+ throw error
91
+ }
92
+ }
93
+
94
+ export async function sweepRpcState(
95
+ root: string,
96
+ activeDir: string,
97
+ ): Promise<void> {
98
+ let projectDirs: Dirent<string>[]
99
+ try {
100
+ projectDirs = await readdir(root, { withFileTypes: true })
101
+ } catch (error) {
102
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return
103
+ throw error
104
+ }
105
+
106
+ const active = resolve(activeDir)
107
+ for (const projectDir of projectDirs) {
108
+ if (!projectDir.isDirectory() || !isManagedRpcStateDir(projectDir.name)) {
109
+ continue
110
+ }
111
+ const dir = join(root, projectDir.name)
112
+ let names: string[]
113
+ try {
114
+ names = await readdir(dir)
115
+ } catch (error) {
116
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') continue
117
+ throw error
118
+ }
119
+ for (const name of names) {
120
+ if (!name.startsWith('port-') || !name.endsWith('.json')) continue
121
+ const portFile = join(dir, name)
122
+ let raw: string | undefined
123
+ try {
124
+ raw = await readFile(portFile, 'utf8')
125
+ } catch {
126
+ continue
127
+ }
128
+ if (raw === undefined) continue
129
+ let parsed: unknown
130
+ try {
131
+ parsed = JSON.parse(raw)
132
+ } catch {
133
+ await removeCorruptPortFile(portFile)
134
+ continue
135
+ }
136
+ if (!isUsablePortFileEntry(parsed)) {
137
+ await removeCorruptPortFile(portFile)
138
+ continue
139
+ }
140
+ const entry = parsed
141
+ if (!pidAlive(entry.pid)) await unlink(portFile).catch(() => {})
142
+ }
143
+ if (resolve(dir) !== active) await rmdir(dir).catch(() => {})
144
+ }
38
145
  }
39
146
 
40
147
  export async function discoverPortFile(
@@ -23,6 +23,7 @@ export interface RpcNotification {
23
23
  export interface ApplyRequest {
24
24
  command: CommandModalName
25
25
  arguments: string
26
+ sessionId?: string
26
27
  }
27
28
 
28
29
  export interface ApplyResult {
@@ -1,9 +1,27 @@
1
1
  import { createHash } from 'node:crypto'
2
2
  import { homedir, tmpdir } from 'node:os'
3
- import { join, resolve } from 'node:path'
3
+ import { dirname, join, resolve } from 'node:path'
4
4
 
5
5
  const RPC_DIR_ENV = 'OPENCODE_OPENAI_AUTH_RPC_DIR'
6
6
 
7
+ export interface RpcDirResolution {
8
+ dir: string
9
+ secureDir: boolean
10
+ sweepRoot?: string
11
+ }
12
+
13
+ function rpcHash(projectDirectory: string): string {
14
+ return createHash('sha256')
15
+ .update(projectDirectory)
16
+ .digest('hex')
17
+ .slice(0, 16)
18
+ }
19
+
20
+ function defaultRpcRoot(): string {
21
+ const base = process.env.XDG_STATE_HOME || join(homedir(), '.local', 'state')
22
+ return join(base, 'cortexkit', 'openai-auth', 'rpc')
23
+ }
24
+
7
25
  // Both processes must resolve the SAME dir from the SAME project directory.
8
26
  export function getRpcDir(projectDirectory: string): string {
9
27
  const override = process.env[RPC_DIR_ENV]?.trim()
@@ -11,12 +29,18 @@ export function getRpcDir(projectDirectory: string): string {
11
29
  // so server and TUI halves always resolve the same dir. An absolute override is
12
30
  // used as-is (resolve(base, absolute) returns the absolute path unchanged).
13
31
  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)
32
+ return join(defaultRpcRoot(), `openai-auth-${rpcHash(projectDirectory)}`)
33
+ }
34
+
35
+ export async function resolveRpcDir(
36
+ projectDirectory: string,
37
+ ): Promise<RpcDirResolution> {
38
+ const override = process.env[RPC_DIR_ENV]?.trim()
39
+ if (override) {
40
+ return { dir: resolve(projectDirectory, override), secureDir: false }
41
+ }
42
+ const dir = getRpcDir(projectDirectory)
43
+ return { dir, secureDir: true, sweepRoot: dirname(dir) }
20
44
  }
21
45
 
22
46
  export { tmpdir }
@@ -8,7 +8,7 @@ import {
8
8
  import { join } from 'node:path'
9
9
  import { createLogger } from '../logger'
10
10
  import type { drainNotifications } from './notifications'
11
- import { writePortFile } from './port-file'
11
+ import { sweepRpcState, writePortFile } from './port-file'
12
12
  import type { ApplyRequest, ApplyResult } from './protocol'
13
13
 
14
14
  const log = createLogger('rpc')
@@ -21,6 +21,8 @@ export interface RpcServerHandle {
21
21
 
22
22
  export interface RpcServerOptions {
23
23
  dir: string
24
+ secureDir?: boolean
25
+ sweepRoot?: string
24
26
  drain: typeof drainNotifications
25
27
  apply: (request: ApplyRequest) => Promise<ApplyResult>
26
28
  timeoutMs?: number
@@ -112,8 +114,22 @@ export async function startRpcServer(
112
114
  })
113
115
  })
114
116
  server.unref()
117
+ if (options.sweepRoot) {
118
+ try {
119
+ await sweepRpcState(options.sweepRoot, options.dir)
120
+ } catch (error) {
121
+ log.warn('rpc state sweep failed', {
122
+ pid: process.pid,
123
+ error: error instanceof Error ? error.message : String(error),
124
+ })
125
+ }
126
+ }
115
127
  try {
116
- await writePortFile(options.dir, { port, token, pid: process.pid })
128
+ await writePortFile(
129
+ options.dir,
130
+ { port, token, pid: process.pid },
131
+ { secureDir: options.secureDir },
132
+ )
117
133
  log.debug('rpc server pid', {
118
134
  pid: process.pid,
119
135
  rpcPort: port,