feat: add OpenAI OAuth, strict model overrides, and Gmail pull mode

This commit is contained in:
William Valentin
2026-02-13 14:55:40 -08:00
parent 8f644d5e25
commit 955b9e28e0
50 changed files with 5955 additions and 160 deletions
+80 -18
View File
@@ -9,9 +9,10 @@ import type { ModelConfig, ModelProvider } from '../../config/schema.js';
import { MODEL_PROVIDERS } from '../../config/schema.js';
import { OllamaClient, LlamaCppClient } from '../../models/index.js';
import { createClientFromConfig } from '../../daemon/index.js';
import { loginGitHub } from '../../auth/index.js';
import { loginGitHub, loginOpenAI } from '../../auth/index.js';
import type { PairingManager } from '../../channels/pairing.js';
import { getColoredBanner } from './banner.js';
import type { HookEngine } from '../../hooks/index.js';
export { parseCommand, type Command };
@@ -42,8 +43,10 @@ export interface MinimalTuiConfig {
onFullscreen?: () => void;
onTransfer?: (target: string) => void;
localProviders?: Record<string, ModelConfig>;
modelProviderConfigs?: Partial<Record<ModelProvider, ModelConfig>>;
currentLocalProvider?: string;
pairingManager?: PairingManager;
hookEngine?: HookEngine;
}
export class MinimalTui {
@@ -99,6 +102,10 @@ export class MinimalTui {
async start(): Promise<void> {
this.running = true;
if (this.config.agent && this.config.modelRouter) {
this.config.agent.setModelTier(this.config.modelRouter.getTier());
}
this.rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
@@ -108,6 +115,26 @@ export class MinimalTui {
},
});
// In minimal TUI we can prompt inline for tool confirmations.
// This avoids deadlocks when hooks are configured to require confirmation
// (e.g. shell.exec) and the tool loop is awaiting a decision.
if (this.config.hookEngine) {
this.config.hookEngine.setInteractiveConfirmer(async (pending) => {
const tool = pending.tool;
const args = pending.args;
const argsStr = Object.keys(args).length > 0 ? ` ${JSON.stringify(args)}` : '';
console.log(`\n${colors.bold}Confirmation required${colors.reset}`);
console.log(`${colors.gray}${tool}${colors.reset}${argsStr}`);
const answer = (await this.prompt(`${colors.orange}${colors.bold}Approve?${colors.reset} ${colors.gray}(y/N)${colors.reset} `))
.trim()
.toLowerCase();
const approved = answer === 'y' || answer === 'yes';
console.log(approved ? `${colors.gray}Approved.${colors.reset}\n` : `${colors.gray}Denied.${colors.reset}\n`);
return approved ? { approved: true } : { approved: false, reason: 'Denied by user' };
});
}
// Listen for line changes to show hints
process.stdin.on('keypress', () => {
// Small delay to let readline update the line
@@ -239,9 +266,22 @@ export class MinimalTui {
}
try {
const client = createClientFromConfig({ provider: provider as ModelProvider, model });
const providerType = provider as ModelProvider;
const template = this.config.modelProviderConfigs?.[providerType];
const client = createClientFromConfig({
...(template ?? {}),
provider: providerType,
model,
});
router.setClient(tier, client, providerModel);
console.log(`${colors.gray}Set ${tier} to:${colors.reset} ${providerModel}\n`);
router.setTierStrict(tier, true);
if (this.config.agent && tier === router.getTier()) {
this.config.agent.setModelTier(tier);
}
console.log(`${colors.gray}Set ${tier} to:${colors.reset} ${providerModel}`);
console.log(`${colors.gray}Fallbacks for ${tier} disabled (strict tier mode).${colors.reset}\n`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`${colors.gray}Failed to create client:${colors.reset} ${message}\n`);
@@ -383,27 +423,49 @@ export class MinimalTui {
private async handleLoginCommand(provider?: string): Promise<void> {
const target = provider ?? 'github';
if (target !== 'github') {
console.log(`${colors.gray}Unknown login provider:${colors.reset} ${target}. Only 'github' is supported.\n`);
if (target === 'github') {
console.log(`${colors.gray}Starting GitHub OAuth device flow...${colors.reset}`);
try {
await loginGitHub((userCode, verificationUri) => {
console.log('');
console.log(`${colors.gray}Please visit:${colors.reset} ${verificationUri}`);
console.log(`${colors.gray}and enter code:${colors.reset} ${userCode}`);
console.log('');
console.log(`${colors.gray}Waiting for authorization...${colors.reset}`);
});
console.log(`${colors.gray}GitHub authentication successful! Token stored.${colors.reset}\n`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`${colors.gray}GitHub login failed:${colors.reset} ${message}\n`);
}
return;
}
console.log(`${colors.gray}Starting GitHub OAuth device flow...${colors.reset}`);
if (target === 'openai') {
console.log(`${colors.gray}Starting OpenAI OAuth device flow...${colors.reset}`);
try {
await loginGitHub((userCode, verificationUri) => {
console.log('');
console.log(`${colors.gray}Please visit:${colors.reset} ${verificationUri}`);
console.log(`${colors.gray}and enter code:${colors.reset} ${userCode}`);
console.log('');
console.log(`${colors.gray}Waiting for authorization...${colors.reset}`);
});
try {
await loginOpenAI((userCode, verificationUri) => {
console.log('');
console.log(`${colors.gray}Please visit:${colors.reset} ${verificationUri}`);
console.log(`${colors.gray}and enter code:${colors.reset} ${userCode}`);
console.log('');
console.log(`${colors.gray}Waiting for authorization...${colors.reset}`);
});
console.log(`${colors.gray}GitHub authentication successful! Token stored.${colors.reset}\n`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`${colors.gray}GitHub login failed:${colors.reset} ${message}\n`);
console.log(`${colors.gray}OpenAI authentication successful! Token stored.${colors.reset}\n`);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.log(`${colors.gray}OpenAI login failed:${colors.reset} ${message}\n`);
}
return;
}
console.log(`${colors.gray}Unknown login provider:${colors.reset} ${target}. Supported: github, openai\n`);
}
private handlePairCommand(action?: 'generate' | 'list' | 'revoke', args?: string): void {