feat(auth): add profile reuse and fix TypeScript errors [AI-assisted]

This commit is contained in:
Nattapong Tapachoom 2026-01-28 08:38:55 +07:00
parent 3fe4b2595a
commit 8f594617bb
13 changed files with 926 additions and 262 deletions

View File

@ -1,12 +1,23 @@
import { upsertAuthProfile } from "../agents/auth-profiles.js"; import { upsertAuthProfile } from "../agents/auth-profiles.js";
declare const process: any;
import { import {
formatApiKeyPreview, formatApiKeyPreview,
normalizeApiKeyInput, normalizeApiKeyInput,
validateApiKeyInput, validateApiKeyInput,
} from "./auth-choice.api-key.js"; } from "./auth-choice.api-key.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import type {
import { buildTokenProfileId, validateAnthropicSetupToken } from "./auth-token.js"; ApplyAuthChoiceParams,
import { applyAuthProfileConfig, setAnthropicApiKey } from "./onboard-auth.js"; ApplyAuthChoiceResult,
} from "./auth-choice.apply.js";
import {
buildTokenProfileId,
validateAnthropicSetupToken,
} from "./auth-token.js";
import {
applyAuthProfileConfig,
ensureAuthProfileStore,
setAnthropicApiKey,
} from "./onboard-auth.js";
export async function applyAuthChoiceAnthropic( export async function applyAuthChoiceAnthropic(
params: ApplyAuthChoiceParams, params: ApplyAuthChoiceParams,
@ -18,9 +29,10 @@ export async function applyAuthChoiceAnthropic(
) { ) {
let nextConfig = params.config; let nextConfig = params.config;
await params.prompter.note( await params.prompter.note(
["Run `claude setup-token` in your terminal.", "Then paste the generated token below."].join( [
"\n", "Run `claude setup-token` in your terminal.",
), "Then paste the generated token below.",
].join("\n"),
"Anthropic setup-token", "Anthropic setup-token",
); );
@ -59,7 +71,10 @@ export async function applyAuthChoiceAnthropic(
} }
if (params.authChoice === "apiKey") { if (params.authChoice === "apiKey") {
if (params.opts?.tokenProvider && params.opts.tokenProvider !== "anthropic") { if (
params.opts?.tokenProvider &&
params.opts.tokenProvider !== "anthropic"
) {
return null; return null;
} }
@ -68,7 +83,10 @@ export async function applyAuthChoiceAnthropic(
const envKey = process.env.ANTHROPIC_API_KEY?.trim(); const envKey = process.env.ANTHROPIC_API_KEY?.trim();
if (params.opts?.token) { if (params.opts?.token) {
await setAnthropicApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); await setAnthropicApiKey(
normalizeApiKeyInput(params.opts.token),
params.agentDir,
);
hasCredential = true; hasCredential = true;
} }
@ -82,12 +100,32 @@ export async function applyAuthChoiceAnthropic(
hasCredential = true; hasCredential = true;
} }
} }
if (!hasCredential) {
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const existing = store.profiles["anthropic:default"];
if (existing && existing.type === "api_key") {
const useExisting = await params.prompter.confirm({
message: `Use existing Anthropic API key (${formatApiKeyPreview(existing.key)})?`,
initialValue: true,
});
if (useExisting) {
hasCredential = true;
}
}
}
if (!hasCredential) { if (!hasCredential) {
const key = await params.prompter.text({ const key = await params.prompter.text({
message: "Enter Anthropic API key", message: "Enter Anthropic API key",
validate: validateApiKeyInput, validate: validateApiKeyInput,
}); });
await setAnthropicApiKey(normalizeApiKeyInput(String(key)), params.agentDir); await setAnthropicApiKey(
normalizeApiKeyInput(String(key)),
params.agentDir,
);
} }
nextConfig = applyAuthProfileConfig(nextConfig, { nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "anthropic:default", profileId: "anthropic:default",

View File

@ -1,11 +1,17 @@
import { ensureAuthProfileStore, resolveAuthProfileOrder } from "../agents/auth-profiles.js"; import {
ensureAuthProfileStore,
resolveAuthProfileOrder,
} from "../agents/auth-profiles.js";
import { resolveEnvApiKey } from "../agents/model-auth.js"; import { resolveEnvApiKey } from "../agents/model-auth.js";
import { import {
formatApiKeyPreview, formatApiKeyPreview,
normalizeApiKeyInput, normalizeApiKeyInput,
validateApiKeyInput, validateApiKeyInput,
} from "./auth-choice.api-key.js"; } from "./auth-choice.api-key.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import type {
ApplyAuthChoiceParams,
ApplyAuthChoiceResult,
} from "./auth-choice.apply.js";
import { applyDefaultModelChoice } from "./auth-choice.default-model.js"; import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
import { import {
applyGoogleGeminiModelDefault, applyGoogleGeminiModelDefault,
@ -97,8 +103,12 @@ export async function applyAuthChoiceApiProviders(
store, store,
provider: "openrouter", provider: "openrouter",
}); });
const existingProfileId = profileOrder.find((profileId) => Boolean(store.profiles[profileId])); const existingProfileId = profileOrder.find((profileId) =>
const existingCred = existingProfileId ? store.profiles[existingProfileId] : undefined; Boolean(store.profiles[profileId]),
);
const existingCred = existingProfileId
? store.profiles[existingProfileId]
: undefined;
let profileId = "openrouter:default"; let profileId = "openrouter:default";
let mode: "api_key" | "oauth" | "token" = "api_key"; let mode: "api_key" | "oauth" | "token" = "api_key";
let hasCredential = false; let hasCredential = false;
@ -114,8 +124,15 @@ export async function applyAuthChoiceApiProviders(
hasCredential = true; hasCredential = true;
} }
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "openrouter") { if (
await setOpenrouterApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); !hasCredential &&
params.opts?.token &&
params.opts?.tokenProvider === "openrouter"
) {
await setOpenrouterApiKey(
normalizeApiKeyInput(params.opts.token),
params.agentDir,
);
hasCredential = true; hasCredential = true;
} }
@ -138,7 +155,10 @@ export async function applyAuthChoiceApiProviders(
message: "Enter OpenRouter API key", message: "Enter OpenRouter API key",
validate: validateApiKeyInput, validate: validateApiKeyInput,
}); });
await setOpenrouterApiKey(normalizeApiKeyInput(String(key)), params.agentDir); await setOpenrouterApiKey(
normalizeApiKeyInput(String(key)),
params.agentDir,
);
hasCredential = true; hasCredential = true;
} }
@ -174,12 +194,40 @@ export async function applyAuthChoiceApiProviders(
params.opts?.token && params.opts?.token &&
params.opts?.tokenProvider === "vercel-ai-gateway" params.opts?.tokenProvider === "vercel-ai-gateway"
) { ) {
await setVercelAiGatewayApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); await setVercelAiGatewayApiKey(
normalizeApiKeyInput(params.opts.token),
params.agentDir,
);
hasCredential = true; hasCredential = true;
} }
const envKey = resolveEnvApiKey("vercel-ai-gateway"); const envKey = resolveEnvApiKey("vercel-ai-gateway");
if (envKey) { const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const profileOrder = resolveAuthProfileOrder({
cfg: nextConfig,
store,
provider: "vercel-ai-gateway",
});
const existingProfileId = profileOrder.find((pid) =>
Boolean(store.profiles[pid]),
);
let profileId = "vercel-ai-gateway:default";
if (existingProfileId) {
const existing = store.profiles[existingProfileId];
const useExisting = await params.prompter.confirm({
message: `Use existing Vercel AI Gateway authentication (${"email" in existing && existing.email ? existing.email : existingProfileId})?`,
initialValue: true,
});
if (useExisting) {
profileId = existingProfileId;
hasCredential = true;
}
}
if (!hasCredential && envKey) {
const useExisting = await params.prompter.confirm({ const useExisting = await params.prompter.confirm({
message: `Use existing AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, message: `Use existing AI_GATEWAY_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
initialValue: true, initialValue: true,
@ -194,10 +242,13 @@ export async function applyAuthChoiceApiProviders(
message: "Enter Vercel AI Gateway API key", message: "Enter Vercel AI Gateway API key",
validate: validateApiKeyInput, validate: validateApiKeyInput,
}); });
await setVercelAiGatewayApiKey(normalizeApiKeyInput(String(key)), params.agentDir); await setVercelAiGatewayApiKey(
normalizeApiKeyInput(String(key)),
params.agentDir,
);
} }
nextConfig = applyAuthProfileConfig(nextConfig, { nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "vercel-ai-gateway:default", profileId,
provider: "vercel-ai-gateway", provider: "vercel-ai-gateway",
mode: "api_key", mode: "api_key",
}); });
@ -219,36 +270,77 @@ export async function applyAuthChoiceApiProviders(
} }
if (authChoice === "moonshot-api-key") { if (authChoice === "moonshot-api-key") {
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const profileOrder = resolveAuthProfileOrder({
cfg: nextConfig,
store,
provider: "moonshot",
});
const existingProfileId = profileOrder.find((pid) =>
Boolean(store.profiles[pid]),
);
let profileId = "moonshot:default";
let hasCredential = false; let hasCredential = false;
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "moonshot") { if (existingProfileId) {
await setMoonshotApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); const existing = store.profiles[existingProfileId];
hasCredential = true;
}
const envKey = resolveEnvApiKey("moonshot");
if (envKey) {
const useExisting = await params.prompter.confirm({ const useExisting = await params.prompter.confirm({
message: `Use existing MOONSHOT_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, message: `Use existing Moonshot authentication (${"email" in existing && existing.email ? existing.email : existingProfileId})?`,
initialValue: true, initialValue: true,
}); });
if (useExisting) { if (useExisting) {
await setMoonshotApiKey(envKey.apiKey, params.agentDir); profileId = existingProfileId;
hasCredential = true; hasCredential = true;
} }
} }
if (
!hasCredential &&
params.opts?.token &&
params.opts?.tokenProvider === "moonshot"
) {
await setMoonshotApiKey(
normalizeApiKeyInput(params.opts.token),
params.agentDir,
);
hasCredential = true;
}
if (!hasCredential) {
const envKey = resolveEnvApiKey("moonshot");
if (envKey) {
const useExisting = await params.prompter.confirm({
message: `Use existing MOONSHOT_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
initialValue: true,
});
if (useExisting) {
await setMoonshotApiKey(envKey.apiKey, params.agentDir);
hasCredential = true;
}
}
}
if (!hasCredential) { if (!hasCredential) {
const key = await params.prompter.text({ const key = await params.prompter.text({
message: "Enter Moonshot API key", message: "Enter Moonshot API key",
validate: validateApiKeyInput, validate: validateApiKeyInput,
}); });
await setMoonshotApiKey(normalizeApiKeyInput(String(key)), params.agentDir); await setMoonshotApiKey(
normalizeApiKeyInput(String(key)),
params.agentDir,
);
hasCredential = true;
}
if (hasCredential) {
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId,
provider: "moonshot",
mode: "api_key",
});
} }
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "moonshot:default",
provider: "moonshot",
mode: "api_key",
});
{ {
const applied = await applyDefaultModelChoice({ const applied = await applyDefaultModelChoice({
config: nextConfig, config: nextConfig,
@ -266,9 +358,41 @@ export async function applyAuthChoiceApiProviders(
} }
if (authChoice === "kimi-code-api-key") { if (authChoice === "kimi-code-api-key") {
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const profileOrder = resolveAuthProfileOrder({
cfg: nextConfig,
store,
provider: "kimi-code",
});
const existingProfileId = profileOrder.find((pid) =>
Boolean(store.profiles[pid]),
);
let profileId = "kimi-code:default";
let hasCredential = false; let hasCredential = false;
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "kimi-code") {
await setKimiCodeApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); if (existingProfileId) {
const existing = store.profiles[existingProfileId];
const useExisting = await params.prompter.confirm({
message: `Use existing Kimi Code authentication (${"email" in existing && existing.email ? existing.email : existingProfileId})?`,
initialValue: true,
});
if (useExisting) {
profileId = existingProfileId;
hasCredential = true;
}
}
if (
!hasCredential &&
params.opts?.token &&
params.opts?.tokenProvider === "kimi-code"
) {
await setKimiCodeApiKey(
normalizeApiKeyInput(params.opts.token),
params.agentDir,
);
hasCredential = true; hasCredential = true;
} }
@ -280,30 +404,39 @@ export async function applyAuthChoiceApiProviders(
].join("\n"), ].join("\n"),
"Kimi Code", "Kimi Code",
); );
}
const envKey = resolveEnvApiKey("kimi-code"); const envKey = resolveEnvApiKey("kimi-code");
if (envKey) { if (envKey) {
const useExisting = await params.prompter.confirm({ const useExisting = await params.prompter.confirm({
message: `Use existing KIMICODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, message: `Use existing KIMICODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
initialValue: true, initialValue: true,
}); });
if (useExisting) { if (useExisting) {
await setKimiCodeApiKey(envKey.apiKey, params.agentDir); await setKimiCodeApiKey(envKey.apiKey, params.agentDir);
hasCredential = true; hasCredential = true;
}
} }
} }
if (!hasCredential) { if (!hasCredential) {
const key = await params.prompter.text({ const key = await params.prompter.text({
message: "Enter Kimi Code API key", message: "Enter Kimi Code API key",
validate: validateApiKeyInput, validate: validateApiKeyInput,
}); });
await setKimiCodeApiKey(normalizeApiKeyInput(String(key)), params.agentDir); await setKimiCodeApiKey(
normalizeApiKeyInput(String(key)),
params.agentDir,
);
hasCredential = true;
}
if (hasCredential) {
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId,
provider: "kimi-code",
mode: "api_key",
});
} }
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "kimi-code:default",
provider: "kimi-code",
mode: "api_key",
});
{ {
const applied = await applyDefaultModelChoice({ const applied = await applyDefaultModelChoice({
config: nextConfig, config: nextConfig,
@ -322,36 +455,75 @@ export async function applyAuthChoiceApiProviders(
} }
if (authChoice === "gemini-api-key") { if (authChoice === "gemini-api-key") {
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const profileOrder = resolveAuthProfileOrder({
cfg: nextConfig,
store,
provider: "google",
});
const existingProfileId = profileOrder.find((pid) =>
Boolean(store.profiles[pid]),
);
let profileId = "google:default";
let hasCredential = false; let hasCredential = false;
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "google") { if (existingProfileId) {
await setGeminiApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); const existing = store.profiles[existingProfileId];
hasCredential = true;
}
const envKey = resolveEnvApiKey("google");
if (envKey) {
const useExisting = await params.prompter.confirm({ const useExisting = await params.prompter.confirm({
message: `Use existing GEMINI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, message: `Use existing Gemini authentication (${"email" in existing && existing.email ? existing.email : existingProfileId})?`,
initialValue: true, initialValue: true,
}); });
if (useExisting) { if (useExisting) {
await setGeminiApiKey(envKey.apiKey, params.agentDir); profileId = existingProfileId;
hasCredential = true; hasCredential = true;
} }
} }
if (
!hasCredential &&
params.opts?.token &&
params.opts?.tokenProvider === "google"
) {
await setGeminiApiKey(
normalizeApiKeyInput(params.opts.token),
params.agentDir,
);
hasCredential = true;
}
if (!hasCredential) {
const envKey = resolveEnvApiKey("google");
if (envKey) {
const useExisting = await params.prompter.confirm({
message: `Use existing GEMINI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
initialValue: true,
});
if (useExisting) {
await setGeminiApiKey(envKey.apiKey, params.agentDir);
hasCredential = true;
}
}
}
if (!hasCredential) { if (!hasCredential) {
const key = await params.prompter.text({ const key = await params.prompter.text({
message: "Enter Gemini API key", message: "Enter Gemini API key",
validate: validateApiKeyInput, validate: validateApiKeyInput,
}); });
await setGeminiApiKey(normalizeApiKeyInput(String(key)), params.agentDir); await setGeminiApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
hasCredential = true;
} }
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "google:default", if (hasCredential) {
provider: "google", nextConfig = applyAuthProfileConfig(nextConfig, {
mode: "api_key", profileId,
}); provider: "google",
mode: "api_key",
});
}
if (params.setDefaultModel) { if (params.setDefaultModel) {
const applied = applyGoogleGeminiModelDefault(nextConfig); const applied = applyGoogleGeminiModelDefault(nextConfig);
nextConfig = applied.next; nextConfig = applied.next;
@ -369,36 +541,74 @@ export async function applyAuthChoiceApiProviders(
} }
if (authChoice === "zai-api-key") { if (authChoice === "zai-api-key") {
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const profileOrder = resolveAuthProfileOrder({
cfg: nextConfig,
store,
provider: "zai",
});
const existingProfileId = profileOrder.find((pid) =>
Boolean(store.profiles[pid]),
);
let profileId = "zai:default";
let hasCredential = false; let hasCredential = false;
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "zai") { if (existingProfileId) {
await setZaiApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); const existing = store.profiles[existingProfileId];
hasCredential = true;
}
const envKey = resolveEnvApiKey("zai");
if (envKey) {
const useExisting = await params.prompter.confirm({ const useExisting = await params.prompter.confirm({
message: `Use existing ZAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, message: `Use existing Z.AI authentication (${"email" in existing && existing.email ? existing.email : existingProfileId})?`,
initialValue: true, initialValue: true,
}); });
if (useExisting) { if (useExisting) {
await setZaiApiKey(envKey.apiKey, params.agentDir); profileId = existingProfileId;
hasCredential = true; hasCredential = true;
} }
} }
if (
!hasCredential &&
params.opts?.token &&
params.opts?.tokenProvider === "zai"
) {
await setZaiApiKey(
normalizeApiKeyInput(params.opts.token),
params.agentDir,
);
hasCredential = true;
}
if (!hasCredential) {
const envKey = resolveEnvApiKey("zai");
if (envKey) {
const useExisting = await params.prompter.confirm({
message: `Use existing ZAI_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
initialValue: true,
});
if (useExisting) {
await setZaiApiKey(envKey.apiKey, params.agentDir);
hasCredential = true;
}
}
}
if (!hasCredential) { if (!hasCredential) {
const key = await params.prompter.text({ const key = await params.prompter.text({
message: "Enter Z.AI API key", message: "Enter Z.AI API key",
validate: validateApiKeyInput, validate: validateApiKeyInput,
}); });
await setZaiApiKey(normalizeApiKeyInput(String(key)), params.agentDir); await setZaiApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
hasCredential = true;
}
if (hasCredential) {
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId,
provider: "zai",
mode: "api_key",
});
} }
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "zai:default",
provider: "zai",
mode: "api_key",
});
{ {
const applied = await applyDefaultModelChoice({ const applied = await applyDefaultModelChoice({
config: nextConfig, config: nextConfig,
@ -415,7 +625,9 @@ export async function applyAuthChoiceApiProviders(
...config.agents?.defaults?.models, ...config.agents?.defaults?.models,
[ZAI_DEFAULT_MODEL_REF]: { [ZAI_DEFAULT_MODEL_REF]: {
...config.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF], ...config.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF],
alias: config.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF]?.alias ?? "GLM", alias:
config.agents?.defaults?.models?.[ZAI_DEFAULT_MODEL_REF]
?.alias ?? "GLM",
}, },
}, },
}, },
@ -432,20 +644,60 @@ export async function applyAuthChoiceApiProviders(
} }
if (authChoice === "synthetic-api-key") { if (authChoice === "synthetic-api-key") {
if (params.opts?.token && params.opts?.tokenProvider === "synthetic") { const store = ensureAuthProfileStore(params.agentDir, {
await setSyntheticApiKey(String(params.opts.token).trim(), params.agentDir); allowKeychainPrompt: false,
} else { });
const profileOrder = resolveAuthProfileOrder({
cfg: nextConfig,
store,
provider: "synthetic",
});
const existingProfileId = profileOrder.find((pid) =>
Boolean(store.profiles[pid]),
);
let profileId = "synthetic:default";
let hasCredential = false;
if (existingProfileId) {
const existing = store.profiles[existingProfileId];
const useExisting = await params.prompter.confirm({
message: `Use existing Synthetic authentication (${"email" in existing && existing.email ? existing.email : existingProfileId})?`,
initialValue: true,
});
if (useExisting) {
profileId = existingProfileId;
hasCredential = true;
}
}
if (
!hasCredential &&
params.opts?.token &&
params.opts?.tokenProvider === "synthetic"
) {
await setSyntheticApiKey(
String(params.opts.token).trim(),
params.agentDir,
);
hasCredential = true;
}
if (!hasCredential) {
const key = await params.prompter.text({ const key = await params.prompter.text({
message: "Enter Synthetic API key", message: "Enter Synthetic API key",
validate: (value) => (value?.trim() ? undefined : "Required"), validate: (value) => (value?.trim() ? undefined : "Required"),
}); });
await setSyntheticApiKey(String(key).trim(), params.agentDir); await setSyntheticApiKey(String(key).trim(), params.agentDir);
hasCredential = true;
}
if (hasCredential) {
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId,
provider: "synthetic",
mode: "api_key",
});
} }
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "synthetic:default",
provider: "synthetic",
mode: "api_key",
});
{ {
const applied = await applyDefaultModelChoice({ const applied = await applyDefaultModelChoice({
config: nextConfig, config: nextConfig,
@ -464,10 +716,41 @@ export async function applyAuthChoiceApiProviders(
} }
if (authChoice === "venice-api-key") { if (authChoice === "venice-api-key") {
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const profileOrder = resolveAuthProfileOrder({
cfg: nextConfig,
store,
provider: "venice",
});
const existingProfileId = profileOrder.find((pid) =>
Boolean(store.profiles[pid]),
);
let profileId = "venice:default";
let hasCredential = false; let hasCredential = false;
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "venice") { if (existingProfileId) {
await setVeniceApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); const existing = store.profiles[existingProfileId];
const useExisting = await params.prompter.confirm({
message: `Use existing Venice AI authentication (${"email" in existing && existing.email ? existing.email : existingProfileId})?`,
initialValue: true,
});
if (useExisting) {
profileId = existingProfileId;
hasCredential = true;
}
}
if (
!hasCredential &&
params.opts?.token &&
params.opts?.tokenProvider === "venice"
) {
await setVeniceApiKey(
normalizeApiKeyInput(params.opts.token),
params.agentDir,
);
hasCredential = true; hasCredential = true;
} }
@ -480,31 +763,36 @@ export async function applyAuthChoiceApiProviders(
].join("\n"), ].join("\n"),
"Venice AI", "Venice AI",
); );
}
const envKey = resolveEnvApiKey("venice"); const envKey = resolveEnvApiKey("venice");
if (envKey) { if (envKey) {
const useExisting = await params.prompter.confirm({ const useExisting = await params.prompter.confirm({
message: `Use existing VENICE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, message: `Use existing VENICE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
initialValue: true, initialValue: true,
}); });
if (useExisting) { if (useExisting) {
await setVeniceApiKey(envKey.apiKey, params.agentDir); await setVeniceApiKey(envKey.apiKey, params.agentDir);
hasCredential = true; hasCredential = true;
}
} }
} }
if (!hasCredential) { if (!hasCredential) {
const key = await params.prompter.text({ const key = await params.prompter.text({
message: "Enter Venice AI API key", message: "Enter Venice AI API key",
validate: validateApiKeyInput, validate: validateApiKeyInput,
}); });
await setVeniceApiKey(normalizeApiKeyInput(String(key)), params.agentDir); await setVeniceApiKey(normalizeApiKeyInput(String(key)), params.agentDir);
hasCredential = true;
}
if (hasCredential) {
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId,
provider: "venice",
mode: "api_key",
});
} }
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "venice:default",
provider: "venice",
mode: "api_key",
});
{ {
const applied = await applyDefaultModelChoice({ const applied = await applyDefaultModelChoice({
config: nextConfig, config: nextConfig,
@ -523,9 +811,41 @@ export async function applyAuthChoiceApiProviders(
} }
if (authChoice === "opencode-zen") { if (authChoice === "opencode-zen") {
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const profileOrder = resolveAuthProfileOrder({
cfg: nextConfig,
store,
provider: "opencode",
});
const existingProfileId = profileOrder.find((pid) =>
Boolean(store.profiles[pid]),
);
let profileId = "opencode:default";
let hasCredential = false; let hasCredential = false;
if (!hasCredential && params.opts?.token && params.opts?.tokenProvider === "opencode") {
await setOpencodeZenApiKey(normalizeApiKeyInput(params.opts.token), params.agentDir); if (existingProfileId) {
const existing = store.profiles[existingProfileId];
const useExisting = await params.prompter.confirm({
message: `Use existing OpenCode Zen authentication (${"email" in existing && existing.email ? existing.email : existingProfileId})?`,
initialValue: true,
});
if (useExisting) {
profileId = existingProfileId;
hasCredential = true;
}
}
if (
!hasCredential &&
params.opts?.token &&
params.opts?.tokenProvider === "opencode"
) {
await setOpencodeZenApiKey(
normalizeApiKeyInput(params.opts.token),
params.agentDir,
);
hasCredential = true; hasCredential = true;
} }
@ -538,30 +858,39 @@ export async function applyAuthChoiceApiProviders(
].join("\n"), ].join("\n"),
"OpenCode Zen", "OpenCode Zen",
); );
}
const envKey = resolveEnvApiKey("opencode"); const envKey = resolveEnvApiKey("opencode");
if (envKey) { if (envKey) {
const useExisting = await params.prompter.confirm({ const useExisting = await params.prompter.confirm({
message: `Use existing OPENCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`, message: `Use existing OPENCODE_API_KEY (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
initialValue: true, initialValue: true,
}); });
if (useExisting) { if (useExisting) {
await setOpencodeZenApiKey(envKey.apiKey, params.agentDir); await setOpencodeZenApiKey(envKey.apiKey, params.agentDir);
hasCredential = true; hasCredential = true;
}
} }
} }
if (!hasCredential) { if (!hasCredential) {
const key = await params.prompter.text({ const key = await params.prompter.text({
message: "Enter OpenCode Zen API key", message: "Enter OpenCode Zen API key",
validate: validateApiKeyInput, validate: validateApiKeyInput,
}); });
await setOpencodeZenApiKey(normalizeApiKeyInput(String(key)), params.agentDir); await setOpencodeZenApiKey(
normalizeApiKeyInput(String(key)),
params.agentDir,
);
hasCredential = true;
}
if (hasCredential) {
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId,
provider: "opencode",
mode: "api_key",
});
} }
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "opencode:default",
provider: "opencode",
mode: "api_key",
});
{ {
const applied = await applyDefaultModelChoice({ const applied = await applyDefaultModelChoice({
config: nextConfig, config: nextConfig,

View File

@ -1,6 +1,13 @@
import { githubCopilotLoginCommand } from "../providers/github-copilot-auth.js"; import { githubCopilotLoginCommand } from "../providers/github-copilot-auth.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; declare const process: any;
import { applyAuthProfileConfig } from "./onboard-auth.js"; import type {
ApplyAuthChoiceParams,
ApplyAuthChoiceResult,
} from "./auth-choice.apply.js";
import {
applyAuthProfileConfig,
ensureAuthProfileStore,
} from "./onboard-auth.js";
export async function applyAuthChoiceGitHubCopilot( export async function applyAuthChoiceGitHubCopilot(
params: ApplyAuthChoiceParams, params: ApplyAuthChoiceParams,
@ -17,19 +24,50 @@ export async function applyAuthChoiceGitHubCopilot(
"GitHub Copilot", "GitHub Copilot",
); );
if (!process.stdin.isTTY) { const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const existing = store.profiles["github-copilot:github"];
if (existing && existing.type === "token") {
const useExisting = await params.prompter.confirm({
message: `Use existing GitHub Copilot authentication (${existing.email ?? "authorized"})?`,
initialValue: true,
});
if (useExisting) {
// already has profile, skip login
} else if (!process.stdin.isTTY) {
await params.prompter.note(
"GitHub Copilot login requires an interactive TTY.",
"GitHub Copilot",
);
return { config: nextConfig };
} else {
try {
await githubCopilotLoginCommand({ yes: true }, params.runtime);
} catch (err) {
await params.prompter.note(
`GitHub Copilot login failed: ${String(err)}`,
"GitHub Copilot",
);
return { config: nextConfig };
}
}
} else if (!process.stdin.isTTY) {
await params.prompter.note( await params.prompter.note(
"GitHub Copilot login requires an interactive TTY.", "GitHub Copilot login requires an interactive TTY.",
"GitHub Copilot", "GitHub Copilot",
); );
return { config: nextConfig }; return { config: nextConfig };
} } else {
try {
try { await githubCopilotLoginCommand({ yes: true }, params.runtime);
await githubCopilotLoginCommand({ yes: true }, params.runtime); } catch (err) {
} catch (err) { await params.prompter.note(
await params.prompter.note(`GitHub Copilot login failed: ${String(err)}`, "GitHub Copilot"); `GitHub Copilot login failed: ${String(err)}`,
return { config: nextConfig }; "GitHub Copilot",
);
return { config: nextConfig };
}
} }
nextConfig = applyAuthProfileConfig(nextConfig, { nextConfig = applyAuthProfileConfig(nextConfig, {
@ -55,7 +93,10 @@ export async function applyAuthChoiceGitHubCopilot(
}, },
}, },
}; };
await params.prompter.note(`Default model set to ${model}`, "Model configured"); await params.prompter.note(
`Default model set to ${model}`,
"Model configured",
);
} }
return { config: nextConfig }; return { config: nextConfig };

View File

@ -4,7 +4,10 @@ import {
normalizeApiKeyInput, normalizeApiKeyInput,
validateApiKeyInput, validateApiKeyInput,
} from "./auth-choice.api-key.js"; } from "./auth-choice.api-key.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import type {
ApplyAuthChoiceParams,
ApplyAuthChoiceResult,
} from "./auth-choice.apply.js";
import { applyDefaultModelChoice } from "./auth-choice.default-model.js"; import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
import { import {
applyAuthProfileConfig, applyAuthProfileConfig,
@ -12,6 +15,7 @@ import {
applyMinimaxApiProviderConfig, applyMinimaxApiProviderConfig,
applyMinimaxConfig, applyMinimaxConfig,
applyMinimaxProviderConfig, applyMinimaxProviderConfig,
ensureAuthProfileStore,
setMinimaxApiKey, setMinimaxApiKey,
} from "./onboard-auth.js"; } from "./onboard-auth.js";
@ -34,8 +38,26 @@ export async function applyAuthChoiceMiniMax(
params.authChoice === "minimax-api-lightning" params.authChoice === "minimax-api-lightning"
) { ) {
const modelId = const modelId =
params.authChoice === "minimax-api-lightning" ? "MiniMax-M2.1-lightning" : "MiniMax-M2.1"; params.authChoice === "minimax-api-lightning"
? "MiniMax-M2.1-lightning"
: "MiniMax-M2.1";
let hasCredential = false; let hasCredential = false;
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const existingProfileId = "minimax:default";
if (store.profiles[existingProfileId]) {
const existing = store.profiles[existingProfileId];
const useExisting = await params.prompter.confirm({
message: `Use existing MiniMax authentication (${"email" in existing && existing.email ? existing.email : existingProfileId})?`,
initialValue: true,
});
if (useExisting) {
hasCredential = true;
}
}
const envKey = resolveEnvApiKey("minimax"); const envKey = resolveEnvApiKey("minimax");
if (envKey) { if (envKey) {
const useExisting = await params.prompter.confirm({ const useExisting = await params.prompter.confirm({
@ -52,7 +74,10 @@ export async function applyAuthChoiceMiniMax(
message: "Enter MiniMax API key", message: "Enter MiniMax API key",
validate: validateApiKeyInput, validate: validateApiKeyInput,
}); });
await setMinimaxApiKey(normalizeApiKeyInput(String(key)), params.agentDir); await setMinimaxApiKey(
normalizeApiKeyInput(String(key)),
params.agentDir,
);
} }
nextConfig = applyAuthProfileConfig(nextConfig, { nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "minimax:default", profileId: "minimax:default",
@ -66,7 +91,8 @@ export async function applyAuthChoiceMiniMax(
setDefaultModel: params.setDefaultModel, setDefaultModel: params.setDefaultModel,
defaultModel: modelRef, defaultModel: modelRef,
applyDefaultConfig: (config) => applyMinimaxApiConfig(config, modelId), applyDefaultConfig: (config) => applyMinimaxApiConfig(config, modelId),
applyProviderConfig: (config) => applyMinimaxApiProviderConfig(config, modelId), applyProviderConfig: (config) =>
applyMinimaxApiProviderConfig(config, modelId),
noteAgentModel, noteAgentModel,
prompter: params.prompter, prompter: params.prompter,
}); });

View File

@ -1,8 +1,16 @@
import { isRemoteEnvironment } from "./oauth-env.js"; import { isRemoteEnvironment } from "./oauth-env.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; declare const process: any;
import type {
ApplyAuthChoiceParams,
ApplyAuthChoiceResult,
} from "./auth-choice.apply.js";
import { loginChutes } from "./chutes-oauth.js"; import { loginChutes } from "./chutes-oauth.js";
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js"; import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
import { applyAuthProfileConfig, writeOAuthCredentials } from "./onboard-auth.js"; import {
applyAuthProfileConfig,
ensureAuthProfileStore,
writeOAuthCredentials,
} from "./onboard-auth.js";
import { openUrl } from "./onboard-helpers.js"; import { openUrl } from "./onboard-helpers.js";
export async function applyAuthChoiceOAuth( export async function applyAuthChoiceOAuth(
@ -12,8 +20,10 @@ export async function applyAuthChoiceOAuth(
let nextConfig = params.config; let nextConfig = params.config;
const isRemote = isRemoteEnvironment(); const isRemote = isRemoteEnvironment();
const redirectUri = const redirectUri =
process.env.CHUTES_OAUTH_REDIRECT_URI?.trim() || "http://127.0.0.1:1456/oauth-callback"; process.env.CHUTES_OAUTH_REDIRECT_URI?.trim() ||
const scopes = process.env.CHUTES_OAUTH_SCOPES?.trim() || "openid profile chutes:invoke"; "http://127.0.0.1:1456/oauth-callback";
const scopes =
process.env.CHUTES_OAUTH_SCOPES?.trim() || "openid profile chutes:invoke";
const clientId = const clientId =
process.env.CHUTES_CLIENT_ID?.trim() || process.env.CHUTES_CLIENT_ID?.trim() ||
String( String(
@ -25,6 +35,29 @@ export async function applyAuthChoiceOAuth(
).trim(); ).trim();
const clientSecret = process.env.CHUTES_CLIENT_SECRET?.trim() || undefined; const clientSecret = process.env.CHUTES_CLIENT_SECRET?.trim() || undefined;
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const existingProfileId = Object.keys(store.profiles).find((id) =>
id.startsWith("chutes:"),
);
if (existingProfileId) {
const existing = store.profiles[existingProfileId];
const useExisting = await params.prompter.confirm({
message: `Use existing Chutes authentication (${"email" in existing && existing.email ? existing.email : existingProfileId})?`,
initialValue: true,
});
if (useExisting) {
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: existingProfileId,
provider: "chutes",
mode: "oauth",
});
return { config: nextConfig };
}
}
await params.prompter.note( await params.prompter.note(
isRemote isRemote
? [ ? [
@ -64,7 +97,7 @@ export async function applyAuthChoiceOAuth(
manual: isRemote, manual: isRemote,
onAuth, onAuth,
onPrompt, onPrompt,
onProgress: (msg) => spin.update(msg), onProgress: (msg: string) => spin.update(msg),
}); });
spin.stop("Chutes OAuth complete"); spin.stop("Chutes OAuth complete");

View File

@ -1,4 +1,6 @@
// @ts-ignore
import { loginOpenAICodex } from "@mariozechner/pi-ai"; import { loginOpenAICodex } from "@mariozechner/pi-ai";
declare const process: any;
import { resolveEnvApiKey } from "../agents/model-auth.js"; import { resolveEnvApiKey } from "../agents/model-auth.js";
import { upsertSharedEnvVar } from "../infra/env-file.js"; import { upsertSharedEnvVar } from "../infra/env-file.js";
import { isRemoteEnvironment } from "./oauth-env.js"; import { isRemoteEnvironment } from "./oauth-env.js";
@ -7,9 +9,16 @@ import {
normalizeApiKeyInput, normalizeApiKeyInput,
validateApiKeyInput, validateApiKeyInput,
} from "./auth-choice.api-key.js"; } from "./auth-choice.api-key.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import type {
ApplyAuthChoiceParams,
ApplyAuthChoiceResult,
} from "./auth-choice.apply.js";
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js"; import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
import { applyAuthProfileConfig, writeOAuthCredentials } from "./onboard-auth.js"; import {
applyAuthProfileConfig,
ensureAuthProfileStore,
writeOAuthCredentials,
} from "./onboard-auth.js";
import { openUrl } from "./onboard-helpers.js"; import { openUrl } from "./onboard-helpers.js";
import { import {
applyOpenAICodexModelDefault, applyOpenAICodexModelDefault,
@ -96,6 +105,29 @@ export async function applyAuthChoiceOpenAI(
].join("\n"), ].join("\n"),
"OpenAI Codex OAuth", "OpenAI Codex OAuth",
); );
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
});
const existing = store.profiles["openai-codex:default"];
if (existing && existing.type === "oauth") {
const useExisting = await params.prompter.confirm({
message: `Use existing OpenAI Codex authentication (${existing.email ?? "authorized"})?`,
initialValue: true,
});
if (useExisting) {
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: "openai-codex:default",
provider: "openai-codex",
mode: "oauth",
});
if (params.setDefaultModel) {
const applied = applyOpenAICodexModelDefault(nextConfig);
nextConfig = applied.next;
}
return { config: nextConfig };
}
}
const spin = params.prompter.progress("Starting OAuth flow…"); const spin = params.prompter.progress("Starting OAuth flow…");
try { try {
const { onAuth, onPrompt } = createVpsAwareOAuthHandlers({ const { onAuth, onPrompt } = createVpsAwareOAuthHandlers({
@ -110,7 +142,7 @@ export async function applyAuthChoiceOpenAI(
const creds = await loginOpenAICodex({ const creds = await loginOpenAICodex({
onAuth, onAuth,
onPrompt, onPrompt,
onProgress: (msg) => spin.update(msg), onProgress: (msg: string) => spin.update(msg),
}); });
spin.stop("OpenAI OAuth complete"); spin.stop("OpenAI OAuth complete");
if (creds) { if (creds) {

View File

@ -11,8 +11,14 @@ import type { MoltbotConfig } from "../config/config.js";
import { enablePluginInConfig } from "../plugins/enable.js"; import { enablePluginInConfig } from "../plugins/enable.js";
import { resolvePluginProviders } from "../plugins/providers.js"; import { resolvePluginProviders } from "../plugins/providers.js";
import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js"; import type { ProviderAuthMethod, ProviderPlugin } from "../plugins/types.js";
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js"; import type {
import { applyAuthProfileConfig } from "./onboard-auth.js"; ApplyAuthChoiceParams,
ApplyAuthChoiceResult,
} from "./auth-choice.apply.js";
import {
applyAuthProfileConfig,
ensureAuthProfileStore,
} from "./onboard-auth.js";
import { openUrl } from "./onboard-helpers.js"; import { openUrl } from "./onboard-helpers.js";
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js"; import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
import { isRemoteEnvironment } from "./oauth-env.js"; import { isRemoteEnvironment } from "./oauth-env.js";
@ -31,16 +37,23 @@ function resolveProviderMatch(
): ProviderPlugin | null { ): ProviderPlugin | null {
const normalized = normalizeProviderId(rawProvider); const normalized = normalizeProviderId(rawProvider);
return ( return (
providers.find((provider) => normalizeProviderId(provider.id) === normalized) ?? providers.find(
(provider) => normalizeProviderId(provider.id) === normalized,
) ??
providers.find( providers.find(
(provider) => (provider) =>
provider.aliases?.some((alias) => normalizeProviderId(alias) === normalized) ?? false, provider.aliases?.some(
(alias) => normalizeProviderId(alias) === normalized,
) ?? false,
) ?? ) ??
null null
); );
} }
function pickAuthMethod(provider: ProviderPlugin, rawMethod?: string): ProviderAuthMethod | null { function pickAuthMethod(
provider: ProviderPlugin,
rawMethod?: string,
): ProviderAuthMethod | null {
const raw = rawMethod?.trim(); const raw = rawMethod?.trim();
if (!raw) return null; if (!raw) return null;
const normalized = raw.toLowerCase(); const normalized = raw.toLowerCase();
@ -85,8 +98,13 @@ function applyDefaultModel(cfg: MoltbotConfig, model: string): MoltbotConfig {
...cfg.agents?.defaults, ...cfg.agents?.defaults,
models, models,
model: { model: {
...(existingModel && typeof existingModel === "object" && "fallbacks" in existingModel ...(existingModel &&
? { fallbacks: (existingModel as { fallbacks?: string[] }).fallbacks } typeof existingModel === "object" &&
"fallbacks" in existingModel
? {
fallbacks: (existingModel as { fallbacks?: string[] })
.fallbacks,
}
: undefined), : undefined),
primary: model, primary: model,
}, },
@ -100,6 +118,7 @@ export async function applyAuthChoicePluginProvider(
options: PluginProviderAuthChoiceOptions, options: PluginProviderAuthChoiceOptions,
): Promise<ApplyAuthChoiceResult | null> { ): Promise<ApplyAuthChoiceResult | null> {
if (params.authChoice !== options.authChoice) return null; if (params.authChoice !== options.authChoice) return null;
let agentModelOverride: string | undefined;
const enableResult = enablePluginInConfig(params.config, options.pluginId); const enableResult = enablePluginInConfig(params.config, options.pluginId);
let nextConfig = enableResult.config; let nextConfig = enableResult.config;
@ -115,11 +134,17 @@ export async function applyAuthChoicePluginProvider(
const defaultAgentId = resolveDefaultAgentId(nextConfig); const defaultAgentId = resolveDefaultAgentId(nextConfig);
const agentDir = const agentDir =
params.agentDir ?? params.agentDir ??
(agentId === defaultAgentId ? resolveMoltbotAgentDir() : resolveAgentDir(nextConfig, agentId)); (agentId === defaultAgentId
? resolveMoltbotAgentDir()
: resolveAgentDir(nextConfig, agentId));
const workspaceDir = const workspaceDir =
resolveAgentWorkspaceDir(nextConfig, agentId) ?? resolveDefaultAgentWorkspaceDir(); resolveAgentWorkspaceDir(nextConfig, agentId) ??
resolveDefaultAgentWorkspaceDir();
const providers = resolvePluginProviders({ config: nextConfig, workspaceDir }); const providers = resolvePluginProviders({
config: nextConfig,
workspaceDir,
});
const provider = resolveProviderMatch(providers, options.providerId); const provider = resolveProviderMatch(providers, options.providerId);
if (!provider) { if (!provider) {
await params.prompter.note( await params.prompter.note(
@ -131,10 +156,56 @@ export async function applyAuthChoicePluginProvider(
const method = pickAuthMethod(provider, options.methodId) ?? provider.auth[0]; const method = pickAuthMethod(provider, options.methodId) ?? provider.auth[0];
if (!method) { if (!method) {
await params.prompter.note(`${options.label} auth method missing.`, options.label); await params.prompter.note(
`${options.label} auth method missing.`,
options.label,
);
return { config: nextConfig }; return { config: nextConfig };
} }
const store = ensureAuthProfileStore(agentDir, {
allowKeychainPrompt: false,
});
const existingProfiles = Object.entries(store.profiles).filter(
([, p]) =>
normalizeProviderId((p as any).provider) ===
normalizeProviderId(options.providerId),
);
if (existingProfiles.length > 0) {
const defaultProfile =
existingProfiles.find(([id]) => id === `${options.providerId}:default`) ??
existingProfiles[0];
const [profileId, profile] = defaultProfile as [string, any];
const useExisting = await params.prompter.confirm({
message: `Use existing ${options.label} authentication (${"email" in profile && profile.email ? profile.email : profileId})?`,
initialValue: true,
});
if (useExisting) {
nextConfig = applyAuthProfileConfig(nextConfig, {
profileId,
provider: profile.provider,
mode: (profile.type === "token" ? "token" : profile.type) as any,
...("email" in profile && profile.email
? { email: profile.email as string }
: {}),
});
if (params.setDefaultModel && provider.models?.models?.[0]?.id) {
nextConfig = applyDefaultModel(
nextConfig,
provider.models.models[0].id,
);
} else if (params.agentId && provider.models?.models?.[0]?.id) {
agentModelOverride = provider.models.models[0].id;
}
return { config: nextConfig, agentModelOverride };
}
}
const isRemote = isRemoteEnvironment(); const isRemote = isRemoteEnvironment();
const result = await method.run({ const result = await method.run({
config: nextConfig, config: nextConfig,
@ -165,18 +236,21 @@ export async function applyAuthChoicePluginProvider(
nextConfig = applyAuthProfileConfig(nextConfig, { nextConfig = applyAuthProfileConfig(nextConfig, {
profileId: profile.profileId, profileId: profile.profileId,
provider: profile.credential.provider, provider: profile.credential.provider,
mode: profile.credential.type === "token" ? "token" : profile.credential.type, mode:
profile.credential.type === "token" ? "token" : profile.credential.type,
...("email" in profile.credential && profile.credential.email ...("email" in profile.credential && profile.credential.email
? { email: profile.credential.email } ? { email: profile.credential.email }
: {}), : {}),
}); });
} }
let agentModelOverride: string | undefined;
if (result.defaultModel) { if (result.defaultModel) {
if (params.setDefaultModel) { if (params.setDefaultModel) {
nextConfig = applyDefaultModel(nextConfig, result.defaultModel); nextConfig = applyDefaultModel(nextConfig, result.defaultModel);
await params.prompter.note(`Default model set to ${result.defaultModel}`, "Model configured"); await params.prompter.note(
`Default model set to ${result.defaultModel}`,
"Model configured",
);
} else if (params.agentId) { } else if (params.agentId) {
agentModelOverride = result.defaultModel; agentModelOverride = result.defaultModel;
await params.prompter.note( await params.prompter.note(

View File

@ -1,6 +1,7 @@
import { randomBytes } from "node:crypto"; import { randomBytes } from "node:crypto";
import { createServer } from "node:http"; import { createServer } from "node:http";
// @ts-ignore
import type { OAuthCredentials } from "@mariozechner/pi-ai"; import type { OAuthCredentials } from "@mariozechner/pi-ai";
import type { ChutesOAuthAppConfig } from "../agents/chutes-oauth.js"; import type { ChutesOAuthAppConfig } from "../agents/chutes-oauth.js";
@ -43,76 +44,83 @@ async function waitForLocalCallback(params: {
}): Promise<{ code: string; state: string }> { }): Promise<{ code: string; state: string }> {
const redirectUrl = new URL(params.redirectUri); const redirectUrl = new URL(params.redirectUri);
if (redirectUrl.protocol !== "http:") { if (redirectUrl.protocol !== "http:") {
throw new Error(`Chutes OAuth redirect URI must be http:// (got ${params.redirectUri})`); throw new Error(
`Chutes OAuth redirect URI must be http:// (got ${params.redirectUri})`,
);
} }
const hostname = redirectUrl.hostname || "127.0.0.1"; const hostname = redirectUrl.hostname || "127.0.0.1";
const port = redirectUrl.port ? Number.parseInt(redirectUrl.port, 10) : 80; const port = redirectUrl.port ? Number.parseInt(redirectUrl.port, 10) : 80;
const expectedPath = redirectUrl.pathname || "/"; const expectedPath = redirectUrl.pathname || "/";
return await new Promise<{ code: string; state: string }>((resolve, reject) => { return await new Promise<{ code: string; state: string }>(
let timeout: NodeJS.Timeout | null = null; (resolve, reject) => {
const server = createServer((req, res) => { // @ts-ignore
try { let timeout: NodeJS.Timeout | null = null;
const requestUrl = new URL(req.url ?? "/", redirectUrl.origin); const server = createServer((req: any, res: any) => {
if (requestUrl.pathname !== expectedPath) { try {
res.statusCode = 404; const requestUrl = new URL(req.url ?? "/", redirectUrl.origin);
res.setHeader("Content-Type", "text/plain; charset=utf-8"); if (requestUrl.pathname !== expectedPath) {
res.end("Not found"); res.statusCode = 404;
return; res.setHeader("Content-Type", "text/plain; charset=utf-8");
} res.end("Not found");
return;
}
const code = requestUrl.searchParams.get("code")?.trim(); const code = requestUrl.searchParams.get("code")?.trim();
const state = requestUrl.searchParams.get("state")?.trim(); const state = requestUrl.searchParams.get("state")?.trim();
if (!code) { if (!code) {
res.statusCode = 400; res.statusCode = 400;
res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Missing code"); res.end("Missing code");
return; return;
} }
if (!state || state !== params.expectedState) { if (!state || state !== params.expectedState) {
res.statusCode = 400; res.statusCode = 400;
res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.end("Invalid state"); res.end("Invalid state");
return; return;
} }
res.statusCode = 200; res.statusCode = 200;
res.setHeader("Content-Type", "text/html; charset=utf-8"); res.setHeader("Content-Type", "text/html; charset=utf-8");
res.end( res.end(
[ [
"<!doctype html>", "<!doctype html>",
"<html><head><meta charset='utf-8' /></head>", "<html><head><meta charset='utf-8' /></head>",
"<body><h2>Chutes OAuth complete</h2>", "<body><h2>Chutes OAuth complete</h2>",
"<p>You can close this window and return to moltbot.</p></body></html>", "<p>You can close this window and return to moltbot.</p></body></html>",
].join(""), ].join(""),
); );
if (timeout) clearTimeout(timeout); if (timeout) clearTimeout(timeout);
server.close(); server.close();
resolve({ code, state }); resolve({ code, state });
} catch (err) { } catch (err) {
if (timeout) clearTimeout(timeout);
server.close();
reject(err);
}
});
server.once("error", (err: any) => {
if (timeout) clearTimeout(timeout); if (timeout) clearTimeout(timeout);
server.close(); server.close();
reject(err); reject(err);
} });
}); server.listen(port, hostname, () => {
params.onProgress?.(
`Waiting for OAuth callback on ${redirectUrl.origin}${expectedPath}`,
);
});
server.once("error", (err) => { timeout = setTimeout(() => {
if (timeout) clearTimeout(timeout); try {
server.close(); server.close();
reject(err); } catch {}
}); reject(new Error("OAuth callback timeout"));
server.listen(port, hostname, () => { }, params.timeoutMs);
params.onProgress?.(`Waiting for OAuth callback on ${redirectUrl.origin}${expectedPath}`); },
}); );
timeout = setTimeout(() => {
try {
server.close();
} catch {}
reject(new Error("OAuth callback timeout"));
}, params.timeoutMs);
});
} }
export async function loginChutes(params: { export async function loginChutes(params: {
@ -127,7 +135,8 @@ export async function loginChutes(params: {
fetchFn?: typeof fetch; fetchFn?: typeof fetch;
}): Promise<OAuthCredentials> { }): Promise<OAuthCredentials> {
const createPkce = params.createPkce ?? generateChutesPkce; const createPkce = params.createPkce ?? generateChutesPkce;
const createState = params.createState ?? (() => randomBytes(16).toString("hex")); const createState =
params.createState ?? (() => randomBytes(16).toString("hex"));
const { verifier, challenge } = createPkce(); const { verifier, challenge } = createPkce();
const state = createState(); const state = createState();

View File

@ -1,8 +1,12 @@
export { ensureAuthProfileStore } from "../agents/auth-profiles.js";
export { export {
SYNTHETIC_DEFAULT_MODEL_ID, SYNTHETIC_DEFAULT_MODEL_ID,
SYNTHETIC_DEFAULT_MODEL_REF, SYNTHETIC_DEFAULT_MODEL_REF,
} from "../agents/synthetic-models.js"; } from "../agents/synthetic-models.js";
export { VENICE_DEFAULT_MODEL_ID, VENICE_DEFAULT_MODEL_REF } from "../agents/venice-models.js"; export {
VENICE_DEFAULT_MODEL_ID,
VENICE_DEFAULT_MODEL_REF,
} from "../agents/venice-models.js";
export { export {
applyAuthProfileConfig, applyAuthProfileConfig,
applyKimiCodeConfig, applyKimiCodeConfig,

View File

@ -1,6 +1,10 @@
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js"; declare const process: any;
import {
resolveAgentWorkspaceDir,
resolveDefaultAgentId,
} from "../../agents/agent-scope.js";
import type { ChannelPluginCatalogEntry } from "../../channels/plugins/catalog.js"; import type { ChannelPluginCatalogEntry } from "../../channels/plugins/catalog.js";
import type { MoltbotConfig } from "../../config/config.js"; import type { MoltbotConfig } from "../../config/config.js";
import { createSubsystemLogger } from "../../logging/subsystem.js"; import { createSubsystemLogger } from "../../logging/subsystem.js";
@ -49,7 +53,10 @@ function resolveLocalPath(
return null; return null;
} }
function addPluginLoadPath(cfg: MoltbotConfig, pluginPath: string): MoltbotConfig { function addPluginLoadPath(
cfg: MoltbotConfig,
pluginPath: string,
): MoltbotConfig {
const existing = cfg.plugins?.load?.paths ?? []; const existing = cfg.plugins?.load?.paths ?? [];
const merged = Array.from(new Set([...existing, pluginPath])); const merged = Array.from(new Set([...existing, pluginPath]));
return { return {
@ -71,7 +78,11 @@ async function promptInstallChoice(params: {
prompter: WizardPrompter; prompter: WizardPrompter;
}): Promise<InstallChoice> { }): Promise<InstallChoice> {
const { entry, localPath, prompter, defaultChoice } = params; const { entry, localPath, prompter, defaultChoice } = params;
const localOptions: Array<{ value: InstallChoice; label: string; hint?: string }> = localPath const localOptions: Array<{
value: InstallChoice;
label: string;
hint?: string;
}> = localPath
? [ ? [
{ {
value: "local", value: "local",
@ -80,11 +91,12 @@ async function promptInstallChoice(params: {
}, },
] ]
: []; : [];
const options: Array<{ value: InstallChoice; label: string; hint?: string }> = [ const options: Array<{ value: InstallChoice; label: string; hint?: string }> =
{ value: "npm", label: `Download from npm (${entry.install.npmSpec})` }, [
...localOptions, { value: "npm", label: `Download from npm (${entry.install.npmSpec})` },
{ value: "skip", label: "Skip for now" }, ...localOptions,
]; { value: "skip", label: "Skip for now" },
];
const initialValue: InstallChoice = const initialValue: InstallChoice =
defaultChoice === "local" && !localPath ? "npm" : defaultChoice; defaultChoice === "local" && !localPath ? "npm" : defaultChoice;
return await prompter.select<InstallChoice>({ return await prompter.select<InstallChoice>({
@ -149,8 +161,8 @@ export async function ensureOnboardingPluginInstalled(params: {
const result = await installPluginFromNpmSpec({ const result = await installPluginFromNpmSpec({
spec: entry.install.npmSpec, spec: entry.install.npmSpec,
logger: { logger: {
info: (msg) => runtime.log?.(msg), info: (msg: string) => runtime.log?.(msg),
warn: (msg) => runtime.log?.(msg), warn: (msg: string) => runtime.log?.(msg),
}, },
}); });
@ -193,17 +205,18 @@ export function reloadOnboardingPluginRegistry(params: {
workspaceDir?: string; workspaceDir?: string;
}): void { }): void {
const workspaceDir = const workspaceDir =
params.workspaceDir ?? resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg)); params.workspaceDir ??
resolveAgentWorkspaceDir(params.cfg, resolveDefaultAgentId(params.cfg));
const log = createSubsystemLogger("plugins"); const log = createSubsystemLogger("plugins");
loadMoltbotPlugins({ loadMoltbotPlugins({
config: params.cfg, config: params.cfg,
workspaceDir, workspaceDir,
cache: false, cache: false,
logger: { logger: {
info: (msg) => log.info(msg), info: (msg: string) => log.info(msg),
warn: (msg) => log.warn(msg), warn: (msg: string) => log.warn(msg),
error: (msg) => log.error(msg), error: (msg: string) => log.error(msg),
debug: (msg) => log.debug(msg), debug: (msg: string) => log.debug(msg),
}, },
}); });
} }

View File

@ -9,12 +9,27 @@ import { loadExecApprovals } from "./controllers/exec-approvals";
import { loadPresence } from "./controllers/presence"; import { loadPresence } from "./controllers/presence";
import { loadSessions } from "./controllers/sessions"; import { loadSessions } from "./controllers/sessions";
import { loadSkills } from "./controllers/skills"; import { loadSkills } from "./controllers/skills";
import { inferBasePathFromPathname, normalizeBasePath, normalizePath, pathForTab, tabFromPath, type Tab } from "./navigation"; import {
inferBasePathFromPathname,
normalizeBasePath,
normalizePath,
pathForTab,
tabFromPath,
type Tab,
} from "./navigation";
import { saveSettings, type UiSettings } from "./storage"; import { saveSettings, type UiSettings } from "./storage";
import { resolveTheme, type ResolvedTheme, type ThemeMode } from "./theme"; import { resolveTheme, type ResolvedTheme, type ThemeMode } from "./theme";
import { startThemeTransition, type ThemeTransitionContext } from "./theme-transition"; import {
startThemeTransition,
type ThemeTransitionContext,
} from "./theme-transition";
import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll"; import { scheduleChatScroll, scheduleLogsScroll } from "./app-scroll";
import { startLogsPolling, stopLogsPolling, startDebugPolling, stopDebugPolling } from "./app-polling"; import {
startLogsPolling,
stopLogsPolling,
startDebugPolling,
stopDebugPolling,
} from "./app-polling";
import { refreshChat } from "./app-chat"; import { refreshChat } from "./app-chat";
import type { MoltbotApp } from "./app"; import type { MoltbotApp } from "./app";
@ -31,6 +46,7 @@ type SettingsHost = {
eventLog: unknown[]; eventLog: unknown[];
eventLogBuffer: unknown[]; eventLogBuffer: unknown[];
basePath: string; basePath: string;
password: string;
themeMedia: MediaQueryList | null; themeMedia: MediaQueryList | null;
themeMediaHandler: ((event: MediaQueryListEvent) => void) | null; themeMediaHandler: ((event: MediaQueryListEvent) => void) | null;
}; };
@ -38,7 +54,8 @@ type SettingsHost = {
export function applySettings(host: SettingsHost, next: UiSettings) { export function applySettings(host: SettingsHost, next: UiSettings) {
const normalized = { const normalized = {
...next, ...next,
lastActiveSessionKey: next.lastActiveSessionKey?.trim() || next.sessionKey.trim() || "main", lastActiveSessionKey:
next.lastActiveSessionKey?.trim() || next.sessionKey.trim() || "main",
}; };
host.settings = normalized; host.settings = normalized;
saveSettings(normalized); saveSettings(normalized);
@ -78,6 +95,10 @@ export function applySettingsFromUrl(host: SettingsHost) {
const password = passwordRaw.trim(); const password = passwordRaw.trim();
if (password) { if (password) {
(host as { password: string }).password = password; (host as { password: string }).password = password;
applySettings(host as unknown as Parameters<typeof applySettings>[0], {
...host.settings,
password,
});
} }
params.delete("password"); params.delete("password");
shouldCleanUrl = true; shouldCleanUrl = true;
@ -115,10 +136,14 @@ export function setTab(host: SettingsHost, next: Tab) {
if (next === "chat") host.chatHasAutoScrolled = false; if (next === "chat") host.chatHasAutoScrolled = false;
if (next === "logs") if (next === "logs")
startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]); startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
else stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]); else
stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
if (next === "debug") if (next === "debug")
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]); startDebugPolling(
else stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]); host as unknown as Parameters<typeof startDebugPolling>[0],
);
else
stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);
void refreshActiveTab(host); void refreshActiveTab(host);
syncUrlWithTab(host, next, false); syncUrlWithTab(host, next, false);
} }
@ -144,8 +169,10 @@ export function setTheme(
export async function refreshActiveTab(host: SettingsHost) { export async function refreshActiveTab(host: SettingsHost) {
if (host.tab === "overview") await loadOverview(host); if (host.tab === "overview") await loadOverview(host);
if (host.tab === "channels") await loadChannelsTab(host); if (host.tab === "channels") await loadChannelsTab(host);
if (host.tab === "instances") await loadPresence(host as unknown as MoltbotApp); if (host.tab === "instances")
if (host.tab === "sessions") await loadSessions(host as unknown as MoltbotApp); await loadPresence(host as unknown as MoltbotApp);
if (host.tab === "sessions")
await loadSessions(host as unknown as MoltbotApp);
if (host.tab === "cron") await loadCron(host); if (host.tab === "cron") await loadCron(host);
if (host.tab === "skills") await loadSkills(host as unknown as MoltbotApp); if (host.tab === "skills") await loadSkills(host as unknown as MoltbotApp);
if (host.tab === "nodes") { if (host.tab === "nodes") {
@ -193,7 +220,10 @@ export function syncThemeWithSettings(host: SettingsHost) {
applyResolvedTheme(host, resolveTheme(host.theme)); applyResolvedTheme(host, resolveTheme(host.theme));
} }
export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme) { export function applyResolvedTheme(
host: SettingsHost,
resolved: ResolvedTheme,
) {
host.themeResolved = resolved; host.themeResolved = resolved;
if (typeof document === "undefined") return; if (typeof document === "undefined") return;
const root = document.documentElement; const root = document.documentElement;
@ -202,7 +232,8 @@ export function applyResolvedTheme(host: SettingsHost, resolved: ResolvedTheme)
} }
export function attachThemeListener(host: SettingsHost) { export function attachThemeListener(host: SettingsHost) {
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return; if (typeof window === "undefined" || typeof window.matchMedia !== "function")
return;
host.themeMedia = window.matchMedia("(prefers-color-scheme: dark)"); host.themeMedia = window.matchMedia("(prefers-color-scheme: dark)");
host.themeMediaHandler = (event) => { host.themeMediaHandler = (event) => {
if (host.theme !== "system") return; if (host.theme !== "system") return;
@ -234,7 +265,8 @@ export function detachThemeListener(host: SettingsHost) {
export function syncTabWithLocation(host: SettingsHost, replace: boolean) { export function syncTabWithLocation(host: SettingsHost, replace: boolean) {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
const resolved = tabFromPath(window.location.pathname, host.basePath) ?? "chat"; const resolved =
tabFromPath(window.location.pathname, host.basePath) ?? "chat";
setTabFromRoute(host, resolved); setTabFromRoute(host, resolved);
syncUrlWithTab(host, resolved, replace); syncUrlWithTab(host, resolved, replace);
} }
@ -263,10 +295,14 @@ export function setTabFromRoute(host: SettingsHost, next: Tab) {
if (next === "chat") host.chatHasAutoScrolled = false; if (next === "chat") host.chatHasAutoScrolled = false;
if (next === "logs") if (next === "logs")
startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]); startLogsPolling(host as unknown as Parameters<typeof startLogsPolling>[0]);
else stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]); else
stopLogsPolling(host as unknown as Parameters<typeof stopLogsPolling>[0]);
if (next === "debug") if (next === "debug")
startDebugPolling(host as unknown as Parameters<typeof startDebugPolling>[0]); startDebugPolling(
else stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]); host as unknown as Parameters<typeof startDebugPolling>[0],
);
else
stopDebugPolling(host as unknown as Parameters<typeof stopDebugPolling>[0]);
if (host.connected) void refreshActiveTab(host); if (host.connected) void refreshActiveTab(host);
} }

View File

@ -24,7 +24,11 @@ import type {
StatusSummary, StatusSummary,
NostrProfile, NostrProfile,
} from "./types"; } from "./types";
import { type ChatAttachment, type ChatQueueItem, type CronFormState } from "./ui-types"; import {
type ChatAttachment,
type ChatQueueItem,
type CronFormState,
} from "./ui-types";
import type { EventLogEntry } from "./app-events"; import type { EventLogEntry } from "./app-events";
import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults"; import { DEFAULT_CRON_FORM, DEFAULT_LOG_LEVEL_FILTERS } from "./app-defaults";
import type { import type {
@ -93,13 +97,18 @@ function resolveOnboardingMode(): boolean {
const raw = params.get("onboarding"); const raw = params.get("onboarding");
if (!raw) return false; if (!raw) return false;
const normalized = raw.trim().toLowerCase(); const normalized = raw.trim().toLowerCase();
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on"; return (
normalized === "1" ||
normalized === "true" ||
normalized === "yes" ||
normalized === "on"
);
} }
@customElement("moltbot-app") @customElement("moltbot-app")
export class MoltbotApp extends LitElement { export class MoltbotApp extends LitElement {
@state() settings: UiSettings = loadSettings(); @state() settings: UiSettings = loadSettings();
@state() password = ""; @state() password = this.settings.password ?? "";
@state() tab: Tab = "chat"; @state() tab: Tab = "chat";
@state() onboarding = resolveOnboardingMode(); @state() onboarding = resolveOnboardingMode();
@state() connected = false; @state() connected = false;
@ -125,7 +134,9 @@ export class MoltbotApp extends LitElement {
@state() chatStream: string | null = null; @state() chatStream: string | null = null;
@state() chatStreamStartedAt: number | null = null; @state() chatStreamStartedAt: number | null = null;
@state() chatRunId: string | null = null; @state() chatRunId: string | null = null;
@state() compactionStatus: import("./app-tool-stream").CompactionStatus | null = null; @state() compactionStatus:
| import("./app-tool-stream").CompactionStatus
| null = null;
@state() chatAvatarUrl: string | null = null; @state() chatAvatarUrl: string | null = null;
@state() chatThinkingLevel: string | null = null; @state() chatThinkingLevel: string | null = null;
@state() chatQueue: ChatQueueItem[] = []; @state() chatQueue: ChatQueueItem[] = [];
@ -263,7 +274,8 @@ export class MoltbotApp extends LitElement {
this as unknown as Parameters<typeof onPopStateInternal>[0], this as unknown as Parameters<typeof onPopStateInternal>[0],
); );
private themeMedia: MediaQueryList | null = null; private themeMedia: MediaQueryList | null = null;
private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null = null; private themeMediaHandler: ((event: MediaQueryListEvent) => void) | null =
null;
private topbarObserver: ResizeObserver | null = null; private topbarObserver: ResizeObserver | null = null;
createRenderRoot() { createRenderRoot() {
@ -276,11 +288,15 @@ export class MoltbotApp extends LitElement {
} }
protected firstUpdated() { protected firstUpdated() {
handleFirstUpdated(this as unknown as Parameters<typeof handleFirstUpdated>[0]); handleFirstUpdated(
this as unknown as Parameters<typeof handleFirstUpdated>[0],
);
} }
disconnectedCallback() { disconnectedCallback() {
handleDisconnected(this as unknown as Parameters<typeof handleDisconnected>[0]); handleDisconnected(
this as unknown as Parameters<typeof handleDisconnected>[0],
);
super.disconnectedCallback(); super.disconnectedCallback();
} }
@ -339,7 +355,10 @@ export class MoltbotApp extends LitElement {
} }
setTab(next: Tab) { setTab(next: Tab) {
setTabInternal(this as unknown as Parameters<typeof setTabInternal>[0], next); setTabInternal(
this as unknown as Parameters<typeof setTabInternal>[0],
next,
);
} }
setTheme(next: ThemeMode, context?: Parameters<typeof setThemeInternal>[2]) { setTheme(next: ThemeMode, context?: Parameters<typeof setThemeInternal>[2]) {
@ -430,7 +449,9 @@ export class MoltbotApp extends LitElement {
handleNostrProfileToggleAdvancedInternal(this); handleNostrProfileToggleAdvancedInternal(this);
} }
async handleExecApprovalDecision(decision: "allow-once" | "allow-always" | "deny") { async handleExecApprovalDecision(
decision: "allow-once" | "allow-always" | "deny",
) {
const active = this.execApprovalQueue[0]; const active = this.execApprovalQueue[0];
if (!active || !this.client || this.execApprovalBusy) return; if (!active || !this.client || this.execApprovalBusy) return;
this.execApprovalBusy = true; this.execApprovalBusy = true;
@ -440,7 +461,9 @@ export class MoltbotApp extends LitElement {
id: active.id, id: active.id,
decision, decision,
}); });
this.execApprovalQueue = this.execApprovalQueue.filter((entry) => entry.id !== active.id); this.execApprovalQueue = this.execApprovalQueue.filter(
(entry) => entry.id !== active.id,
);
} catch (err) { } catch (err) {
this.execApprovalError = `Exec approval failed: ${String(err)}`; this.execApprovalError = `Exec approval failed: ${String(err)}`;
} finally { } finally {

View File

@ -5,6 +5,7 @@ import type { ThemeMode } from "./theme";
export type UiSettings = { export type UiSettings = {
gatewayUrl: string; gatewayUrl: string;
token: string; token: string;
password?: string;
sessionKey: string; sessionKey: string;
lastActiveSessionKey: string; lastActiveSessionKey: string;
theme: ThemeMode; theme: ThemeMode;
@ -24,6 +25,7 @@ export function loadSettings(): UiSettings {
const defaults: UiSettings = { const defaults: UiSettings = {
gatewayUrl: defaultUrl, gatewayUrl: defaultUrl,
token: "", token: "",
password: "",
sessionKey: "main", sessionKey: "main",
lastActiveSessionKey: "main", lastActiveSessionKey: "main",
theme: "system", theme: "system",
@ -44,6 +46,10 @@ export function loadSettings(): UiSettings {
? parsed.gatewayUrl.trim() ? parsed.gatewayUrl.trim()
: defaults.gatewayUrl, : defaults.gatewayUrl,
token: typeof parsed.token === "string" ? parsed.token : defaults.token, token: typeof parsed.token === "string" ? parsed.token : defaults.token,
password:
typeof parsed.password === "string"
? parsed.password
: defaults.password,
sessionKey: sessionKey:
typeof parsed.sessionKey === "string" && parsed.sessionKey.trim() typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()
? parsed.sessionKey.trim() ? parsed.sessionKey.trim()