mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
main
974
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
d683e85395 |
feat(buddy): hero pixel-art companions with signature Enter animations (#1972)
* feat(buddy): hero pixel-art companions with signature Enter animations Rebuild the buddy system as heroes-only. The 18 legacy rolled species are removed; the hatch pool is now 7 hero forms — robinhood, kaio, strawhat, merlin, kage, ember, corsair — each hand-pickable via /buddy set. Every hero has 22x16 truecolor half-block pixel art (idle + action poses), a line-art fallback for low-color terminals, a narrow-mode face, and a signature effect that fires on every message submission: arrow with impact thunk, charging full-width energy wave, stretchy punch that extends and snaps back, twinkling sparkle stream, spinning shuriken, gradient fire cone, and cannonball with smoke trail. Engine: companion animation moves from a raw 500ms setInterval to the shared animation clock (useAnimationFrame; pauses when hidden, respects prefersReducedMotion), with a one-shot 50ms burst driver (useShotClock, arm-then-anchor to avoid stale-tick draw-phase skips) and a general ActionEffect system (pure draw/travel/impact functions, frame-tested). Effects travel right-to-left toward the prompt — matching where the sprite actually stands. Commands: /buddy set <form|random>, /buddy name <name>, muted-buddy feedback (silent no-op pets now explain themselves), and a hatch-message fix so the announced species always matches the displayed sprite (the message previously rolled with a different seed). BREAKING: existing rolled pets transform into a hero on upgrade (name and personality persist; speciesOverride pins are unaffected). Co-Authored-By: OpenClaude <openclaude@gitlawb.com> * fix(buddy): address CodeRabbit review on PR #1972 - useShotClock: consume an in-flight shot when playback becomes ineligible mid-flight (mute/reduced-motion/resize), so re-enabling can't resume a stale animation. - /buddy unmute: emit a greeting reaction — the sprite reads companionMuted non-reactively and its clock is paused while hidden, so a config-only unmute left it invisible until an unrelated re-render. - /buddy name: strip ANSI escapes and control/format characters before saving, and cap by display width (stringWidth) instead of UTF-16 length. - CompanionSprite: track bubble age in sync-render state instead of an effect-updated ref, so a fresh reaction can't render pre-faded. - companion_intro already keyed on name+species (prior commit); tests now pin exact faces for all seven heroes, separate idle/shoot pixel frame counts, and decode-guard the charCode species constants. - CompanionActionFX tests: deterministic companion fixture via complete-config module mock; raw (untrimmed) output compared against a rendered-null baseline so a spurious blank FX row fails. - companion.test: re-register the real config module in afterAll (mock.restore does not undo mock.module). - Types: SPECIES_COLORS and FORM_FLAVOR are full Records (compile error on a colorless/flavorless future hero); dead RARITY_COLORS removed. Co-Authored-By: OpenClaude <openclaude@gitlawb.com> * test(buddy): regression coverage for bubble age reset on reaction change Renders CompanionSprite against a fake shared clock: ages the first bubble past the fade threshold, swaps the reaction WITHOUT advancing the clock, and asserts the fresh bubble renders unfaded. Fading is detected structurally (border and text collapse to one color when fading) so the test is independent of the active theme's exact values. Requested by CodeRabbit on PR #1972. Co-Authored-By: OpenClaude <openclaude@gitlawb.com> --------- Co-authored-by: OpenClaude <openclaude@gitlawb.com> |
||
|
|
7b9e477519 |
fix(memdir): enforce entrypoint cap in bytes, not UTF-16 char length (#1918)
* fix(memdir): enforce entrypoint cap in bytes, not UTF-16 char length
truncateEntrypointContent bounds MEMORY.md/CLAUDE.md content with
MAX_ENTRYPOINT_BYTES (the field is byteCount, the flag wasByteTruncated,
and the warning renders it via formatFileSize) but measured and cut with
String.length, which counts UTF-16 code units. For multibyte content
(CJK/emoji at 3-4 bytes each) that undercounts by up to ~4x, so a file
that is tens of KB of real bytes but under 25,000 chars slips past the cap
entirely — reported as a fraction of its true size and passed through
uncapped, defeating the budget whose purpose is to bound context bloat.
Measure with Buffer.byteLength and perform the newline-boundary cut in byte
space (Buffer.lastIndexOf(0x0a) + subarray) so the cap actually bounds
bytes. Add regression coverage for a multibyte file over the byte cap but
under the char cap, and for small multibyte content left untouched.
* fix(memdir): keep the hard byte cut on a UTF-8 boundary
The no-newline fallback in truncateEntrypointContent hard-cut the UTF-8
buffer at MAX_ENTRYPOINT_BYTES. If that offset landed inside a multibyte
character, decoding with toString('utf8') emitted a U+FFFD replacement char,
so the body was no longer bounded by the cap — e.g. a single 30KB line of
CJK produced a 25,002-byte pre-warning body against a 25,000 cap.
Back up to the character's first byte (skip continuation bytes 0b10xxxxxx)
before slicing so the decoded body stays within the cap and contains no
replacement chars. Add a regression for the no-newline CJK case.
* docs(memdir): tell memory writers about the entrypoint byte cap
The save instructions only warned that MEMORY.md is truncated after 200
lines. Now that the 25KB byte cap is effective for multibyte content, a user
can stay well under 200 short CJK entries and still lose the tail of the
index without the writer knowing the byte cap exists. Update both the
private-memory guidance (buildMemoryLines) and the team-memory guidance
(teamMemPrompts) to state both limits — 200 lines or ~24KB, whichever comes
first.
* docs(memdir): state the byte cap in the extraction writer prompts
The background auto-only and combined memory extractors load MEMORY.md through
truncateEntrypointContent, which this branch makes enforce the 25KB byte cap for
multibyte content. Their save instructions still told the model only that lines
after 200 are truncated, so an extractor could satisfy the prompt with fewer
than 200 long entries and still lose the index tail on the next load. State both
the line and byte limits, matching the interactive private/team guidance.
* test(memdir): cover the combined line + byte truncation path
Add a case where content exceeds both MAX_ENTRYPOINT_LINES and
MAX_ENTRYPOINT_BYTES, so line truncation runs first and the byte cut then
applies to the already line-clipped result. Asserts the combined reason string,
the byte-bounded body, and no split-character replacement chars.
|
||
|
|
9fc8806f8d |
fix(cache): use a monotonic clock for conversation-cache LRU recency (#1965)
evictLRU picks the entry with the smallest recorded access order, but that order was set to accessOrder.size — which plateaus at ~maxSize once the cache is full and drops on delete, so it is not monotonic. After the cache saturates, every set/get stamps roughly the same value, and a freshly-inserted key can end up ranked below entries a prior get already bumped. The next insert then evicts the most-recently-touched key instead of the least-recently-used one, churning the 50-entry session-history cache (sessionHistory.ts) into needless refetches. Stamp recency from a monotonic counter that only ever increases. |
||
|
|
f961ae7432 | fix: suspend footer work during ctrl-c feedback (#1963) | ||
|
|
ed3927d0f5 |
fix(editor): account for NFC boundary composition in insert offset (#1954)
Cursor.modifyText builds newText from prefix + insert + suffix and hands it to
Cursor.fromText, which NFC-normalizes the whole string. But the new cursor
offset was computed as startOffset + insertString.normalize('NFC').length,
normalizing the insert in isolation. When the inserted text begins with a
combining mark that composes with the last character of the prefix (e.g. "e" +
U+0301 -> "é"), the normalized newText is one UTF-16 unit shorter than that
formula assumes, so the returned offset overshoots by one and the cursor lands
past the following text — the next keystroke then edits the wrong spot.
Measure the normalized prefix-plus-insert instead, so cross-boundary
composition is accounted for. Reduces to the previous behavior whenever no
boundary composition occurs (plain ASCII, astral emoji, insert at start).
|
||
|
|
c11c88bd91 |
perf: bound file IO concurrency (#1948)
* perf: bound file IO concurrency * fix: honor abort and failure gates in bounded IO * fix: preserve attachment timeout behavior --------- Co-authored-by: Gautam Manchandani <gautammanch@Gautams-MacBook-Air.local> |
||
|
|
af0885d8ec |
fix(bg): revalidate process identity before signals (#1937)
* fix(bg): revalidate process identity before signals * fix(bg): sanitize process identity probe failures * fix(bg): skip signals for terminal sessions |
||
|
|
6b2a1d8aef | test(websearch): allow shared timeout test lock wait (#1959) | ||
|
|
0dc622e129 |
test(cli): clean skills temp directories (#1946)
* test(cli): clean skills temp directories * test(cli): handle stream cancellation errors |
||
|
|
85cf2ac55a |
fix(update): avoid upstream package commands for custom builds (#1944)
* fix(update): avoid upstream package commands for custom builds * test(update): cover runtime package identity |
||
|
|
2970b5fd5f |
fix(context): order pruned messages by envelope timestamp, not phantom field (#1934)
pruneByRelevance keyed recency scoring, the group tie-break, and the final "restore chronological order" sort off message.message?.created_at. That nested API-body field is never populated on our Message objects (nothing in the tree assigns it), so every read was undefined and `?? 0` made all three into no-ops: the recency bonus never fired, the tie-break never broke ties, and the final sort left the list as [...recentMessages, ...olderGroups] — the newest preserveRecent messages jumped ahead of older retained ones. The chronological key actually lives on the Message envelope as `timestamp` (an ISO-8601 string present on every variant). Add a messageTimeMs() helper that parses it and route all three sites through it. This runs in the auto-compaction path (autoCompact -> pruneByRelevance), so the reordered list was being sent to the model. Add a regression using the real envelope shape asserting retained messages stay in chronological order. |
||
|
|
9bf9926805 |
fix(mcp): preserve ":-" inside ${VAR:-default} default values (#1933)
expandEnvVarsInString split the ${VAR:-default} syntax with
varContent.split(':-', 2). The code comment says the limit is there to
"preserve :- in defaults", but JavaScript's String.split(sep, limit) caps
the array length and discards the remainder — it is not a maxsplit that
glues the tail back on. So a default that itself contains ':-' is
truncated at the first occurrence: ${VAR:-a:-b} expands to "a" instead of
bash's "a:-b".
Slice at the first ':-' with indexOf so any later ':-' stays in the
default. This runs over user .mcp.json command/args/env/url/headers values.
Add coverage for the ':-'-in-default case plus set/unset/empty-default and
missing-var paths.
|
||
|
|
2448ea9bfe |
fix(query): warn before repeated tool failures stop (#1927)
* fix(query): warn before repeated tool failures stop * fix(query): preserve tool failure advisories * fix(query): forward all tool failure advisories * fix(query): harden tool failure advisories * fix(query): preserve tool failure advisories * fix(query): keep advisories one-shot * fix(query): avoid duplicate advisories after compaction * fix(query): compare advisory message IDs * fix(query): cover advisory forwarding edges * fix(query): retain advisories without tools |
||
|
|
ffc5a6e8fc |
fix(gitdiff): apply the 1MB diff cap in bytes, not UTF-16 char length (#1932)
parseGitDiff skips per-file diffs larger than MAX_DIFF_SIZE_BYTES (1 MB) — the constant is named _BYTES, its comment says "1 MB", and the function's doc promises "Files >1MB: skipped entirely". But the guard measured fileDiff.length, i.e. UTF-16 code units. Multibyte content (CJK/emoji at 3-4 bytes each) undercounts real size by up to ~4x, so a single-file diff of several MB in bytes but under 1M code units slips past the cap and is parsed and inserted into the result map — defeating the bound that keeps huge single-file diffs from bloating memory/context in the diff viewer. Measure with Buffer.byteLength. Add a regression: a diff under the cap in code units but over it in bytes is now skipped. |
||
|
|
0369c86e3b |
fix(diff): number diff-snippet hunks by their new-file position (#1917)
getSnippetForTwoFileDiff builds each hunk's snippet from the new-file lines (deletions are filtered out) but seeded addLineNumbers with the hunk's oldStart. For the first hunk oldStart === newStart so labels are correct, but any hunk following one that inserted or removed lines is mislabeled by the net line delta of the earlier hunks — the shown numbers no longer match the file the model is looking at. Seed from newStart so the new-file content carries new-file line numbers. Add a regression with a two-hunk diff whose second hunk shifts by +3. |
||
|
|
4faf666cba | fix: keep footer mounted across slash suggestions (#1943) | ||
|
|
4f971a1316 | fix(permissions): resolve relative worktree edit paths (#1930) | ||
|
|
5918e330b2 |
fix(model): default NVIDIA NIM main loop model (#1928)
* fix(model): default NVIDIA NIM main loop model * fix(cost): avoid default pricing for unknown models * fix(model): use NIM descriptor default * test(model): pin NIM descriptor fallback |
||
|
|
b588c26db2 |
fix(effort): keep effort indicator visible in prompt footer (#1919)
* fix: keep effort indicator visible * fix: preserve footer status for effort fallback * fix: simplify effort footer selection logic * fix: harden effort footer fallback * test: cover brief effort footer suppression * fix: ignore empty footer notifications * test: cover live effort footer updates * fix: handle empty JSX footer notifications |
||
|
|
9e06951e6e |
refactor(messages): extract content helpers (4 of 8) (#1901)
* refactor(messages): extract content helpers * test(messages): cover extracted content helpers * test(messages): statically import content helper * test(compact): preserve assistant text helper semantics |
||
|
|
64d164d207 |
fix(tools): keep HY3 tool schemas inline (#1923)
* fix(tools): keep HY3 tool schemas inline * fix(hy3): parse documented tool wrapper * fix(hy3): retain split wrapper openers * fix(hy3): recover zero-argument tool calls * test(hy3): restore fetch after XML stream tests * fix(hy3): scope compatibility to Tencent |
||
|
|
2259c809f7 |
fix(shim): don't infer Z.AI tool_stream for non-catalog GLM gateways (#1908)
The name-based shim matcher inferred the full Z.AI GLM contract — including enableToolStreaming — for any glm-<n> model without a catalog entry. tool_stream is a Z.AI-proprietary streaming extension, so serving GLM through an arbitrary OpenAI-compatible gateway (e.g. NVIDIA NIM, integrate.api.nvidia.com) made every request fail immediately with 400 Unsupported parameter(s): tool_stream. Only a catalog entry may opt into tool_stream (Z.AI-contract gateways set it explicitly via transportOverrides.openaiShim). Inferred GLM routes keep the reasoning-shaping fields, which any GLM endpoint benefits from, but no longer send tool_stream; without it tool calls are simply not streamed. |
||
|
|
6d6b8de2ea |
refactor(messages): extract API cleanup helpers (8 of 8) (#1906)
* refactor(messages): extract API cleanup helpers * fix(messages): strip nullable tool callers * fix(messages): resolve extract review feedback |
||
|
|
218064e376 |
fix(clipboard): fall back to image retrieval after Windows probe failure (#1922)
* fix(clipboard): run Windows Forms access in STA * fix(clipboard): try image retrieval after Windows probe failure |
||
|
|
7995b9f492 |
fix(powershell): make CMDLET_PATH_CONFIG prototype-safe (#1913)
* fix(powershell): make CMDLET_PATH_CONFIG prototype-safe CMDLET_PATH_CONFIG is a plain object literal keyed by the lowercased cmdlet name, unlike its sibling maps CMDLET_ALLOWLIST (readOnlyValidation.ts) and COMMON_ALIASES (parser.ts) which are Object.create(null) specifically to defend against prototype-chain pollution. extractPathsFromCommand does `CMDLET_PATH_CONFIG[resolveToCanonical(cmd.name)]` guarded only by `if (!config)`. A command named 'constructor' or '__proto__' (both survive the lowercasing) makes the lookup return an inherited Object.prototype member — truthy — so the guard is bypassed and `[...config.knownSwitches]` throws (spread of undefined), crashing path-constraint validation instead of treating the name as an unknown non-path cmdlet. Give the map a null prototype like its siblings so inherited-key lookups return undefined. * test(powershell): assert set-content control resolves to ask decision The control case previously only asserted checkPathConstraints did not throw, so it would still pass if CMDLET_PATH_CONFIG stopped resolving its own entries and set-content fell through as an unknown cmdlet. Assert the returned behavior is 'ask' so the test proves the null-prototype map still holds and classifies its own keys. Also reword the collision comment to present constructor/__proto__ as representative reachable names rather than the only ones. |
||
|
|
07dd90db7b |
refactor(messages): extract plan mode rendering (7 of 8) (#1905)
* refactor(messages): extract plan mode rendering * test(messages): cover plan mode reminder branches |
||
|
|
87c457c2fa |
fix(diff): count dropped lines correctly in truncated diff snippet (#1916)
getSnippetForTwoFileDiff caps the two-file diff snippet at DIFF_SNIPPET_MAX_BYTES and appends a "[N lines truncated]" notice. When the cut snaps back to a line boundary (the normal case), kept ends right before the boundary newline, so full[cutoff] is that '\n'. Counting newlines from kept.length already includes the boundary newline plus every later one — which is exactly the number of dropped lines — so the unconditional +1 double-counts it and the notice reports one line too many (two if the snippet ends with a trailing newline). Split the boundary and mid-line cases: at a boundary the count needs no +1; only the mid-line cut (no newline within the cap) keeps a partial tail line that must be added. Add a regression asserting kept + reported == total lines. |
||
|
|
eb5d719780 |
refactor(messages): extract basic factories (5 of 8) (#1902)
* refactor(messages): extract basic factories * test(messages): expand factory parity coverage * fix(messages): own factory synthetic constants * test(compact): restore real message helpers per test * fix(messages): preserve assistant tool scan behavior |
||
|
|
eeed68f4fd |
feat(provider): add Cloudflare Workers AI integration (#1100) (#1178)
* feat(provider): add Cloudflare Workers AI integration
Adds Cloudflare Workers AI as a first-class OpenAI-compatible provider
preset, modeled on the Venice / Xiaomi MiMo descriptors.
- New `src/integrations/vendors/cloudflare.ts` descriptor:
- `classification: 'openai-compatible'`
- Default base URL with literal `<ACCOUNT_ID>` placeholder — users
substitute via `/provider` baseUrl edit, same shape as the Azure
OpenAI example already in `docs/advanced-setup.md`
- `CLOUDFLARE_API_TOKEN` env, with `OPENAI_API_KEY` as fallback
- `removeBodyFields: ['store']` since Workers AI rejects unknown
OpenAI body fields (mirrors Mistral / Gemini / Cerebras strip)
- Static catalog with current Workers AI chat models
(`@cf/meta/llama-3.3-70b-instruct-fp8-fast`,
`@cf/meta/llama-3.1-8b-instruct`,
`@cf/deepseek-ai/deepseek-r1-distill-qwen-32b`,
`@cf/qwen/qwen2.5-coder-32b-instruct`)
- Validation routing on `api.cloudflare.com` /
`gateway.ai.cloudflare.com` hosts so an env-pasted URL maps back
to the preset
- Env mirror sites in `src/utils/providerProfiles.ts`: mirror api key
into `CLOUDFLARE_API_TOKEN` when baseUrl contains a Cloudflare host
(3 sites: same-env check, openAIProfileEnv build, applyEnv).
- `CLOUDFLARE_API_TOKEN` added to `PROFILE_ENV_KEYS` / `SECRET_ENV_KEYS` /
`ProfileEnv` / `SecretValueSource` in `src/utils/providerProfile.ts`
so the profile-clean and secret-redact paths know about it.
- `src/utils/providerFlag.ts` `--provider <name>` startup flag now
detects a Cloudflare profile from `OPENAI_API_KEY ===
CLOUDFLARE_API_TOKEN` (mirrors how the other host-key mirrors are
reverse-mapped to their preset id).
- `bun run scripts/generate-integrations-artifacts.ts` regenerated
`integrationArtifacts.generated.ts` to include the cloudflare preset
+ route + vendor.
- Tests: `compatibility.test.ts` PRESETS list, new
`buildProfileSaveMessage` Cloudflare case in `provider.test.tsx`,
new `applyProviderProfileToProcessEnv` Cloudflare case in
`providerProfiles.test.ts`.
- Docs: README providers table row + `docs/advanced-setup.md` section
matching the MiMo / Mistral entries.
- Dedicated AI Gateway integration with `gateway_id` URL templating.
Today users can still paste a full Gateway URL into `OPENAI_BASE_URL`
and the preset's `matchBaseUrlHosts` picks `gateway.ai.cloudflare.com`
up.
- Dynamic `/models` discovery on the Groq #1143 / `mapModel` pattern —
Cloudflare's `/v1/models` returns the runnable model list and the
hybrid catalog path drops in cleanly. Left as a separate PR so this
one stays a focused preset add.
Closes #1100
* fix(cloudflare): narrow route matching to api.cloudflare.com host
`gateway.ai.cloudflare.com` is the shared host for *all* Cloudflare AI
Gateway routes (Workers AI, Anthropic, OpenAI, etc.), so matching it to
the Workers AI preset applied Workers-AI runtime metadata and
credential precedence (CLOUDFLARE_API_TOKEN before OPENAI_API_KEY, body
'store' strip, max_tokens field) to other providers' Gateway URLs.
Drop the shared host from the match list; a dedicated AI Gateway
integration with path-aware routing is the right follow-up.
Refs #1100.
* fix(provider-manager): keep Codex OAuth after DeepSeek when cloudflare added
The picker hardcoded `options.splice(7, 0, …)` to drop the Codex OAuth
entry right after DeepSeek. Adding cloudflare to ORDERED_PROVIDER_PRESETS
bumped DeepSeek to index 7, so the splice now lands Codex OAuth *before*
DeepSeek and breaks the test fixture that drives navigateToPreset by
keypress count.
Switch to a dynamic `findIndex('deepseek') + 1` lookup so any future
preset inserted between Bankr and DeepSeek keeps the established
ordering. Fixture updated to mirror the new picker order.
Caught by CI on 12b3ff… smoke-and-tests: 8 ProviderManager tests
timing out because navigateToPreset overshot/undershot the target.
* fix(cloudflare): exclude the shared AI Gateway host from Cloudflare routing
The profile env/alignment/startup paths mirrored CLOUDFLARE_API_TOKEN whenever
the profile URL merely contained 'gateway.ai.cloudflare.com'. That host is the
shared AI Gateway for all Cloudflare AI routes (Workers AI, OpenAI, Anthropic,
...), so a profile retargeted to /openai or /anthropic Gateway URLs was wrongly
tied to the Cloudflare route and credential precedence.
Add isCloudflareBaseUrl (hostname === api.cloudflare.com, matching the Workers
AI host and the descriptor's matchBaseUrlHosts) and route all three sites
through it, consistent with isXaiBaseUrl/isFireworksBaseUrl. Also restore
CLOUDFLARE_API_TOKEN in the provider profile test cleanup keys.
* fix(cloudflare): don't seed the placeholder base URL from the CLI shortcut
`openclaude --provider cloudflare` fell through the generic OpenAI-compatible
branch and applied the descriptor default base URL verbatim — including the
unresolved `<ACCOUNT_ID>` placeholder — leaving the shortcut 'configured' with
an endpoint that cannot serve a request. Skip seeding any base URL that still
contains a `<...>` placeholder, so the user must supply a real account-scoped
URL (OPENAI_BASE_URL / `/provider` edit) first, matching how the wizard treats
placeholder endpoints.
* test(cloudflare): assert exact null fallback for AI Gateway routes
The shared AI Gateway URL assertions used `.not.toBe('cloudflare')`, which
would also pass for any other non-cloudflare return value. The intended
fallback is null, so assert `.toBe(null)` to lock the regression boundary.
* fix(cloudflare): gate profile token mirroring on base URL host only
applyProviderProfileToProcessEnv mirrored CLOUDFLARE_API_TOKEN whenever
route.routeId === 'cloudflare'. route comes from the saved profile.provider,
so that disjunct is always true for a cloudflare profile, including one
retargeted to the shared gateway.ai.cloudflare.com AI Gateway host. The
sibling sites (isProcessEnvAlignedWithProfile, buildOpenAICompatibleStartupEnv)
already key on isCloudflareBaseUrl only; align this site with them so a
shared-gateway profile no longer leaks the token or stays pinned to the
cloudflare route. Add a regression test for the gateway.ai.cloudflare.com case.
* chore(integrations): regenerate artifacts for the Cloudflare vendor
The rebase took main's generated artifacts at the conflict; regenerate so the
Cloudflare vendor descriptor is registered in VENDOR_DESCRIPTORS and the
manifest alongside the providers main added.
* fix(cloudflare): mirror CLOUDFLARE_API_TOKEN into the OpenAI-compatible auth path
The --provider cloudflare shortcut fell through to the generic
OpenAI-compatible default branch and never copied CLOUDFLARE_API_TOKEN
into OPENAI_API_KEY, so a user who only set the token sent an
unauthenticated request. Add a dedicated cloudflare case that mirrors the
token (and clears a stale generic key when absent), keeping the
placeholder-URL skip.
buildOpenAICompatibleStartupEnv also returned from its strict-env branch
before the fallback CLOUDFLARE_API_TOKEN mirror, so a keyed Cloudflare
profile persisted a startup env that omitted the token and re-detected
inconsistently after relaunch. Mirror it in the strict branch alongside
nearai/fireworks. Add regression coverage for both paths.
* fix(cloudflare): gate token mirroring on a real Cloudflare endpoint
The cloudflare shortcut copied CLOUDFLARE_API_TOKEN into the generic
OPENAI_API_KEY unconditionally. The descriptor default carries an
unresolved `<ACCOUNT_ID>` placeholder and is never seeded, so with
OPENAI_BASE_URL unset (or still pointing at a previous OpenAI-compatible
provider) the token would be attached to the wrong host. Gate the mirror
on isCloudflareBaseUrl(getConfiguredOpenAIBaseUrl()) — only seed
OPENAI_API_KEY once the configured base URL resolves to
api.cloudflare.com, otherwise fail fast and leave it unset. Add
regression coverage for the unconfigured, stale-host, and AI-Gateway-host
cases.
* fix(cloudflare): reject placeholder URL and keep the OPENAI_API_KEY fallback
The token mirror keyed on the api.cloudflare.com host only, so the literal
<ACCOUNT_ID> placeholder URL (same host) passed the gate and copied the
token onto a non-working endpoint. It also deleted any generic
OPENAI_API_KEY when no token was set, breaking the documented
compatibility fallback for users authenticating a real Workers AI URL with
OPENAI_API_KEY. Mirror only on a real (non-placeholder) Cloudflare
endpoint, and preserve an existing generic key there when no dedicated
token is present.
Refs #1100
* refactor(cloudflare): model Workers AI as a gateway, not a vendor
Cloudflare Workers AI is a hosted OpenAI-compatible inference endpoint
reached over the shared openai transport, so it belongs with the gateway
providers (atlas-cloud, groq, together, ...) rather than the transport
vendors. Move it to gateways/cloudflare.ts via defineGateway (category
hosted, vendorId openai), regenerate the integration artifacts, and
allowlist its provider-specific @cf/* catalog ids in the gateway
descriptor check (no shared cross-provider descriptor exists, same as
azure-deployment).
Refs #1100
* fix(cloudflare): key Workers AI detection on the account path, not the host
api.cloudflare.com also serves the general Cloudflare REST API, so matching the
whole host treated unrelated URLs (e.g. /client/v4/user/tokens/verify) as the
Workers AI route and mirrored CLOUDFLARE_API_TOKEN into OPENAI_API_KEY for them.
isCloudflareBaseUrl now requires the Workers AI path
/client/v4/accounts/<account_id>/ai/v1 with a real (non-placeholder) account id,
and resolveRouteIdFromBaseUrl guards its cloudflare hostname match through the
same predicate. Both route detection and token/profile mirroring key on the
actual Workers AI endpoint.
Adds same-host negative regressions (general REST path is not routed and does
not mirror the token; unresolved <ACCOUNT_ID> placeholder is excluded) and
asserts the Cloudflare Workers AI preset appears in the first-run picker.
* fix(cloudflare): honor the Workers AI path boundary in the profile-provider fallback
resolveActiveRouteIdFromEnv returned the saved active-profile provider's route
id before consulting its base URL. For a `cloudflare` profile that had been
retargeted to a non-Workers URL — the shared AI Gateway host, or a general
api.cloudflare.com REST path — this still resolved as `cloudflare`, so the
Workers AI shim config (removeBodyFields: ['store'], Cloudflare model metadata)
and CLOUDFLARE_API_TOKEN mirroring were applied to a generic endpoint, even
though resolveRouteIdFromBaseUrl already excludes those URLs.
Gate the profile-provider shortcut through profileRouteHonorsBaseUrlBoundary,
which requires the path-aware isCloudflareBaseUrl for the cloudflare route (all
other routes are host-scoped by resolveProfileRoute and unaffected). A retargeted
profile now falls through to the generic openai/custom resolution; a genuine
Workers AI profile base URL still resolves as cloudflare.
Adds regressions for both retarget cases (gateway host + REST path) and the
positive Workers AI profile case.
* fix(cloudflare): require HTTPS and honor the Workers AI path in validation
isCloudflareBaseUrl accepted any scheme, so http://api.cloudflare.com/
client/v4/accounts/<id>/ai/v1 resolved as the cloudflare route and mirrored
CLOUDFLARE_API_TOKEN into OPENAI_API_KEY over cleartext. Require url.protocol
=== 'https:'.
Startup validation selected the Cloudflare target on host match alone, so a
non-Workers path like /client/v4/user/tokens/verify demanded Workers AI auth
instead of falling back to generic OpenAI validation. Gate the cloudflare
target on isCloudflareBaseUrl(request.baseUrl), mirroring the runtime route
resolver's path boundary.
* test(cloudflare): lock non-Workers path token boundary; fix stale host-only comments
The apply/persist paths already gate CLOUDFLARE_API_TOKEN mirroring on the
isCloudflareBaseUrl path predicate, but had no coverage for a same-host
non-Workers path (api.cloudflare.com/client/v4/user/tokens/verify) and the
comments beside the mirroring sites still described a host-only boundary.
Add negative apply and persist regressions asserting the token is not mirrored
or persisted for that non-Workers URL, and update the comments to describe the
real Workers AI path predicate instead of host-only matching.
* fix(cloudflare): fall back to a generic route for retargeted profiles
resolveProfileCapabilityRouteId returned the cloudflare capability route id for
any saved cloudflare profile whose base URL no longer resolves — including one
retargeted to gateway.ai.cloudflare.com or another OpenAI-compatible host. That
stripped generic capabilities (apiFormat, custom auth/request headers) from
profile sanitize/apply even though the runtime resolver runs such a profile as
a generic OpenAI-compatible route. Mirror the same isCloudflareBaseUrl boundary:
keep the cloudflare route only for the real Workers AI URL (or the unset
descriptor default) and fall back to 'custom' otherwise. Regression asserts a
retargeted cloudflare profile preserves OPENAI_API_FORMAT.
* test(cloudflare): assert retargeted profile resolves to the custom route
Pin both resolveActiveRouteIdFromEnv assertions for a retargeted cloudflare
profile to .toBe('custom') instead of .not.toBe('cloudflare'), so the test
locks the intended generic OpenAI-compatible fallback rather than merely
excluding the cloudflare route.
|
||
|
|
3f85b255dd |
fix(commands): escape named-argument names before building the regex (#1914)
substituteArguments builds a dynamic RegExp from each frontmatter-defined
argument name without escaping regex metacharacters:
new RegExp(`\$${name}(?![\[\w])`, 'g'). parseArgumentNames only rejects
empty and numeric-only names, so an author-defined name that contains a regex
special char reaches the constructor. A name with an unbalanced '(' / '[' (e.g.
'pattern)') throws a SyntaxError on every invocation of that skill/command, and
a name like 'a.' silently over-matches ('.' turns $ab into the arg value).
Escape the name with the existing escapeRegExp helper so it is matched
literally.
Consumers: src/skills/loadSkillsDir.ts and src/utils/plugins/loadPluginCommands.ts
feed frontmatter argument names into substituteArguments.
|
||
|
|
6602076b53 |
refactor(messages): extract system factories (6 of 8) (#1903)
* refactor(messages): extract system factories * test(messages): cover system factory extraction |
||
|
|
a154f711da |
refactor(messages): extract normalization helpers (3 of 8) (#1900)
* refactor(messages): extract normalization helpers * fix(messages): avoid discarded UUID allocation * style(messages): align normalize re-export quotes |
||
|
|
fc568b9a34 |
refactor(messages): extract streaming helpers (2 of 8) (#1899)
* refactor(messages): extract streaming helpers * fix(messages): remove streaming EOF whitespace * chore(messages): clean streaming extraction imports |
||
|
|
e42aeb34f7 |
refactor(messages): extract tool pairing helpers (1 of 8) (#1898)
* refactor(messages): extract tool pairing helpers * style(messages): align tool pairing re-export quotes |
||
|
|
e086e8c35a |
fix(safety): relax over-restrictive safety checks for benign coding tasks (#1897)
* fix(safety): relax over-restrictive safety checks for benign coding tasks (Fixes #1616) Issue #1616 reports refusals for routine, benign coding tasks. Two layers caused this: 1. Model-level over-refusal: CYBER_RISK_INSTRUCTION and the 'ask before acting' guidance biased the model toward refusing normal work. Reworded to explicitly permit ordinary engineering tasks and dual-use/security-adjacent work in authorized contexts, and to ask a clarifying question rather than refuse when intent is ambiguous. 2. Application-level heuristics that become hard blocks in auto/YOLO/headless mode: the bash command-injection check, the broad DANGEROUS_FILES/DANGEROUS_DIRECTORIES auto-edit guard, and the auto-mode stripping of ordinary interpreter allow-rules (Bash(python:*), npm run:*, etc.). Added an OPENCLAUDE_SAFETY_LEVEL knob (strict|balanced|permissive, default balanced). In 'permissive' the application-level heuristics above are relaxed while genuine Windows-path/symlink guards remain active. Default behavior is unchanged. Validation: - bun run typecheck: clean - bun run build: succeeds - bun test safetyLevel.test.ts, bashSecurity.safety.test.ts: pass - bashSecurity.test.ts, filesystem.test.ts, permissionSetup.test.ts, security-hardening.test.ts: pass (no regressions) * fix(safety): narrow permissive safety relaxations * fix(safety): address review follow-ups * fix(safety): address additional review findings * refactor(permissions): share rule normalization |
||
|
|
de9729500b |
fix(nvidia-nim): enable reasoning template kwargs (#1893)
* fix(nvidia-nim): enable reasoning template kwargs * fix(nim): preserve explicit provider selections * fix(nim): limit env-only startup precedence |
||
|
|
cde6e090d3 |
fix(read): report zero lines for an empty file (#1881)
* fix(read): report zero lines for an empty file readFileInRangeFast ran the final-fragment block unconditionally, so a 0-byte file pushed one phantom empty line and returned totalLines: 1. FileReadTool picks its empty-file warning on totalLines === 0, so an empty file instead hit the else branch and emitted the wrong message: "the file exists but is shorter than the provided offset (1). The file has 1 lines." — leaving the dedicated "the contents are empty" message unreachable. Short-circuit empty input right after the BOM strip to return totalLines: 0. Trailing-newline counting (split semantics) is unchanged. Regression covers the empty file plus one-line and two-line no-trailing-newline controls. * test(read): clean up temp dirs created by readFileInRange tests Track each mkdtempSync directory and remove it in afterEach so the new tests don't leak openclaude-readrange-* dirs under the OS temp path across runs. |
||
|
|
2047fb250f |
fix(query-guard): exclude human-interaction wait from session timeout (#1879)
* fix(query-guard): exclude human-interaction wait from session timeout The session watchdog (idle 5min / hard-max 30min) counted time spent blocked on a human decision — permission prompts, AskUserQuestion, plan-mode selection — as stuck work and force-ended the query when a user took too long to choose. The watchdog is a fork-local addition (upstream Claude Code has no session-level query timeout) that never excluded human think-time. Add a reference-counted QueryGuard.beginUserInteraction() that freezes the watchdog while blocked on the user and, on resume, shifts the hard-max and lease deadlines forward by the paused duration and restarts the idle window. Wire it through queryActivity and wrap the permission resolution in toolExecution so every interactive 'ask' is covered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-guard): scope watchdog suspension to interactive permission wait Address review feedback on #1879: - Move the suspend/resume out of checkPermissionsAndCallTool's wrapper around the whole permission resolution and into interactiveHandler, so it covers only the window that truly blocks on user input. Non-human async work (e.g. the classifier in hasPermissionsToUseTool) stays watched and a genuinely stuck check can still fire the watchdog. - Reset _suspendedAt alongside _suspendCount in end()/forceEnd() to keep the 'not suspended => suspendedAt=0' invariant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(query-guard): cover watchdog suspend/resume wiring in interactiveHandler Address CodeRabbit feedback on #1879: add a focused test for the interactive permission path asserting beginUserInteraction runs once and the resume fn fires exactly once per terminal resolution (allow/reject/abort), and only once when two paths race, so a future resolution path bypassing resolveOnce fails the test instead of silently reintroducing the timeout bug. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-guard): resume watchdog on claim, not on resolve Address maintainer review on #1879 (P2a/P2b): resume the query watchdog the moment a permission decision is claimed, not when resolveOnce() completes. Every terminal path claims first, so resuming there means (a) post-decision async work (handleUserAllow -> persistPermissions) runs watched again once the human has decided, and (b) an exception in that work can no longer strand the watchdog suspended for the rest of the turn -- the likely cause of the CI smoke-and-tests full-suite hang. Resume stays idempotent (a resolveOnce safety net remains). Adds tests: resume-before-await and resume-on-throw. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(test): stop QueryGuard suspension tests leaking fake timers The beginUserInteraction describe block was accidentally nested inside the QueryLifecycleOperationTracker describe, which has no afterEach — so its vi.useFakeTimers() was never restored. The leaked fake clock froze setTimeout in whatever test file ran next, hanging the full single-concurrency suite: CI smoke-and-tests ran ~29m and was cancelled, stalling at src/utils/cwd.test.ts. Moved the block into the QueryGuard describe whose afterEach restores real timers. Verified locally: QueryGuard.test.ts + cwd.test.ts, and the full CI-order utils batch that previously hung >115s, now pass in <1s. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-guard): call watchdog resume at most once (exactly-once contract) Address Copilot review on #1879: beginUserInteraction()'s resume fn is documented as call-exactly-once, but it was invoked from both claim() and the resolveOnce safety net. Wrap it in a local idempotent helper so the underlying QueryActivity resume runs at most once, rather than relying on QueryGuard's own idempotence (other implementations may not have it). The test mock is now a plain spy, so a double-call would fail the exactly-once assertions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-guard): resume watchdog on external aborts An abort that bypasses the permission dialog callbacks — a bridge interrupt, or the REPL's now-priority/backgrounding paths calling abortController.abort() while a prompt is open — never runs claim()/resolveOnce(), so the captured resume was never called and QueryGuard stayed suspended. The turn could then hang on the unresolved permission promise with the watchdog disabled, unable to recover. Resume on the abort signal (idempotent, once) so the watchdog always recovers. Adds tests: abort-after-open, already-aborted, and no-double-resume. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-guard): resume watchdog if dialog setup throws synchronously Address review [P3]: beginUserInteraction() suspends QueryGuard before the handler pushes the dialog and before any claim(). A synchronous throw during setup (e.g. pushToQueue or bridge wiring) would exit the handler before any claim()/resolveOnce() runs, leaving the watchdog suspended for the rest of the turn. Wrap the setup path in try/catch that resumes then rethrows, so the error still propagates. Complements the abort-signal path (async cancel) — the two cover distinct exit modes and resume stays idempotent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-guard): resolve externally-aborted prompts immediately Address review [P2]: on an abort that bypasses the dialog callbacks (bridge interrupt, REPL now-priority/backgrounding), the abort handler only resumed the watchdog. That left the permission promise unresolved while resetting the idle deadline, so an already-open prompt kept queryGuard.isActive true for a full idle timeout before cleanup, blocking the queued interrupt/background work. The abort path now claims and resolves/cancels the pending permission (claim() also resumes the watchdog), so the awaiter unblocks immediately. Bridge/channel blocks still clean up their own subscriptions on abort. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-guard): stop setup on immediate abort, detach abort listener Address CodeRabbit review: - When abortSignal is already aborted before the dialog is shown, cancel and return so setup never enqueues a prompt (pushToQueue / bridge / channel / hook) that is immediately stale. - Detach the external abort listener on any normal terminal resolution so a resolved prompt doesn't retain a closure on the query-scoped abort signal. - Test: assert pushToQueue is not called for a pre-aborted prompt. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-guard): dequeue prompt on external abort; indent guarded setup Address review: - [P2] External abort (now-priority/bridge interrupt, backgrounding) resolved the permission but never removed the queued ToolUseConfirm. Since the REPL derives focusedInputDialog from toolUseConfirmQueue[0] and only UI actions call onDone, the stale dialog could stay focused after the aborted turn and interfere with foreground work. onExternalAbort now calls ctx.removeFromQueue() (no-op on the immediate-abort path, before the dialog is pushed). - [P3] Indent the setup body inside the try added for synchronous-throw safety, matching the surrounding two-space block style so the catch scope is visible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-guard): clean up partial setup in the catch path Address CodeRabbit review: if setup throws after the abort listener is registered or after pushToQueue() succeeds, the catch now detaches the listener and dequeues the prompt before rethrowing, so failed setup can't leave stale UI/listeners behind. Moved removeExternalAbortListener out of the try so catch can reach it. Trim over-explanatory comments. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-guard): cancel bridge/channel prompts on external abort Address review: onExternalAbort resolved/dequeued the local prompt but skipped the bridgeCallbacks.cancelRequest(bridgeRequestId) and channelUnsubscribe() cleanup the normal allow/reject/abort paths do. With the bridge response handler unsubscribed by the abort listener, the remote UI could keep showing a stale prompt whose reply is ignored. Moved onExternalAbort below the bridgeRequestId/channelUnsubscribe declarations (so the immediate-abort branch can reach them without a TDZ) and mirror the local cleanup there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(query-guard): keep lease deadlines active during human waits --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
06e0ae6e0b |
feat(settings): per-model context_window and max_output_tokens overrides (#1234)
* feat(settings): per-model context_window and max_output_tokens overrides
Adds a `modelLimits` settings.json map so users can declare context window
and max output tokens for OpenAI-compatible models that are not in the
built-in catalog. Resolution order is env var → settings → catalog, so the
existing CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS / _MAX_OUTPUT_TOKENS env vars
keep priority. Keys are matched exactly, then by prefix, with optional
"<host>:<model>" host-qualified forms.
Example:
"modelLimits": {
"qwen3.6-plus": { "contextWindow": 1048576, "maxOutputTokens": 32768 }
}
Closes #478
* fix(model-limits): apply settings overrides in resolveModelRuntimeLimits
The settings.json `modelLimits` fallback was only wired into the scalar
getOpenAIContextWindow / getOpenAIMaxOutputTokens helpers. The runtime
resolution path (resolveModelRuntimeLimits) instead consumes the *Matches
variants, which only returned env-var matches — so a configured modelLimits
override never reached the model whose limits are actually resolved at
runtime, contrary to the documented env → settings → catalog order.
Fold the settings lookup into getOpenAIContextWindowMatches /
getOpenAIMaxOutputTokenMatches as a dedicated `settings` field, and insert it
between the exact env override and the built-in catalog in
resolveModelRuntimeLimits. Declare `modelLimits` on GlobalConfig so the lookup
is typed. Add an integration test that drives resolveModelRuntimeLimits and
asserts settings resolve for exact, host-qualified and prefix keys, and that an
env override still wins.
* fix(model-limits): resolve modelLimits from settings.json, not global config
readSettingsLimits read getGlobalConfig().modelLimits, which is ~/.openclaude.json
— a different file from the settings.json the feature is documented and schema'd
for (SettingsSchema in settings/types.ts). A user adding modelLimits to
settings.json saw no effect. Read it from getInitialSettings() (the merged
settings snapshot, session-cached) instead, and drop the now-unused modelLimits
field from GlobalConfig so the override lives in one place. Rewire both test
suites to a gated-passthrough getInitialSettings mock.
* test(model-limits): capture real settings module at load, not in beforeEach
The settings modelLimits suites re-imported the real settings module with
a cold dynamic import inside beforeEach. That import sat right at Bun's
default 5s hook timeout and intermittently failed the first test. Capture
the genuine module once via a top-level query-string-busted static import
so the cost moves to module load (no hook timeout) and the gated
passthrough still bypasses other suites' settings.js mocks. Also correct
the integration-test comment to name the helpers the runtime path
actually calls (getOpenAIContextWindowMatches / getOpenAIMaxOutputTokenMatches).
* fix(model-limits): keep env-prefix override above settings in runtime resolver
resolveModelRuntimeLimits ordered the settings `modelLimits` value above
the env-prefix match, so a broad env-prefix override (e.g.
`{"my-custom":N}`) was silently overtaken by a more specific settings
entry (`my-custom-deployment`). The scalar getOpenAIContextWindow treats
env (exact ?? prefix) as strictly higher priority than settings; mirror
that in the runtime resolver: env.exact, then env.prefix, then settings,
then catalog/cache/descriptor. Regression covers both contextWindow and
maxOutputTokens.
* docs(model-limits): document the settings.json modelLimits override
Add a user-facing section near the env-var overrides covering the
modelLimits map, JSON shape, exact/prefix/host-qualified key matching, and
the env > settings > catalog > descriptor precedence.
* fix(model-limits): keep catalog above env-prefix in runtime precedence
The previous reorder put the env-prefix match above the catalog value,
which broke the existing invariant that a `:cloud` catalog variant takes
its known catalog limit rather than inheriting a broad base-model env
prefix (deepseek-v4-pro:cloud regressed to 262144). Correct order:
exact env -> catalog/cache -> env prefix -> settings -> descriptor. This
still keeps settings strictly below env-prefix (the original drift fix)
while preserving catalog precedence over prefix. Docs precedence updated
to match.
* docs(advanced-setup): document CLAUDE_CODE_OPENAI_MAX_OUTPUT_TOKENS env var
The modelLimits section referenced the max-output env var but the
Environment Variables table only listed the context-window one, leaving
output-only configuration undocumented. Add the matching table row.
Refs #478
* docs(model): align openaiContextWindows precedence comments with the resolver
The module header and the OpenALimitOverrideMatches.settings comment claimed a
resolution order of "env → settings → catalog", which contradicts
resolveModelRuntimeLimits (exact env → catalog/discovery cache → prefix env →
settings modelLimits → descriptor default). Since this module only produces the
override candidates and does not own the precedence, narrow the comments to say
so and point at runtimeMetadata.ts as the authoritative chain, so this
precedence-sensitive code isn't misread when touched again.
* fix(model): rank host-qualified modelLimits keys above bare model keys
lookupByModel grouped all exact matches (host-qualified and bare) ahead of all
prefix matches, so a bare exact key like `qwen3.6-plus` beat a host-qualified
prefix like `openrouter.ai:qwen3`. That defeated the advertised per-endpoint
disambiguation for versioned model families. A host-qualified key is strictly
more specific than a bare one, so rank both host-qualified forms (exact and
prefix) in the high-priority tier ahead of the bare exact match, leaving only
the bare prefix in the low-priority tier.
* fix(model): keep an exact modelLimits key ahead of any host-qualified prefix
The previous commit ranked host-qualified PREFIX matches above bare exact
matches, which broke the deliberate precedence in context.test.ts: an exact
`gpt-4o` limit was overridden by an unrelated `api.foo.com:gpt-4` prefix that
only matches a shorter, different model name.
Restore the tiering so an exact match (host-qualified or bare) always beats a
prefix, and a host-qualified key beats a bare key WITHIN the same match kind.
The supported way to set a different limit for the same model per endpoint is a
host-qualified EXACT key. Narrow the regression + comment to that behavior.
* docs(model): align modelLimits matching wording with exact-over-prefix rule
Narrow the advanced-setup wording so a host-qualified key only wins over a bare
key within the same match kind (a bare exact key still beats a host-qualified
prefix), matching lookupByModel's exact ?? prefix behavior; per-endpoint limits
for the same model need host-qualified exact keys. Also note modelLimits as part
of the documented user-override layer in the integration add-model and
common-pitfalls guides.
* docs(model): clarify modelLimits host-port key and catalog/cache precedence
Spell out that the host-qualified key uses new URL(baseUrl).host — including the
port when present (localhost:4000:my-model, not localhost:my-model) — and split
the precedence line so the built-in catalog is shown as checked before the
discovery-cache value, matching resolveModelRuntimeLimits.
|
||
|
|
de751f369c |
fix(editor): guard editor-override lookup against prototype keys (#1915)
editFileInEditor resolved the editor command with EDITOR_OVERRIDES[editor] ?? editor, where editor comes from $VISUAL / $EDITOR (arbitrary strings). For a name that collides with an Object.prototype member — constructor, __proto__, hasOwnProperty, toString — the bare lookup returns the inherited member (a function / Object.prototype), which is non-nullish, so the '?? editor' fallback is defeated and editorCommand becomes a stringified function rather than the literal editor name; execSync then runs a corrupted command instead of the editor the user named. Extract resolveEditorCommand and gate the lookup on Object.hasOwn so unknown/proto names fall through to the literal name. |
||
|
|
ae9a765fb5 | fix(env): align WebSearch and Ollama env docs (#1904) | ||
|
|
3b41cf3adb |
fix(command-semantics): cover remaining linter runner exits (#1700)
* fix(command-semantics): cover remaining linter runner exits * fix(command-semantics): handle env prefixes and PowerShell chains * fix(command-semantics): preserve runner and pipeline failures * fix(command-semantics): narrow setup and runner failure guards * fix(command-semantics): catch real setup failure stderr * fix(command-semantics): preserve setup failures with output * fix(command-semantics): parse inline env split strings * fix(command-semantics): inspect Bash merged failure output * fix(command-semantics): align PowerShell failure parsing * test(web-search): stabilize Brave timeout assertion * fix(command-semantics): cover package scripts and wrapper failures * fix(command-semantics): handle script run forms and PS call operator * fix(command-semantics): cover package prefixes and npm errors * fix(command-semantics): stabilize brave timeout and ps flags * test(websearch): assert brave timeout aborts fetch signal * fix(command-semantics): handle tsc diagnostic exit 1 * fix(command-semantics): preserve script diagnostics * fix(command-semantics): guard silent skipped diagnostics * fix(command-semantics): tighten setup failure guards --------- Co-authored-by: jatmn <the@jat.mn> |
||
|
|
9a53290588 |
feat(aimlapi): add guided top-up and key provisioning (#1886)
* fix(aimlapi): send valid rebate partner id (part_62yQ…) instead of literal 'Gitlawb' * feat(aimlapi): add guided top-up and key provisioning * fix: restore accidentally removed OpenGateway badge * fix(aimlapi): restore preset order and harden topup polling/logging * fix(aimlapi): validate --method choices instead of silently defaulting to card * feat(aimlapi): guided top-up and API key provisioning * fix(aimlapi): point non-interactive credential error at existing flags * docs(aimlapi): document guided top-up alongside the existing-key path --------- Co-authored-by: Lookoff123 <bataryshkinairina@gmail.com> |
||
|
|
e204d5ad36 |
feat(doctor): add WebSearch backend diagnostics (#1884)
* feat(doctor): add WebSearch backend diagnostics * fix(doctor): tighten Firecrawl cloud URL diagnostics * fix(firecrawl): align cloud URL detection * test(websearch): stabilize Brave timeout assertion * fix(firecrawl): handle bare cloud host casing * fix(doctor): align WebSearch auto diagnostics with fallback * fix(doctor): align custom preset diagnostics |
||
|
|
780f703747 |
fix(installer): gate native-binary install behind NATIVE_PACKAGE_URL (#1838)
* fix(installer): gate native-binary install behind NATIVE_PACKAGE_URL openclaude install inherited the upstream native installer, which downloads the first-party Claude Code binary from the GCS bucket, symlinks ~/.local/bin/openclaude to it, and uninstalls the npm package the user is running. Gate every native-installer surface behind hasNativeDistribution() so npm-only builds never touch the native path; setting NATIVE_PACKAGE_URL at build time re-enables it unchanged. Also gate background cleanupOldVersions(): the versions/staging/locks directories under ~/.local/share/claude (etc.) are shared with a coexisting first-party native Claude Code install, and the protection logic only recognizes our own launcher symlink — an npm-only build kept only the newest VERSION_RETENTION_COUNT binaries and could delete the version a user's pinned `claude` launcher still points to. Co-Authored-By: OpenClaude <openclaude@gitlawb.com> * fix(installer): keep npm-only guidance on npm paths * test(installer): share macro mock helper * fix(installer): clean stale native launcher in npm fallback * fix(update): clean stale native launcher in slash update * test(update): reuse shared macro helper --------- Co-authored-by: OpenClaude <openclaude@gitlawb.com> Co-authored-by: jatmn <the@jat.mn> |
||
|
|
fb1137275a |
feat: add ultrathink keyword detection and ultracode effort level (#1551) (#1630)
* feat: add ultrathink keyword detection and ultracode effort level Rebased onto current main so the diff contains only these changes — #1780's model-level effort routing now comes from main rather than being duplicated. - ultrathink: a `\bultrathink\b` keyword in a prompt injects a high-effort reminder, gated behind the isUltrathinkEnabled() rollout flag. - ultracode: a new session-only EffortLevel that maps to xhigh (or high) on the wire and grants a standing multi-agent orchestration permission. First-party only, suppressed under a per-agent providerOverride, and gated to xhigh-capable models. Honors CLAUDE_CODE_EFFORT_LEVEL precedence across the API path, the permission attachment, and the display surfaces; rejected from every agent-definition input (markdown/skill/plugin frontmatter, SDK, and JSON). Closes #1551 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(effort): clamp ultracode display availability * fix(effort): report effective effort overrides * test(model): avoid catalog-dependent effort label * fix(model): resolve current effort against session model * fix(spinner): resolve effort suffix against session model --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: jatmn <the@jat.mn> |
||
|
|
aac2d8cdc8 |
fix(openai-shim): guard tool-arg field lookup against prototype keys (#1880)
STRING_ARGUMENT_TOOL_FIELDS is a plain-object lookup table keyed by a
provider-supplied tool-call name. hasToolFieldMapping used `name in table` and
getPlainStringToolArgumentField used a bare `table[name]` lookup, both of which
resolve inherited Object.prototype members. A tool call whose function.name is
'constructor', 'toString', '__proto__', etc. therefore reported a field mapping
(the Object constructor function), which is truthy, so the `?? null` fallback
never fired and normalizeToolArguments wrapped a JSON-encoded string argument
into a garbage-keyed object ({ 'function Object() { [native code] }': value })
instead of passing it through. Gate both lookups on Object.hasOwn, matching the
codebase convention for provider-keyed maps.
Consumers: src/services/api/openaiShim.ts (normalizeToolArguments /
hasToolFieldMapping over decoded tool_calls[i].function.name).
|
||
|
|
2f98208eaf |
fix(command-semantics): treat linter exit 1 as violations, not an error (#1846)
* fix(command-semantics): treat linter exit 1 as violations, not an error Linters and formatters use exit code 1 to mean "violations found", not a crash. commandSemantics fell back to DEFAULT_SEMANTIC for ruff/eslint, so a run that merely reported lint findings was flagged isError: true and the model retried the same command up to 3 times before giving up (observed on Windows with `uvx ruff check --fix`). Add a LINT_SEMANTIC (exit 1 = violations found, 2+ = real error, mirroring the existing grep/diff pattern) for ruff and eslint, in both the Bash and PowerShell tables. Wrapper runners (uvx, npx) inherit the wrapped tool's semantics only when it resolves to a recognized command — an unrecognized wrapped tool still falls back to the default, so `uvx <arbitrary>` is not blanket-treated as non-error. Adds coverage for ruff/eslint exit codes and the uvx/npx unwrap (including the unknown-wrapper fallback) in both Bash and PowerShell suites. * fix(command-semantics): normalize path-prefixed and quoted Bash linters extractBaseCommand returned the raw first token, so path-prefixed or quoted invocations (./node_modules/.bin/eslint, "ruff", /usr/bin/uvx ruff, npx ./node_modules/.bin/eslint) fell through to default exit-code semantics and a linter's exit 1 was mis-reported as an error. Normalize the base and wrapped command names (strip surrounding quotes and any path prefix) like the PowerShell implementation does, and match the wrapper by its normalized name. Adds regression coverage for path-prefixed and quoted linter/wrapper commands. * fix(command-semantics): normalize Windows .cmd/.bat/.ps1 shims on PowerShell path extractBaseCommand only stripped .exe, so npm-installed tools and wrappers invoked via their Windows .cmd shims (eslint.cmd, npx.cmd, .\node_modules\.bin\eslint.cmd) fell back to DEFAULT_SEMANTIC and reported exit 1 as an error, regressing the lint-exit-code fix on the PowerShell path. Broaden the suffix strip to the common PATHEXT executable/shim extensions (.exe/.cmd/.bat/.ps1) so direct and wrapped .cmd invocations resolve to the tool/wrapper name. Adds regression coverage for direct, path-prefixed, and wrapped .cmd forms. |
||
|
|
5105dff5f4 |
fix(config): recover from a healthy backup when the global config is corrupt (#1819)
* fix(config): recover from a healthy backup when the global config is corrupt A present-but-corrupt ~/.openclaude.json took getConfig down the ConfigParseError path, which reset to defaults (silently discarding the user's settings) or re-threw, even though healthy timestamped backups exist in ~/.claude/backups. getConfig only consulted a backup on ENOENT, and then only to print a manual 'cp' hint. Now a corrupt parse first tries to recover the most recent backup that still parses (merged over defaults) before doing anything destructive, so a one-off bad write no longer wipes config or crashes startup. Falls back to the existing defaults path when no backup is usable. Adds direct unit tests for the recovery helper. Closes #1807 * fix(config): iterate backups on recovery and skip rotating a corrupt file Address review on #1819: - recoverConfigFromBackup only tried the single newest backup, so a corrupt newest .backup left startup on the corrupted-config fallback even when an older healthy snapshot existed. Add listBackupsNewestFirst and iterate candidates newest-first, recovering from the first that parses. Added a newest-corrupt/older-healthy regression test. - saveGlobalConfig copied the live file into the backup rotation before writing. After an in-memory recovery the on-disk file is still corrupt, so that copy poisoned the rotation with the same bad content. Only rotate the current file into backups when it parses. * fix(config): never prune backups while the live config is corrupt Address review: the MAX_BACKUPS cleanup ran unconditionally, so when the live config is corrupt and only an older snapshot is healthy, startup's runMigrations() -> saveGlobalConfig() could unlink that last usable backup before the recovered config is durably written (#1807). Extract the prune decision into a pure, exported selectBackupsToPrune() that returns [] whenever the live config fails to parse, and gate the cleanup loop on it. Hoist the parse check so both the backup-copy and the prune paths share it. Add production-path coverage: getConfig recovery via _getConfigForTesting (corrupt live config -> healthy backup, and newest-corrupt -> older-healthy), plus direct selectBackupsToPrune tests (corrupt -> prunes nothing; healthy -> keeps newest maxBackups). * fix(config): skip valid-but-non-object backups during recovery recoverConfigFromBackup() returned the first backup that parsed, even when the parsed value was not a config object. A newest backup holding valid JSON like null, [], or a bare string spread into bare defaults or index/char keys and stopped, discarding the older healthy snapshots the #1807 recovery is meant to fall through to. Guard that parsedBackup is a non-null, non-array object before returning; otherwise continue to the next older backup. Adds a regression test: a valid-but-unusable newest backup (null) is skipped in favor of an older healthy one. * fix(config): recover global config from legacy backups and self-heal corrupt startup Two gaps in the #1807 recovery path surfaced in review: - listBackupsNewestFirst filtered only on the active basename, so once the global config is .openclaude.json it never tried the pre-rename .claude.json.backup.* snapshots that #1807 reports as the only surviving clean source. Recover the global config from the legacy basename too, and order backups by their .backup.<ts> timestamp so current and legacy snapshots interleave by recency instead of grouping by filename. - enableConfigs validated the global config with the throwing mode, so a corrupt config with no usable backup rethrew ConfigParseError through startup and locked users out on every launch. Drop the throwing mode so the corrupt-file/default fallback runs and startup self-heals. Adds regression coverage: legacy-basename recovery plus a scoping guard, and an enableConfigs no-crash test for the unrecoverable corrupt-config case. * test(config): re-register real env module after startup validation test The enableConfigs (#1807) regression test installs mock.module('./env.js', ...) so getGlobalClaudeFile() points at the virtual config path. Bun's mock.module() is process-global and is not undone by mock.restore(), so the teardown that only reset the fs implementation left later same-process tests importing ./env.js on the virtual path. Capture the real env module once in beforeAll and re-register it in afterEach alongside the fs reset, matching the established restore pattern in user.test.ts/effort.test.ts. --------- Co-authored-by: Pablosinyores <nikhilbajaj0182@gmail.com> |
||
|
|
f7d472e826 |
fix(provider): add Use Anthropic option to switch back from third-party profiles (#1429)
* fix(provider): add Use Anthropic option to switch back from third-party profiles The /provider menu offered no way back to built-in Anthropic once any third-party provider profile was active: getActiveProviderProfile falls back to profiles[0] when activeProviderProfileId is unset, so clearing the active id still re-selected a saved profile. Users had to hand-edit ~/.openclaude.json and restart. Add an explicit ANTHROPIC_DEFAULT_PROFILE_ID sentinel that getActive- ProviderProfile resolves to undefined (Anthropic) instead of profiles[0], and a clearActiveProviderProfile() that records the sentinel, clears the managed provider env in-session, and removes the startup profile mirror. Surface it as a 'Use Anthropic (built-in)' choice in /provider, shown whenever the current provider is not Anthropic. Saved profiles are kept for re-selection; the switch takes effect without a restart. Fixes #1426 * fix(provider): wire Use Anthropic into live ProviderManager and keep the sentinel Addresses review feedback on #1429: - The "Use Anthropic (built-in)" option now lives in the live ProviderManager "Set active provider" flow (the wizard path is test-only). It is offered only when a third-party profile or GitHub Models is currently active, and routes through clearActiveProviderProfile() + resets the session model to the built-in Anthropic default so the switch takes effect without a restart. - Teach the add/update/delete fallbacks that ANTHROPIC_DEFAULT_PROFILE_ID is a valid active state. Previously, adding a profile with makeActive:false, updating any profile, or deleting an inactive profile while on built-in Anthropic would silently reactivate profiles[0], switching the user back to a third-party provider. The delete path also no longer resolves the sentinel to profiles[0] when re-applying env. Added regression tests covering the add/update/delete sentinel-preservation paths. * fix(provider): clear startup provider overrides when switching back to Anthropic The /provider Anthropic activation branch cleared the managed session env and the startup profile file but left the startup provider override in user settings intact, so a restart would replay the third-party provider. Clear it the same way the saved-profile and GitHub paths do, surfacing any cleanup failure as a warning. Also assert the managed provider env is removed in the clearActiveProviderProfile session-env test. * fix(provider): clear startup overrides from /provider Anthropic branch; honor makeActive:false for implicit-active profiles Addresses two review findings on #1429: - The /provider 'Use Anthropic (built-in)' branch only called clearActiveProviderProfile(), so settings-backed startup overrides (CLAUDE_CODE_USE_OPENAI / OPENAI_BASE_URL / API key) survived and re-selected the third-party provider on restart. It now also calls clearStartupProviderOverrides() and surfaces a cleanup warning in the onDone message instead of reporting unconditional success, mirroring the ProviderManager Anthropic branch. - addProviderProfile(makeActive:false) still promoted the new profile when activeProviderProfileId was unset but saved profiles existed, because getActiveProviderProfile() implicitly resolves that state to the first profile while the old ternary treated !currentActive as 'no active'. Resolve the effective active state (sentinel, explicit id, or implicit first profile) before deciding, so makeActive:false never silently switches the active provider. Adds a regression test for the implicit-first-profile case (fails on the old ternary). * fix(provider): honor stale active id and clear hydrated GitHub token on Anthropic switch Addresses two findings on the switch-back-to-Anthropic path (#1426). P2 — stale active profile id (providerProfiles.ts): addProviderProfile's makeActive:false guard only preserved the implicit-first-profile case when activeProviderProfileId was unset. If the config carried an id for a deleted/missing profile, the guard treated it as "no active" and promoted the newly added profile, ignoring makeActive:false — even though getActiveProviderProfile() resolves a stale id to profiles[0]. Resolve an effectiveActiveId the same way getActiveProviderProfile does (sentinel -> built-in Anthropic, valid id -> that profile, stale/unset id with profiles -> implicit first, none -> nothing active) and keep it when makeActive:false. +regression test for the stale-id case (fails on the old guard). P2 — hydrated GitHub token leak (ProviderManager.tsx): Selecting "Use Anthropic (built-in)" while GitHub Models was active called only clearActiveProviderProfile(), which clears managed flags but leaves a GITHUB_TOKEN hydrated from secure storage (and its marker) in the session. Mirror the GitHub delete path: new clearHydratedGithubModelsTokenFromEnv() drops the hydrated token + marker while preserving a user-supplied token (one that does not match the stored credential). +unit tests for match / user-supplied / empty-storage / no-marker cases. * fix(provider): keep switch-back reachable when a non-Anthropic provider is active hasSelectableProviders gated the 'Set active provider' menu item, so when a non-Anthropic provider (GitHub Models or a saved profile) was active but no profile was saved and GitHub credentials were unavailable, the 'Use Anthropic (built-in)' recovery option was unreachable. Add a scoped canSwitchActiveProvider (true whenever GitHub is active or a profile is active) for the activate path only; edit/delete still require an actual profile. Add a ProviderManager UI test for the switch-back flow: select 'Use Anthropic (built-in)' and assert the onDone state (provider name, model reset) and that no managed CLAUDE_CODE_USE_* flags remain. * fix(provider): drop dead wizard switch-back path, dedup switch guard Address review on the legacy ProviderWizard 'anthropic' branch. ProviderWizard is test-only (live /provider renders ProviderManager), so its switch-back option duplicated the real path while diverging from it (no hydrated-GitHub-token cleanup, no model reset) and went untested. Remove the wizard's 'Use Anthropic' option, its handler branch, the now-unused ProviderChoice member, and the imports only it used, leaving the single tested switch-back in ProviderManager. Also reuse the component-scope canSwitchActiveProvider in renderMenu instead of recomputing it, so the two sites cannot drift. * test(provider): restore env + dispose mount in finally, assert token cleanup Address review on the switch-back manager-UI test: snapshot and restore the mutated process env and dispose the Ink mount in a finally block so a failed wait or assertion cannot leak provider flags or a live mount into later tests, and assert clearHydratedGithubModelsTokenFromEnv was called so dropping the hydrated GitHub token cleanup cannot pass unnoticed. * test(provider): assert switch-back forwards stored GitHub Models token The switch-back test seeded no stored token (readGithubModelsToken returned undefined) and only asserted clearHydratedGithubModelsTokenFromEnv was called, so it would still pass if the branch stopped forwarding the stored token into the helper. Seed a stored token and assert toHaveBeenCalledWith(storedToken) so the regression covers the exact GitHub Models switch-back path that preserves a user-supplied GITHUB_TOKEN while clearing only the hydrated secure-storage token. * fix(provider): keep built-in Anthropic active through the startup fallback applyActiveProviderProfileFromConfig() returned without marking provider env as handled for the Anthropic sentinel (getActiveProviderProfile resolves it to undefined). On a cold start after clearActiveProviderProfile() deleted the profile mirror, buildStartupEnvFromProfile() then treated the missing file as a fresh install and synthesized the default Gitlawb OpenGateway env, moving the user off built-in Anthropic. Clear managed provider env and set the applied flag for the sentinel so the fresh-install fallback is suppressed; an explicit startup provider selection still wins. Adds a cold-start regression test. * test(provider): assert startup-override cleanup and isolate cold-start env Address review findings: - ProviderManager switch-back test now anchors on the mocked clearStartupProviderOverrides symbol and asserts the Anthropic branch calls it, so the test fails if that call is dropped and a restart replays the third-party provider (proven fail-on-removal). - Cold-start sentinel test snapshots and clears every CLAUDE_CODE_USE_* flag (OpenAI/GitHub/Gemini/Mistral/Bedrock/Vertex/Foundry) plus the base-url/model and applied markers, restoring them in finally, so an inherited provider flag can no longer route it down the explicit-selection path or leak into later tests. * fix(provider): undo Copilot-key hydration on env cleanup hydrateGithubModelsTokenFromSecureStorage() has two hydration modes: a copilot_key blob populates GITHUB_COPILOT_KEY, while an OAuth blob populates GITHUB_TOKEN. clearHydratedGithubModelsTokenFromEnv() only cleared GITHUB_TOKEN, so undoing a copilot_key hydration removed the ownership marker while leaving the hydrated Copilot key in the session. Clear the GITHUB_COPILOT_KEY branch symmetrically (same stored-token match guard that preserves a user-supplied value). Adds helper coverage for both Copilot-key cases (matched key cleared; user-supplied differing key preserved). * fix(provider): revert hydrated Copilot key on GitHub provider delete The GitHub Models delete path hand-rolled its own env cleanup that only dropped GITHUB_TOKEN, so a hydrated copilot_key (stored in GITHUB_COPILOT_KEY under the same marker) was left behind once the marker was removed. Delegate to the shared clearHydratedGithubModelsTokenFromEnv helper so the delete flow reverts both hydration modes consistently with the switch-back path, and add a ProviderManager delete-flow regression test. * test(provider): assert switch-back refreshes session AppState model Capture AppState updates via onChangeAppState in the switch-back test and assert the Use Anthropic (built-in) path sets mainLoopModel to the Anthropic model from the result and clears mainLoopModelForSession to null. Without this the test would still pass if the setAppState block regressed, leaving a running session on the previous provider model. |