Since 354feb48 (#1856) mapped react/jsx-dev-runtime to React's production
file, the CLI launched to the startup banner and then rendered no UI at
all — no prompt box, no typing, no visible error.
Root cause: Bun transpiles our JSX with the dev transform (no
NODE_ENV=production at build time), so every JSX callsite compiles to
jsxDEV(). React's react-jsx-dev-runtime.production.js deliberately exports
`jsxDEV: undefined` (production bundles are expected to use the non-dev
transform), so every element creation invoked undefined() and React never
committed a single frame. Nothing surfaced because the failure happens
while building the element tree, before the renderer's error callbacks.
Fix: map react/jsx-dev-runtime to a local shim that dispatches jsxDEV onto
the production jsx/jsxs — the same dispatch React's own dev runtime
performs, minus dev-only validation. The shim's own react/jsx-runtime
import is remapped by the plugin, so the bundle stays all-production
(memory goal of #1856 intact): react, jsx-runtime, reconciler, constants,
and scheduler all resolve to .production.js, with no development copies.
Regression tests (per review) pin the shim's dispatch: jsxDEV must exist,
route to jsx/jsxs on isStaticChildren, pass the key through, re-export the
real Fragment, and produce the standard element shape — compared directly
against the real react/jsx-runtime exports so the tests track React.
Verified live in the TUI (tmux): prompt box renders, typing echoes, slash
menu opens and filters, Esc dismisses; sourcemap shows only production
React modules plus the shim.
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
* feat(ux): honest feedback pass — visible retries, statusline truncation marker, hint grace period
Three fixes with one principle: never look frozen, never silently hide state.
- SystemAPIErrorMessage: retries were fully hidden until attempt 4, so
transient rate limits / overloads were indistinguishable from a hang.
Attempts 1-3 now render a compact dim line ("Rate limited — retrying
in 4s… (attempt 2/10)") with a live countdown; the full error block
is unchanged at attempt >= 4. The transcript already keeps only the
last api_error message and hides it on the next non-error message,
so early visibility adds no stacking. New briefAPIErrorReason()
classifies 429/529/5xx/connection failures, including the
OpenAI-compat shim's plain-text transport errors that carry no cause
chain. Component rewritten from react-compiler output to plain React.
- BuiltinStatusLine: fitSegments dropped rate-limit -> cost -> context
silently on narrow terminals. Segments now degrade to short forms
first (ctx 37% -> 37%, $1.23 -> $1), and anything still dropped is
marked with a trailing dim "…" so hidden data is visible as hidden.
The marker is best-effort: at extreme widths the bare model name
beats showing nothing.
- PromptInputFooter: "? for shortcuts" was suppressed whenever a status
line rendered — the default state since the builtin statusline
shipped, killing the hint's discoverability path entirely. New users
(numStartups <= 10) keep the hint alongside the status line;
established users get the quieter footer.
- docs: BASH_MAX_OUTPUT_LENGTH env var documented on the website env
reference (default 30000, cap 150000).
Verified live in the TUI (tmux + mock OpenAI endpoint): compact retry
line from attempt 1 against a dead endpoint, full block at attempt 4,
Esc interrupts cleanly; statusline at 100/32/24 cols shows full /
degraded / "test-model · 2% · …"; hint present at numStartups=2,
suppressed at 50.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
* fix(review): address CodeRabbit feedback — custom statusline yields immediately, timeout test, docs wording
- shouldSuppressShortcutsHint: a custom status line is explicit user
configuration, so it now always wins over the discoverability grace
period; only the builtin status line grants new users the hint.
Test added to lock the semantics.
- Test the ETIMEDOUT -> "Request timed out" branch. Note: CodeRabbit's
suggested test shape (plain object with a cause) would not exercise
the branch — extractConnectionErrorDetails only walks Error
instances — so the error itself carries the code.
- Docs: soften "full output saved" to reflect the persisted-file cap.
Co-Authored-By: OpenClaude <openclaude@gitlawb.com>
---------
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
* fix(clipboard): use .NET Clipboard.GetImage() for Windows raw bitmap paste
Fixes#1844
Replace Get-Clipboard -Format Image with System.Windows.Forms
Clipboard.GetImage() / ContainsImage() on Windows. The old PowerShell
cmdlet does not properly convert raw DIB/CF_BITMAP data placed on the
clipboard by PrintScreen and Win+Shift+S (Snipping Tool). The .NET API
natively handles all clipboard image formats.
Also enables hasImageInClipboard() on Windows so the 'Image in
clipboard' hint notification fires when the terminal regains focus.
Changes:
- Extract WIN32_CLIPBOARD_HAS_IMAGE_CMD shared constant
- Update getClipboardCommands() win32 checkImage and saveImage
- Add Windows path to hasImageInClipboard()
- Add clarifying comment on checkImage exit code behavior
- Add unit tests for exported imagePaste functions
* test(clipboard): make Windows path assertion platform-aware
* fix(clipboard): address Windows image paste review findings
* test(clipboard): cover Windows image paste success path
* test(clipboard): expect Windows image dimensions
* test(clipboard): allow optional image dimensions
* test(clipboard): assert mocked image dimensions
* fix: add governance controls for memory and git attribution
Default persistent memory writes to explicit approval unless memory.requireApprovalBeforeWrite is disabled. Skip automatic memory extraction/dream writes while approval is required, avoid auto-creating memory directories in that mode, and surface approval prompts for auto-memory file writes.
Make generated commit and PR attribution opt-in through git.addAICoAuthor and git.addGeneratedWithFooter while preserving explicit custom attribution. Add forbidden commit-message patterns and surface them in commit prompts.
Enforce commit-message governance in Bash and PowerShell permission checks, including global git options, file-backed messages, shell wrappers, env split strings, chained commands, and PowerShell call/operator forms. Add regression coverage for the policy paths.
* test: isolate governance policy settings state
* fix: address governance policy review findings
* fix: require approval for overridden memory writes
* fix: fail closed on expandable commit messages
Treat Bash and PowerShell expandable commit-message sources as uninspectable when commit-message governance policy is active. This prevents runtime-expanded variables, command substitutions, and expandable here-strings from bypassing forbidden attribution checks while preserving literal quoted heredocs/here-strings.
Validation: bun test src/tools/BashTool/bashSecurity.test.ts src/tools/BashTool/bashPermissions.test.ts; bun test src/tools/PowerShellTool/powershellPermissions.test.ts; bun test src/tools/BashTool/bashSecurity.test.ts src/tools/BashTool/bashPermissions.test.ts src/tools/BashTool/readOnlyValidation.test.ts src/tools/BashTool/utils.test.ts src/tools/PowerShellTool/powershellPermissions.test.ts src/utils/permissions/filesystem.test.ts src/utils/permissions/permissions.test.ts src/memdir/paths.test.ts; bun run typecheck; bun run smoke; bun run security:pr-scan -- --base origin/main --head HEAD
* fix: fail closed on unquoted expandable commit messages
Extend Bash and PowerShell commit-message governance to treat unquoted expandable message tokens as uninspectable under active policy. This covers Bash forms like `git commit -m fix` and PowerShell forms like `git commit -m `, alongside the quoted and heredoc cases already covered.
Validation: bun test src/tools/BashTool/bashSecurity.test.ts src/tools/BashTool/bashPermissions.test.ts; bun test src/tools/PowerShellTool/powershellPermissions.test.ts; bun test src/tools/BashTool/bashSecurity.test.ts src/tools/BashTool/bashPermissions.test.ts src/tools/BashTool/readOnlyValidation.test.ts src/tools/BashTool/utils.test.ts src/tools/PowerShellTool/powershellPermissions.test.ts src/utils/permissions/filesystem.test.ts src/utils/permissions/permissions.test.ts src/memdir/paths.test.ts; bun run typecheck; bun run smoke; bun run security:pr-scan -- --base origin/main --head HEAD
* fix: fail closed on unquoted cat substitutions
Treat plain unquoted command substitutions like git commit -m $(cat .git/OPENCLAUDE_COMMIT_MSG) as uninspectable while still letting the heredoc-specific parser handle literal cat heredoc commit messages.
Validation: bun test src/tools/BashTool/bashSecurity.test.ts src/tools/BashTool/bashPermissions.test.ts; bun test src/tools/BashTool/bashSecurity.test.ts src/tools/BashTool/bashPermissions.test.ts src/tools/BashTool/readOnlyValidation.test.ts src/tools/BashTool/utils.test.ts src/tools/PowerShellTool/powershellPermissions.test.ts src/utils/permissions/filesystem.test.ts src/utils/permissions/permissions.test.ts src/memdir/paths.test.ts; bun run typecheck; bun run security:pr-scan -- --base origin/main --head HEAD; bun run smoke
* fix: honor PR footer governance opt-in
Make getEnhancedPRAttribution use the same generated PR attribution opt-in and block semantics as getAttributionTexts, so git.addGeneratedWithFooter enables the enhanced PR footer path and false blocks legacy generated PR attribution.
Validation: bun test src/utils/attribution.test.ts; bun test src/utils/governancePolicy.test.ts src/utils/attribution.test.ts src/commands/commit-push-pr.test.ts (commit-push-pr test path absent, 32 tests ran); bun run typecheck; bun run security:pr-scan -- --base origin/main --head HEAD; bun run smoke; git diff --check
* fix: fail closed on bare git commit governance
Recognize bare git commit as an uninspectable commit-message source in both Bash and PowerShell governance checks, so active forbidden-pattern policy prompts instead of passing through the editor/template path.
Validation: bun test --feature=UNATTENDED_RETRY src/tools/BashTool/bashSecurity.test.ts src/tools/BashTool/bashPermissions.test.ts src/tools/PowerShellTool/powershellPermissions.test.ts; bun run typecheck; bun run smoke; bun run security:pr-scan -- --base origin/main --head HEAD; git diff --check
---------
Co-authored-by: jatmn <jatmn@users.noreply.github.com>
normalizeLegacyToolName looked up a caller-supplied tool name in the plain
object LEGACY_TOOL_NAME_ALIASES with a bare bracket access. Names that collide
with Object.prototype members — `constructor`, `toString`, `valueOf`,
`hasOwnProperty`, `__proto__` — resolved to the inherited function/object,
and the `?? name` fallback never fired because that value is non-null.
The function is declared to return `string` but returned a function for those
inputs, breaking every caller: availableToolNames.has() strips a tool named
`constructor` from tool_reference blocks, hook matcher `===` comparisons
misbehave, and permission rules like `hasOwnProperty(...)` parse to a function
as toolName. Guard the lookup with Object.hasOwn so only own aliases match.
Adds proto-name coverage for normalizeLegacyToolName and
permissionRuleValueFromString.
* fix(codex): make Codex alias lookup prototype-safe
CODEX_ALIAS_MODELS is a plain object literal, and the three alias lookups
(isCodexAlias, parseModelDescriptor x2) keyed it directly with a
config/CLI-controlled model string. Because the string is lower-cased first,
the already-lowercase inherited names `constructor` and `__proto__` resolve
through Object.prototype: `key in map` returns true and `map[key]` returns a
truthy inherited value.
Effect: isCodexAlias('constructor') returns true, so with no explicit base URL
shouldUseCodexTransport misroutes the request through the Codex transport; and
parseModelDescriptor('constructor') passes its `if (aliasConfig)` guard and
returns baseModel = undefined, which then flows into API-name resolution.
Guard each read with Object.hasOwn so only own alias entries match. Keeps the
`as const` map (and the CodexAlias keyof type) intact, unlike a null-proto
rewrite. Adds a regression test covering both reachable proto keys plus
positive controls.
* fix(codex): guard the reasoning-effort alias lookup too; cover the descriptor path
getReasoningEffortForModel indexed CODEX_ALIAS_MODELS without an own-property
check — the fourth alias lookup surface, alongside the two parseModelDescriptor
branches and isCodexAlias. It feeds supportsCodexReasoningEffort, /effort, and
EffortPicker, so an inherited property matching a model id could be read as a
Codex reasoning default. Apply the same Object.hasOwn guard so all four lookup
sites are protected together.
Extend the proto-alias test:
- a polluted-prototype case (plant an alias on Object.prototype, cleaned up in
finally) that proves getReasoningEffortForModel / supportsCodexReasoningEffort
ignore inherited entries — this red-greens the new guard, which constructor/
__proto__ can't since they carry no reasoningEffort.
- resolveProviderRequest cases for `constructor` / `__proto__`, exercising the
parseModelDescriptor path so its guards can't be silently removed (a missing
guard makes descriptor.baseModel undefined → resolvedModel wrong).
* test(codex): red-green the descriptor query-branch alias guard
The proto-alias coverage exercised only the no-query parseModelDescriptor
read. Add a query-branch case that plants an inherited alias whose .model
differs from the key and asserts a `<key>?reasoning=medium` request keeps
the literal base model, so removing the own-property guard from the
`baseModel?reasoning=...` read now fails the suite.
parseRawDiffToToolUseDiff skipped hunk lines matching `startsWith('+++')` /
`startsWith('---')`, intending to ignore the `+++ b/file` / `--- a/file`
headers. But those headers only appear before the first `@@`, so they never
reach the counter (which is already gated on `inHunks`). Inside a hunk the
guards instead dropped genuine content whose text begins with `++`/`--` — a
YAML `---` separator, `+++quiet-flag`, `---legacy-peer-deps`, a Markdown rule —
undercounting additions/deletions/changes in the tool-result gitDiff payload.
Remove the redundant guards so every in-hunk `+`/`-` line is counted. This is
the same defect the sibling parseGitDiff already documents and guards against
with its `!currentHunk` check; parseRawDiffToToolUseDiff had the latent
instance. Export it and add a regression covering the `--`/`++` content lines.
validateOfficialNameSource gated the reserved-name git-URL path on
`url.includes('github.com/anthropics/')` / `url.includes('git@github.com:anthropics/')`.
A substring match also accepts URLs whose host is not github.com or where that
text sits in the path — e.g. `https://notgithub.com/anthropics/x`,
`https://evilgithub.com/anthropics/x`, or `https://evil.com/github.com/anthropics/x`.
An attacker could register a repo they control under a reserved official
marketplace name and have it validated as official.
Parse the URL instead and require the exact host `github.com` with the official
org as the first path segment. Handles https/http, `ssh://`, and scp-like SSH
(`git@github.com:anthropics/...`) forms, case-insensitively. The GitHub `repo`
branch already anchored with startsWith and is unchanged.
Adds a regression covering the real bypasses plus genuine official URLs and
wrong-org rejection.
* fix(model): resolve [1m]-tagged aliases when 1M context is disabled
parseUserSpecifiedModel gated stripping of the [1m] tag on has1mContext,
which returns false when CLAUDE_CODE_DISABLE_1M_CONTEXT is set (the
C4E/HIPAA admin control). With the flag on, an aliased request like
'sonnet[1m]' kept the tag attached, never matched the 'sonnet' alias, and
returned the literal unservable string 'sonnet[1m]' — so disabling 1M
broke [1m]-aliased model selection entirely instead of gracefully serving
the base model.
Separate 'tag is present in the input' (always strip before matching) from
'1M is active' (re-append the suffix). When 1M is disabled, the tag is
dropped and the alias/custom id resolves to its base model. Covers the
alias and custom-model paths; the ant path already stripped unconditionally.
Adds regression coverage for both the disabled (tag dropped, base resolved)
and enabled (tag preserved) directions.
* fix(model): drop disabled [1m] from custom default alias targets
The alias branches resolved to getDefaultSonnetModel()/getDefaultOpusModel()
etc. and appended the tag, but a custom default override such as
ANTHROPIC_DEFAULT_SONNET_MODEL=MySonnetDeploy[1m] bakes the suffix into the
resolved value. With CLAUDE_CODE_DISABLE_1M_CONTEXT=1 a request like
`sonnet[1m]` still resolved to `MySonnetDeploy[1m]`, leaving the disabled tag
on (and a tagged input doubled it to `...[1m][1m]`).
Route the alias- and legacy-opus-resolved defaults through a small helper that
strips whatever [1m] is present and re-attaches it only when a tag was
requested (input or resolved default) AND 1M context is enabled. Disabled 1M
now drops the tag regardless of source; enabled still honors an env default's
opt-in without duplicating it. Custom deployment casing is preserved.
Adds coverage for default-model env overrides (disabled + enabled directions)
and a mixed-case custom id.
* test(model): cover disabled-1M mixed-case custom ids and Codex aliases
The disabled-1M block covered the Claude-family aliases and a lowercase custom
id, but not two paths flagged in review: a mixed-case custom deployment id
(must drop [1m]/[1M] while preserving casing) and the Codex aliases, which are
resolved by a separate branch from the Claude-family aliases and so need their
own disabled-1M assertions (codexplan[1m] / codexspark[1M] drop the tag).
* fix(memoryscan): abort remaining workers when one throws on iterator error
scanMemoryFilesWithDependencies spawns HEADER_READ_CONCURRENCY workers consuming a shared async generator. When one worker's fileIterator.next() throws (e.g. filesystem error), the error is re-thrown, Promise.all rejects, and the outer catch returns []. But without an abort signal, the remaining workers keep iterating indefinitely, leaking unresolved promises and open file handles.
Create an internal AbortController linked to the caller's AbortSignal, and call controller.abort() before re-throwing so peer workers terminate on any error.
* fix: reorder abort check so throw error is reachable
* fix(format): show sub-second durations with one decimal instead of "0s"
formatDuration's sub-second branch was guarded by `ms < 1` (1 millisecond)
while its comment documents "For durations < 1s, show 1 decimal place". At
that threshold the decimal branch could only fire for sub-millisecond values
(always returning "0.0s"), so real sub-second durations fell through to
Math.floor(ms / 1000) and rendered as "0s" — e.g. a 500ms agent run showed
"0s". Corrected the threshold to `ms < 1000`. Adds formatDuration tests
(the function was previously untested).
* fix(format): round sub-second durations in integer milliseconds
Rounding the raw fraction with (ms / 1000).toFixed(1) is unstable because
values like 0.95 are not exactly representable in binary floating point,
so 950ms rendered as 0.9s and 850ms as 0.8s. Round in integer
milliseconds first via a shared oneDecimalSeconds helper, used by both
formatSecondsShort and formatDuration's sub-second branch. Added halfway
regression cases.
---------
Co-authored-by: Pablosinyores <nikhilbajaj0182@gmail.com>
* fix(bash): surface rolled-output file path on non-zero exit (#1359)
When a Bash command produces more than `getMaxOutputLength()` bytes the
shell rolls the captured output to a file and leaves only the first
chunk on `result.stdout`. The success path already persists that file
into the tool-results dir so the model can read it back via FileRead;
the non-zero-exit path threw `ShellError` before reaching that block, so
the captured-but-rolled output was effectively unreachable. The model
saw the exit code and the truncated chunk and had no way to recover the
failure body — the user had to re-run the command with explicit
redirection. Issue #1359 reads as "almost every command returns only
the exit code on error" because the offenders (`bun run build`, test
runners, language compilers) routinely emit more than 30k bytes before
they fail.
Extract the persist step into a shared `persistShellOutputFile` helper
and call it from both the success path and the error path. On the
error path, append an `[output truncated above — full output (<bytes>
bytes) saved to <path>; read with the Read tool]` marker to the stdout
slot of the `ShellError` so `formatError` / `getErrorParts` carries the
location alongside the exit code and the in-memory preview.
Tests: new regression in BashTool.errorOutput.test.ts drives ~50k bytes
into a command that exits 1 and asserts the canonical marker phrasing
appears in `formatError(err)` along with the exit code.
Closes#1359
* fix(bash): mark capped persisted output instead of calling it full
persistShellOutputFile() truncates the rolled-output file to
MAX_PERSISTED_SHELL_OUTPUT_SIZE (64 MB) before linking it into the
tool-results dir. The non-zero-exit hint still labelled it the
"full output (N bytes)", so for a failing command emitting more than
64 MB the model was told the complete failure body was on disk even
though the tail past the cap was dropped — hiding a compiler/test
error that appears late in the log.
Thread a truncated flag out of persistShellOutputFile() and, when set,
report the cap ("first 64 MB of the N-byte output saved ... (capped,
tail not saved)") instead of claiming full output. The success path
keeps reporting the original total size unchanged.
Refs #1359
* fix(bash): don't claim full output on the success path when the file was capped
The success path persisted persistedOutputPath/persistedOutputSize but dropped
persistShellOutputFile's truncated flag, so buildLargeToolResultMessage always
said 'Full output saved to:'. For a successful command whose rolled output
exceeds the 64 MiB cap, the saved file holds only the first 64 MiB while the
model was told the complete output was available.
Thread the truncated state through the success-path result and message;
buildLargeToolResultMessage now says 'Partial output ... (output was capped)'
when the persisted file is truncated.
* test(bash): read back the persisted file and cover capped-output wording
- The large-output error test now extracts the saved path from the marker,
asserts the file is readable and contains the tail line (line 0700) that
#1359 needs the model to recover, and removes the artifact in a finally block
so it isn't left under the project storage.
- Add buildLargeToolResultMessage cases for the full vs capped wording.
* fix(bash): cap persisted output copy, not the rolled-output source
persistShellOutputFile truncated the shell's rolled-output file in place
before the link/copy, so the error fallback and resizeShellImageOutput
read a source that had already lost its tail. Copy the oversized output
first (a hardlink shares the inode), then cap the destination copy,
leaving the source intact for downstream recovery.
Refs #1359
* fix(bash): write only capped bytes when persisting oversized shell output
The oversized branch copied the entire rolled-output source into tool-results
and only then truncated the copy. For a multi-GB failure log that briefly
materializes the whole file under session storage, and if the post-copy
truncate failed the helper caught the error and returned null while leaving the
full-size copy behind.
Stream a bounded byte range (first maxSize bytes, inclusive end = maxSize-1)
straight into the destination instead, and unlink any partial destination on
failure before bubbling to the outer catch. The source is still left untouched
so the error fallback and image-resize paths can read its tail.
Adds a test pinning the destination to exactly the first maxSize bytes
(distinguishable head/tail halves catch an off-by-one on the read range).
* fix(openai-shim): strip `store` when baseUrl points at Mistral
Mistral's chat-completions endpoint rejects requests with a `store`
field — `422 body.store: Extra inputs are not permitted`. The shim
already strips `store` for Gemini and Cerebras hosts via
`hasGeminiApiHost` / `hasCerebrasApiHost`; add the symmetric host check
for Mistral so users hitting `api.mistral.ai` directly (without
`CLAUDE_CODE_USE_MISTRAL=1` to engage the gateway profile) don't hit
the same wall.
Closes#739.
* test(openai-shim): cover the Mistral-host store-strip fallback directly
The existing api.mistral.ai test resolves to the Mistral descriptor route,
whose removeBodyFields already strips store, so it passes without the
hasMistralApiHost change. Add a test on an unresolved Mistral-host proxy
(proxy.mistral.ai → no descriptor route) where store is stripped only by
the host fallback, plus a hasMistralApiHost predicate test asserting
subdomains match while look-alikes (notmistral.ai, api.mistral.ai.evil.com)
keep store. Export hasMistralApiHost for the predicate test.
* fix(openai-shim): map max_completion_tokens on Mistral-host fallback
The unresolved Mistral-host route (e.g. proxy.mistral.ai) does not carry
the descriptor's max_tokens field mapping, so chat completions still sent
max_completion_tokens, which Mistral rejects with the same 422 as store.
Gate the token-field rewrite on hasMistralApiHost too, mirroring the
store strip, and assert the fallback body sends max_tokens.
Closes#739
Remove the unused Python helper island under python/, including the standalone Ollama adapter, smart router, pytest tests, and Python requirements file.
Drop the corresponding Python setup, dependency install, and pytest steps from the PR checks workflow now that no repo-level Python helper suite remains.
Clean contributor-facing references in README, AGENTS.md, and CONTRIBUTING.md so the repository map and validation guidance no longer point at deleted Python helper code.
Validation:
- bun run build: passed
- bun run typecheck: passed
- bun run typecheck:type-tests: passed
- bun run test:provider-recommendation: passed
- bun run security:pr-scan -- --base upstream/main --head HEAD: passed
- bun run check: failed in existing broader test suites unrelated to this removal (bughunter git context, Conversation Arc Scale and Stability, xAI OAuth callback)
- bun run test:provider: failed in existing xAI OAuth callback tests
Co-authored-by: jatmn <jatmn@users.noreply.github.com>
Marketplace names come from user settings (extraKnownMarketplaces), and
diffMarketplaces looks each one up with materialized[name]. When the
materialized map is a plain object, a name colliding with an
Object.prototype member (constructor / toString / valueOf / hasOwnProperty)
resolves to the inherited value instead of undefined, so the entry is
misclassified as already-materialized (sourceChanged) rather than missing.
reconcileMarketplaces produced exactly such a plain object in its error
fallback. Two changes:
- diffMarketplaces now does an own-property-exact lookup via Object.hasOwn,
so the comparison is correct regardless of the map's prototype.
- reconcileMarketplaces uses loadKnownMarketplacesConfigSafe, which already
degrades a corrupted/unreadable config to a null-prototype empty map (and
logs via logForDebugging rather than the error file). The snapshot is only
used to diff — the install step re-reads and mutates the real config — so
the graceful empty fallback cannot clobber the user's file.
Same prototype-pollution class as the marketplace cache fix in #1787.
Adds the first tests for diffMarketplaces, which was previously untested.
* feat(integrations): add ClinePass gateway provider with usage support
Adds ClinePass (https://cline.bot) as an OpenAI-compatible gateway provider.
Gateway
- New descriptor at src/integrations/gateways/clinepass.ts with 10 static models.
- Uses wireFormat: 'reasoning_effort' and full granular levels (low/medium/high/xhigh)
so each model can expose reasoning controls consistent with Atlas Cloud.
- Dedicated credentials only: requires CLINE_API_KEY and ignores stale OPENAI_API_KEY.
- Generated integration artifacts updated via bun run integrations:generate.
/usage support
- New service module under src/services/api/clinepassUsage/ for types, fetching,
and normalizing the ClinePass usage-limits response.
- New UI component src/components/Settings/ClinePassUsage.tsx rendered by Usage.tsx.
- Displays 5-hour, weekly, and monthly usage progress bars with longer progress bars.
Provider profile fixes
- routeMetadata.ts now resolves the active provider from the saved profile even when
CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED is not yet set, so /usage works immediately
after switching providers with /provider.
- providerProfile.ts / providerProfiles.ts learn CLINE_API_KEY so saved profiles apply
the ClinePass credential alongside the OpenAI-compatible env vars.
Tests & docs
- Updated ProviderManager.test.tsx and compatibility.test.ts for the new preset.
- Added routeMetadata.test.ts cases for ClinePass and generic profile fallback.
- Added clinepassUsage.test.ts for payload normalization and row building.
- Updated .env.example and README.md with CLINE_API_KEY instructions.
Validation
- bun run typecheck
- bun run build
- bun run test:provider (964 pass)
- bun run check (5331 pass; 1 unrelated Windows file-mode failure in branch.test.ts)
* fixup: wire CLINE_API_KEY/CLINE_API_MODEL into runtime routing and profile persistence
- Add 'clinepass' to the ProviderProfile union so saved profiles can use it.
- Add CLINE_API_KEY to PROFILE_ENV_KEYS so profile switching clears stale keys.
- routeMetadata.ts: env-only CLINE_API_KEY now selects the clinepass route; other
env-only intents treat CLINE_API_KEY as a competing credential. Added
isClinePassBaseUrl/getClinePassBaseUrlOverride and activeProfileBaseUrl option
to resolveActiveRouteIdFromEnv so custom/unknown profiles targeting api.cline.bot
resolve correctly.
- providerConfig.ts: resolveProviderRequest now reads CLINE_API_MODEL when
CLINE_API_KEY is present and defaults the base URL to https://api.cline.bot/api/v1.
- providerProfiles.ts: CLINE_API_KEY is mirrored into startup/profile env for
clinepass and custom profiles at api.cline.bot, and is checked in
isProcessEnvAlignedWithProfile.
- parse.ts: toIsoDate drops invalid resetsAt values instead of echoing them.
- Add fetchClinePassUsage tests covering auth, headers, non-OK responses, and errors.
- Add routeMetadata and providerConfig regression tests for ClinePass env/model wiring.
- Add providerProfiles tests for CLINE_API_KEY propagation in apply/env and startup persistence.
- Remove extra blank line in .env.example.
Validation:
- bun run typecheck
- bun run build
- bun run test:provider (972 pass)
- bun run check (5346 pass; 1 unrelated Windows file-mode failure in branch.test.ts)
* fixup: address PR review findings for ClinePass env routing and profile persistence
- routeMetadata.ts: Let the concrete OPENAI_BASE_URL match win before falling
back to activeProfileProvider / activeProfileBaseUrl. Prevents a saved ClinePass
profile from overriding an explicit CLAUDE_CODE_USE_OPENAI env pointing at
another gateway.
- providerConfig.ts: Gate the ClinePass model branch behind !isGithubMode so a
stale CLINE_API_KEY/CLINE_API_MODEL does not override GitHub Copilot model
selection.
- providerProfiles.ts: Introduce isClinePassProfile() predicate that uses route
resolution (routeId === 'clinepass' || baseUrl includes api.cline.bot) and
share it across live application, alignment, and startup persistence paths so
saved ClinePass profiles keep the dedicated credential consistently even with
non-default base URLs.
- Add regression tests covering all three findings.
Validation:
- bun run typecheck
- bun run build
- bun run test:provider (973 pass)
* fixup: use hostname-based ClinePass detection in providerProfiles
Replace includes('api.cline.bot') substring matching with isClinePassBaseUrl
which validates the exact hostname via URL parsing, preventing spoofed hosts
like api.cline.bot.evil.example from triggering CLINE_API_KEY mirroring.
Validation:
- bun run typecheck
- bun run build
- bun run test:provider (973 pass)
* fixup: gate ClinePass model selection on resolved base URL
Move base URL resolution before model selection in resolveProviderRequest
so effectiveClinePassMode is only active when no explicit non-ClinePass base
URL is set via options.baseUrl, OPENAI_BASE_URL, or OPENAI_API_BASE.
Previously CLINE_API_KEY=cp-key + CLINE_API_MODEL=cline-pass/qwen3.7-max
+ OPENAI_BASE_URL=https://api.openai.com/v1 would return
requestedModel=cline-pass/qwen3.7-max with baseUrl=https://api.openai.com/v1,
sending a ClinePass model ID to a non-Cline provider.
Now the resolver returns the correct OPENAI_MODEL and OpenAI base URL in that
scenario, and only uses ClinePass model/default-base when the resolved base
URL is absent or actually api.cline.bot.
Added regression tests for:
- CLINE_API_KEY + CLINE_API_MODEL + explicit OPENAI_BASE_URL
- CLINE_API_KEY + CLINE_API_MODEL + explicit baseUrl option
- CLINE_API_KEY + CLINE_API_MODEL with no base URL (ClinePass default)
Validation:
- bun run typecheck
- bun run build
- bun run test:provider (976 pass)
* fixup: default ClinePass model for blank env
Treat whitespace-only CLINE_API_MODEL as unset so OPENAI_MODEL can still provide the ClinePass model override.
When CLINE_API_KEY selects ClinePass without any model env, fall back to the ClinePass route default instead of the generic codexplan alias.
Validation:
- timeout 600 bun test src/services/api/providerConfig.test.ts
- timeout 600 bun test src/services/api/clinepassUsage.test.ts src/integrations/routeMetadata.test.ts src/services/api/providerConfig.test.ts src/utils/providerProfiles.test.ts src/integrations/compatibility.test.ts src/components/ProviderManager.test.tsx
- timeout 600 bun test src/services/api/providerConfig.test.ts src/services/api/client.test.ts src/integrations/routeMetadata.test.ts src/integrations/runtimeMetadata.test.ts src/services/api/clinepassUsage.test.ts src/services/api/minimaxUsage.test.ts src/utils/providerProfiles.test.ts
- timeout 600 bun run integrations:check
- timeout 600 bun run typecheck
- timeout 600 bun run build
- timeout 600 bun run security:pr-scan
- git diff --check origin/main...HEAD
Scale HISTORY_SNIP context-efficiency nudges with the active model window so large-context sessions do not get prompted after fixed low token growth.
Keep existing reset behavior and add focused regression coverage for custom intervals and model-aware attachment gating.
* fix(ollama): cap qwen3-coder-next:cloud output at 32768
Ollama Cloud rejects qwen3-coder-next:cloud requests with
max_tokens above 32768 (400). The shared qwen3-coder-next descriptor
stays at its 65536 default; add a gateway catalog override so only the
:cloud variant is capped to 32768, with the 262144 context window
inherited from the descriptor. A local qwen3-coder-next run keeps the
descriptor default.
Re-submission of the abandoned #1133, with the deepseek-v4-pro:cloud
trailing comma kept and one added after the new entry's notes.
* test(discovery): include qwen3-coder-next:cloud in the ollama stale-cache fixture
The new gateway catalog entry adds qwen3-coder-next:cloud to the ollama
route, so the stale-cache discovery fixture must list it alongside
deepseek-v4-pro:cloud and the discovered model.
* feat: add /set-context-window and /clear-context-window commands
Add session-scoped context window overrides for OpenAI-compatible models
that fall back to the default 128k context window.
- /set-context-window [model] <tokens>: set override for current model
- /clear-context-window [model]: clear override(s)
- Overrides are in-memory, per-session, and die with the process
- Auto-compact and all context display commands respect the override
- Minimum 32k tokens to avoid auto-compact floor paradox
* fix: tighten parsing, fix minimum floor, add tests
- Reject extra arguments in /set-context-window (require exactly 1 or 2 tokens)
- Reject non-integer tokens like '64000foo' with strict regex check
- Fix MIN_CONTEXT_WINDOW_OVERRIDE from 32k to 33k to match documented floor
- Add 8 tests for session override set/get/clear/normalization/precedence
* fix: test isolation, integer validation, env override precedence test
- Add clearSessionContextWindowOverride to beforeEach/afterEach for test isolation
- Add Number.isInteger check to setSessionContextWindowOverride
- Split regression test into env override and unknown model fallback cases
- Add test for fractional values (64000.5)
* fix: provider-qualified model isolation, session cleanup, model resolution
- Store full normalized model name (lowercase only, no prefix stripping)
- Lookup tries full name first, then stripped prefix fallback
- Provider-qualified names (zai-org/glm-5.2) no longer collide with unqualified (glm-5.2)
- Clear overrides in clearSessionCaches() so /clear resets state
- Resolve active model via getMainLoopModel() instead of raw context.options.mainLoopModel
- Add tests for known model precedence, provider isolation, session isolation
* fix: use session-active model, clear both exact and stripped keys
- Revert to context.options.mainLoopModel for session-active model (not global getMainLoopModel)
- clearSessionContextWindowOverride now deletes both exact and stripped-prefix keys
- Add regression test: clearing openai/gpt-4o also clears gpt-4o fallback
* fix: store both exact and stripped keys on write for symmetric lookup
- setSessionContextWindowOverride now stores both normalized and stripped-prefix keys
- Reading openai/gpt-4o via gpt-4o now works on first query (no fallback needed)
- Update test: provider-qualified writes now create both keys (symmetric with clear)
* test: canonicalize session context overrides and fix mixed-order alias tests
* test: verify CLAUDE_CODE_MAX_CONTEXT_TOKENS precedence over session overrides
* fix(remote-session): match ingress host by hostname, not raw substring
isRemoteSessionLocal and isRemoteSessionStaging decided which Claude AI
base URL receives remote-session traffic (production, staging, or
http://localhost:4000) by testing whether the ingress URL contained the
substring 'localhost' / 'staging' anywhere in the string. A production
ingress URL that merely carried those words in its path or query — e.g.
https://claude.ai/code/x?ref=staging — was misrouted to the staging or
local-dev endpoint.
Parse the ingress hostname and match it precisely instead: localhost or
127.0.0.1 for local (mirroring isLocalhostBaseUrl), and a 'staging'
hostname label for staging. A malformed URL yields no match. The
session-id (_local_ / _staging_) signals are unchanged, and the real
local/staging hosts still resolve correctly. Add product.ts tests.
* fix(remote-session): allowlist real staging ingress hosts
The dot-label match (`hostname.split('.').includes('staging')`) both
missed the real default staging ingress host `api-staging.anthropic.com`
(no bare `staging` label, so staging remote-control links fell back to
production claude.ai) and over-matched unrelated hosts such as
`foo.staging.example.com`. Replace it with an explicit allowlist: exact
`api-staging.anthropic.com` or a subdomain of the `.staging.ant.dev`
zone. Add regression coverage for both directions.
* fix(plugins): treat prototype-shadowing marketplace names as not found
Marketplace names are user-supplied and used directly as object keys for
membership and lookup (config[name]) throughout marketplaceManager. The config
came back as a normal object, so a name shadowing an Object.prototype member —
constructor, __proto__, toString, hasOwnProperty, valueOf — resolved up the
prototype chain to a truthy inherited value. 'claude plugin marketplace remove
constructor' therefore skipped the "not found" guard and acted on a bogus
inherited entry: it crashes on entry.installLocation when a plugin seed dir is
configured, or silently no-ops the removal otherwise, instead of reporting that
no such marketplace exists.
Return a null-prototype object from loadKnownMarketplacesConfig (and its safe
variant) so every config[name] lookup across this module is own-property exact.
Add a regression test covering removeMarketplaceSource for the shadowing names.
* fix(plugins): harden cache-only marketplace lookups against prototype names
getMarketplaceCacheOnly and getPluginByIdCacheOnly parsed
known_marketplaces.json directly with jsonParse, leaving the result on
the normal Object prototype. A marketplace name shadowing a prototype
member (constructor, __proto__, ...) resolved to the inherited value, so
the not-found guard was skipped and the readers took a bogus path. Route
both through toNullProtoConfig so the lookup matches the rest of the
module. Add cache-only regression coverage and assert the removal path
never rms a directory derived from an inherited entry.
* docs(plugins): reunite loadKnownMarketplacesConfig JSDoc with its function
The toNullProtoConfig helper and its comment were inserted between the
loader's JSDoc block and the loader, orphaning the documentation. Move
toNullProtoConfig (and its explanation) above the loader's JSDoc so the
doc sits directly on loadKnownMarketplacesConfig again, and cross-link
the null-prototype rationale from the loader doc.
* test(plugins): assert getMarketplaceCacheOnly never reads a bogus cache path
The direct getMarketplaceCacheOnly proto-name test only asserted a null
result, which the unhardened path also yields (it reaches
readCachedMarketplace on an inherited entry, which then fails and returns
null). Add a deterministic regression signal: seed a sentinel
installLocation on Object.prototype so a removed null-prototype guard
would drive the reader into a cache path derived from the bogus inherited
entry, and assert no such path is ever read. Fails if the
toNullProtoConfig wrapper is dropped from getMarketplaceCacheOnly alone.
* fix(ollama): preserve chat history with native context
Route Ollama chat requests through the native /api/chat endpoint so OpenClaude can send request-level options.num_ctx instead of relying on Ollama's OpenAI-compatible shim.
Default the Ollama request context to 32768 tokens, support OPENCLAUDE_OLLAMA_NUM_CTX and OLLAMA_CONTEXT_LENGTH overrides, and map max tokens/temperature/top_p into native Ollama options.
Adapt native Ollama streaming and non-streaming responses back into the existing OpenAI-shaped conversion pipeline, including usage, text, structured tool calls, and tool_use stop reasons.
Normalize native Ollama request messages for images and historical tool calls, avoiding OpenAI-only image_url/id/type payload fields in /api/chat requests.
Add Ollama context diagnostics, loopback-only ollama ps status checks, regression coverage, and documentation for verifying active context length.
* fix(ollama): address native routing review feedback
* fix(ollama): restrict loopback host matching
* fix(ollama): exclude wildcard bind address
* fix(ollama): keep https localhost proxies on chat completions
---------
Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com>
Remove the unused top-level Python Atomic Chat helper and its dedicated pytest coverage. The active Atomic Chat provider path is implemented in TypeScript through the OpenAI-compatible shim, so keeping this standalone Python module only adds dead code.
Leave the remaining Python helper modules, pytest suite, and CI workflow intact.
Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com>
Treat all-zero assistant usage from providers that do not report token counts as unsupported usage instead of real zero usage. Estimate current input and output tokens from the transcript, mark the values as estimated in the status-line payload, and use estimated totals whenever the active provider lacks usage data so mixed-provider sessions do not show stale cumulative totals.
Preserve tiny nonzero context percentages in the status JSON and render them as ctx <1% in the built-in status line instead of rounding back to 0%. Update the /statusline setup schema and add focused regression coverage for normal reported usage, unsupported all-zero usage, stale-provider fallback avoidance, tiny percentage display, and context percentage rounding.
Validation run locally with Bun 1.3.13: bun run build, bun run smoke, bun run typecheck, bun run typecheck:type-tests, focused tests for tokens/context/BuiltinStatusLine, bun run check, bun run test:provider, bun run test:provider-recommendation, python -m pytest -q python/tests, and bun run security:pr-scan -- --base upstream/main.
Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com>
* fix(compaction): respect disabled microcompact setting
Skip automatic query-loop microcompact when the message-count compaction threshold is explicitly set to off, while preserving default, numeric, and explicit compact behavior.
* test(query): isolate auto-compact config regression
* test(query): type auto-compact deps fixture
* fix linux openclaude native install launcher
Create the package-aware OpenClaude launcher for native installs while keeping the downloaded native payload name unchanged.
Repair the native launcher after npm cleanup with a relink-only helper so npm uninstall cannot remove ~/.local/bin/openclaude.
Update protocol registration and cleanup fallbacks to use the OpenClaude command name, with regression coverage for install surfaces.
* test linux install repair flow
* Add verified Kimi effort metadata
Add kimi-k2.7-code to the Kimi Code gateway catalog and the direct Moonshot catalog, ordered newest/highest first.
Annotate Moonshot and Kimi Code Kimi models with context/output limits, reasoning capabilities, and reasoning_effort metadata limited to the verified low/medium/high levels.
Add runtime metadata, effort resolution, and provider override tests covering catalog order, qualified aliases, stale xhigh clamping, and Kimi Code request serialization.
Validation run locally: bun run build; bun run smoke; bun run check; bun run test:provider; bun run typecheck; bun run integrations:check; py -3.13 -m pytest -q -p no:cacheprovider python/tests; bun run typecheck:type-tests; bun run test:provider-recommendation; bun run security:pr-scan; focused effort/runtime/client/providerConfig tests; git diff --check.
* Fix Kimi K2.7 PR review findings
Clamp Kimi K2.7 max output metadata to 32,768 for Kimi Code, direct Moonshot, and Atlas Cloud so runtime limits do not advertise the context window as a generation cap.
Update the Moonshot runtime metadata assertion to the corrected cap and keep Hicap's separate K2.7 cap unchanged.
Restore globalThis.fetch in the Kimi Code providerOverride test with try/finally to avoid cross-test leakage.
Validation: bun test --feature=UNATTENDED_RETRY src/integrations/runtimeMetadata.test.ts src/services/api/client.test.ts src/utils/effort.codex.test.ts; bun run integrations:check; bun run build; bun run test:provider; bun run typecheck; git diff --check; bun run check.
* Fix Moonshot K2.6 and K2.5 output caps
Raise direct Moonshot Kimi K2.6 and K2.5 max output metadata from the default token count to the provider context-budget ceiling.
Add runtime metadata assertions so direct Moonshot K2.6 and K2.5 keep the 262,144 output cap while K2.7 Code remains capped at 32,768.
Validation: bun test --feature=UNATTENDED_RETRY src/integrations/runtimeMetadata.test.ts; bun run integrations:check; bun run typecheck; git diff --check; bun run build.
* Clamp Atlas Kimi K2.7 effort levels
Remove xhigh from the Atlas Cloud moonshotai/kimi-k2.7-code metadata so the gateway no longer advertises an unverified effort level for that model.
Update providerOverride and effort metadata tests to cover the exact Atlas K2.7 path and assert stale xhigh settings clamp to high.
Validation: bun test --feature=UNATTENDED_RETRY src/services/api/client.test.ts src/utils/effort.codex.test.ts src/integrations/runtimeMetadata.test.ts; git diff --check; bun run integrations:check; bun run typecheck; bun run build.
* Address Kimi metadata review findings
* Update xAI model metadata and effort handling
Refresh xAI direct provider catalog with live Grok 4.3, Grok Build, and Grok 4.20 model metadata, aliases, context windows, and output caps.
Align Atlas Cloud xAI entries with the refreshed model aliases and verified effort semantics.
Treat Grok Build as a coding model without reasoning effort support while keeping its xAI Responses endpoint override.
Update xAI defaults, display names, alias descriptor lookup, and regression coverage for runtime metadata, effort selection, context limits, model options, and vision capability lookup.
Validated with bun run integrations:check, focused provider/model tests, bun run typecheck, bun run build, and git diff --check.
* Address xAI PR review findings
Add the Grok 4.20 max output cap to both xAI descriptor paths and assert the runtime metadata.
Keep route-less vision catalog lookup from resolving aliases globally while preserving route-specific alias resolution.