feat(installer): add anthropic-max preset for Max/Pro OAuth subscription#84
feat(installer): add anthropic-max preset for Max/Pro OAuth subscription#84Steffen025 merged 5 commits intodevfrom
Conversation
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
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds an "anthropic-max" installer preset, bridge plugin, installer/refresh scripts, and docs to enable Anthropic Max/Pro OAuth usage without an API key: installer copies the plugin, merges Keychain-sourced OAuth tokens into auth.json, and the plugin normalizes system prompts and adapts request headers/auth for OAuth. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User/CLI
participant Installer as PAI Installer
participant Keychain as macOS Keychain
participant AuthFile as auth.json
participant OpenCode as OpenCode Runtime
participant Plugin as Anthropic Bridge
participant API as Anthropic API
User->>Installer: run installer with --preset anthropic-max
Installer->>User: request confirmation
User-->>Installer: confirm
Installer->>Keychain: security find-generic-password (Claude Code-credentials)
Keychain-->>Installer: JSON with claudeAiOauth tokens
Installer->>Installer: validate token shape & expiry
Installer->>AuthFile: write/merge anthropic oauth entry (access, refresh, expires)
Installer->>Installer: copy bridge plugin into plugins dir
User->>OpenCode: start
OpenCode->>Plugin: load anthropic-max bridge
OpenCode->>AuthFile: read oauth token
User->>OpenCode: send chat request
OpenCode->>Plugin: pre-send transform
Plugin->>Plugin: convert system->[{type:"text",text:...}]
Plugin->>Plugin: ensure anthropic-beta: oauth-2025-04-20
Plugin->>Plugin: set Authorization: Bearer <access>, remove x-api-key
Plugin->>API: send transformed request
API-->>Plugin: respond
Plugin-->>OpenCode: deliver response
OpenCode-->>User: display result
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
PAI-Install/engine/steps-fresh.ts (1)
476-481:⚠️ Potential issue | 🟡 MinorProgress percentage regression: 92% after 95%.
Line 480 reports progress at 92%, but
stepInstallPAIalready reported 95% at line 408 ("Created .env with secure permissions..."). Progress should monotonically increase.🔧 Suggested fix
writeFileSync( join(localOpencodeDir, "opencode.json"), JSON.stringify(opencode, null, 2), ); - onProgress(92, "Generated opencode.json..."); + onProgress(96, "Generated opencode.json...");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@PAI-Install/engine/steps-fresh.ts` around lines 476 - 481, The progress percentage regresses from 95% to 92% here; update the onProgress call in the block that writes opencode.json (the onProgress(92, "...") call) to a value >=95 (e.g., onProgress(96, "Generated opencode.json...")) so progress remains monotonic, and if there is a shared progress-tracking variable in stepInstallPAI or surrounding code ensure this value is computed from or compared to the last reported progress before calling onProgress.
🧹 Nitpick comments (4)
.opencode/plugins/anthropic-max-bridge.js (1)
105-121: Consider documenting the intentional "failed" authorize behavior.The
authorizefunction always returns{ type: "failed" }which is intentional since tokens are injected viaauth.json. A brief inline comment explaining this prevents confusion for future maintainers looking at the code without reading the header.The current comment on lines 111-112 is helpful but could be more explicit about why "failed" is the correct return value.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.opencode/plugins/anthropic-max-bridge.js around lines 105 - 121, Add an explicit inline comment inside the methods array next to the authorize function for the "Claude Pro/Max (OAuth)" method to state that returning { type: "failed" } is intentional because tokens are injected via auth.json (or install script) and the OAuth authorize flow is not used in normal operation; reference the methods array, the authorize function, and the label "Claude Pro/Max (OAuth)" so future maintainers know this is deliberate and not an unfinished implementation.docs/providers/anthropic-max.md (2)
1-6: Consider adding frontmatter for this important document.As per coding guidelines, important documents should include frontmatter for Dataview queries. This helps with documentation organization and discoverability.
📝 Suggested frontmatter
+--- +title: Anthropic Max/Pro OAuth Preset +provider: anthropic-max +requires_api_key: false +platform: macOS +--- + # Anthropic Max/Pro — OAuth Preset🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/providers/anthropic-max.md` around lines 1 - 6, This Markdown doc lacks YAML frontmatter required for Dataview queries and discoverability; add a frontmatter block at the top of "Anthropic Max/Pro — OAuth Preset" (the top-level header) containing keys such as title, tags (e.g., providers, anthropic), published: true, type: docs (or guide), and any metadata like summary or slug so Dataview can index it; ensure the frontmatter is the very first content in the file and follows YAML syntax.
83-86: Use Obsidian callout syntax for the tip.As per coding guidelines, callouts should use
> [!type]syntax for Obsidian compatibility.📝 Suggested fix
-> **Tip:** Claude Code CLI silently refreshes its own token whenever you use it. -> So `bash PAI-Install/anthropic-max-refresh.sh` right after any `claude` session -> will always find a fresh token. +> [!tip] +> Claude Code CLI silently refreshes its own token whenever you use it. +> So `bash PAI-Install/anthropic-max-refresh.sh` right after any `claude` session +> will always find a fresh token.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@docs/providers/anthropic-max.md` around lines 83 - 86, Replace the Markdown tip paragraph that begins with "**Tip:** Claude Code CLI silently refreshes its own token..." with an Obsidian callout using the > [!tip] syntax (i.e., transform the existing "**Tip:** ..." block into a callout block starting with "> [!tip]" and keep the same explanatory line about running `bash PAI-Install/anthropic-max-refresh.sh` after any `claude` session so the token is fresh). Ensure the callout preserves the newline and the sentence about the refresh script exactly as written.PAI-Install/engine/steps-fresh.ts (1)
218-241: Consider making the plugin source path more robust.The path calculation on line 227 uses string manipulation on
import.meta.url. This could break if:
- The file is bundled/compiled to a different location
- The URL contains encoded characters
Consider adding a fallback or validation:
🛡️ Suggested improvement
- const repoRoot = join(dirname(import.meta.url.replace("file://", "")), "..", "..", ".."); + // Handle both file:// URLs and direct paths + const currentPath = import.meta.url.startsWith("file://") + ? decodeURIComponent(import.meta.url.slice(7)) + : import.meta.url; + const repoRoot = join(dirname(currentPath), "..", "..", "..");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@PAI-Install/engine/steps-fresh.ts` around lines 218 - 241, The repoRoot/pluginSrc path logic in installAnthropicMaxBridge currently slices import.meta.url as a string which is brittle; replace that with a robust conversion using fileURLToPath(new URL(import.meta.url)) (import fileURLToPath/URL from 'url') and then use path.dirname + path.resolve/path.join to compute repoRoot and pluginSrc, ensuring URL decoding and bundler-safe behavior; keep the existsSync check and mkdirSync/copyFileSync logic unchanged, and optionally add a clear fallback error message if pluginSrc still isn't found.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@PAI-Install/anthropic-max-refresh.sh`:
- Around line 39-47: The current heredoc interpolates shell variables into
Python, risking injection; instead export the token values into the environment
and change the embedded Python block (the heredoc starting with python3 -
<<PYEOF) to read os.environ['ACCESS_TOKEN'], os.environ['REFRESH_TOKEN'],
os.environ['EXPIRES_AT'] (convert EXPIRES_AT to int) inside Python; also use a
quoted heredoc delimiter (e.g. <<'PYEOF') to prevent accidental shell expansion
before Python reads the environment and then write the JSON to $AUTH_FILE as
before.
- Around line 30-35: The script extracts ACCESS_TOKEN, REFRESH_TOKEN and
EXPIRES_AT from KEYCHAIN_JSON but doesn't validate EXPIRES_AT (it defaults to 0)
or check that the Python one-liners returned valid values; add defensive
validation after those extractions: verify EXPIRES_AT is a positive integer
(numeric and >0) and that ACCESS_TOKEN and REFRESH_TOKEN are non-empty strings,
and if validation fails call die with a clear message (same pattern as the
ACCESS_TOKEN check). Reference EXPIRES_AT, ACCESS_TOKEN, REFRESH_TOKEN,
KEYCHAIN_JSON and die when adding these checks so subsequent arithmetic or token
logic cannot operate on missing/malformed values.
---
Outside diff comments:
In `@PAI-Install/engine/steps-fresh.ts`:
- Around line 476-481: The progress percentage regresses from 95% to 92% here;
update the onProgress call in the block that writes opencode.json (the
onProgress(92, "...") call) to a value >=95 (e.g., onProgress(96, "Generated
opencode.json...")) so progress remains monotonic, and if there is a shared
progress-tracking variable in stepInstallPAI or surrounding code ensure this
value is computed from or compared to the last reported progress before calling
onProgress.
---
Nitpick comments:
In @.opencode/plugins/anthropic-max-bridge.js:
- Around line 105-121: Add an explicit inline comment inside the methods array
next to the authorize function for the "Claude Pro/Max (OAuth)" method to state
that returning { type: "failed" } is intentional because tokens are injected via
auth.json (or install script) and the OAuth authorize flow is not used in normal
operation; reference the methods array, the authorize function, and the label
"Claude Pro/Max (OAuth)" so future maintainers know this is deliberate and not
an unfinished implementation.
In `@docs/providers/anthropic-max.md`:
- Around line 1-6: This Markdown doc lacks YAML frontmatter required for
Dataview queries and discoverability; add a frontmatter block at the top of
"Anthropic Max/Pro — OAuth Preset" (the top-level header) containing keys such
as title, tags (e.g., providers, anthropic), published: true, type: docs (or
guide), and any metadata like summary or slug so Dataview can index it; ensure
the frontmatter is the very first content in the file and follows YAML syntax.
- Around line 83-86: Replace the Markdown tip paragraph that begins with
"**Tip:** Claude Code CLI silently refreshes its own token..." with an Obsidian
callout using the > [!tip] syntax (i.e., transform the existing "**Tip:** ..."
block into a callout block starting with "> [!tip]" and keep the same
explanatory line about running `bash PAI-Install/anthropic-max-refresh.sh` after
any `claude` session so the token is fresh). Ensure the callout preserves the
newline and the sentence about the refresh script exactly as written.
In `@PAI-Install/engine/steps-fresh.ts`:
- Around line 218-241: The repoRoot/pluginSrc path logic in
installAnthropicMaxBridge currently slices import.meta.url as a string which is
brittle; replace that with a robust conversion using fileURLToPath(new
URL(import.meta.url)) (import fileURLToPath/URL from 'url') and then use
path.dirname + path.resolve/path.join to compute repoRoot and pluginSrc,
ensuring URL decoding and bundler-safe behavior; keep the existsSync check and
mkdirSync/copyFileSync logic unchanged, and optionally add a clear fallback
error message if pluginSrc still isn't found.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9d59eb5d-32c4-42e4-917d-97a51da761b0
📒 Files selected for processing (6)
.opencode/plugins/anthropic-max-bridge.jsPAI-Install/anthropic-max-refresh.shPAI-Install/cli/quick-install.tsPAI-Install/engine/provider-models.tsPAI-Install/engine/steps-fresh.tsdocs/providers/anthropic-max.md
…rib 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
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
.opencode/plugins/anthropic-max-bridge.js (1)
106-120: Inconsistent indentation in theauthorizefunction.The
authorizefunction body (lines 111-118) uses inconsistent indentation compared to the surrounding object structure. This appears to be a formatting issue.📝 Proposed fix
{ // Primary method — token comes from auth.json (see README) label: "Claude Pro/Max (OAuth)", type: "oauth", - authorize: async () => { - // INTENTIONAL: This OAuth authorize flow is deliberately disabled. - // Tokens are NOT obtained through a browser redirect here. - // Instead, they are extracted from the macOS Keychain (where - // Claude Code CLI stores them) and written to auth.json by the - // PAI-Install installer or the refresh script. - // The plugin only reads from auth.json — it never triggers its own - // OAuth flow. - return { type: "failed" }; - }, + authorize: async () => { + // INTENTIONAL: This OAuth authorize flow is deliberately disabled. + // Tokens are NOT obtained through a browser redirect here. + // Instead, they are extracted from the macOS Keychain (where + // Claude Code CLI stores them) and written to auth.json by the + // PAI-Install installer or the refresh script. + // The plugin only reads from auth.json — it never triggers its own + // OAuth flow. + return { type: "failed" }; + }, },🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In @.opencode/plugins/anthropic-max-bridge.js around lines 106 - 120, The authorize property inside the object for "Claude Pro/Max (OAuth)" has inconsistent indentation; reformat the authorize async function and its block so it aligns with the surrounding object keys (label, type) and braces—ensure the "authorize: async () => {" line, inner comments, return { type: "failed" }; and the closing "}," match the same indentation level as "label" and "type" to fix formatting for the authorize function.contrib/anthropic-max-bridge/README.md (1)
104-132: Add language specifiers to ASCII diagrams.The file reference tree (line 104) and token flow diagram (line 118) are missing language specifiers. Use
textorplaintextfor these blocks.📝 Proposed fix
-``` +```text contrib/anthropic-max-bridge/ ├── README.md ← You are here ...-
+text
Claude Code CLI
└─ authenticates with Anthropic
...🤖 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 104 - 132, Update the two ASCII diagram fenced code blocks in README.md (the file reference tree block and the "How tokens get from Claude Code into OpenCode" flow block) to include a language specifier such as "text" (i.e., change the opening fences from ``` to ```text) so syntax highlighters treat them as plain text; locate the diagrams around the blocks containing "contrib/anthropic-max-bridge/" and "Claude Code CLI" and modify their opening triple-backtick lines accordingly.contrib/anthropic-max-bridge/plugins/anthropic-max-bridge.js (1)
1-131: Duplicate plugin file — consider single source of truth.This file is byte-for-byte identical to
.opencode/plugins/anthropic-max-bridge.js. Having two copies creates maintenance burden and risk of divergence. Consider:
- Using a symlink in the contrib package pointing to the canonical copy, or
- A build/copy script that ensures synchronization
The same issues flagged in the main plugin file apply here (missing
Array.isArrayguard onoutput.system, inconsistent indentation inauthorize).🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@contrib/anthropic-max-bridge/plugins/anthropic-max-bridge.js` around lines 1 - 131, This file is a duplicate of .opencode/plugins/anthropic-max-bridge.js and also contains small bugs: ensure you deduplicate by keeping a single canonical source (either replace this copy with a symlink to the canonical AnthropicMaxBridgePlugin file or add a build/copy step that syncs the contrib copy from the canonical one), and while editing the canonical code fix the runtime issues in AnthropicMaxBridgePlugin — add an Array.isArray guard before mapping output.system in the experimental.chat.system.transform handler (check output.system exists and is an array) and normalize indentation/formatting of the authorize method inside the auth.methods entry so it aligns with the surrounding object structure.PAI-Install/engine/steps-fresh.ts (1)
262-269: Consider more specific error handling for Keychain access failure.The catch block silently catches all errors. Consider checking for common failure modes (e.g., Keychain locked, permission denied) to provide more actionable error messages.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@PAI-Install/engine/steps-fresh.ts` around lines 262 - 269, The catch block in steps-fresh.ts currently swallows all errors; change it to catch the error object (e.g., catch (err)) and inspect err.message/err.code to return more specific messages (e.g., "Keychain locked", "Permission denied", "Keychain item not found") while still returning { success: false, tokenHoursRemaining: 0 }; reference the existing return shape and update the returned message based on matches against known Keychain failure strings/codes and also log or include the original err details for diagnostics.contrib/anthropic-max-bridge/TECHNICAL.md (1)
27-39: Add language specifiers to fenced code blocks.The header examples on lines 27-29 and 33-39 are missing language specifiers. Use
httportextfor consistency and to satisfy linters.📝 Proposed fix
-``` +```http anthropic-beta: oauth-2025-04-20```diff -``` +```http # API key (wrong for OAuth) x-api-key: sk-ant-api03-... # OAuth token (required) Authorization: Bearer sk-ant-oat01-...</details> <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against the current code and only fix it if needed.
In
@contrib/anthropic-max-bridge/TECHNICAL.mdaround lines 27 - 39, The fenced
code blocks showing the header example "anthropic-beta: oauth-2025-04-20" and
the API key vs OAuth example are missing language specifiers; update those
triple-backtick blocks to include a language (use "http") so linters accept them
and syntax highlighting is correct—specifically add ```http above the block
containing "anthropic-beta: oauth-2025-04-20" and the block containing the lines
"# API key (wrong for OAuth)" / "Authorization: Bearer sk-ant-oat01-..." in
contrib/anthropic-max-bridge/TECHNICAL.md.</details> </blockquote></details> <details> <summary>contrib/anthropic-max-bridge/refresh-token.sh (1)</summary><blockquote> `108-109`: **Shell variable interpolation in Python string is safe but unconventional.** Line 109 interpolates `$EXPIRES_AT` and `$NOW_MS` directly into a Python expression. This works because both are validated as numeric, but using env vars (like the heredoc above) would be more consistent. <details> <summary>📝 Proposed fix for consistency</summary> ```diff +export PAI_EXPIRES_AT_CALC="$EXPIRES_AT" +export PAI_NOW_MS="$NOW_MS" + NOW_MS=$(python3 -c "import time; print(int(time.time() * 1000))") -HOURS_LEFT=$(python3 -c "print(round(($EXPIRES_AT - $NOW_MS) / 3600000, 1))") +HOURS_LEFT=$(python3 -c "import os; print(round((int(os.environ['PAI_EXPIRES_AT_CALC']) - int(os.environ['PAI_NOW_MS'])) / 3600000, 1))") +unset PAI_EXPIRES_AT_CALC PAI_NOW_MS ``` </details> <details> <summary>🤖 Prompt for AI Agents</summary> ``` Verify each finding against the current code and only fix it if needed. In `@contrib/anthropic-max-bridge/refresh-token.sh` around lines 108 - 109, The current HOURS_LEFT calculation injects shell vars into the Python one-liner (NOW_MS/HOURS_LEFT using $EXPIRES_AT and $NOW_MS); make it consistent by passing EXPIRES_AT (and NOW_MS if desired) into Python via the environment or as arguments and reading them inside Python (os.environ or sys.argv) instead of interpolating into the string. Update the HOURS_LEFT assignment so the python process obtains EXPIRES_AT and NOW_MS from environment/args, converts them to integers, computes the hours left with the same arithmetic/rounding, and prints the result; keep the variable names NOW_MS, HOURS_LEFT and EXPIRES_AT to locate where to change. ``` </details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.opencode/plugins/anthropic-max-bridge.js:
- Around line 45-51: The transform handler experimental.chat.system.transform
should guard against output.system being undefined or not an array before
calling .map(); update it to first check Array.isArray(output.system) and only
perform the mapping in that case, and otherwise normalize output.system (e.g.,
if undefined set to an empty array, or if it's a single string/object wrap it
into an array and then normalize) so you never call .map on a non-array while
keeping the existing string->{type:"text",text} conversion logic.In
@contrib/anthropic-max-bridge/install.sh:
- Around line 175-180: After writing auth.json in the Python block (the code
that opens auth_file, json.dump(existing, ...), and prints "auth.json updated"),
immediately set restrictive file permissions on that file (mode 0o600) so tokens
are not world-readable; locate the write logic around the variables auth_file
and existing and add a chmod operation on auth_file (equivalent to what
steps-fresh.ts does) right after the file is closed/written.In
@PAI-Install/engine/steps-fresh.ts:
- Around line 329-335: Clamp the computed hours to zero and avoid showing
negative values: compute hoursRemaining by clamping the rounded hour delta
(expiresAt - Date.now())/3_600_000 with Math.max(0, …) (or Math.ceil if you
prefer rounding up), then use that hoursRemaining for tokenHoursRemaining;
additionally, if hoursRemaining === 0, change the return message (currently
using message with ~${hoursRemaining} hours) to a clear expiration message like
"Token installed — expired or valid for less than 1 hour." Ensure you update the
variables referenced (hoursRemaining, expiresAt, tokenHoursRemaining, message)
accordingly.
Nitpick comments:
In @.opencode/plugins/anthropic-max-bridge.js:
- Around line 106-120: The authorize property inside the object for "Claude
Pro/Max (OAuth)" has inconsistent indentation; reformat the authorize async
function and its block so it aligns with the surrounding object keys (label,
type) and braces—ensure the "authorize: async () => {" line, inner comments,
return { type: "failed" }; and the closing "}," match the same indentation level
as "label" and "type" to fix formatting for the authorize function.In
@contrib/anthropic-max-bridge/plugins/anthropic-max-bridge.js:
- Around line 1-131: This file is a duplicate of
.opencode/plugins/anthropic-max-bridge.js and also contains small bugs: ensure
you deduplicate by keeping a single canonical source (either replace this copy
with a symlink to the canonical AnthropicMaxBridgePlugin file or add a
build/copy step that syncs the contrib copy from the canonical one), and while
editing the canonical code fix the runtime issues in AnthropicMaxBridgePlugin —
add an Array.isArray guard before mapping output.system in the
experimental.chat.system.transform handler (check output.system exists and is an
array) and normalize indentation/formatting of the authorize method inside the
auth.methods entry so it aligns with the surrounding object structure.In
@contrib/anthropic-max-bridge/README.md:
- Around line 104-132: Update the two ASCII diagram fenced code blocks in
README.md (the file reference tree block and the "How tokens get from Claude
Code into OpenCode" flow block) to include a language specifier such as "text"
(i.e., change the opening fences fromtotext) so syntax highlighters
treat them as plain text; locate the diagrams around the blocks containing
"contrib/anthropic-max-bridge/" and "Claude Code CLI" and modify their opening
triple-backtick lines accordingly.In
@contrib/anthropic-max-bridge/refresh-token.sh:
- Around line 108-109: The current HOURS_LEFT calculation injects shell vars
into the Python one-liner (NOW_MS/HOURS_LEFT using $EXPIRES_AT and $NOW_MS);
make it consistent by passing EXPIRES_AT (and NOW_MS if desired) into Python via
the environment or as arguments and reading them inside Python (os.environ or
sys.argv) instead of interpolating into the string. Update the HOURS_LEFT
assignment so the python process obtains EXPIRES_AT and NOW_MS from
environment/args, converts them to integers, computes the hours left with the
same arithmetic/rounding, and prints the result; keep the variable names NOW_MS,
HOURS_LEFT and EXPIRES_AT to locate where to change.In
@contrib/anthropic-max-bridge/TECHNICAL.md:
- Around line 27-39: The fenced code blocks showing the header example
"anthropic-beta: oauth-2025-04-20" and the API key vs OAuth example are missing
language specifiers; update those triple-backtick blocks to include a language
(use "http") so linters accept them and syntax highlighting is
correct—specifically add ```http above the block containing "anthropic-beta:
oauth-2025-04-20" and the block containing the lines "# API key (wrong for
OAuth)" / "Authorization: Bearer sk-ant-oat01-..." in
contrib/anthropic-max-bridge/TECHNICAL.md.In
@PAI-Install/engine/steps-fresh.ts:
- Around line 262-269: The catch block in steps-fresh.ts currently swallows all
errors; change it to catch the error object (e.g., catch (err)) and inspect
err.message/err.code to return more specific messages (e.g., "Keychain locked",
"Permission denied", "Keychain item not found") while still returning { success:
false, tokenHoursRemaining: 0 }; reference the existing return shape and update
the returned message based on matches against known Keychain failure
strings/codes and also log or include the original err details for diagnostics.</details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: Path: .coderabbit.yaml **Review profile**: CHILL **Plan**: Pro **Run ID**: `7a47bb88-f9ee-43ea-849b-a0ad40cf0408` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 48d3fbffe8b6e768831b42fb85821c69de015fd5 and f674785c759e7c9966e2f2b409e1476c2ce9a03e. </details> <details> <summary>📒 Files selected for processing (9)</summary> * `.opencode/plugins/anthropic-max-bridge.js` * `PAI-Install/anthropic-max-refresh.sh` * `PAI-Install/engine/steps-fresh.ts` * `contrib/anthropic-max-bridge/README.md` * `contrib/anthropic-max-bridge/TECHNICAL.md` * `contrib/anthropic-max-bridge/install.sh` * `contrib/anthropic-max-bridge/plugins/anthropic-max-bridge.js` * `contrib/anthropic-max-bridge/refresh-token.sh` * `docs/providers/anthropic-max.md` </details> <details> <summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary> * PAI-Install/anthropic-max-refresh.sh </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
- 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
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@PAI-Install/engine/steps-fresh.ts`:
- Around line 248-261: The call to Bun.spawn that runs the `security` command
does not check the process exit code, so `keychainJson` could contain error
text; update the `proc` handling in the block using `Bun.spawn` to await the
exit code (from `proc.exited`), capture and inspect both stdout and stderr, and
only accept `keychainJson` when the exit code is 0; if non-zero, treat it as a
failure and return the existing failure object (or include stderr in the
message) instead of relying solely on an empty `keychainJson` check. Ensure you
reference and update the `proc`, `keychainJson`, and the surrounding conditional
that currently returns the failure object.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9e310786-b1c9-46ec-b346-e856dc7bb66d
📒 Files selected for processing (3)
.opencode/plugins/anthropic-max-bridge.jsPAI-Install/engine/steps-fresh.tscontrib/anthropic-max-bridge/install.sh
🚧 Files skipped from review as they are similar to previous changes (2)
- .opencode/plugins/anthropic-max-bridge.js
- contrib/anthropic-max-bridge/install.sh
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.
There was a problem hiding this comment.
🧹 Nitpick comments (1)
PAI-Install/engine/steps-fresh.ts (1)
300-302: Consider logging a warning when refresh token is missing.If
refreshTokenis undefined, an empty string is written toauth.json. When the token expires, the refresh script will fail and the user will need to re-authenticate via Claude CLI. This is acceptable behavior, but a warning during installation would improve the user experience.💡 Optional: Add a warning for missing refresh token
accessToken = oauth.accessToken; refreshToken = oauth.refreshToken ?? ""; + if (!refreshToken) { + console.warn("⚠️ No refresh token found — you'll need to re-authenticate Claude CLI when the token expires."); + } expiresAt = oauth.expiresAt ?? Date.now() + 8 * 60 * 60 * 1000;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@PAI-Install/engine/steps-fresh.ts` around lines 300 - 302, When building the auth object from oauth in steps-fresh.ts (where accessToken, refreshToken and expiresAt are set from oauth), detect when oauth.refreshToken is undefined and emit a clear warning via the installer logger (e.g., console.warn or the existing logger used in this file) before writing auth.json; keep setting refreshToken = oauth.refreshToken ?? "" and expiresAt as-is but add the warning message referencing the missing refresh token so users know they will need to re-authenticate once the access token expires.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@PAI-Install/engine/steps-fresh.ts`:
- Around line 300-302: When building the auth object from oauth in
steps-fresh.ts (where accessToken, refreshToken and expiresAt are set from
oauth), detect when oauth.refreshToken is undefined and emit a clear warning via
the installer logger (e.g., console.warn or the existing logger used in this
file) before writing auth.json; keep setting refreshToken = oauth.refreshToken
?? "" and expiresAt as-is but add the warning message referencing the missing
refresh token so users know they will need to re-authenticate once the access
token expires.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 24fa7819-8415-4161-ad6b-b4d7cc626214
📒 Files selected for processing (1)
PAI-Install/engine/steps-fresh.ts
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.
…ion (#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.
…ion (#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.
#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)
Summary
anthropic-maxas a built-in installer preset so users with an existing Anthropic Max or Pro subscription can use PAI-OpenCode without paying extra for API keysChanges
PAI-Install/engine/provider-models.tsanthropic-maxtoProviderNameunion,PROVIDER_MODELS, andPROVIDER_LABELSPAI-Install/engine/steps-fresh.tsinstallAnthropicMaxBridge(): copies plugin, extracts Keychain token, writes auth.json, reports hours remainingstepInstallPAI(): calls bridge installer when provider === anthropic-maxrunFreshInstall(): skips API key prompt for anthropic-max, shows Claude CLI check insteadPAI-Install/cli/quick-install.ts--preset anthropic-maxworks without--api-key(all other presets still enforce it)New files
.opencode/plugins/anthropic-max-bridge.js— 80-line plugin, 3 API fixes onlyPAI-Install/anthropic-max-refresh.sh— one-command token refreshdocs/providers/anthropic-max.md— user-facing guideThe 3 plugin fixes
Usage
Requires: macOS + Claude Code CLI authenticated. Community workaround — may violate Anthropic ToS.
Summary by CodeRabbit
New Features
Documentation