Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@dnd-kit/utilities": "^3.2.2",
"@tailwindcss/vite": "^4.1.18",
"@tauri-apps/api": "^2",
"@tauri-apps/plugin-log": "^2.8.0",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-process": "^2.3.1",
"@tauri-apps/plugin-store": "^2.4.2",
Expand Down
46 changes: 41 additions & 5 deletions plugins/claude/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,13 @@
if (parsed) {
const oauth = parsed.claudeAiOauth
if (oauth && oauth.accessToken) {
ctx.host.log.info("credentials loaded from file")
return { oauth, source: "file", fullData: parsed }
}
}
ctx.host.log.warn("credentials file exists but no valid oauth data")
} catch (e) {
ctx.host.log.warn("credentials file read failed: " + String(e))
}
}

Expand All @@ -145,13 +148,17 @@
if (parsed) {
const oauth = parsed.claudeAiOauth
if (oauth && oauth.accessToken) {
ctx.host.log.info("credentials loaded from keychain")
return { oauth, source: "keychain", fullData: parsed }
}
}
ctx.host.log.warn("keychain has data but no valid oauth")
}
} catch (e) {
ctx.host.log.info("keychain read failed (may not exist): " + String(e))
}

ctx.host.log.warn("no credentials found")
return null
}

Expand Down Expand Up @@ -184,8 +191,12 @@

function refreshToken(ctx, creds) {
const { oauth, source, fullData } = creds
if (!oauth.refreshToken) return null
if (!oauth.refreshToken) {
ctx.host.log.warn("refresh skipped: no refresh token")
return null
}

ctx.host.log.info("attempting token refresh")
try {
const resp = ctx.util.request({
method: "POST",
Expand All @@ -204,17 +215,27 @@
let errorCode = null
const body = ctx.util.tryParseJson(resp.bodyText)
if (body) errorCode = body.error || body.error_description
ctx.host.log.error("refresh failed: status=" + resp.status + " error=" + String(errorCode))
if (errorCode === "invalid_grant") {
throw "Session expired. Run `claude` to log in again."
}
throw "Token expired. Run `claude` to log in again."
}
if (resp.status < 200 || resp.status >= 300) return null
if (resp.status < 200 || resp.status >= 300) {
ctx.host.log.warn("refresh returned unexpected status: " + resp.status)
return null
}

const body = ctx.util.tryParseJson(resp.bodyText)
if (!body) return null
if (!body) {
ctx.host.log.warn("refresh response not valid JSON")
return null
}
const newAccessToken = body.access_token
if (!newAccessToken) return null
if (!newAccessToken) {
ctx.host.log.warn("refresh response missing access_token")
return null
}

// Update oauth credentials
oauth.accessToken = newAccessToken
Expand All @@ -227,9 +248,11 @@
fullData.claudeAiOauth = oauth
saveCredentials(ctx, source, fullData)

ctx.host.log.info("refresh succeeded, new token expires in " + (body.expires_in || "unknown") + "s")
return newAccessToken
} catch (e) {
if (typeof e === "string") throw e
ctx.host.log.error("refresh exception: " + String(e))
return null
}
}
Expand All @@ -252,6 +275,7 @@
function probe(ctx) {
const creds = loadCredentials(ctx)
if (!creds || !creds.oauth || !creds.oauth.accessToken || !creds.oauth.accessToken.trim()) {
ctx.host.log.error("probe failed: not logged in")
throw "Not logged in. Run `claude` to authenticate."
}

Expand All @@ -260,8 +284,13 @@

// Proactively refresh if token is expired or about to expire
if (needsRefresh(ctx, creds.oauth, nowMs)) {
ctx.host.log.info("token needs refresh (expired or expiring soon)")
const refreshed = refreshToken(ctx, creds)
if (refreshed) accessToken = refreshed
if (refreshed) {
accessToken = refreshed
} else {
ctx.host.log.warn("proactive refresh failed, trying with existing token")
}
}

let resp
Expand All @@ -272,29 +301,36 @@
try {
return fetchUsage(ctx, token || accessToken)
} catch (e) {
ctx.host.log.error("usage request exception: " + String(e))
if (didRefresh) {
throw "Usage request failed after refresh. Try again."
}
throw "Usage request failed. Check your connection."
}
},
refresh: () => {
ctx.host.log.info("usage returned 401, attempting refresh")
didRefresh = true
return refreshToken(ctx, creds)
},
})
} catch (e) {
if (typeof e === "string") throw e
ctx.host.log.error("usage request failed: " + String(e))
throw "Usage request failed. Check your connection."
}

if (ctx.util.isAuthStatus(resp.status)) {
ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
throw "Token expired. Run `claude` to log in again."
}

if (resp.status < 200 || resp.status >= 300) {
ctx.host.log.error("usage returned error: status=" + resp.status)
throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
}

ctx.host.log.info("usage fetch succeeded")

