Skip to content

feat(oauth): add runtime auto-refresh token bridge#85

Merged
Steffen025 merged 4 commits intodevfrom
feat/oauth-auto-refresh
Mar 21, 2026
Merged

feat(oauth): add runtime auto-refresh token bridge#85
Steffen025 merged 4 commits intodevfrom
feat/oauth-auto-refresh

Conversation

@Steffen025
Copy link
Owner

@Steffen025 Steffen025 commented Mar 21, 2026

Summary

Changes

New files

  • .opencode/plugins/anthropic-token-bridge.js — compiled plugin, runs in OpenCode
  • .opencode/plugins/anthropic-token-bridge.ts — source TypeScript
  • .opencode/plugins/lib/token-utils.ts — reads/writes auth.json, checks expiry
  • .opencode/plugins/lib/refresh-manager.ts — 3-strategy refresh (Keychain → claude setup-token → exchange API)
  • contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js — included in standalone contrib package

Modified files

  • .opencode/plugins/lib/file-logger.ts — add info() / warn() / error() wrappers (needed by token bridge)
  • contrib/anthropic-max-bridge/install.sh — copies both plugins, updates token refresh note

How it works

Every 5 user messages the plugin checks auth.json. If the token expires in <2h it triggers an async refresh:

  1. Strategy 1 — Extract fresh token from macOS Keychain (security find-generic-password)
  2. Strategy 2 — Generate setup token via claude setup-token CLI
  3. Strategy 3 — Exchange setup token via api.anthropic.com/v1/oauth/setup_token/exchange

Refresh has a 5-minute cooldown to prevent hammering. All logging goes to /tmp/pai-opencode-debug.log (TUI-safe, ADR-004).

Summary by CodeRabbit

  • New Features

    • Automatic Anthropic OAuth refresh: checks every 5 user messages and at session start; triggers background refresh when tokens are missing or near expiry.
    • Refresh safeguards: prevents concurrent refreshes and enforces a cooldown between attempts.
  • Documentation

    • Installer and README updated to install both plugins and explain auto-refresh plus a manual fallback.
  • Other

    • Added local debug logging for refresh activity and outcomes.

- Add anthropic-token-bridge.js/.ts: checks token every 5 messages,
  auto-refreshes from macOS Keychain when <2h remaining
- Add lib/token-utils.ts + lib/refresh-manager.ts: token lifecycle logic
  (3 strategies: Keychain → claude setup-token → exchange API)
- Add info()/warn()/error() wrappers to file-logger.ts (needed by token bridge)
- Add anthropic-token-bridge.js to contrib/anthropic-max-bridge/plugins/
- Update contrib/install.sh: copies both plugins, updates token refresh note
@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

📝 Walkthrough

Walkthrough

Adds an Anthropic OAuth token auto-refresh plugin and supporting utilities that read/write session auth at ~/.local/share/opencode/auth.json, check token expiry, attempt refresh (Keychain → Claude CLI → Anthropic exchange), persist new tokens, and register handlers to trigger checks during chat sessions.

Changes

Cohort / File(s) Summary
Core Token Bridge Plugin
./.opencode/plugins/anthropic-token-bridge.ts, ./.opencode/plugins/anthropic-token-bridge.js, contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js
New plugin registering chat.message (every 5 user messages) and experimental.chat.system.transform handlers; checks Anthropic token status and asynchronously triggers guarded refresh when missing, expired, or expiring soon; logs outcomes to file.
Token Management
./.opencode/plugins/lib/token-utils.ts, ./.opencode/plugins/lib/refresh-manager.ts
Implements token status inspection, masking, auth.json read/write, and a multi-strategy refresh manager with single-flight, 5‑minute cooldown, Keychain extraction, Claude CLI fallback, OAuth setup-token exchange, and persistence via updateAnthropicTokens.
Logging Helpers
./.opencode/plugins/lib/file-logger.ts
Adds info, warn, and error wrappers that serialize optional metadata and forward messages to the existing file logger.
Contrib Installer & Docs
contrib/anthropic-max-bridge/install.sh, contrib/anthropic-max-bridge/README.md, contrib/anthropic-max-bridge/TECHNICAL.md
Installer now installs the token-bridge plugin and documentation updated to describe automatic 5-message refresh checks, Keychain/Claude refresh paths, cooldown behavior, and manual refresh-token.sh fallback.

Sequence Diagram(s)

sequenceDiagram
    participant Handler as Chat Handler
    participant Plugin as Token Bridge Plugin
    participant TokenUtils as Token Utils
    participant AuthFile as Auth File\n(~/.local/share/opencode/auth.json)
    participant Keychain as macOS Keychain
    participant Claude as Claude CLI
    participant AnthropicAPI as Anthropic OAuth API

    Handler->>Plugin: Trigger check (every 5th user message or session start)
    Plugin->>TokenUtils: checkAnthropicToken()
    TokenUtils->>AuthFile: Read auth.json
    AuthFile-->>TokenUtils: Token data / expiresAt

    alt Missing/expired/expiring soon
        TokenUtils-->>Plugin: status (requires refresh)
        Plugin->>Plugin: isRefreshInProgress? / cooldown
        alt Not refreshing
            Plugin->>Keychain: Run `security find-generic-password`
            alt Keychain returns tokens
                Keychain-->>Plugin: access_token, refresh_token
            else Keychain failed
                Plugin->>Claude: Run `claude setup-token`
                Claude-->>Plugin: setup_token (sk-ant-...)
                Plugin->>AnthropicAPI: POST /v1/oauth/setup_token/exchange
                AnthropicAPI-->>Plugin: access_token, refresh_token, expires_in
            end
            Plugin->>TokenUtils: updateAnthropicTokens(access, refresh, expires_in)
            TokenUtils->>AuthFile: Write updated auth.json
            AuthFile-->>TokenUtils: Write success
            TokenUtils-->>Plugin: success
        else Refresh in progress or cooldown
            Plugin-->>Handler: Skip refresh (in-progress/cooldown)
        end
    else Valid
        TokenUtils-->>Plugin: no action required
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hop through logs at midnight’s gate,
Five tiny hops to check the state,
Keychain hums, Claude whispers a key,
I stitch new tokens, set sessions free,
Hopping happy — auth refreshed with glee ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(oauth): add runtime auto-refresh token bridge' accurately summarizes the main change: introducing a new runtime plugin that automatically refreshes Anthropic OAuth tokens.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/oauth-auto-refresh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- README: Token Expiry → auto-refresh primary, manual fallback secondary
- README: File Reference → add anthropic-token-bridge.js entry
- README: install.sh step → mention both plugins copied
- README: token flow diagram → show both plugins
- README: HTTP 401 troubleshooting → mention auto-refresh failed
- TECHNICAL: Token lifetime → replace manual-only with auto-refresh details
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (3)
.opencode/plugins/lib/refresh-manager.ts (1)

