diff --git a/src/cli/tui.ts b/src/cli/tui.ts index 3ccc228..c4795a2 100644 --- a/src/cli/tui.ts +++ b/src/cli/tui.ts @@ -62,6 +62,10 @@ export function registerTuiCommand(program: Command): void { // local_providers, retry config, and per-tier fallback logic. const modelRouter = createModelRouter(config); + const { initPairingManager } = await import('../daemon/services.js'); + const pairingStore = config.pairing.enabled ? sessionStore.getPairingStore() : undefined; + const pairingManager = initPairingManager(config, pairingStore); + const systemPrompt = loadSystemPrompt(); const hookEngine = new HookEngine(config.hooks); @@ -145,6 +149,7 @@ export function registerTuiCommand(program: Command): void { modelRouter, systemPrompt, agent, + pairingManager, localProviders: config.models.local_providers, currentLocalProvider: config.models.local?.provider, onTransfer: (target) => { diff --git a/src/frontends/tui/minimal.ts b/src/frontends/tui/minimal.ts index c4c89ec..01147b6 100644 --- a/src/frontends/tui/minimal.ts +++ b/src/frontends/tui/minimal.ts @@ -9,6 +9,7 @@ import type { ModelConfig } from '../../config/schema.js'; import { OllamaClient, LlamaCppClient } from '../../models/index.js'; import { createClientFromConfig } from '../../daemon/index.js'; import { loginGitHub } from '../../auth/index.js'; +import type { PairingManager } from '../../channels/pairing.js'; export { parseCommand, type Command }; @@ -40,6 +41,7 @@ export interface MinimalTuiConfig { onTransfer?: (target: string) => void; localProviders?: Record; currentLocalProvider?: string; + pairingManager?: PairingManager; } export class MinimalTui { @@ -196,6 +198,10 @@ export class MinimalTui { await this.handleLoginCommand(command.provider); break; + case 'pair': + this.handlePairCommand(command.action, command.args); + break; + case 'transfer': this.config.onTransfer?.(command.target); break; @@ -321,6 +327,74 @@ export class MinimalTui { } } + private handlePairCommand(action?: 'generate' | 'list' | 'revoke', args?: string): void { + const pm = this.config.pairingManager; + if (!pm) { + console.log(`${colors.gray}Pairing not enabled. Set pairing.enabled: true in config.${colors.reset}\n`); + return; + } + + switch (action) { + case 'generate': { + const code = pm.generateCode(args); + const pending = pm.listPendingCodes().find(p => p.code === code); + const expiresIn = pending ? Math.round((pending.expiresAt - Date.now()) / 1000) : '?'; + console.log(`${colors.bold}Pairing code: ${code}${colors.reset}`); + console.log(`${colors.gray}Expires in ${expiresIn}s${args ? ` (label: ${args})` : ''}${colors.reset}\n`); + break; + } + + case 'revoke': { + if (!args) { + console.log(`${colors.gray}Usage: /pair revoke ${colors.reset}\n`); + return; + } + const parts = args.split(/\s+/); + if (parts.length < 2) { + console.log(`${colors.gray}Usage: /pair revoke ${colors.reset}\n`); + return; + } + const [channel, senderId] = parts; + const revoked = pm.revokeApproval(channel, senderId); + if (revoked) { + console.log(`${colors.bold}Revoked approval for ${channel}:${senderId}${colors.reset}\n`); + } else { + console.log(`${colors.gray}No approval found for ${channel}:${senderId}${colors.reset}\n`); + } + break; + } + + case 'list': + default: { + const pending = pm.listPendingCodes(); + const approved = pm.listApproved(); + + if (pending.length === 0 && approved.length === 0) { + console.log(`${colors.gray}No pending codes or approved senders.${colors.reset}\n`); + return; + } + + if (pending.length > 0) { + console.log(`${colors.bold}Pending codes:${colors.reset}`); + for (const p of pending) { + const expiresIn = Math.round((p.expiresAt - Date.now()) / 1000); + console.log(` ${p.code} expires in ${expiresIn}s${p.label ? ` (${p.label})` : ''}`); + } + } + + if (approved.length > 0) { + console.log(`${colors.bold}Approved senders:${colors.reset}`); + for (const a of approved) { + const date = new Date(a.approvedAt).toISOString().slice(0, 16).replace('T', ' '); + console.log(` ${a.channel}:${a.senderId} since ${date} (code: ${a.codeUsed})`); + } + } + console.log(''); + break; + } + } + } + private getAvailableBackends(): string[] { const backends: string[] = []; if (this.config.currentLocalProvider) {