add Claude SDK gateway installer setup
This commit is contained in:
+157
-12
@@ -7,6 +7,7 @@ import { homedir } from 'os';
|
||||
import { dirname, join } from 'path';
|
||||
import { SettingsDefaultsManager, type SettingsDefaults } from '../../shared/SettingsDefaultsManager.js';
|
||||
import { USER_SETTINGS_PATH } from '../../shared/paths.js';
|
||||
import { loadClaudeMemEnv, saveClaudeMemEnv } from '../../shared/EnvManager.js';
|
||||
import { ensureWorkerStarted, type WorkerStartResult } from '../../services/worker-spawner.js';
|
||||
import {
|
||||
ensureBun,
|
||||
@@ -584,13 +585,103 @@ function mergeSettings(updates: Record<string, string>): boolean {
|
||||
}
|
||||
|
||||
type ProviderId = 'claude' | 'gemini' | 'openrouter';
|
||||
type ClaudeAccessMode = 'subscription' | 'api-key';
|
||||
type ClaudeApiMode = 'direct' | 'gateway';
|
||||
|
||||
async function promptProvider(options: InstallOptions): Promise<ProviderId> {
|
||||
const initialProvider = (getSetting('CLAUDE_MEM_PROVIDER') as ProviderId) || 'claude';
|
||||
|
||||
const persistClaudeProvider = () => {
|
||||
const wrote = mergeSettings({ CLAUDE_MEM_PROVIDER: 'claude' });
|
||||
if (wrote) log.info('Saved provider=claude to ~/.claude-mem/settings.json');
|
||||
const persistClaudeProvider = (authMethod: 'subscription' | 'api-key' | 'gateway' = 'subscription') => {
|
||||
const wrote = mergeSettings({
|
||||
CLAUDE_MEM_PROVIDER: 'claude',
|
||||
CLAUDE_MEM_CLAUDE_AUTH_METHOD: authMethod,
|
||||
});
|
||||
if (wrote) log.info('Saved Claude Agent SDK configuration to ~/.claude-mem/settings.json');
|
||||
};
|
||||
|
||||
const useSubscriptionAuth = () => {
|
||||
persistClaudeProvider('subscription');
|
||||
saveClaudeMemEnv({
|
||||
ANTHROPIC_API_KEY: '',
|
||||
ANTHROPIC_BASE_URL: '',
|
||||
ANTHROPIC_AUTH_TOKEN: '',
|
||||
});
|
||||
log.info('Configured claude-mem to use your logged-in Claude SDK account.');
|
||||
};
|
||||
|
||||
const configureDirectApiKey = async (): Promise<void> => {
|
||||
const existing = loadClaudeMemEnv().ANTHROPIC_API_KEY || '';
|
||||
if (existing.trim().length > 0) {
|
||||
saveClaudeMemEnv({
|
||||
ANTHROPIC_API_KEY: existing.trim(),
|
||||
ANTHROPIC_BASE_URL: '',
|
||||
ANTHROPIC_AUTH_TOKEN: '',
|
||||
});
|
||||
persistClaudeProvider('api-key');
|
||||
return;
|
||||
}
|
||||
|
||||
const apiKeyResult = await p.password({
|
||||
message: 'Paste your Anthropic API key:',
|
||||
mask: '*',
|
||||
validate: (v?: string) => (!v || v.trim().length === 0) ? 'API key required' : undefined,
|
||||
});
|
||||
|
||||
if (p.isCancel(apiKeyResult)) {
|
||||
log.warn('API key prompt cancelled — using your logged-in Claude SDK account.');
|
||||
useSubscriptionAuth();
|
||||
return;
|
||||
}
|
||||
|
||||
saveClaudeMemEnv({
|
||||
ANTHROPIC_API_KEY: String(apiKeyResult).trim(),
|
||||
ANTHROPIC_BASE_URL: '',
|
||||
ANTHROPIC_AUTH_TOKEN: '',
|
||||
});
|
||||
persistClaudeProvider('api-key');
|
||||
log.info('Saved Anthropic API key for the Claude Agent SDK path.');
|
||||
};
|
||||
|
||||
const configureGateway = async (): Promise<void> => {
|
||||
const existing = loadClaudeMemEnv();
|
||||
const baseUrlResult = await p.text({
|
||||
message: 'Gateway URL:',
|
||||
placeholder: existing.ANTHROPIC_BASE_URL || 'http://localhost:4000',
|
||||
defaultValue: existing.ANTHROPIC_BASE_URL || '',
|
||||
validate: (v?: string) => {
|
||||
const value = v?.trim() ?? '';
|
||||
if (!value) return 'Gateway URL required';
|
||||
try {
|
||||
new URL(value);
|
||||
return undefined;
|
||||
} catch {
|
||||
return 'Enter a valid URL, for example http://localhost:4000';
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
if (p.isCancel(baseUrlResult)) {
|
||||
log.warn('Gateway setup cancelled — using your logged-in Claude SDK account.');
|
||||
useSubscriptionAuth();
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenResult = await p.password({
|
||||
message: 'Gateway key/token (leave blank if your local gateway does not require one):',
|
||||
mask: '*',
|
||||
});
|
||||
|
||||
if (p.isCancel(tokenResult)) {
|
||||
log.warn('Gateway key prompt cancelled — continuing without a gateway token.');
|
||||
}
|
||||
|
||||
saveClaudeMemEnv({
|
||||
ANTHROPIC_API_KEY: '',
|
||||
ANTHROPIC_BASE_URL: String(baseUrlResult).trim(),
|
||||
ANTHROPIC_AUTH_TOKEN: p.isCancel(tokenResult) ? '' : String(tokenResult).trim(),
|
||||
});
|
||||
persistClaudeProvider('gateway');
|
||||
log.info('Configured Claude Agent SDK gateway in ~/.claude-mem/.env.');
|
||||
};
|
||||
|
||||
if (!isInteractive) {
|
||||
@@ -611,21 +702,44 @@ async function promptProvider(options: InstallOptions): Promise<ProviderId> {
|
||||
if (options.provider) {
|
||||
selectedProvider = options.provider;
|
||||
} else {
|
||||
const result = await p.select<ProviderId>({
|
||||
message: 'Which LLM provider should claude-mem use to compress observations?',
|
||||
const result = await p.select<ClaudeAccessMode>({
|
||||
message: 'Do you use a subscription plan or an API key/gateway for the memory agent?',
|
||||
options: [
|
||||
{ value: 'claude', label: 'Claude Code auth (default — no extra setup, uses your existing Claude Code subscription)' },
|
||||
{ value: 'gemini', label: 'Gemini API key (free tier available — fast and cheap)' },
|
||||
{ value: 'openrouter', label: 'OpenRouter API key (BYO model — wide selection of frontier and open models)' },
|
||||
{ value: 'subscription', label: 'Subscription plan (recommended — uses your logged-in Claude SDK account)' },
|
||||
{ value: 'api-key', label: 'API key or gateway (Anthropic, LiteLLM, or compatible proxy)' },
|
||||
],
|
||||
initialValue: initialProvider,
|
||||
initialValue: initialProvider === 'claude' ? 'subscription' : 'api-key',
|
||||
});
|
||||
|
||||
if (p.isCancel(result)) {
|
||||
p.cancel('Installation cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
selectedProvider = result as ProviderId;
|
||||
if (result === 'subscription') {
|
||||
useSubscriptionAuth();
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
const apiModeResult = await p.select<ClaudeApiMode>({
|
||||
message: 'How should claude-mem connect?',
|
||||
options: [
|
||||
{ value: 'direct', label: 'Anthropic API key' },
|
||||
{ value: 'gateway', label: 'LiteLLM or custom gateway' },
|
||||
],
|
||||
initialValue: loadClaudeMemEnv().ANTHROPIC_BASE_URL ? 'gateway' : 'direct',
|
||||
});
|
||||
|
||||
if (p.isCancel(apiModeResult)) {
|
||||
p.cancel('Installation cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (apiModeResult === 'gateway') {
|
||||
await configureGateway();
|
||||
} else {
|
||||
await configureDirectApiKey();
|
||||
}
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
if (selectedProvider === 'claude') {
|
||||
@@ -648,7 +762,7 @@ async function promptProvider(options: InstallOptions): Promise<ProviderId> {
|
||||
const apiKeyResult = await p.password({
|
||||
message: `Paste your ${providerLabel} API key:`,
|
||||
mask: '*',
|
||||
validate: (v: string) => (!v || v.trim().length === 0) ? 'API key required' : undefined,
|
||||
validate: (v?: string) => (!v || v.trim().length === 0) ? 'API key required' : undefined,
|
||||
});
|
||||
|
||||
if (p.isCancel(apiKeyResult)) {
|
||||
@@ -674,8 +788,10 @@ async function promptClaudeModel(options: InstallOptions): Promise<void> {
|
||||
'claude-sonnet-4-6',
|
||||
'claude-opus-4-7',
|
||||
]);
|
||||
const authMethod = getSetting('CLAUDE_MEM_CLAUDE_AUTH_METHOD');
|
||||
const allowCustomModel = authMethod === 'gateway';
|
||||
|
||||
if (options.model) {
|
||||
if (options.model && !allowCustomModel) {
|
||||
if (!allowed.has(options.model)) {
|
||||
throw new Error(
|
||||
`Unknown Claude model: ${options.model}. Allowed: ${[...allowed].join(', ')}`,
|
||||
@@ -687,10 +803,39 @@ async function promptClaudeModel(options: InstallOptions): Promise<void> {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (options.model && allowCustomModel) {
|
||||
const wrote = mergeSettings({ CLAUDE_MEM_MODEL: options.model });
|
||||
if (wrote) {
|
||||
log.info(`Saved gateway model=${options.model} to ~/.claude-mem/settings.json`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isInteractive) return;
|
||||
|
||||
const initialModel = getSetting('CLAUDE_MEM_MODEL');
|
||||
|
||||
if (allowCustomModel) {
|
||||
const result = await p.text({
|
||||
message: 'Which model should the gateway use?',
|
||||
placeholder: 'claude-haiku-4-5-20251001',
|
||||
defaultValue: initialModel || 'claude-haiku-4-5-20251001',
|
||||
validate: (v?: string) => (!v || v.trim().length === 0) ? 'Model required' : undefined,
|
||||
});
|
||||
|
||||
if (p.isCancel(result)) {
|
||||
p.cancel('Installation cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const selectedModel = String(result).trim();
|
||||
const wrote = mergeSettings({ CLAUDE_MEM_MODEL: selectedModel });
|
||||
if (wrote) {
|
||||
log.info(`Saved gateway model=${selectedModel} to ~/.claude-mem/settings.json`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const initialValue = allowed.has(initialModel) ? initialModel : 'claude-haiku-4-5-20251001';
|
||||
|
||||
const result = await p.select<string>({
|
||||
|
||||
Reference in New Issue
Block a user