21-40: Consider adding a timeout to prevent indefinite hangs.

The execCommand function has no timeout, so if security or claude commands hang (e.g., waiting for user input, Keychain prompt stuck), the refresh process will hang indefinitely.

⏱️ Proposed fix to add timeout
-function execCommand(command: string, args: string[]): Promise<ExecResult> {
-	return new Promise((resolve) => {
+const EXEC_TIMEOUT_MS = 30_000; // 30 seconds
+
+function execCommand(command: string, args: string[]): Promise<ExecResult> {
+	return new Promise((resolve) => {
 		const child = spawn(command, args);
 		let stdout = "";
 		let stderr = "";
+		const timeout = setTimeout(() => {
+			child.kill();
+			resolve({ stdout, stderr: "Command timed out", exitCode: 124 });
+		}, EXEC_TIMEOUT_MS);

 		child.stdout?.on("data", (data: Buffer) => {
 			stdout += data.toString();
 		});
 		child.stderr?.on("data", (data: Buffer) => {
 			stderr += data.toString();
 		});
 		child.on("close", (exitCode: number | null) => {
+			clearTimeout(timeout);
 			resolve({ stdout, stderr, exitCode: exitCode ?? 0 });
 		});
 		child.on("error", (err: Error) => {
+			clearTimeout(timeout);
 			resolve({ stdout, stderr: String(err), exitCode: 1 });
 		});
 	});
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/lib/refresh-manager.ts around lines 21 - 40, The
execCommand function can hang indefinitely; update execCommand to accept an
optional timeout parameter (or use a default timeout) and enforce it by starting
a timer when spawn(command, args) is called, and on timeout kill the child
process (child.kill()) and resolve with stderr indicating a timeout and a
non-zero exitCode; ensure you clear the timer on normal close or error and
remove/avoid double-resolving the promise by wiring the timer, 'close', and
'error' handlers to a single resolve path. Reference symbols: function
execCommand, local child variable (spawn), child.on('close'), child.on('error').
.opencode/plugins/anthropic-token-bridge.js (1)

41-59: Async/sync inconsistency: functions marked async but use synchronous operations.

readAuthFile and writeAuthFile are marked async and await on the error() calls, but error() is synchronous (returns void). The TypeScript source uses synchronous functions. This won't break at runtime, but indicates the bundle may be out of sync with the source.

Consider regenerating this bundle from the TypeScript source to ensure consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/anthropic-token-bridge.js around lines 41 - 59, The bundle
incorrectly marks readAuthFile and writeAuthFile as async and awaits error(),
while they use synchronous fs APIs; change these to synchronous functions to
match the TS source by removing the async keyword and any await before error(),
keeping fs.readFileSync and fs.writeFileSync and returning values directly (use
error(...) instead of await error(...)) so readAuthFile, writeAuthFile,
AUTH_FILE and error() usage are consistent; alternatively regenerate the bundle
from the TypeScript source if that was the intended fix.
contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js (1)

1-405: This file is a duplicate of .opencode/plugins/anthropic-token-bridge.js.

The same issues noted in the other review apply here:

  • Line 164: Dead code (promisify(spawn))
  • Lines 41-59: Async/sync inconsistency
  • Lines 206-207: Pipeline false positive for secret detection

Consider generating both files from a single source to avoid maintenance burden and drift between copies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js` around lines
1 - 405, This file duplicates .opencode/plugins/anthropic-token-bridge.js and
has three actionable issues: remove the dead promisify(spawn) usage (the exec
variable should call your execCommand wrapper, remove "var exec =
promisify(spawn)" and any use that relies on it), fix async/sync mismatch in
readAuthFile and writeAuthFile (these functions are declared async but use
synchronous fs methods and call await error/logger which are synchronous —
either make the logger functions return Promises or remove the unnecessary
async/await and return values synchronously; update readAuthFile/writeAuthFile
to consistently use either fs.promises or to be non-async), and stop leaking
potential setup tokens in logs by not matching or logging raw tokens in
generateSetupToken (replace the raw regex tokenMatch usage by safely validating
presence without storing or logging the full token, and ensure any logs
redact/mask matches). Also consolidate this file with the existing one to avoid
duplication and drift (generate both outputs from a single source or remove the
duplicate).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.opencode/plugins/anthropic-token-bridge.js:
- Around line 206-207: The pipeline flagged runtime extractions of secrets for
accessToken and refreshToken as hardcoded — update the lines that define const
accessToken = credentials.accessToken || credentials.access_token; and const
refreshToken = credentials.refreshToken || credentials.refresh_token; to include
the same suppression comment used in the TypeScript source (e.g., an appropriate
Snyk/semgrep/secret-scan suppression annotation) indicating these are runtime
Keychain extractions, so the scanner treats them as false positives; place the
suppression comment immediately above or on the same line as those const
declarations and ensure it references the scanner rule ID used in the project.
- Around line 163-164: Remove the dead promisify usage: the line creating exec
via promisify(spawn) is invalid because spawn returns a ChildProcess, and exec
is unused; delete the import of promisify and the var exec = promisify(spawn)
statement so the module only uses spawn directly (see symbols promisify, spawn,
and exec in this file) and ensure no remaining references to exec remain.

In @.opencode/plugins/anthropic-token-bridge.ts:
- Around line 13-14: The chat.message handler incorrectly reads role from input;
change it to read the message from output by extracting const msg = (output as
any).message and then guard with if (!msg?.role || msg.role !== "user") return;
— update the async "chat.message"(...) handler to use msg/role from
output.message instead of input.role so only user messages pass.

In @.opencode/plugins/lib/refresh-manager.ts:
- Around line 70-71: This is a false positive from the secret scanner on runtime
token extractions: update the token-extraction site (where you set accessToken =
credentials.accessToken ?? credentials.access_token and refreshToken =
credentials.refreshToken ?? credentials.refresh_token) to include a suppression
comment recognized by our secret-scanner immediately above those lines (or the
surrounding function) so the scanner knows these are runtime values from
Keychain, not hardcoded secrets; reference the variables accessToken,
refreshToken and the credentials object when adding the suppression and ensure
the comment follows the scanner's expected syntax/pattern for ignored findings.

In @.opencode/plugins/lib/token-utils.ts:
- Line 5: AUTH_FILE currently uses process.env.HOME with a literal "~" fallback
which won't expand; update token-utils.ts to import node:os and use os.homedir()
when building AUTH_FILE (replace the AUTH_FILE definition that references
process.env.HOME or "~" with a path.join(os.homedir(), ".local", "share",
"opencode", "auth.json")) and ensure the os import is added at the top of the
file.

---

Nitpick comments:
In @.opencode/plugins/anthropic-token-bridge.js:
- Around line 41-59: The bundle incorrectly marks readAuthFile and writeAuthFile
as async and awaits error(), while they use synchronous fs APIs; change these to
synchronous functions to match the TS source by removing the async keyword and
any await before error(), keeping fs.readFileSync and fs.writeFileSync and
returning values directly (use error(...) instead of await error(...)) so
readAuthFile, writeAuthFile, AUTH_FILE and error() usage are consistent;
alternatively regenerate the bundle from the TypeScript source if that was the
intended fix.

In @.opencode/plugins/lib/refresh-manager.ts:
- Around line 21-40: The execCommand function can hang indefinitely; update
execCommand to accept an optional timeout parameter (or use a default timeout)
and enforce it by starting a timer when spawn(command, args) is called, and on
timeout kill the child process (child.kill()) and resolve with stderr indicating
a timeout and a non-zero exitCode; ensure you clear the timer on normal close or
error and remove/avoid double-resolving the promise by wiring the timer,
'close', and 'error' handlers to a single resolve path. Reference symbols:
function execCommand, local child variable (spawn), child.on('close'),
child.on('error').

In `@contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js`:
- Around line 1-405: This file duplicates
.opencode/plugins/anthropic-token-bridge.js and has three actionable issues:
remove the dead promisify(spawn) usage (the exec variable should call your
execCommand wrapper, remove "var exec = promisify(spawn)" and any use that
relies on it), fix async/sync mismatch in readAuthFile and writeAuthFile (these
functions are declared async but use synchronous fs methods and call await
error/logger which are synchronous — either make the logger functions return
Promises or remove the unnecessary async/await and return values synchronously;
update readAuthFile/writeAuthFile to consistently use either fs.promises or to
be non-async), and stop leaking potential setup tokens in logs by not matching
or logging raw tokens in generateSetupToken (replace the raw regex tokenMatch
usage by safely validating presence without storing or logging the full token,
and ensure any logs redact/mask matches). Also consolidate this file with the
existing one to avoid duplication and drift (generate both outputs from a single
source or remove the duplicate).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9ee43b80-3f36-4f66-acf3-2ac8928f2e25

📥 Commits

Reviewing files that changed from the base of the PR and between b1b1f0c and 11e5e88.

📒 Files selected for processing (7)
  • .opencode/plugins/anthropic-token-bridge.js
  • .opencode/plugins/anthropic-token-bridge.ts
  • .opencode/plugins/lib/file-logger.ts
  • .opencode/plugins/lib/refresh-manager.ts
  • .opencode/plugins/lib/token-utils.ts
  • contrib/anthropic-max-bridge/install.sh
  • contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
contrib/anthropic-max-bridge/README.md (1)

88-90: Broaden auto-refresh wording to match actual fallback behavior.

Current text implies Keychain is the only refresh source, but runtime logic also falls back to claude setup-token and setup-token exchange. Please align this section with the 3-strategy flow to avoid support confusion.

Suggested doc tweak
-**Auto-refresh (default):** The `anthropic-token-bridge` plugin checks your token every 5 messages and refreshes it automatically from the macOS Keychain — no action needed.
+**Auto-refresh (default):** The `anthropic-token-bridge` plugin checks your token every 5 messages and refreshes it automatically (Keychain first, then CLI/setup-token fallback paths) — no action needed.
...
-> So the Keychain always has a fresh token after any `claude` use — which is what the auto-refresh plugin pulls from.
+> So the Keychain is usually fresh after any `claude` use, and the plugin uses it first before fallback refresh paths.

Also applies to: 99-99

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contrib/anthropic-max-bridge/README.md` around lines 88 - 90, Update the
"Auto-refresh (default)" / "Manual refresh (fallback)" wording to reflect the
three-step refresh flow used by the anthroptic-token-bridge: state that the
plugin attempts automatic refresh from the macOS Keychain every 5 messages, then
falls back to invoking the local `claude setup-token` flow, and finally to the
interactive `setup-token` exchange if the previous methods fail; mention all
three strategies and that no user action is needed unless all fallbacks fail,
and reference the `anthropic-token-bridge`, `claude setup-token`, and
`setup-token` terms so readers can find the commands.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@contrib/anthropic-max-bridge/README.md`:
- Around line 88-90: Update the "Auto-refresh (default)" / "Manual refresh
(fallback)" wording to reflect the three-step refresh flow used by the
anthroptic-token-bridge: state that the plugin attempts automatic refresh from
the macOS Keychain every 5 messages, then falls back to invoking the local
`claude setup-token` flow, and finally to the interactive `setup-token` exchange
if the previous methods fail; mention all three strategies and that no user
action is needed unless all fallbacks fail, and reference the
`anthropic-token-bridge`, `claude setup-token`, and `setup-token` terms so
readers can find the commands.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d5173885-03f4-4b39-890b-25077b5b8fb9

📥 Commits

Reviewing files that changed from the base of the PR and between 11e5e88 and 050eed7.

📒 Files selected for processing (2)
  • contrib/anthropic-max-bridge/README.md
  • contrib/anthropic-max-bridge/TECHNICAL.md

- token-utils.ts: os.homedir() instead of process.env.HOME (avoids unexpanded ~)
- refresh-manager.ts: remove dead promisify(spawn), add execCommand timeout (15s),
  add pragma: allowlist secret on Keychain extractions, mask token in logs
- anthropic-token-bridge.ts/.js: read role from output.message instead of input
- Regenerate compiled .js from fixed TS sources (both plugin + contrib copy)
The macOS Keychain stores credentials as { claudeAiOauth: { accessToken, refreshToken } }
but the plugin was looking for flat structure. This caused auto-refresh to fail with
hasAccess: false, hasRefresh: false errors, falling back to browser OAuth flow.

Now correctly extracts from nested structure with fallback for legacy formats.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
.opencode/plugins/lib/token-utils.ts (1)

29-47: Synchronous I/O is acceptable here, but note the lack of file locking.

The readFileSync/writeFileSync calls are fine for small JSON files. However, as noted in refresh-manager.ts, there's no inter-process locking. If multiple processes (e.g., concurrent OpenCode instances) attempt to refresh tokens simultaneously, auth.json could be corrupted or updates could be lost.

For a plugin of this scope, this is an acceptable trade-off given the 5-minute cooldown between refresh attempts, but worth documenting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/lib/token-utils.ts around lines 29 - 47, Document that
readAuthFile and writeAuthFile use synchronous I/O without file locking and can
suffer inter-process races: add a clear comment (or a short JSDoc) above the
functions readAuthFile and writeAuthFile explaining that
readFileSync/writeFileSync are chosen intentionally, that concurrent processes
can corrupt auth.json or lose updates, and reference the 5-minute refresh
cooldown as the rationale and trade-off; optionally include a TODO suggesting
future use of advisory locks or an atomic write strategy if stronger guarantees
are required.
contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js (1)

1-33: Bundled JS file risks becoming stale.

This file inlines all modules (file-logger, token-utils, refresh-manager) into a single distributable JS file. Per install.sh (lines 132-137), it's installed via cp rather than symlink.

If the source TypeScript files are updated, this bundled JS will not automatically reflect those changes. Consider:

  1. Adding a build step to regenerate this file from TS sources
  2. Documenting the regeneration process
  3. Adding a version/hash comment to detect drift
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js` around lines
1 - 33, The bundled JS (which defines LOG_PATH, fileLog, fileLogError, info,
warn, error) can drift from the TypeScript sources because it's copied in
install.sh; update the repo to ensure the bundle regenerates automatically and
is detectable: add a build step that compiles the TypeScript sources
(token-utils, refresh-manager, file-logger) into this JS during CI and/or npm
build, add a documented regeneration command in CONTRIBUTING or INSTALL
(referencing the bundling step that produces the file containing
fileLog/fileLogError), and insert a version/hash comment header into the
generated file so mismatches between source and bundle are easy to detect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.opencode/plugins/lib/refresh-manager.ts:
- Around line 76-87: The code parses claudeAiOauth.expiresAt but then ignores it
and always passes a hardcoded 28800s to updateAnthropicTokens; update
refreshAnthropicToken to compute the real TTL from claudeAiOauth.expiresAt
(convert to seconds relative to now), clamp to a sensible minimum (e.g., 0) and
use that value when calling updateAnthropicTokens (fall back to 28800 only if
expiresAt is missing or invalid) so you don't store an incorrect expiry;
reference claudeAiOauth.expiresAt, refreshAnthropicToken, and
updateAnthropicTokens when making the change.

---

Nitpick comments:
In @.opencode/plugins/lib/token-utils.ts:
- Around line 29-47: Document that readAuthFile and writeAuthFile use
synchronous I/O without file locking and can suffer inter-process races: add a
clear comment (or a short JSDoc) above the functions readAuthFile and
writeAuthFile explaining that readFileSync/writeFileSync are chosen
intentionally, that concurrent processes can corrupt auth.json or lose updates,
and reference the 5-minute refresh cooldown as the rationale and trade-off;
optionally include a TODO suggesting future use of advisory locks or an atomic
write strategy if stronger guarantees are required.

In `@contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js`:
- Around line 1-33: The bundled JS (which defines LOG_PATH, fileLog,
fileLogError, info, warn, error) can drift from the TypeScript sources because
it's copied in install.sh; update the repo to ensure the bundle regenerates
automatically and is detectable: add a build step that compiles the TypeScript
sources (token-utils, refresh-manager, file-logger) into this JS during CI
and/or npm build, add a documented regeneration command in CONTRIBUTING or
INSTALL (referencing the bundling step that produces the file containing
fileLog/fileLogError), and insert a version/hash comment header into the
generated file so mismatches between source and bundle are easy to detect.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7802f728-80d8-4d9d-a670-b3b12819aebd

📥 Commits

Reviewing files that changed from the base of the PR and between 050eed7 and 5b4e1ce.

📒 Files selected for processing (5)
  • .opencode/plugins/anthropic-token-bridge.js
  • .opencode/plugins/anthropic-token-bridge.ts
  • .opencode/plugins/lib/refresh-manager.ts
  • .opencode/plugins/lib/token-utils.ts
  • contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js
✅ Files skipped from review due to trivial changes (1)
  • .opencode/plugins/anthropic-token-bridge.js

Comment on lines +76 to +87
const credentials = JSON.parse(stdout.trim()) as {
claudeAiOauth?: {
accessToken?: string;
refreshToken?: string;
expiresAt?: number;
};
// Legacy fallback for direct storage (rare)
accessToken?: string;
refreshToken?: string;
access_token?: string;
refresh_token?: string;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider using the Keychain token's actual expiration.

The claudeAiOauth structure includes an expiresAt field (line 80), but it's not used. Instead, a hardcoded 8-hour expiry (28800 seconds) is passed to updateAnthropicTokens at line 211.

If the Keychain token has a different remaining lifetime, this could cause premature "expires soon" warnings or, worse, storing an already-expired token as if it were valid for 8 hours.

🔧 Suggested improvement
 		// Extract from nested claudeAiOauth structure (standard Claude Code format)
 		const oauth = credentials.claudeAiOauth;
+		const expiresAt = oauth?.expiresAt;
 		// 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) {
 			// ...
 		}
 
-		return { accessToken, refreshToken };
+		return { accessToken, refreshToken, expiresAt };

Then in refreshAnthropicToken:

 		if (keychainTokens) {
 			info("Found tokens in Keychain, updating auth.json");
+			// Calculate remaining seconds, default to 8 hours if no expiry
+			const expiresInSeconds = keychainTokens.expiresAt
+				? Math.max(0, Math.floor((keychainTokens.expiresAt - Date.now()) / 1000))
+				: 28800;
 			const success = updateAnthropicTokens(
 				keychainTokens.accessToken,
 				keychainTokens.refreshToken,
-				28800,
+				expiresInSeconds,
 			);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.opencode/plugins/lib/refresh-manager.ts around lines 76 - 87, The code
parses claudeAiOauth.expiresAt but then ignores it and always passes a hardcoded
28800s to updateAnthropicTokens; update refreshAnthropicToken to compute the
real TTL from claudeAiOauth.expiresAt (convert to seconds relative to now),
clamp to a sensible minimum (e.g., 0) and use that value when calling
updateAnthropicTokens (fall back to 28800 only if expiresAt is missing or
invalid) so you don't store an incorrect expiry; reference
claudeAiOauth.expiresAt, refreshAnthropicToken, and updateAnthropicTokens when
making the change.

@Steffen025 Steffen025 merged commit 43adf8d into dev Mar 21, 2026
4 checks passed
@Steffen025 Steffen025 deleted the feat/oauth-auto-refresh branch March 21, 2026 16:49
Steffen025 added a commit that referenced this pull request Mar 21, 2026
* feat(oauth): add runtime auto-refresh token bridge

- Add anthropic-token-bridge.js/.ts: checks token every 5 messages,
  auto-refreshes from macOS Keychain when <2h remaining
- Add lib/token-utils.ts + lib/refresh-manager.ts: token lifecycle logic
  (3 strategies: Keychain → claude setup-token → exchange API)
- Add info()/warn()/error() wrappers to file-logger.ts (needed by token bridge)
- Add anthropic-token-bridge.js to contrib/anthropic-max-bridge/plugins/
- Update contrib/install.sh: copies both plugins, updates token refresh note

* docs(contrib): update README + TECHNICAL for auto-refresh token bridge

- README: Token Expiry → auto-refresh primary, manual fallback secondary
- README: File Reference → add anthropic-token-bridge.js entry
- README: install.sh step → mention both plugins copied
- README: token flow diagram → show both plugins
- README: HTTP 401 troubleshooting → mention auto-refresh failed
- TECHNICAL: Token lifetime → replace manual-only with auto-refresh details

* fix(oauth): address CI findings in token bridge

- token-utils.ts: os.homedir() instead of process.env.HOME (avoids unexpanded ~)
- refresh-manager.ts: remove dead promisify(spawn), add execCommand timeout (15s),
  add pragma: allowlist secret on Keychain extractions, mask token in logs
- anthropic-token-bridge.ts/.js: read role from output.message instead of input
- Regenerate compiled .js from fixed TS sources (both plugin + contrib copy)

* fix(oauth): parse claudeAiOauth nested structure from Keychain

The macOS Keychain stores credentials as { claudeAiOauth: { accessToken, refreshToken } }
but the plugin was looking for flat structure. This caused auto-refresh to fail with
hasAccess: false, hasRefresh: false errors, falling back to browser OAuth flow.

Now correctly extracts from nested structure with fallback for legacy formats.
Steffen025 added a commit that referenced this pull request Mar 21, 2026
* feat(oauth): add runtime auto-refresh token bridge

- Add anthropic-token-bridge.js/.ts: checks token every 5 messages,
  auto-refreshes from macOS Keychain when <2h remaining
- Add lib/token-utils.ts + lib/refresh-manager.ts: token lifecycle logic
  (3 strategies: Keychain → claude setup-token → exchange API)
- Add info()/warn()/error() wrappers to file-logger.ts (needed by token bridge)
- Add anthropic-token-bridge.js to contrib/anthropic-max-bridge/plugins/
- Update contrib/install.sh: copies both plugins, updates token refresh note

* docs(contrib): update README + TECHNICAL for auto-refresh token bridge

- README: Token Expiry → auto-refresh primary, manual fallback secondary
- README: File Reference → add anthropic-token-bridge.js entry
- README: install.sh step → mention both plugins copied
- README: token flow diagram → show both plugins
- README: HTTP 401 troubleshooting → mention auto-refresh failed
- TECHNICAL: Token lifetime → replace manual-only with auto-refresh details

* fix(oauth): address CI findings in token bridge

- token-utils.ts: os.homedir() instead of process.env.HOME (avoids unexpanded ~)
- refresh-manager.ts: remove dead promisify(spawn), add execCommand timeout (15s),
  add pragma: allowlist secret on Keychain extractions, mask token in logs
- anthropic-token-bridge.ts/.js: read role from output.message instead of input
- Regenerate compiled .js from fixed TS sources (both plugin + contrib copy)

* fix(oauth): parse claudeAiOauth nested structure from Keychain

The macOS Keychain stores credentials as { claudeAiOauth: { accessToken, refreshToken } }
but the plugin was looking for flat structure. This caused auto-refresh to fail with
hasAccess: false, hasRefresh: false errors, falling back to browser OAuth flow.

Now correctly extracts from nested structure with fallback for legacy formats.
Steffen025 added a commit that referenced this pull request Mar 22, 2026
#83#85) (#87)

* refactor(v3.0 Part B/2): add correct structure — Utilities/, VoiceServer/, PAI root docs, voice ID fix (#83)

* refactor: move orphan skills into Utilities/ category

Moved 6 flat-level skills into Utilities/ for PAI 4.0.3 parity:
- AudioEditor → Utilities/AudioEditor (was missing from Utilities)
- CodeReview → Utilities/CodeReview (OpenCode-specific)
- OpenCodeSystem → Utilities/OpenCodeSystem (OpenCode-specific)
- Sales → Utilities/Sales (OpenCode-specific)
- System → Utilities/System (OpenCode-specific)
- WriteStory → Utilities/WriteStory (OpenCode-specific)

OpenCode-specific skills preserved in Utilities/ category.

* feat: add missing PAI/ root docs from 4.0.3

Added 3 files missing from PAI/ root:
- doc-dependencies.json (documentation dependency graph)
- PIPELINES.md (pipeline system documentation)
- THEHOOKSYSTEM.md (hooks/plugin system documentation)

All .claude/ references replaced with .opencode/.

* refactor: rename voice-server/ to VoiceServer/ + add missing files

Renamed: voice-server/ → VoiceServer/ (CamelCase, matches PAI 4.0.3)

Added missing files from PAI 4.0.3:
- install.sh, start.sh, stop.sh, restart.sh, status.sh, uninstall.sh
- voices.json, pronunciations.json
- menubar/ directory

All .claude/ references replaced with .opencode/ in copied files.

* fix: restore missing files + remove legacy content from skills/PAI/ deletion

Restored from git history (accidentally deleted with skills/PAI/):
- PAI/THEPLUGINSYSTEM.md (OpenCode replacement for THEHOOKSYSTEM.md)
- PAI/Tools/GenerateSkillIndex.ts + ValidateSkillStructure.ts + SkillSearch.ts
- PAI/PIPELINES.md, PAI/doc-dependencies.json
- PAISECURITYSYSTEM/HOOKS.md

Removed legacy files NOT in 4.0.3 upstream:
- PAI/BACKUPS.md, BROWSERAUTOMATION.md, CONSTITUTION.md, RESPONSEFORMAT.md,
  SCRAPINGREFERENCE.md, TERMINALTABS.md (all pre-v3.0 artifacts)
- PAI/UPDATES/ directory (not in 4.0.3)
- PAI/Workflows/ (11 legacy workflows not in 4.0.3 root)
- PAI/USER/ reset to 4.0.3 template (README.md placeholders only,
  removed 47 personal template files that don't belong in public repo)

Fixed references: THEHOOKSYSTEM → THEPLUGINSYSTEM in SKILL.md and DOCUMENTATIONINDEX.md

Regenerated skill-index.json (52 skills, 7 categories)

PAI/ root now matches 4.0.3 exactly, with only 2 justified deviations:
- THEPLUGINSYSTEM.md (replaces THEHOOKSYSTEM.md per ADR-001)
- MINIMAL_BOOTSTRAP.md (OpenCode lazy loading, created in v3.0 WP-C)

* chore: remove INSTALLER-REFACTOR-PLAN.md (internal planning doc)

* fix: replace all hardcoded ElevenLabs voice IDs with dynamic routing

VoiceServer sendNotification() now uses title (agent name) instead of
voiceId for voice resolution. This enables dynamic provider switching
via TTS_PROVIDER env var (google/elevenlabs/macos).

Changed: voice_id:'fTtv3eikoepIosk8dTZ5' → voice_id:'default' + title:'AgentName'

Files affected:
- VoiceServer/server.ts (line 425: voiceId → safeTitle)
- PAI/SKILL.md (7 phase curls)
- PAI/Algorithm/v3.7.0.md (2 voice curls)
- agents/Algorithm.md (frontmatter + 3 references)
- PAI/Tools/algorithm.ts (VOICE_ID constant)

Zero hardcoded ElevenLabs IDs remain in the codebase.

* fix: address CodeRabbit review findings on PR #83

Fixes verified against actual code:

Tools path resolution:
- SkillSearch.ts: INDEX_FILE pointed to PAI/Skills/ instead of skills/
- ValidateSkillStructure.ts: SKILLS_DIR went 3 levels up instead of 2
- Both now match GenerateSkillIndex.ts (.opencode/skills/)

Symlink cycle prevention:
- GenerateSkillIndex.ts: findSkillFiles now tracks visited canonical paths
- ValidateSkillStructure.ts: scanDirectory now populates visitedPaths Set

Error handling:
- GenerateSkillIndex.ts + SkillSearch.ts: main().catch now exits non-zero

Documentation accuracy:
- doc-dependencies.json: add missing THEPLUGINSYSTEM.md entry
- THEHOOKSYSTEM.md: PAI_DIR example $HOME/.claude → $HOME/.opencode
- THEPLUGINSYSTEM.md: library count 8 → 9 (matches table)
- pronunciations.json: stale skills/PAI/USER/ → PAI/USER/ path

Stats accuracy:
- ValidateSkillStructure.ts: category SKILL.md files no longer counted as flat skills
- pai-voice.5s.sh: PAI_DIR default $HOME/.claude → $HOME/.opencode

Skipped (verified not applicable):
- PIPELINES.md paths: upstream semantics, both paths correct in context
- VoiceServer shell script hardening: upstream 4.0.3 files, out of scope
- PAISECURITYSYSTEM/HOOKS.md: doc is factually accurate for hook architecture
- THEHOOKSYSTEM.md voiceId examples: settings.json config docs, not hardcoded IDs

* fix: delete THEHOOKSYSTEM.md and HOOKS.md, complete hook→plugin migration (ADR-001)

- Delete .opencode/PAI/THEHOOKSYSTEM.md (superseded by THEPLUGINSYSTEM.md)
- Delete .opencode/PAISECURITYSYSTEM/HOOKS.md (superseded by PLUGINS.md)
- Update doc-dependencies.json: remove THEHOOKSYSTEM.md entry, rename
  hook-system section to plugin-system referencing THEPLUGINSYSTEM.md
- Update PAISECURITYSYSTEM/ARCHITECTURE.md: all hook→plugin terminology,
  execution flow updated for plugin model (throw Error, not exit codes)
- Update PAISECURITYSYSTEM/README.md: hook→plugin in prose and file tree
- Fix GenerateSkillIndex.ts division by zero when totalSkills == 0

* feat(installer): add anthropic-max preset for Max/Pro OAuth subscription (#84)

* feat(installer): add anthropic-max preset for Max/Pro OAuth subscription

Adds 'anthropic-max' as a built-in installer preset so users with an
existing Anthropic Max or Pro subscription can use PAI-OpenCode without
paying extra for API keys.

## What's new

### Provider preset
- provider-models.ts: add 'anthropic-max' to ProviderName union, PROVIDER_MODELS,
  and PROVIDER_LABELS (label: 'Anthropic Max/Pro (OAuth)')
- Model tiers: haiku-4-5 / sonnet-4-6 / opus-4-6 — same models as 'anthropic'
  but routed through OAuth instead of an API key

### Installer engine (steps-fresh.ts)
- installAnthropicMaxBridge(): new helper function that:
    1. Copies .opencode/plugins/anthropic-max-bridge.js to the local plugins dir
    2. Extracts the OAuth token from macOS Keychain (service: Claude Code-credentials)
    3. Parses claudeAiOauth.{accessToken,refreshToken,expiresAt}
    4. Writes token to ~/.local/share/opencode/auth.json under the 'anthropic' key
    5. Returns success + hours remaining, non-throwing (install continues on failure)
- stepInstallPAI(): call installAnthropicMaxBridge() when provider === 'anthropic-max'
- runFreshInstall(): skip API key prompt for anthropic-max, show Claude CLI check message

### CLI (quick-install.ts)
- --preset anthropic-max works without --api-key (all other presets still require it)
- Inline instructions printed when anthropic-max selected
- Updated help text and examples

### New files
- .opencode/plugins/anthropic-max-bridge.js: 80-line minimal plugin (3 API fixes only)
    Fix 1: system prompt array-of-objects format (prevents HTTP 400)
    Fix 2: anthropic-beta: oauth-2025-04-20 header (prevents HTTP 401)
    Fix 3: Authorization: Bearer <token> instead of x-api-key
- PAI-Install/anthropic-max-refresh.sh: one-command token refresh after expiry
- docs/providers/anthropic-max.md: user-facing setup guide, troubleshooting, tech details

## Usage

Interactive:
  bash install.sh  →  choose 'Anthropic Max/Pro (OAuth)'

Headless:
  bun PAI-Install/cli/quick-install.ts --preset anthropic-max --name 'User'

Token refresh (every ~8-12 hours):
  bash PAI-Install/anthropic-max-refresh.sh

## Notes
- macOS only (requires Keychain access)
- Requires Claude Code CLI installed and authenticated
- Using OAuth tokens in third-party tools may violate Anthropic ToS
- Non-fatal: if Keychain extraction fails, install continues with a warning

* fix(anthropic-max): address CodeRabbit findings + add standalone contrib package

CodeRabbit fixes (PR #84):
- anthropic-max-refresh.sh: use quoted heredoc ('PYEOF') and export tokens
  as env vars to prevent shell injection via token values
- anthropic-max-refresh.sh: add validation for REFRESH_TOKEN (non-empty) and
  EXPIRES_AT (numeric, >0) before writing auth.json
- steps-fresh.ts: fix progress regression — onProgress(92) called after 95%
  steps; corrected to 96
- steps-fresh.ts: replace brittle import.meta.url.replace('file://', '') with
  fileURLToPath(new URL(import.meta.url)) from node:url
- anthropic-max-bridge.js: add explicit comment explaining the authorize block
  returns { type: 'failed' } intentionally (tokens come from auth.json, not
  from an OAuth redirect flow)
- docs/providers/anthropic-max.md: add YAML frontmatter (title, tags,
  published, type, summary) and convert tip block to Obsidian callout syntax

Goal 2 — standalone extractable package:
- contrib/anthropic-max-bridge/ — self-contained, no Bun/PAI required
  - install.sh (bash only, injection-safe heredoc, validates all tokens)
  - refresh-token.sh (same safety fixes as PAI-Install version)
  - plugins/anthropic-max-bridge.js (identical to .opencode/plugins/)
  - README.md (quick-start focused, team-sharing notes)
  - TECHNICAL.md (curl proof, token structure, what we ruled out)
- docs/providers/anthropic-max.md: Option B now points to contrib/ in this
  repo instead of the external jeremy-opencode repository

* fix(anthropic-max): address second-round CodeRabbit findings

- anthropic-max-bridge.js: guard experimental.chat.system.transform against
  output.system being undefined/null/non-array before calling .map(); normalise
  to [] if missing, wrap bare string/object into array, then apply existing
  string->{type,text} conversion
- contrib/anthropic-max-bridge/install.sh: chmod auth.json to 0o600
  (owner read/write only) immediately after writing via os.chmod + stat module;
  mirrors the chmodSync(authFile, 0o600) already present in steps-fresh.ts
- steps-fresh.ts: add zero-case message when hoursRemaining === 0 ('Token
  installed — expired or valid for less than 1 hour. Run
  anthropic-max-refresh.sh now.'); Math.max(0, ...) clamp was already present

* fix(anthropic-max): check Bun.spawn exit code for Keychain lookup

Previously proc.exited was awaited but its return value discarded, so a
non-zero exit from the 'security' command (e.g. item not found) was only
caught via an empty-stdout check. Stdout and stderr are now read concurrently
with proc.exited via Promise.all to avoid pipe deadlock; the exit code is
inspected before accepting keychainJson, and stderr is surfaced in the error
message when present.

* nitpick(steps-fresh): add warning when refresh token is missing

When extracting OAuth tokens from Keychain, warn users if the refresh
token is undefined. This alerts them that they'll need to re-authenticate
with 'claude' when the access token expires.

* feat(oauth): add runtime auto-refresh token bridge (#85)

* feat(oauth): add runtime auto-refresh token bridge

- Add anthropic-token-bridge.js/.ts: checks token every 5 messages,
  auto-refreshes from macOS Keychain when <2h remaining
- Add lib/token-utils.ts + lib/refresh-manager.ts: token lifecycle logic
  (3 strategies: Keychain → claude setup-token → exchange API)
- Add info()/warn()/error() wrappers to file-logger.ts (needed by token bridge)
- Add anthropic-token-bridge.js to contrib/anthropic-max-bridge/plugins/
- Update contrib/install.sh: copies both plugins, updates token refresh note

* docs(contrib): update README + TECHNICAL for auto-refresh token bridge

- README: Token Expiry → auto-refresh primary, manual fallback secondary
- README: File Reference → add anthropic-token-bridge.js entry
- README: install.sh step → mention both plugins copied
- README: token flow diagram → show both plugins
- README: HTTP 401 troubleshooting → mention auto-refresh failed
- TECHNICAL: Token lifetime → replace manual-only with auto-refresh details

* fix(oauth): address CI findings in token bridge

- token-utils.ts: os.homedir() instead of process.env.HOME (avoids unexpanded ~)
- refresh-manager.ts: remove dead promisify(spawn), add execCommand timeout (15s),
  add pragma: allowlist secret on Keychain extractions, mask token in logs
- anthropic-token-bridge.ts/.js: read role from output.message instead of input
- Regenerate compiled .js from fixed TS sources (both plugin + contrib copy)

* fix(oauth): parse claudeAiOauth nested structure from Keychain

The macOS Keychain stores credentials as { claudeAiOauth: { accessToken, refreshToken } }
but the plugin was looking for flat structure. This caused auto-refresh to fail with
hasAccess: false, hasRefresh: false errors, falling back to browser OAuth flow.

Now correctly extracts from nested structure with fallback for legacy formats.

* fix: address CodeRabbit review findings on PR #87

Security:
- auth.json now written with mode 0600 (token-utils, compiled JS, contrib)
- Validate access token and expires field before reporting healthy status
- Refresh scripts check for already-expired Keychain tokens before writing
- Refresh scripts restore chmod 600 after rewriting auth.json
- quick-install.ts fails fast on non-macOS for anthropic-max preset

Correctness:
- Use actual Keychain expiresAt instead of hardcoded 8h TTL
- Token bridge attempts refresh on no_anthropic_config (not just expiry)
- Installer copies anthropic-token-bridge.js alongside the bridge plugin
- contrib anthropic-max-bridge.js gets defensive output.system null-safety
- Remove non-existent FEEDSYSTEM.md from doc-dependencies.json
- GenerateSkillIndex frontmatter regex accepts CRLF line endings
- SkillSearch normalizes PascalCase names for natural language queries

Shell scripts:
- VoiceServer: sed replaces cut -d= -f2 to handle = in values
- VoiceServer: remove unused RED/PLIST_PATH variables
- restart.sh: check exit codes from stop.sh/start.sh

Docs:
- Add language tags to fenced code blocks in contrib README/TECHNICAL

* fix: CodeRabbit round 2 - session-start validation, platform guards, token expiry logic

Session-start improvements:
- GenerateSkillIndex.ts: case-insensitive ALWAYS_LOADED_SKILLS matching
- anthropic-token-bridge: enhanced session-start token validation
- refresh-manager: improved expiresAt handling with null/undefined/past checks

Platform guards:
- VoiceServer/install.sh: curl --fail for notify endpoint
- VoiceServer/uninstall.sh: Darwin platform guard + safe process kill
- steps-fresh.ts: macOS platform gate for anthropic-max + expiresAt validation

Token logic fixes:
- steps-fresh.ts: skip provider API key for anthropic-max in .env
- refresh-manager: AbortController 10s timeout for fetch
- contrib plugin: mirror all fixes

* fix(token-bridge): CRITICAL - synchronous token refresh at session start

The 5-second setTimeout delay caused API calls to fail with expired tokens
before refresh completed. Now refreshes immediately and synchronously
when session starts with an expired/soon-expiring token.

- Changed from setTimeout(5000) to immediate await
- Added fallback async retry on failure
- Applied to: .ts source + compiled .js + contrib plugin

* fix(token-bridge): always sync Keychain→auth.json at session start

Root cause fix for stale token issue: the session-start hook previously
only checked auth.json's `expires` timestamp, which can be valid even
when Anthropic has invalidated the token (e.g. after re-authentication).

Now the hook always reads the Keychain first (source of truth) and writes
it to auth.json before any expiry logic runs. If Keychain and auth.json
tokens differ, a warning is logged and the fresh token is synced.

Fallback path (Keychain unavailable or expired) still triggers full
refreshAnthropicToken() as before.

Also exports extractFromKeychain() from refresh-manager.ts so it can
be used directly in the bridge hook.

* fix: address PR #87 review comments

contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js
- Session-start hook now syncs Keychain→auth.json first (same pattern as
  main plugin); extractFromKeychain() result is awaited and wrapped in its
  own try/catch with fileLog/fileLogError; existing refreshAnthropicToken()
  / isRefreshInProgress() fallback path is unchanged

.opencode/PAI/Tools/GenerateSkillIndex.ts
- Duplicate skill name detection now throws an Error instead of warn+continue,
  making the build fail fast with a clear message

.opencode/VoiceServer/install.sh
- API_KEY extraction strips surrounding quotes, inline comments, and
  whitespace before comparing against sentinel values
- ProgramArguments now point to a runtime wrapper (run-server.sh) that
  resolves Bun via PATH at launch time instead of baking in /Users/steffen/.bun/bin/bun
- ELEVENLABS_API_KEY plist entry is only emitted when API_KEY is non-empty
- Fixed sleep 2 with a polling health-check loop (60s timeout, 1s interval)
  that exits with an error if the server never responds

.opencode/VoiceServer/uninstall.sh
- Port cleanup now uses lsof -ti for PID-only output (no header line risk)
  and verifies each PID's command via ps before killing
- Script directory echo now resolves symlinks via readlink -f / realpath

* fix: address second round of PR #87 review comments

.opencode/VoiceServer/install.sh
- ENV_FILE now matches the server's path resolution:
  ${OPENCODE_DIR:-${PAI_DIR:-$HOME/.opencode}}/.env (was $HOME/.env)
- API_KEY extraction uses grep -m1 to avoid concatenating multiple matching
  lines; inline-comment stripping now only removes trailing '#[^"']*$'
  (unquoted comments) so '#' inside a key value is preserved

.opencode/plugins/lib/refresh-manager.ts
.opencode/plugins/anthropic-token-bridge.js
contrib/anthropic-max-bridge/plugins/anthropic-token-bridge.js
- extractFromKeychain: destructure { stdout, stderr, exitCode } from
  execCommand and pass the real stderr into the error() call (was
  incorrectly passing stdout as stderr)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant