agentleFS
nikshipsnikships/droidproxy/AGENTS.md

droidproxy

AGENTS.md128 starsChanged 5 months agoMITView on GitHub

The Swift package lives in src/. Run all swift build, swift run, and swift package commands from there, not from the repo root. dev-relaunch.sh is the preferred way to run DroidProxy during development. It calls create-app-bundle.sh (which runs swift build -c release and assembles the signed .app) after killing any running CLIProxyMenuBar / cli-proxy-api processes, then launches the fresh bundle. Do not use it for releases — those go through .github/workflows/release.yml. create-app-bundle.sh currently builds DroidProxy.app at the repo root and…

  • Reads credentials
  • Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.

    Of the AGENTS.md files we read, the 31,700 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.

  • In this repository5 other files it gives its agents.

The file

We split this AGENTS.md into the rules it gives an agent

12 rules. About 0 are common, 1 is distinctive, and 11 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 11

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    No Claude adaptive thinking injection (Opus 4.8 / Sonnet 5 — thinking + output_config)

    line 91
  2. Very specific:

    No classic thinking.budget_tokens injection

    line 92
  3. Very specific:

    No Codex reasoning.effort injection

    line 93
  4. Very specific:

    No Gemini generationConfig.thinkingConfig injection

    line 94
  5. Very specific:

    No Kimi reasoning_effort injection

    line 95
  6. Distinctive:

    No Max Budget Mode override

    line 97
  7. Very specific:

    No Amp CLI routing (the /auth/cli-login redirect, /provider/* rewrite, ampcode.com management forwarding, and Amp response normalization …

    line 98
  8. Very specific:

    Use NSLog, not print or os_log

    line 194
  9. Very specific:

    Treat DroidProxy.app, CLIProxyMenuBar, and com.droidproxy.app as the active app identity

    line 196
  10. Very specific:

    If a task touches release tooling, audit the current workflow and create-app-bundle.sh.

    line 205
  11. Very specific:

    Add the awaiting-cliproxyapi label to the PR.

    line 211
  12. Very specific:

    Put one Requires-CLIProxyAPI-Model: <model-id> line per model in the PR body.

    line 212

≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
AGENTS.md

AGENTS.md

Build & Run

The Swift package lives in src/. Run all swift build, swift run, and swift package commands from there, not from the repo root.

# Preferred dev loop: kill any running DroidProxy, rebuild the .app bundle, and
# launch the freshly signed build. Use this instead of running create-app-bundle.sh
# + open by hand — it guarantees the old menu-bar process and bundled
# cli-proxy-api are stopped before the new app starts.
./dev-relaunch.sh

# Debug build (no .app bundle, no relaunch)
cd src && swift build

# Run the app manually (menu bar app — swift run does not work for LSUIElement apps)
# Build the .app bundle first, then open it:
./create-app-bundle.sh && open DroidProxy.app

# Release .app bundle at repo root
# Picks up CODESIGN_IDENTITY / APP_VERSION / TARGET_ARCH from env when present
./create-app-bundle.sh

dev-relaunch.sh is the preferred way to run DroidProxy during development. It calls create-app-bundle.sh (which runs swift build -c release and assembles the signed .app) after killing any running CLIProxyMenuBar / cli-proxy-api processes, then launches the fresh bundle. Do not use it for releases — those go through .github/workflows/release.yml.

create-app-bundle.sh currently builds DroidProxy.app at the repo root and bundles resources from src/Sources/Resources/.

Notarization (local)

ditto -c -k --sequesterRsrc --keepParent "DroidProxy.app" "DroidProxy-notarize.zip"
xcrun notarytool submit "DroidProxy-notarize.zip" --keychain-profile "notarytool" --wait
xcrun stapler staple "DroidProxy.app"

Sparkle update signing

src/.build/artifacts/sparkle/Sparkle/bin/sign_update DroidProxy-arm64.zip

Source Of Truth

The compiled app code is under src/. Treat src/Sources/**, src/Info.plist, and create-app-bundle.sh as source of truth. There is no longer a mirrored top-level resources/ tree — older AGENTS notes about it are stale.

Architecture

DroidProxy is a macOS menu bar app (LSUIElement) with:

  1. ThinkingProxy on localhost:8317, the user-facing TCP proxy.
  2. Bundled CLIProxyAPI on 127.0.0.1:8318, managed as a child process by ServerManager.
  3. A separate localhost-only Copilot API gateway on 127.0.0.1:8319, managed by CopilotGatewayManager.

Typical request flow:

Client -> :8317 ThinkingProxy -> :8318 CLIProxyAPI -> upstream provider

Selected GitHub Copilot models instead use:

Client -> :8319 Copilot API gateway -> GitHub Copilot

Meta Muse models (muse-wonder-1.0, muse-wonder-1.0-contributor, muse-wonder-full-1.0, muse-wonder-full-1.0-contributor) split by API:

  • Responses (/v1/responses, /responses/compact): ThinkingProxy TLS-forwards to https://api.meta.ai/v1 with the minted Model API key. CLIProxyAPI's generic openai-compatibility executor otherwise rewrites Responses into /chat/completions, which drops reasoning.encrypted_content and breaks long Muse/Foundry runs.
  • Chat Completions: still :8317 -> :8318 -> api.meta.ai via the openai-compatibility block ServerManager writes into merged-config.yaml, so multi-account failover keeps working for Completions clients.

Current ThinkingProxy behavior

Reasoning effort is owned by Droid CLI, not the proxy. Each Factory custom model is registered with native reasoning metadata (enableThinking, supportedReasoningEfforts, defaultReasoningEffort, reasoningEffort) so Droid's per-session selector exposes every level the model supports, and Droid sends the chosen value in the request body. The proxy does not inject thinking, reasoning, reasoning_effort, output_config, budget_tokens, or generationConfig.thinkingConfig for any model — it forwards the request unchanged.

What it still does today:

  • Anthropic-Beta rewriting: When a Claude request has thinking.type of enabled/adaptive/auto, the proxy strips redact-thinking-2026-02-12 from the Anthropic-Beta header and appends the visible-thinking beta list (interleaved-thinking, prompt-caching-scope, etc.). It never injects fast-mode-* and always strips it if Factory/Droid sent it — CLIProxyAPI treats any Fast-marked 429 as request-scoped, which blocks OAuth seat failover. Claude has no Fast mode. Without the redact-thinking strip, Claude emits only signed empty thinking blocks.
  • Service tier (fast mode) for Responses API paths (/v1/responses, /api/v1/responses): injects "service_tier":"priority" for gpt-6-astra, gpt-6.1-sol, gpt-6.1-terra, or gpt-6-luna when AppPreferences.gpt6AstraFastMode / gpt61SolFastMode / gpt61TerraFastMode / gpt6LunaFastMode is enabled and the client did not already set service_tier. Fast mode is API priority and is independent of reasoning effort.
  • Grok routing: every Grok chat/Responses model goes to cli-chat-proxy.grok.com (Grok Build billing). Never send them to api.x.ai: the same OAuth bearer there is billed as X API usage (productUsage Api instead of GrokBuild in /v1/billing?format=credits). Only grok-imagine-* image requests still use api.x.ai. grok-4.7-build-fast is a separate SuperGrok catalog model (same OAuth bearer as grok-4.7) that the public API does not serve. ThinkingProxy sends both with X-XAI-Token-Auth: xai-grok-cli, x-grok-model-override: <requested model id> (grok-4.7 or grok-4.7-build-fast), x-grok-client-version, and x-grok-client-identifier: grok-shell. The proxy rejects a missing client version. The JSON model field stays grok-4.7-build-fast. Reasoning effort stays whatever Droid put in the body.
  • Grok native tool-call rewrite: Grok often leaks Factory markup (<|tool_calls_begin|> / <|tool_call_begin|>) into message.content, or a fenced {"name","arguments"} JSON object. Factory's first-party Grok adapter parses the markup; the custom OpenAI route does not, so the worker turn ends as plain text. The proxy buffers Grok responses and lifts markup or JSON into OpenAI tool_calls before the client sees it.
  • Grok EndFeatureRun repair: Grok often calls EndFeatureRun without handoff, or with validatorsPassed as a thought-string. Factory rejects those and the worker loops. The proxy inserts a stub handoff and coerces validatorsPassed before the client sees the call.
  • Grok Execute repair: Grok often calls Execute with only summary (or cmd/input/shell) and Factory rejects it (command is required). The proxy remaps aliases, lifts a command embedded in summary or a same-turn fenced code block, and promotes a summary that is already a shell string. It does not invent a command from English. On the request path, GrokRequestSanitizer also pins command as a required Execute/Bash/Shell parameter so xAI sees the field.
  • Gemini path rewrite: /v1/responses (and /api/v1/responses) are rewritten to /v1/chat/completions for OAuth Code Assist Gemini models (the -preview-suffixed names) since CLIProxyAPI does not support those via the Responses API endpoint.
  • Meta Muse Responses TLS-forward: muse-wonder-1.0 / muse-wonder-1.0-contributor / muse-wonder-full-1.0 / muse-wonder-full-1.0-contributor POSTs to /v1/responses (and /responses/compact) skip CLIProxyAPI and go to api.meta.ai with the minted Model API key, preserving encrypted reasoning. Chat Completions for those models still use the openai-compatibility block. All four variants accept reasoning efforts minimal through max (live-verified; none/ultra return 400 and are not advertised).
  • Meta usage sniffing: Meta has no usage endpoint, so the proxy inspects (never modifies) relayed Meta Responses bytes for response.subscription_usage SSE events (window 5h + weekly percents with resets_at) and records them per serving account into MetaMuseUsageStore, which backs the Meta cards in OAuth Quota Usage with muse /usage-style "as of" semantics.
  • Usage endpoint: GET /droidproxy/usage[?provider=codex|claude|grok|meta&account=<id|email>] on :8317 is answered locally by UsageEndpoint (never forwarded) with live quota windows as JSON (remaining_percent, resets_at, resets_in_seconds per window). Clicking a gauge in OAuth Quota Usage copies a curl for that account so an agent can poll its own quota and stop before it runs out (Droid gives up retrying failed requests after ~30s). No auth or loopback restriction, by design.
  • Control API: /droidproxy/control/ on :8317 is answered locally by ControlAPI (never forwarded). See "Control API" below.
  • Per-request reasoning log to /tmp/droidproxy-debug.log: each POST emits a REQUEST REASONING: line that extracts just reasoning / reasoning_effort / thinking / output_config / service_tier / generationConfig from the parsed body so the actual values Droid is sending are visible without dumping the whole prompt. Example: REQUEST REASONING: model=gpt-6.1-sol reasoning={"effort":"high","summary":"auto"}.
  • Preserves JSON key order by editing the raw JSON string instead of re-serializing (critical for Anthropic's prompt cache). The remaining helpers (injectJSONField, findTopLevelFieldLocation, etc.) exist for processOpenAIFastMode.

What it no longer does (removed in the Droid-CLI-thinking refactor):

  • No Claude adaptive thinking injection (Opus 4.8 / Sonnet 5 — thinking + output_config)
  • No classic thinking.budget_tokens injection
  • No Codex reasoning.effort injection
  • No Gemini generationConfig.thinkingConfig injection
  • No Kimi reasoning_effort injection
  • No claude-opus-4-8(high) / gpt-5.2(xhigh) etc. “advanced variant” suffix parsing — every level now ships in the single base entry via Droid CLI metadata
  • No Max Budget Mode override
  • No Amp CLI routing (the /auth/cli-login redirect, /provider/* rewrite, ampcode.com management forwarding, and Amp response normalization were removed when switching to mainline CLIProxyAPI)

Control API

Version 1 of the API Droid Office uses to drive DroidProxy (its contract is docs/subscriptions.md in nikships/droid-office). ThinkingProxy serves it on its own port under /droidproxy/control/; parsing, authorization, routing and JSON live in ControlAPI (pure, unit tested), the work in ProviderController.

  • Discovery. At launch DroidProxy writes ~/.droidproxy/control.json (folder 0700, file 0600, temp file and rename): { "api": 1, "port": 8317, "token": "<64 hex>", "version", "pid" }. The token is 32 bytes from SecRandomCopyBytes, new at every launch, and is never logged. The file is removed at quit.
  • Every request needs Authorization: Bearer <token> (compared in constant time; else 401) and is refused with 403 from a non-loopback peer, with any Origin header, or with a Host other than 127.0.0.1:<port> or localhost:<port>. A POST without Content-Type: application/json is 415; a body over 64 KiB is 413 (refused from its Content-Length before it is read). Errors are { "error": "<words for the owner>" }.
CallDoesAnswers
GET /statusVersion, backend, providers (switch, accounts, a sign-in in flight or its error), Droid's models{ api, version, backend: { running }, providers: [{ key, name, enabled, connect, accounts: [{ id, label, expired, disabled }], connecting }], factory: { installed, available, stale } }
POST /connect { provider }Starts the provider's sign-in, as Add Account does202 { connecting: { started_at, user_code, verification_uri, error } }; 409 while one runs; 400 for Junie (connect: "app")
POST /connect/cancel { provider }Stops it200 {}
POST /accounts/remove { provider, account }Forgets an account (by id or email; Copilot's is github_token)200 status; 404 for an unknown account; 409 when an email names several (a Claude email can hold a seat in several organizations)
POST /providers { provider, enabled }The provider switch (hot reload)200 status
POST /factory-models {}Apply Factory Models, backup and all200 { installed, backup }
GET /modelsWhether each catalogue model can run now{ generated_at, models: [{ id, name, provider, availability }] }
POST /open {}Brings up the Settings window200 {}
  • Availability (AvailabilityJudge): disabled when the provider is off; signed-out with no account that isn't expired or disabled (Grok: the active credential only). An account is expired only when its file's expired time (the access token's) has passed and it has no refresh_token: CLIProxyAPI renews a refreshable token on the next request (AuthAccount.isExpired vs isAccessExpired); ok when any such account has every covering window above 0% or no usage to go by, unknown when none of them has usage (and always for Antigravity, Kimi, Junie and Copilot, which have no usage reader); else exhausted with the window and until of the account that comes back first (an account is back when the last of its exhausted windows resets). A window covers every model of its provider except Claude's family windows (Weekly (Opus) only Opus, Weekly (Sonnet) only Sonnet) and Claude's other-product windows (Weekly (OAuth Apps), (Cowork), (Omelette)), which cover none.
  • Usage source (ControlUsage): route every Codex, Claude and Grok quota fetch through OAuthUsageTracker.fetchUsage and its shared OAuthUsageCache. The cache keys by provider and credential-file path, not endpoint filter or bearer token, and coalesces in-flight reads across /models, /usage and Settings. Claude waits at least five minutes plus 0–30 s jitter after each attempt; 429 fallback waits grow 5/10/20/40/60 minutes, never shorter than Retry-After. Other providers retain a 15 s interval. Failed reads preserve the last successful windows and sample time while carrying the error. Samples, request reservations and cooldowns survive restart in ~/.droidproxy/usage-cache.json (0600, no tokens). Do not bypass the cache on manual refresh or token rotation. Meta comes from its stored snapshot without a probe in /models, so polling models never spends Meta quota.
  • Launched by Droid Office. open -g -a DroidProxy.app --args --launched-by droid-office runs normally but shows nothing of its own: no notification prompt or "Server Started" banner, and Sparkle starts only when the owner opens Settings or Check for Updates (LaunchContext).

Auth And Providers

The current app/UI exposes these provider types:

  • claude
  • codex
  • gemini
  • kimi
  • copilot (device-code OAuth; credentials stay in ~/.droidproxy/copilot-api/; the separate local Copilot API gateway serves only the user-selected models)
  • meta (Meta Muse subscription; device-code OAuth against auth.meta.com reproduces the muse CLI's own login flow, then mints a Model API key from api.meta.ai/muse-code/key. Completions keys are written into a generic openai-compatibility block in merged-config.yaml; Responses are TLS-forwarded by ThinkingProxy. This contract is reverse-engineered from the muse binary and a third-party client, not official docs.)

Auth data for AuthManager-managed providers lives in ~/.cli-proxy-api/ as JSON files. Copilot is the exception: its gateway owns ~/.droidproxy/copilot-api/github_token, which DroidProxy never reads. Meta Muse is a second exception: its per-account identity tokens and minted Model API keys live in ~/.droidproxy/meta/accounts.json (mode 0600, parent directory 0700; legacy credentials.json is migrated automatically), never in ~/.cli-proxy-api/.

  • type
  • email
  • login
  • expired
  • disabled

Behavior to know:

  • Multiple accounts per provider are supported
  • Per-account disable/enable is supported via the disabled field in each auth JSON
  • The last enabled account for a provider cannot be disabled
  • Provider-level toggles in SettingsView are separate from per-account disable flags
  • Provider-level disable writes oauth-excluded-models into ~/.cli-proxy-api/merged-config.yaml
  • CLIProxyAPI hot-reloads config changes, so provider enable/disable does not require a restart
  • The app watches ~/.cli-proxy-api/ for changes from both AppDelegate and SettingsView

Key Files

FileRole
src/Sources/main.swiftNSApplication entry point that instantiates AppDelegate and calls NSApplicationMain.
src/Sources/AppDelegate.swiftApp lifecycle, menu bar UI, settings window, notifications, Sparkle updater, auth-directory watcher, startup ordering for the two local servers.
src/Sources/ServerManager.swiftStarts/stops bundled cli-proxy-api, captures logs, merges config (including injecting the remote-management allow-remote/secret-key settings from UserDefaults), handles provider enable/disable, runs Claude/Codex/Gemini login commands, and kills orphaned backend processes.
src/Sources/ThinkingProxy.swiftRaw TCP HTTP proxy that forwards requests to CLIProxyAPI (and TLS-forwards Junie/Grok/Meta Responses). Rewrites the Anthropic-Beta header to drop redact-thinking-2026-02-12 on Claude thinking requests and always strips fast-mode-* on Claude traffic, injects service_tier=priority on enabled Codex fast-mode models, rewrites OAuth Code Assist Gemini /v1/responses to /v1/chat/completions, sanitizes Grok tool types, sends all Grok chat models to cli-chat-proxy.grok.com (Grok Imagine to api.x.ai), lifts Grok tool markup into OpenAI tool_calls, repairs Grok EndFeatureRun/Execute arguments, TLS-forwards Muse Wonder /v1/responses to api.meta.ai, sniffs response.subscription_usage SSE events from Meta Responses streams into MetaMuseUsageStore (relayed bytes pass through unchanged), and emits a REQUEST REASONING log line per request. Does not inject reasoning or thinking fields.
src/Sources/ControlAPI.swiftThe control API's pure parts: request parsing, authorization (loopback, Origin, Host, bearer token, content type, body size), routing, JSON shapes, the ControlBackend protocol, and ControlDiscovery (the token and ~/.droidproxy/control.json).
src/Sources/ProviderController.swiftConnect (browser, device code), cancel, remove account, the provider switch and Droid's models, shared by SettingsView and the control API. Tracks each provider's sign-in in flight (started_at, device code, the error until the next connect).
src/Sources/ProviderController+Control.swiftProviderController as the control API's backend, and ControlUsage (the usage /models judges by).
src/Sources/ModelAvailability.swiftAvailabilityJudge: whether a model can run from its provider switch, accounts and usage windows.
src/Sources/FactoryModels.swiftReads and merges DroidProxy's entries in ~/.factory/settings.json (customModels), with the timestamped backup.
src/Sources/LaunchContext.swiftRecognizes a launch by Droid Office (--launched-by droid-office).
src/Sources/UsageEndpoint.swiftServes GET /droidproxy/usage JSON for ThinkingProxy and builds the curl command the Settings gauges copy on click. Reuses OAuthUsageTracker.fetchUsage and AuthManager.loadAccountsSnapshot.
src/Sources/MetaMuseUpstream.swiftPure helpers: Muse model detection, Responses-path detection, and api.meta.ai path normalization used by ThinkingProxy's TLS forward.
src/Sources/ClaudeAnthropicBetaRewriter.swiftStrips fast-mode-* from Claude Anthropic-Beta headers (never injects it) and, on thinking requests, drops redact-thinking-2026-02-12 then appends the visible-thinking beta list.
src/Sources/GrokAuth.swiftGrok device-flow OAuth, token refresh (single-flight + terminal-refresh quarantine), path/header helpers. all Grok chat models → cli-chat-proxy.grok.com plus x-grok-model-override; grok-imagine-* → api.x.ai.
src/Sources/GrokRequestSanitizer.swiftRemaps Factory custom tools to function (and drops unsupported types) before Grok upstream.
src/Sources/GrokNativeToolCallRewriter.swiftLifts Factory/Droid <|tool_calls_begin|> markup out of Grok chat-completion content into OpenAI tool_calls (JSON, SSE, and full HTTP responses).
src/Sources/GrokEndFeatureRunRepair.swiftRepairs Grok EndFeatureRun arguments that omit handoff or send a non-bool validatorsPassed, and Execute/Read/Grep calls that omit command/file_path/pattern or use aliases.
src/Sources/DroidProxyModelCatalog.swiftAuthoritative catalog of DroidProxy-exposed models. Each DroidProxyModelDefinition carries its supported levels plus a defaultLevelValue, and settingsEntry always embeds Factory's native reasoning metadata (enableThinking, supportedReasoningEfforts, defaultReasoningEffort, reasoningEffort) so Droid CLI's per-session selector can expose the full level set.
src/Sources/CopilotSupport.swiftLocal @jeffreycao/copilot-api gateway lifecycle (CopilotGatewayState of idle/starting/running/failed, with a /v1/models readiness probe so a gateway that exits before binding its port surfaces as failed), device-code authentication, account-specific model discovery, and persistence for at most three Factory-selected Copilot models.
src/Sources/MetaMuseSupport.swiftMetaMuseAuthManager reproduces muse login's device-code flow entirely in-process (no child process): device authorization + token polling against auth.meta.com, then Model API key minting against api.meta.ai/muse-code/key. AppDelegate checks hourly and independently refreshes enabled accounts within 6h of their ~24h key expiry. Completions keys are written into CLIProxyAPI; Responses skip it.
src/Sources/MetaMuseCredentialStore.swiftPrivate multi-account persistence, legacy migration, account disable/remove, and stale-refresh protection. ServerManager emits all enabled, unexpired keys as api-key-entries for Completions round-robin/sequential failover. Responses TLS-forward uses the first usable key. .metaAccountsChanged triggers config hot reload and the shared Settings account list.
src/Sources/WindowPrimer.swiftOpt-in "Auto-start daily" for Claude, Codex, and Meta 5-hour windows (rows in Settings → Services via WindowPrimerRow.swift; per-provider toggle + time in UserDefaults windowPrimerEnabled.<p> / windowPrimerMinutes.<p>, default 07:00). A 30s timer in AppDelegate fires once a day at or after the chosen time (2h catch-up, 5-minute retry on failure) and sends one tiny request per enabled account: Meta directly via MetaMuseUsageProbe (forced), Claude/Codex through the local proxy (claude-haiku-4-5-20251001 /v1/messages max_tokens 1; gpt-6-luna /v1/chat/completions). The proxy picks the account, so each account gets a request but placement is the router's. Starting the window early makes it reset mid-workday; weekly caps still apply.
src/Sources/MetaMuseUsageProbe.swiftRefreshes a stale Meta snapshot (older than 60s) by streaming a 16-token /v1/responses request with the account's own key and recording the response.subscription_usage event, then dropping the stream. Used by OAuthUsageTracker.fetchMetaUsage, so both the Settings refresh and /droidproxy/usage get live Meta numbers even when Meta is idle. Costs a few tokens of quota per probe; failures fall back to the stored snapshot.
src/Sources/MetaMuseUsageStore.swiftLast-observed Meta subscription quota per account id (~/.droidproxy/meta/usage.json). Meta has no usage endpoint — the 5-hour window and weekly percents arrive as response.subscription_usage SSE events that ThinkingProxy sniffs into this store (same "as of last response" semantics as muse /usage). Snapshots older than 60s are refreshed by MetaMuseUsageProbe (see below). .metaUsageChanged drives live Settings card updates without refetching other providers.
src/Sources/SettingsView.swiftSwiftUI settings UI for server status, launch-at-login, provider toggles, auth flows (through ProviderController), the Codex fast-mode (service_tier=priority) subsection, the Copilot gateway status row (renders the failed reason plus a Retry button), the Factory custom-models Apply button, OLED theme, background opacity, and remote-access settings. No thinking/reasoning selectors — those live in Droid CLI.
src/Sources/AuthStatus.swiftAuthManager, account parsing, expiry detection, file deletion, and per-account disabled-state updates.
src/Sources/AppPreferences.swiftUserDefaults-backed preferences: fast-mode toggles for GPT 6 Astra/6.1 Sol/6.1 Terra/Luna; allowRemote, secretKey, oledTheme, backgroundOpacity, verboseLogging. No thinking-effort keys — reasoning is driven entirely by Droid CLI.
src/Sources/OAuthUsageTracker.swiftReads Codex/Claude OAuth quota windows, the SuperGrok pooled credit window (cli-chat-proxy.grok.com/v1/billing?format=credits, bearer from GrokAuth.ensureValidAccessToken), and Meta usage from MetaMuseUsageStore, refreshed by MetaMuseUsageProbe when the stored snapshot is over 60s old (a failed probe falls back to the stored "as of" value; the live updateMetaAccounts path only reads the store) for the "OAuth Quota Usage" section in SettingsView. Codex window titles come from limit_window_seconds because plans differ (5-hour + weekly, weekly only, or monthly only); Claude always has 5-hour + weekly; Meta has 5-hour + weekly with an "as of" stamp; SuperGrok has one weekly window. Owns its own refresh button; there is no menu-bar usage display.
src/Sources/OAuthUsageViews.swiftCompact quota dashboard: a wrapping flow of uniform dual-ring gauges with no per-account boxes (provider logo inside each ring; outer track stays empty without a second limit), 3+ windows stack half-size. An email header only appears above a provider's rings when it has several accounts. Clicking a gauge copies its usage curl, covers that gauge with a green checkmark for 1s, and calls onCopiedCurl so SettingsView shows a "Copied <provider> usage curl" toast (CopiedToast, also announced to VoiceOver). Both timers use TransientConfirmation, so a repeat click restarts them. A failed pasteboard write shows no confirmation. Depends only on the usage model types, so it can be rendered outside the full app.
src/Sources/NotificationNames.swiftShared Notification.Name constants (serverStatusChanged, authDirectoryChanged).
src/Sources/IconCatalog.swiftCaches NSImage lookups from the bundle's resource path so menu-bar / settings icons aren't re-decoded per access.
src/Sources/LogoView.swiftInline-SVG LogoView used in the settings UI.
src/Sources/AuthDirectoryMonitor.swiftDebounced DispatchSource watcher on ~/.cli-proxy-api that fires an onChange callback when auth JSON files are added, changed, or removed. Used by both AppDelegate and SettingsView.
src/Sources/AuthPaths.swiftSingle source of truth for the auth directory location (~/.cli-proxy-api).
src/Sources/Resources/config.yamlBundled CLIProxyAPI config (port: 8318, localhost binding, auth dir).
src/Info.plistBundle metadata. Current source-of-truth values include app name DroidProxy, bundle ID com.droidproxy.app, and Sparkle feed URL on anand-92/droidproxy.

Conventions

  • Use NSLog, not print or os_log
  • Source-of-truth edits land under src/ (especially src/Sources/**, src/Sources/Resources/, src/Info.plist) and create-app-bundle.sh at the repo root; there is no longer a parallel top-level resources/ mirror
  • Treat DroidProxy.app, CLIProxyMenuBar, and com.droidproxy.app as the active app identity
  • CLIProxyAPI is bundled as src/Sources/Resources/cli-proxy-api
  • ThinkingProxy uses surgical string insertion for JSON edits to preserve cache-sensitive key ordering (do not switch to JSONSerialization.data round-trips)
  • Local backend traffic is intended to stay on localhost only (127.0.0.1:8318)

Release Notes For Agents

Release automation lives in .github/workflows/release.yml (no Makefile or scripts/create-release.sh in this repo). The app ships as a single arm64 build; there is no x86_64 appcast or Intel release path.

If a task touches release tooling, audit the current workflow and create-app-bundle.sh.

Models that wait on a CLIProxyAPI release

CLIProxyAPI resolves providers from its model registry only, so a catalog PR for a model the bundled binary does not list yet fails with unknown provider for model <id>. To ship such a PR together with the bump that fixes it:

  1. Add the awaiting-cliproxyapi label to the PR.
  2. Put one Requires-CLIProxyAPI-Model: <model-id> line per model in the PR body.

When update-cliproxyapi.yml opens the next bump-cliproxyapi-* PR, .github/workflows/merge-gated-prs.yml checks whether the bumped binary's embedded registry lists every declared id. If so it merges the gated branch into the bump branch, waits for the Swift Build check, squash-merges the combined PR (one release), and closes the gated PR. PRs whose models are still missing are left alone. Re-run it manually with the workflow's workflow_dispatch (bump_pr, optional dry_run).

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.