Auto-Import Credentials: detect existing API keys and configure providers automatically #449

Description

@jeonghun-jj-lee

Important

Problem: When a user re-runs Amicode onboarding (or installs fresh on a machine with existing LLM tool configs), they must manually re-enter API keys they've already configured elsewhere — in opencode's own auth store, shell environment variables, or other AI coding tools. This is friction that shouldn't exist.

Approach: Add an "Import existing credentials" alternative path to the Stage 0 onboarding webview. The extension host scans flat-file credential sources in priority order, deduplicates by provider, tests each key in the background, and presents a preview card (provider + source + live test status). The user picks which provider becomes their default model, confirms with one click, and all detected providers are written to opencode.json. Keys never leave the host process — the webview sees only provider names and test status.

Approaches Considered:

ApproachVerdict
Webview-only scanner (chosen)Scans flat-file sources from the host, reuses existing testConnection and writeOnboardingConfig, keys stay in Node, cross-platform
Keychain integration (macOS security CLI)Finds more credentials (Claude Code OAuth, Copilot tokens) but platform-specific, OAuth tokens aren't portable as static API keys, Keychain prompts confuse users
Passive notification on activationZero-click discovery but notifications are dismissible/missable, harder to present model selection, feels presumptuous

Scope:

  • New credential_scanner.ts module: scans sources in priority order, returns DetectedCredential[]
  • New message types: scan-credentials, scan-results, test-status-update, confirm-import
  • Webview UI: "Import existing credentials" link below manual form, preview card with provider rows and radio for default selection
  • Batch-write variant of writeOnboardingConfig (writes multiple providers in one pass)
  • Does NOT include: Keychain access, OAuth token import, passive auto-detection notification

Assumptions:

  • The opencode auth store (~/.local/share/opencode/auth.json and account.json) is the primary source and uses plaintext keys
  • Shell RC files use export VAR=value patterns parseable by regex without eval
  • Claude Code's .credentials.json fallback may contain type: "api" entries (importable) or type: "oauth" entries (skipped)
  • At least one detected provider must pass the connection test for "Confirm & Save" to enable

Acceptance Criteria

  • "Import existing credentials" link appears below the manual provider/key form in the onboarding webview
  • Clicking it triggers a scan and shows a status indicator: pulsing orange dot + "Searching..." (matches the devtools rebuild indicator pattern — same CSS classes, same animation)
  • On scan completion, the status transitions to static green dot + "Found N providers!" (or inline "No credentials found" message if empty)
  • On scan failure, the status transitions to static red dot + error message
  • Preview card shows each detected provider with: name, source label, and live connection test status (spinning/checkmark/X)
  • User can select which provider becomes the active model via radio selection
  • Default model per provider is auto-selected (first entry in PROVIDER_MODELS)
  • "Confirm & Save" writes all detected providers to opencode.json with correct schema (provider.<id>.options.apiKey, env as string[])
  • No key material is ever sent to the webview — only provider names, source labels, and test results
  • If no credentials are found, an inline message appears and the manual form remains available
  • If a source file is unreadable or malformed, it is skipped silently (logged to output channel)
  • Duplicate providers across sources are deduplicated (first source wins per priority order)
  • Connection tests run in parallel in the background and update the preview asynchronously
  • "Back" link returns to the manual form and drops held credentials from memory
  • Panel close mid-scan aborts without writing config

Key Decisions

Credential Source Priority

