From 43d7ec5577154ec48afeaf908bfb636b6e742ba5 Mon Sep 17 00:00:00 2001 From: Ivanko1608 <29154527+Ivanko1608@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:16:51 +0400 Subject: [PATCH] fix: count claude-code as configured when the CLI is installed --- src/server/model-catalog.ts | 13 +++++--- tests/model-catalog.test.ts | 61 ++++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 11 deletions(-) diff --git a/src/server/model-catalog.ts b/src/server/model-catalog.ts index e833368..633a2f7 100644 --- a/src/server/model-catalog.ts +++ b/src/server/model-catalog.ts @@ -43,6 +43,7 @@ import type { } from "../core/contracts/models.ts"; import type { EffectiveModelSpec, EffectiveSlot } from "../core/settings-resolution.ts"; import { AUTH_PROVIDER_KEYS } from "../core/auth-keys.ts"; +import { getClaudeBinary } from "../agent/claude-code/binary.ts"; import { CLAUDE_CODE_PROVIDER } from "../agent/claude-code/constants.ts"; import { BACKGROUND_MODEL_ROLES, MODEL_SLOTS, type BackgroundModelRole, type ModelSlotName, type ModelsSettings } from "../core/settings.ts"; @@ -135,15 +136,19 @@ export function catalogProviders(): CatalogProvider[] { id: p.id, envVar: p.envVar, keyPresent: keyValue(p.envVar) !== undefined, - })).concat([{ id: "chatgpt", envVar: "", keyPresent: chatGPTConnected() }]).sort((a, b) => a.id.localeCompare(b.id)); + })) + .concat([ + { id: "chatgpt", envVar: "", keyPresent: chatGPTConnected() }, + { id: CLAUDE_CODE_PROVIDER, envVar: "", keyPresent: getClaudeBinary() !== null }, + ]) + .sort((a, b) => a.id.localeCompare(b.id)); } /** The providers whose key is present — the picker federates exactly * these (a keyless provider's models are unreachable by design; the - * Keys page is the discovery surface, same discipline as search), plus - * claude-code, which runs on the local CLI's subscription and takes no key. */ + * Keys page is the discovery surface, same discipline as search). */ function keyedProviderIds(): Set { - return new Set([...catalogProviders().filter((p) => p.keyPresent).map((p) => p.id), CLAUDE_CODE_PROVIDER]); + return new Set(catalogProviders().filter((p) => p.keyPresent).map((p) => p.id)); } /** Project one catalog model to the client DTO. */ diff --git a/tests/model-catalog.test.ts b/tests/model-catalog.test.ts index 5d93a8e..b3de078 100644 --- a/tests/model-catalog.test.ts +++ b/tests/model-catalog.test.ts @@ -1,3 +1,6 @@ +import * as fs from "node:fs"; +import * as os from "node:os"; +import * as path from "node:path"; import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { @@ -329,16 +332,60 @@ describe("pi thinking truth (pinned against pi-ai upgrades)", () => { assert.throws(() => piEngineFor("prober"), /no configured model for role "prober"/); }); }); +/** Runs fn with PATH and HOME pointed at a temp dir, so the CLI lookup only + * finds a claude binary when installed is true. */ +function withClaudeInstalled(installed: boolean, fn: () => void): void { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "claude-catalog-")); + const saved = { PATH: process.env.PATH, HOME: process.env.HOME, npm_config_prefix: process.env.npm_config_prefix }; + + if (installed) fs.writeFileSync(path.join(dir, "claude"), "#!/bin/sh\n", { mode: 0o755 }); + process.env.PATH = dir; + process.env.HOME = dir; + delete process.env.npm_config_prefix; + + try { + fn(); + } finally { + for (const [name, value] of Object.entries(saved)) { + if (value === undefined) delete process.env[name]; + else process.env[name] = value; + } + fs.rmSync(dir, { recursive: true, force: true }); + } +} + describe("the claude-code provider (subscription CLI, no key)", () => { - it("modelCatalog() lists claude-code opus/sonnet/haiku with no keys saved", () => { - const ids = modelCatalog() - .models.filter((m) => m.provider === "claude-code") - .map((m) => m.id); - assert.deepEqual(ids, ["haiku", "opus", "sonnet"]); + it("modelCatalog() lists claude-code opus/sonnet/haiku when the CLI is installed", () => { + withClaudeInstalled(true, () => { + const ids = modelCatalog() + .models.filter((m) => m.provider === "claude-code") + .map((m) => m.id); + assert.deepEqual(ids, ["haiku", "opus", "sonnet"]); + }); }); - it("catalogContextLimits() has claude-code:opus", () => { - assert.ok((catalogContextLimits().get("claude-code:opus") ?? 0) > 0); + it("catalogProviders() marks claude-code present when the CLI is installed", () => { + withClaudeInstalled(true, () => { + const row = catalogProviders().find((p) => p.id === "claude-code"); + assert.deepEqual(row, { id: "claude-code", envVar: "", keyPresent: true }); + }); + }); + + it("catalogProviders() marks claude-code absent and modelCatalog() hides it when the CLI is missing", () => { + withClaudeInstalled(false, () => { + const row = catalogProviders().find((p) => p.id === "claude-code"); + assert.equal(row?.keyPresent, false); + assert.equal( + modelCatalog().models.some((m) => m.provider === "claude-code"), + false, + ); + }); + }); + + it("catalogContextLimits() has claude-code:opus when the CLI is installed", () => { + withClaudeInstalled(true, () => { + assert.ok((catalogContextLimits().get("claude-code:opus") ?? 0) > 0); + }); }); it('piEngineForSpec({ provider: "claude-code", id: "sonnet" }) resolves', () => {