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
396 changes: 396 additions & 0 deletions .opencode/plugins/anthropic-token-bridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,396 @@
// lib/file-logger.ts
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
import { dirname } from "node:path";
var LOG_PATH = "/tmp/pai-opencode-debug.log";
function fileLog(message, level = "info") {
try {
const timestamp = new Date().toISOString();
const levelPrefix = level.toUpperCase().padEnd(5);
const logLine = `[${timestamp}] [${levelPrefix}] ${message}\n`;
const dir = dirname(LOG_PATH);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
appendFileSync(LOG_PATH, logLine);
} catch {}
}
function fileLogError(message, error2) {
const errorMessage = error2 instanceof Error ? `${error2.message}\n${error2.stack || ""}` : String(error2);
fileLog(`${message}: ${errorMessage}`, "error");
}
function info(message, meta) {
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
fileLog(`${message}${metaStr}`, "info");
}
function warn(message, meta) {
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
fileLog(`${message}${metaStr}`, "warn");
}
function error(message, meta) {
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
fileLog(`${message}${metaStr}`, "error");
}

// lib/token-utils.ts
import * as fs from "node:fs";
import * as os from "node:os";
import * as path from "node:path";
var AUTH_FILE = path.join(os.homedir(), ".local", "share", "opencode", "auth.json");
var REFRESH_THRESHOLD_MS = 2 * 60 * 60 * 1000;
function readAuthFile() {
try {
const content = fs.readFileSync(AUTH_FILE, "utf8");
return JSON.parse(content);
} catch (err) {
error("Failed to read auth.json", { error: String(err) });
return null;
}
}
function writeAuthFile(authData) {
try {
fs.writeFileSync(AUTH_FILE, JSON.stringify(authData, null, 2) + "\n");
return true;
} catch (err) {
error("Failed to write auth.json", { error: String(err) });
return false;
}
}
function checkAnthropicToken() {
const auth = readAuthFile();
if (!auth) {
return { valid: false, exists: false, expiresSoon: false, timeRemainingMs: 0, reason: "auth_file_not_readable" };
}
const anthropic = auth.anthropic;
if (!anthropic) {
return { valid: false, exists: false, expiresSoon: false, timeRemainingMs: 0, reason: "no_anthropic_config" };
}
if (anthropic.type !== "oauth") {
return {
valid: false,
exists: true,
expiresSoon: false,
timeRemainingMs: 0,
reason: "not_oauth_type",
maskedAccess: maskToken(anthropic.access)
};
}
const now = Date.now();
const expires = anthropic.expires ?? 0;
const timeRemainingMs = expires - now;
if (timeRemainingMs <= 0) {
warn("Anthropic token expired", {
expiredAt: new Date(expires).toISOString(),
maskedAccess: maskToken(anthropic.access)
});
return {
valid: false,
exists: true,
expiresSoon: true,
timeRemainingMs: 0,
expiresAt: new Date(expires),
maskedAccess: maskToken(anthropic.access),
reason: "token_expired"
};
}
const expiresSoon = timeRemainingMs < REFRESH_THRESHOLD_MS;
const hoursRemaining = Math.floor(timeRemainingMs / (60 * 60 * 1000));
if (expiresSoon) {
warn("Anthropic token expires soon", {
hoursRemaining,
expiresAt: new Date(expires).toISOString(),
maskedAccess: maskToken(anthropic.access)
});
} else {
info("Anthropic token valid", {
hoursRemaining,
expiresAt: new Date(expires).toISOString()
});
}
return {
valid: true,
exists: true,
expiresSoon,
timeRemainingMs,
expiresAt: new Date(expires),
maskedAccess: maskToken(anthropic.access),
reason: expiresSoon ? "expires_soon" : "valid"
};
}
function updateAnthropicTokens(accessToken, refreshToken, expiresInSeconds) {
const auth = readAuthFile();
if (!auth) {
error("Cannot update tokens: auth.json not readable");
return false;
}
const expiresAt = Date.now() + expiresInSeconds * 1000;
auth.anthropic = {
type: "oauth",
access: accessToken,
refresh: refreshToken,
expires: expiresAt
};
const success = writeAuthFile(auth);
if (success) {
info("Updated anthropic tokens", {
expiresAt: new Date(expiresAt).toISOString(),
maskedAccess: maskToken(accessToken)
});
}
return success;
}
function maskToken(token) {
if (!token || token.length < 20) return "***";
return `${token.slice(0, 8)}...${token.slice(-4)}`;
}

