Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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