Commit Graph
100 Commits
Author SHA1 Message Date
31ac8a6eca perf: stop busting the prompt cache and slim per-turn context (#2142)
* perf: stop busting the prompt cache and slim per-turn context

Benchmarked against a comparable harness on grok-4.6 (identical one-shot
coding task), OpenClaude used 84k total tokens per task with near-zero
cache reuse. Root causes and fixes:

- Auto-memory now defaults off in non-interactive (-p) sessions
  (src/memdir/paths.ts). The conversation-arc append gated behind it
  rewrote the system prompt every request (Date.now()-relative durations,
  running token counters, per-turn RAG retrieval), which invalidates
  implicit prefix caches from byte one on chat-completions providers.
  An explicit settings opt-in (autoMemoryEnabled / memory.autoWrite)
  still enables it; also drops the ~3.2k-token memory protocol section
  from one-shot runs.

- The OpenAI shim no longer runs compressToolHistory for providers with
  implicit prefix caching (OpenAI, xAI, DeepSeek, Kimi/Moonshot, Codex)
  (requestPreparation.ts, both call sites). Its end-relative window
  retro-edits already-sent tool results each turn, mutating the middle
  of the request prefix — the native Anthropic transport already guards
  against exactly this (claude.ts shouldCompressNativeToolHistory).

- Remove the wall-clock-relative "Ns ago" line from the multi-turn
  tracking block (conversationArc.ts) — it changed on every request.

- Ship the ~1.7k-token git commit/PR protocol in the Bash tool
  description only when the session is inside a git repository
  (gitSettings.ts). The probe is cached per cwd, not per process, since
  worktree tools and daemon/SDK processes change directories mid-life.

- Add a code-robustness bullet to the Doing-tasks system prompt section:
  derive timing-sensitive logic from elapsed time, and wire up every
  element introduced (prompts.ts).

Measured on the same benchmark (8 runs): 84k -> ~46.6k total tokens per
task (-45%), 89s -> ~60s wall clock, per-call cache reads up from a
constant 128 tokens to 12k-29k, baseline context 16.8k -> 11.7k tokens.

Tests: 573 targeted tests pass, including new coverage for the
non-interactive memory default and the per-cwd git probe; tsc --noEmit
clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address review findings on the prompt-cache changes

- Correct the prefix-caching route ids ('moonshot'/'kimi-code', not
  'kimi'/'codex' — the latter never matched a real routeId) and replace
  the unanchored host regex with parsed-hostname comparison so
  path-routed gateways are not misclassified.

- gitSettings: an explicit includeGitInstructions settings value now
  always wins over the repo probe (recourse for bare-repo/GIT_DIR
  layouts), and the probe reuses the existing LRU-memoized findGitRoot
  on getCwd() instead of a second process.cwd()-keyed implementation —
  fixing stale results after Bash `cd`, `git init`, and in daemon/SDK
  processes serving multiple directories.

- Memory gate: env-provisioned memory (CLAUDE_COWORK_MEMORY_PATH_OVERRIDE,
  CLAUDE_CODE_REMOTE with a mounted memory dir) counts as explicit opt-in,
  so Cowork/remote sessions keep extraction and indexing.

- Multi-turn tracking block: render only completed turns and drop the
  running token totals — the in-progress turn's tool-call list and the
  aggregate counters changed between model requests, still rewriting the
  system prompt mid-turn.

- Update the Kimi K3 compression test to assert the new policy (history
  kept uncompressed on implicit-prefix-caching hosts) and extend
  gitSettings tests to cover settings overrides and session-cwd tracking.

569 tests pass, tsc --noEmit clean, benchmark re-run confirms metrics
hold (45.1k total tokens, 62.8s, 3 calls).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: green CI and address CodeRabbit review on the prompt-cache changes

CI: the conversation-arc suites assumed the multi-turn tracking block
renders with only an in-progress turn, and that auto-memory is on in the
(non-interactive) test process. Both now seed a completed prior turn,
mark the session interactive where they exercise interactive behavior,
and assert the new cache-stability invariants directly: the in-progress
turn is never rendered, and no Duration/token-total lines appear.

CodeRabbit findings:

- Codex transport now skips tool-history compression too: Codex talks to
  OpenAI Responses backends with implicit prefix caching, and the
  end-relative compression window rewrites already-sent tool results,
  busting the cache (mirrors the openaiShim/requestPreparation skip).
  Its compression test now pins the uncompressed behavior.

- New regression tests for both compression decision paths: an
  implicit-prefix-caching host skips compression on chat-completions and
  Responses requests, while a non-caching custom endpoint still
  compresses.

- New byte-stability test: the same turn rendered twice with an advanced
  clock and a grown in-progress tool-call list produces an identical
  system prompt.

- gitSettings: cover the CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS=0
  defined-falsy override outside a repository.

- Focused system-prompt test asserting the new timing/wiring guidance
  without snapshotting the full prompt.

bun run check (smoke, deadcode, full suite) passes; tsc --noEmit clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: isolate CLAUDE_CODE_SIMPLE in the doing-tasks prompt test

getSystemPrompt short-circuits to a minimal prompt when CLAUDE_CODE_SIMPLE
is truthy; save, unset, and restore it around the test so the full prompt
path is always exercised regardless of process-level state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-19 22:49:55 +08:00
6e3590303b feat(partners): add Concentrate and Exa to partner roster (#2141)
Adds Concentrate (concentrate.ai) and Exa (exa.ai) to the README
partners table and the web landing page, with light/dark logo variants
self-hosted under docs/assets/ and web/public/partners/.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-08-19 14:13:09 +08:00
575b407275 feat(partners): add ApiSmart, refresh Novita AI logo (#2121)
- Add ApiSmart (https://www.apismart.ai) to the README partners table
  and the web partner strip, with a dark-theme logo variant (near-black
  wordmark recolored to white, white matte removed).
- Replace the Novita AI PNG logo with the new SVG wordmark plus a
  generated dark variant, wired through the same prefers-color-scheme
  <picture> pattern (README) and logoDark field (web).

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-08-13 11:11:43 +08:00
7b03ad19a4 feat(opengateway): add Ling 3.0 Tiny :free — Day-0 launch, free until August 13 (#2112)
* feat(opengateway): add Ling 3.0 Tiny :free — Day-0 launch, free until Aug 13

inclusionai/ling-3.0-tiny:free (7.9B MoE, ~1.3B active, 262k ctx)
joins the picker via the gateway's OpenRouter wiring. The gateway
time-boxes it (free through Aug 13, rate limited) and delists it
server-side when the window closes.

* test(opengateway): Ling Tiny gateway mapping test + explicit window note

Addresses CodeRabbit review on #2112:
- new ling-tiny.test.ts (macaron.test.ts pattern) asserting the
  opengateway-ling-3.0-tiny-free entry maps both apiName and
  modelDescriptorId to inclusionai/ling-3.0-tiny:free, plus descriptor
  capabilities and runtime limits
- catalog note now dated explicitly ('Free through August 13, 2026')
  and the entry's lifecycle documented: the gateway time-boxes the id
  server-side and 400s after the window; this static catalog has no
  expiry mechanism (Ling Flash precedent), so the entry is removed or
  updated at window close

* feat(integrations): availableUntil catalog-entry expiry + Tiny lifecycle guard

Addresses CodeRabbit round 2 on #2112:
- new optional ModelCatalogEntry.availableUntil (ISO-8601): entries past
  the cutoff are dropped in getCatalogEntriesForRoute, the single choke
  point behind the model picker, gateway catalogs, and runtime limits;
  the pre-existing (previously unenforced) hidden flag is honored in the
  same filter; unparseable dates fail open
- the Ling Tiny entry sets availableUntil to the gateway's window end
  (2026-08-13T10:00:00Z), so the picker drops it the instant the
  gateway starts rejecting the id — no client release needed
- boundary regression test on both sides of the cutoff, and the picker
  expected-list test pins the clock inside the window (setSystemTime)
  so it stays deterministic after the date passes
- ling-tiny.test.ts now also asserts supportsPreciseTokenCount: false

* test(integrations): exact-cutoff + hidden + malformed-date coverage; fix stale lifecycle comment

Addresses CodeRabbit round 3 on #2112:
- ling-tiny.test.ts asserts the boundary at exactly 2026-08-13T10:00:00Z
  (cutoff is exclusive: entry already gone at that instant)
- registry.test.ts covers the two previously-untested filter branches:
  hidden entries dropped, availableUntil expiry (before / at / after
  cutoff), and a malformed availableUntil failing open
- the catalog comment above the Ling Tiny entry no longer claims the
  static catalog has no expiry mechanism — availableUntil is the guard

Validation commands run locally:
  bun run integrations:generate
  bun test src/integrations src/commands/model/model.test.tsx src/utils/model
  bunx tsc --noEmit
558 tests pass, typecheck clean.

* fix(model): route static picker entries through the availability filter

Addresses jatmn's P1 on #2112: model.tsx read catalog.models directly,
bypassing the availableUntil/hidden filter that only lived in
getCatalogEntriesForRoute — so after 2026-08-13T10:00Z the /model
picker would still offer inclusionai/ling-3.0-tiny:free and selecting
it would persist an id the gateway 400s.

- registry.ts exports filterAvailableCatalogEntries (shared with
  getCatalogEntriesForRoute)
- model.tsx filters the static entries AND the static+discovery merged
  list, so discovery-sourced entries with their own markers are covered
- routeMetadata.ts getRouteDefaultModel's catalog fallback filters too,
  so an expired entry can never become the implicit default
- new picker regression test pinned just past the cutoff asserts the
  expired entry is gone while the rest of the catalog is untouched

Validation: bun run integrations:generate; bun test src/integrations
src/commands/model/model.test.tsx src/utils/model (559 pass); bunx tsc
--noEmit (clean).

* fix(model): merge raw static entries so expired ones mask cached duplicates

Addresses CodeRabbit round 4 on #2112: filtering static entries before
mergeRouteCatalogEntries let a cached discovery entry with the same
apiName (and no availableUntil marker) re-enter the merged list, where
the post-merge filter could not remove it. The merge now takes the RAW
static list — the expired static entry wins the apiName dedup and the
post-merge filter then drops it, so neither copy survives. The filtered
list still drives the non-discovery path.

Regression tests in routeCatalogOptions.test.ts cover the cached
duplicate after the cutoff (including documenting the buggy pre-filter
order) and the masking inside the window.

Validation: bun run integrations:generate; bun test src/integrations
src/commands/model/model.test.tsx src/utils/model (561 pass); bunx tsc
--noEmit (clean).

* test(integrations): default-model fallback skips hidden and expired entries

Addresses CodeRabbit round 5 on #2112: getRouteDefaultModel's catalog
fallback changed in the availability-filter fix but had no focused
coverage. New routeMetadata.test.ts case (self-contained registry
mutation with the shared lock, mirroring registry.test.ts) verifies a
hidden default-marked entry and a past-cutoff availableUntil entry are
both skipped in favor of the remaining valid entry, and that a catalog
with nothing valid yields undefined rather than a rejected id.

Validation: bun test ./src/integrations/routeMetadata.test.ts (63
pass); bun run integrations:generate; bun test src/integrations
src/commands/model/model.test.tsx src/utils/model (562 pass); bunx tsc
--noEmit (clean).

* test(integrations): release shared mutation lock even if registry restore throws

Addresses CodeRabbit round 6 on #2112: the fallback test's finally
block ran _clearRegistryForTesting/ensureIntegrationsLoaded before
releaseSharedMutationLock, so a throw there would leave the lock held
and block later tests. Nested try/finally, matching registry.test.ts's
afterEach shape.

Validation: bun test ./src/integrations/routeMetadata.test.ts (63
pass); full related suites 562 pass; tsc clean.

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-08-11 20:18:40 +08:00
54b9cd8389 feat(opengateway): free retirement — paid Ling id, dual Nemotron, Macaron Venti (#2108)
The gateway retired its free models on 2026-08-10, keeping Nemotron 3
Ultra :free as the one free model (rate-limited by OpenRouter's shared
pool) and adding its paid throttle-free sibling as a separate entry.
Ling 3.0 Flash moves to its paid id (the :free id is aliased
server-side for older clients), Macaron V1 Tall is now paid, and
Macaron V1 Venti (748B MoL on GLM-5.2, 1M ctx) joins the catalog. The
ling entry id keeps its historical -free suffix so saved selections
resolve; HY3's stale Free label removed.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-08-10 22:16:49 +08:00
5844d1fe8a Feat/ultracode blue spinner (#2096)
* feat(ultracode): add blue/cyan spinner and effort visual treatment

- Add EFFORT_ULTRACODE (◆) figure for effort display surfaces
- Add ultracode/ultracodeShimmer theme colors across all 6 theme variants
- Wire ultracode case into effortLevelToSymbol() for icon rendering
- Use blue-cyan RGB shimmer for "thinking" text when ultracode is active
- Set spinner color override in REPL when displayed effort is ultracode

* feat(ultracode): tint prompt border and unify shimmer to theme tokens

Add a persistent cyan-blue prompt border whenever ultracode is the active
effort, reacting immediately to /effort and ranking below bash/teammate
overrides. Derive the spinner thinking-shimmer from the ultracode/
ultracodeShimmer theme tokens (with an ANSI/daltonized fallback) instead of
a divergent hardcoded cyan, so border, spinner, and shimmer share one source
of truth.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* test(spinner): cover ultracode shimmer color selection and ANSI fallback

Extracts the thinking-shimmer color computation into an exported
getThinkingShimmerColor helper (renderToString strips ANSI color, so the
selection logic is only observable through a direct call) and adds focused
tests for ultracode rgb() token interpolation, the ansi:* fallback
endpoints, and the non-ultracode gray interpolation.

Addresses CodeRabbit review on #2096.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-08-06 13:07:46 +08:00
77c82829c4 docs(readme): add npm monthly downloads badge (#2069)
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-30 15:57:28 +08:00
e636f7d1cb feat(opengateway): add Macaron V1 Tall to the gateway catalog (#2067)
* feat(opengateway): add Macaron V1 Tall to the gateway catalog

Served by opengateway via direct Novita (model is not on OpenRouter).
Free launch window; the gateway delists it 2026-08-10. Adds the model
and brand descriptors and regenerates integration artifacts.

* test(opengateway): add Macaron regression coverage + picker expectation

Adds macaron.test.ts (descriptor capabilities/limits, gateway catalog
apiName/modelDescriptorId wiring, runtime limits — tencent.test.ts
pattern) and includes mindai/macaron-v1-tall in the /model picker's
expected opengateway option list.

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-30 11:54:38 +08:00
56a920196d feat(web): replace favicon/logo with Ember Block O brand mark (#2065)
The site icon was still the 2026-06 terminal-face + git-fork circuit
mark, predating the ember identity the product now leads with (the
ANSI-Shadow startup logo and the orange pixel wordmark in the README).

Replace it with the Ember Block O: the startup screen's figlet "O"
letterform re-plotted as pure SVG rects — five ember gradient bands
(#ffb15f → #be5008, the exact stops from StartupScreen.palettes.ts)
with the wordmark's thin offset outline shadow, on a dark rounded tile.
Reads as a crisp orange O at 16px and matches CLI, README, and site.

- openclaude-logo.svg: new mark (same filename, Head.astro untouched)
- openclaude.png: 512px transparent-corner render (PNG favicon and the
  nav/footer images, which already reference this path)
- og/{default,docs,commands,buddy}.png: all four social cards
  regenerated with the new mark; layout, copy, and grid unchanged

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-29 20:45:24 +08:00
c2030bbb2b fix(web): make web/ build standalone — stop importing the repo-root p… (#2061)
* fix(web): make web/ build standalone — stop importing the repo-root package.json

vercel --prod deploys only the web/ directory, so site.ts importing
../../../package.json (and verify-dist.ts reading it) broke every Vercel
build with ts(2307) while local builds passed.

- SITE.version now derives from latestVersion, the newest entry in
  src/data/releases.ts — committed data inside web/, so builds are
  deterministic and need nothing outside the directory
- verify-dist gains a best-effort npm freshness guard: fails the build
  only when registry.npmjs.org reports a newer @gitlawb/openclaude than
  releases.ts; unreachable registry or malformed responses skip the
  check, and site-ahead-of-npm is allowed for release PRs
- verify-dist.test.ts covers the guard via injected fetch (no network)

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(web): reject leading-zero semver from the npm registry

Number() would normalize a malformed '01.2.3' to 1.2.3; require strict
semver components so malformed registry values skip the freshness check
instead of being silently coerced.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-29 13:12:35 +08:00
0f76b5490b feat(web): v0.26 refresh — buddy page, changelog, partners, provider … (#2060)
* feat(web): v0.26 refresh — buddy page, changelog, partners, provider catalog

- Single-source the site version from the root package.json (never stale again)
- New /buddy/ page: all 7 hero sprites rendered as animated SVGs generated
  from src/buddy/pixelSprites.ts, attack descriptions, commands, hatch lore,
  plus a dedicated 1200x630 OG image composed from the real sprites
- New /changelog/ page: curated release highlights 0.19 -> 0.26 from a typed
  releases.ts data file
- Landing: buddy teaser section, partners strip (GitLawb, Bankr, Atomic Chat,
  Xiaomi MiMo, Atlas Cloud, AI/ML API, Novita AI) with self-hosted logos,
  community links, refreshed provider strip, node >= 22 fix
- Providers docs rebuilt as grouped catalog (39 providers: subscriptions,
  gateways, vendors, local, custom) incl. xAI OAuth, AI/ML API, Cloudflare
  Workers AI, NVIDIA NIM, Kimi K3, GPT-5.6, Opengateway free models
- Data refresh vs v0.26.0 source: 16 new slash commands, pdf skill, new CLI
  flags + 10 subcommands, modelLimits/providerFallbackChain/agentRouting
  settings, corrected env vars (GEMINI_API_KEY, OPENGATEWAY_API_KEY, ...)
- Nav/footer/docs sidebar link the new pages; JSON-LD breadcrumbs on both

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(web): address CodeRabbit review — flag description + dist verification

- Correct --disable-slash-commands description: the flag empties the entire
  slash-command list (REPL.tsx filters all commands), not just skills; the
  upstream help string "Disable all skills" is the misleading one
- Add scripts/verify-dist.ts, wired into `bun run build` (so the existing CI
  web job runs it): asserts SITE.version matches the root package.json in the
  rendered pages, nav exposes /buddy/ and /changelog/, every release renders
  with its GitHub URL, every hero renders with its sprite asset, partner and
  community links render on the landing page, and the sitemap covers the new
  routes

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(web): harden verify-dist per review — empty-page guard, rendered-nav check, tests

- page() now records a failure for a present-but-empty file, so '' is only
  ever returned alongside a recorded failure and skipped assertions can no
  longer mask a blank page
- assert the rendered docs sidebar (dist/docs/) links every docsNav route,
  not just the source data array and the landing nav
- extract pure verifyDist(dist) and add 9 fixture-based bun tests covering
  missing/empty pages, lost sidebar links, missing sprites, stale partner
  links, missing release URLs, and sitemap regressions; discovered by the
  root `bun test` run in CI, no workflow changes needed

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* test(web): derive the missing-sprite fixture from heroes data

Hard-coding robinhood.svg would make the test throw during fixture mutation
if that hero were renamed, instead of exercising verifyDist().

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-29 12:32:47 +08:00
3c5856a004 feat(integrations): add Ling 3.0 Flash free to the Opengateway catalog (#2057)
* feat(integrations): add Ling 3.0 Flash free to the Opengateway catalog

inclusionai/ling-3.0-flash:free — 124B MoE reasoning model, 262K
context, 32K max output, tool calling verified through the gateway.
Free window on the gateway runs until 2026-08-03; the gateway delists
it automatically after that.

* test(model): include Ling 3.0 Flash in the Opengateway picker expectation

The static descriptor picker asserts the exact Opengateway catalog;
inclusionai/ling-3.0-flash:free now sits between Nemotron and HY3.

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-28 22:33:47 +08:00
12994f2c97 feat(ui): single-row centered startup logo with ANSI Shadow wordmark (#2053)
* feat(ui): single-row centered startup logo with ANSI Shadow wordmark

Render OPEN and CLAUDE side by side as one centered 6-line block on the
pre-Ink startup screen, redrawn in ANSI Shadow letterforms (consistent
shadow corners, D-shaped D, clean N). Terminals narrower than the 94-col
row fall back to two stacked blocks, each centered as a unit so rows stay
aligned. The tagline, provider box, and version line are centered to
match.

The Ink welcome panel wordmark (constants/brand.ts) becomes a matching
single row: letter-spaced caps flanked by shade-gradient accents,
keeping the shimmer/brand two-tone split.

Adds layout unit tests (one-row vs stacked switchover, block centering,
box centering) and brand wordmark invariant tests; updates the D-shape
glyph regression for the new font.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* test(ui): address CodeRabbit review — wordmark render test, row centering asserts

Extract the LogoV2 wordmark row into a WordmarkRow component and add a
focused render test asserting segment order and the shimmer/brand color
split (left accent + OPEN in brandShimmer, CLAUDE + right accent in
brand), via renderToAnsiString with chalk pinned to truecolor.

Extend the startup-screen layout test to assert the tagline and version
rows are centered, alongside the existing provider-box check.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-28 13:51:08 +08:00
158bdd0dcf docs(readme): rename Sponsors to Partners, add AI/ML API and Novita AI, new wordmark (#2054)
- Rename the Sponsors section and nav link to Partners
- Add AI/ML API and Novita AI to the partners table with local logo
  assets; AI/ML API ships light/dark SVG variants behind a <picture>
  element so the wordmark stays readable on both GitHub themes
- Replace the green SVG header wordmark with the orange pixel-art
  OPENCLAUDE wordmark PNG

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-28 13:31:10 +08:00
fff83a1a7f feat(onboarding): first-run experience for third-party providers (#1864)
* feat(onboarding): first-run experience for third-party providers

Two gates in showSetupScreens were keyed on usesAnthropicAccountFlow(),
so users of any non-Anthropic provider skipped onboarding entirely:

- Onboarding (theme + security notes) now runs for all providers. The
  component already drops its preflight/OAuth steps when Anthropic auth
  is not enabled, so third-party users get theme -> security notes ->
  terminal setup with no login screens.
- The trust dialog now runs for all providers. Workspace trust is
  orthogonal to the API provider — an untrusted repo is exactly as
  dangerous over a local model as over Anthropic. (The block comment
  even said "always show"; the inner gate contradicted it.)

Also: the login-method screen now detects OPENAI_BASE_URL+OPENAI_MODEL
in the environment and offers "Use current environment configuration"
as the first (default) option. Selecting it saves and activates a
provider profile via addProviderProfile — env vars alone do NOT
activate the OpenAI route (resolveActiveRouteIdFromEnv requires
CLAUDE_CODE_USE_OPENAI or a saved profile), a gap previously masked in
manual testing by a stray legacy .openclaude-profile.json in the cwd.

Verified live (tmux, scratch config dir, mock OpenAI server):
fresh 3P first run walks theme -> security -> trust -> REPL; env
option saves "Local OpenAI-compatible", the session completes a real
turn against the env endpoint, and the profile persists across
relaunch. Second launch shows no onboarding.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(onboarding): address review — testable gating seam + env-profile dedup

- The first-run screen decisions move into src/utils/setupScreenGates.ts,
  a provider-free importable seam (showSetupScreens' import chain cannot
  be loaded under bun test — the same constraint and pattern as the
  dev-channels registration seam). Behavioral tests cover the gate matrix
  (fresh install, completed install, theme-missing re-show, trust
  independence, claubbit skip); the bugfixes.test.ts checks now assert
  the wiring (both dialogs consult the seam, no provider gate at the
  call sites) instead of only regexing for the removed string.
- The "use current environment configuration" onboarding option dedupes:
  an existing profile matching the env base URL + model is re-activated
  via setActiveProviderProfile (which also re-applies profile env and
  syncs the startup profile file) instead of appending a near-identical
  profile on every pass through the flow.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(onboarding): refresh reused profile credentials + accurate env var label

- The reuse branch now refreshes the stored credential from the
  environment before activating: a rotated OPENAI_API_KEY would otherwise
  leave the flow running on the profile's stale key. Falls back to the
  existing key when the env no longer carries one, so a working
  credential is never blanked. Status text says "Activated" for reuse and
  keeps "Saved" for a newly created profile.
- The environment option's label names the variable the value actually
  came from (OPENAI_BASE_URL vs OPENAI_API_BASE) instead of hardcoding
  the former, so troubleshooting points at a variable that is really set.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(onboarding): preserve profile fields and verify the credential refresh

Follow-up on the reuse path added last round:

- updateProviderProfile REPLACES the profile (toProfile builds a fresh
  object rather than merging), so passing only name/baseUrl/model/apiKey
  silently dropped any configured apiFormat, azureStyle, authHeader,
  authScheme, authHeaderValue, customHeaders, or maxContextLength. Spread
  the existing profile and override only the refreshed credential.
- A null return from updateProviderProfile (env values failing profile
  validation) no longer falls through to activation: reporting
  "Activated" while still running on the stale key is worse than routing
  the user to guided setup, which is what the create path already does.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(onboarding): redact credential-bearing endpoints before display

OPENAI_BASE_URL / OPENAI_API_BASE are credential-bearing in the wild
(userinfo like https://user:pass@host/v1, or ?token=/?api_key= query
params), and both the option label and the completion status message
rendered the raw value straight into terminal scrollback.

The derivation moves into src/utils/envProviderOption.ts, which owns the
disclosure boundary explicitly: `displayBaseUrl` is passed through the
codebase's existing redactUrlForDisplay and is the only form the UI may
render, while the raw `baseUrl` is retained for profile creation and
activation so the saved profile still authenticates. Both rendered sites
now use the redacted value.

Regression coverage: envProviderOption.test.ts asserts userinfo and
sensitive query params never reach displayBaseUrl (including via the
non-URL fallback path) while baseUrl stays intact, plus var-name and
availability cases; a wiring guard in bugfixes.test.ts fails if either
rendered site is ever pointed back at the raw endpoint.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-20 16:23:14 +08:00
83d54b0ac8 feat(perf): tier1 token optimization — universal tool compression, doom loop detection, configurable compaction (#1869)
* feat(perf): tier1 token optimization — universal tool compression, doom loop detection, configurable compaction

- Extend compressToolHistory to Anthropic-native transports (firstParty/
  bedrock/vertex/native-GitHub), gated to runs where prompt caching is
  inactive: rewriting old messages as they cross tier boundaries diverges
  the request prefix every call, so cached native sessions keep relying on
  the cache-aware microCompact instead. Shim-routed traffic (OpenAI-compatible
  env providers, per-agent providerOverride, Codex) still compresses at its
  own layer, where the local fast-path opt-out applies. compressToolHistory
  is now idempotent (skips its own stub/truncation markers) so layered call
  sites can never re-mangle output.

- Add doom loop detection: blocks after 3 consecutive identical tool calls
  (same name + input signature). State is keyed per agent (main thread and
  each subagent separately) so concurrent subagents neither trip nor reset
  each other's counters. Resets at the start of each agent's query turn.

- Add configurable compactTailTurns in GlobalConfig, wired into autoCompact's
  relevance pruning (default: 3, clamped to positive) and exposed in the
  /config UI next to the other compaction settings.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(perf): address review — tier-aware idempotency, full-input signatures

Post-rebase reconciliation with #1958's compressToolHistory rework (the
guard's extractText call no longer even typechecked against the new
signature) plus the CodeRabbit findings:

- Tier-aware idempotency replaces the blanket already-compressed skip,
  which permanently blocked mid→old upgrades: stubs stay terminal, and a
  truncated block left alone while mid-tier still upgrades to a stub when
  later exchanges age it into the old tier. The upgraded stub's
  omitted-chars count is recovered from the truncation marker (visible
  length + marker's omitted count = exact pre-truncation length), so it
  reports the tool's real output size — asserted equal to what a fresh
  single-pass stub would have produced. Regression tests cover the
  aging upgrade, recovered length, and same-input no-op.
- computeSignature hashes the FULL serialized input (sha-256, fixed-size
  stored signature) instead of comparing a 2KB prefix, which treated
  distinct calls sharing a long prefix (e.g. Write calls differing only
  in trailing content) as identical — a false-positive block on
  legitimate work. Regression test included.
- DEFAULT_COMPACT_TAIL_TURNS shared constant replaces the `3` duplicated
  across autoCompact, pruneByRelevance's default, and the /config UI.
- Doom-loop block path: added a tengu_doom_loop_blocked analytics event
  (false-positive rates become observable for threshold tuning) and the
  nudge now tells the model a deliberate repeat is fine once something
  observable has changed. The blocked yield's message shape mirrors the
  sibling pre-execution error paths, preserving tool_use/tool_result
  pairing.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(perf): normalize compactTailTurns everywhere + native-routing coverage

Second CodeRabbit round:

- normalizeCompactTailTurns (relevancePruning.ts, next to the shared
  default) is now the single rule for the hand-editable config: finite
  values >= 1 floor to integers, everything else falls back to the
  default. The /config UI displays AND persists through it, so the picker
  shows exactly what autoCompact preserves (a hand-edited 2.5 no longer
  displays as 2.5 while running as 2; 0/negatives no longer display as
  selected while running as 3). Also fixes a real edge in the previous
  inline clamp: 0.5 passed the `> 0` check and floored to a ZERO-message
  tail, pruning everything. Unit tests cover the boundary matrix.

- shouldCompressNativeToolHistory extracted from queryModel and exported:
  the request-mutating routing decision is now parameterized-tested
  across all four native transports (first-party, Bedrock, Vertex,
  GitHub-native-Anthropic) x caching on/off, the providerOverride
  exclusion, and non-native providers — queryModel itself needs a live
  client, so the predicate is the honest testable seam.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(perf): guard custom first-party endpoints + strict config coercion

Third CodeRabbit round:

- shouldCompressNativeToolHistory now requires an Anthropic first-party
  base URL before accepting the firstParty provider as native, mirroring
  the exact guard getPromptCachingEnabled uses. Without it, a custom
  ANTHROPIC_BASE_URL (proxy / compatible endpoint) reported firstParty
  with caching disabled and had every request's messages compressed —
  an assumption we cannot make about arbitrary endpoints. Test added for
  the custom-base-URL exclusion.

- normalizeCompactTailTurns only coerces numbers (persisted config) and
  strings (the /config picker channel); other hand-edited shapes no
  longer smuggle a tiny tail through Number() coercion (true → 1,
  [2] → 2) and fall back to the default instead. Boundary tests added.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-20 14:26:12 +08:00
cb460e516b feat(codex): add GPT-5.6 family models and fix saved-model rehydration (#2014)
* feat(codex): add GPT-5.6 family models and fix saved-model rehydration

Add the GPT-5.6 family (gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna) to the
Codex OAuth provider: alias map with default reasoning efforts, /model
picker options, display names, and Codex-route context metadata. Bare
`gpt-5.6` resolves to the flagship Sol tier at parse time (Codex CLI
convention) — matched on the base name so a ?reasoning=/?thinking= query
suffix cannot defeat the rewrite — keeping context sizing and display on
real descriptor metadata.

Context windows reconcile with the #1961 direct-OpenAI routing: the
gpt.ts descriptors pin the ~272k effective Codex input cap (issue #1118
precedent; the Codex base URL resolves to a catalog-less route that
reads descriptors), while the openai vendor catalog keeps the true
1.05M window for direct api.openai.com /v1/responses traffic. The
gpt-5.6 alias-default reasoning effort is likewise Codex-transport-only:
OPENAI_API_BASE gateways do not inherit first-party effort metadata
(explicit /effort and ?reasoning= picks still flow everywhere).

Fix startup rehydration for Codex profiles: profileSupportsModel is now
authoritative for Codex-backend profiles — it accepts every Codex alias
and Codex-eligible gpt-5.x free-text pick (shared isCodexEligibleGpt5Model
predicate), so a /model choice (e.g. gpt-5.6-terra) survives restart
instead of silently reverting to codexplan/gpt-5.5. A trailing [1m] tag
is normalized off before matching, so tagged picks stick too. Foreign
leftovers (kimi-k2.6) and API-only tiers the backend does not serve
(gpt-5-mini/-nano) still fall back to the profile default instead of
400ing.

Also: generalize the picker's custom-model recovery to keep curated
labels for all Codex models across provider switches ([1m]-tolerant,
single lookup), and add GPT-5.6 cases to the display-name maps.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(codex): address CodeRabbit review — [1m] tag parsing + coverage

- parseModelDescriptor now strips a trailing [1m] context tag (whole-string
  or base-id position) before parsing: the tag is a client-side 1M opt-in,
  never a wire model id, so tagged aliases keep their mapping and effort
  defaults and resolvedModel never leaks the bracket suffix to the backend
  (pre-existing gap for e.g. gpt-5.5[1m], now fixed at the parse layer).
- The bare-gpt-5.6 rewrite keeps a [1m] tag TRAILING after a preserved
  query (gpt-5.6?reasoning=medium[1m] → gpt-5.6-sol?reasoning=medium[1m]);
  the previously emitted tag-before-query form broke the request-time
  base-model split. End-to-end regression tests cover parse + request.
- New coverage per review: alias effort defaults are asserted suppressed on
  a custom OpenAI-compatible gateway (non-Codex transport) while explicit
  ?reasoning= overrides flow; picker-recovery tests assert a persisted
  gpt-5.6-terra[1m] under a non-Codex provider keeps its exact tagged value
  with the curated label/description instead of a "Custom model" entry.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-20 12:47:53 +08:00
ca7a7e0791 feat(install): enforce and guard the zero-warning npm install contract (#2019)
* feat(install): enforce and guard the zero-warning npm install contract

`npm install -g @gitlawb/openclaude` is verified zero-warning today, but
nothing kept it that way: the runtime deps were caret ranges resolved
fresh on every user install (the published tarball ships no lockfile),
no CI step ever installed the package, and registry-side drift (a
transitive dep deprecated after we ship) is invisible to file-based CI.

Static contract (fast, offline, every PR via `bun run build`):
- Pin the 3 runtime deps to exact versions so the verified resolution IS
  the shipped resolution.
- New validators in scripts/externalsValidation.ts (unit-tested):
  dependencies must equal RUNTIME_DEPENDENCY_CONTRACT exactly (no ranges,
  no unreviewed additions), no consumer-run install hooks or funding
  field, engines.node pinned. Wired into validate-externals.ts.

Runtime verification (scripts/verify-clean-install.ts, `install:verify`):
- Tarball mode (release gate) and published mode (registry watch), each
  running cold-install and upgrade-over-previous scenarios in throwaway
  prefixes with a cold cache and normalized env/flags.
- Strict output whitelist (summary lines only) with network failures
  retried and reported as infra (exit 2), never as a hygiene verdict.
- Structural authority over the installed tree: any package declaring
  install scripts fails, the installed manifest must match the static
  contract, tarball payload/size asserted.
- Boot must be silent: --version prints the exact packed version;
  --help (which, unlike the --version zero-import fast path, loads the
  real bundle) must exit 0 with empty stderr.

CI: release publishes only after the verify passes on Node 22 (npm 10,
the supported floor — warning phrasing and EBADENGINE behavior differ
from npm 11) and Node 24, plus a final gate on the publishing machine
replacing `npm pack --dry-run`. A daily install-hygiene workflow
re-verifies the published @latest on {ubuntu, macos, windows} x
{Node 22, 24} — the only defense against post-release registry drift,
and the OS matrix covers the per-platform @vscode/ripgrep packages.

Found-by-the-guard fix: a fresh machine printed "Warning: ignoring saved
provider profile. OPENGATEWAY_API_KEY is required..." on every command
(even --help) because the injected fresh-install Opengateway default
fails validation without a key (#1651 chose ignore+warn). The default
env is still ignored, but the warning now only fires for genuinely
persisted profiles; published 0.24.0 carries the old noise, so the
verify script exempts exactly that version until the next release.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(install): address CodeRabbit review on the install-hygiene guard

- release.yml: pin install-verify to least-privilege `contents: read` and
  disable credential persistence on its checkout; same persist-credentials
  hardening on the install-hygiene cron checkout.
- verify-clean-install: previousPublishedVersion now follows the same
  retry/infra discipline as installWithRetry — transient registry failures
  retry and then exit 2 (infra) instead of silently skipping the
  upgrade-scenario coverage; a clean not-published answer still skips.
- providerProfile: the fresh-install warning suppression now keys on
  explicit provenance (persisted profile resolved once in
  applyStartupEnvFromProfile) instead of sniffing the
  DEFAULT_STARTUP_PROVIDER_ENV_VAR marker, which a persisted profile's
  env can inherit from a parent CLI process; regression test covers the
  marker-collision case.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* test(install): cover previousPublishedVersion retry/skip/infra branches

CodeRabbit follow-up: the branches deciding whether the upgrade-install
scenario runs, skips, or aborts as infra were untested. Extract the loop
as resolvePreviousPublishedVersion with injected effects (runView,
onRetry, onInfraFailure) per the repo's dependency-injection testing
convention, guard main() behind import.meta.main so the test import does
not launch a real verification, and add regression tests: first-try
success, transient-infra retry then success, clean E404 → null skip
without retries, persistent infra → onInfraFailure (exit 2 in the real
wiring), and unparseable version output → null.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-20 12:24:55 +08:00
14648213a6 Chore/readme cleanup (#1976)
* docs: README cleanup, green wordmark header, Trendshift badges

Header: the startup wordmark (src/constants/brand.ts half-block art)
rendered as a green two-shade SVG (docs/assets/openclaude-wordmark.svg,
textLength-pinned so rows align in any monospace font), with the three
Trendshift badges (daily/monthly/repository) centered beneath it.

Cleanup (536 -> ~430 lines, nothing lost):
- Agent routing, maxSteps limits, and GitHub Copilot sub-agent tuning
  moved to docs/agent-routing.md; headless gRPC server moved to
  docs/grpc-server.md; README keeps linked summaries.
- Build/test/validation commands were repeated in three sections —
  consolidated into one Development section; Contributing links to it.
- New "Meet Your Buddy" section documenting the companion heroes and
  their /buddy commands; added to What Works and Why OpenClaude.
- Star History moved from the header flow down beside Community.
- Setup Guides indexes the new docs pages; fixed a missing blank line
  before Repository Structure and a curly quote.

All relative links, image paths, and internal anchors validated.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* docs: pure-rect wordmark for crisp rendering; drop broken Star History

The wordmark SVG previously drew the half-block art as monospace <text>,
which rendered raggedly (font-dependent glyph stretching and seams).
Regenerated as pure SVG rects computed from the brand.ts wordmark grid —
no font dependence, pixel-crisp at any size, same two-shade green split.

Star History chart removed: the badge endpoint errors and displays a
broken image.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* docs: render the wordmark at full README column width

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-07-15 22:27:50 +08:00
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>
2026-07-15 21:50:10 +08:00
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>
2026-07-08 08:42:01 +08:00
2edec9a140 fix(deps): ship a zero-warning, minimal install (#1784)
* fix(deps): ship a zero-warning, minimal install

The published package declared 62 runtime `dependencies`, but `dist/cli.mjs`
is a fully-bundled esbuild output that inlines almost all of them. End users
therefore installed ~476 transitive packages — including three subtrees the
bundle never needs at install time, each emitting an install warning:

  - node-domexception (deprecated) via google-auth-library
  - protobufjs (allow-scripts)     via @grpc/* (already bundled into dist)
  - sharp (allow-scripts)          native image module

The repo's `overrides`/`allowScripts` silence these locally, but those are
root-only npm settings and are ignored when the package is installed as a
dependency — so end users saw the warnings.

Core changes:
  - package.json: runtime dependencies trimmed 62 -> 3 (@orama/orama,
    @orama/plugin-data-persistence, @vscode/ripgrep). Bundled packages, plus
    the optional sharp/google-auth-library, move to devDependencies so they
    are built/tested but not shipped.
  - package.json: @anthropic-ai/sdk, @modelcontextprotocol/sdk, react and
    react-reconciler declared as OPTIONAL peerDependencies — externalized by
    the ./sdk bundle but bundled into the CLI. Optional peers keep the CLI
    install minimal and warning-free while still resolving for ./sdk consumers.
  - externals.ts: sharp, google-auth-library and @anthropic-ai/bedrock-sdk
    marked OPTIONAL_RUNTIME_EXTERNALS (loaded on demand, not shipped).
  - validate-externals.ts: runtime deps validate against externals; bundled
    deps validate against dependencies + devDependencies.
  - client.ts: load @anthropic-ai/bedrock-sdk via the runtime importer so
    esbuild no longer inlines it and hoists its static @aws-sdk import into
    the CLI bundle (that was a startup crash for default installs).

Optional-dependency UX (consistent, actionable errors):
  - New src/utils/optionalRuntimeModule.ts exports importRuntimeModule and
    importOptionalRuntimeModule. The optional variant translates a missing
    package (code === 'ERR_MODULE_NOT_FOUND', specifier present in message)
    into "<feature> requires "<pkg>" ... Run `npm i -g <pkg>`". Generic so
    typed call sites keep their module types.
  - Routed ALL optional-package load sites through it (previously only one
    did): google-auth-library (client.ts, auth.ts, geminiAuth.ts),
    @anthropic-ai/foundry-sdk + @azure/identity (client.ts), and the
    @aws-sdk/* Bedrock paths (model/bedrock.ts, tokenEstimation.ts, aws.ts).
  - imageProcessor.ts: sharp-missing error now says `npm i -g sharp`.
  - docs/advanced-setup.md: new "Optional provider packages" table and a
    Vertex note documenting the on-demand installs.
  - Unit test for the helper (friendly error, success path, specifier match,
    raw passthrough).
  - knip.json: ignore google-auth-library (now loaded via runtime string).

Verified on the current tree:
  - tsc, build/validate-externals, knip, and tests all pass.
  - npm pack + install --omit=dev adds 8 packages, zero deprecation/
    allow-scripts/funding warnings; --version/--help/mcp list run.
  - With packages absent, CLAUDE_CODE_USE_BEDROCK and CLAUDE_CODE_USE_VERTEX
    print the friendly `npm i -g <pkg>` error (verified end-to-end).
  - ./sdk imports once its optional peers are present (24 exports, no warns).
  - Bundled ajv + ajv-formats validate with no ajv installed; no unguarded
    native runtime requires (fsevents absent in chokidar 4; bun:sqlite Bun-only).

Trade-off: image reads, AWS Bedrock, Azure Foundry and GCP/Vertex now prompt
a one-time `npm i -g <pkg>` instead of being shipped to every user.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

Review fixes (CodeRabbit + jatmn):
- validate-externals: the INTENTIONALLY_BUNDLED exemption is now scoped per
  bundle. The CLI exempts every bundled package; the SDK does NOT exempt
  packages declared as peerDependencies (keyed on package.json, an independent
  source of truth) so dropping react/@anthropic-ai/sdk from SDK_EXTERNALS now
  fails validation instead of silently passing. Added an explicit minimal-
  install contract check: bundled packages must be devDependencies-only — never
  in `dependencies`, and only the SDK-external subset may be optional peers.
  Validation logic extracted to scripts/externalsValidation.ts + tests.
- FileReadTool oversized-image fallback now loads via the shared
  getImageProcessor() (not a raw import('sharp')) and re-throws
  ImageProcessorUnavailableError, so a missing processor surfaces the
  `npm i -g sharp` install hint instead of returning an over-budget image.
- optionalRuntimeModule: match the missing specifier as a QUOTED token, not a
  raw substring, so a missing transitive package whose name contains the
  requested one (sharp vs sharp-libvips, @aws-sdk/client-bedrock vs
  @aws-sdk/client-bedrock-runtime) no longer triggers the wrong install hint.
  Predicate extracted to isMissingSpecifierError() with regression tests.
- docs/advanced-setup.md: the Vertex auth section now shows both documented
  paths (gcloud ADC and a GOOGLE_APPLICATION_CREDENTIALS service-account file).

Review fixes (round 2, CodeRabbit):
- validate-externals: assert the optional-peer install contract — every
  peerDependency must be { optional: true } in peerDependenciesMeta
  (validateOptionalPeers), so losing that flag fails the build instead of
  silently reintroducing install warnings.
- validate-externals: hard-check OPTIONAL_RUNTIME_EXTERNALS placement
  (validateOptionalRuntimeexternals). Anything esbuild can see statically must
  stay external in BOTH bundles (dropping sharp/google-auth-library now fails);
  the runtime-indirection-only subset (new RUNTIME_INDIRECTION_ONLY_EXTERNALS)
  must stay OUT of externals so esbuild never re-exposes their static imports.
- Deeper-dig fix: @anthropic-ai/foundry-sdk was misclassified as
  INTENTIONALLY_BUNDLED, but it is loaded only through the Function indirection
  (esbuild never sees it, so it was never actually bundled) — its sole presence
  in dist is the specifier string. Per the PR's own "Azure Foundry now prompts"
  trade-off it is on-demand, so it now lives in OPTIONAL_RUNTIME_EXTERNALS +
  RUNTIME_INDIRECTION_ONLY_EXTERNALS (mirroring bedrock-sdk). sandbox-runtime is
  genuinely statically imported, so it stays bundled.
- Provider-routing coverage (scripts/optionalRuntimeSpecifiers.test.ts): a
  static scan asserts every importOptionalRuntimeModule specifier is a declared
  OPTIONAL_RUNTIME_EXTERNAL and never also INTENTIONALLY_BUNDLED — the
  invariant that keeps a provider's optional package loadable on demand.
- All new validators extracted to scripts/externalsValidation.ts with tests.

Review fixes (round 3, CodeRabbit):
- client.ts: gate the Vertex google-auth-library import behind the non-skip
  branch. CLAUDE_CODE_SKIP_VERTEX_AUTH (proxy/test) uses a mock GoogleAuth and
  must not require the optional package; it was loaded unconditionally before.
- optionalRuntimeModule: drop the hard-coded `npm i -g`. The helper backs both
  the global CLI and project-local ./sdk consumers, so the hint is now
  context-neutral ("npm install <pkg>" / add -g for the global CLI).
- validate-externals: every SDK_ONLY_EXTERNALS entry must STAY a
  peerDependency (a dropped peer leaves runtimeDeps while the SDK still
  externalizes it); and OPTIONAL_RUNTIME_EXTERNALS must never be shipped (fail
  on overlap with dependencies/peerDependencies). Both with tests + live-verified.
- optionalRuntimeSpecifiers.test: pin the EXACT set of optionally-loaded
  specifiers instead of a >=5 count (a count passes even if a provider path
  regresses).
- attachments: extract tryReadEditedImageAttachment() — background watched-file
  image attachments DEGRADE to null on any failure (incl.
  ImageProcessorUnavailableError) so a missing optional package never aborts a
  turn, while the explicit FileReadTool path still surfaces the install hint.
  Deterministic regression test (bad path -> null).
- docs: Bedrock row notes profile-based auth also needs
  @aws-sdk/credential-providers; install-hint wording matches the new message.

Review fixes (round 4, CodeRabbit):
- attachments: stop sending the raw file path through the analytics
  bypass-cast (tengu_watched_file_compression_failed). Send only the safe
  file extension via getFileExtensionForAnalytics, matching the existing
  tengu_file_read_dedup pattern, so no usernames/project paths can leak.
- externals.ts: corrected the OPTIONAL_RUNTIME_EXTERNALS header comment,
  which still claimed all entries "remain in COMMON_EXTERNALS" — no longer
  true since the indirection-only subset (bedrock/foundry) must stay OUT of
  the externals lists.

(Other CodeRabbit comments on this push re-surface items already addressed in
prior commits: the peerDependenciesMeta-optional check (validateOptionalPeers),
the SDK-peers-present and optional-not-shipped validator rules, the
exact-specifier-set test, the attachments degrade contract + test, and the
context-neutral install hint are all present. The "assert every optional
external is a devDependency" suggestion is intentionally NOT applied: @aws-sdk/*
and @azure/identity are transitive devDeps via bedrock-sdk/foundry-sdk, so a
blanket assertion would be incorrect; source resolution is covered by the
build + tests that import these packages.)

Review fixes (round 5, CodeRabbit):
- attachments: stop leaking file paths via logError in the background-image
  degrade path. readImageWithTokenBudget can throw path-bearing messages
  (e.g. "Image file is empty: <path>") and logError persists message/stack, so
  log only the error TYPE name now. (Analytics payload was already sanitized.)
- attachments: tryReadEditedImageAttachment takes an injectable reader so the
  degrade contract is tested for the EXACT error types — ImageProcessorUnavailableError
  and a path-bearing read error both degrade to null (not just ENOENT) — plus a
  success case. No mocking.
- validate-externals: enforce the source-install half of the optional contract.
  Non-transitive OPTIONAL_RUNTIME_EXTERNALS must be devDependencies so `bun
  install` source builds resolve them. The new TRANSITIVE_OPTIONAL_EXTERNALS
  documents the exemption (@aws-sdk/* via @anthropic-ai/bedrock-sdk, @azure/identity
  via @anthropic-ai/foundry-sdk — provided transitively, not direct devDeps). A
  blanket "all optionals are devDeps" check would have wrongly failed on those.
  Tests + live-verified (dropping sharp from devDependencies now fails).

Review fixes (round 6, CodeRabbit + jatmn):
- optionalRuntimeSpecifiers.test: the call-site scan regex missed
  generic-annotated calls (importOptionalRuntimeModule<...>(...)) in
  model/bedrock.ts and tokenEstimation.ts, so the exact-set assertion was
  incomplete. Regex now allows an optional generic; EXPECTED_SPECIFIERS adds
  @aws-sdk/client-bedrock and @aws-sdk/client-bedrock-runtime (7 total).
- importOptionalRuntimeModule default generic is now <T = unknown> (was any),
  so destructured imports are no longer silently any. Every call site now
  supplies its module type — typeof import('<pkg>') where the package is
  type-resolvable (bedrock-sdk, foundry-sdk, @aws-sdk/credential-providers,
  google-auth-library), and a named minimal-shape alias for @azure/identity
  (not a direct devDep, so typeof import can't resolve it). This gives
  compile-time verification of each provider's module contract (export names,
  shapes) — the structural answer to the "cover the provider branches" ask.
- attachments: tryReadEditedImageAttachment takes injectable {read,log,track};
  a new test asserts the sanitized-telemetry contract directly — the logError
  payload is path-free and the analytics payload carries only `ext`, never the
  edited-image path.

* fix(deps): address optional runtime review findings

* test(deps): isolate optional runtime importer mocks

* fix(deps): clarify AWS optional auth labels

* fix(deps): close optional runtime review gaps

---------

Co-authored-by: jatmn <the@jat.mn>
2026-07-07 13:19:39 +08:00
Kevin CodexandGitHub 2a506f9f38 added tencent hy3 to opengateway available models (#1876) 2026-07-07 13:18:26 +08:00
203f05538e fix(build): shim jsxDEV when bundling production React — TUI rendered nothing (#1863)
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>
2026-07-05 21:24:12 +08:00
77c0a0d780 feat(ux): honest feedback pass — visible retries, statusline truncation marker, hint grace period (#1862)
* 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>
2026-07-05 19:31:14 +08:00
Kevin CodexGitHubOpenClaudecoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2083d1cdff fix(openai-shim): recover GLM/Qwen XML tool calls emitted as text (#1791)
* fix(openai-shim): recover GLM/Qwen XML tool calls emitted as text

GLM/Qwen-family models routed through OpenAI-compatible gateways emit
tool calls as XML text (`<tool_call><function=…>`) instead of structured
`tool_calls`. The shim had no parser, so these leaked into visible prose
and never executed — the turn ended with no tool_use block and the agent
appeared to "forget" and stop mid-task.

Add `parseXmlToolCalls` covering the three dialects seen in the wild
(function/parameter, GLM-native arg_key/arg_value, Hermes JSON), wired
into both the streaming and non-streaming paths. The streaming path
holds back text from `<tool_call>` onward (incl. an opener split across
SSE deltas), converts it to tool_use blocks at finalize, and flips the
finish reason to tool_calls — mirroring the existing Ollama fallback.
Structured tool_calls and false-positive prose are handled losslessly.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* Update src/services/api/openaiShim.ts

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-06-26 10:48:57 +08:00
13f7401541 perf(repl): batch streaming text, cache normalize, coalesce config writes (#1744)
Three independent runtime hot-path wins for long sessions and fast streams:

- Streaming text (REPL.tsx): the Ink root is a LegacyRoot, so every
  per-delta setStreamingText committed a synchronous REPL render. The
  preview hides the in-progress trailing line, so deltas between newlines
  changed nothing on screen yet still re-rendered. Hold the full text in a
  ref and publish to state only when the newline-truncated preview changes
  (or on clear), dropping those no-op renders under 100-300 delta/sec
  streams. Displayed text is byte-identical; the Esc-interrupt path reads
  the ref so no trailing partial line is lost.

- Incremental normalize (messages.ts + Messages.tsx): normalizeMessages
  flat-maps the whole transcript on every append (O(n) over 2,800+
  messages). normalizeMessagesCached memoizes per-message output keyed on
  (message identity, isNewChain entry flag) in a WeakMap, so unchanged
  messages are reused with stable object identity (reviving downstream
  memo/WeakMap bailouts and cutting GC pressure). It is an allocation/
  identity optimization — the call still scans the list, it is not an O(1)
  append. Proven equivalent to normalizeMessages in tests.

- Coalesced config writes (config.ts): saveGlobalConfig does a sync
  lock+reread+backup+fsync per call. saveGlobalConfigDeferred queues the
  updater, write-throughs the cache for read coherence, and folds the
  batch into one locked write on a 500ms debounce (flushed on cleanup and
  process exit). Auth-loss guard, lock and backup stay on the disk path.

Also fixes handleMessageFromStream's input_json_delta to update tool input
in place instead of reordering the updated tool to the array tail.

Review fixes (CodeRabbit + jatmn):
- REPL onStreamingText updates streamingTextRef BEFORE the showStreamingText
  guard so reduced-motion / cursor-up-yank-bug terminals preserve partial
  assistant output on Esc. The publish decision is extracted to a pure
  helper (streamingTextPublish.ts) with streamingTextPublish.test.ts, and
  replStreamingTextClear.test.ts source-scans REPL.tsx to assert both
  turn-boundary paths fully clear the streaming refs (no stale re-append).
- saveGlobalConfigDeferred primes the cache (getGlobalConfig) before
  enqueueing, and a direct saveGlobalConfig flushes pending deferred writes
  first. Together these keep same-process reads coherent: the first deferred
  counter update is visible immediately even on a cold cache, and a direct
  save can no longer clobber a queued delta with a disk snapshot. Regressions
  in deferredConfigWrites.test.ts.
- Extracted the deferred-write queue/debounce/write-through/drain into a
  generic, disk-free engine (deferredConfigWrites.ts) with injected
  storage/scheduler; deferredConfigWrites.test.ts exercises the real branch.
- config.deferredWrite.test.ts loads config via a query-suffixed specifier
  so a leaked mock.module('./config.js') can no longer silently turn its
  assertions into no-ops; it always exercises the real path.
- messages.streamingToolUses.test.ts covers interleaved input_json_delta
  order preservation; messages.normalizeCached.test.ts now covers the
  reused-message entryFlag true<->false cache transition.
- Corrected the normalizeMessagesCached comment (O(n) scan / allocation
  optimization, not an O(1) append).

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-25 14:06:46 +08:00
a723540163 perf(build): minify the CLI bundle (whitespace + syntax, keep identifiers) (#1743)
dist/cli.mjs shipped unminified at 21.7MB; whitespace+syntax minification
cuts it to ~16MB (-26%) and shaves V8 parse time on every invocation.
Identifier mangling stays off because the codebase matches
constructor.name (errors.ts, toolExecution.ts, useCanUseTool). The SDK
bundle stays unminified — its React/Ink leak check greps import syntax
that minification would rewrite.

The bundle guard's missing-module tripwire relied on Bun's
`// missing-module-stub:<path>` module-boundary comments, which
minification strips. The stub loader now also emits the marker as a
side-effecting string push (survives treeshaking and syntax-minify), and
the guard parses both forms.

Review fix (CodeRabbit + jatmn): the marker parser previously truncated
paths at the first backslash or space, so a JSON-escaped Windows marker
like "missing-module-stub:C:\\Users\\Jane Doe\\...\\src\\...\\foo.js" was
captured as a useless `C:` (or `C:\\Users\\Jane`) fragment and canonicalized
to the wrong key — letting a newly stubbed module slip past the tripwire on
Windows/spaced build hosts. Parse each marker form to its correct
terminator instead: the string literal runs to its matching (back-ref)
closing quote consuming escaped pairs, and Bun's comment runs to end of
line. Extract canonicalStub() + the parser into scripts/stubMarkerGuard.ts
so the logic is unit-testable, and add regression tests for Windows,
spaced, comment-form, and multi-marker-per-line cases.

Verified: build green, bundle ~16MB minified, guard passes against the real
bundle, stub-guard tests pass, --version works through the minified bundle.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-25 12:34:59 +08:00
a1a3cfc31e perf(integrations): load descriptor catalog lazily on first registry read (#1742)
Split the generated output into integrationManifest.generated.ts (plain
preset metadata, type-only deps) and integrationArtifacts.generated.ts
(the ~10k-line descriptor graph). Add a setRegistryLazyLoader() hook in
registry.ts so read getters evaluate the catalog on first access instead
of eagerly at module load — cutting startup cost on paths that never read
the registry (index.ts previously eval'd 60+ descriptor modules at import).

Review fixes:
- registry.ts ensureLoaded() keeps the lazy loader armed until it
  succeeds (re-entrancy guard + clear-on-success), so a throwing lazy
  import retries on the next read instead of leaving the registry empty.
- providerSecrets.ts no longer statically imports the heavy descriptor
  artifacts. It sat on the bootstrap startup chain
  (bootstrap -> providerConfig -> providerProfile -> providerSecrets), so
  the static import undercut the lazy-loading goal. Import
  PROVIDER_PRESET_MANIFEST from the lightweight manifest module and lazily
  require() the descriptor arrays inside readDescriptorCredentialEnvKeys()
  (cached by getKnownProviderSecretEnvKeys(), so it runs at most once).
- artifactGenerator.test.ts resolves artifacts by full
  src/integrations/generated/<name> path instead of tuple order; the last
  order-dependent test now uses splitGeneratedArtifacts().
- Added registry + providerSecrets regression tests (loader retry on
  failure; no static descriptor import on the bootstrap path).

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-25 11:22:13 +08:00
b8c34645c9 chore(deps): clean npm install — fix CVEs, silence warnings (#1782)
* chore(deps): clean npm install — fix CVEs, silence warnings

- bump undici 7.24.6 → 7.28.0 (7 high CVEs: TLS bypass, header injection,
  DoS, cache poisoning, SameSite downgrade, cross-origin routing)
- bump ws 8.20.0 → 8.21.0 (2 high CVEs: uninitialized memory disclosure,
  memory exhaustion DoS)
- add allowScripts for sharp + protobufjs to silence install-script warnings
- vendor node-domexception shim (re-exports native DOMException) and override
  the deprecated polyfill pulled transitively by google-auth-library →
  gaxios → node-fetch@3 → fetch-blob

Result: `npm install` reports 0 vulnerabilities, 0 warnings.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* chore(deps): update bun.lock for undici/ws bumps and node-domexception override

CI runs `bun install --frozen-lockfile`, which requires bun.lock to match
package.json. The previous commit bumped undici/ws and added the
node-domexception shim override but didn't include the regenerated lockfile,
causing frozen-lockfile CI to fail.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(publish): include vendor/node-domexception-shim in npm tarball

The file: override in package.json points at vendor/node-domexception-shim,
but the files array didn't list vendor/, so npm pack excluded it. End-user
npm installs would fail resolving the override.

Add vendor/node-domexception-shim/ to the files array. Verified via
npm pack --dry-run: tarball now contains both shim files (12 → 14 files).

Addresses reviewer finding #1.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-25 09:26:11 +08:00
d32b6f0476 fix(update): stop false "development build" block on npm installs with NODE_ENV=development (#1781)
`/update` and `openclaude update` reported "Auto-update is unavailable for
a development build." even when OpenClaude was correctly installed from npm,
as long as the launching shell had `NODE_ENV=development` exported.

Root cause: `getCurrentInstallationType()` checked `NODE_ENV === 'development'`
as its first branch, before any path-based detection. A user's shell env var
then downgraded a real npm install to 'development', which routed
`resolveUpdateStrategy()` to `{ action: 'blocked', reason: 'development' }`.

Two-part fix:

1. doctorDiagnostic.ts — move the `NODE_ENV === 'development'` check to a
   fallback position after all real-install path markers (bundled mode, local
   npm, npm-global paths, /npm/, /nvm/, `npm config get prefix`). Path
   detection runs first; NODE_ENV only classifies as 'development' when no
   install path matches (i.e. an actual source-tree `bun run dev` run).

2. bin/openclaude — the heap-sizing relaunch previously used
   `fileURLToPath(import.meta.url)`, which resolves symlinks. After relaunch,
   `process.argv[1]` pointed at the real file target (repo path for
   `npm install -g .`, package path inside node_modules for real installs),
   defeating path-based detection. Preserve `process.argv[1]` (the original
   invocation path, e.g. /usr/local/bin/openclaude or nvm bin symlink) so
   npm-global path markers can match correctly.

Verified: `bun run typecheck` passes; `openclaude doctor` now reports
npm-global (not development) with NODE_ENV=development set on a real
npm global install.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-25 06:52:58 +08:00
adafde30fa feat(integrations): add GLM 5.2 as an Opengateway-routed model (#1772)
* feat(integrations): add GLM 5.2 as an Opengateway-routed model

Add an `opengateway-glm-5.2` catalog entry (apiName `z-ai/glm-5.2`) to
the gitlawb-opengateway gateway, reusing the existing `glm-5.2` model
descriptor. Routes GLM 5.2 through the credit-billed Opengateway
(OpenRouter upstream) alongside the existing direct Z.AI vendor path.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* test(model): add z-ai/glm-5.2 to the opengateway picker fixture

The model-picker option-order assertion enumerates the gitlawb-opengateway
catalog; add the new z-ai/glm-5.2 entry (after qwen/qwen3.7-max) so it
matches the catalog.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-24 15:03:55 +08:00
1b1dfcfdbc Feat/ads sponsored tips (#1674)
* initial commit

* feat(ads): passive per-turn earning tips + mask earn code

- Gitlawb earning tips now appear on a per-turn cadence (default every 2nd
  tip slot, OPENCLAUDE_ADS_TIP_EVERY-tunable) for opt-in users, bypassing the
  per-startup sponsored gate that only ever showed one ad per session.
- Each shown earning tip fetches a real impression and confirms after the
  dwell, crediting opengateway credits; fails silent so ads never break the CLI.
- Mark /ads isSensitive so the earn code is redacted from history.

* feat(ads): mask earn code via paste dialog; never accept it inline

/ads on always opens a masked TextInput dialog (mask="*") — the code is
never typed inline, because the terminal echoes inline args as you type
(isSensitive only redacts history after submission, not the live echo). An
inline /ads on <code> now also opens the dialog and warns that the typed
code is exposed and should be rotated. Converts /ads to a local-jsx command.

* fix(ads): address CodeRabbit review on PR #1674

- ads.ts: hard 5s timeout (AbortController) on both fetchNextTip and confirmTip
  so a stalled connection can never hang the spinner-tip path; the abort timer
  is unref'd. (fetchWithProxyRetry forwards init.signal and treats AbortError as
  non-retryable.)
- gitlawbEarn.ts: unref the best-effort confirm timer so it can't keep a
  short-lived CLI run alive for the dwell window.
- ads.tsx: clear the stored earnCode on `/ads off` (it's a credential, no reason
  to keep it at rest after opt-out); fix the `/ads on` doc comment to match the
  always-masked-dialog behavior.
- ads.test.ts: restore ADS_BASE_URL in afterEach; add submit/cancel coverage for
  the masked dialog (enables + persists code / cancellation message) and assert
  `/ads off` clears the code.
- gitlawbEarn.test.ts: restore ADS_BASE_URL + OPENCLAUDE_ADS_TIP_EVERY in
  afterEach to stop env leaking across suites.
- tipScheduler.test.ts: cover the earning-tip branch precedence in
  getTipToShowOnSpinner (driven via the existing config mock + env, no module
  mock — avoids the bun mock.module cross-file leak).

Testing: tsc clean; tips+ads suite 36 pass; smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* feat(ads): contextual sponsored tips — share the latest prompt for ad matching

Switches the earning tips from generic to contextually-matched ads. When the
viewer enables sponsored tips (the /ads on dialog now discloses this), the
client sends their most recent prompt to the ads service, which matches a
relevant ad via Gravity's contextual endpoint.

- types.ts: TipContext gains `latestUserMessage` (opt-in earning tip only).
- REPL.tsx: extract the latest user message at the spinner-tip pick site and
  thread the full TipContext into tip.content() (previously only {theme}).
- gitlawbEarn.ts: pass ctx.latestUserMessage to fetchNextTip.
- ads.ts: new sanitizeForAds() redacts secrets/JWTs/emails/long hex and
  truncates to 500 chars; fetchNextTip POSTs { context:{messages:[user]} } when
  a prompt is present (else GET, identity-only).
- ads.tsx: /ads on dialog + enable message disclose that the recent prompt
  (secrets redacted) is shared with the ad partner — consent folded into enabling.

Privacy: explicit disclosed consent, minimal context (last prompt only),
sanitized client-side (the ads service re-bounds size server-side too).

Testing: tsc clean; new ads.test.ts (7 sanitize cases); ads+tips suite 43 pass;
smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(ads): show the real advertiser + ad click URL in earning tips

renderEarningTip hardcoded the Gitlawb name + gitlawb.com URL, so every served
ad rendered as "Sponsored · Gitlawb — … gitlawb.com" and discarded the actual
ad's `name` and `link`. Pass the served ad's advertiser name and click URL
(Gravity's tracker — required for click attribution/payout) into the renderer;
fall back to Gitlawb only for the static no-ad line.

Testing: tsc clean; tips suite 16 pass; smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* feat(ads): hyperlink the advertiser name instead of printing the tracker URL

Sponsored/earning tips render the advertiser as a terminal hyperlink to the
ad's click URL (Gravity's tracker) via a shared renderSponsorLink helper, rather
than printing the long tracker URL inline. Clicks still route through the
tracker, so attribution/payout are unchanged.

Testing: tsc clean; tips/ads suite 46 pass (incl. tipLink); smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(ads): address CodeRabbit review on the contextual ads changes

Security:
- tipLink.ts: sponsor name/url are advertiser-controlled (untrusted). Strip
  C0/C1 control chars from the name and accept only http(s) URLs (parsed via
  URL()), preventing terminal escape-sequence injection and javascript:/file:
  click targets.
- ads.tsx: soften the consent copy to "best-effort secret redaction" (both the
  dialog and the enable message) — sanitizeForAds is heuristic, not absolute.

Correctness:
- ads.ts: clamp dwell_ms to a finite, non-negative integer (malformed values no
  longer yield NaN/Infinity in the confirm-delay math).
- gitlawbEarn.ts: never point a third-party advertiser name at the Gitlawb URL —
  a real ad uses only its own click URL; the Gitlawb fallback is reserved for the
  static no-ad line.

Tests + isolation:
- ads.test.ts: cover fetchNextTip/confirmTip (POST-with-context vs GET, !ok→null,
  ad:null→null, dwell clamp, confirm normalization) via a stubbed fetch.
- tipLink: renderSponsorLink takes an injectable `hyperlinks` flag so both
  branches are deterministically tested; added control-char/unsafe-URL cases.
- gitlawbEarn.test.ts: restore global `ads` config in afterEach.
- tipScheduler.test.ts: preserve OPENCLAUDE_ADS_TIP_EVERY in cleanup.

Note: CodeRabbit's packages/memory/* findings are moot — that stray package was
removed from the branch.

Testing: tsc clean; ads+tips suite 56 pass; smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(ads): address CodeRabbit follow-up review (round 2)

- ads.tsx: remove warmOneEarn — it fetched AND confirmed a tip during /ads on
  that the user never saw, crediting an unshown impression (and could confirm
  after a quick opt-out). Earning now happens only on the per-turn rendered-tip
  path. Also drop the unsupported "Run /ads for your balance" instruction.
- ads.ts: normalize confirm amounts (earned_micro/balance_micro) to finite
  integers via toFiniteInt, mirroring the dwell_ms clamp.
- gitlawbEarn.ts: degrade to the static fallback when tip_text is blank, so a
  malformed ad never renders an empty line and credits a blank impression.
- ads.test.ts (commands): restore global ads config in afterEach; assert the
  inline-code exposure warning (warnExposed) is shown.
- ads.test.ts (services): build the AWS-shaped fixture at runtime so it isn't
  flagged by security:pr-scan; add a success-path test for buildEarningTip()
  rendering a fetched ad + a blank-ad fallback test.

Testing: tsc clean; ads+tips suite 58 pass; smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(ads): address CodeRabbit full review (round 3) + comments

Blocking:
- ads.ts: gate the serve response on a string `token` (the signed impression),
  not on an `ad` field. A served tip carries NO `ad` key, so CodeRabbit's
  suggested `data.ad == null` would have suppressed every real tip; the empty
  slot (`{ ad: null }`) and malformed responses both lack a token, so the token
  check covers them correctly. Documented the contract.
- gitlawbEarn.ts: drop the dead try/catch around fetchNextTip — it is
  contractually non-throwing (catches everything, returns null), so the wrapper
  was unreachable/misleading.
- ads.ts: make the base64-blob redaction boundary reliable — \b is meaningless
  around + and / (both \W), so bound the run with explicit look-around instead.

Comments / clarity (per review):
- ads.ts: note withAbortTimeout is a per-CALL deadline (shared across retries),
  not per-attempt.
- tipLink.ts: clarify the CONTROL_CHARS_RE comment — the ESC/BEL literals above
  are intentional terminal-sequence constants, not part of the matcher.
- config.ts: document that GlobalConfig.ads is managed via /ads and intentionally
  excluded from GLOBAL_CONFIG_KEYS (earnCode is a credential).

(resetEarningCadenceForTesting export is an accepted in-repo pattern — no change.)

Testing: tsc clean; ads+tips suite 58 pass; smoke green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-24 14:04:03 +08:00
4dee44a542 feat(ux): long-turn visibility + default-on stream hang safety net (#1758)
Long-running turns looked frozen ("are you still working?") and a dropped
network stream could hang for the full 5-minute QueryGuard idle timeout.
Three focused changes:

- Spinner: show the elapsed-time counter at 5s instead of 30s (split the
  timer gate from the token-count gate). The timer is wall-clock derived,
  so it keeps ticking during long tool calls — proof of life even when no
  tokens stream.
- Spinner: surface the currently-executing tool name in the status line
  (reusing the spinnerSuffix channel; stop-hook progress still wins). A
  long subagent/typecheck now reads "(↓ Bash · 1m 20s)" instead of a bare
  spinner.
- claude.ts: enable the stream idle-timeout watchdog by default, matching
  the always-on read-timeout already used by the OpenAI/Codex shims. A
  silently dropped Anthropic-family stream now aborts and falls back to a
  non-streaming retry within STREAM_IDLE_TIMEOUT_MS (90s) instead of
  hanging to 5 minutes. Opt out with CLAUDE_DISABLE_STREAM_WATCHDOG=1.

Testing: tsc --noEmit clean; bugfixes.test.ts (31) and src/services/api
(835) pass; bun run smoke builds + runs.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-24 07:27:16 +08:00
bcf9421824 feat(permissions): allow npm/bun/tsc --version as read-only (#1759)
`node --version` and `python --version` are auto-approved as read-only Bash,
but `npm`, `bun`, and `tsc` version queries were missing from the allowlist, so
they fell through to a permission prompt. Add them in the same exact-anchored
form (no trailing args) so a version flag can't smuggle a script-running suffix
past the check (the `node -v --run <task>` class of bypass).

Closes the only read-only gap that the now-superseded #787 classifier covered,
without a parallel classification surface.

Testing: new readOnlyValidation.test.ts (18 cases — allows -v/--version, rejects
install/suffixed forms); tsc clean; BashTool suite 117 pass.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-24 07:26:57 +08:00
dc6a7781bf feat: auto-detect and persist project conventions to wiki (#1010)
* feat: auto-detect and persist project conventions to wiki

Adds a convention scanner that reads project config files (package.json,
tsconfig.json, eslint, prettier, Dockerfile, CI workflows, lockfiles) on
startup and saves extracted conventions to .openclaude/wiki/pages/conventions.md.
Includes a fingerprint cache to avoid redundant writes and a /wiki scan
command for manual re-scans.

New modules:
- src/services/wiki/conventions.ts — scanner + cache + save
- src/services/wiki/identity.ts — project identity (name, languages, monorepo)
- src/services/wiki/conventions.test.ts — 7 tests

Modified:
- paths/types/init/status — extended wiki infrastructure for conventions
- wiki.tsx/index — added /wiki scan command
- main.tsx — fires scan via startDeferredPrefetches
- init.test.ts/status.test.ts — updated for new conventions page/fields

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(wiki): address PR #1010 review — trust gate, cache, pre-init, indexing

Resolves the three blockers raised by @jatmn and @Vasanthdev2004, plus a
post-rebase integration gap.

1. Trust gate (blocker): the startup convention scan ran git (via project
   identity) before workspace trust was established. Now gated behind the same
   check as prefetchSystemContextIfSafe — runs only when non-interactive
   (implicit trust) or the trust dialog has been accepted.

2. Cache fingerprint (blocker): computeFingerprint hashed only the detected
   config sections, so identity-only changes (project name, language counts,
   monorepo, default branch) left conventions.md stale. Identity inputs are now
   folded into the fingerprint. (Hashing the rendered markdown isn't viable — it
   embeds a "Last scanned" timestamp.)

3. /wiki scan pre-init crash (blocker): forceScanConventions caught the page
   write failure but still wrote the cache, throwing ENOENT before /wiki init.
   It now only writes the cache on a successful page write and reports
   saved=false; /wiki scan surfaces "run /wiki init first".

4. Index integration: the conventions page is written to pages/, but the wiki
   index (rebuildWikiIndex, added after this PR was opened) was never refreshed,
   so the new page wasn't listed. Both save paths now reindex on success.

Testing: 3 new regression tests (identity-only re-save, pre-init no-crash/no-cache,
reindex); wiki suite 14 pass; tsc clean; smoke + knip green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(wiki): address CodeRabbit review on PR #1010

- main.tsx: add a terminal .catch(logError) to the deferred conventions-scan
  promise chain so a failed dynamic import or scan can't become an unhandled
  rejection on the startup path. (Major)
- conventions.ts: narrow both page-write catch blocks to ENOENT (= wiki not
  initialized → skip); rethrow other failures (EACCES, EROFS, …) instead of
  masking them as "not initialized". (Major)
- identity.ts: replace blocking execFileSync('git', …) with async execa
  (the service-layer subprocess convention); getProjectIdentity is now async,
  awaited in scanProjectConventions. Keeps the startup scan off the event loop.
- commands/wiki/index.ts: restore `ingest` in the argumentHint —
  `[init|status|scan|ingest <path>]` — it's still an implemented subcommand.

Testing: tsc clean; wiki suite 14 pass; smoke + knip green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-24 07:24:58 +08:00
c2467eedad feat(atlas-cloud): add GLM 5.2 to vendor catalog (#1755)
Adds zai-org/glm-5.2 with the same 202,752 context window as GLM 5.1
so the model is selectable through the Atlas Cloud provider.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-23 10:06:30 +08:00
c4aa756689 feat(commands): add /update command with package-manager auto-detection (#1687)
Adds a `/update` slash command that updates OpenClaude to the latest
published version, routing by how the running install is actually
managed so it updates the installation the user is running.

`globalPackageManager.ts` detects the owning package manager (npm,
yarn, pnpm, bun) for npm-style installs and maps it to the correct
global-install command. `installGlobalPackage()` and `getLatestVersion()`
now consume it, so the legacy `openclaude update` CLI and the background
auto-updater gain yarn/pnpm support; `getLatestVersion()` also falls
back to a direct npm-registry HTTP lookup when npm isn't on the PATH.

`updateStrategy.ts` factors the install-type routing and the
third-party-build guard out of `src/cli/update.ts` (now shared by both
entrypoints). `/update` uses it to refuse development/third-party builds,
point package-manager/native/local installs at their safe update paths,
and only do a global npm install when that's what's actually running —
instead of always installing a stray global package.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-18 09:50:12 +08:00
a36ef463ce docs(readme): add social links and clarify license line (#1660)
- Add Discord (discord.gg/k68zFR6AcB) and X (x.com/gitlawb) as shields.io
  badges in the top badge row and as descriptive links in the Community section.
- License section now notes contributor modifications are MIT while the derived
  Claude Code remains Anthropic's, with a "See more" link to LICENSE.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-16 09:14:52 +08:00
00ff6de4ca fix(suggestions): stop slash-command dropdown freezing on a throwing getter (#1657)
Typing a multi-character slash command (e.g. /provider) left the dropdown
frozen on the bare "/" list with /simplify highlighted. Root cause: the
/sandbox command's `get description()` read
`SandboxManager.checkDependencies().errors`, but checkDependencies() returns
null at runtime, so `.errors` threw. That getter runs for every command while
building the Fuse search index (only for non-empty queries — bare "/" returns
before the index is built), so the throw rejected the whole updateSuggestions()
call and the list never narrowed.

Fixes:
- sandbox-toggle: null-guard checkDependencies() so the getter can't throw.
- commandSuggestions: make index building resilient — a single command whose
  description/isHidden/aliases getter throws degrades gracefully (safe
  fallback) instead of breaking suggestions for every command; a command whose
  name can't resolve is dropped. Broken getters now leave a one-time `warn`
  debug breadcrumb instead of failing silently.
- useTypeahead: command results re-rank every keystroke, so the highlight
  snaps to the top/best match (selectedSuggestion = 0) instead of following the
  previously selected command by id (which made it stick to /simplify).

Tests:
- New sandbox-toggle getter tests (null/present/missing deps, via spyOn).
- New commandSuggestions tests: narrowing, throwing description/isHidden/name
  getters, broken command still listed, and best-prefix-match-first ranking.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-16 06:54:07 +08:00
f4c3be850e chore: remove dead code and add knip gate to CI check (#1612)
Delete 32 unreferenced source files (~4,000 lines) verified dead by
import-specifier grep and knip: test-only token utilities, orphaned hooks
(useTaskListWatcher, useSkillImprovementSurvey + its component), the
removed DevBar and ConfigTool UIs, unregistered bundled skills (stuck,
verifyContent), unused analytics sinks, the benchmark command, and
stale migrations/helpers.

Remove unused dependencies code-excerpt, stack-utils, and tsx from
package.json plus their entries in build stub/external lists.

Add knip with a tuned knip.json (entrypoints, build-time stub targets,
subprocess-launched fixtures, and runtime-string-imported SDKs ignored;
providerAutoDetect kept intentionally as provider pre-wiring) and wire
`bun run deadcode` into the `check` script so dead code stays dead.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-14 10:13:37 +08:00
a3a3c3659d perf(cli): restore --version fast path with dynamic provider imports (#1611)
The static imports of providerProfile.js and providerValidation.js at the
top of cli.tsx side-effect-loaded the entire integrations graph (~11.7k
lines of vendor/gateway/model descriptors) at module evaluation, defeating
the zero-import --version fast path. Convert them to dynamic imports at
their use sites, matching the file's existing convention.

--version: ~0.47s median (0.35-0.60s) -> steady 0.26s.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-13 16:11:39 +08:00
d08593de92 feat(web): rebuild landing as Astro static site with gitlawb theme and full docs (#1606)
* feat(web): rebuild landing as Astro static site with gitlawb theme and full docs

Replaces the Vite+React SPA in web/ with a fully static Astro 6 site:

- gitlawb-aligned design system: dark-default monochrome surface tokens,
  Geist Mono, hairline grids, [light]/[dark] toggle — keeping the orange
  #ff7a1a openclaude accent (#e85d00 in light mode for AA contrast)
- 9-page docs section: installation, quickstart, providers, slash
  commands (all 69 user-facing commands with argument hints), CLI
  reference (every non-hidden flag), configuration, keybindings, skills —
  rendered from typed data files seeded from the CLI source
- SEO: per-page canonicals/OG/Twitter, JSON-LD (SoftwareApplication,
  TechArticle, BreadcrumbList), @astrojs/sitemap, robots.txt, and
  generated 1200x630 OG cards, all on https://openclaude.gitlawb.com
- zero framework JS: theme toggle, copy buttons, mobile nav, and TOC
  highlighting are small vanilla scripts
- CI-compatible: same typecheck/build script names, bun.lock regenerated

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* feat(web): redesign logo as gitlawb-aligned circuit mark

White terminal face (hollow node eyes, >_ prompt mouth) on a black
square with an orange git-fork trace descending to two commit nodes —
same stroke language as the gitlawb mark. Adds the SVG source as the
favicon and regenerates all three OG cards with the new logo.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(web): address review feedback on copy button a11y and css lint

- CopyCommand: generic aria-label (component is reusable, not
  install-specific) and a visually-hidden role="status" live region so
  screen readers announce the copied state
- global.css: lowercase text-rendering keyword, blank line before
  color-scheme, kebab-case fade-up keyframe name

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-12 23:54:24 +08:00
2002e4c116 UI/upgraded UI (#1605)
* feat(ui): OpenClaude brand identity — orange accent, wordmark, dot-pulse spinner

Give OpenClaude a real visual identity instead of reskinned upstream visuals:

- theme.ts: add brand/brandShimmer keys and repoint the claude family
  (claude, claudeShimmer, clawd_body, briefLabelClaude) to the gitlawb
  orange #ff7a1a across all 6 themes — darkened variants for light
  backgrounds, luminance-separated variants for daltonized themes,
  redBright floor for 16-color ANSI themes. Values stay rgb() strings
  (parseRGB in Spinner/utils.ts silently fails on hex).
- brand.ts (new): BRAND_NAME, BRAND_TAGLINE, accent constant, and a
  2-row Unicode half-block OPENCLAUDE wordmark split for two-tone render.
- LogoV2: full logo shows the wordmark + centralized tagline;
  CondensedLogo drops the duplicated OPEN CLAUDE header and renders a
  brand-colored name+version line; Clawd mascot body now clawd_body.
- Startup splash: new 'ember' gradient palette (brand orange) as the
  default (/logo keeps sunset et al.); tagline centralized from brand.ts.
- Spinner: dot-pulse glyph sweep (· ∘ ○ ◎ ◉ ●) replacing the asterisk
  set (drops Ghostty/darwin special-cases); defaults switched to
  brand/brandShimmer. Reduced-motion and stall interpolation unchanged.
- figures.ts: TEARDROP_ASTERISK marker → ◎ to match the new glyph family.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* feat(ui): built-in default statusline

Ship a status bar that renders when no custom /statusline command is
configured: `model · ctx % · session cost · rate limit`. Previously this
slot was empty unless the user wired up their own shell command.

- BuiltinStatusLine.tsx (new): pure segment builders
  (buildBuiltinStatusSegments / fitSegments — drops lowest-priority
  segments right-to-left as the terminal narrows) plus a memoized
  component reusing the exact data pipeline the custom StatusLine feeds
  to user commands (getRuntimeMainLoopModel, getCurrentUsage,
  calculateContextPercentages, getTotalCost, getRawUtilization). Pure
  in-process computation — no subprocess, no debounce.
- Context % colors warning ≥70 / error ≥90 (aligned with auto-compact
  warnings); rate limit shows the worst of the 5h/7d windows and is
  absent (not 0%) for API-key users; cost hidden at $0.
- PromptInputFooter: custom statusline always wins; the built-in bar
  also suppresses the "? for shortcuts" hint like the custom one does.
- defaultStatusLineEnabled global-config toggle (default on) surfaced in
  /config settings.
- Unit tests for segment building, narrow-width fitting, threshold
  colors, and custom-statusline precedence.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* feat(ui): polish pass — fuzzy match highlighting, completion flash, diff gutter, dialog hints

- Fuzzy pickers: new highlightFuzzyMatch (contiguous-run or greedy-
  subsequence, bold so the focused row's color survives) applied to
  Quick Open and prompt-history results. FuzzyPicker now passes the live
  query as a third renderItem argument so memoized render callbacks
  can't capture a stale query.
- Select: inline option descriptions now truncate with an ellipsis on
  narrow terminals instead of clipping at the edge (row made shrinkable,
  description wrapped in truncate-end).
- CompletionFlash (new): static `✓ Done · 12s` row for ~1.5s after a
  response completes. Keys off the isLoading transition (the spinner
  also hides mid-turn for streaming text); same row footprint as the
  spinner; suppressed for sub-second turns, brief mode, open
  permission/prompt queues, running teammates, and reduced motion.
- Diff gutter: line numbers on +/- lines are now dimmed (previously
  full decoration intensity competed with content, worst on light
  themes); +/- sigils keep full intensity. Mirrored in the
  non-highlighted Fallback renderer.
- Dialog: optional showNavigationHint prepends "↑/↓ navigate" to the
  default input guide; opted in from Select-hosting dialogs
  (IdleReturn, DevChannels, WorktreeExit). Dialog rewritten as plain
  React (was react-compiler output) to take the new prop safely.
- Spinner: the ↑/↓ request-direction glyph now leads the status parens
  whenever any status shows — previously buried in the tokens part,
  which only appears after 30s; width gating reserves its space.
- Replaced the stale `/`-search TODO in ScrollKeybindingHandler with a
  pointer to the shipped REPL transcript search.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(ui): address review — explicit statusline default, narrow-terminal thinking gate

- createDefaultGlobalConfig now sets defaultStatusLineEnabled: true
  explicitly, matching the factory's other default-true booleans (read
  sites already coalesced to true; no behavior change).
- SpinnerAnimationRow: the width gate reserves mode-glyph space that a
  thinking-only spin never uses; on narrow terminals where nothing else
  renders, re-try the thinking gate with that space returned so
  '(thinking)' shows instead of nothing.

CompletionFlash NaN guard was reviewed and not applied: both refs are
useRef(0) — initialized numbers by type and construction — and the
active→inactive transition guard guarantees the start time was set
before the elapsed math runs.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(ui): address review round 2 — hint accuracy, flash suppression, coverage

- Dialog: the cancel shortcut hint now renders only when isCancelActive,
  matching the keybinding it advertises.
- PromptInputFooter: new exported resolveFooterStatusLine(settings,
  guards, config?) is the single source of truth for which status line
  renders — custom wins over builtin, and all render guards (prompt
  mode, short fullscreen, exit message, pasting) force null. suppressHint
  and the render now share it, so '? for shortcuts' is suppressed only
  when a status line actually renders.
- CompletionFlash: suppression now clears an already-active flash (the
  effect resets state and the render guard skips the commit-gap frame)
  instead of only preventing new ones.
- REPL: flash suppression reuses the existing hasActivePrompt aggregate,
  which covers the sandbox, worker-sandbox, and elicitation queues the
  hand-rolled expression missed.
- builtinStatusLineShouldDisplay takes an injectable config (defaults to
  getGlobalConfig()) so the config-off path is testable without module
  mocks; tests added for config-off, custom-wins-regardless, and the
  resolver's full variant x guard matrix.

Validation: typecheck exit 0; full suite 3733 green; smoke + bundle
guard green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(ui): drop fit-budget floor, cover reduced-motion render gap

- BuiltinStatusLine: remove the artificial 10-column floor on the fit
  budget — fitSegments returning [] is the signal that nothing fits,
  and the empty branch already handles it (row-reserve in fullscreen,
  null otherwise). The floor forced a truncated segment onto panes
  narrower than the model name.
- CompletionFlash: the render guard now also skips the one frame
  between reducedMotion flipping on and the effect clearing the flash,
  same as the suppressed case.

Component-level mount tests for PromptInputFooter were considered and
deliberately not added: the footer needs 10+ mocked contexts to mount,
mock.module harnesses leak across bun test files in this repo, and the
branching under review is exactly the pure resolveFooterStatusLine
contract already pinned by tests.

Validation: typecheck exit 0; full suite 3733 green; smoke + bundle
guard green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-12 21:59:32 +08:00
94d2a6a503 ci: split typecheck into its own PR-checks job (#1599)
The Typecheck step lived inside the smoke-and-tests job and
typecheck:type-tests ran inside `bun run check`, so type errors were
buried mid-job and serialized behind the build. They now run as a
dedicated parallel `typecheck` job (tsc --noEmit + the focused type
tests) with its own status check, and `check` slims to
smoke + test:full so nothing runs twice in CI. Local scripts
(typecheck, typecheck:type-tests, hardening:strict) are unchanged.

Review feedback: the new job's checkout sets
persist-credentials: false (no credentials needed), and
CONTRIBUTING.md now documents typecheck as a CI-enforced check
instead of a recommended-local-only one.

Validation: workflow YAML parses (jobs: smoke-and-tests, typecheck,
web); typecheck exit 0; type-tests green; `bun run check` green.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-12 10:55:11 +08:00
9755550137 Typecheck/zero tsc errors (#1597)
* ci(typecheck): add error-count ratchet toward zero tsc errors

tsc --noEmit currently reports 697 pre-existing errors (issue #473), so
PRs cannot be gated on a clean typecheck yet. This adds
scripts/typecheck-ratchet.ts and a per-file baseline: CI fails when the
count rises above the baseline (listing exactly which files regressed),
passes at or below it, and --update lowers the baseline to lock in
gains. Wired into pr-checks as its own step; once the baseline reaches
zero the step becomes a plain `bun run typecheck`.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(typecheck): mechanical sweep — 697 → 624 tsc errors

Type-only fixes with no runtime behavior change, except the deliberate
NODE_ENV restorations:

- Restore process.env.NODE_ENV comparisons that the source snapshot had
  baked into the literal "production", making the conditions constant
  (AutoUpdater dev/test skip, useTypeahead, ink devtools injection,
  interactiveHelpers onboarding skip, TestingPermissionTool.isEnabled —
  the last now correctly enables under bun test, +3 tests run green)
- Type stream read helpers in openaiShim/codexShim as
  Bun.ReadableStreamDefaultReadResult<Uint8Array<ArrayBuffer>> and
  annotate throwClassifiedTransportError as never-returning, clearing
  the reader/response undefined cascades (29 errors)
- Delete 14 stale @ts-expect-error directives
- Widen useState/useRef/array generics inferred from null/[] literals
- as-const notification priority/color literals to match Priority
- Accept readonly Tool[] in checkLocalModelContextLoad/getCombinedTools

Baseline lowered via typecheck:ratchet --update; full suite green
(3690 tests), smoke + bundle guard green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(typecheck): recreate missing modules — 624 → 415 tsc errors

The open snapshot never mirrored ~60 modules; the bundler noop-stubs
them at build time (() => null named exports), so every recreated
module here is runtime-inert by construction: no import-time side
effects, gated features stay off (isAssistantMode/isSkillSearchEnabled
→ false, tools isEnabled → false, dialogs render null), lookups return
empty, telemetry no-ops. Types are honest and derived from importer
usage — no any.

Highlights:
- sdk: runtimeTypes re-exports/aliases, sdkUtilityTypes
  (NonNullableUsage), settingsTypes.generated; coreTypes.generated
  usage fields regenerated as a self-contained structural type (the
  consumer package ships without sdkUtilityTypes/@anthropic-ai/sdk, so
  the generated file must stay dependency-free — generator override
  updated to match, package-consumer-types tests green)
- services: contextCollapse operations/persist/stats, compact
  cachedMicrocompact state/types + reactiveCompact, skillSearch (7
  modules), oauth/types, lsp/types, sessionTranscript
- cli/server/daemon: Transport interface, parseConnectUrl, server/*
  (7), daemon/*, bg/templateJobs/runners; assistant/* (KAIROS), ssh/*
- tools/components: WorkflowTool trio, ReviewArtifact pair,
  OverflowTest/TerminalCapture/VerifyPlanExecution/DiscoverSkills,
  WebBrowserPanel, task dialogs, message variants, ink events/cursor
- types: statusLine, fileSuggestion, notebook, messageQueueTypes;
  SerializedMessage rebuilt as distributed Omit-union so transcript
  guards narrow again; vitest-compat.d.ts mirrors Bun's runtime
  'vitest' → 'bun:test' aliasing
- TS2304 names: ant-model helpers imported from existing antModels.ts,
  inert Ultraplan/Gates/LogoV2 stubs, PromiseWithResolvers local type
- build.ts: ACCEPTABLE_RUNTIME_STUBS emptied — both grandfathered
  bundle-reaching stubs (MonitorMcpDetailDialog,
  VerifyPlanExecutionTool/constants) are now real typed modules, so
  the degrade-on-use debt the guard tracked is retired

Validation: full suite 3690 green, smoke + bundle guard green,
typecheck:type-tests green, sdk package-consumer tests green; baseline
lowered via typecheck:ratchet --update.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(typecheck): reconstruct Message discriminated union — 415 → 342 tsc errors

src/types/message.ts was a stub where all ~40 message type aliases were
'export type X = any'. Bare-any aliases break the one thing the union is
for: narrowing. Type predicates like isHookAttachmentMessage collapsed to
'never' in guard chains, cascading TS2339/TS2345 through utils/messages.ts,
messageFilters.ts, groupToolUses.ts, collapseReadSearch.ts, REPL.tsx,
compact.ts, stopHooks.ts and the message components.

Envelope design (permissive-body discriminated union):
- Each variant declares its literal discriminant(s) — message.type for the
  envelope union (user/assistant/attachment/progress/system), subtype for
  the 17-variant System family — plus the properties constructor functions
  in utils/messages.ts actually populate, with '[key: string]: any' as an
  escape hatch so unreconstructed properties never error.
- UserMessage<C> / AssistantMessage<T> are generic over content shape so
  NormalizedUserMessage / NormalizedAssistantMessage<T> reuse the envelope
  without Omit (Omit over an index-signature type collapses keyof to
  string and silently drops the discriminant, breaking narrowing).
- AssistantMessage.message is a structural AssistantMessageContent<T>, not
  the SDK's BetaMessage: synthetic constructors don't populate every
  SDK-required field (stop_details), and SDK-facing consumers need
  assignability to Record<string, unknown>-style bodies.
- AttachmentMessage<T = Attachment> / ProgressMessage<T = Progress> stay
  generic over their payloads (utils/attachments.ts and Tool.ts types).
- UI wrappers (GroupedToolUseMessage, CollapsedReadSearchGroup,
  CollapsibleMessage, RenderableMessage) and stream/control envelopes
  (StreamEvent over BetaRawMessageStreamEvent, RequestStartEvent,
  TombstoneMessage, ToolUseSummaryMessage) reconstructed from call sites.
- logs.ts SerializedMessage switched from the Omit<Message, never> trick
  (only sound against an any stub) to an Extract-based distributed union,
  keeping TranscriptMessage assignable to Message.

All other touched files are type-level-only adjustments (annotations on
evolving arrays that inferred never[], predicate types, casts in SDK wire
adapters and test fixtures) — no runtime logic changed anywhere; the full
bun test suite passes 3690/0 before and after.

Result: 415 → 342 tsc errors, every never-cascade in the message pipeline
resolved, no file above its per-file baseline (ratchet updated).

Part of issue #473.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(typecheck): narrow unknowns and fix signature drift — 342 → 94 tsc errors

Clears every remaining non-test error. Honest fixes dominate: evolving
array/let/useState/useRef annotations (the repo's noImplicitAny:false
disables evolving types), real type guards over unknown wire payloads,
hoisted react-compiler-style params annotated with their components'
real Props, and callee signature corrections (useRegisterOverlay
optional param, generic useVoiceState<T>, growthbook shim's accepted
refresh-interval param) that each cleared several call sites. Targeted
reason-commented casts only at SDK/stub/wire boundaries; no any, no
new suppressions.

Runtime deviations are confined to already-broken paths: benchmark.ts
imported a function name that never existed (module-load crash),
caches.ts called stub methods unguarded (TypeError for ant-gated
users), messageActions returned undefined from a string function;
CACHE_EDITING_BETA_HEADER is a best-effort reconstruction of a
squash-lost constant, reachable only behind feature-gated first-party
paths (flagged for review).

Also: ConnectorTextBlock gains its wire-proven optional signature
field; MCP server factory ambient types gain close(); ink
render-node-to-output's nodeType cast fixed (intersection was
collapsing the intended widening); upstreamproxy relay normalizes the
socket data union.

Validation: full suite 3690 green, smoke + bundle guard green;
remaining 94 errors are all in test files (PR 5). Baseline lowered via
typecheck:ratchet --update.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(typecheck): clean test typing, gate CI on zero tsc errors — 94 → 0

Closes the typecheck burn-down (issue #473): bun run typecheck now
exits 0 across the whole repo and CI fails on any new error.

Test typing: new src/test/typedMocks.ts centralizes the two bun:test
gaps (asMockFetch — Mock<T> lacks fetch.preconnect; callArgs —
argless-signature mocks collapse mock.calls to []). Beyond the
helpers, fixes are honest: discriminated-union narrowing before
member access, fixture typing with boundary casts, assertion-type
corrections, and two tests realigned to production signatures they
had drifted from (requestLogging logApiCallEnd args,
incrementalTokenCounter tokenBudget rename) with identical assert
outcomes. No assertion semantics changed; all touched suites pass.

CI: the ratchet served its purpose and is retired — pr-checks now
runs a plain `bun run typecheck` step; ratchet script and baseline
deleted.

Burn-down summary across the series: 697 → 624 (mechanical sweep) →
415 (recreate ~60 missing modules) → 342 (Message discriminated
union) → 94 (narrowing + signature drift) → 0 (this PR).

Validation: tsc --noEmit exit 0, full suite 3690 green, smoke +
bundle guard green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(typecheck): reconcile with upstream parallel typecheck fixes

Upstream landed #1591/#1592/#1595 while this series was in flight,
fixing some of the same errors differently. Rebase resolutions prefer
upstream where it is authoritative: their CACHE_EDITING_BETA_HEADER
value ('cache-editing-2025-12-01', unconditional) replaces this
series' feature-gated reconstruction; their cachedMicrocompact stub
shapes (with their new test file) replace ours, with boundary casts in
claude.ts where the stub's unknown[] edits meet the local pinned
delete-edit shape; their reader/ReadResult stream typing in openaiShim
replaces ours. MessageWithoutProgress now matches its name
(Exclude<NormalizedMessage, ProgressMessage>), reconciling upstream's
RenderableMessage GroupingResult with this series' message union; the
@ts-expect-error upstream added for settingsTypes.generated is removed
since the module now exists.

tsc exit 0; full suite 3697 green (incl. upstream's new
cachedMicrocompact tests); smoke + bundle guard green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(sdk): keep result usage counters required, fix assistant stub exports

Addresses jatmn's and chioarub's review on the typecheck PR:

1. SDK usage contract restored: the generated result types' usage now
   keeps input_tokens, output_tokens, cache_creation_input_tokens, and
   cache_read_input_tokens as REQUIRED numbers — result messages are
   populated from QueryEngine.totalUsage (initialized from
   EMPTY_USAGE), so they are always present at runtime and strict
   consumers may sum them without undefined guards. The richer nested
   metadata (cache_creation, server_tool_use, service_tier) is modeled
   explicitly instead of hiding behind the index signature; the nested
   objects carry no index signature so the SDK's interface types stay
   assignable. Generator override updated and artifacts regenerated; a
   new package-consumer type test sums the counters and reads the
   nested fields so this contract cannot silently regress. The
   sessionHistory test fixture now carries all four counters, matching
   runtime shape.

2. Assistant install wizard stub mismatch fixed: dialogLaunchers
   imported NewInstallWizard/computeDefaultInstallDir through a module
   shape cast, but the assistant stub only exported default — a
   guaranteed runtime crash if the gated path lit up. The stub now
   provides real typed exports: a wizard that cancels immediately (so
   the launcher resolves null/user-cancelled instead of hanging on an
   empty dialog) and an inert computeDefaultInstallDir; the unsafe
   cast in dialogLaunchers is gone.

Validation: tsc exit 0; full suite 3698 green (incl. the new consumer
counters test); smoke + bundle guard green.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-11 07:59:18 +08:00
5040c491da feat(opengateway): surface the gateway's "auto" smart-routing model in /model (#1588)
Opengateway now supports a virtual model "auto": the gateway scores each
request (context size, tools, code, reasoning) and routes it to the
cheapest model expected to handle it, escalating on upstream failure.
Add it to the static catalog so the /model picker offers it (listed
first) when the opengateway provider is active.

- catalog entry only — defaultModel stays mimo-v2.5-pro until the
  routing heuristics are calibrated
- no MODEL_ALIASES change: "auto" validates through the normal live
  sideQuery path, which the gateway accepts
- exempt the virtual entry from the shared-descriptor test (no concrete
  model descriptor exists for a server-side router)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 13:43:02 +08:00
5b01c2b595 feat(provider): add Atlas Cloud as official OpenAI-compatible provider (#1585)
New top-level vendor descriptor, env key wiring, startup screen
detection, and provider flag support for Atlas Cloud AI.
Catalog mirrors https://www.atlascloud.ai/models/list/llm (48 models,
namespaced IDs) and the preset is covered by compatibility,
ProviderManager preset-order, applyProviderFlag, route-credential,
activation, and restart-relaunch tests.
Endpoint: https://api.atlascloud.ai/v1
Auth: Bearer (standard) via ATLAS_CLOUD_API_KEY (dedicated key
required; no OPENAI_API_KEY fallback anywhere). Scoped to Atlas Cloud
only — venice/xiaomi-mimo/xai keep their documented OPENAI_API_KEY
compatibility fallback unchanged.

Credential hardening (atlas-cloud only): setup.dedicatedCredentialsOnly
(new descriptor flag) stops route credential resolution from falling
back to OPENAI_API_KEY; validation requires the dedicated key; the
--provider flag path replaces stale known base URLs via
applyOpenAIBaseUrlDefault (custom proxy URLs preserved), prefers the
dedicated key over a lingering OPENAI_API_KEY, and clears the generic
key when the dedicated key is absent; buildLaunchEnv carries dedicated
vendor keys across restarts (live shell value wins over persisted) so
relaunched profiles stay authenticated.

Review feedback: OAuth picker entries anchor to DeepSeek by value
with an append fallback when the anchor is absent; startup-screen
base URL check moved into the authoritative base-URL section with the
model regex dot escaped.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-10 13:18:17 +08:00
03c8e224dc Feat/opengateway nemotron free (#1539)
* feat(opengateway): NVIDIA Nemotron 3 Ultra free model

Add the OpenRouter :free Nemotron 3 Ultra endpoint (550B MoE, 1M
context, 65K output, tools + reasoning) to the opengateway catalog.
Bills $0 and bypasses the gateway credit gate, so it works for every
user — no credits or premium plan needed.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* feat(model): surface catalog notes as a picker tag; tag Nemotron free

Wire ModelCatalogEntry.notes into the route catalog description so the
Nemotron 3 Ultra entry shows "Free · Provider: Gitlawb Opengateway" in
the model picker.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-05 10:32:14 +08:00
b1a80267a5 feat(xiaomi): retire deprecated MiMo V2 Pro and V2 Omni (#1538)
Xiaomi deprecated mimo-v2-pro and mimo-v2-omni upstream — requests now
404 with a migrate-to-v2.5 message. Remove them from the opengateway
catalog, the Xiaomi MiMo vendor catalog, model descriptors, brand ids,
and the legacy model picker list. mimo-v2-flash remains (still served).

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-05 08:57:07 +08:00
ea091768ae feat(opengateway): Gemini 3.1 Flash Lite GA model id (#1537)
The model left preview — OpenRouter and the gateway now serve
google/gemini-3.1-flash-lite as the canonical id. Swap the opengateway
catalog entry, model descriptors, and brand ids to the GA id and drop
the -preview variants; pricing is unchanged ($0.25/1M in, $1.50/1M out).

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-05 08:38:15 +08:00
73a2833819 feat(sponsors): add Atlas Cloud sponsor and sponsored tip (#1536)
Add Atlas Cloud (atlascloud.ai) to the README sponsors table with its
banner asset, and add an Atlas Cloud sponsored tip to the tip catalog.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-05 07:13:33 +08:00
8705cd35f7 feat(opengateway): add MiniMax M3 and Qwen 3.7 Max to the model catalog (#1515)
Two new models reachable through the Gitlawb Opengateway unified route,
both served upstream via OpenRouter (gateway-side routing lands separately
in the opengateway repo):

- MiniMax M3 (`minimax/minimax-m3`): reuses the existing minimax-m3
  descriptor — 1M context, 131k output, reasoning/coding.
- Qwen 3.7 Max (`qwen/qwen3.7-max`): new descriptor — 1M context, 65k
  output, text-only per the OpenRouter catalog (no vision), so it skips
  the qwenModel helper's vision defaults.

The /model picker test's exact-list assertion gains both entries.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-05 06:09:57 +08:00
96ddec7183 fix(test): stop use-input test from leaking a global stdin mock (#1501)
The use-input.test.ts added in #1198 broke the full `bun test` run two ways:

1. It imported `@testing-library/react-hooks`, which was never installed and
   is React 16/17/18-only (incompatible with this repo's React 19), so the
   file errored on load.
2. Its top-level `vi.mock('./use-stdin.js', …)` registered a module mock that
   leaks across every later file in the same `bun test` process. The fake
   eventEmitter's `.on` was a no-op, so `useInput` silently registered no
   listener and dropped all keystrokes — surfacing as timeouts in
   MonitorPermissionRequest and the agent-menu/wizard TextInput tests (which
   passed in isolation but failed in the full suite).

Rewrite the test to inject the stdin handle via StdinContext.Provider (no
leaking global mock) and render through the real ink root (no
@testing-library/react-hooks). Drop the now-dead react-hooks and
react-test-renderer devDependencies and reconcile the lockfile.

Full suite: 3429 pass, 0 fail (was 9 fail + 1 error).

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-03 20:40:58 +08:00
e2fa248376 feat(minimax): add MiniMax M3 model with 1M context window (#1470)
* feat(minimax): add MiniMax M3 model with 1M context window

M3 is MiniMax's next-gen flagship with coding/agentic capabilities,
1M token context (1,048,576), and benchmark performance on par with
Opus 4.7 on SWE-bench.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* feat(minimax): add M3 model definition to integrations models

The integrations/models/minimax.ts was missing the minimax-m3
model definition that was added to the vendor config. This
caused the model picker to not show MiniMax M3 as a selectable
option despite it being present in the picker list.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(minimax): show full catalog in /model picker and default to M3

When a MiniMax provider profile was active, the /model picker collapsed
to the single model pinned in the profile (e.g. MiniMax-M2.7), hiding the
rest of the catalog — including M3. mergeActiveProfileModelOptions now
surfaces the complete catalog for native vendor routes (which ship a
curated static catalog), while gateways keep the profile model list as a
whitelist. Added isNativeVendorCatalogRoute() to draw that distinction.

Also harden isMiniMaxProvider() to detect the MiniMax host on
ANTHROPIC_BASE_URL (anthropic-proxy transport), not just OPENAI_BASE_URL,
and switch the vendor defaultModel to MiniMax-M3.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* test: fix 13 cross-file isolation failures in the full suite

These tests passed individually but failed under `bun test` because bun does
not unregister mock.module() overrides on mock.restore(), so stubs leaked
into later files. Root-caused and fixed each leak at the source:

- providerFallback: inject settings/profiles into getProviderFallbackChain
  and resolveNextFallbackProviderFromState; the test now uses DI instead of
  mock.module on settings.js/providerProfiles.js (fixed 7 attribution fails).
- apiPreconnect: accept an injected apiProvider (defaults to getAPIProvider);
  the test passes it explicitly so a leaked providers.js mock can't force
  'firstParty' (fixed 3 preconnect fails).
- preflightChecks: build a COMPLETE axios stub (defaults + interceptors) and
  re-register the real module in afterEach, so the partial stub no longer
  breaks proxy.ts's axios.defaults usage in tests/sdk/query-lifecycle (fixed
  2 query-resume fails).
- flagSettings: realpath the temp dir so the macOS /tmp -> /private/tmp
  symlink doesn't mismatch the canonicalised --settings path (fixed 1 fail).

Full suite now green: 3343 pass / 0 fail under both `bun test` and
`bun test --max-concurrency=1`.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(minimax): default env-only MiniMax sessions to M3

The descriptor/profile path and --provider minimax already default to
MiniMax-M3, but getDefaultMainLoopModelSetting() still fell back to
MiniMax-M2.7 for env-only sessions (only MINIMAX_API_KEY / a MiniMax base
URL set, no explicit model env). That left part of the default-model
update unapplied. Align the env-only fallback (and the dead applyProviderFlag
fallback) on M3, and update the regression test accordingly. Users who pin
a model via env/profile are unaffected.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-02 06:47:13 +08:00
d35d4687ad fix(ci): build before unit tests in release workflow (#1463)
The release workflow ran `bun test` before `bun run build`, but the
bundle regression tests in scripts/missing-module-stub.test.ts read the
shipped dist/cli.mjs. On a fresh release-tag checkout dist/ (gitignored)
does not exist yet, so both tests threw "dist/cli.mjs not found" and
failed the npm publish job. pr-checks.yml already builds first (via
`bun run smoke`); reorder release.yml to match.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-06-01 09:27:01 +08:00
690b3f07a4 fix(test): prevent providerProfiles config mock from leaking across files (#1432)
The mock.module('./config.js') replacement in providerProfiles.test.ts
returned a partial config object (mockConfigState) with no
autoCompactEnabled field. Because bun's mock.restore() does not revert
mock.module(), this incomplete config leaked into later test files in
the same process, making getGlobalConfig().autoCompactEnabled undefined.

That caused isAutoCompactEnabled() to be falsy and the auto-compact
cooldown safety-net block in query.ts to be skipped, failing 3 tests in
src/query/autoCompactCooldown.test.ts — but only in the full suite, not
in isolation.

Spread the real getGlobalConfig() into the mock so it stays a complete
GlobalConfig and only the provider-profile fields are overridden.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-05-30 00:06:08 +08:00
7c23fb7a05 fix(provider): require API key input when adding OpenGateway (#1384)
The OpenGateway preset was missing `apiKeyEnvVars`, so credential
resolution fell back to the descriptor's `credentialEnvVars` chain
which includes `OPENAI_API_KEY`.  If a user had `OPENAI_API_KEY` set
for a different provider, the add-provider flow silently pre-populated
the draft key and skipped the API key input screen entirely.

Add an explicit `apiKeyEnvVars: ['OPENGATEWAY_API_KEY']` to the preset
so the UI only auto-fills from the provider-specific env var.  The
runtime setup/validation still falls back to `OPENAI_API_KEY` for
existing configs.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-05-27 09:55:05 +08:00
326f082682 feat(xai): add xAI/Grok OAuth provider (browser + device-code) (#1284)
* feat(xai): add xAI/Grok OAuth provider (browser + device-code)

Sign in to xAI with your account instead of an API key. Inference uses
the access token as a Bearer to api.x.ai/v1 (same surface as XAI_API_KEY)
with automatic refresh ~60s before expiry.

CLI:  openclaude auth xai {login|device|status|logout}
UI:   /login → 3rd-party platform → xAI OAuth (Grok)

Implementation mirrors openclaw's xai-oauth (shared client_id, PKCE,
OIDC discovery against auth.x.ai, trusted-host gating, refresh_token).
The loopback callback server (127.0.0.1:56121) explicitly echoes CORS
preflight for auth.x.ai / accounts.x.ai so xAI's browser-side push
reaches us; if the loopback still fails (firewall, remote host), users
can paste the code shown on xAI's auth page directly into the CLI or
the ProviderManager input.

Also adds grok-code-fast-1 to the xAI catalog and the x-grok-conv-id
prompt-caching header (mirrors hermes-agent).

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(xai): satisfy provider validation with OAuth + drop raw timeout signal

Two P1 review items from the xAI OAuth PR:

1. After signing in with `openclaude auth xai login` (or via the
   ProviderManager), the xAI vendor still required XAI_API_KEY because
   validation only checked `credentialEnvVars: ['XAI_API_KEY']`. A user
   running `openclaude -p` could exit on the missing-key warning before
   openaiShim ever resolved the stored OAuth token. Adds a new
   `xai-credential` validation kind that accepts, in order:
     a. XAI_API_KEY (legacy / explicit override)
     b. XAI_CREDENTIAL_SOURCE=oauth env marker (set by the saved
        OAuth profile when its env is applied at startup)
     c. stored OAuth credentials in secure storage (covers the
        first-process gap before applySavedProfile runs)
   Resolver (c) is injectable so tests aren't sensitive to the
   developer's actual login state. Adds regression tests for all four
   paths (API key, env marker, stored creds, none).

2. `fetchXaiOAuthDiscovery` used `AbortSignal.timeout(...)` directly,
   which the `scripts/no-raw-abort-signal-timeout.test.ts` repo guard
   forbids (raw timeout signals leak timers in Bun). Routes through the
   existing `createCombinedAbortSignal` helper with proper cleanup in
   `finally`.

Test counts: 27/27 providerValidation (was 22, +5 xAI), 11/11
xaiOAuthCallback, 13/13 xaiOAuthShared. The repo guard now passes.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(xai): let Esc cancel xAI OAuth setup when manual-code input is focused

The manual-code TextInput's child-effect Esc handler ran before the
parent XaiOAuthSetup's `useKeybinding('confirm:no')`, so pressing Esc
triggered "press Esc again to clear input" instead of going back. Set
`disableEscapeDoublePress` so the parent keybinding fires immediately.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(xai): register Esc-to-back at ProviderManager top level

The child-component `useKeybinding('confirm:no', onBack)` in
XaiOAuthSetup wasn't reliably firing while the manual-code TextInput
held the input loop — even with disableEscapeDoublePress, the input's
listener still ran first and the keybinding context resolution lost
the race in practice. Move the binding to the top level of
ProviderManager with `context: 'Settings'` and `isActive: screen ===
'xai-oauth'`, matching the proven preset-api-key pattern (which also
has a TextInput and where Esc works correctly).

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(xai): persist OAuth startup profile with marker so logout cleans it up

`setActiveProviderProfile()` for an xAI OAuth profile (provider='xai',
no API key) was writing the startup file via the generic
`buildOpenAICompatibleStartupEnv` path — producing a plain
profile='openai' file with OPENAI_BASE_URL=https://api.x.ai/v1 and no
credential marker. Two downstream bugs:

  - `clearPersistedXaiOAuthProfile()` only matches files with
    profile='xai' + XAI_CREDENTIAL_SOURCE='oauth', so the logout
    cleanup left this file untouched. Next non-interactive launch
    (e.g. `openclaude -p`) re-applied the stale base URL with no
    credential and hit the missing-XAI_API_KEY validation warning
    even though the user had just logged out.
  - Startup validation could not distinguish "OAuth profile, token
    will be resolved at request time" from "user just forgot to set
    XAI_API_KEY".

`buildStartupProfileFromActiveProfile()` now detects xAI OAuth
profiles (xai vendor + empty apiKey) and writes profile='xai' with
XAI_CREDENTIAL_SOURCE='oauth'. `buildLaunchEnv()`'s xai branch
preserves the marker so it lands in process.env at startup, where
the existing xai-credential validation kind accepts it without
needing XAI_API_KEY.

Regression test: setActiveProviderProfile for an OAuth profile must
write the marker, isPersistedXaiOAuthProfile must recognise it, and
clearPersistedXaiOAuthProfile must remove the file.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(xai): set Access-Control-Allow-Private-Network so browser auto-detect works

Chrome/Edge require Access-Control-Allow-Private-Network: true on the
preflight response when an HTTPS origin (auth.x.ai) fetches a
private-network address (127.0.0.1). Without it the preflight returns
2xx but the actual GET is silently blocked — our loopback never
receives the callback, the CLI promise never resolves, and the user
has to fall back to pasting the code even after a successful sign-in.

Mirror openclaw's CORS setup: static `Allow-Methods: GET, OPTIONS`,
default `Allow-Headers: content-type` when none requested, and the
private-network header on every trusted-origin response.

Regression-locked because the failure mode is silent: the test now
asserts that an OPTIONS preflight from auth.x.ai with
Access-Control-Request-Private-Network: true gets the matching
Access-Control-Allow-Private-Network: true header back.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(xai): update mainLoopModel when xAI OAuth profile is activated

After the OAuth completion handler activated the new xAI profile, it
never wrote the new model back to app state. The chat session kept
sending the previous provider's model name (e.g. kimi-k2.6) against
api.x.ai/v1, yielding 400 "Model not found: kimi-k2.6". Mirrors the
existing activateSelectedProvider / saveAndCloseProvider flows that
set mainLoopModel + clear mainLoopModelForSession.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(xai): pause stdin on cleanup + CLI logout clears startup profile

Two P2 review items from the xAI OAuth PR:

1. `listenForManualCode()` resumed stdin unconditionally and cleanup
   only removed the data listener. A resumed stdin keeps a one-shot
   CLI process alive even after sign-in succeeds — the user sees
   `openclaude auth xai login` complete but the prompt never returns
   until they send EOF. Now records the pre-existing paused state and
   only resumes (and re-pauses on cleanup) when stdin was paused to
   begin with.

2. `openclaude auth xai logout` only cleared secure storage. If the
   user had configured xAI OAuth through `/provider`, the
   marker-tagged `.openclaude-profile.json` and the provider profile
   in global config both survived. Startup validation still accepted
   `XAI_CREDENTIAL_SOURCE=oauth`, but openaiShim could no longer
   resolve a token — the next non-interactive xAI launch was left
   pointed at api.x.ai with no credentials instead of being logged
   out cleanly. CLI logout now mirrors the /provider UI logout:
   clear secure storage → delete the xAI OAuth provider profile from
   global config (matched by canonical name) → remove the
   marker-tagged startup file → clear the global startup-provider
   override if the active profile changed.

New regression tests in `src/cli/handlers/xaiAuth.test.ts`:
  - logout removes the marker-tagged startup profile (the documented
    /provider-then-CLI-logout sequence)
  - logout is a no-op when no profile is stored
  - logout leaves unrelated (non-xAI) startup profiles alone

The tests assert file-level cleanup directly because Bun's
`mock.module(...)` in ProviderManager.test.tsx leaks providerProfiles
stubs across files within the same `bun test` process; in-memory
lookups aren't reliable here, but the startup-file path is what users
actually hit at next launch.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* test(xai): make xaiLogout regression test hermetic in the parallel suite

The test passed in isolated runs but failed in the full `bun test` run
(2812 pass / 1 fail). Two compounding leaks from other test files:

  1. ProviderManager.test.tsx / model.test.tsx / others install
     `mock.module('../utils/providerProfile.js', ...)` stubs that omit
     `clearPersistedXaiOAuthProfile`. Bun's `mock.restore()` only
     restores `mock.fn()` mocks; module mocks persist across files in
     the same process, so xaiAuth's static import of
     `clearPersistedXaiOAuthProfile` resolved to `undefined`.
  2. SQLite / knowledgeGraph / paths tests call
     `setClaudeConfigHomeDirForTesting(...)` in parallel and don't
     always restore it, so the fresh providerProfile module's call to
     `getClaudeConfigHomeDir()` returned a leaked override instead of
     our CLAUDE_CONFIG_DIR. The real `clearPersistedXaiOAuthProfile`
     ran fine, just against the wrong directory.

Refactor `xaiLogout` to accept an optional `XaiLogoutDeps` object so
the test can inject:
  - the real `clearPersistedXaiOAuthProfile` (resolved via cache-bust
    import) wrapped to pin `configDir: tempConfigDir`, bypassing the
    parallel-test override leak
  - the real `clearXaiCredentials` / `getProviderProfiles` /
    `deleteProviderProfile` / `clearStartupProviderOverrides`,
    bypassing the `mock.module` leak

Production callers (just `main.tsx`) omit the argument and pick up the
static imports — behavior unchanged.

Full test count: 2813 pass / 0 fail (was 2812 pass / 1 fail).

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(xai): isolate OAuth profile from shell OPENAI_API_KEY + close callback on early cancel

Two more review items from the xAI OAuth PR.

P1 — buildLaunchEnv() leaked OPENAI_API_KEY into xAI OAuth profiles.
For a marker-tagged xAI profile, the launch env builder fell back
through processEnv.OPENAI_API_KEY → persistedEnv.OPENAI_API_KEY when
no XAI_API_KEY was set, then handed that key to buildXaiProfileEnv()
which copied it to BOTH OPENAI_API_KEY and XAI_API_KEY. The OAuth
credential-source marker was then dropped (because env.XAI_API_KEY
became truthy), and openaiShim short-circuited on the ambient
OPENAI_API_KEY before ever resolving the stored OAuth token —
sending the user's generic OpenAI key as a bearer to api.x.ai/v1.

Fix: when the persisted profile is OAuth-tagged, build xaiKey only
from explicit XAI_API_KEY env (shell or persisted), never from
OPENAI_API_KEY. Pass a scrubbed processEnv into buildXaiProfileEnv()
so it can't re-introduce the key via its internal fallback. Also
clear OPENAI_API_KEY from the returned launch env (defensive: the
clearManagedProfileEnv step already drops it, but be explicit).

Three regression tests:
  - ambient OPENAI_API_KEY does NOT leak into XAI_API_KEY /
    OPENAI_API_KEY for an OAuth profile; XAI_CREDENTIAL_SOURCE
    survives.
  - explicit XAI_API_KEY still overrides OAuth (existing precedence
    preserved).
  - non-OAuth xAI profile (legacy api-key flow, no marker) still
    accepts the OPENAI_API_KEY fallback — backward compat.

P2 — useXaiOAuthFlow leaked the loopback callback server when the
user cancelled mid-start. If unmount/Esc happened while
beginOAuthFlow() was still awaiting discovery or starting the
listener, the cleanup ran before the service had tracked the handle.
beginOAuthFlow() then resolved, the IIFE saw `cancelled === true`
and returned without closing the just-started server — leaving the
fixed 56121 port held. Next OAuth attempt failed with EADDRINUSE.

Fix: track the resolved handle in a closure variable shared between
the IIFE and the cleanup callback. The IIFE calls handle.cancel()
when it observes cancellation after beginOAuthFlow resolved; the
cleanup callback also calls activeHandle?.cancel() to cover the
common "handle exists by unmount" case. handle.cancel() → service
cleanup is idempotent.

New test file `useXaiOAuthFlow.test.tsx` with two cases:
  - unmount while beginOAuthFlow is pending → cancel fires once
    beginOAuthFlow eventually resolves
  - unmount after handle exists → cancel fires immediately

Test count: 2818 pass / 0 fail (was 2813), TS error count unchanged
at 1692.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-05-23 08:22:57 +08:00
c366f7062b fix(grpc): register built-in agents so Agent tool isn't always empty (#1296)
The gRPC entrypoint constructed QueryEngine with `agents: []`, so the
moment the model tried to spawn a subagent (e.g. `general-purpose` for
project investigation) the Agent tool threw "Agent type 'general-purpose'
not found. Available agents: " with nothing after the colon. The CLI
entrypoint hydrates these via getBuiltInAgents(); do the same here so
gRPC-hosted sessions (playground sandbox, etc.) get parity.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 07:51:39 +08:00
0fbfc12a99 feat(opengateway): require API key on /v1/* and switch to bearer auth (#1322)
Opengateway flipped from zero-auth to per-user API keys (mint at
https://gitlawb.com/opengateway/keys). Update the client to match:

- Descriptor: setup.requiresAuth=true, authMode='api-key',
  credentialEnvVars=['OPENGATEWAY_API_KEY','OPENAI_API_KEY'] in both
  setup (used by the generator + auth-prompt) and validation (used by
  getProviderValidationError). Added missingCredentialMessage pointing
  users at the console URL.
- Transport: defaultAuthHeader changed from {name:'api-key',scheme:'raw'}
  to {name:'authorization',scheme:'bearer'} — the gateway only validates
  Authorization: Bearer ogw_live_..., the previous raw 'api-key' header
  was a leftover from the direct-Xiaomi era.
- Auto-detect: defaultOpengatewayProvider now returns null when no key
  env var is set instead of unconditionally selecting opengateway —
  surfaces the missing-credential prompt instead of silently routing to
  an endpoint that will 401.
- Tests: providerValidation.test.ts no-auth tests replaced with
  positive/negative key cases; providerAutoDetect.test.ts updated four
  fallback tests to include OPENGATEWAY_API_KEY (or assert null for the
  empty-env case).
- Regenerated integrationArtifacts.generated.ts via integrations:generate.

Full test suite: 2783 pass / 0 fail.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 07:49:20 +08:00
Kevin CodexandGitHub e5535577aa docs: add Xiaomi MiMo sponsor (#1213)
* docs: add Xiaomi MiMo sponsor

* feat(tips): add Xiaomi MiMo sponsored tips
2026-05-17 11:01:06 +08:00
Kevin CodexandGitHub 2d20109edc fix(gemini): parse raw tool call text (#1212) 2026-05-17 10:13:28 +08:00
Kevin CodexandGitHub 13a090162f fix(gemini): preserve tool calls through opengateway (#1204) 2026-05-17 05:12:07 +08:00
4d04f5bf4f feat(opengateway): add Gemini 3.1 Flash Lite + GLM 5.1 FP8 to catalog (#1194)
* feat(opengateway): add Gemini 3.1 Flash Lite + GLM 5.1 FP8 to catalog

Opengateway now routes non-Xiaomi models through GMI Cloud (configured
in opengateway/src/providers.ts via modelIds:
  - google/gemini-3.1-flash-lite-preview
  - zai-org/GLM-5.1-FP8

Adding both as catalog entries on the gitlawb-opengateway gateway
descriptor so openclaude users see them in the model picker when the
Opengateway preset is active. Each catalog entry reuses the existing
upstream model descriptor (`gemini-3.1-flash-lite-preview`, `GLM-5.1`)
for capability metadata; the apiName uses the full vendor-prefixed
form the gateway routes on.

No new model/brand/vendor descriptors needed — only the gateway
catalog gets the new IDs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* update opengateway

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:20:30 +08:00
6174d75e98 fix(openai-shim): surface in-stream errors and truncation hints (#1174)
Two related fixes to the OpenAI-compatible streaming parser:

1. Detect `data: {"error": {...}}` events inside the stream and throw a
   proper APIError. OpenAI sends this when a stream fails after headers
   have been sent, and intermediaries (gateways, proxies) use it to
   signal structured failures without dropping the TCP connection.
   Previously this chunk was silently dropped and the parser kept
   waiting for [DONE], producing the confusing "unexpected response"
   error after the stream ended without a proper close.

2. When finish_reason is "length" (model hit max_tokens, OR an upstream
   watchdog synthesized a graceful end after detecting a stalled
   stream), append a visible "[Response truncated]" hint inline. Mirrors
   the existing content_filter hint pattern. Users now know to ask the
   model to continue rather than wondering why the answer cut off.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 09:44:50 +08:00
5b5ba8853b feat(provider): add Gitlawb Opengateway as default provider with MiMo (#1165)
* feat(provider): add Gitlawb Opengateway as default provider with MiMo

Registers https://opengateway.gitlawb.com/v1/xiaomi-mimo as a first-class
gateway descriptor in openclaude. Pins it first in the provider picker
order (ahead of anthropic) and surfaces it with a green [FREE] badge in
the picker UI. Wires detectBestProvider() to fall back to opengateway
with mimo-v2.5-pro when no other credentials or local services are
detected, making fresh installs work zero-config during the Xiaomi
free-inference partnership window.

- src/integrations/gateways/gitlawb-opengateway.ts: gateway descriptor,
  static catalog of all five mimo models, requiresAuth: false
- src/integrations/artifactGenerator.ts: sort comparator pins
  gitlawb-opengateway before anthropic
- src/utils/providerAutoDetect.ts: opengateway is the last-resort
  fallback; OPENGATEWAY_BASE_URL env override for local dev;
  skipOpengatewayFallback escape hatch for tests
- src/components/ProviderManager.tsx: getPresetLabel renders a green
  [FREE] badge for the opengateway preset, matching the existing
  [Sponsor] badge pattern used for xiaomi-mimo

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(provider): keep Codex OAuth after DeepSeek with Opengateway pinned

Pinning Gitlawb Opengateway at the top of the preset list shifted every
subsequent index by 1, which dropped Codex OAuth from "after DeepSeek"
to "before DeepSeek" because the splice insertion index was a hardcoded
6. Bumps the insertion index to 7 to keep Codex OAuth in its established
position, and updates PRESET_ORDER in ProviderManager.test.tsx so the
keyboard-navigation harness lines up with the new layout.

Restores 9 ProviderManager tests that were timing out because
navigateToPreset() was landing on the wrong row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 22:24:29 +08:00
18483e4d96 feat(provider): add Xiaomi MiMo integration (#1152)
* feat(provider): add Xiaomi MiMo integration

Add Xiaomi MiMo as an official OpenAI-compatible provider with provider-profile persistence, env-only detection, and sponsor labeling in the provider picker.

Co-Authored-By: OpenClaude (zai-org-glm-5-1) <openclaude@gitlawb.com>

* feat(provider): rebase Xiaomi MiMo as top-level provider

Promote Xiaomi MiMo from generic OpenAI-compatible shim route to a
first-class top-level provider matching the MiniMax pattern. Includes
brand/model descriptors, env-only detection, provider auto-detect,
status labels, legacy provider type, model picker, and profile env
persistence.

Co-Authored-By: OpenClaude (zai-org-glm-5-1) <openclaude@gitlawb.com>

* Fix Xiaomi MiMo provider integration

Promote Xiaomi MiMo as a first-tier OpenAI-compatible vendor and align its descriptor with the integration guide.

Use the resolving Xiaomi MiMo API host while normalizing the stale docs host alias, wire Xiaomi catalog options into /model, and ensure model selection/display uses OPENAI_MODEL for Xiaomi instead of Claude defaults.

Update provider profile/startup handling, OpenAI shim detection, docs, generated integration artifacts, and focused regression tests.

Verification: bun run integrations:check; focused bun test provider/model suites; bun run build; bun run smoke.

* Fix MiMo startup profile base URL normalization

---------

Co-authored-by: OpenClaude (zai-org-glm-5-1) <openclaude@gitlawb.com>
Co-authored-by: JATMN <the@jat.mn>
2026-05-14 07:48:26 +08:00
2c71e09394 chore(build): clean up external dependency validation warnings (#1124)
* chore(build): clean up external dependency validation warnings

Remove 2 unused externals (@opentelemetry/sdk-trace-node, ink) and add
12 missing packages to package.json that are dynamically imported at
runtime but weren't declared as dependencies. Also remove the unused
@opentelemetry/sdk-trace-node dependency.

This eliminates all 13 build validation warnings:
- 8 missing OTel exporter deps (http, proto, grpc variants + prometheus)
- 4 missing AWS SDK deps (bedrock, bedrock-runtime, sts, credential-providers)
- 1 missing Azure dep (@azure/identity)
- ink external pointed to local reimplementation, not npm package
- sdk-trace-node was declared external but never imported

Build validation now passes cleanly with 0 warnings.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* chore(build): eliminate external validation warnings

Remove unused @opentelemetry/sdk-trace-node from externals and package.json
(it's not imported anywhere in src/). Remove ink from SDK_ONLY_EXTERNALS
(the project reimplements ink locally at src/ink/). Add OPTIONAL_RUNTIME_EXTERNALS
list for packages that are dynamically imported but intentionally not direct
deps — OTel protocol exporters and cloud provider SDKs are resolved from
transitive deps or installed by users who need them.

Validation now passes with 0 warnings instead of 13.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* feat(telemetry): full OpenTelemetry purge — remove all tracking dependencies

Replace all @opentelemetry/* runtime dependencies with no-op stubs,
delete OTel-only source modules, and remove 10 @opentelemetry packages
from package.json plus @growthbook/growthbook.

Key changes:
- Delete 5 OTel-only modules (instrumentation, betaSessionTracing,
  bigqueryExporter, logger, firstPartyEventLoggingExporter)
- Replace 9 modules with no-op stubs (sessionTracing, events,
  telemetryAttributes, firstPartyEventLogger, growthbook, index,
  sink, datadog, sinkKillswitch, perfettoTracing)
- Remove all @opentelemetry/* imports from bootstrap/state.ts,
  entrypoints/init.ts, and ~20 caller files
- Remove all OTel counter types, meter/provider state from state.ts
- Clean externals.ts: remove 27 @opentelemetry/* entries
- Clean build.ts: remove OTel native-stub namespace exports
- Simplify no-telemetry-plugin.ts: remove redundant source-level stubs
- Remove 10 @opentelemetry/* + @growthbook/growthbook from package.json
- GrowthBook stub reads local ~/.claude/feature-flags.json for overrides

Co-Authored-By: OpenClaude (zai-org-glm-5-1) <openclaude@gitlawb.com>

* fix format

* chore: regenerate lockfile after OTel dependency removal

Co-Authored-By: OpenClaude (zai-org-glm-5-1) <openclaude@gitlawb.com>

* fix(growthbook): route gate helpers through local flag overrides

checkStatsigFeatureGate_CACHED_MAY_BE_STALE() and
checkGate_CACHED_OR_BLOCKING() now resolve from
~/.claude/feature-flags.json like getFeatureValue_* does,
so gates like tengu_thinkback, tengu_ccr_bridge, and
VS Code upsells can be flipped on locally. Security gates
(checkSecurityRestrictionGate) remain hard-false.

Also adds 5 tests covering gate helper override behavior
and unifies JSDoc wording for _getFlagValue-routed functions.

Co-Authored-By: OpenClaude (zai-org-glm-5-1) <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-05-13 17:07:45 +08:00
a4cbb78585 feat: add sponsored tips with frequency-gated display (#1140)
Implement sponsored tip system with registry, scheduler integration,
and config-gated frequency control. Sponsored tips appear based on
startup count frequency (default: 1 per 10 startups) and are
tracked separately from regular tip history.

Co-authored-by: OpenClaude (zai-org-glm-5-1) <openclaude@gitlawb.com>
2026-05-13 16:43:22 +08:00
f9621ab575 feat(provider): add Venice official provider (#1109)
Adds Venice as a descriptor-backed OpenAI-compatible provider with VENICE_API_KEY support, provider UI metadata, startup persistence, and regression coverage.

Co-authored-by: OpenClaude (gpt-5.5) <openclaude@gitlawb.com>
2026-05-11 22:06:09 +08:00
dea7ef998a Feat/xai grok 4 3 default (#1080)
* feat(xai): add Grok 4.3 as default model

Add Grok 4.3 to xAI model metadata and make it the provider default while preserving explicit Grok 4 and Grok 3 selections.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(xai): correct Grok 4.3 context window to 1M tokens

Grok 4.3 docs list 1,000,000 token context window. The PR had
incorrectly registered 2,000,000, which would allow local budgeting
to retain ~2x the supported input size before compaction triggers,
leading to provider-side failures.

- src/integrations/models/xai.ts: set contextWindow to 1_000_000
- src/utils/context.test.ts: update expectation to match

🤖 Generated with [OpenClaude](https://github.com/Gitlawb/openclaude)

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-05-11 07:57:41 +08:00
Kevin CodexandGitHub d19f4d335d fix flaky test (#1021) 2026-05-05 18:33:55 +08:00
7b02695b15 Feat/codex default provider (#1014)
* chore: add .openclaude/ to gitignore

The .openclaude/ directory contains auto-generated project-local files
(wiki pages, convention cache, local settings) that should not be
committed to the repository.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* feat: make Codex + GPT 5.5 the default provider and model

Changes the default provider to Codex and default model to GPT 5.5:

- package.json: dev script now uses provider-launch.ts codex
- providerRecommendation.ts: getGoalDefaultOpenAIModel returns gpt-5.5
  for coding and balanced goals (was gpt-4o)
- providerConfig.ts: fallback model changed from gpt-4o to codexplan
  (resolves to gpt-5.5)
- ProviderManager.tsx: Codex OAuth option now shows green
  "★ Recommended" badge in the provider picker

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix: replace Box with nested Text in Codex label

Ink's <Text> component cannot contain <Box>. The label is rendered
inside a <Text> parent, so use nested <Text> elements instead.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix: default to Codex when no provider profile is saved

When no persisted provider profile exists (fresh install / first run),
buildStartupEnvFromProfile now injects Codex + GPT 5.5 env vars instead
of returning process.env unchanged. Falls back gracefully — if Codex
credentials are available (OAuth or existing), uses those; otherwise
injects base URL and model defaults so the provider picker shows
GPT 5.5 as the default.

This closes the gap where node dist/cli.mjs (production start) would
default to firstParty (Anthropic) when no profile or env vars were set.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* chore: resolve stash conflict markers from accidental stash pop

Cleans up merge conflict artifacts left by a git stash pop from an
unrelated branch (chore/add-atomic-chat-partner). Kept upstream
(current branch) version in all cases.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix: restore memoize import and cleanup stash artifacts

Restores the memoize import dropped during conflict resolution in
modelSupportOverrides.ts. Removes duplicate originalEnv declaration
and redundant delete statements in providerValidation.test.ts.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* revert change in package.json

* fix broken test

* fix color

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-05-05 18:13:33 +08:00
Kevin CodexandGitHub 1f66d322ad fix flaky tests in full test run (#1020) 2026-05-05 17:57:24 +08:00
95a817fdb0 fix(provider): apply Codex OAuth session switch correctly (#974)
* fix(provider): apply Codex OAuth session switch correctly

Ensure Codex OAuth activation in an existing session does not briefly apply an empty OpenAI API key, preventing missing Authorization headers until restart.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(provider): preserve explicit env for Codex API profiles

Limit the Codex session-switch override to OAuth profiles so explicit OpenAI environment settings keep taking precedence for regular Codex profiles.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(provider): isolate Codex OAuth env from ambient credentials

Keep Codex OAuth profile activation from inheriting ambient Codex API credentials so CI and user shells cannot poison the in-session OAuth regression path.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-05-02 10:24:13 +08:00
4eb486ef83 Feat/web landing refresh (#958)
* feat(web): openclaude landing — runs anywhere, uses anything

A new marketing site for openclaude under web/, plus the minimal root
infrastructure to build, ignore, and gate it without affecting the
published npm package.

Landing page (web/)
- Vite + React 19 with monospace gitlawb typography (sf mono / fira code).
- Hero: pill, two-line wordmark "runs anywhere. / uses anything.",
  copy-to-clipboard install command, github cta.
- Six feature rows in hermes-style "title — sentence" format on hairline
  dividers (any model, real tools, profiles per repo, streaming,
  gateway routing, editor + server modes).
- Install block: same copyable command + three numbered steps.
- One-line footer with brand, version, gitlawb link, and license.
- Light theme is the default with a no-flash bootstrap script and a
  ☀ / ☾ toggle persisted to localStorage.
- New orange terminal-face logo at 36px in the nav.
- Body wash: dual orange radial gradients for warmth on both themes.

Root infra
- web/ excluded from npm publish via .npmignore (belt-and-suspenders
  alongside the existing files whitelist).
- web/ excluded from docker context (.dockerignore).
- web:dev / web:build / web:preview / web:typecheck scripts in
  package.json that delegate via --cwd web (no root deps added).
- web typecheck + build added to the pr-checks workflow.
- web/dist/ and web/*.tsbuildinfo ignored.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* added vercel in .gitignore

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-30 18:22:01 +08:00
c0b5535d86 docs: add Atomic Chat partner (#942)
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-28 23:35:25 +08:00
4c93a9f9f1 feat: add Opus 4.7 as default model and fix alias/thinking bugs (#928)
- Add CLAUDE_OPUS_4_7_CONFIG and register it in ALL_MODEL_CONFIGS
- Set Opus 4.7 as default for firstParty in getDefaultOpusModel() (3P stays on 4.6 until rollout)
- Fix sonnet[1m] → 404 bug: query.ts was passing raw alias to API without resolving via parseUserSpecifiedModel
- Add opus-4-7 to modelSupportsAdaptiveThinking so it uses { type: 'adaptive' } not { type: 'enabled' }
- Fix duplicate opus47 case and wrong opus46[1m] fallthrough in getPublicModelDisplayName switch
- Update user-facing display strings (picker labels, plan mode description) to reference Opus 4.7
- Add 3P fallback suggestion chain for opus-4-7 → opus-4-6 in validateModel

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-28 17:31:06 +08:00
46a9d3eec4 chore: rebrand user-facing copy to OpenClaude (#851)
* chore: rebrand user-facing copy to OpenClaude

Replace lingering Claude Code branding in CLI, tips, and runtime UI with OpenClaude/openclaude, including the startup tip Gitlawb mention.

Co-Authored-By: Claude GPT-5.4 <noreply@openclaude.dev>

* chore: address branding-sweep review feedback

- PermissionRequest.tsx: rebrand the two remaining "Claude needs your
  approval/permission" notifications to OpenClaude (review-artifact and
  generic tool permission paths).
- main.tsx, teleport.tsx, session.tsx, WebFetchTool/utils.ts,
  skills/bundled/{debug,updateConfig}.ts: replace leftover `claude --…`
  CLI hints and "Claude Code" labels missed by the original sweep.
- main.tsx: drop the inline gitlawb.com marketing copy from the
  stale-prompt tip; keep it a pure rebrand.
- auth.ts: finish the half-rename so both `claude setup-token` and
  `claude auth login` references in the same error block now read
  `openclaude …`.
- mcp/client.ts: keep `name: 'claude-code'` for MCP server allowlist
  compatibility (now explicit via comment) and replace the
  "Anthropic's agentic coding tool" description with an OpenClaude one.
- MCPSettings.tsx: point the empty-server-list hint at
  https://github.com/Gitlawb/openclaude instead of code.claude.com.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: replace help link with OpenClaude repo URL

Replace https://code.claude.com/docs/en/overview with
https://github.com/Gitlawb/openclaude in the help screen.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: Claude GPT-5.4 <noreply@openclaude.dev>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-26 22:14:36 +08:00
2586a9cddb feat: add xAI as official provider (#865)
* feat: add xAI as official provider

- Add xAI preset to ProviderManager (alphabetical order)
- Add xAI provider detection via XAI_API_KEY
- Add xAI startup screen heuristic (x.ai base URL or grok model)
- Add xAI status display properties
- Add grok-4 and grok-3 context windows
- Add xAI model fallbacks across all tiers
- Fix JSDoc priority order in providerAutoDetect

Co-Authored-By: Claude Opus 4.6 <noreply@openclaude.dev>

* fix(xai): persist relaunch classification for xAI profiles

Addresses reviewer feedback on feat/xai-official-provider:
- isProcessEnvAlignedWithProfile now validates XAI_API_KEY for x.ai
  base URLs, mirroring the Bankr pattern. Without this, relaunch
  skips re-applying the profile, XAI_API_KEY stays unset, and
  getAPIProvider() falls back to 'openai'.
- buildOpenAICompatibleStartupEnv now sets XAI_API_KEY when syncing
  active xAI profile to the legacy fallback file.
- Adds 'xai' to VALID_PROVIDERS and --provider xai CLI flag support.
- Adds xAI detection to providerDiscovery label heuristics.
- Adds 'xai' to legacy ProviderProfile type/isProviderProfile guard.
- Adds targeted tests for relaunch alignment, flag application, and
  discovery labeling.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@openclaude.dev>
Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-26 21:26:44 +08:00
818689b2ee fix(query): restore system prompt structure and add missing config import (#907)
- import getGlobalConfig — six call sites referenced it without an import;
  five short-circuited via feature() gates, but src/query.ts:1896 always
  ran and crashed every queryLoop iteration with "getGlobalConfig is not
  defined" (e.g. Explore subagent: "Agent failed: getGlobalConfig is not
  defined").
- stop coercing SystemPrompt (string[]) into a template-string before
  appendSystemContext — that made [...systemPrompt] spread the string
  character-by-character, replacing the structured prompt with thousands
  of one-char system blocks. Append arcSummary as its own array element
  instead.
- gate the finalizeArcTurn call behind feature('CONVERSATION_ARC') so it
  matches the rest of the memory-PR call sites and gets dead-code-
  eliminated for users without the flag.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-26 12:45:09 +08:00
Kevin CodexandGitHub d9ae56bc58 fix provider switch not presistingin session (#903)
* fix provider switch not presistingin session

* fix broken tests
2026-04-26 11:15:25 +08:00
64b1014b9a Feat/bankr provider (#888)
* feat(provider): add Bankr LLM Gateway support

Add Bankr as an OpenAI-compatible provider preset with dedicated env vars:
- BNKR_API_KEY, BANKR_BASE_URL, BANKR_MODEL
- Uses X-API-Key header instead of Authorization Bearer
- Base URL: https://llm.bankr.bot/v1
- Default model: claude-opus-4.6

Changes:
- Add 'bankr' to VALID_PROVIDERS and provider flag handling
- Add buildBankrProfileEnv() with env key registration
- Add Bankr detection in startup screen and provider discovery
- Map Bankr env vars to OpenAI-compatible vars in shim
- Add Bankr preset to ProviderManager (alphabetical order)
- Update PRESET_ORDER test to include Bankr

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fixup(provider): address Bankr PR review feedback

1. Map BNKR_API_KEY → OPENAI_API_KEY in providerFlag.ts so
   --provider bankr works with BNKR_API_KEY in non-interactive startup.

2. Remove unconditional BANKR_MODEL read from model.ts; it maps to
   OPENAI_MODEL via providerFlag.ts and openaiShim.ts, preventing
   cross-provider leakage.

3. Use X-API-Key for Bankr model discovery in openaiModelDiscovery.ts
   and providerDiscovery.ts, matching chat request auth.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-24 23:03:45 +08:00
038f715b7a feat(model): add GPT-5.5 support for Codex provider (#880)
- Bump Codex provider defaults from gpt-5.4 to gpt-5.5 across all ModelConfigs
- Update codexplan alias to resolve to gpt-5.5
- Add gpt-5.5 and gpt-5.5-mini to model picker with reasoning effort mappings
- Add context window and max output token specs for gpt-5.5 family
- Add gpt-5.5 entries to COPILOT_MODELS registry
- Keep official OpenAI API preset at gpt-5.4 (API availability pending)
- Update codexShim tests to expect gpt-5.5 from codexplan alias

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-24 19:06:36 +08:00
Kevin CodexandGitHub b694ccfff1 Add sponsors section to README (#874) 2026-04-24 11:47:55 +08:00
531e3f1059 feat(tools): resilient web search and fetch across all providers (#836)
- Add exponential backoff retry to DuckDuckGo adapter (3 attempts with
  jitter) to handle transient rate-limiting and connection errors.
- Add native fetch() fallback in WebFetch when axios hangs with custom
  DNS lookup in bundled contexts.
- Prevent broken native-path fallback for web search on OpenAI shim
  providers (minimax, moonshot, nvidia-nim, etc.) that do not support
  Anthropic's web_search_20250305 tool.
- Cherry-pick existing fixes:
  - a48bd56: cover codex/minimax/nvidia-nim in getSmallFastModel()
  - 31f0b68: 45s budget + raw-markdown fallback for secondary model
  - 446c1e8: sparse Codex /responses payload parsing
  - ae3f0b2: echo reasoning_content on assistant tool-call messages
- Fix domainCheck.test.ts mock modules to include isFirstPartyAnthropicBaseUrl
  and isGithubNativeAnthropicMode exports.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-23 01:14:00 +08:00
67de6bd2cf fix(openai-shim): echo reasoning_content on assistant tool-call messages for Moonshot (#828)
Kimi / Moonshot's chat completions endpoint requires that every assistant
message carrying tool_calls also carry reasoning_content when the
"thinking" feature is active. When an agent sends prior-turn assistant
history back (standard multi-turn / subagent / Explore patterns), the
shim previously stripped the thinking block:

  case 'thinking':
  case 'redacted_thinking':
    // Strip thinking blocks for OpenAI-compatible providers.
    break

That's correct for providers that would mis-interpret serialized
<thinking> tags, but Moonshot validates the schema strictly and rejects
with:

  API Error: 400 {"error":{"message":"thinking is enabled but
  reasoning_content is missing in assistant tool call message at
  index N","type":"invalid_request_error"}}

Reproducer: launch with Kimi profile, run any tool-using command
(Explore, Bash, etc.) — every request after the first 400s.

Fix: in convertMessages(), when the per-request flag
preserveReasoningContent is set (only for Moonshot baseUrls today),
attach the original thinking block's text as reasoning_content on the
outgoing OpenAI-shaped assistant message. Other providers continue to
strip (unknown-field rejection risk).

OpenAIMessage type grows a reasoning_content?: string field.
convertMessages() accepts an options object and threads the flag
through; the only call site (_doOpenAIRequest) gates via
isMoonshotBaseUrl(request.baseUrl).

Tests (openaiShim.test.ts):
  - Moonshot: echoes reasoning_content on assistant tool-call messages
    (regression for the reported 400)
  - non-Moonshot providers do NOT receive reasoning_content (guards
    against leaking the field to strict-parse endpoints)

Full suite: 1195/1195 pass under --max-concurrency=1. PR scan clean.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-22 22:47:57 +08:00
c13842e91c fix(test): autoCompact floor assertion is flag-sensitive (#816)
The test "never returns negative even for unknown 3P models (issue #635)"
asserted that getEffectiveContextWindowSize() returns >= 33_000 for an
unknown 3P model under the OpenAI shim. That specific number assumes
reservedTokensForSummary = 20_000 (MAX_OUTPUT_TOKENS_FOR_SUMMARY), which
holds only when the tengu_otk_slot_v1 GrowthBook flag is disabled.

When the flag is ON — which is the case in CI but not always locally —
getMaxOutputTokensForModel() caps the model's default output at
CAPPED_DEFAULT_MAX_TOKENS (8_000). Then reservedTokensForSummary = 8_000,
floor = 8_000 + 13_000 = 21_000, and the test fails with 21_000 < 33_000.

The test reliably passes locally and reliably fails in CI, manifesting as
the intermittent PR-check failure.

Fix: relax the lower bound to 21_000 (cap-enabled worst case), which is
still well above zero — preserving the anti-regression intent of
issue #635 (no infinite auto-compact from a negative effective window)
without binding the test to GrowthBook flag state.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-22 09:37:57 +08:00
458120889f fix(model): codex/nvidia-nim/minimax now read OPENAI_MODEL env (#815)
getUserSpecifiedModelSetting() decides which env var to consult based on
the active provider. The check included openai and github but omitted
codex, nvidia-nim, and minimax — even though all three use the OpenAI
shim transport and get their model routing via CLAUDE_CODE_USE_OPENAI=1
+ OPENAI_MODEL (set by applyProviderProfileToProcessEnv).

Concrete failure: user switches from Moonshot profile (which persisted
settings.model='kimi-k2.6') to the Codex profile. The new profile
correctly writes OPENAI_MODEL=codexplan + base URL to
chatgpt.com/backend-api/codex. Startup banner reflects Codex / gpt-5.4
correctly. But at request time getUserSpecifiedModelSetting() returns
early for provider='codex' (not in the env-consult list), falls through
to the stale settings.model='kimi-k2.6', and the Codex API rejects:

  API Error 400: "The 'kimi-k2.6' model is not supported when using
  Codex with a ChatGPT account."

Fix: extract an isOpenAIShimProvider flag covering openai|codex|github|
nvidia-nim|minimax — all providers that set OPENAI_MODEL as their model
env var. The Gemini and Mistral branches stay as-is (they use
GEMINI_MODEL / MISTRAL_MODEL).

Five regression tests pin the fix for each OpenAI-shim provider plus
guard tests for openai and github that already worked.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-22 09:01:44 +08:00
13de4e85df fix(provider): saved profile ignored when stale CLAUDE_CODE_USE_* in shell (#807)
* fix(provider): saved profile ignored when stale CLAUDE_CODE_USE_* in shell

Users reported "my saved /provider profile isn't picked up at startup —
the banner shows gpt-4o / api.openai.com even though I saved Moonshot".

Root cause: applyActiveProviderProfileFromConfig() bailed out whenever
hasProviderSelectionFlags(processEnv) was true — i.e. whenever ANY
CLAUDE_CODE_USE_* flag was present. But a bare `CLAUDE_CODE_USE_OPENAI=1`
with no paired OPENAI_BASE_URL / OPENAI_MODEL is almost always a stale
shell export left over from a prior manual setup, not genuine startup
intent. Respecting it skipped the saved profile and let StartupScreen.ts
fall through to the hardcoded `gpt-4o` / `https://api.openai.com/v1`
defaults — the exact symptom users see.

Fix: narrow the guard from "any flag set" to "flag set AND at least one
concrete config value (BASE_URL, MODEL, or API_KEY)". A bare stale flag
no longer blocks the saved profile. A real shell selection (flag + URL
or flag + model) still wins, preserving the "explicit startup intent
overrides saved profile" contract.

New helper: hasCompleteProviderSelection(env). Per-provider check for a
paired concrete value. Bedrock/Vertex/Foundry keep the flag-alone
semantic since they rely on ambient AWS/GCP credentials rather than env
config.

Three new tests cover the bug and the two counter-cases:
  - bare USE flag → profile applies (fixes the bug)
  - USE flag + BASE_URL → profile blocked (preserves explicit intent)
  - USE flag + MODEL → profile blocked (preserves explicit intent)

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(provider): don't overlay stale legacy profile on plural-managed env

Second half of the "saved profile not picked up in banner" bug. The prior
commit fixed the guard that prevented applyActiveProviderProfileFromConfig()
from firing when a stale CLAUDE_CODE_USE_* flag was in the shell. But even
when the plural system applies correctly, buildStartupEnvFromProfile() was
then loading the legacy .openclaude-profile.json AND overwriting the
plural-managed env with whatever that file contained.

addProviderProfile() (the call path the /provider preset picker uses) does
NOT sync the legacy file, so a user who went:

  manual setup: CLAUDE_CODE_USE_OPENAI=1 + OPENAI_MODEL=gpt-4o
              → writes .openclaude-profile.json as { openai, gpt-4o, ... }
  /provider:   add Moonshot preset, mark active
              → writes plural config; legacy file UNCHANGED

would see startup reliably apply Moonshot env first, then get it clobbered
by the stale legacy file. Banner shows gpt-4o / api.openai.com while
runtime ends up with the correct env via a different code path — exactly
the user-reported symptom.

Fix: in buildStartupEnvFromProfile, when the plural system has already
set env (CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED === '1'), skip the
legacy-file overlay entirely and return processEnv unchanged. Legacy is
now strictly a first-run / fallback path for users who haven't adopted
the plural system.

Also removes the stripped-then-rebuilt env construction that was part of
the old overlay path — no longer needed.

Test updates:
  - Replaced "lets saved startup profile override profile-managed env"
    (encoded the old broken behavior) with a regression test that pins
    the new semantic: plural env survives when legacy is stale.
  - Added "falls back to legacy when plural hasn't applied" to pin the
    first-run path still works.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-22 00:59:32 +08:00
a5bfcbbadf feat(provider): zero-config autodetection primitive (#784)
First-run users with a credential already exported (ANTHROPIC_API_KEY,
OPENAI_API_KEY, etc.) currently still have to navigate the provider picker
or set CLAUDE_CODE_USE_* flags manually. Selecting the right provider from
ambient state should be automatic.

New module src/utils/providerAutoDetect.ts:

- detectProviderFromEnv() — synchronous env scan in a deterministic priority
  order (anthropic → codex → github → openai → gemini → mistral → minimax).
  Also detects Codex via ~/.codex/auth.json presence.
- detectLocalService() — parallel probes for Ollama (:11434) and LM Studio
  (:1234), with honoring of OLLAMA_BASE_URL / LM_STUDIO_BASE_URL overrides.
  Short 1.2s default timeout so first-run latency stays low when no local
  service is running.
- detectBestProvider() — orchestrator. Env scan short-circuits the probe;
  only hits the network when env has nothing.

All detection paths are side-effect-free: returns a DetectedProvider
descriptor describing what was found and why. Callers decide whether to
apply it (gated on hasExplicitProviderSelection() / profile file existence)
and how to hydrate the launch env.

Codex auth-file check is injectable (hasCodexAuth option) so tests are
hermetic from the dev machine's ~/.codex/auth.json state.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-21 23:37:04 +08:00
e908864da7 feat(api): smart model routing primitive (cheap-for-simple, strong-for-hard) (#785)
Most everyday turns ("ok", "thanks", "yep go ahead", "what does that do?")
get no measurable quality improvement from Opus-tier models over Haiku-tier,
but cost ~10x more and stream slower. Smart routing opts a user into
automatically routing obviously-simple turns to a cheaper model while
keeping the strong model for anything non-trivial.

New module src/services/api/smartModelRouting.ts:

- routeModel(input, config) → { model, complexity, reason }
- Pure primitive: no env reads, no state, caller supplies everything.
- Config is opt-in (enabled: false by default).

Routes to strong (conservative) when ANY of:
  - First turn of session (task-setup is worth the quality)
  - Code fence or inline code span present
  - Reasoning/planning keyword (plan, design, refactor, debug, architect,
    investigate, root cause, etc. — 20+ anchors)
  - Multi-paragraph input
  - Over char/word cutoff (defaults: 160 chars, 28 words; matches hermes)

Routes to simple only for clearly-trivial chatter.
Decision includes a reason string for a future UI indicator that shows
which tier handled the turn.

Integration into query path is intentionally deferred to a follow-up PR so
the heuristics can be reviewed and tuned in isolation first.

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-21 21:50:24 +08:00
b95d2221df Feat/kimi moonshot support (#805)
* feat(provider): first-class Moonshot (Kimi) direct-API support

Moonshot's direct API (api.moonshot.ai/v1) is OpenAI-compatible and works
today via the generic OpenAI shim, including the reasoning_content channel
that Kimi returns alongside the user-visible content. But the UX was rough:
unknown context window triggered the conservative 128k fallback + a warning,
and the provider displayed as "Local OpenAI-compatible".

Makes Moonshot a recognized provider:

- src/utils/model/openaiContextWindows.ts: add the Kimi K2 family and
  moonshot-v1-* variants to both the context-window and max-output tables.
  Values from Moonshot's model card — K2.6 and K2-thinking are 256K,
  K2/K2-instruct are 128K, moonshot-v1 sizes are embedded in the model id.
- src/utils/providerDiscovery.ts: recognize the api.moonshot.ai hostname
  and label it "Moonshot (Kimi)" in the startup banner and provider UI.

Users can now launch with:

  CLAUDE_CODE_USE_OPENAI=1 \
  OPENAI_BASE_URL=https://api.moonshot.ai/v1 \
  OPENAI_API_KEY=sk-... \
  OPENAI_MODEL=kimi-k2.6 \
  openclaude

and get accurate compaction + correct labeling + correct max_tokens out
of the box.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(openai-shim): Moonshot API compatibility — max_tokens + strip store

Moonshot's direct API (api.moonshot.ai and api.moonshot.cn) uses the
classic OpenAI `max_tokens` parameter, not the newer `max_completion_tokens`
that the shim defaults to. It also hasn't published support for `store`
and may reject it on strict-parse — same class of error as Gemini's
"Unknown name 'store': Cannot find field" 400.

- Adds isMoonshotBaseUrl() that recognizes both .ai and .cn hosts.
- Converts max_completion_tokens → max_tokens for Moonshot requests
  (alongside GitHub / Mistral / local providers).
- Strips body.store for Moonshot requests (alongside Mistral / Gemini).

Two shim tests cover both the .ai and .cn hostnames.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix: null-safe access on getCachedMCConfig() in external builds

External builds stub src/services/compact/cachedMicrocompact.ts so
getCachedMCConfig() returns null, but two call sites still dereferenced
config.supportedModels directly. The ?. operator was in the wrong place
(config.supportedModels? instead of config?.supportedModels), so the null
config threw "Cannot read properties of null (reading 'supportedModels')"
on every request.

Reproduces with any external-build provider (notably Kimi/Moonshot just
enabled in the sibling commits, but equally DeepSeek, Mistral, Groq,
Ollama, etc.):

  ❯ hey
  ⏺ Cannot read properties of null (reading 'supportedModels')

- prompts.ts: early-return from getFunctionResultClearingSection() when
  config is null, before touching .supportedModels.
- claude.ts: guard the debug-log jsonStringify with ?. so the log line
  never throws.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* fix(startup): show "Moonshot (Kimi)" on the startup banner

The startup-screen provider detector had regex branches for OpenRouter,
DeepSeek, Groq, Together, Azure, etc., but nothing for Moonshot. Remote
Moonshot sessions fell through to the generic "OpenAI" label —
getLocalOpenAICompatibleProviderLabel() only runs for local URLs, and
api.moonshot.ai / api.moonshot.cn are not local.

Adds a Moonshot branch matching /moonshot/ in the base URL OR /kimi/ in
the model id. Now launches with:

  OPENAI_BASE_URL=https://api.moonshot.ai/v1 OPENAI_MODEL=kimi-k2.6

display the Provider row as "Moonshot (Kimi)" instead of "OpenAI".

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

* refactor(provider): sort preset picker alphabetically; Custom at end

The /provider preset picker was in ad-hoc order (Anthropic, Ollama,
OpenAI, then a jumble of third-party / local / codex / Alibaba / custom /
nvidia / minimax). Hard to scan when you know the provider name you want.

Sorts the list alphabetically by label A→Z. Pins "Custom" to the end —
it's the catch-all / escape hatch so it's scanned last, not shuffled into
the alphabetical run where a user looking for a named provider might
grab it by mistake. First-run-only "Skip for now" stays at the very
bottom, after Custom.

Test churn:
- ProviderManager.test.tsx: four tests hardcoded press counts (1 or 3 'j'
  presses) that broke when targets moved. Replaces them with a
  navigateToPreset(stdin, label) helper driven from a declared
  PRESET_ORDER array, so future list edits only update the array.
- ConsoleOAuthFlow.test.tsx: the 13-row test frame only renders the first
  ~13 providers. "Ollama", "OpenAI", "LM Studio" sentinels moved below
  the fold; swap them for alphabetically-early providers still visible
  in-frame ("Azure OpenAI", "DeepSeek", "Google Gemini"). Test intent
  (picker opened with providers listed) is preserved.

Co-Authored-By: OpenClaude <openclaude@gitlawb.com>

---------

Co-authored-by: OpenClaude <openclaude@gitlawb.com>
2026-04-21 21:20:54 +08:00
336ddcc50d fix(api): replace phrase-based reasoning sanitizer with tag-based filter (#779)
Reasoning models (MiniMax M2.7, GLM-4.5/5, DeepSeek, Kimi K2) inline
chain-of-thought inside <think>...</think> tags in the content field
rather than using the reasoning_content channel. The prior phrase-matching
sanitizer (looksLikeLeakedReasoningPrefix) only caught English-prose
preambles like "I should"/"the user asked", missed tag-based leaks
entirely, and risked false-stripping legitimate assistant output.

Replace with a structural tag-based approach (same pattern as hermes-agent):

- createThinkTagFilter() — streaming state machine that buffers partial
  tags across SSE delta boundaries (<th| + |ink>), so tags split mid-chunk
  still parse correctly.
- stripThinkTags() — whole-text cleanup for non-streaming responses and
  as a safety net. Handles closed pairs, unterminated opens at block
  boundaries, and orphan tags.
- Recognizes think, thinking, reasoning, thought, REASONING_SCRATCHPAD
  case-insensitively, including tags with attributes.
- False-negative bias: flush() discards buffered partial tags at stream
  end rather than leaking them.

Existing phrase-based shim tests updated to exercise the actual <think>
tag leak. Added regression tests confirming legitimate prose starting
with "I should..." is preserved (the old sanitizer's main false-positive).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 15:18:58 +08:00
Kevin CodexandGitHub aab489055c fix: require trusted approval for sandbox override (#778) 2026-04-20 12:01:44 +08:00
Kevin CodexandGitHub 7002cb302b fix: enforce Bash path constraints after sandbox allow (#777) 2026-04-20 11:46:24 +08:00
Kevin CodexandGitHub 739b8d1f40 fix: enforce MCP OAuth callback state before errors (#775) 2026-04-20 09:36:05 +08:00
Kevin CodexandGitHub 13e9f22a83 feat: mask provider api key input (#772) 2026-04-20 08:25:22 +08:00
Kevin CodexandGitHub f828171ef1 fix: allow provider recovery during startup (#765) 2026-04-20 06:46:05 +08:00