@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.
- package/dist/cli.js +1 -1
- package/dist/commands.d.ts +3 -1
- package/dist/core/accounts.d.ts +6 -0
- package/dist/core/cachekeep.d.ts +16 -0
- package/dist/core/oauth.d.ts +1 -1
- package/dist/core/quota-manager.d.ts +33 -1
- package/dist/{index-gwmhhrrc.js → index-1xmasv0x.js} +1 -1
- package/dist/{index-899gpxdd.js → index-5s67238f.js} +1 -1
- package/dist/{index-53j7bdev.js → index-e7m7qbne.js} +1 -1
- package/dist/{index-7w467pz8.js → index-k5zwf3j1.js} +1 -1
- package/dist/{index-qp2nja20.js → index-mrg2fms9.js} +2 -2
- package/dist/{index-jn8xd8fy.js → index-qdh7pt5a.js} +1 -1
- package/dist/index-y8d01tgn.js +92 -0
- package/dist/index.js +68 -62
- package/dist/{quota-normalize-dxftsgep.js → quota-normalize-dfdpwwm3.js} +1 -1
- package/dist/rpc/port-file.js +1 -1
- package/dist/rpc/rpc-client.js +1 -1
- package/dist/rpc/rpc-dir.js +1 -1
- package/dist/sidebar-state.js +1 -1
- package/dist/{token-28n5g707.js → token-01dcf92a.js} +1 -1
- package/dist/token-util-rkqd82q9.js +1 -0
- package/dist/tui-preferences.js +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/ws-pool.d.ts +11 -0
- package/dist/ws.d.ts +1 -1
- package/package.json +6 -4
- package/src/tui/command-dialogs.tsx +77 -0
- package/src/tui/entry.mjs +37 -0
- package/src/tui-compiled/logger.ts +161 -0
- package/src/tui-compiled/rpc/port-file.ts +70 -0
- package/src/tui-compiled/rpc/protocol.ts +31 -0
- package/src/tui-compiled/rpc/rpc-client.ts +78 -0
- package/src/tui-compiled/rpc/rpc-dir.ts +22 -0
- package/src/tui-compiled/sidebar-state.ts +316 -0
- package/src/tui-compiled/tui/command-dialogs.tsx +640 -0
- package/src/tui-compiled/tui-preferences.ts +328 -0
- package/src/tui-compiled/tui.tsx +1041 -0
- package/src/tui-compiled/util/open-url.ts +30 -0
- package/src/tui.tsx +16 -13
- package/dist/index-z21wp7ac.js +0 -92
- 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-
|
|
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};
|
package/dist/rpc/port-file.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{da as a,ea as b}from"../index-k5zwf3j1.js";import"../index-e7m7qbne.js";export{a as writePortFile,b as discoverPortFile};
|
package/dist/rpc/rpc-client.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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};
|
package/dist/rpc/rpc-dir.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{fa as a,ga as b}from"../index-1xmasv0x.js";import"../index-e7m7qbne.js";export{a as tmpdir,b as getRpcDir};
|
package/dist/sidebar-state.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
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-
|
|
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();
|
package/dist/tui-preferences.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{
|
|
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.
|
|
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
|
-
/**
|
|
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.
|
|
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.
|
|
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
|
|
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 }
|