let data
data = ctx.util.tryParseJson(resp.bodyText)
Expand Down
61 changes: 51 additions & 10 deletions plugins/codex/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,21 @@
const REFRESH_AGE_MS = 8 * 24 * 60 * 60 * 1000

function loadAuth(ctx) {
if (!ctx.host.fs.exists(AUTH_PATH)) return null
if (!ctx.host.fs.exists(AUTH_PATH)) {
ctx.host.log.warn("auth file not found: " + AUTH_PATH)
return null
}
try {
const text = ctx.host.fs.readText(AUTH_PATH)
return ctx.util.tryParseJson(text)
} catch {
const auth = ctx.util.tryParseJson(text)
if (auth) {
ctx.host.log.info("auth loaded from file")
} else {
ctx.host.log.warn("auth file exists but not valid JSON")
}
return auth
} catch (e) {
ctx.host.log.warn("auth file read failed: " + String(e))
return null
}
}
Expand All @@ -23,8 +33,12 @@
}

function refreshToken(ctx, auth) {
if (!auth.tokens || !auth.tokens.refresh_token) return null
if (!auth.tokens || !auth.tokens.refresh_token) {
ctx.host.log.warn("refresh skipped: no refresh token")
return null
}

ctx.host.log.info("attempting token refresh")
try {
const resp = ctx.util.request({
method: "POST",
Expand All @@ -43,6 +57,7 @@
if (body) {
code = body.error?.code || body.error || body.code
}
ctx.host.log.error("refresh failed: status=" + resp.status + " code=" + String(code))
if (code === "refresh_token_expired") {
throw "Session expired. Run `codex` to log in again."
}
Expand All @@ -54,12 +69,21 @@
}
throw "Token expired. Run `codex` to log in again."
}
if (resp.status < 200 || resp.status >= 300) return null
if (resp.status < 200 || resp.status >= 300) {
ctx.host.log.warn("refresh returned unexpected status: " + resp.status)
return null
}

const body = ctx.util.tryParseJson(resp.bodyText)
if (!body) return null
if (!body) {
ctx.host.log.warn("refresh response not valid JSON")
return null
}
const newAccessToken = body.access_token
if (!newAccessToken) return null
if (!newAccessToken) {
ctx.host.log.warn("refresh response missing access_token")
return null
}

auth.tokens.access_token = newAccessToken
if (body.refresh_token) auth.tokens.refresh_token = body.refresh_token
Expand All @@ -68,11 +92,15 @@

try {
ctx.host.fs.writeText(AUTH_PATH, JSON.stringify(auth, null, 2))
} catch {}
ctx.host.log.info("refresh succeeded, auth file updated")
} catch (e) {
ctx.host.log.warn("refresh succeeded but failed to save auth: " + String(e))
}

return newAccessToken
} catch (e) {
if (typeof e === "string") throw e
ctx.host.log.error("refresh exception: " + String(e))
return null
}
}
Expand Down Expand Up @@ -118,6 +146,7 @@
function probe(ctx) {
const auth = loadAuth(ctx)
if (!auth) {
ctx.host.log.error("probe failed: not logged in")
throw "Not logged in. Run `codex` to authenticate."
}

Expand All @@ -127,8 +156,13 @@
const accountId = auth.tokens.account_id

if (needsRefresh(ctx, auth, nowMs)) {
ctx.host.log.info("token needs refresh (age > " + (REFRESH_AGE_MS / 1000 / 60 / 60 / 24) + " days)")
const refreshed = refreshToken(ctx, auth)
if (refreshed) accessToken = refreshed
if (refreshed) {
accessToken = refreshed
} else {
ctx.host.log.warn("proactive refresh failed, trying with existing token")
}
}

let resp
Expand All @@ -138,30 +172,37 @@
request: (token) => {
try {
return fetchUsage(ctx, token || accessToken, accountId)
} catch {
} catch (e) {
ctx.host.log.error("usage request exception: " + String(e))
if (didRefresh) {
throw "Usage request failed after refresh. Try again."
}
throw "Usage request failed. Check your connection."
}
},
refresh: () => {
ctx.host.log.info("usage returned 401, attempting refresh")
didRefresh = true
return refreshToken(ctx, auth)
},
})
} catch (e) {
if (typeof e === "string") throw e
ctx.host.log.error("usage request failed: " + String(e))
throw "Usage request failed. Check your connection."
}

if (ctx.util.isAuthStatus(resp.status)) {
ctx.host.log.error("usage returned auth error after all retries: status=" + resp.status)
throw "Token expired. Run `codex` to log in again."
}

if (resp.status < 200 || resp.status >= 300) {
ctx.host.log.error("usage returned error: status=" + resp.status)
throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
}

ctx.host.log.info("usage fetch succeeded")

const data = ctx.util.tryParseJson(resp.bodyText)
if (data === null) {
Expand Down
Loading