feat: Add intelligent auto-router and enhanced integrations
- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
243
dexto/packages/agent-management/src/preferences/loader.ts
Normal file
243
dexto/packages/agent-management/src/preferences/loader.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
// packages/core/src/preferences/loader.ts
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import { promises as fs } from 'fs';
|
||||
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
||||
import { getDextoGlobalPath } from '../utils/path.js';
|
||||
import { logger } from '@dexto/core';
|
||||
import { DextoValidationError, DextoRuntimeError } from '@dexto/core';
|
||||
import type { LLMProvider } from '@dexto/core';
|
||||
import { GlobalPreferencesSchema, type GlobalPreferences } from './schemas.js';
|
||||
import { PREFERENCES_FILE } from './constants.js';
|
||||
import { PreferenceError } from './errors.js';
|
||||
|
||||
/**
|
||||
* Load global preferences from ~/.dexto/preferences.yml
|
||||
* @returns Global preferences object
|
||||
* @throws DextoRuntimeError if file not found or corrupted
|
||||
* @throws DextoValidationError if preferences are invalid
|
||||
*/
|
||||
export async function loadGlobalPreferences(): Promise<GlobalPreferences> {
|
||||
const preferencesPath = getDextoGlobalPath(PREFERENCES_FILE);
|
||||
|
||||
// Check if preferences file exists
|
||||
if (!existsSync(preferencesPath)) {
|
||||
throw PreferenceError.fileNotFound(preferencesPath);
|
||||
}
|
||||
|
||||
try {
|
||||
// Read and parse YAML
|
||||
const fileContent = await fs.readFile(preferencesPath, 'utf-8');
|
||||
const rawPreferences = parseYaml(fileContent);
|
||||
|
||||
// Validate with schema
|
||||
const validation = GlobalPreferencesSchema.safeParse(rawPreferences);
|
||||
if (!validation.success) {
|
||||
throw PreferenceError.validationFailed(validation.error);
|
||||
}
|
||||
|
||||
logger.debug(`Loaded global preferences from: ${preferencesPath}`);
|
||||
return validation.data;
|
||||
} catch (error) {
|
||||
if (error instanceof DextoValidationError || error instanceof DextoRuntimeError) {
|
||||
throw error; // Re-throw our own errors
|
||||
}
|
||||
|
||||
throw PreferenceError.fileReadError(
|
||||
preferencesPath,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Header comment for preferences.yml file
|
||||
*/
|
||||
const PREFERENCES_FILE_HEADER = `# Dexto Global Preferences
|
||||
# Documentation: https://dexto.dev/docs/configuration/preferences
|
||||
#
|
||||
# Sound Notifications:
|
||||
# Dexto plays sounds for approval requests and task completion.
|
||||
# To customize sounds, place audio files in ~/.dexto/sounds/:
|
||||
# - approval.wav (or .mp3, .ogg, .aiff, .m4a) - played when tool approval is needed
|
||||
# - complete.wav (or .mp3, .ogg, .aiff, .m4a) - played when agent finishes a task
|
||||
# Set sounds.enabled: false to disable all sounds.
|
||||
|
||||
`;
|
||||
|
||||
/**
|
||||
* Save global preferences to ~/.dexto/preferences.yml
|
||||
* @param preferences Validated preferences object
|
||||
* @throws DextoRuntimeError if write fails
|
||||
*/
|
||||
export async function saveGlobalPreferences(preferences: GlobalPreferences): Promise<void> {
|
||||
const preferencesPath = getDextoGlobalPath(PREFERENCES_FILE);
|
||||
|
||||
// Validate preferences against schema before saving
|
||||
const validation = GlobalPreferencesSchema.safeParse(preferences);
|
||||
if (!validation.success) {
|
||||
throw PreferenceError.validationFailed(validation.error);
|
||||
}
|
||||
|
||||
try {
|
||||
logger.debug(`Saving global preferences to: ${preferencesPath}`);
|
||||
// Ensure ~/.dexto directory exists
|
||||
const dextoDir = getDextoGlobalPath('');
|
||||
await fs.mkdir(dextoDir, { recursive: true });
|
||||
|
||||
// Convert to YAML with nice formatting
|
||||
const yamlContent = stringifyYaml(preferences, {
|
||||
indent: 2,
|
||||
lineWidth: 100,
|
||||
minContentWidth: 20,
|
||||
});
|
||||
|
||||
// Write to file with header comment
|
||||
await fs.writeFile(preferencesPath, PREFERENCES_FILE_HEADER + yamlContent, 'utf-8');
|
||||
|
||||
logger.debug(
|
||||
`✓ Saved global preferences ${JSON.stringify(preferences)} to: ${preferencesPath}`
|
||||
);
|
||||
} catch (error) {
|
||||
throw PreferenceError.fileWriteError(
|
||||
preferencesPath,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if global preferences exist (for first-time detection)
|
||||
* @returns true if preferences.yml exists
|
||||
*/
|
||||
export function globalPreferencesExist(): boolean {
|
||||
const preferencesPath = getDextoGlobalPath(PREFERENCES_FILE);
|
||||
return existsSync(preferencesPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get global preferences file path
|
||||
* @returns Absolute path to preferences.yml
|
||||
*/
|
||||
export function getGlobalPreferencesPath(): string {
|
||||
return getDextoGlobalPath(PREFERENCES_FILE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for creating initial preferences
|
||||
*/
|
||||
export interface CreatePreferencesOptions {
|
||||
provider: LLMProvider;
|
||||
model: string;
|
||||
/** API key env var (optional for providers like Ollama that don't need auth) */
|
||||
apiKeyVar?: string;
|
||||
defaultAgent?: string;
|
||||
defaultMode?: 'cli' | 'web' | 'server' | 'discord' | 'telegram' | 'mcp';
|
||||
baseURL?: string;
|
||||
/** Reasoning effort for OpenAI reasoning models (o1, o3, codex, gpt-5.x) */
|
||||
reasoningEffort?: 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
|
||||
setupCompleted?: boolean;
|
||||
/** Whether API key setup was skipped and needs to be configured later */
|
||||
apiKeyPending?: boolean;
|
||||
/** Whether baseURL setup was skipped and needs to be configured later */
|
||||
baseURLPending?: boolean;
|
||||
/** Sound notification preferences */
|
||||
sounds?: {
|
||||
enabled?: boolean;
|
||||
onApprovalRequired?: boolean;
|
||||
onTaskComplete?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create initial preferences from setup data
|
||||
* @param options Configuration options for preferences
|
||||
*/
|
||||
export function createInitialPreferences(options: CreatePreferencesOptions): GlobalPreferences {
|
||||
const llmConfig: GlobalPreferences['llm'] = {
|
||||
provider: options.provider,
|
||||
model: options.model,
|
||||
};
|
||||
|
||||
// Only add apiKey if provided (optional for local providers like Ollama)
|
||||
if (options.apiKeyVar) {
|
||||
llmConfig.apiKey = '$' + options.apiKeyVar;
|
||||
}
|
||||
|
||||
// Only add baseURL if provided
|
||||
if (options.baseURL) {
|
||||
llmConfig.baseURL = options.baseURL;
|
||||
}
|
||||
|
||||
// Only add reasoningEffort if provided
|
||||
if (options.reasoningEffort) {
|
||||
llmConfig.reasoningEffort = options.reasoningEffort;
|
||||
}
|
||||
|
||||
return {
|
||||
llm: llmConfig,
|
||||
defaults: {
|
||||
defaultAgent: options.defaultAgent || 'coding-agent',
|
||||
defaultMode: options.defaultMode || 'web',
|
||||
},
|
||||
setup: {
|
||||
completed: options.setupCompleted ?? true,
|
||||
apiKeyPending: options.apiKeyPending ?? false,
|
||||
baseURLPending: options.baseURLPending ?? false,
|
||||
},
|
||||
sounds: {
|
||||
enabled: options.sounds?.enabled ?? true,
|
||||
onApprovalRequired: options.sounds?.onApprovalRequired ?? true,
|
||||
onTaskComplete: options.sounds?.onTaskComplete ?? true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates type that allows partial nested objects
|
||||
*/
|
||||
export type GlobalPreferencesUpdates = {
|
||||
llm?: GlobalPreferences['llm'];
|
||||
defaults?: Partial<GlobalPreferences['defaults']>;
|
||||
setup?: Partial<GlobalPreferences['setup']>;
|
||||
sounds?: Partial<GlobalPreferences['sounds']>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update specific preference sections
|
||||
* @param updates Partial preference updates
|
||||
* @returns Updated preferences object
|
||||
* @throws DextoRuntimeError if load/save fails
|
||||
* @throws DextoValidationError if merged preferences are invalid
|
||||
*/
|
||||
export async function updateGlobalPreferences(
|
||||
updates: GlobalPreferencesUpdates
|
||||
): Promise<GlobalPreferences> {
|
||||
// Load existing preferences
|
||||
const existing = await loadGlobalPreferences();
|
||||
|
||||
// Hybrid merge strategy: different sections have different coherence requirements
|
||||
const merged = {
|
||||
...existing,
|
||||
...updates,
|
||||
// LLM section requires complete replacement (high coherence - provider/model/apiKey must match)
|
||||
llm: updates.llm || existing.llm,
|
||||
// Defaults, setup, and sounds sections allow partial updates (low coherence - independent fields)
|
||||
defaults: updates.defaults
|
||||
? { ...existing.defaults, ...updates.defaults }
|
||||
: existing.defaults,
|
||||
setup: updates.setup ? { ...existing.setup, ...updates.setup } : existing.setup,
|
||||
sounds: updates.sounds ? { ...existing.sounds, ...updates.sounds } : existing.sounds,
|
||||
};
|
||||
|
||||
// Validate merged result
|
||||
const validation = GlobalPreferencesSchema.safeParse(merged);
|
||||
if (!validation.success) {
|
||||
throw PreferenceError.validationFailed(validation.error);
|
||||
}
|
||||
|
||||
// Save updated preferences
|
||||
await saveGlobalPreferences(validation.data);
|
||||
|
||||
return validation.data;
|
||||
}
|
||||
Reference in New Issue
Block a user