GitHub Copilot whitelists 'claude-cli' but may not whitelist
'openclaude-cli'. Keep using the upstream-compatible identity
for GitHub Copilot until confirmed safe to switch.
Refs: PR #976
* Update README.md
Access token and local ADC workflow authentication is NOT available for Gemini.
* Update advanced-setup.md
API key authentication is the only available method for Gemini
Local OpenAI-compatible servers (vLLM, llama.cpp, custom self-hosted
gateways) often validate request bodies against a strict JSON schema
and reject unknown fields with `400 Bad Request`. The shim already
sends `store: false` (an OpenAI-only field for cloud conversation
persistence) and strips it for known cloud hosts that share the same
intolerance (Gemini, Cerebras). Local servers have no notion of
remote conversation storage and fall in the same bucket.
Add `isLocal` to `shouldStripResponsesStore` so any baseUrl resolved
by `isLocalProviderUrl` (localhost / 127.0.0.1 / ::1 / 0.0.0.0) gets
the field removed. Lenient locals (Ollama) already ignored it; this
unblocks strict ones (vLLM Qwen) without behavior change for the
former.
Closes#672 (the `store: false` symptom; the separate `max_tokens`
default vs. vLLM `max_model_len` collision is a different concern).
* feat: incremental and cached token counting
- Add IncrementalTokenCounter for performance (avoids recounting entire context)
- Add getCacheTokens() to extract cache read/creation tokens
- Add getNewTokensOnly() to get new tokens excluding cache
- Add getTokenBreakdown() with cache efficiency percentage
- Add comprehensive tests (10 passing)
PR 1A: Token Counting Core (Features 1.6, 1.13)
* refactor: extract IncrementalTokenCounter and tokenCache to separate files
- Move IncrementalTokenCounter to incrementalTokenCounter.ts with stats tracking
- Move cache utilities to tokenCache.ts with cost estimation and analytics
- Remove duplicate implementations from tokens.ts
- Update tokens.test.ts to import from new files
- Add comprehensive tests for both new modules
* fix: content-aware cache invalidation + high-precision costs
Blocker fixes:
- IncrementalTokenCounter: hash last message content for cache key
- tokenCache: use 4-decimal precision for cost estimates (was 3, collapsed to $0)
- exceedsBudget: use raw high-precision cost for budget comparisons
Content hash prevents stale cache on same-length edits.
* fix: PR 795 - fix cache invalidation and hash collisions
Blockers:
- getMessageHash now hashes ALL messages with full content (not just last message)
- Prevents hash collisions when edits occur outside recent window
- Incremental branch now verifies prefix hash matches before using cached count
- If earlier messages mutated, full recalculation instead of stale increment
* fix: PR 795 - fix prefix mutation + append invalidation
Blocking:
- Store both lastFullHash and lastPrefixHash separately
- Compare actual prefix hash values, not just length (which always passes)
- If prefix mutated, do full recalculation instead of stale increment
Non-blocking:
- Add regression tests for prefix mutation + append case
* fix: PR 795 - fix isApproachingLimit to use tokenBudget not cache size
Blocking:
- Rename maxCacheSize to tokenBudget in IncrementalCounterConfig
- isApproachingLimit now correctly compares against token budget (context window size)
- Not cache entry size which was meaningless
- Update CounterFactory to use appropriate tokenBudget values
* fix: PR 795 - wire IncrementalTokenCounter into tokenCountWithEstimation
- Integrate incremental counter into production token counting path
- tokenCountWithEstimation now uses IncrementalTokenCounter.getCount() for rough estimation
- Preserves exact usage baseline from last API response
- getIncrementalTokenCounter() exported for external use
- Uses lazy init to avoid circular dependency issues
* fix: PR 795 - trim unused token cache surface
- Remove tokenCache.ts (300+ line unused utility with no production caller)
- Remove tokenCache.test.ts
- Remove unused getCacheInfoFromUsage() from tokens.ts
- Remove related tests in tokens.test.ts
- PR now focused on IncrementalTokenCounter wired into tokenCountWithEstimation
* fix(ink): import logForDebugging in App.tsx to prevent ReferenceError
App.tsx used `logForDebugging()` in four call sites (XTVERSION async
handlers, handleReadable/handleDataChunk stdin error-recovery branches)
without importing it. When esbuild bundled this, the unresolved symbol
collided with another identifier in scope; the bundler renamed most
references to `logForDebugging2` but left the four in App.tsx pointing
at the original name, which became undefined in the bundle. At runtime
any modern terminal replying to the XTVERSION probe triggers an
`unhandledRejection: logForDebugging is not defined`.
Adding the missing import resolves the symbol before bundling, so
the bundler emits a single consistent name for every call site.
Refs #825
* fix(hooks): always close stdin after initial hook payload
The conditional `if (!requestPrompt) child.stdin.end()` kept stdin
open in interactive mode (where requestPrompt is always truthy while
the REPL is mounted). Every plugin hook written against the Anthropic
hook input contract reads stdin until EOF, so hooks blocked on the
per-hook timeout (default 60s) on every user message — no HTTP request
to the provider was made until every UserPromptSubmit hook had timed
out. With ~10 plugins hooked to UserPromptSubmit (pipeline-orchestrator,
superpowers, skill-advisor, episodic-memory, reflexion, etc.), a single
prompt accumulated minutes of wait before any model call.
Always closing stdin after the initial JSON payload restores the
documented EOF-based contract. Verified locally: typical `oi`
turnaround drops from ~60s to ~1s.
Trade-off: the existing duplex-stdin path (hooks emitting prompt
requests on stdout and receiving responses written back to stdin at
hooks.ts:1237) is incompatible with an EOF contract by design and
stops working with this change. Restoring that feature requires a
separate IPC channel (named pipe, node IPC, or a second stream)
rather than reusing the initial stdin; that refactor is out of scope
for this fix. Given the blast radius of the current behaviour (every
user with a plugin ecosystem sees unusable interactive mode), trading
the rarely-used duplex path for the documented single-shot contract
is the right short-term move.
Closes#825
Closes#808. Today `openclaude --model <name>` is parsed by Commander
inside main.tsx but the startup banner and any provider-env-reading
code run first, so the override is invisible until request time and
saved-profile users see their stale model in the banner.
Add applyModelFlagFromArgs that runs after saved-profile env
application and before the banner. It routes the value to the env var
matching the already-active provider (OPENAI_MODEL / GEMINI_MODEL /
MISTRAL_MODEL / ANTHROPIC_MODEL) so the banner, resolution, and
request payload all agree. Skipped when --provider is also present;
that path is still handled by applyProviderFlagFromArgs.
No writes to .openclaude-profile.json — override is process-scoped.
This PR addresses three real bugs in WebSearch's provider layer plus adds Brave
as a first-class adapter so users with a BRAVE_API_KEY get auto-detection +
auto-chain inclusion (matching the ergonomics of TAVILY_API_KEY, EXA_API_KEY,
etc.).
## 1. New: Brave first-class adapter
`providers/brave.ts` — auto-detects `BRAVE_API_KEY`, slots into the auto chain
between Jina and Bing. Sends the bare token in `X-Subscription-Token` per
Brave's API contract. Mirrors the structure of `tavily.ts` / `bing.ts`.
Brave runs an independent web index (~30B pages), making it a useful
non-Google, non-Bing fallback. Bing's hosted API was sunsetted in Aug 2025
for new users, so Brave is a more practical default fallback in 2026.
## 2. Bug fix: Brave preset sent malformed auth header
The `WEB_PROVIDER=brave` preset in `custom.ts` declared
`authHeader: 'X-Subscription-Token'` but no `authScheme`, so the default
`'Bearer'` scheme prefix kicked in, producing:
X-Subscription-Token: Bearer <token> ← wrong, returns 401
Brave's API expects:
X-Subscription-Token: <token> ← bare token, no scheme
Fix: declare `authScheme: ''` on the preset; update
`buildAuthHeadersForPreset` to emit a bare token (no leading space) when the
scheme is empty.
## 3. Bug fix: Google preset never worked
`WEB_PROVIDER=google` was wired with `Authorization: Bearer <key>`, but the
Google Custom Search JSON API does not support Bearer auth. It requires:
- `?key=<API_KEY>` as a query param
- `?cx=<ENGINE_ID>` as a query param (Programmable Search Engine ID)
The preset previously had no slot for the engine ID at all, so any user
trying `WEB_PROVIDER=google` hit a 400/401 immediately.
Fix: extend `ProviderPreset` with two minimal fields — `authQueryParam` (key
goes in URL, not header) and `envQueryParams` (additional URL params sourced
from env vars). Rewire the `google` preset to use them; reading
`GOOGLE_CSE_ID` for `cx`.
A clear error fires fast if either `WEB_KEY` or `GOOGLE_CSE_ID` is missing,
instead of silently producing a 400 from upstream.
> Note: Google has announced the Custom Search JSON API will be discontinued
> on 2027-01-01 and is closed to new customers. The fix unbreaks existing
> users for the remaining ~8 months; the README includes a sunset notice and
> recommends Brave/Tavily/Exa for new setups.
## 4. Bug fix: Exa results had empty descriptions
`providers/exa.ts` never passed `contents` in the request body. Per the Exa
docs (https://docs.exa.ai/reference/search-api-guide-for-coding-agents):
> Use `highlights` for agent workflows. Highlights return 10x fewer
> tokens with the most relevant excerpts.
Without `contents: { highlights: true }`, the Exa response includes only
`{title, url, id, ...}` — no `text`, no `highlights`, no `summary`. The
adapter was then mapping `r.snippet ?? r.text` (neither field exists in the
default response shape), so every Exa hit came back with
`description: undefined`. Tavily/Brave/DDG all return snippets — Exa was
silently degraded.
Fix: request `contents: { highlights: true }` in the body, and map
`results[].highlights[]` (an array of strings) into the description by
joining up to 3 excerpts with ` … `. Falls back to `text` when present, then
`undefined` if neither field is populated.
## Tests
105 / 105 pass in `src/tools/WebSearchTool/providers/`:
- `providers/brave.test.ts` (new) — 7 tests: auth header, mapping,
domain filters, error paths
- `providers/exa.test.ts` (new) — 9 tests: contents request shape,
highlights mapping, fallback
chain, error paths
- `providers/custom.test.ts` (extended) — 6 new tests covering
`authScheme: ''` (Brave preset),
`authQueryParam` suppression
(Google preset), GOOGLE_CSE_ID +
WEB_KEY fail-fast errors, full
request shape via mocked fetch
205 / 205 pass across `src/tools/`. tsc clean for changed files.
## Docs
- `README_SEARCH_PROVIDERS.md` — promotes `BRAVE_API_KEY` to first-class,
documents `GOOGLE_CSE_ID` + sunset notice, fixes the provider table,
updates the auto-chain priority list and mode list
- `.env.example` — adds `BRAVE_API_KEY` line, documents `GOOGLE_CSE_ID`
requirement + sunset notice, updates auto-chain priority comment
## Migration / behavior changes
- `WEB_PROVIDER=google` users must now set `GOOGLE_CSE_ID` (was previously
unable to function at all, so this is a strict improvement).
- `WEB_PROVIDER=brave` users with `WEB_AUTH_SCHEME=""` workarounds can
drop the workaround — the preset now emits a bare token by default.
- Brave joins the auto-chain priority order
(firecrawl → tavily → exa → you → jina → **brave** → bing → mojeek →
linkup → ddg).
Cerebras Cloud's chat-completions endpoint rejects requests with a
`store` field — `400 store: property 'store' is unsupported`. The
shim already strips `store` for Gemini hosts via `hasGeminiApiHost`;
add the symmetric host check for Cerebras so users hitting
`api.cerebras.ai` (directly or via the Custom provider preset) don't
hit the same wall.
Closes#1023.
* fix(effort): persist xhigh and send reasoning_effort on chat_completions
Fixes#853.
Persistence:
- EffortPicker.handleSelect normalizes the OpenAI-shaped `xhigh` to the
standard `max` before writing AppState/settings. Previously `xhigh` fell
through `toPersistableEffort` as undefined and the setting reverted on
reload.
- `/effort xhigh` takes the same normalization path.
- `toPersistableEffort` maps `xhigh` -> `max` as a defensive backstop.
- EffortPicker initialFocus reflects the user's stored selection
(`max` shown as `xhigh`) instead of always snapping to the alias default.
Payload:
- openaiShim chat_completions body now emits `reasoning_effort` from
`request.reasoning.effort`. Previously only codex_responses transport
forwarded it, so Custom API users on chat_completions got no effort.
- `getAnthropicClient` accepts `effortValue` and forwards it as
`reasoningEffort` to `createOpenAIShimClient` (both providerOverride
and direct-OpenAI paths) after converting `max` -> `xhigh` at the
OpenAI boundary via `standardEffortToOpenAI`.
- `claude.ts` threads the resolved effort through main streaming and
both non-streaming fallback call sites.
Tests:
- openaiShim.test.ts: reasoning_effort emitted when override is passed,
omitted otherwise, falls back to codex alias default.
- effort.codex.test.ts: xhigh -> max normalization and
standard<->OpenAI conversion.
* fix(effort): skip max→high clamp for OpenAI/Codex models
resolveAppliedEffort() unconditionally downgraded any non-Opus 'max'
to 'high'. OpenAI/Codex models use 'max' as the standard internal form
of 'xhigh' (the client shim converts on the wire), so the clamp was
silently turning xhigh selections into high.
The picker stored xhigh→max correctly and the shim emitted
reasoning_effort, but the resolver in between rewrote max→high before
the shim ever saw it. End result: UI showed xhigh, request sent high.
Skip the clamp when modelUsesOpenAIEffort(model). Anthropic non-Opus
clamp behavior unchanged.
Adds two e2e tests in effort.codex.test.ts walking the full chain
(persist → resolve → wire) so the regression can't recur silently.
* test(effort): keep mocked module surfaces compatible with downstream tests
bun:test's `mock.module()` is process-global and is not undone by
`mock.restore()` (see comment in user.test.ts). When this file mocked
`./auth.js`, `./thinking.js`, `../services/api/providerConfig.js`, etc.
with reduced surfaces, later test files that imported the missing
exports (refreshAndGetAwsCredentials, getRainbowColor, ...) crashed at
module load with `SyntaxError: Export named 'X' not found`.
Spread the real module exports into each mock factory so subsequent
imports keep the full surface, while the targeted overrides for this
file's tests still take effect.
Updated the tool result instructions for async launched agents to explicitly
command the model to end its turn. This prevents the main agent from
prematurely continuing and duplicating work already delegated to a subagent,
reducing redundant costs and improving orchestration reliability.
In commit 1f66d32, require() was changed to createRequire(). While this
likely fixed an ESM warning in tests, createRequire bypasses the Bun bundler.
As a result, teammate.js was no longer inlined into dist/cli.mjs.
At runtime in the published npm package, this resolves to <root>/utils/teammate.js,
but utils/ is not included in the npm files array, causing a crash on startup.
Replaced with a static import. The original comment regarding a circular
dependency is stale, as teammate.ts only relies on an import type from
AppState.ts which gets erased at compile time.
* fix(theme): remove stale React Compiler memo wrappers from theme hooks
Rebase on current main (includes #589 reconciler fix).
The React Compiler memo caches (_c) in useTheme() and usePreviewTheme()
use referential equality checks on destructured context values. These
caches can return stale references when the ThemeProvider's useMemo
recreates the context value object but the individual property
references (setThemeSetting, setPreviewTheme, etc.) compare equal —
the memo short-circuits and returns a cached tuple/object that still
holds the old closure captures.
This is a distinct bug from #589 (which fixed the ink reconciler's
commitUpdate path for host prop updates). #589 ensures that when
React _does_ re-render a component with new props, those props actually
reach the DOM node. But the memo wrappers here prevent React from
_even seeing_ the new context value in the first place — the hook
returns the stale cached result.
Removing the memo wrappers ensures useTheme() and usePreviewTheme()
always read the current context value, eliminating the stale-reference
path entirely.
* test(theme): add regression tests for useTheme()/usePreviewTheme() stale-value bug
These tests verify that context hooks always return fresh values after
ThemeProvider re-renders, even when React Compiler memo caches are in play.
- useTheme() must reflect currentTheme changes immediately after
setThemeSetting is called (not return a stale cached tuple).
- usePreviewTheme() must return functional actions after context
re-renders (not stale closures from before the theme change).
On current main (with _c memo wrappers), these tests expose the bug:
the memo cache compares setThemeSetting by reference (stable across
renders via useMemo) and short-circuits, returning the old cached result
with stale currentTheme.
* fix(test): correct import paths for ThemeProvider.test.tsx
Fix relative paths for ink.js, KeybindingSetup, AppStateProvider,
useStdin mock, systemTheme mock, and config mock to account for
the test file being in src/components/design-system/ rather than
src/components/.
* fix(test): rewrite ThemeProvider tests using Ink renderer
Use Ink's createRoot instead of react-dom/client, matching the pattern
from ThemePicker.test.tsx. The tests now render through Ink's terminal
renderer and check frame output for theme values, which is the same
environment ThemeProvider actually runs in.
* fix(test): correct all relative import paths for design-system/ depth
- ink.js, KeybindingSetup, AppStateProvider: ../ → ../../
- StructuredDiff: same pattern as ThemePicker test adjusted for depth
---------
Co-authored-by: root <root@vm7508.lumadock.com>
* chore: add .openclaude/ to gitignore
The .openclaude/ directory contains auto-generated project-local files
(wiki pages, convention cache, local settings) that should not be
committed to the repository.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* feat: make Codex + GPT 5.5 the default provider and model
Changes the default provider to Codex and default model to GPT 5.5:
- package.json: dev script now uses provider-launch.ts codex
- providerRecommendation.ts: getGoalDefaultOpenAIModel returns gpt-5.5
for coding and balanced goals (was gpt-4o)
- providerConfig.ts: fallback model changed from gpt-4o to codexplan
(resolves to gpt-5.5)
- ProviderManager.tsx: Codex OAuth option now shows green
"★ Recommended" badge in the provider picker
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix: replace Box with nested Text in Codex label
Ink's <Text> component cannot contain <Box>. The label is rendered
inside a <Text> parent, so use nested <Text> elements instead.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix: default to Codex when no provider profile is saved
When no persisted provider profile exists (fresh install / first run),
buildStartupEnvFromProfile now injects Codex + GPT 5.5 env vars instead
of returning process.env unchanged. Falls back gracefully — if Codex
credentials are available (OAuth or existing), uses those; otherwise
injects base URL and model defaults so the provider picker shows
GPT 5.5 as the default.
This closes the gap where node dist/cli.mjs (production start) would
default to firstParty (Anthropic) when no profile or env vars were set.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* chore: resolve stash conflict markers from accidental stash pop
Cleans up merge conflict artifacts left by a git stash pop from an
unrelated branch (chore/add-atomic-chat-partner). Kept upstream
(current branch) version in all cases.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix: restore memoize import and cleanup stash artifacts
Restores the memoize import dropped during conflict resolution in
modelSupportOverrides.ts. Removes duplicate originalEnv declaration
and redundant delete statements in providerValidation.test.ts.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* revert change in package.json
* fix broken test
* fix color
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
`getAttributionHeader()` (src/constants/system.ts) builds an
`x-anthropic-billing-header: cc_version=...; cc_entrypoint=...; ...` line
that gets prepended to the system-prompt block array in
src/services/api/claude.ts:1390-1401. The Anthropic API path needs it
(server _parse_cc_header consumes it), but the OpenAI / Codex shims
joined every text block straight into the outbound `system` /
`instructions` payload, so non-Anthropic providers received an
Anthropic-only billing string in their prompt — token waste, plus a
per-build fingerprint that churns local-model KV cache and any
upstream prompt cache (the slowdown unsloth flagged for Claude Code).
Fix the two `convertSystemPrompt` helpers (openaiShim.ts:241,
codexShim.ts:124) to drop blocks whose text starts with
`x-anthropic-billing-header`. Anthropic-bound traffic is unaffected —
the block is built into Anthropic-shaped requests directly and never
flows through these helpers.
Tests:
- openaiShim.test.ts: e2e capturedBody assertions on chat-completions
+ responses-API paths confirm the line is absent and the rest of the
system prompt survives.
- codexShim.test.ts: convertSystemPrompt is now exported (pure helper)
and unit-tested for array + only-attribution + plain-string cases.
Closes#607.
@mendable/firecrawl-js@4.18.1 (lazy-loaded by WebSearch + WebFetch) requires
Node >=22, and CI runs Node 22 + 24, but package.json still advertised
>=20.0.0. Result: npm install on Node 20 surfaced an EBADENGINE warning that
users routinely ignored, then exploded with a cryptic syntax error the first
time a web tool actually pulled firecrawl in.
Bump engines.node to >=22.0.0 so npm refuses Node 20 up front, and refresh
the stale comment in withResolvers.ts that still pointed at the long-gone
>=18.0.0 baseline.
Closes#1009 (engine half — the EACCES half is standard global-install perms,
not openclaude's to fix).
For openai-shim providers (minimax, moonshot, nvidia-nim, github
copilot, etc.) hasNativeSearchFallback() is false, so when the
DuckDuckGo adapter returns 0 hits in auto mode the call() path
silently falls through to the native Anthropic web_search_20250305
tool. Those providers don't support that tool, producing a silent
"Did 0 searches" with no signal that the default DDG backend is
rate-limited or that no API-key backend is configured.
Convert the silent fallthrough into an actionable result that names
the active provider, the failing backend, and the env vars to set
(FIRECRAWL_API_KEY / TAVILY_API_KEY / EXA_API_KEY / JINA_API_KEY /
BING_API_KEY / MOJEEK_API_KEY / LINKUP_API_KEY / YOU_API_KEY) plus
the native-provider escape hatch (Anthropic / Vertex / Foundry).
Same root-cause family as the catch branch directly below — that
already throws an actionable error for the throw-on-failure case;
this matches it for the success-with-0-hits case.
Auto mode + 0 hits + native fallback available is unchanged: still
falls through so Anthropic/Vertex/Foundry/Codex can serve the
result. Explicit adapter mode is unchanged.
Adds bun:test coverage for the new helpers via the WebSearchTool
__test export.
Refs #614, #994
* feat(sdk): add SDK foundation — type declarations, errors, and utilities
Adds standalone SDK building blocks with no SDK source dependencies:
- sdk.d.ts: ambient type declarations for SDK bundle
- coreSchemas.ts + coreTypes.generated.ts: Zod schemas and generated types
- errors.ts: SDK-specific error classes
- validation.ts: input validation utilities
- messageFilters.ts: extracted message filter logic
- handlePromptSubmit.ts: imports from messageFilters
- 16 generated-types tests
* fix(sdk): narrow assertFunction type from broad Function to callable signature
Code review finding: assertFunction used `asserts value is Function` which
accepts any function-like value without narrowing. Changed to
`(...args: any[]) => any` for better type safety.
* fix(sdk): update sdk.d.ts header — manually maintained, not generated
Reviewer noted the header said "Generated from index.ts" but no generator
produces this file. Updated to "Manually maintained — keep in sync with
index.ts". Drift detection added in validate-externals.ts (PR 3).
* fix(sdk): align sdk.d.ts types with canonical coreTypes.generated.ts
Tighten SDK public type contract to resolve reviewer blockers:
- PermissionResult: unknown[] → precise 6-shape discriminated union
(addRules/replaceRules/removeRules/setMode/addDirectories/removeDirectories)
- SDKSessionInfo: snake_case → camelCase (sessionId, lastModified, etc.)
- ForkSessionResult: session_id → sessionId
- SDKPermissionRequestMessage: uuid + session_id now required
- SDKPermissionTimeoutMessage: added uuid + session_id
- SessionMessage: parent_uuid → parentUuid
- SDKMessage/SDKUserMessage/SDKResultMessage: replaced loose inline
definitions with re-exports from coreTypes.generated.ts
* feat(sdk): wire existing code modules + SDK shared utilities
Modifies core modules for SDK integration:
- QueryEngine, tools, state, commands: SDK type hooks
- SDK shared utilities (shared.ts, permissions.ts)
- 21 SDK tests (shared-utils, permissions)
Stack: main ← pr1-foundation ← pr2-sdk-core
* feat(sdk): add snake_case ↔ camelCase key mapping utilities
casing.ts provides recursive key transformation for the SDK boundary
layer. Internal runtime uses snake_case; public API exposes camelCase.
Will be used by shared.ts, sessions.ts, query.ts at export boundaries.
* test(sdk): add tests for snake_case ↔ camelCase mapping utilities
Covers snakeToCamel, camelToSnake, mapKeysToCamel, mapKeysToSnake
including nested objects, arrays, null/undefined, and round-trips.
* feat(sdk): add SDK runtime — query engine, sessions, build pipeline
Completes the SDK implementation:
- SDK build target (dist/sdk.mjs) with TUI dependency stubbing
- External dependency lists (scripts/externals.ts)
- SDK type generation from Zod schemas (scripts/generate-sdk-types.ts)
- External validation (scripts/validate-externals.ts)
- SDK source: index, query, v2, sessions modules
- agentSdkTypes: re-exports SDK functions (query, createSession, etc.)
- 136 SDK tests + 7 build scanner tests
Stack: main ← pr1-foundation ← pr2-sdk-core ← pr3-sdk-runtime
* fix(sdk): align internal SDK types with camelCase public contract
shared.ts: SDKSessionInfo, ForkSessionResult, SessionMessage fields
now use camelCase matching sdk.d.ts. SDKPermissionRequestMessage and
SDKPermissionTimeoutMessage gain required uuid + session_id fields.
permissions.ts: onPermissionRequest/onTimeout callbacks now include
uuid and session_id in emitted messages.
* fix(sdk): update runtime modules to use camelCase field names
sessions.ts: toSDKSessionInfo outputs camelCase keys, entryToSessionMessage
uses parentUuid, forkSession returns sessionId.
query.ts: reads sessionId from listSessions/forkSession results
instead of snake_case session_id.
* fix(test): update session tests to use camelCase field names
session_id → sessionId in forkSession result assertions and
getSessionMessages calls.
* fix(sdk): prevent permission timeout race condition with once-only resolve wrapper
Add createOnceOnlyResolve utility to prevent double-resolution of promises
when timeout and host response happen simultaneously. This ensures
deterministic behavior in the permission handling flow.
* fix(sdk): improve race condition test robustness
* fix(sdk): handle consecutive underscores in snakeToCamel conversion
Changes:
- Use _+([a-z]) regex to match multiple consecutive underscores before letters
- Add lookahead (?=. ) to preserve underscore-letter pairs at string end
- Handle dunder names (__proto__, __typename) by stripping wrapper and capitalizing
- Add tests for consecutive underscores and trailing underscore preservation
* fix(sdk): include original error message in permission callback denial
When a canUseTool callback throws an error, the catch block now
includes the original error message in the denial message, making
debugging easier for SDK consumers.
* feat(sdk): add optional timeout to env mutex for deadlock prevention
Add timeout parameter to acquireEnvMutex() to prevent infinite waits
in deadlock scenarios. The timeout is optional and defaults to no timeout
(wait forever) for backward compatibility.
Returns a MutexAcquireResult object with acquired status and optional
timeout reason for failed acquisitions.
* fix(sdk): remove timed-out callback from mutex queue to prevent deadlock
* test(sdk): add missing error path and timeout scenario tests
Add tests for timeout scenarios when host doesn't respond to permission
requests, fallback behavior when no onPermissionRequest callback, and
MCP connection edge cases for undefined/empty config.
* fix(sdk): address code review issues - race conditions, validation, error handling
- Add createPermissionTarget() factory that applies onceOnlyResolve at
registration time, fixing race condition where timeout and host response
could both try to resolve the same promise
- Add try-catch to releaseEnvMutex() to prevent permanent lock if callback throws
- Extract DEFAULT_PERMISSION_TIMEOUT_MS constant (30 seconds)
- Add MCP config validation rejecting null, non-objects, and arrays
- Preserve error stack traces in MCP connection failures
- Add runtime validation to mapMessageToSDK for null/non-object/invalid type
- Update tests to use createPermissionTarget and add validation tests
* fix(sdk): syntax fixes and MCP connection error handling
- Remove extra closing parenthesis in permissions.ts
- Remove extra closing braces in shared.ts type definitions
- Wrap MCP connection in try/catch to continue without MCP tools on failure
* fix(sdk): syntax fixes, MCP error handling, and logic clarity
- Remove extra closing parenthesis in permissions.ts
- Remove extra closing braces in shared.ts type definitions
- Wrap MCP connection in try/catch to continue without MCP tools on failure
- Clarify thinkingConfig logic: use ?? true instead of !== false
- Add explanatory comment about thinkingEnabled default behavior
- Apply createOnceOnlyResolve wrapper in QueryImpl.registerPendingPermission
* fix(sdk): comprehensive error handling and resource cleanup
- Add try-catch around injectAgents() to gracefully handle plugin agent
tool validation failures (prevents test crashes from unknown 'LS' tool)
- Add console.warn logging to agent loading/injection catch blocks for
debugging visibility (matches v2.ts pattern)
- Add pendingPermissionPrompts.clear() to close() and interrupt() methods
in both query.ts and v2.ts to prevent memory accumulation
- Add close() method to SDKSession interface and SDKSessionImpl
- Wrap MCP connection in query.ts with try-catch (matches v2.ts behavior)
- Add timeoutQueue cleanup in finally blocks (query.ts + v2.ts)
- Remove error.stack from MCP error messages to prevent internal path leak
All 208 SDK tests pass. TypeScript errors are pre-existing.
* fix(sdk): address code review non-blocking issues
- Add SDKAgentLoadFailureMessage type for agent load failure events
- Emit agent definition/injection failures to SDK message stream
- Add tool name to permission timeout denial message
- Replace 'as any' casts with proper typed state access
- Fix supportedCommands to use correct mcp.commands/plugins.commands paths
- Update test for correct AppState structure
* fix(sdk): address code review blocking and non-blocking issues
Blocking Issues Fixed:
- MCP cleanup missing on session/query close - now disconnects MCP clients
to prevent resource leaks in long-running processes with multiple sessions
- Engine reference not cleared on close - now sets _engine = null to prevent
memory leaks
- Added MCP cleanup tests (9 new tests covering cleanup scenarios)
Non-Blocking Issues Fixed:
- Removed redundant catch block that just rethrew errors (query.ts)
- Fixed inconsistent timeout denial message format (permissions.ts)
- Fixed hardcoded tool name 'Bash' in test (permissions.test.ts)
- Exported PermissionResolveDecision type for SDK consumers (index.ts)
All 217 SDK tests pass.
* fix(sdk): address code review type consistency issues
- Add close() method to SDKSession interface (documented but missing from type)
- Fix SDKSessionInfo, ForkSessionResult, SessionMessage field naming:
snake_case → camelCase to match sdk.d.ts public contract and implementation
- Add uuid and session_id to SDKPermissionTimeoutMessage for correlation
- Fix JSDoc comment in forkSession to use sessionId (not session_id)
These changes align internal types (shared.ts) with the public SDK contract
(sdk.d.ts) and actual implementation output. The merge from origin/main
introduced snake_case types that mismatched camelCase implementation and tests.
* fix: restore openclaude.json comment in REPL.tsx
Merge 0f3aa7a incorrectly took main's side for this comment, reverting
PR2 fix c725c48. Project has migrated to ~/.openclaude.json, not ~/.claude.json.
This is the only PR2 fix lost during merge - all other PR2 fixes
(permissions.ts race conditions, state.ts parentSessionId, etc.)
are preserved in PR3 via subsequent fix commits.
* fix(sdk): add missing type declarations to sdk.d.ts
Add SDKAgentLoadFailureMessage and PermissionResolveDecision to sdk.d.ts
to resolve type declaration drift detected by build validation.
- SDKAgentLoadFailureMessage: Agent loading failure notification
(stage: definitions/injection, error_message)
- PermissionResolveDecision: SDK-specific permission resolution result
(allow with updatedInput, deny with message + decisionReason)
Build validation now passes: 56 exports match between index.ts and sdk.d.ts.
* fix(sdk): resource leak and null safety in close/interrupt paths
- unstable_v2_prompt: wrap session in try/finally to guarantee
session.close() on both success and error paths, preventing
MCP connection and engine resource leaks
- QueryImpl.interrupt(): add null guard on _engine so calling
interrupt() after close() is a safe no-op instead of throwing
- SDKSessionImpl.interrupt(): add matching null guard for v2
sessions, consistent with the Query fix
- QueryImpl.close(): call this.interrupt() before cleanup to
properly stop in-flight engine operations, matching v2's close()
pattern and ensuring engine.interrupt() runs before nulling
* fix(sdk): abort AbortController in SDKSessionImpl.close() to prevent resource leak
SDKSessionImpl.close() was not aborting the AbortController, unlike
QueryImpl.close() which does. This meant in-flight HTTP requests and
async operations could continue running after session closure.
- Store AbortController reference via _abortController field + late-bind setter
- Abort and null the controller in close(), mirroring QueryImpl pattern
- Also null _appStateStore in close() to release state snapshots
- Wire abortController through createEngineFromOptions return value
* fix(sdk): index ALL entries in byUuid for compact preserved segment
The byUuid map must index system compact_boundary entries, not just
user/assistant. When anchorUuid === boundary.uuid, the relink walk
needs to find the boundary in byUuid.
Changes:
- query.ts: Index ALL non-sidechain entries (user, assistant, system)
- v2.ts: Same fix — index ALL entries, leaf selection user/assistant only
- Add regression test: boundary.uuid as anchorUuid scenario
Test verifies preserved messages kept, stale pre-compact dropped,
post-boundary chain intact when anchorUuid points to boundary itself.
* fix(sdk): complete preserved segment handling for compact resumes
Multiple fixes for compact-aware transcript loading:
1. Index ALL entries in byUuid (including system compact_boundary)
- Needed when anchorUuid === boundary.uuid
2. Keep anchorUuid when pruning preserved segment entries
- The anchor is the parent of preserved head after relink
- Deleting it breaks the conversation chain
3. Filter system entries from final messages
- compact_boundary is metadata, shouldn't pass to engine
4. Fix test timestamp format (ISO 8601 requires 2-digit hours)
- '2025-01-04T0:00:00Z' → '2025-01-04T00:00:00Z'
5. Update test expectations for anchor inclusion
- When anchor is a stale entry, it appears in messages
- preserved(4) + anchor(1) + post(4) = 9 max
All 224 SDK tests pass.
* fix(sdk): MCP type:sdk tools properly convert SdkMcpToolDefinition to Tool
- Import MCPTool base from tools/MCPTool/MCPTool.js
- Spread MCPTool properties for proper Tool interface compliance
- Add tools field to SdkMcpSdkConfig type declaration
- Add regression tests for type:sdk tools wiring
Fix ensures in-process SDK tools match Tool interface expected
by QueryEngine and permission handlers.
* test(sdk): strengthen preserved segment and MCP tools tests
Preserved segment test improvements:
- Fix content extraction (access message.content, not message)
- Add exact count assert: messages.length === 6
- Add exact content asserts: preserved turn 1/2, post-boundary present
- Assert no stale, no system entries in final messages
MCP tools test additions:
- Direct test of connectSdkMcpServers() function
- Assert clients.length === 0 (in-process, no MCP connections)
- Assert tools.length === 1 with proper name/description
- Verify handler works via direct call (not via Tool.call which needs context)
* fix(sdk): published types complete, init errors fatal, permission session IDs
Three fixes for SDK production readiness:
1. HIGH: Published SDK types incomplete
- Add coreTypes.generated.d.ts to package.json "files" array
- sdk.d.ts re-exports from ./sdk/coreTypes.generated.js which was missing
- TypeScript consumers would get module resolution errors
2. MEDIUM: query() swallows real init() failures
- Add _engineWasInjected field to track pre-injected vs fresh engine
- Check _engineWasInjected, not _engine !== null (always true after setEngine)
- Auth/config/init errors now properly fatal for normal query() calls
3. MEDIUM: SDK permission events lose real session id
- Pass sessionId to createExternalCanUseTool() in both query.ts and v2.ts
- Permission_request/timeout messages now have correct session_id
- Hosts can correlate permission callbacks to sessions
Test result: 225 pass, 0 fail
* fix(sdk): complete package types + dynamic permission session_id
Two fixes for SDK production readiness:
1. Published SDK types now include actual definitions
- Replace 215-byte wrapper with 63KB coreTypes.generated.ts
- TypeScript consumers get full type definitions (SDKMessage, etc.)
- npm pack now includes real generated types
2. Permission event session_id dynamic for all query() paths
- createExternalCanUseTool accepts string | (() => string | undefined)
- query.ts passes () => queryImpl.sessionId getter
- Fresh/fork/continue queries emit correct session_id at event time
- V2 passes static sessionId (stable at creation/resume)
- Add 4 tests: static sessionId, getter resolution, undefined fallback, timeout
Test result: 229 pass, 0 fail
* fix(sdk): fix sdk.d.ts for real TypeScript consumer compilation
Two issues prevented external consumers from compiling against packed SDK types:
1. SDKRateLimitError used constructor parameter properties (readonly resetsAt,
readonly rateLimitType) which are invalid in .d.ts declarations — moved to
class properties with separate constructor signature.
2. Re-exported SDKMessage/SDKUserMessage/SDKResultMessage were not imported
into local scope — added import type alongside export type so TypeScript
can resolve them for use in other declarations within the same file.
Added package-consumer-types.test.ts that compiles a real temp project against
the SDK types with skipLibCheck:false, catching both regressions.
* fix(sdk): eliminate React/Ink imports from SDK bundle
SDK bundle leaked React/Ink imports via tool UI modules, keybindings,
react-compiler-runtime, and spawnMultiAgent's static React import.
Changes:
- Stub root ink.js barrel, tool UI.js, keybindings/, react-compiler-runtime,
It2SetupPrompt, and React hook files in SDK build
- Add local no-op stub for react/jsx-dev-runtime (jsxDEV returns null)
- Convert spawnMultiAgent's static React/It2SetupPrompt imports to dynamic
await import() — spawnTeammate logic stays fully intact
- Add post-build leakage validation (fails on from "react"/"ink"/jsx-dev-runtime)
- Remove react/jsx-dev-runtime from SDK externals (now handled by build plugin)
* fix(sdk): wire disallowedTools through permission context
QueryOptions.disallowedTools was declared but never used. buildPermissionContext()
now passes it to alwaysDenyRules.cliArg so getTools() filters denied tools from
the model-visible list. Also added to V2 SDKSessionOptions for API consistency.
* fix(sdk): defer permission warning to execution time
createDefaultCanUseTool() warned at construction time even when the caller
provided canUseTool/onPermissionRequest. Move warning to first actual default
denial so valid SDK consumers never see false warnings. Add tests for
disallowedTools filtering, tool exclusion, and warning timing.
* refactor(sdk): extract transcript helpers + fix permission typing
- Extract shared transcript utilities to transcript.ts
(parseJsonlEntries, findLastCompactBoundary, applyPreservedSegmentRelinks,
buildConversationChain, stripExtraFields) deduplicating query.ts and v2.ts
- Add PermissionTarget interface to hide internal pendingPermissionPrompts
map from createExternalCanUseTool, with deletePendingPermission and
denyPendingPermission methods on QueryImpl and SDKSessionImpl
- Fix sessionId stability: preserve constructor UUID for fresh queries
when continue:true finds no existing sessions, and when explicit
sessionId does not resolve to a valid transcript file
- Add getMcpClients/setMcpClients to QueryEngine for SDK cleanup access
* fix(sdk): resolve remaining TypeScript errors in SDK modules
- Fix PermissionDecision type compatibility: import from types/permissions
and cast PermissionResolveDecision to PermissionDecision properly
- Fix AsyncIterator/AsyncGenerator: async generators must return
AsyncGenerator (which implements AsyncIterable), not AsyncIterator
- Fix Map method callable errors: cast additionalWorkingDirectories
to Map<string, unknown> before calling .set() and .keys()
- Fix ApiKeySource type: map internal ApiKeySource to SDK's narrower
type using conversion function, spread info before apiKeySource
to avoid override
- Fix MCP config scope type: cast 'session' scope to ScopedMcpServerConfig
for connectToServer compatibility
- Add PermissionMode import and cast for decisionReason.mode
- Deny pending permissions in interrupt(): resolve all pending promises
with deny before clearing the map (both query.ts and v2.ts)
* fix(sdk): correct init skip logic and test mocks
- query.ts: skip init() entirely for injected engines (mocks, SDK host
overrides) instead of calling init() and swallowing errors. Pass
{ injected: false } from query() factory to distinguish real engine
from test mocks.
- mock-engine.ts: add getMcpClients() and setMcpClients() methods to
match QueryEngine API added in this PR.
- permissions.test.ts: use filterToolsByDenyRules instead of getTools
for disallowedTools tests, with proper base tool fixtures.
* fix: address code review feedback for exports and build script
package.json exports (Breaking Change Mitigation):
- Add "./package.json": "./package.json" for tool compatibility
- Add "./dist/cli.mjs": "./dist/cli.mjs" for CLI bundle access
- Keep ./sdk as sole library entrypoint
- Root import intentionally blocked (CLI-first package, no main field)
build.ts (Bug Fix):
- Add | undefined to result/sdkResult type declarations
- Add optional chaining: result?.success, sdkResult?.success
- Prevents TypeError masking actual build errors when Bun.build throws
tests/sdk/package-consumer-types.test.ts:
- Update simulated exports to match real package.json
- Add tests verifying exports map structure and file existence
---------
Co-authored-by: Ali Alakbarli <ali.alakbarli@users.noreply.github.com>
* feat: support self-hosted Firecrawl via FIRECRAWL_API_URL
Adds FIRECRAWL_API_URL env var to enable self-hosted Firecrawl
instances. Both WebFetchTool and firecrawl search provider now check
for either FIRECRAWL_API_KEY (cloud) or FIRECRAWL_API_URL (self-hosted).
The FirecrawlClient accepts apiUrl for custom endpoints.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove incorrect non-null assertion on FIRECRAWL_API_KEY
Passing undefined to FirecrawlClient.apiKey is correct when using
FIRECRAWL_API_URL without an API key. Also adds regression tests for
isConfigured() covering all four env combinations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: trigger CI
---------
Co-authored-by: Kunthawat Greethong <kunthawat@gmailcom>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: context partitioning and relevance-based pruning
PR 2B - Section 2.3, 2.4:
- Add contextPartitioning.ts with priority zones
- Add relevancePruning.ts with keyword overlap scoring
- Add comprehensive tests (13 passing)
* fix: resolve PR 849 blocking issues
- Fix older system messages now appended in partitionContext()
- Fix getAvailableSpace() accepts contextWindow param
- Fix hasToolCalls/hasErrors() handle structured content blocks
- Wire helpers into autoCompact.ts
* fix: resolve PR 849 remaining blockers
- Preserve system messages in pruning (not dropped)
- Group messages by id to preserve transcript pairs
- Add test for message ID group preservation
* fix: rewrite pruneByRelevance to use API-round grouping - preserves tool_use/tool_result pairs
- Add groupMessagesByApiRound() matching repo invariant from grouping.ts
- Groups both recent and older messages at assistant message.id boundaries
- Ensures tool_use + tool_result pairs stay together, not split across prune boundary
- Add test for API-round grouping with real tool_use + tool_result transcript shape
- disable /feedback and /mobile from command availability while keeping implementation code in place
- remove or rewrite lingering user guidance that pointed to /feedback or /mobile
- switch HelpV2 to a public build version helper and fix the help dialog wrapper regression
- update OpenClaude-facing links and prompt copy for issue reporting and branding consistency
- StartupScreen.test.ts: Scope settings module mock to prevent process-global pollution.
- providerConfig.github.test.ts: Explicitly scrub environment variables before each test.
- providerProfiles.test.ts: Add beforeEach to clear RESTORED_KEYS and ensure fresh registry state.
- providerValidation.test.ts: Ensure integrations registry is loaded and scrub environment to prevent leakage from host shell.
This consolidates fixes from PR 944 and addresses additional failures found after rebasing onto main (v0.8.0).
Co-authored-by: Kevin Codex <kevin@gitlawb.com>
* feat: rework release notes around GitHub releases
- switch /release-notes from upstream changelog parsing to OpenClaude GitHub release data
- add a public build version helper and use it for release URL/seen-version tracking
- render release notes in-app with section headers like Features and Bug Fixes
- cache serialized GitHub release notes locally for startup and command fallback paths
- preserve snake_case identifiers while sanitizing markdown content
- keep LogoV2 whats-new output within its display budget
- add focused tests for parsing, formatting, version tags, and display slicing
* Normalize release-please changelog versions
Use normalizePublicVersion when parsing cached changelog headings so release-please markdown headings like ## [0.8.0](...) (2026-05-02) map to the expected version key.
Also normalize direct version lookups consistently and add a regression test covering getReleaseNotesForVersion and getRecentReleaseNotes against the new CHANGELOG.md format.
* fix(startup): make CLAUDE logo D distinct
Adjust the startup ASCII logo so the D in CLAUDE no longer reads as an O, and add a focused regression test for the rendered shape.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(startup): clear CI flag for logo rendering assertion
Ensure the startup logo regression test exercises the interactive render path under GitHub Actions, where CI is set by default.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
When a user runs openclaude with a third-party provider, project-scope
MCP servers added via `openclaude mcp add -s project ...` were silently
dropped from `/mcp` and `openclaude mcp list`. Re-adding the same
server printed "MCP server already exists in .mcp.json" but the server
never actually loaded. Reporter @gbmerrall pinpointed the cause to the
`if (usesAnthropicSetup)` gate around `handleMcpjsonServerApprovals`
in src/interactiveHelpers.tsx.
The MCP approval dialog and the CLAUDE.md external-includes warning
are about workspace trust, not about Anthropic auth. Gating them on
`usesAnthropicSetup` meant 3P-provider users never saw the dialog
that writes `enableAllProjectMcpServers: true` and
`enabledMcpjsonServers: [...]` to settings.local.json — without
those settings, the MCP server isn't loaded for use.
Drop the `usesAnthropicSetup` gate around the approval flow. The
inner logic is unchanged and `handleMcpjsonServerApprovals` already
early-returns when no servers are pending, so users without project
`.mcp.json` see no new behavior.
- src/interactiveHelpers.tsx: drop the gate, add a comment explaining
why (and cite #696 so future readers can find context).
- src/__tests__/bugfixes.test.ts: +2 regression tests asserting the
gate is gone and the issue is referenced.
Verified locally on Linux: build passes (v0.7.0), 1634 tests pass,
the 4 remaining failures (StartupScreen.test.ts, thinking.test.ts)
reproduce on main and are unrelated. The bundled dist/cli.mjs shows
`handleMcpjsonServerApprovals(root2)` running directly after
`setSessionTrustAccepted(true)` with no auth gate.
* fix(provider): apply Codex OAuth session switch correctly
Ensure Codex OAuth activation in an existing session does not briefly apply an empty OpenAI API key, preventing missing Authorization headers until restart.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(provider): preserve explicit env for Codex API profiles
Limit the Codex session-switch override to OAuth profiles so explicit OpenAI environment settings keep taking precedence for regular Codex profiles.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(provider): isolate Codex OAuth env from ambient credentials
Keep Codex OAuth profile activation from inheriting ambient Codex API credentials so CI and user shells cannot poison the in-session OAuth regression path.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Network/transport failures from custom OpenAI providers (e.g. ENETDOWN,
EAI_AGAIN, generic fetch failed) were thrown via APIError.generate(503,
...). The OpenAI SDK formats the error message as "${status} ${message}",
so users saw output like:
503 OpenAI API transport error: fetch failed (code=ENETDOWN)
This is misleading: a 503 implies the upstream server returned Service
Unavailable, but no HTTP response was received at all -- the failure
happened at the transport layer.
Use APIError.generate(0, ...) instead. The SDK's own factory returns
APIConnectionError for status 0, which is the semantically correct class
for "no response received" and produces a message without the spurious
"503" prefix. APIConnectionError extends APIError, so existing
`instanceof APIError` branches in errors.ts and elsewhere keep working.
Add a regression test that asserts the constructed error is an
APIConnectionError, has no status, and the message does not start with
"503".
Split Anthropic-owned endpoint user agents from provider-routed API traffic.
Keep compatibility-sensitive Anthropic requests on claude-cli/99.0.0 while using openclaude-cli with the current public build version for non-first-party/OpenAI-compatible connections.
Wire getAnthropicClient() to pass its providerOverride-aware first-party decision into the user-agent helper so per-request routing stays correct.
Expand focused tests to cover first-party compatibility traffic, Anthropic-owned endpoints under third-party env, non-first-party provider traffic, and explicit first-party override behavior.
Keep compatibility-sensitive claude-cli/claude-code user agents on MACRO.VERSION while adding a shared public build version helper for MCP-facing metadata.
Update MCP client metadata and MCP user-agent strings to report DISPLAY_VERSION where appropriate without reintroducing first-party version-gate regressions.
Add focused regression tests covering:
- public build version helper behavior
- API client user-agent compatibility behavior
When /statusline (or any AgentTool path that creates a worktree) hits
`git rev-parse HEAD` failure during base-branch resolution, the
previous error swallowed git's stderr and reported only:
Failed to resolve base branch "HEAD": git rev-parse failed
That message gives users no way to distinguish empty repos
('unknown revision or path'), detached HEADs pointing at missing
objects, or a missing git binary on PATH — all surfaced identically.
Extract the message construction into buildRevParseFailureMessage()
and include git's stderr in the thrown Error. When the failing ref is
literally 'HEAD' (the fallback path when fetching origin/<branch>
fails), append a short hint about the most common cause (no commits)
and PATH check.
Adds focused tests in worktree.test.ts covering the empty-repo case,
the empty-stderr fallback (exit code), the branch-ref path (no HEAD
hint), and stderr whitespace trimming.
Fixes#690
Co-authored-by: 0xfandom <nikhoriariteish@gmail.com>
Git 2.30+ refuses to start when any environment value contains a NUL,
CR, or LF character ("Unsafe environment: control characters are not
allowed in values"). User shells frequently leak such values — a
copy-pasted API key with a trailing newline, a terminal-set variable
with embedded escape sequences — which made every /plugin marketplace
add and /plugin install fail with that error before git even ran.
Add a small shared helper that builds the env passed to git child
processes and drops keys whose name or value contains a control
character. The legacy GIT_NO_PROMPT_ENV overrides (terminal prompt
disabled, askpass cleared) move into the same helper. Apply it to
every git invocation in marketplaceManager.ts (5 sites: gitPull,
gitClone, sparse-checkout, post-sparse checkout, reconcileSparseCheckout)
and pluginLoader.ts (8 sites: clone, fetch, checkout in both gitClone
and installFromGitSubdir).
A debug-level warning is logged once per process listing the dropped
key NAMES (not values) so the user can clean them up in their shell.
- src/utils/plugins/gitEnv.ts (new): sanitizeEnvForGit + buildGitChildEnv
- src/utils/plugins/gitEnv.test.ts (new): 10 unit tests covering CR/LF/NUL
in values, control char in key name, undefined values, defaults,
extras override
- src/utils/plugins/marketplaceManager.ts: replace 5 inline env spreads
with buildGitChildEnv()
- src/utils/plugins/pluginLoader.ts: pass env: buildGitChildEnv() to 8
git exec sites that previously inherited process.env unfiltered
Verified locally on Linux: before fix, git --version with a leaked
control-char env value fails with "Unsafe environment"; after fix it
runs cleanly. Live marketplaceManager.gitClone against a real GitHub
repo with the same leaked env succeeds and the repo is materialized
on disk.