1. ~/.local/share/opencode/account.json (v2, active account per serviceID)
2. ~/.local/share/opencode/auth.json (v1, provider.<id>.key)
3. process.env (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
4. Shell RC files (~/.zshrc, ~/.bashrc, ~/.zprofile, ~/.bash_profile)
5. ~/.claude/.credentials.json (type: "api" only, skip OAuth)

First hit per provider wins. No Keychain access.

Provider ID Normalization

Source IDNormalized to
amazon-bedrock (account.json)amazon-bedrock
opencode-go / opencode (account.json)opencode
ANTHROPIC_API_KEY (env)anthropic
OPENAI_API_KEY (env)openai
GOOGLE_API_KEY (env)google
OPENROUTER_API_KEY (env)openrouter
OPENCODE_API_KEY (env)opencode

Scan Status Indicator (reuses devtools rebuild pattern)

Three states, matching the RebuildStatusIndicator in developer-tools.tsx:

StateDotTextTrigger
searching8px orange, pulsing (devtools-pulse keyframe)"Searching..."User clicks "Import existing credentials"
found8px green, static"Found N providers!"Scan completes with results
failed / empty8px red, static (failed) or no dot (empty)Error message / "No credentials found"Scan errors or finds nothing

Reuse the existing CSS classes: devtools-status-dot--orange, devtools-status-dot--green, devtools-status-dot--red, and the devtools-pulse keyframe animation. The onboarding webview is a separate document context, so the relevant CSS must be inlined or duplicated (not imported from the app bundle).

Security Model

  • Keys exist only in the extension host's memory (the DetectedCredential[] array)
  • Webview messages contain { provider, source, modelDefault } — never key material
  • Shell RC parsing uses strict regex: /^\s*export\s+([\w]+)=["']?([^"'\s]+)/ — no eval, no subshell
  • On "Back" or panel close, the held credential array is dropped immediately
  • A security test asserts no postMessage payload from host→webview contains a string field matching /key|secret|token|credential/i with value length > 8

Data Contract (host → webview messages)

// scan-status (sent immediately on scan start, then on completion){type: "scan-status",payload: {state: "searching"|"found"|"empty"|"failed",count?: number,error?: string}}// scan-results (sent once after scan completes successfully){type: "scan-results",payload: {providers: Array<{provider: string,source: string,model: string}>}}// test-status-update (sent per-provider as connection tests complete){type: "test-status-update",payload: {provider: string,ok: boolean,error?: string}}

Data Contract (webview → host messages)

// scan-credentials (triggers the scan){type: "scan-credentials"}// confirm-import (user confirms selection){type: "confirm-import",payload: {activeProvider: string}}

Constraints & Invariants

  • Keys are NEVER serialized to the webview process
  • Shell RC parsing NEVER uses eval, child_process, or subshell execution
  • OAuth tokens (from Claude Code or any source) are NEVER imported — only type: "api" entries
  • The scan NEVER blocks the manual form — it is an alternative path, not a prerequisite
  • Written config MUST match the existing schema: provider.<id>.options.apiKey (nested), env as string[], valid model IDs from PROVIDER_MODELS

Prior Art

  • RebuildStatusIndicator in developer-tools.tsx — the orange/green/red dot + text pattern this feature replicates
  • devtools-status-dot CSS classes and devtools-pulse keyframe in amicode.css (lines 1487-1525)
  • hasProviderEnvVar() in onboarding_routing.ts (exported but unused — checks env vars exist)
  • Stage 3 context-seed in the overture score (opt-in scan → grouped preview → confirm pattern)
  • writeOnboardingConfig() in onboarding_panel.ts (existing config writer, correct schema)
  • testConnection() in onboarding_panel.ts (existing per-provider HTTP test)

Source

Sub-issue of #359 (Enhanced Onboarding: Orient > Demo > Collect)

Sub-issues

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

    , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
     blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
    }
    } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
    })();
    (function(){
    try {
    var __m = "github.com";
    var __re = new RegExp('^' + "github\\.com" + '
    
    Skip to content

    Auto-Import Credentials: detect existing API keys and configure providers automatically #449

    Description

    @jeonghun-jj-lee

    Important

    Problem: When a user re-runs Amicode onboarding (or installs fresh on a machine with existing LLM tool configs), they must manually re-enter API keys they've already configured elsewhere — in opencode's own auth store, shell environment variables, or other AI coding tools. This is friction that shouldn't exist.

    Approach: Add an "Import existing credentials" alternative path to the Stage 0 onboarding webview. The extension host scans flat-file credential sources in priority order, deduplicates by provider, tests each key in the background, and presents a preview card (provider + source + live test status). The user picks which provider becomes their default model, confirms with one click, and all detected providers are written to opencode.json. Keys never leave the host process — the webview sees only provider names and test status.

    Approaches Considered:

    ApproachVerdict
    Webview-only scanner (chosen)Scans flat-file sources from the host, reuses existing testConnection and writeOnboardingConfig, keys stay in Node, cross-platform
    Keychain integration (macOS security CLI)Finds more credentials (Claude Code OAuth, Copilot tokens) but platform-specific, OAuth tokens aren't portable as static API keys, Keychain prompts confuse users
    Passive notification on activationZero-click discovery but notifications are dismissible/missable, harder to present model selection, feels presumptuous

    Scope:

    • New credential_scanner.ts module: scans sources in priority order, returns DetectedCredential[]
    • New message types: scan-credentials, scan-results, test-status-update, confirm-import
    • Webview UI: "Import existing credentials" link below manual form, preview card with provider rows and radio for default selection
    • Batch-write variant of writeOnboardingConfig (writes multiple providers in one pass)
    • Does NOT include: Keychain access, OAuth token import, passive auto-detection notification

    Assumptions:

    • The opencode auth store (~/.local/share/opencode/auth.json and account.json) is the primary source and uses plaintext keys
    • Shell RC files use export VAR=value patterns parseable by regex without eval
    • Claude Code's .credentials.json fallback may contain type: "api" entries (importable) or type: "oauth" entries (skipped)
    • At least one detected provider must pass the connection test for "Confirm & Save" to enable

    Acceptance Criteria

    • "Import existing credentials" link appears below the manual provider/key form in the onboarding webview
    • Clicking it triggers a scan and shows a status indicator: pulsing orange dot + "Searching..." (matches the devtools rebuild indicator pattern — same CSS classes, same animation)
    • On scan completion, the status transitions to static green dot + "Found N providers!" (or inline "No credentials found" message if empty)
    • On scan failure, the status transitions to static red dot + error message
    • Preview card shows each detected provider with: name, source label, and live connection test status (spinning/checkmark/X)
    • User can select which provider becomes the active model via radio selection
    • Default model per provider is auto-selected (first entry in PROVIDER_MODELS)
    • "Confirm & Save" writes all detected providers to opencode.json with correct schema (provider.<id>.options.apiKey, env as string[])
    • No key material is ever sent to the webview — only provider names, source labels, and test results
    • If no credentials are found, an inline message appears and the manual form remains available
    • If a source file is unreadable or malformed, it is skipped silently (logged to output channel)
    • Duplicate providers across sources are deduplicated (first source wins per priority order)
    • Connection tests run in parallel in the background and update the preview asynchronously
    • "Back" link returns to the manual form and drops held credentials from memory
    • Panel close mid-scan aborts without writing config

    Key Decisions

    Credential Source Priority

    1. ~/.local/share/opencode/account.json (v2, active account per serviceID)
    2. ~/.local/share/opencode/auth.json (v1, provider.<id>.key)
    3. process.env (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
    4. Shell RC files (~/.zshrc, ~/.bashrc, ~/.zprofile, ~/.bash_profile)
    5. ~/.claude/.credentials.json (type: "api" only, skip OAuth)
    

    First hit per provider wins. No Keychain access.

    Provider ID Normalization

    Source IDNormalized to
    amazon-bedrock (account.json)amazon-bedrock
    opencode-go / opencode (account.json)opencode
    ANTHROPIC_API_KEY (env)anthropic
    OPENAI_API_KEY (env)openai
    GOOGLE_API_KEY (env)google
    OPENROUTER_API_KEY (env)openrouter
    OPENCODE_API_KEY (env)opencode

    Scan Status Indicator (reuses devtools rebuild pattern)

    Three states, matching the RebuildStatusIndicator in developer-tools.tsx:

    StateDotTextTrigger
    searching8px orange, pulsing (devtools-pulse keyframe)"Searching..."User clicks "Import existing credentials"
    found8px green, static"Found N providers!"Scan completes with results
    failed / empty8px red, static (failed) or no dot (empty)Error message / "No credentials found"Scan errors or finds nothing

    Reuse the existing CSS classes: devtools-status-dot--orange, devtools-status-dot--green, devtools-status-dot--red, and the devtools-pulse keyframe animation. The onboarding webview is a separate document context, so the relevant CSS must be inlined or duplicated (not imported from the app bundle).

    Security Model

    • Keys exist only in the extension host's memory (the DetectedCredential[] array)
    • Webview messages contain { provider, source, modelDefault } — never key material
    • Shell RC parsing uses strict regex: /^\s*export\s+([\w]+)=["']?([^"'\s]+)/ — no eval, no subshell
    • On "Back" or panel close, the held credential array is dropped immediately
    • A security test asserts no postMessage payload from host→webview contains a string field matching /key|secret|token|credential/i with value length > 8

    Data Contract (host → webview messages)

    // scan-status (sent immediately on scan start, then on completion){type: "scan-status",payload: {state: "searching"|"found"|"empty"|"failed",count?: number,error?: string}}// scan-results (sent once after scan completes successfully){type: "scan-results",payload: {providers: Array<{provider: string,source: string,model: string}>}}// test-status-update (sent per-provider as connection tests complete){type: "test-status-update",payload: {provider: string,ok: boolean,error?: string}}

    Data Contract (webview → host messages)

    // scan-credentials (triggers the scan){type: "scan-credentials"}// confirm-import (user confirms selection){type: "confirm-import",payload: {activeProvider: string}}

    Constraints & Invariants

    • Keys are NEVER serialized to the webview process
    • Shell RC parsing NEVER uses eval, child_process, or subshell execution
    • OAuth tokens (from Claude Code or any source) are NEVER imported — only type: "api" entries
    • The scan NEVER blocks the manual form — it is an alternative path, not a prerequisite
    • Written config MUST match the existing schema: provider.<id>.options.apiKey (nested), env as string[], valid model IDs from PROVIDER_MODELS

    Prior Art

    • RebuildStatusIndicator in developer-tools.tsx — the orange/green/red dot + text pattern this feature replicates
    • devtools-status-dot CSS classes and devtools-pulse keyframe in amicode.css (lines 1487-1525)
    • hasProviderEnvVar() in onboarding_routing.ts (exported but unused — checks env vars exist)
    • Stage 3 context-seed in the overture score (opt-in scan → grouped preview → confirm pattern)
    • writeOnboardingConfig() in onboarding_panel.ts (existing config writer, correct schema)
    • testConnection() in onboarding_panel.ts (existing per-provider HTTP test)

    Source

    Sub-issue of #359 (Enhanced Onboarding: Orient > Demo > Collect)

    Sub-issues

    Activity

    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Metadata

    Metadata

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
      Skip to content

      Auto-Import Credentials: detect existing API keys and configure providers automatically #449

      Description

      @jeonghun-jj-lee

      Important

      Problem: When a user re-runs Amicode onboarding (or installs fresh on a machine with existing LLM tool configs), they must manually re-enter API keys they've already configured elsewhere — in opencode's own auth store, shell environment variables, or other AI coding tools. This is friction that shouldn't exist.

      Approach: Add an "Import existing credentials" alternative path to the Stage 0 onboarding webview. The extension host scans flat-file credential sources in priority order, deduplicates by provider, tests each key in the background, and presents a preview card (provider + source + live test status). The user picks which provider becomes their default model, confirms with one click, and all detected providers are written to opencode.json. Keys never leave the host process — the webview sees only provider names and test status.

      Approaches Considered:

      ApproachVerdict
      Webview-only scanner (chosen)Scans flat-file sources from the host, reuses existing testConnection and writeOnboardingConfig, keys stay in Node, cross-platform
      Keychain integration (macOS security CLI)Finds more credentials (Claude Code OAuth, Copilot tokens) but platform-specific, OAuth tokens aren't portable as static API keys, Keychain prompts confuse users
      Passive notification on activationZero-click discovery but notifications are dismissible/missable, harder to present model selection, feels presumptuous

      Scope:

      • New credential_scanner.ts module: scans sources in priority order, returns DetectedCredential[]
      • New message types: scan-credentials, scan-results, test-status-update, confirm-import
      • Webview UI: "Import existing credentials" link below manual form, preview card with provider rows and radio for default selection
      • Batch-write variant of writeOnboardingConfig (writes multiple providers in one pass)
      • Does NOT include: Keychain access, OAuth token import, passive auto-detection notification

      Assumptions:

      • The opencode auth store (~/.local/share/opencode/auth.json and account.json) is the primary source and uses plaintext keys
      • Shell RC files use export VAR=value patterns parseable by regex without eval
      • Claude Code's .credentials.json fallback may contain type: "api" entries (importable) or type: "oauth" entries (skipped)
      • At least one detected provider must pass the connection test for "Confirm & Save" to enable

      Acceptance Criteria

      • "Import existing credentials" link appears below the manual provider/key form in the onboarding webview
      • Clicking it triggers a scan and shows a status indicator: pulsing orange dot + "Searching..." (matches the devtools rebuild indicator pattern — same CSS classes, same animation)
      • On scan completion, the status transitions to static green dot + "Found N providers!" (or inline "No credentials found" message if empty)
      • On scan failure, the status transitions to static red dot + error message
      • Preview card shows each detected provider with: name, source label, and live connection test status (spinning/checkmark/X)
      • User can select which provider becomes the active model via radio selection
      • Default model per provider is auto-selected (first entry in PROVIDER_MODELS)
      • "Confirm & Save" writes all detected providers to opencode.json with correct schema (provider.<id>.options.apiKey, env as string[])
      • No key material is ever sent to the webview — only provider names, source labels, and test results
      • If no credentials are found, an inline message appears and the manual form remains available
      • If a source file is unreadable or malformed, it is skipped silently (logged to output channel)
      • Duplicate providers across sources are deduplicated (first source wins per priority order)
      • Connection tests run in parallel in the background and update the preview asynchronously
      • "Back" link returns to the manual form and drops held credentials from memory
      • Panel close mid-scan aborts without writing config

      Key Decisions

      Credential Source Priority

      1. ~/.local/share/opencode/account.json (v2, active account per serviceID)
      2. ~/.local/share/opencode/auth.json (v1, provider.<id>.key)
      3. process.env (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
      4. Shell RC files (~/.zshrc, ~/.bashrc, ~/.zprofile, ~/.bash_profile)
      5. ~/.claude/.credentials.json (type: "api" only, skip OAuth)
      

      First hit per provider wins. No Keychain access.

      Provider ID Normalization

      Source IDNormalized to
      amazon-bedrock (account.json)amazon-bedrock
      opencode-go / opencode (account.json)opencode
      ANTHROPIC_API_KEY (env)anthropic
      OPENAI_API_KEY (env)openai
      GOOGLE_API_KEY (env)google
      OPENROUTER_API_KEY (env)openrouter
      OPENCODE_API_KEY (env)opencode

      Scan Status Indicator (reuses devtools rebuild pattern)

      Three states, matching the RebuildStatusIndicator in developer-tools.tsx:

      StateDotTextTrigger
      searching8px orange, pulsing (devtools-pulse keyframe)"Searching..."User clicks "Import existing credentials"
      found8px green, static"Found N providers!"Scan completes with results
      failed / empty8px red, static (failed) or no dot (empty)Error message / "No credentials found"Scan errors or finds nothing

      Reuse the existing CSS classes: devtools-status-dot--orange, devtools-status-dot--green, devtools-status-dot--red, and the devtools-pulse keyframe animation. The onboarding webview is a separate document context, so the relevant CSS must be inlined or duplicated (not imported from the app bundle).

      Security Model

      • Keys exist only in the extension host's memory (the DetectedCredential[] array)
      • Webview messages contain { provider, source, modelDefault } — never key material
      • Shell RC parsing uses strict regex: /^\s*export\s+([\w]+)=["']?([^"'\s]+)/ — no eval, no subshell
      • On "Back" or panel close, the held credential array is dropped immediately
      • A security test asserts no postMessage payload from host→webview contains a string field matching /key|secret|token|credential/i with value length > 8

      Data Contract (host → webview messages)

      // scan-status (sent immediately on scan start, then on completion){type: "scan-status",payload: {state: "searching"|"found"|"empty"|"failed",count?: number,error?: string}}// scan-results (sent once after scan completes successfully){type: "scan-results",payload: {providers: Array<{provider: string,source: string,model: string}>}}// test-status-update (sent per-provider as connection tests complete){type: "test-status-update",payload: {provider: string,ok: boolean,error?: string}}

      Data Contract (webview → host messages)

      // scan-credentials (triggers the scan){type: "scan-credentials"}// confirm-import (user confirms selection){type: "confirm-import",payload: {activeProvider: string}}

      Constraints & Invariants

      • Keys are NEVER serialized to the webview process
      • Shell RC parsing NEVER uses eval, child_process, or subshell execution
      • OAuth tokens (from Claude Code or any source) are NEVER imported — only type: "api" entries
      • The scan NEVER blocks the manual form — it is an alternative path, not a prerequisite
      • Written config MUST match the existing schema: provider.<id>.options.apiKey (nested), env as string[], valid model IDs from PROVIDER_MODELS

      Prior Art

      • RebuildStatusIndicator in developer-tools.tsx — the orange/green/red dot + text pattern this feature replicates
      • devtools-status-dot CSS classes and devtools-pulse keyframe in amicode.css (lines 1487-1525)
      • hasProviderEnvVar() in onboarding_routing.ts (exported but unused — checks env vars exist)
      • Stage 3 context-seed in the overture score (opt-in scan → grouped preview → confirm pattern)
      • writeOnboardingConfig() in onboarding_panel.ts (existing config writer, correct schema)
      • testConnection() in onboarding_panel.ts (existing per-provider HTTP test)

      Source

      Sub-issue of #359 (Enhanced Onboarding: Orient > Demo > Collect)

      Sub-issues

      Activity

      Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

      Metadata

      Metadata

      Labels

      enhancementNew feature or request

      Type

      No type

      Projects

      No projects

        Milestone

        No milestone

        Relationships

        None yet

        Development

        No branches or pull requests

        Issue actions

        , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
        Skip to content

        Auto-Import Credentials: detect existing API keys and configure providers automatically #449

        Description

        @jeonghun-jj-lee

        Important

        Problem: When a user re-runs Amicode onboarding (or installs fresh on a machine with existing LLM tool configs), they must manually re-enter API keys they've already configured elsewhere — in opencode's own auth store, shell environment variables, or other AI coding tools. This is friction that shouldn't exist.

        Approach: Add an "Import existing credentials" alternative path to the Stage 0 onboarding webview. The extension host scans flat-file credential sources in priority order, deduplicates by provider, tests each key in the background, and presents a preview card (provider + source + live test status). The user picks which provider becomes their default model, confirms with one click, and all detected providers are written to opencode.json. Keys never leave the host process — the webview sees only provider names and test status.

        Approaches Considered:

        ApproachVerdict
        Webview-only scanner (chosen)Scans flat-file sources from the host, reuses existing testConnection and writeOnboardingConfig, keys stay in Node, cross-platform
        Keychain integration (macOS security CLI)Finds more credentials (Claude Code OAuth, Copilot tokens) but platform-specific, OAuth tokens aren't portable as static API keys, Keychain prompts confuse users
        Passive notification on activationZero-click discovery but notifications are dismissible/missable, harder to present model selection, feels presumptuous

        Scope:

        • New credential_scanner.ts module: scans sources in priority order, returns DetectedCredential[]
        • New message types: scan-credentials, scan-results, test-status-update, confirm-import
        • Webview UI: "Import existing credentials" link below manual form, preview card with provider rows and radio for default selection
        • Batch-write variant of writeOnboardingConfig (writes multiple providers in one pass)
        • Does NOT include: Keychain access, OAuth token import, passive auto-detection notification

        Assumptions:

        • The opencode auth store (~/.local/share/opencode/auth.json and account.json) is the primary source and uses plaintext keys
        • Shell RC files use export VAR=value patterns parseable by regex without eval
        • Claude Code's .credentials.json fallback may contain type: "api" entries (importable) or type: "oauth" entries (skipped)
        • At least one detected provider must pass the connection test for "Confirm & Save" to enable

        Acceptance Criteria

        • "Import existing credentials" link appears below the manual provider/key form in the onboarding webview
        • Clicking it triggers a scan and shows a status indicator: pulsing orange dot + "Searching..." (matches the devtools rebuild indicator pattern — same CSS classes, same animation)
        • On scan completion, the status transitions to static green dot + "Found N providers!" (or inline "No credentials found" message if empty)
        • On scan failure, the status transitions to static red dot + error message
        • Preview card shows each detected provider with: name, source label, and live connection test status (spinning/checkmark/X)
        • User can select which provider becomes the active model via radio selection
        • Default model per provider is auto-selected (first entry in PROVIDER_MODELS)
        • "Confirm & Save" writes all detected providers to opencode.json with correct schema (provider.<id>.options.apiKey, env as string[])
        • No key material is ever sent to the webview — only provider names, source labels, and test results
        • If no credentials are found, an inline message appears and the manual form remains available
        • If a source file is unreadable or malformed, it is skipped silently (logged to output channel)
        • Duplicate providers across sources are deduplicated (first source wins per priority order)
        • Connection tests run in parallel in the background and update the preview asynchronously
        • "Back" link returns to the manual form and drops held credentials from memory
        • Panel close mid-scan aborts without writing config

        Key Decisions

        Credential Source Priority

        1. ~/.local/share/opencode/account.json (v2, active account per serviceID)
        2. ~/.local/share/opencode/auth.json (v1, provider.<id>.key)
        3. process.env (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
        4. Shell RC files (~/.zshrc, ~/.bashrc, ~/.zprofile, ~/.bash_profile)
        5. ~/.claude/.credentials.json (type: "api" only, skip OAuth)
        

        First hit per provider wins. No Keychain access.

        Provider ID Normalization

        Source IDNormalized to
        amazon-bedrock (account.json)amazon-bedrock
        opencode-go / opencode (account.json)opencode
        ANTHROPIC_API_KEY (env)anthropic
        OPENAI_API_KEY (env)openai
        GOOGLE_API_KEY (env)google
        OPENROUTER_API_KEY (env)openrouter
        OPENCODE_API_KEY (env)opencode

        Scan Status Indicator (reuses devtools rebuild pattern)

        Three states, matching the RebuildStatusIndicator in developer-tools.tsx:

        StateDotTextTrigger
        searching8px orange, pulsing (devtools-pulse keyframe)"Searching..."User clicks "Import existing credentials"
        found8px green, static"Found N providers!"Scan completes with results
        failed / empty8px red, static (failed) or no dot (empty)Error message / "No credentials found"Scan errors or finds nothing

        Reuse the existing CSS classes: devtools-status-dot--orange, devtools-status-dot--green, devtools-status-dot--red, and the devtools-pulse keyframe animation. The onboarding webview is a separate document context, so the relevant CSS must be inlined or duplicated (not imported from the app bundle).

        Security Model

        • Keys exist only in the extension host's memory (the DetectedCredential[] array)
        • Webview messages contain { provider, source, modelDefault } — never key material
        • Shell RC parsing uses strict regex: /^\s*export\s+([\w]+)=["']?([^"'\s]+)/ — no eval, no subshell
        • On "Back" or panel close, the held credential array is dropped immediately
        • A security test asserts no postMessage payload from host→webview contains a string field matching /key|secret|token|credential/i with value length > 8

        Data Contract (host → webview messages)

        // scan-status (sent immediately on scan start, then on completion){type: "scan-status",payload: {state: "searching"|"found"|"empty"|"failed",count?: number,error?: string}}// scan-results (sent once after scan completes successfully){type: "scan-results",payload: {providers: Array<{provider: string,source: string,model: string}>}}// test-status-update (sent per-provider as connection tests complete){type: "test-status-update",payload: {provider: string,ok: boolean,error?: string}}

        Data Contract (webview → host messages)

        // scan-credentials (triggers the scan){type: "scan-credentials"}// confirm-import (user confirms selection){type: "confirm-import",payload: {activeProvider: string}}

        Constraints & Invariants

        • Keys are NEVER serialized to the webview process
        • Shell RC parsing NEVER uses eval, child_process, or subshell execution
        • OAuth tokens (from Claude Code or any source) are NEVER imported — only type: "api" entries
        • The scan NEVER blocks the manual form — it is an alternative path, not a prerequisite
        • Written config MUST match the existing schema: provider.<id>.options.apiKey (nested), env as string[], valid model IDs from PROVIDER_MODELS

        Prior Art

        • RebuildStatusIndicator in developer-tools.tsx — the orange/green/red dot + text pattern this feature replicates
        • devtools-status-dot CSS classes and devtools-pulse keyframe in amicode.css (lines 1487-1525)
        • hasProviderEnvVar() in onboarding_routing.ts (exported but unused — checks env vars exist)
        • Stage 3 context-seed in the overture score (opt-in scan → grouped preview → confirm pattern)
        • writeOnboardingConfig() in onboarding_panel.ts (existing config writer, correct schema)
        • testConnection() in onboarding_panel.ts (existing per-provider HTTP test)

        Source

        Sub-issue of #359 (Enhanced Onboarding: Orient > Demo > Collect)

        Sub-issues

        Activity

        Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

        Metadata

        Metadata

        Labels

        enhancementNew feature or request

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
          Skip to content

          Auto-Import Credentials: detect existing API keys and configure providers automatically #449

          Description

          @jeonghun-jj-lee

          Important

          Problem: When a user re-runs Amicode onboarding (or installs fresh on a machine with existing LLM tool configs), they must manually re-enter API keys they've already configured elsewhere — in opencode's own auth store, shell environment variables, or other AI coding tools. This is friction that shouldn't exist.

          Approach: Add an "Import existing credentials" alternative path to the Stage 0 onboarding webview. The extension host scans flat-file credential sources in priority order, deduplicates by provider, tests each key in the background, and presents a preview card (provider + source + live test status). The user picks which provider becomes their default model, confirms with one click, and all detected providers are written to opencode.json. Keys never leave the host process — the webview sees only provider names and test status.

          Approaches Considered:

          ApproachVerdict
          Webview-only scanner (chosen)Scans flat-file sources from the host, reuses existing testConnection and writeOnboardingConfig, keys stay in Node, cross-platform
          Keychain integration (macOS security CLI)Finds more credentials (Claude Code OAuth, Copilot tokens) but platform-specific, OAuth tokens aren't portable as static API keys, Keychain prompts confuse users
          Passive notification on activationZero-click discovery but notifications are dismissible/missable, harder to present model selection, feels presumptuous

          Scope:

          • New credential_scanner.ts module: scans sources in priority order, returns DetectedCredential[]
          • New message types: scan-credentials, scan-results, test-status-update, confirm-import
          • Webview UI: "Import existing credentials" link below manual form, preview card with provider rows and radio for default selection
          • Batch-write variant of writeOnboardingConfig (writes multiple providers in one pass)
          • Does NOT include: Keychain access, OAuth token import, passive auto-detection notification

          Assumptions:

          • The opencode auth store (~/.local/share/opencode/auth.json and account.json) is the primary source and uses plaintext keys
          • Shell RC files use export VAR=value patterns parseable by regex without eval
          • Claude Code's .credentials.json fallback may contain type: "api" entries (importable) or type: "oauth" entries (skipped)
          • At least one detected provider must pass the connection test for "Confirm & Save" to enable

          Acceptance Criteria

          • "Import existing credentials" link appears below the manual provider/key form in the onboarding webview
          • Clicking it triggers a scan and shows a status indicator: pulsing orange dot + "Searching..." (matches the devtools rebuild indicator pattern — same CSS classes, same animation)
          • On scan completion, the status transitions to static green dot + "Found N providers!" (or inline "No credentials found" message if empty)
          • On scan failure, the status transitions to static red dot + error message
          • Preview card shows each detected provider with: name, source label, and live connection test status (spinning/checkmark/X)
          • User can select which provider becomes the active model via radio selection
          • Default model per provider is auto-selected (first entry in PROVIDER_MODELS)
          • "Confirm & Save" writes all detected providers to opencode.json with correct schema (provider.<id>.options.apiKey, env as string[])
          • No key material is ever sent to the webview — only provider names, source labels, and test results
          • If no credentials are found, an inline message appears and the manual form remains available
          • If a source file is unreadable or malformed, it is skipped silently (logged to output channel)
          • Duplicate providers across sources are deduplicated (first source wins per priority order)
          • Connection tests run in parallel in the background and update the preview asynchronously
          • "Back" link returns to the manual form and drops held credentials from memory
          • Panel close mid-scan aborts without writing config

          Key Decisions

          Credential Source Priority

          1. ~/.local/share/opencode/account.json (v2, active account per serviceID)
          2. ~/.local/share/opencode/auth.json (v1, provider.<id>.key)
          3. process.env (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
          4. Shell RC files (~/.zshrc, ~/.bashrc, ~/.zprofile, ~/.bash_profile)
          5. ~/.claude/.credentials.json (type: "api" only, skip OAuth)
          

          First hit per provider wins. No Keychain access.

          Provider ID Normalization

          Source IDNormalized to
          amazon-bedrock (account.json)amazon-bedrock
          opencode-go / opencode (account.json)opencode
          ANTHROPIC_API_KEY (env)anthropic
          OPENAI_API_KEY (env)openai
          GOOGLE_API_KEY (env)google
          OPENROUTER_API_KEY (env)openrouter
          OPENCODE_API_KEY (env)opencode

          Scan Status Indicator (reuses devtools rebuild pattern)

          Three states, matching the RebuildStatusIndicator in developer-tools.tsx:

          StateDotTextTrigger
          searching8px orange, pulsing (devtools-pulse keyframe)"Searching..."User clicks "Import existing credentials"
          found8px green, static"Found N providers!"Scan completes with results
          failed / empty8px red, static (failed) or no dot (empty)Error message / "No credentials found"Scan errors or finds nothing

          Reuse the existing CSS classes: devtools-status-dot--orange, devtools-status-dot--green, devtools-status-dot--red, and the devtools-pulse keyframe animation. The onboarding webview is a separate document context, so the relevant CSS must be inlined or duplicated (not imported from the app bundle).

          Security Model

          • Keys exist only in the extension host's memory (the DetectedCredential[] array)
          • Webview messages contain { provider, source, modelDefault } — never key material
          • Shell RC parsing uses strict regex: /^\s*export\s+([\w]+)=["']?([^"'\s]+)/ — no eval, no subshell
          • On "Back" or panel close, the held credential array is dropped immediately
          • A security test asserts no postMessage payload from host→webview contains a string field matching /key|secret|token|credential/i with value length > 8

          Data Contract (host → webview messages)

          // scan-status (sent immediately on scan start, then on completion){type: "scan-status",payload: {state: "searching"|"found"|"empty"|"failed",count?: number,error?: string}}// scan-results (sent once after scan completes successfully){type: "scan-results",payload: {providers: Array<{provider: string,source: string,model: string}>}}// test-status-update (sent per-provider as connection tests complete){type: "test-status-update",payload: {provider: string,ok: boolean,error?: string}}

          Data Contract (webview → host messages)

          // scan-credentials (triggers the scan){type: "scan-credentials"}// confirm-import (user confirms selection){type: "confirm-import",payload: {activeProvider: string}}

          Constraints & Invariants

          • Keys are NEVER serialized to the webview process
          • Shell RC parsing NEVER uses eval, child_process, or subshell execution
          • OAuth tokens (from Claude Code or any source) are NEVER imported — only type: "api" entries
          • The scan NEVER blocks the manual form — it is an alternative path, not a prerequisite
          • Written config MUST match the existing schema: provider.<id>.options.apiKey (nested), env as string[], valid model IDs from PROVIDER_MODELS

          Prior Art

          • RebuildStatusIndicator in developer-tools.tsx — the orange/green/red dot + text pattern this feature replicates
          • devtools-status-dot CSS classes and devtools-pulse keyframe in amicode.css (lines 1487-1525)
          • hasProviderEnvVar() in onboarding_routing.ts (exported but unused — checks env vars exist)
          • Stage 3 context-seed in the overture score (opt-in scan → grouped preview → confirm pattern)
          • writeOnboardingConfig() in onboarding_panel.ts (existing config writer, correct schema)
          • testConnection() in onboarding_panel.ts (existing per-provider HTTP test)

          Source

          Sub-issue of #359 (Enhanced Onboarding: Orient > Demo > Collect)

          Sub-issues

          Activity

          Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

          Metadata

          Metadata

          Labels

          enhancementNew feature or request

          Type

          No type

          Projects

          No projects

            Milestone

            No milestone

            Relationships

            None yet

            Development

            No branches or pull requests

            Issue actions

            , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
            Skip to content

            Auto-Import Credentials: detect existing API keys and configure providers automatically #449

            Description

            @jeonghun-jj-lee

            Important

            Problem: When a user re-runs Amicode onboarding (or installs fresh on a machine with existing LLM tool configs), they must manually re-enter API keys they've already configured elsewhere — in opencode's own auth store, shell environment variables, or other AI coding tools. This is friction that shouldn't exist.

            Approach: Add an "Import existing credentials" alternative path to the Stage 0 onboarding webview. The extension host scans flat-file credential sources in priority order, deduplicates by provider, tests each key in the background, and presents a preview card (provider + source + live test status). The user picks which provider becomes their default model, confirms with one click, and all detected providers are written to opencode.json. Keys never leave the host process — the webview sees only provider names and test status.

            Approaches Considered:

            ApproachVerdict
            Webview-only scanner (chosen)Scans flat-file sources from the host, reuses existing testConnection and writeOnboardingConfig, keys stay in Node, cross-platform
            Keychain integration (macOS security CLI)Finds more credentials (Claude Code OAuth, Copilot tokens) but platform-specific, OAuth tokens aren't portable as static API keys, Keychain prompts confuse users
            Passive notification on activationZero-click discovery but notifications are dismissible/missable, harder to present model selection, feels presumptuous

            Scope:

            • New credential_scanner.ts module: scans sources in priority order, returns DetectedCredential[]
            • New message types: scan-credentials, scan-results, test-status-update, confirm-import
            • Webview UI: "Import existing credentials" link below manual form, preview card with provider rows and radio for default selection
            • Batch-write variant of writeOnboardingConfig (writes multiple providers in one pass)
            • Does NOT include: Keychain access, OAuth token import, passive auto-detection notification

            Assumptions:

            • The opencode auth store (~/.local/share/opencode/auth.json and account.json) is the primary source and uses plaintext keys
            • Shell RC files use export VAR=value patterns parseable by regex without eval
            • Claude Code's .credentials.json fallback may contain type: "api" entries (importable) or type: "oauth" entries (skipped)
            • At least one detected provider must pass the connection test for "Confirm & Save" to enable

            Acceptance Criteria

            • "Import existing credentials" link appears below the manual provider/key form in the onboarding webview
            • Clicking it triggers a scan and shows a status indicator: pulsing orange dot + "Searching..." (matches the devtools rebuild indicator pattern — same CSS classes, same animation)
            • On scan completion, the status transitions to static green dot + "Found N providers!" (or inline "No credentials found" message if empty)
            • On scan failure, the status transitions to static red dot + error message
            • Preview card shows each detected provider with: name, source label, and live connection test status (spinning/checkmark/X)
            • User can select which provider becomes the active model via radio selection
            • Default model per provider is auto-selected (first entry in PROVIDER_MODELS)
            • "Confirm & Save" writes all detected providers to opencode.json with correct schema (provider.<id>.options.apiKey, env as string[])
            • No key material is ever sent to the webview — only provider names, source labels, and test results
            • If no credentials are found, an inline message appears and the manual form remains available
            • If a source file is unreadable or malformed, it is skipped silently (logged to output channel)
            • Duplicate providers across sources are deduplicated (first source wins per priority order)
            • Connection tests run in parallel in the background and update the preview asynchronously
            • "Back" link returns to the manual form and drops held credentials from memory
            • Panel close mid-scan aborts without writing config

            Key Decisions

            Credential Source Priority

            1. ~/.local/share/opencode/account.json (v2, active account per serviceID)
            2. ~/.local/share/opencode/auth.json (v1, provider.<id>.key)
            3. process.env (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
            4. Shell RC files (~/.zshrc, ~/.bashrc, ~/.zprofile, ~/.bash_profile)
            5. ~/.claude/.credentials.json (type: "api" only, skip OAuth)
            

            First hit per provider wins. No Keychain access.

            Provider ID Normalization

            Source IDNormalized to
            amazon-bedrock (account.json)amazon-bedrock
            opencode-go / opencode (account.json)opencode
            ANTHROPIC_API_KEY (env)anthropic
            OPENAI_API_KEY (env)openai
            GOOGLE_API_KEY (env)google
            OPENROUTER_API_KEY (env)openrouter
            OPENCODE_API_KEY (env)opencode

            Scan Status Indicator (reuses devtools rebuild pattern)

            Three states, matching the RebuildStatusIndicator in developer-tools.tsx:

            StateDotTextTrigger
            searching8px orange, pulsing (devtools-pulse keyframe)"Searching..."User clicks "Import existing credentials"
            found8px green, static"Found N providers!"Scan completes with results
            failed / empty8px red, static (failed) or no dot (empty)Error message / "No credentials found"Scan errors or finds nothing

            Reuse the existing CSS classes: devtools-status-dot--orange, devtools-status-dot--green, devtools-status-dot--red, and the devtools-pulse keyframe animation. The onboarding webview is a separate document context, so the relevant CSS must be inlined or duplicated (not imported from the app bundle).

            Security Model

            • Keys exist only in the extension host's memory (the DetectedCredential[] array)
            • Webview messages contain { provider, source, modelDefault } — never key material
            • Shell RC parsing uses strict regex: /^\s*export\s+([\w]+)=["']?([^"'\s]+)/ — no eval, no subshell
            • On "Back" or panel close, the held credential array is dropped immediately
            • A security test asserts no postMessage payload from host→webview contains a string field matching /key|secret|token|credential/i with value length > 8

            Data Contract (host → webview messages)

            // scan-status (sent immediately on scan start, then on completion){type: "scan-status",payload: {state: "searching"|"found"|"empty"|"failed",count?: number,error?: string}}// scan-results (sent once after scan completes successfully){type: "scan-results",payload: {providers: Array<{provider: string,source: string,model: string}>}}// test-status-update (sent per-provider as connection tests complete){type: "test-status-update",payload: {provider: string,ok: boolean,error?: string}}

            Data Contract (webview → host messages)

            // scan-credentials (triggers the scan){type: "scan-credentials"}// confirm-import (user confirms selection){type: "confirm-import",payload: {activeProvider: string}}

            Constraints & Invariants

            • Keys are NEVER serialized to the webview process
            • Shell RC parsing NEVER uses eval, child_process, or subshell execution
            • OAuth tokens (from Claude Code or any source) are NEVER imported — only type: "api" entries
            • The scan NEVER blocks the manual form — it is an alternative path, not a prerequisite
            • Written config MUST match the existing schema: provider.<id>.options.apiKey (nested), env as string[], valid model IDs from PROVIDER_MODELS

            Prior Art

            • RebuildStatusIndicator in developer-tools.tsx — the orange/green/red dot + text pattern this feature replicates
            • devtools-status-dot CSS classes and devtools-pulse keyframe in amicode.css (lines 1487-1525)
            • hasProviderEnvVar() in onboarding_routing.ts (exported but unused — checks env vars exist)
            • Stage 3 context-seed in the overture score (opt-in scan → grouped preview → confirm pattern)
            • writeOnboardingConfig() in onboarding_panel.ts (existing config writer, correct schema)
            • testConnection() in onboarding_panel.ts (existing per-provider HTTP test)

            Source

            Sub-issue of #359 (Enhanced Onboarding: Orient > Demo > Collect)

            Sub-issues

            Activity

            Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

            Metadata

            Metadata

            Labels

            enhancementNew feature or request

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              Skip to content

              Auto-Import Credentials: detect existing API keys and configure providers automatically #449

              Description

              @jeonghun-jj-lee

              Important

              Problem: When a user re-runs Amicode onboarding (or installs fresh on a machine with existing LLM tool configs), they must manually re-enter API keys they've already configured elsewhere — in opencode's own auth store, shell environment variables, or other AI coding tools. This is friction that shouldn't exist.

              Approach: Add an "Import existing credentials" alternative path to the Stage 0 onboarding webview. The extension host scans flat-file credential sources in priority order, deduplicates by provider, tests each key in the background, and presents a preview card (provider + source + live test status). The user picks which provider becomes their default model, confirms with one click, and all detected providers are written to opencode.json. Keys never leave the host process — the webview sees only provider names and test status.

              Approaches Considered:

              ApproachVerdict
              Webview-only scanner (chosen)Scans flat-file sources from the host, reuses existing testConnection and writeOnboardingConfig, keys stay in Node, cross-platform
              Keychain integration (macOS security CLI)Finds more credentials (Claude Code OAuth, Copilot tokens) but platform-specific, OAuth tokens aren't portable as static API keys, Keychain prompts confuse users
              Passive notification on activationZero-click discovery but notifications are dismissible/missable, harder to present model selection, feels presumptuous

              Scope:

              • New credential_scanner.ts module: scans sources in priority order, returns DetectedCredential[]
              • New message types: scan-credentials, scan-results, test-status-update, confirm-import
              • Webview UI: "Import existing credentials" link below manual form, preview card with provider rows and radio for default selection
              • Batch-write variant of writeOnboardingConfig (writes multiple providers in one pass)
              • Does NOT include: Keychain access, OAuth token import, passive auto-detection notification

              Assumptions:

              • The opencode auth store (~/.local/share/opencode/auth.json and account.json) is the primary source and uses plaintext keys
              • Shell RC files use export VAR=value patterns parseable by regex without eval
              • Claude Code's .credentials.json fallback may contain type: "api" entries (importable) or type: "oauth" entries (skipped)
              • At least one detected provider must pass the connection test for "Confirm & Save" to enable

              Acceptance Criteria

              • "Import existing credentials" link appears below the manual provider/key form in the onboarding webview
              • Clicking it triggers a scan and shows a status indicator: pulsing orange dot + "Searching..." (matches the devtools rebuild indicator pattern — same CSS classes, same animation)
              • On scan completion, the status transitions to static green dot + "Found N providers!" (or inline "No credentials found" message if empty)
              • On scan failure, the status transitions to static red dot + error message
              • Preview card shows each detected provider with: name, source label, and live connection test status (spinning/checkmark/X)
              • User can select which provider becomes the active model via radio selection
              • Default model per provider is auto-selected (first entry in PROVIDER_MODELS)
              • "Confirm & Save" writes all detected providers to opencode.json with correct schema (provider.<id>.options.apiKey, env as string[])
              • No key material is ever sent to the webview — only provider names, source labels, and test results
              • If no credentials are found, an inline message appears and the manual form remains available
              • If a source file is unreadable or malformed, it is skipped silently (logged to output channel)
              • Duplicate providers across sources are deduplicated (first source wins per priority order)
              • Connection tests run in parallel in the background and update the preview asynchronously
              • "Back" link returns to the manual form and drops held credentials from memory
              • Panel close mid-scan aborts without writing config

              Key Decisions

              Credential Source Priority

              1. ~/.local/share/opencode/account.json (v2, active account per serviceID)
              2. ~/.local/share/opencode/auth.json (v1, provider.<id>.key)
              3. process.env (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
              4. Shell RC files (~/.zshrc, ~/.bashrc, ~/.zprofile, ~/.bash_profile)
              5. ~/.claude/.credentials.json (type: "api" only, skip OAuth)
              

              First hit per provider wins. No Keychain access.

              Provider ID Normalization

              Source IDNormalized to
              amazon-bedrock (account.json)amazon-bedrock
              opencode-go / opencode (account.json)opencode
              ANTHROPIC_API_KEY (env)anthropic
              OPENAI_API_KEY (env)openai
              GOOGLE_API_KEY (env)google
              OPENROUTER_API_KEY (env)openrouter
              OPENCODE_API_KEY (env)opencode

              Scan Status Indicator (reuses devtools rebuild pattern)

              Three states, matching the RebuildStatusIndicator in developer-tools.tsx:

              StateDotTextTrigger
              searching8px orange, pulsing (devtools-pulse keyframe)"Searching..."User clicks "Import existing credentials"
              found8px green, static"Found N providers!"Scan completes with results
              failed / empty8px red, static (failed) or no dot (empty)Error message / "No credentials found"Scan errors or finds nothing

              Reuse the existing CSS classes: devtools-status-dot--orange, devtools-status-dot--green, devtools-status-dot--red, and the devtools-pulse keyframe animation. The onboarding webview is a separate document context, so the relevant CSS must be inlined or duplicated (not imported from the app bundle).

              Security Model

              • Keys exist only in the extension host's memory (the DetectedCredential[] array)
              • Webview messages contain { provider, source, modelDefault } — never key material
              • Shell RC parsing uses strict regex: /^\s*export\s+([\w]+)=["']?([^"'\s]+)/ — no eval, no subshell
              • On "Back" or panel close, the held credential array is dropped immediately
              • A security test asserts no postMessage payload from host→webview contains a string field matching /key|secret|token|credential/i with value length > 8

              Data Contract (host → webview messages)

              // scan-status (sent immediately on scan start, then on completion){type: "scan-status",payload: {state: "searching"|"found"|"empty"|"failed",count?: number,error?: string}}// scan-results (sent once after scan completes successfully){type: "scan-results",payload: {providers: Array<{provider: string,source: string,model: string}>}}// test-status-update (sent per-provider as connection tests complete){type: "test-status-update",payload: {provider: string,ok: boolean,error?: string}}

              Data Contract (webview → host messages)

              // scan-credentials (triggers the scan){type: "scan-credentials"}// confirm-import (user confirms selection){type: "confirm-import",payload: {activeProvider: string}}

              Constraints & Invariants

              • Keys are NEVER serialized to the webview process
              • Shell RC parsing NEVER uses eval, child_process, or subshell execution
              • OAuth tokens (from Claude Code or any source) are NEVER imported — only type: "api" entries
              • The scan NEVER blocks the manual form — it is an alternative path, not a prerequisite
              • Written config MUST match the existing schema: provider.<id>.options.apiKey (nested), env as string[], valid model IDs from PROVIDER_MODELS

              Prior Art

              • RebuildStatusIndicator in developer-tools.tsx — the orange/green/red dot + text pattern this feature replicates
              • devtools-status-dot CSS classes and devtools-pulse keyframe in amicode.css (lines 1487-1525)
              • hasProviderEnvVar() in onboarding_routing.ts (exported but unused — checks env vars exist)
              • Stage 3 context-seed in the overture score (opt-in scan → grouped preview → confirm pattern)
              • writeOnboardingConfig() in onboarding_panel.ts (existing config writer, correct schema)
              • testConnection() in onboarding_panel.ts (existing per-provider HTTP test)

              Source

              Sub-issue of #359 (Enhanced Onboarding: Orient > Demo > Collect)

              Sub-issues

              Activity

              Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

              Metadata

              Metadata

              Labels

              enhancementNew feature or request

              Type

              No type

              Projects

              No projects

                Milestone

                No milestone

                Relationships

                None yet

                Development

                No branches or pull requests

                Issue actions

                , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                Skip to content

                Auto-Import Credentials: detect existing API keys and configure providers automatically #449

                Description

                @jeonghun-jj-lee

                Important

                Problem: When a user re-runs Amicode onboarding (or installs fresh on a machine with existing LLM tool configs), they must manually re-enter API keys they've already configured elsewhere — in opencode's own auth store, shell environment variables, or other AI coding tools. This is friction that shouldn't exist.

                Approach: Add an "Import existing credentials" alternative path to the Stage 0 onboarding webview. The extension host scans flat-file credential sources in priority order, deduplicates by provider, tests each key in the background, and presents a preview card (provider + source + live test status). The user picks which provider becomes their default model, confirms with one click, and all detected providers are written to opencode.json. Keys never leave the host process — the webview sees only provider names and test status.

                Approaches Considered:

                ApproachVerdict
                Webview-only scanner (chosen)Scans flat-file sources from the host, reuses existing testConnection and writeOnboardingConfig, keys stay in Node, cross-platform
                Keychain integration (macOS security CLI)Finds more credentials (Claude Code OAuth, Copilot tokens) but platform-specific, OAuth tokens aren't portable as static API keys, Keychain prompts confuse users
                Passive notification on activationZero-click discovery but notifications are dismissible/missable, harder to present model selection, feels presumptuous

                Scope:

                • New credential_scanner.ts module: scans sources in priority order, returns DetectedCredential[]
                • New message types: scan-credentials, scan-results, test-status-update, confirm-import
                • Webview UI: "Import existing credentials" link below manual form, preview card with provider rows and radio for default selection
                • Batch-write variant of writeOnboardingConfig (writes multiple providers in one pass)
                • Does NOT include: Keychain access, OAuth token import, passive auto-detection notification

                Assumptions:

                • The opencode auth store (~/.local/share/opencode/auth.json and account.json) is the primary source and uses plaintext keys
                • Shell RC files use export VAR=value patterns parseable by regex without eval
                • Claude Code's .credentials.json fallback may contain type: "api" entries (importable) or type: "oauth" entries (skipped)
                • At least one detected provider must pass the connection test for "Confirm & Save" to enable

                Acceptance Criteria

                • "Import existing credentials" link appears below the manual provider/key form in the onboarding webview
                • Clicking it triggers a scan and shows a status indicator: pulsing orange dot + "Searching..." (matches the devtools rebuild indicator pattern — same CSS classes, same animation)
                • On scan completion, the status transitions to static green dot + "Found N providers!" (or inline "No credentials found" message if empty)
                • On scan failure, the status transitions to static red dot + error message
                • Preview card shows each detected provider with: name, source label, and live connection test status (spinning/checkmark/X)
                • User can select which provider becomes the active model via radio selection
                • Default model per provider is auto-selected (first entry in PROVIDER_MODELS)
                • "Confirm & Save" writes all detected providers to opencode.json with correct schema (provider.<id>.options.apiKey, env as string[])
                • No key material is ever sent to the webview — only provider names, source labels, and test results
                • If no credentials are found, an inline message appears and the manual form remains available
                • If a source file is unreadable or malformed, it is skipped silently (logged to output channel)
                • Duplicate providers across sources are deduplicated (first source wins per priority order)
                • Connection tests run in parallel in the background and update the preview asynchronously
                • "Back" link returns to the manual form and drops held credentials from memory
                • Panel close mid-scan aborts without writing config

                Key Decisions

                Credential Source Priority

                1. ~/.local/share/opencode/account.json (v2, active account per serviceID)
                2. ~/.local/share/opencode/auth.json (v1, provider.<id>.key)
                3. process.env (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)
                4. Shell RC files (~/.zshrc, ~/.bashrc, ~/.zprofile, ~/.bash_profile)
                5. ~/.claude/.credentials.json (type: "api" only, skip OAuth)
                

                First hit per provider wins. No Keychain access.

                Provider ID Normalization

                Source IDNormalized to
                amazon-bedrock (account.json)amazon-bedrock
                opencode-go / opencode (account.json)opencode
                ANTHROPIC_API_KEY (env)anthropic
                OPENAI_API_KEY (env)openai
                GOOGLE_API_KEY (env)google
                OPENROUTER_API_KEY (env)openrouter
                OPENCODE_API_KEY (env)opencode

                Scan Status Indicator (reuses devtools rebuild pattern)

                Three states, matching the RebuildStatusIndicator in developer-tools.tsx:

                StateDotTextTrigger
                searching8px orange, pulsing (devtools-pulse keyframe)"Searching..."User clicks "Import existing credentials"
                found8px green, static"Found N providers!"Scan completes with results
                failed / empty8px red, static (failed) or no dot (empty)Error message / "No credentials found"Scan errors or finds nothing

                Reuse the existing CSS classes: devtools-status-dot--orange, devtools-status-dot--green, devtools-status-dot--red, and the devtools-pulse keyframe animation. The onboarding webview is a separate document context, so the relevant CSS must be inlined or duplicated (not imported from the app bundle).

                Security Model

                • Keys exist only in the extension host's memory (the DetectedCredential[] array)
                • Webview messages contain { provider, source, modelDefault } — never key material
                • Shell RC parsing uses strict regex: /^\s*export\s+([\w]+)=["']?([^"'\s]+)/ — no eval, no subshell
                • On "Back" or panel close, the held credential array is dropped immediately
                • A security test asserts no postMessage payload from host→webview contains a string field matching /key|secret|token|credential/i with value length > 8

                Data Contract (host → webview messages)

                // scan-status (sent immediately on scan start, then on completion){type: "scan-status",payload: {state: "searching"|"found"|"empty"|"failed",count?: number,error?: string}}// scan-results (sent once after scan completes successfully){type: "scan-results",payload: {providers: Array<{provider: string,source: string,model: string}>}}// test-status-update (sent per-provider as connection tests complete){type: "test-status-update",payload: {provider: string,ok: boolean,error?: string}}

                Data Contract (webview → host messages)

                // scan-credentials (triggers the scan){type: "scan-credentials"}// confirm-import (user confirms selection){type: "confirm-import",payload: {activeProvider: string}}

                Constraints & Invariants

                • Keys are NEVER serialized to the webview process
                • Shell RC parsing NEVER uses eval, child_process, or subshell execution
                • OAuth tokens (from Claude Code or any source) are NEVER imported — only type: "api" entries
                • The scan NEVER blocks the manual form — it is an alternative path, not a prerequisite
                • Written config MUST match the existing schema: provider.<id>.options.apiKey (nested), env as string[], valid model IDs from PROVIDER_MODELS

                Prior Art

                • RebuildStatusIndicator in developer-tools.tsx — the orange/green/red dot + text pattern this feature replicates
                • devtools-status-dot CSS classes and devtools-pulse keyframe in amicode.css (lines 1487-1525)
                • hasProviderEnvVar() in onboarding_routing.ts (exported but unused — checks env vars exist)
                • Stage 3 context-seed in the overture score (opt-in scan → grouped preview → confirm pattern)
                • writeOnboardingConfig() in onboarding_panel.ts (existing config writer, correct schema)
                • testConnection() in onboarding_panel.ts (existing per-provider HTTP test)

                Source

                Sub-issue of #359 (Enhanced Onboarding: Orient > Demo > Collect)

                Sub-issues

                Activity

                Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

                Metadata

                Metadata

                Labels

                enhancementNew feature or request

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions