Skip to content

Releases: Fanfulla/Lupin

Lupin 0.4.0: secure key rotation and xAI authentication

Choose a tag to compare

@Fanfulla Fanfulla released this 01 Sep 08:36

Highlights

  • Replace an API key safely from the TUI with c. Input stays masked, and the previous key remains active unless verification succeeds or saving anyway is explicitly confirmed.
  • Add direct xAI/Grok support through API keys and an experimental device OAuth profile.
  • Reuse long-lived TUI HTTP clients for faster, more reliable refresh and control actions.
  • Document the current CLI and TUI command surface, including a new glossary.

Security

  • Reject cross-provider catalogue profiles before resolving or sending credentials.
  • Restrict xAI OAuth credentials to HTTPS xAI hosts.
  • Redact exact API keys, access tokens, refresh tokens, and authorization codes from provider, network, OAuth, and streaming errors.
  • Update runtime HTTP dependencies and explicitly ignore local credential and runtime state.

Install or update

npm install -g lupin-code@0.4.0

Notes

xAI OAuth remains experimental and account-tier dependent. If the account is not entitled to OAuth inference, use the xAI API-key profile.

Lupin 0.2.5: native onboarding and reliable Windows updates

Choose a tag to compare

@Fanfulla Fanfulla released this 10 Aug 14:29

Lupin now opens native TUI onboarding on a fresh install, lists the provider catalogue, supports API key and OAuth setup, masks secrets, verifies credentials before saving, and moves directly into the dashboard. The README now includes a guided first-run tutorial and troubleshooting reference. On Windows, lupin update now releases daemon and watchdog package locks before npm replaces the global install, verifies process ownership, serializes lifecycle operations, restarts a previously running daemon, and bypasses a damaged global npm shim through Node's bundled npm CLI. CI is verified on Ubuntu, Windows, and the Rust TUI. Full changelog: https://github.com/Fanfulla/Lupin/blob/main/CHANGELOG.md

Lupin 0.2.3: update fixes a stale sidecar too

Choose a tag to compare

@Fanfulla Fanfulla released this 09 Aug 15:14

One fix over 0.2.2, found on the first real machine to need it: lupin update on an already-latest package now also rebuilds a lupin-tui sidecar whose --version is behind (the bootstrap case: npm i -g from a version predating the command leaves the sidecar stale). An unreadable sidecar version is left alone rather than guessed at.

Full changelog: https://github.com/Fanfulla/Lupin/blob/main/CHANGELOG.md

Lupin 0.2.2: lupin update and agents --wire

Choose a tag to compare

@Fanfulla Fanfulla released this 09 Aug 15:08

Two follow-ups to 0.2.1's agent routes, both asked for in review:

  • lupin update (ADR-49): one command from "there is a newer Lupin" to "everything on this machine runs it". Registry check (only when you invoke it, never a startup phone-home), npm i -g lupin-code@latest with npm's own output, and, when a lupin-tui sidecar is on your PATH and a Rust toolchain exists, the sidecar is rebuilt to the matching version from the TUI sources the package now ships and replaced only on build success. No sidecar, no offer; no cargo, the manual command is printed.
  • lupin agents ... --wire (ADR-48): the last manual step of subagent routing gone. set <name> --profile <p> --wire writes model: claude-lupin-agent:<name> into the agent's definition file (project .claude/agents first, then home; frontmatter name: match first, filename fallback); unset <name> --wire restores inherit. The one deliberate exception to "Lupin never touches your harness": explicit flag, that single field, old value printed, every other byte preserved, no frontmatter block means refusal, and built-in agents (no file) are named as out of reach: for those the blanket subagents route is the lever.

Full changelog: https://github.com/Fanfulla/Lupin/blob/main/CHANGELOG.md

Lupin 0.2.1: mix subagents with total control

Choose a tag to compare

@Fanfulla Fanfulla released this 09 Aug 14:38

The launch thread's most asked feature: per-subagent model and provider routing, with total control from Lupin.

  • A global agents table in the config maps a route name to a model or a profile; the id claude-lupin-agent:<name> resolves through it.
  • Put the id in an agent's frontmatter model:, the Agent tool model parameter, or CLAUDE_CODE_SUBAGENT_MODEL; declare the conventional subagents route and lupin run wires the env var for you.
  • Edit the table from the CLI (lupin agents), the TUI (a opens agents mode) or POST /v1/lupin/agents (atomic). Hot reload everywhere, no restart.
  • Every routed request is visible in the log and in lupin top / the sidecar as agent:<name>; an id naming a missing route serves on the normal path as agent:unknown:<name>, never an error.
  • Opt-in end to end: no table, no behaviour change. Content routes never reroute an agent-routed request. Nothing new is published in the model picker.

Design record: ADR-47, SPEC-PROVIDERS §4decies. The wire facts that shaped it (no header or metadata identifies a subagent; the model id travels verbatim; CLAUDE_CODE_SUBAGENT_MODEL overrides frontmatter) were verified 2026-08-09 against the official docs and issue tracker.

Full changelog: https://github.com/Fanfulla/Lupin/blob/main/CHANGELOG.md

v0.2.0: a cut stream stops passing for a finished answer

Choose a tag to compare

@Fanfulla Fanfulla released this 07 Aug 10:35

