chore: migrate to oxlint and oxfmt

Co-authored-by: Christoph Nakazawa <christoph.pojer@gmail.com>
This commit is contained in:
Peter Steinberger
2026-01-14 14:31:43 +00:00
parent 912ebffc63
commit c379191f80
1480 changed files with 28608 additions and 43547 deletions

View File

@@ -78,10 +78,7 @@ export async function modelsAliasesAddCommand(
runtime.log(`Alias ${alias} -> ${resolved.provider}/${resolved.model}`);
}
export async function modelsAliasesRemoveCommand(
aliasRaw: string,
runtime: RuntimeEnv,
) {
export async function modelsAliasesRemoveCommand(aliasRaw: string, runtime: RuntimeEnv) {
const alias = normalizeAlias(aliasRaw);
const updated = await updateConfig((cfg) => {
const nextModels = { ...cfg.agents?.defaults?.models };
@@ -111,9 +108,7 @@ export async function modelsAliasesRemoveCommand(
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
if (
!updated.agents?.defaults?.models ||
Object.values(updated.agents.defaults.models).every(
(entry) => !entry?.alias?.trim(),
)
Object.values(updated.agents.defaults.models).every((entry) => !entry?.alias?.trim())
) {
runtime.log("No aliases configured.");
}

View File

@@ -1,7 +1,4 @@
import {
resolveAgentDir,
resolveDefaultAgentId,
} from "../../agents/agent-scope.js";
import { resolveAgentDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
import {
type AuthProfileStore,
ensureAuthProfileStore,
@@ -20,9 +17,7 @@ function resolveTargetAgent(
agentId: string;
agentDir: string;
} {
const agentId = raw?.trim()
? normalizeAgentId(raw.trim())
: resolveDefaultAgentId(cfg);
const agentId = raw?.trim() ? normalizeAgentId(raw.trim()) : resolveDefaultAgentId(cfg);
const agentDir = resolveAgentDir(cfg, agentId);
return { agentId, agentDir };
}
@@ -67,14 +62,8 @@ export async function modelsAuthOrderGetCommand(
runtime.log(`Agent: ${agentId}`);
runtime.log(`Provider: ${provider}`);
runtime.log(
`Auth file: ${shortenHomePath(`${agentDir}/auth-profiles.json`)}`,
);
runtime.log(
order.length > 0
? `Order override: ${order.join(", ")}`
: "Order override: (none)",
);
runtime.log(`Auth file: ${shortenHomePath(`${agentDir}/auth-profiles.json`)}`);
runtime.log(order.length > 0 ? `Order override: ${order.join(", ")}` : "Order override: (none)");
}
export async function modelsAuthOrderClearCommand(
@@ -92,8 +81,7 @@ export async function modelsAuthOrderClearCommand(
provider,
order: null,
});
if (!updated)
throw new Error("Failed to update auth-profiles.json (lock busy?).");
if (!updated) throw new Error("Failed to update auth-profiles.json (lock busy?).");
runtime.log(`Agent: ${agentId}`);
runtime.log(`Provider: ${provider}`);
@@ -115,9 +103,7 @@ export async function modelsAuthOrderSetCommand(
allowKeychainPrompt: false,
});
const providerKey = normalizeProviderId(provider);
const requested = (opts.order ?? [])
.map((entry) => String(entry).trim())
.filter(Boolean);
const requested = (opts.order ?? []).map((entry) => String(entry).trim()).filter(Boolean);
if (requested.length === 0) {
throw new Error("Missing profile ids. Provide one or more profile ids.");
}
@@ -128,9 +114,7 @@ export async function modelsAuthOrderSetCommand(
throw new Error(`Auth profile "${profileId}" not found in ${agentDir}.`);
}
if (normalizeProviderId(cred.provider) !== providerKey) {
throw new Error(
`Auth profile "${profileId}" is for ${cred.provider}, not ${provider}.`,
);
throw new Error(`Auth profile "${profileId}" is for ${cred.provider}, not ${provider}.`);
}
}
@@ -139,8 +123,7 @@ export async function modelsAuthOrderSetCommand(
provider,
order: requested,
});
if (!updated)
throw new Error("Failed to update auth-profiles.json (lock busy?).");
if (!updated) throw new Error("Failed to update auth-profiles.json (lock busy?).");
runtime.log(`Agent: ${agentId}`);
runtime.log(`Provider: ${provider}`);

View File

@@ -1,10 +1,6 @@
import { spawnSync } from "node:child_process";
import {
confirm as clackConfirm,
select as clackSelect,
text as clackText,
} from "@clack/prompts";
import { confirm as clackConfirm, select as clackSelect, text as clackText } from "@clack/prompts";
import {
CLAUDE_CLI_PROFILE_ID,
@@ -15,10 +11,7 @@ import { normalizeProviderId } from "../../agents/model-selection.js";
import { parseDurationMs } from "../../cli/parse-duration.js";
import { CONFIG_PATH_CLAWDBOT } from "../../config/config.js";
import type { RuntimeEnv } from "../../runtime.js";
import {
stylePromptHint,
stylePromptMessage,
} from "../../terminal/prompt-style.js";
import { stylePromptHint, stylePromptMessage } from "../../terminal/prompt-style.js";
import { applyAuthProfileConfig } from "../onboard-auth.js";
import { updateConfig } from "./shared.js";
@@ -37,9 +30,7 @@ const select = <T>(params: Parameters<typeof clackSelect<T>>[0]) =>
...params,
message: stylePromptMessage(params.message),
options: params.options.map((opt) =>
opt.hint === undefined
? opt
: { ...opt, hint: stylePromptHint(opt.hint) },
opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) },
),
});
@@ -121,8 +112,7 @@ export async function modelsAuthPasteTokenCommand(
throw new Error("Missing --provider.");
}
const provider = normalizeProviderId(rawProvider);
const profileId =
opts.profileId?.trim() || resolveDefaultTokenProfileId(provider);
const profileId = opts.profileId?.trim() || resolveDefaultTokenProfileId(provider);
const tokenInput = await text({
message: `Paste token for ${provider}`,
@@ -132,8 +122,7 @@ export async function modelsAuthPasteTokenCommand(
const expires =
opts.expiresIn?.trim() && opts.expiresIn.trim().length > 0
? Date.now() +
parseDurationMs(String(opts.expiresIn).trim(), { defaultUnit: "d" })
? Date.now() + parseDurationMs(String(opts.expiresIn).trim(), { defaultUnit: "d" })
: undefined;
upsertAuthProfile({
@@ -146,18 +135,13 @@ export async function modelsAuthPasteTokenCommand(
},
});
await updateConfig((cfg) =>
applyAuthProfileConfig(cfg, { profileId, provider, mode: "token" }),
);
await updateConfig((cfg) => applyAuthProfileConfig(cfg, { profileId, provider, mode: "token" }));
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
runtime.log(`Auth profile: ${profileId} (${provider}/token)`);
}
export async function modelsAuthAddCommand(
_opts: Record<string, never>,
runtime: RuntimeEnv,
) {
export async function modelsAuthAddCommand(_opts: Record<string, never>, runtime: RuntimeEnv) {
const provider = (await select({
message: "Token provider",
options: [
@@ -229,8 +213,5 @@ export async function modelsAuthAddCommand(
).trim()
: undefined;
await modelsAuthPasteTokenCommand(
{ provider: providerId, profileId, expiresIn },
runtime,
);
await modelsAuthPasteTokenCommand({ provider: providerId, profileId, expiresIn }, runtime);
}

View File

@@ -1,7 +1,4 @@
import {
buildModelAliasIndex,
resolveModelRefFromString,
} from "../../agents/model-selection.js";
import { buildModelAliasIndex, resolveModelRefFromString } from "../../agents/model-selection.js";
import { CONFIG_PATH_CLAWDBOT, loadConfig } from "../../config/config.js";
import type { RuntimeEnv } from "../../runtime.js";
import {
@@ -37,10 +34,7 @@ export async function modelsFallbacksListCommand(
for (const entry of fallbacks) runtime.log(`- ${entry}`);
}
export async function modelsFallbacksAddCommand(
modelRaw: string,
runtime: RuntimeEnv,
) {
export async function modelsFallbacksAddCommand(modelRaw: string, runtime: RuntimeEnv) {
const updated = await updateConfig((cfg) => {
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
const targetKey = modelKey(resolved.provider, resolved.model);
@@ -75,9 +69,7 @@ export async function modelsFallbacksAddCommand(
defaults: {
...cfg.agents?.defaults,
model: {
...(existingModel?.primary
? { primary: existingModel.primary }
: undefined),
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
fallbacks: [...existing, targetKey],
},
models: nextModels,
@@ -87,15 +79,10 @@ export async function modelsFallbacksAddCommand(
});
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
runtime.log(
`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`,
);
runtime.log(`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`);
}
export async function modelsFallbacksRemoveCommand(
modelRaw: string,
runtime: RuntimeEnv,
) {
export async function modelsFallbacksRemoveCommand(modelRaw: string, runtime: RuntimeEnv) {
const updated = await updateConfig((cfg) => {
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
const targetKey = modelKey(resolved.provider, resolved.model);
@@ -111,10 +98,7 @@ export async function modelsFallbacksRemoveCommand(
aliasIndex,
});
if (!resolvedEntry) return true;
return (
modelKey(resolvedEntry.ref.provider, resolvedEntry.ref.model) !==
targetKey
);
return modelKey(resolvedEntry.ref.provider, resolvedEntry.ref.model) !== targetKey;
});
if (filtered.length === existing.length) {
@@ -132,9 +116,7 @@ export async function modelsFallbacksRemoveCommand(
defaults: {
...cfg.agents?.defaults,
model: {
...(existingModel?.primary
? { primary: existingModel.primary }
: undefined),
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
fallbacks: filtered,
},
},
@@ -143,9 +125,7 @@ export async function modelsFallbacksRemoveCommand(
});
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
runtime.log(
`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`,
);
runtime.log(`Fallbacks: ${(updated.agents?.defaults?.model?.fallbacks ?? []).join(", ")}`);
}
export async function modelsFallbacksClearCommand(runtime: RuntimeEnv) {
@@ -160,9 +140,7 @@ export async function modelsFallbacksClearCommand(runtime: RuntimeEnv) {
defaults: {
...cfg.agents?.defaults,
model: {
...(existingModel?.primary
? { primary: existingModel.primary }
: undefined),
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
fallbacks: [],
},
},

View File

@@ -1,7 +1,4 @@
import {
buildModelAliasIndex,
resolveModelRefFromString,
} from "../../agents/model-selection.js";
import { buildModelAliasIndex, resolveModelRefFromString } from "../../agents/model-selection.js";
import { CONFIG_PATH_CLAWDBOT, loadConfig } from "../../config/config.js";
import type { RuntimeEnv } from "../../runtime.js";
import {
@@ -37,10 +34,7 @@ export async function modelsImageFallbacksListCommand(
for (const entry of fallbacks) runtime.log(`- ${entry}`);
}
export async function modelsImageFallbacksAddCommand(
modelRaw: string,
runtime: RuntimeEnv,
) {
export async function modelsImageFallbacksAddCommand(modelRaw: string, runtime: RuntimeEnv) {
const updated = await updateConfig((cfg) => {
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
const targetKey = modelKey(resolved.provider, resolved.model);
@@ -75,9 +69,7 @@ export async function modelsImageFallbacksAddCommand(
defaults: {
...cfg.agents?.defaults,
imageModel: {
...(existingModel?.primary
? { primary: existingModel.primary }
: undefined),
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
fallbacks: [...existing, targetKey],
},
models: nextModels,
@@ -92,10 +84,7 @@ export async function modelsImageFallbacksAddCommand(
);
}
export async function modelsImageFallbacksRemoveCommand(
modelRaw: string,
runtime: RuntimeEnv,
) {
export async function modelsImageFallbacksRemoveCommand(modelRaw: string, runtime: RuntimeEnv) {
const updated = await updateConfig((cfg) => {
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
const targetKey = modelKey(resolved.provider, resolved.model);
@@ -111,10 +100,7 @@ export async function modelsImageFallbacksRemoveCommand(
aliasIndex,
});
if (!resolvedEntry) return true;
return (
modelKey(resolvedEntry.ref.provider, resolvedEntry.ref.model) !==
targetKey
);
return modelKey(resolvedEntry.ref.provider, resolvedEntry.ref.model) !== targetKey;
});
if (filtered.length === existing.length) {
@@ -132,9 +118,7 @@ export async function modelsImageFallbacksRemoveCommand(
defaults: {
...cfg.agents?.defaults,
imageModel: {
...(existingModel?.primary
? { primary: existingModel.primary }
: undefined),
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
fallbacks: filtered,
},
},
@@ -160,9 +144,7 @@ export async function modelsImageFallbacksClearCommand(runtime: RuntimeEnv) {
defaults: {
...cfg.agents?.defaults,
imageModel: {
...(existingModel?.primary
? { primary: existingModel.primary }
: undefined),
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
fallbacks: [],
},
},

View File

@@ -6,10 +6,7 @@ import {
resolveAuthStorePathForDisplay,
resolveProfileUnusableUntilForDisplay,
} from "../../agents/auth-profiles.js";
import {
getCustomProviderApiKey,
resolveEnvApiKey,
} from "../../agents/model-auth.js";
import { getCustomProviderApiKey, resolveEnvApiKey } from "../../agents/model-auth.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { shortenHomePath } from "../../utils.js";
import { maskApiKey } from "./list.format.js";
@@ -25,10 +22,7 @@ export function resolveProviderAuthOverview(params: {
const now = Date.now();
const profiles = listProfilesForProvider(store, provider);
const withUnusableSuffix = (base: string, profileId: string) => {
const unusableUntil = resolveProfileUnusableUntilForDisplay(
store,
profileId,
);
const unusableUntil = resolveProfileUnusableUntilForDisplay(store, profileId);
if (!unusableUntil || now >= unusableUntil) return base;
const stats = store.usageStats?.[profileId];
const kind =
@@ -42,16 +36,10 @@ export function resolveProviderAuthOverview(params: {
const profile = store.profiles[profileId];
if (!profile) return `${profileId}=missing`;
if (profile.type === "api_key") {
return withUnusableSuffix(
`${profileId}=${maskApiKey(profile.key)}`,
profileId,
);
return withUnusableSuffix(`${profileId}=${maskApiKey(profile.key)}`, profileId);
}
if (profile.type === "token") {
return withUnusableSuffix(
`${profileId}=token:${maskApiKey(profile.token)}`,
profileId,
);
return withUnusableSuffix(`${profileId}=token:${maskApiKey(profile.token)}`, profileId);
}
const display = resolveAuthProfileDisplayLabel({ cfg, store, profileId });
const suffix =
@@ -63,15 +51,9 @@ export function resolveProviderAuthOverview(params: {
const base = `${profileId}=OAuth${suffix ? ` ${suffix}` : ""}`;
return withUnusableSuffix(base, profileId);
});
const oauthCount = profiles.filter(
(id) => store.profiles[id]?.type === "oauth",
).length;
const tokenCount = profiles.filter(
(id) => store.profiles[id]?.type === "token",
).length;
const apiKeyCount = profiles.filter(
(id) => store.profiles[id]?.type === "api_key",
).length;
const oauthCount = profiles.filter((id) => store.profiles[id]?.type === "oauth").length;
const tokenCount = profiles.filter((id) => store.profiles[id]?.type === "token").length;
const apiKeyCount = profiles.filter((id) => store.profiles[id]?.type === "api_key").length;
const envKey = resolveEnvApiKey(provider);
const customKey = getCustomProviderApiKey(cfg, provider);
@@ -85,8 +67,7 @@ export function resolveProviderAuthOverview(params: {
}
if (envKey) {
const isOAuthEnv =
envKey.source.includes("OAUTH_TOKEN") ||
envKey.source.toLowerCase().includes("oauth");
envKey.source.includes("OAUTH_TOKEN") || envKey.source.toLowerCase().includes("oauth");
return {
kind: "env",
detail: isOAuthEnv ? "OAuth (env)" : maskApiKey(envKey.apiKey),
@@ -112,8 +93,7 @@ export function resolveProviderAuthOverview(params: {
? {
env: {
value:
envKey.source.includes("OAUTH_TOKEN") ||
envKey.source.toLowerCase().includes("oauth")
envKey.source.includes("OAUTH_TOKEN") || envKey.source.toLowerCase().includes("oauth")
? "OAuth (env)"
: maskApiKey(envKey.apiKey),
source: envKey.source,

View File

@@ -43,12 +43,9 @@ export function resolveConfiguredEntries(cfg: ClawdbotConfig) {
const imageModelConfig = cfg.agents?.defaults?.imageModel as
| { primary?: string; fallbacks?: string[] }
| undefined;
const modelFallbacks =
typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : [];
const modelFallbacks = typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : [];
const imageFallbacks =
typeof imageModelConfig === "object"
? (imageModelConfig?.fallbacks ?? [])
: [];
typeof imageModelConfig === "object" ? (imageModelConfig?.fallbacks ?? []) : [];
const imagePrimary = imageModelConfig?.primary?.trim() ?? "";
modelFallbacks.forEach((raw, idx) => {

View File

@@ -1,19 +1,13 @@
import {
colorize,
isRich as isRichTerminal,
theme,
} from "../../terminal/theme.js";
import { colorize, isRich as isRichTerminal, theme } from "../../terminal/theme.js";
export const isRich = (opts?: { json?: boolean; plain?: boolean }) =>
Boolean(isRichTerminal() && !opts?.json && !opts?.plain);
export const pad = (value: string, size: number) => value.padEnd(size);
export const formatKey = (key: string, rich: boolean) =>
colorize(rich, theme.warn, key);
export const formatKey = (key: string, rich: boolean) => colorize(rich, theme.warn, key);
export const formatValue = (value: string, rich: boolean) =>
colorize(rich, theme.info, value);
export const formatValue = (value: string, rich: boolean) => colorize(rich, theme.info, value);
export const formatKeyValue = (
key: string,
@@ -22,8 +16,7 @@ export const formatKeyValue = (
valueColor: (value: string) => string = theme.info,
) => `${formatKey(key, rich)}=${colorize(rich, valueColor, value)}`;
export const formatSeparator = (rich: boolean) =>
colorize(rich, theme.muted, " | ");
export const formatSeparator = (rich: boolean) => colorize(rich, theme.muted, " | ");
export const formatTag = (tag: string, rich: boolean) => {
if (!rich) return tag;

View File

@@ -8,11 +8,7 @@ import { resolveConfiguredEntries } from "./list.configured.js";
import { loadModelRegistry, toModelRow } from "./list.registry.js";
import { printModelTable } from "./list.table.js";
import type { ModelRow } from "./list.types.js";
import {
DEFAULT_PROVIDER,
ensureFlagCompatibility,
modelKey,
} from "./shared.js";
import { DEFAULT_PROVIDER, ensureFlagCompatibility, modelKey } from "./shared.js";
export async function modelsListCommand(
opts: {
@@ -44,9 +40,7 @@ export async function modelsListCommand(
runtime.error(`Model registry unavailable: ${String(err)}`);
}
const modelByKey = new Map(
models.map((model) => [modelKey(model.provider, model.id), model]),
);
const modelByKey = new Map(models.map((model) => [modelKey(model.provider, model.id), model]));
const { entries } = resolveConfiguredEntries(cfg);
const configuredByKey = new Map(entries.map((entry) => [entry.key, entry]));
@@ -97,10 +91,7 @@ export async function modelsListCommand(
}
} else {
for (const entry of entries) {
if (
providerFilter &&
entry.ref.provider.toLowerCase() !== providerFilter
) {
if (providerFilter && entry.ref.provider.toLowerCase() !== providerFilter) {
continue;
}
const model = modelByKey.get(entry.key);

View File

@@ -1,16 +1,10 @@
import type { Api, Model } from "@mariozechner/pi-ai";
import {
discoverAuthStorage,
discoverModels,
} from "@mariozechner/pi-coding-agent";
import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
import { resolveClawdbotAgentDir } from "../../agents/agent-paths.js";
import type { AuthProfileStore } from "../../agents/auth-profiles.js";
import { listProfilesForProvider } from "../../agents/auth-profiles.js";
import {
getCustomProviderApiKey,
resolveEnvApiKey,
} from "../../agents/model-auth.js";
import { getCustomProviderApiKey, resolveEnvApiKey } from "../../agents/model-auth.js";
import { ensureClawdbotModelsJson } from "../../agents/models-config.js";
import type { ClawdbotConfig } from "../../config/config.js";
import type { ModelRow } from "./list.types.js";
@@ -32,11 +26,7 @@ const isLocalBaseUrl = (baseUrl: string) => {
}
};
const hasAuthForProvider = (
provider: string,
cfg: ClawdbotConfig,
authStore: AuthProfileStore,
) => {
const hasAuthForProvider = (provider: string, cfg: ClawdbotConfig, authStore: AuthProfileStore) => {
if (listProfilesForProvider(authStore, provider).length > 0) return true;
if (resolveEnvApiKey(provider)) return true;
if (getCustomProviderApiKey(cfg, provider)) return true;
@@ -50,9 +40,7 @@ export async function loadModelRegistry(cfg: ClawdbotConfig) {
const registry = discoverModels(authStorage, agentDir);
const models = registry.getAll() as Model<Api>[];
const availableModels = registry.getAvailable() as Model<Api>[];
const availableKeys = new Set(
availableModels.map((model) => modelKey(model.provider, model.id)),
);
const availableKeys = new Set(availableModels.map((model) => modelKey(model.provider, model.id)));
return { registry, models, availableKeys };
}
@@ -65,15 +53,7 @@ export function toModelRow(params: {
cfg?: ClawdbotConfig;
authStore?: AuthProfileStore;
}): ModelRow {
const {
model,
key,
tags,
aliases = [],
availableKeys,
cfg,
authStore,
} = params;
const { model, key, tags, aliases = [], availableKeys, cfg, authStore } = params;
if (!model) {
return {
key,

View File

@@ -11,25 +11,15 @@ import {
resolveProfileUnusableUntilForDisplay,
} from "../../agents/auth-profiles.js";
import { resolveEnvApiKey } from "../../agents/model-auth.js";
import {
parseModelRef,
resolveConfiguredModelRef,
} from "../../agents/model-selection.js";
import { parseModelRef, resolveConfiguredModelRef } from "../../agents/model-selection.js";
import { CONFIG_PATH_CLAWDBOT, loadConfig } from "../../config/config.js";
import {
getShellEnvAppliedKeys,
shouldEnableShellEnvFallback,
} from "../../infra/shell-env.js";
import { getShellEnvAppliedKeys, shouldEnableShellEnvFallback } from "../../infra/shell-env.js";
import type { RuntimeEnv } from "../../runtime.js";
import { colorize, theme } from "../../terminal/theme.js";
import { shortenHomePath } from "../../utils.js";
import { resolveProviderAuthOverview } from "./list.auth-overview.js";
import { isRich } from "./list.format.js";
import {
DEFAULT_MODEL,
DEFAULT_PROVIDER,
ensureFlagCompatibility,
} from "./shared.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER, ensureFlagCompatibility } from "./shared.js";
export async function modelsStatusCommand(
opts: { json?: boolean; plain?: boolean; check?: boolean },
@@ -52,26 +42,21 @@ export async function modelsStatusCommand(
| string
| undefined;
const rawModel =
typeof modelConfig === "string"
? modelConfig.trim()
: (modelConfig?.primary?.trim() ?? "");
typeof modelConfig === "string" ? modelConfig.trim() : (modelConfig?.primary?.trim() ?? "");
const resolvedLabel = `${resolved.provider}/${resolved.model}`;
const defaultLabel = rawModel || resolvedLabel;
const fallbacks =
typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : [];
const fallbacks = typeof modelConfig === "object" ? (modelConfig?.fallbacks ?? []) : [];
const imageModel =
typeof imageConfig === "string"
? imageConfig.trim()
: (imageConfig?.primary?.trim() ?? "");
const imageFallbacks =
typeof imageConfig === "object" ? (imageConfig?.fallbacks ?? []) : [];
const aliases = Object.entries(cfg.agents?.defaults?.models ?? {}).reduce<
Record<string, string>
>((acc, [key, entry]) => {
const alias = entry?.alias?.trim();
if (alias) acc[alias] = key;
return acc;
}, {});
typeof imageConfig === "string" ? imageConfig.trim() : (imageConfig?.primary?.trim() ?? "");
const imageFallbacks = typeof imageConfig === "object" ? (imageConfig?.fallbacks ?? []) : [];
const aliases = Object.entries(cfg.agents?.defaults?.models ?? {}).reduce<Record<string, string>>(
(acc, [key, entry]) => {
const alias = entry?.alias?.trim();
if (alias) acc[alias] = key;
return acc;
},
{},
);
const allowed = Object.keys(cfg.agents?.defaults?.models ?? {});
const agentDir = resolveClawdbotAgentDir();
@@ -90,22 +75,11 @@ export async function modelsStatusCommand(
);
const providersFromModels = new Set<string>();
const providersInUse = new Set<string>();
for (const raw of [
defaultLabel,
...fallbacks,
imageModel,
...imageFallbacks,
...allowed,
]) {
for (const raw of [defaultLabel, ...fallbacks, imageModel, ...imageFallbacks, ...allowed]) {
const parsed = parseModelRef(String(raw ?? ""), DEFAULT_PROVIDER);
if (parsed?.provider) providersFromModels.add(parsed.provider);
}
for (const raw of [
defaultLabel,
...fallbacks,
imageModel,
...imageFallbacks,
]) {
for (const raw of [defaultLabel, ...fallbacks, imageModel, ...imageFallbacks]) {
const parsed = parseModelRef(String(raw ?? ""), DEFAULT_PROVIDER);
if (parsed?.provider) providersInUse.add(parsed.provider);
}
@@ -145,23 +119,15 @@ export async function modelsStatusCommand(
const applied = getShellEnvAppliedKeys();
const shellFallbackEnabled =
shouldEnableShellEnvFallback(process.env) ||
cfg.env?.shellEnv?.enabled === true;
shouldEnableShellEnvFallback(process.env) || cfg.env?.shellEnv?.enabled === true;
const providerAuth = providers
.map((provider) =>
resolveProviderAuthOverview({ provider, cfg, store, modelsPath }),
)
.map((provider) => resolveProviderAuthOverview({ provider, cfg, store, modelsPath }))
.filter((entry) => {
const hasAny =
entry.profiles.count > 0 ||
Boolean(entry.env) ||
Boolean(entry.modelsJson);
const hasAny = entry.profiles.count > 0 || Boolean(entry.env) || Boolean(entry.modelsJson);
return hasAny;
});
const providerAuthMap = new Map(
providerAuth.map((entry) => [entry.provider, entry]),
);
const providerAuthMap = new Map(providerAuth.map((entry) => [entry.provider, entry]));
const missingProvidersInUse = Array.from(providersInUse)
.filter((provider) => !providerAuthMap.has(provider))
.sort((a, b) => a.localeCompare(b));
@@ -169,15 +135,11 @@ export async function modelsStatusCommand(
const providersWithOauth = providerAuth
.filter(
(entry) =>
entry.profiles.oauth > 0 ||
entry.profiles.token > 0 ||
entry.env?.value === "OAuth (env)",
entry.profiles.oauth > 0 || entry.profiles.token > 0 || entry.env?.value === "OAuth (env)",
)
.map((entry) => {
const count =
entry.profiles.oauth +
entry.profiles.token +
(entry.env?.value === "OAuth (env)" ? 1 : 0);
entry.profiles.oauth + entry.profiles.token + (entry.env?.value === "OAuth (env)" ? 1 : 0);
return `${entry.provider} (${count})`;
});
@@ -202,10 +164,7 @@ export async function modelsStatusCommand(
remainingMs: number;
}> = [];
for (const profileId of Object.keys(store.usageStats ?? {})) {
const unusableUntil = resolveProfileUnusableUntilForDisplay(
store,
profileId,
);
const unusableUntil = resolveProfileUnusableUntilForDisplay(store, profileId);
if (!unusableUntil || now >= unusableUntil) continue;
const stats = store.usageStats?.[profileId];
const kind =
@@ -226,12 +185,9 @@ export async function modelsStatusCommand(
const checkStatus = (() => {
const hasExpiredOrMissing =
oauthProfiles.some((profile) =>
["expired", "missing"].includes(profile.status),
) || missingProvidersInUse.length > 0;
const hasExpiring = oauthProfiles.some(
(profile) => profile.status === "expiring",
);
oauthProfiles.some((profile) => ["expired", "missing"].includes(profile.status)) ||
missingProvidersInUse.length > 0;
const hasExpiring = oauthProfiles.some((profile) => profile.status === "expiring");
if (hasExpiredOrMissing) return 1;
if (hasExpiring) return 2;
return 0;
@@ -282,12 +238,9 @@ export async function modelsStatusCommand(
}
const rich = isRich(opts);
const label = (value: string) =>
colorize(rich, theme.accent, value.padEnd(14));
const label = (value: string) => colorize(rich, theme.accent, value.padEnd(14));
const displayDefault =
rawModel && rawModel !== resolvedLabel
? `${resolvedLabel} (from ${rawModel})`
: resolvedLabel;
rawModel && rawModel !== resolvedLabel ? `${resolvedLabel} (from ${rawModel})` : resolvedLabel;
runtime.log(
`${label("Config")}${colorize(rich, theme.muted, ":")} ${colorize(rich, theme.info, CONFIG_PATH_CLAWDBOT)}`,
@@ -368,11 +321,7 @@ export async function modelsStatusCommand(
rich,
shellFallbackEnabled ? theme.success : theme.muted,
shellFallbackEnabled ? "on" : "off",
)}${
applied.length
? colorize(rich, theme.muted, ` (applied: ${applied.join(", ")})`)
: ""
}`,
)}${applied.length ? colorize(rich, theme.muted, ` (applied: ${applied.join(", ")})`) : ""}`,
);
runtime.log(
`${label(`Providers w/ OAuth/tokens (${providersWithOauth.length || 0})`)}${colorize(
@@ -472,9 +421,7 @@ export async function modelsStatusCommand(
? ` expires in ${formatRemainingShort(profile.remainingMs)}`
: " expires unknown";
const source =
profile.source !== "store"
? colorize(rich, theme.muted, ` (${profile.source})`)
: "";
profile.source !== "store" ? colorize(rich, theme.muted, ` (${profile.source})`) : "";
runtime.log(`- ${label} ${status}${expiry}${source}`);
}

View File

@@ -36,9 +36,7 @@ const mocks = vi.hoisted(() => {
.filter(([, cred]) => cred.provider === provider)
.map(([id]) => id);
}),
resolveAuthProfileDisplayLabel: vi.fn(
({ profileId }: { profileId: string }) => profileId,
),
resolveAuthProfileDisplayLabel: vi.fn(({ profileId }: { profileId: string }) => profileId),
resolveAuthStorePathForDisplay: vi
.fn()
.mockReturnValue("/tmp/clawdbot-agent/auth-profiles.json"),
@@ -58,9 +56,7 @@ const mocks = vi.hoisted(() => {
return null;
}),
getCustomProviderApiKey: vi.fn().mockReturnValue(undefined),
getShellEnvAppliedKeys: vi
.fn()
.mockReturnValue(["OPENAI_API_KEY", "ANTHROPIC_OAUTH_TOKEN"]),
getShellEnvAppliedKeys: vi.fn().mockReturnValue(["OPENAI_API_KEY", "ANTHROPIC_OAUTH_TOKEN"]),
shouldEnableShellEnvFallback: vi.fn().mockReturnValue(true),
loadConfig: vi.fn().mockReturnValue({
agents: {
@@ -80,8 +76,7 @@ vi.mock("../../agents/agent-paths.js", () => ({
}));
vi.mock("../../agents/auth-profiles.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../agents/auth-profiles.js")>();
const actual = await importOriginal<typeof import("../../agents/auth-profiles.js")>();
return {
...actual,
ensureAuthProfileStore: mocks.ensureAuthProfileStore,
@@ -102,8 +97,7 @@ vi.mock("../../infra/shell-env.js", () => ({
}));
vi.mock("../../config/config.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../../config/config.js")>();
const actual = await importOriginal<typeof import("../../config/config.js")>();
return {
...actual,
loadConfig: mocks.loadConfig,
@@ -121,18 +115,12 @@ const runtime = {
describe("modelsStatusCommand auth overview", () => {
it("includes masked auth sources in JSON output", async () => {
await modelsStatusCommand({ json: true }, runtime as never);
const payload = JSON.parse(
String((runtime.log as vi.Mock).mock.calls[0][0]),
);
const payload = JSON.parse(String((runtime.log as vi.Mock).mock.calls[0][0]));
expect(payload.defaultModel).toBe("anthropic/claude-opus-4-5");
expect(payload.auth.storePath).toBe(
"/tmp/clawdbot-agent/auth-profiles.json",
);
expect(payload.auth.storePath).toBe("/tmp/clawdbot-agent/auth-profiles.json");
expect(payload.auth.shellEnvFallback.enabled).toBe(true);
expect(payload.auth.shellEnvFallback.appliedKeys).toContain(
"OPENAI_API_KEY",
);
expect(payload.auth.shellEnvFallback.appliedKeys).toContain("OPENAI_API_KEY");
expect(payload.auth.missingProvidersInUse).toEqual([]);
expect(payload.auth.oauth.warnAfterMs).toBeGreaterThan(0);
expect(payload.auth.oauth.profiles.length).toBeGreaterThan(0);
@@ -152,14 +140,10 @@ describe("modelsStatusCommand auth overview", () => {
expect(openai?.env?.value).toContain("...");
expect(
(payload.auth.providersWithOAuth as string[]).some((e) =>
e.startsWith("anthropic"),
),
(payload.auth.providersWithOAuth as string[]).some((e) => e.startsWith("anthropic")),
).toBe(true);
expect(
(payload.auth.providersWithOAuth as string[]).some((e) =>
e.startsWith("openai-codex"),
),
(payload.auth.providersWithOAuth as string[]).some((e) => e.startsWith("openai-codex")),
).toBe(true);
});
@@ -175,10 +159,7 @@ describe("modelsStatusCommand auth overview", () => {
mocks.resolveEnvApiKey.mockImplementation(() => null);
try {
await modelsStatusCommand(
{ check: true, plain: true },
localRuntime as never,
);
await modelsStatusCommand({ check: true, plain: true }, localRuntime as never);
expect(localRuntime.exit).toHaveBeenCalledWith(1);
} finally {
mocks.store.profiles = originalProfiles;

View File

@@ -51,8 +51,7 @@ export function printModelTable(
const ctxLabel = pad(formatTokenK(row.contextWindow), CTX_PAD);
const localText = row.local === null ? "-" : row.local ? "yes" : "no";
const localLabel = pad(localText, LOCAL_PAD);
const authText =
row.available === null ? "-" : row.available ? "yes" : "no";
const authText = row.available === null ? "-" : row.available ? "yes" : "no";
const authLabel = pad(authText, AUTH_PAD);
const tagsLabel =
row.tags.length > 0
@@ -68,20 +67,12 @@ export function printModelTable(
);
const coloredLocal = colorize(
rich,
row.local === null
? theme.muted
: row.local
? theme.success
: theme.muted,
row.local === null ? theme.muted : row.local ? theme.success : theme.muted,
localLabel,
);
const coloredAuth = colorize(
rich,
row.available === null
? theme.muted
: row.available
? theme.success
: theme.error,
row.available === null ? theme.muted : row.available ? theme.success : theme.error,
authLabel,
);

View File

@@ -1,13 +1,6 @@
import {
cancel,
multiselect as clackMultiselect,
isCancel,
} from "@clack/prompts";
import { cancel, multiselect as clackMultiselect, isCancel } from "@clack/prompts";
import { resolveApiKeyForProvider } from "../../agents/model-auth.js";
import {
type ModelScanResult,
scanOpenRouterModels,
} from "../../agents/model-scan.js";
import { type ModelScanResult, scanOpenRouterModels } from "../../agents/model-scan.js";
import { withProgressTotals } from "../../cli/progress.js";
import { CONFIG_PATH_CLAWDBOT, loadConfig } from "../../config/config.js";
import type { RuntimeEnv } from "../../runtime.js";
@@ -26,9 +19,7 @@ const multiselect = <T>(params: Parameters<typeof clackMultiselect<T>>[0]) =>
...params,
message: stylePromptMessage(params.message),
options: params.options.map((opt) =>
opt.hint === undefined
? opt
: { ...opt, hint: stylePromptHint(opt.hint) },
opt.hint === undefined ? opt : { ...opt, hint: stylePromptHint(opt.hint) },
),
});
@@ -81,21 +72,15 @@ function sortImageResults(results: ModelScanResult[]): ModelScanResult[] {
}
function buildScanHint(result: ModelScanResult): string {
const toolLabel = result.tool.ok
? `tool ${formatMs(result.tool.latencyMs)}`
: "tool fail";
const toolLabel = result.tool.ok ? `tool ${formatMs(result.tool.latencyMs)}` : "tool fail";
const imageLabel = result.image.skipped
? "img skip"
: result.image.ok
? `img ${formatMs(result.image.latencyMs)}`
: "img fail";
const ctxLabel = result.contextLength
? `ctx ${formatTokenK(result.contextLength)}`
: "ctx ?";
const ctxLabel = result.contextLength ? `ctx ${formatTokenK(result.contextLength)}` : "ctx ?";
const paramLabel = result.inferredParamB ? `${result.inferredParamB}b` : null;
return [toolLabel, imageLabel, ctxLabel, paramLabel]
.filter(Boolean)
.join(" | ");
return [toolLabel, imageLabel, ctxLabel, paramLabel].filter(Boolean).join(" | ");
}
function printScanSummary(results: ModelScanResult[], runtime: RuntimeEnv) {
@@ -121,30 +106,16 @@ function printScanTable(results: ModelScanResult[], runtime: RuntimeEnv) {
for (const entry of results) {
const modelLabel = pad(truncate(entry.modelRef, MODEL_PAD), MODEL_PAD);
const toolLabel = pad(
entry.tool.ok ? formatMs(entry.tool.latencyMs) : "fail",
10,
);
const toolLabel = pad(entry.tool.ok ? formatMs(entry.tool.latencyMs) : "fail", 10);
const imageLabel = pad(
entry.image.ok
? formatMs(entry.image.latencyMs)
: entry.image.skipped
? "skip"
: "fail",
entry.image.ok ? formatMs(entry.image.latencyMs) : entry.image.skipped ? "skip" : "fail",
10,
);
const ctxLabel = pad(formatTokenK(entry.contextLength), CTX_PAD);
const paramsLabel = pad(
entry.inferredParamB ? `${entry.inferredParamB}b` : "-",
8,
);
const paramsLabel = pad(entry.inferredParamB ? `${entry.inferredParamB}b` : "-", 8);
const notes = entry.modality ? `modality:${entry.modality}` : "";
runtime.log(
[modelLabel, toolLabel, imageLabel, ctxLabel, paramsLabel, notes].join(
" ",
),
);
runtime.log([modelLabel, toolLabel, imageLabel, ctxLabel, paramsLabel, notes].join(" "));
}
}
@@ -166,17 +137,11 @@ export async function modelsScanCommand(
runtime: RuntimeEnv,
) {
const minParams = opts.minParams ? Number(opts.minParams) : undefined;
if (
minParams !== undefined &&
(!Number.isFinite(minParams) || minParams < 0)
) {
if (minParams !== undefined && (!Number.isFinite(minParams) || minParams < 0)) {
throw new Error("--min-params must be >= 0");
}
const maxAgeDays = opts.maxAgeDays ? Number(opts.maxAgeDays) : undefined;
if (
maxAgeDays !== undefined &&
(!Number.isFinite(maxAgeDays) || maxAgeDays < 0)
) {
if (maxAgeDays !== undefined && (!Number.isFinite(maxAgeDays) || maxAgeDays < 0)) {
throw new Error("--max-age-days must be >= 0");
}
const maxCandidates = opts.maxCandidates ? Number(opts.maxCandidates) : 6;
@@ -188,10 +153,7 @@ export async function modelsScanCommand(
throw new Error("--timeout must be > 0");
}
const concurrency = opts.concurrency ? Number(opts.concurrency) : undefined;
if (
concurrency !== undefined &&
(!Number.isFinite(concurrency) || concurrency <= 0)
) {
if (concurrency !== undefined && (!Number.isFinite(concurrency) || concurrency <= 0)) {
throw new Error("--concurrency must be > 0");
}
@@ -288,9 +250,7 @@ export async function modelsScanCommand(
});
if (isCancel(selection)) {
cancel(
stylePromptTitle("Model scan cancelled.") ?? "Model scan cancelled.",
);
cancel(stylePromptTitle("Model scan cancelled.") ?? "Model scan cancelled.");
runtime.exit(0);
}
@@ -307,9 +267,7 @@ export async function modelsScanCommand(
});
if (isCancel(imageSelection)) {
cancel(
stylePromptTitle("Model scan cancelled.") ?? "Model scan cancelled.",
);
cancel(stylePromptTitle("Model scan cancelled.") ?? "Model scan cancelled.");
runtime.exit(0);
}
@@ -340,9 +298,7 @@ export async function modelsScanCommand(
const nextImageModel =
selectedImages.length > 0
? {
...(existingImageModel?.primary
? { primary: existingImageModel.primary }
: undefined),
...(existingImageModel?.primary ? { primary: existingImageModel.primary } : undefined),
fallbacks: selectedImages,
...(opts.setImage ? { primary: selectedImages[0] } : {}),
}
@@ -353,9 +309,7 @@ export async function modelsScanCommand(
const defaults = {
...cfg.agents?.defaults,
model: {
...(existingModel?.primary
? { primary: existingModel.primary }
: undefined),
...(existingModel?.primary ? { primary: existingModel.primary } : undefined),
fallbacks: selected,
...(opts.setDefault ? { primary: selected[0] } : {}),
},

View File

@@ -2,10 +2,7 @@ import { CONFIG_PATH_CLAWDBOT } from "../../config/config.js";
import type { RuntimeEnv } from "../../runtime.js";
import { resolveModelTarget, updateConfig } from "./shared.js";
export async function modelsSetImageCommand(
modelRaw: string,
runtime: RuntimeEnv,
) {
export async function modelsSetImageCommand(modelRaw: string, runtime: RuntimeEnv) {
const updated = await updateConfig((cfg) => {
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
const key = `${resolved.provider}/${resolved.model}`;
@@ -21,9 +18,7 @@ export async function modelsSetImageCommand(
defaults: {
...cfg.agents?.defaults,
imageModel: {
...(existingModel?.fallbacks
? { fallbacks: existingModel.fallbacks }
: undefined),
...(existingModel?.fallbacks ? { fallbacks: existingModel.fallbacks } : undefined),
primary: key,
},
models: nextModels,
@@ -33,7 +28,5 @@ export async function modelsSetImageCommand(
});
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
runtime.log(
`Image model: ${updated.agents?.defaults?.imageModel?.primary ?? modelRaw}`,
);
runtime.log(`Image model: ${updated.agents?.defaults?.imageModel?.primary ?? modelRaw}`);
}

View File

@@ -18,9 +18,7 @@ export async function modelsSetCommand(modelRaw: string, runtime: RuntimeEnv) {
defaults: {
...cfg.agents?.defaults,
model: {
...(existingModel?.fallbacks
? { fallbacks: existingModel.fallbacks }
: undefined),
...(existingModel?.fallbacks ? { fallbacks: existingModel.fallbacks } : undefined),
primary: key,
},
models: nextModels,
@@ -30,7 +28,5 @@ export async function modelsSetCommand(modelRaw: string, runtime: RuntimeEnv) {
});
runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
runtime.log(
`Default model: ${updated.agents?.defaults?.model?.primary ?? modelRaw}`,
);
runtime.log(`Default model: ${updated.agents?.defaults?.model?.primary ?? modelRaw}`);
}

View File

@@ -11,10 +11,7 @@ import {
writeConfigFile,
} from "../../config/config.js";
export const ensureFlagCompatibility = (opts: {
json?: boolean;
plain?: boolean;
}) => {
export const ensureFlagCompatibility = (opts: { json?: boolean; plain?: boolean }) => {
if (opts.json && opts.plain) {
throw new Error("Choose either --json or --plain, not both.");
}
@@ -38,9 +35,7 @@ export async function updateConfig(
): Promise<ClawdbotConfig> {
const snapshot = await readConfigFileSnapshot();
if (!snapshot.valid) {
const issues = snapshot.issues
.map((issue) => `- ${issue.path}: ${issue.message}`)
.join("\n");
const issues = snapshot.issues.map((issue) => `- ${issue.path}: ${issue.message}`).join("\n");
throw new Error(`Invalid config at ${snapshot.path}\n${issues}`);
}
const next = mutator(snapshot.config);
@@ -48,10 +43,10 @@ export async function updateConfig(
return next;
}
export function resolveModelTarget(params: {
raw: string;
cfg: ClawdbotConfig;
}): { provider: string; model: string } {
export function resolveModelTarget(params: { raw: string; cfg: ClawdbotConfig }): {
provider: string;
model: string;
} {
const aliasIndex = buildModelAliasIndex({
cfg: params.cfg,
defaultProvider: DEFAULT_PROVIDER,
@@ -82,9 +77,7 @@ export function normalizeAlias(alias: string): string {
const trimmed = alias.trim();
if (!trimmed) throw new Error("Alias cannot be empty.");
if (!/^[A-Za-z0-9_.:-]+$/.test(trimmed)) {
throw new Error(
"Alias must use letters, numbers, dots, underscores, colons, or dashes.",
);
throw new Error("Alias must use letters, numbers, dots, underscores, colons, or dashes.");
}
return trimmed;
}