// lib/refresh-manager.ts
import { spawn } from "node:child_process";
var EXEC_TIMEOUT_MS = 15000;
var REFRESH_COOLDOWN_MS = 5 * 60 * 1000;
var isRefreshing = false;
var lastRefreshAttempt = 0;
function isRefreshInProgress() {
if (isRefreshing) return true;
return Date.now() - lastRefreshAttempt < REFRESH_COOLDOWN_MS;
}
function execCommand(command, args) {
return new Promise((resolve) => {
const child = spawn(command, args);
let stdout = "";
let stderr = "";
let settled = false;
const settle = (result) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolve(result);
};
const timer = setTimeout(() => {
child.kill();
settle({ stdout, stderr: "timeout", exitCode: 1 });
}, EXEC_TIMEOUT_MS);
child.stdout?.on("data", (data) => {
stdout += data.toString();
});
child.stderr?.on("data", (data) => {
stderr += data.toString();
});
child.on("close", (exitCode) => {
settle({ stdout, stderr, exitCode: exitCode ?? 0 });
});
child.on("error", (err) => {
settle({ stdout, stderr: String(err), exitCode: 1 });
});
});
}
async function extractFromKeychain() {
try {
const { stdout, exitCode } = await execCommand("security", [
"find-generic-password",
"-s",
"Claude Code-credentials",
"-w"
]);
if (exitCode !== 0) {
error("Failed to extract from Keychain", { exitCode, stderr: stdout });
return null;
}
const credentials = JSON.parse(stdout.trim());
const oauth = credentials.claudeAiOauth;
// pragma: allowlist secret — runtime extraction from macOS Keychain
const accessToken = oauth?.accessToken ?? credentials.accessToken ?? credentials.access_token;
const refreshToken = oauth?.refreshToken ?? credentials.refreshToken ?? credentials.refresh_token;
if (!accessToken || !refreshToken) {
error("Invalid credentials format from Keychain", {
hasAccess: !!accessToken,
hasRefresh: !!refreshToken
});
return null;
}
return { accessToken, refreshToken };
} catch (err) {
error("Exception extracting from Keychain", { error: String(err) });
return null;
}
}
async function generateSetupToken() {
try {
info("Generating new setup token via Claude Code");
const { stdout, exitCode, stderr } = await execCommand("claude", ["setup-token"]);
if (exitCode !== 0) {
error("claude setup-token failed", { exitCode, stderr });
return null;
}
// Validate token presence without logging the raw value
const tokenMatch = stdout.match(/sk-ant-[a-z0-9-]+/i);
if (!tokenMatch) {
error("Could not find token in Claude output");
return null;
}
info("Successfully generated setup token");
return tokenMatch[0]; // pragma: allowlist secret — runtime value from claude CLI, not hardcoded
} catch (err) {
error("Exception generating setup token", { error: String(err) });
return null;
}
}
async function exchangeSetupToken(setupToken) {
try {
info("Exchanging setup token for OAuth credentials");
const response = await fetch("https://api.anthropic.com/v1/oauth/setup_token/exchange", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${setupToken}`,
"anthropic-version": "2023-06-01"
},
body: JSON.stringify({ grant_type: "setup_token" })
});
if (!response.ok) {
error("Token exchange failed", { status: response.status });
return null;
}
const data = await response.json();
if (!data.access_token || !data.refresh_token) {
error("Invalid exchange response", {
hasAccess: !!data.access_token,
hasRefresh: !!data.refresh_token
});
return null;
}
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
expiresIn: data.expires_in ?? 28800
};
} catch (err) {
error("Exception exchanging setup token", { error: String(err) });
return null;
}
}
async function refreshAnthropicToken() {
if (isRefreshing) {
info("Refresh already in progress, skipping");
return false;
}
const timeSinceLastAttempt = Date.now() - lastRefreshAttempt;
if (timeSinceLastAttempt < REFRESH_COOLDOWN_MS) {
info("Refresh on cooldown", {
minutesRemaining: Math.ceil((REFRESH_COOLDOWN_MS - timeSinceLastAttempt) / 60000)
});
return false;
}
isRefreshing = true;
lastRefreshAttempt = Date.now();
try {
info("Starting token refresh process");
const keychainTokens = await extractFromKeychain();
if (keychainTokens) {
info("Found tokens in Keychain, updating auth.json");
const success2 = updateAnthropicTokens(keychainTokens.accessToken, keychainTokens.refreshToken, 28800);
if (success2) {
info("Token refresh successful via Keychain");
return true;
}
}
info("Keychain extraction failed, generating new setup token");
const setupToken = await generateSetupToken();
if (!setupToken) {
error("Failed to generate setup token - is Claude Code authenticated?");
return false;
}
const oauthTokens = await exchangeSetupToken(setupToken);
if (!oauthTokens) {
error("Failed to exchange setup token");
return false;
}
const success = updateAnthropicTokens(oauthTokens.accessToken, oauthTokens.refreshToken, oauthTokens.expiresIn);
if (success) {
info("Token refresh successful via setup token exchange");
return true;
}
return false;
} catch (err) {
error("Unexpected error during refresh", { error: String(err) });
return false;
} finally {
isRefreshing = false;
}
}

// anthropic-token-bridge.ts
var CHECK_INTERVAL_MESSAGES = 5;
var messageCount = 0;
async function AnthropicTokenBridge() {
fileLog("AnthropicTokenBridge plugin loaded", "info");
return {
async "chat.message"(_input, output) {
const msg = output?.message;
if (!msg?.role || msg.role !== "user") return;
messageCount++;
if (messageCount % CHECK_INTERVAL_MESSAGES !== 0) return;
try {
fileLog(`Checking token status (message #${messageCount})`, "debug");
const status = checkAnthropicToken();
if (!status.exists) {
fileLog("No Anthropic OAuth token configured", "debug");
return;
}
if (status.valid && !status.expiresSoon) {
fileLog("Token valid, no refresh needed", "debug");
return;
}
if (status.expiresSoon || !status.valid) {
const hoursRemaining = Math.floor(status.timeRemainingMs / (60 * 60 * 1000));
fileLog(`Token expires in ${hoursRemaining}h, triggering refresh`, "warn");
if (isRefreshInProgress()) {
fileLog("Refresh already in progress, skipping", "info");
return;
}
fileLog("Starting async token refresh", "info");
refreshAnthropicToken().then((success) => {
if (success) {
fileLog("Token refresh completed successfully", "info");
} else {
fileLog("Token refresh failed - will retry on next check", "error");
}
}).catch((err) => {
fileLogError("Unexpected error during refresh", err);
});
}
} catch (err) {
fileLogError("Error in chat.message handler", err);
}
},
async "experimental.chat.system.transform"(_input, _output) {
try {
fileLog("Session started, checking initial token status", "info");
const status = checkAnthropicToken();
if (!status.exists) {
fileLog("No Anthropic token configured at session start", "debug");
return;
}
if (status.expiresSoon || !status.valid) {
fileLog("Token expires soon at session start, scheduling refresh", "warn");
setTimeout(() => {
if (!isRefreshInProgress()) {
refreshAnthropicToken().then((success) => {
if (success) fileLog("Session-start refresh successful", "info");
}).catch((err) => {
fileLogError("Session-start refresh error", err);
});
}
}, 5000);
} else {
const hoursRemaining = Math.floor(status.timeRemainingMs / (60 * 60 * 1000));
fileLog(`Token valid for ${hoursRemaining}h at session start`, "info");
}
} catch (err) {
fileLogError("Error in system.transform handler", err);
}
}
};
}
export {
AnthropicTokenBridge as default
};
Loading
Loading