feat(tui): wire /pair command execution with PairingManager

This commit is contained in:
William Valentin
2026-02-09 21:56:27 -08:00
parent 322852917c
commit 3ea4f64d6b
2 changed files with 79 additions and 0 deletions
+5
View File
@@ -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) => {
+74
View File
@@ -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<string, ModelConfig>;
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 <channel> <senderId>${colors.reset}\n`);
return;
}
const parts = args.split(/\s+/);
if (parts.length < 2) {
console.log(`${colors.gray}Usage: /pair revoke <channel> <senderId>${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) {