Added

  • editRetryHint, the first behavioural adapter (ADR-45, opt-in). Edits are
    applied by exact match, and a model that returns the right content with the
    wrong bytes (re-indented, tabs for spaces, trailing newline dropped) gets its
    edit refused, then often resends the same old_string for several turns. With
    this quirk enabled on a profile, the turn right after a rejected edit carries
    one extra system block naming the exact-match rule and telling the model to
    copy the bytes rather than retype them. It repairs nothing on the model's
    behalf: the proxy never rewrites old_string, because it has neither the file
    nor a way to know which occurrence was meant, and a wrong guess would corrupt
    a source file. Off by default, never applied to count_tokens, and the report
    now says on how many turns it fired, because an adapter nobody can see cannot
    be judged. Enable it knowing that it is unproven: the first live
    measurement (2026-08-07) confirmed it fires exactly when it should, and did
    not show it helping, on a model that turned out not to have the defect it
    targets. The numbers and what they do not license are in ROADMAP M5.

Changed

  • The Rust sidecar shows the same request markers as lupin top. It was
    reading four of them and dropping three, so retryAfterMs and dialect were
    documented but invisible there, and the new editHint would have been too. All
    three now print, in the wording the Node side has always used (waited:1500ms,
    dialect:a+b, editHint), with a test on each side: one log line reads the
    same whichever front end is watching it. An empty dialect list prints
    nothing rather than a marker with no name.
  • lupin doctor now names the quirks active on the profile. The report
    printed the dialect normalizations that fired but never the quirks configured
    on the profile, which the --submit body has always carried: the person
    reading their own terminal knew less than a stranger reading the scoreboard.
    The new line carries no warning glyph, because a configured quirk is true on
    every run and an alarm that always fires stops being read. It is printed
    because some request quirks (noParallelToolCalls, singleSystemMessage,
    identityHint) change what the model was asked, so a score earned under them
    is not comparable with a bare one. --json carries the same information as a
    quirks array next to dialects, always present, so a machine consumer of
    the payload alone can tell the two apart without reading the config.

Fixed

  • lupin doctor no longer scores a session that died on the transport
    (ADR-23).
    The headless result was still read starting from subtype, whose
    name promises a verdict and only reports how Claude Code's own loop exited. A
    run whose terminal_reason is api_error or auth_error is now voided on
    that field alone, without waiting for is_error to agree, so a provider that
    never answered can no longer earn the "session completed" point and a score
    that reads as a judgement on the model.

  • A truncated stream is no longer reported as a finished turn (ADR-44, issue
    #1).
    A provider that answered 200, streamed part of the answer and then
    dropped reached Claude Code as a short but well formed turn with
    stop_reason: end_turn. On the translated lanes the proxy now ends that
    stream with an error event instead of a synthesized clean close, and a tool
    call rescued from the cut text is no longer delivered. In passthrough the
    bytes are still forwarded untouched, but the request log gains
    streamError: truncated and the provider no longer earns the success that
    clears its failover cooldown. A stream that did send its stop_reason is
    unaffected, even if the connection ends before message_stop.

v0.1.2: the credential store stops lying

Choose a tag to compare

@Fanfulla Fanfulla released this 06 Aug 12:57

Two fixes for the same family of failure, both found on a real Windows machine on 2026-08-05/06.

Fixed

  • The credential split-brain now tells the truth (ADR-43). When a secret moves from the 600 file into the OS keychain (lazy promotion, or a keychain-mode overwrite of a stale file copy), the file copy is replaced by a non-secret marker instead of being deleted. A file-only install that misses the credential now says it lives in the OS keychain this install cannot read, names the move date, and gives the real fix (npm i @napi-rs/keyring, or a re-login) instead of claiming there are no credentials. Deleting a credential removes the marker too, and a pure-keychain write still never creates the file.
  • A keychain that refuses a value no longer breaks the read that already has it. Lazy promotion swallows the backend error, serves the file value, and leaves the file copy in place for the next attempt.

Full record: CHANGELOG.md, docs/DECISIONS.md ADR-43.

v0.1.1: Claude Code, on any model

Choose a tag to compare

@Fanfulla Fanfulla released this 05 Aug 14:02

npx lupin-code@latest init and you are routed.

Your setup lives in Claude Code, not in the model: MCP servers, skills, CLAUDE.md, hooks, plugins. Lupin changes the model and leaves the rest alone.

Verified, not claimed

Every score is a real headless Claude Code session, graded on artefacts on disk (files really edited, scripts that really run), never on what the model said it did.

Profile Score Notes
Kimi Code subscription 10/10 three runs at 10/10 for reproducibility
ChatGPT subscription 10/10 116s over 15 requests, 49% of input served from cache
Gemini Code Assist 8/10 free tier, ended on a tier capacity limit
GitHub Copilot works, unscored end to end on a free plan, up to a real tool call
LM Studio + gemma-4-12b 0/10 honest: the harness floor and a 10GB GPU cannot both be satisfied

Fixed since 0.1.0

  • OAuth subscriptions on Windows. With the OS keychain active, no subscription worked: the credential store sized its chunks in UTF-8 bytes while Windows stores the value as UTF-16 and counts those, so the real budget was 1280 code units rather than 2560 of anything. Measured against the live backend before fixing.
  • The TUI called a watchdog a healthy daemon. When the daemon dies the watchdog answers 529 on every path, and that body parsed into an empty health object.
  • lupin use ignored flags it did not know, including the --opus/--sonnet/--haiku that lupin login was printing as the way to aim a profile.
  • TUI secondary text was invisible on dark themes, and the job panel truncated long lines instead of wrapping them.

New

  • d runs the doctor from the TUI, : opens a command palette.
  • GitHub Copilot as a provider, and profile switching from inside Claude Code through the /model picker.

Full detail in CHANGELOG.md.