Skip to main content
Glama
runtime-state.ts5.46 kB
/** * Runtime Role State * * Singleton to track the active role during a session. * Supports runtime role switching via wpnav_load_role or CLI. * * State persistence: * - In-memory for current process * - .wpnav-state.json for CLI sessions (auto-gitignored) * * @package WP_Navigator_MCP * @since 2.7.0 */ import * as fs from 'fs'; import * as path from 'path'; import { LoadedRole } from './types.js'; import { getRole } from './loader.js'; import { RuntimeRoleStateData, WpnavStateFile } from './filter-types.js'; /** * State file name (auto-gitignored) */ export const STATE_FILE_NAME = '.wpnav-state.json'; /** * Runtime Role State Singleton * * Tracks the currently active role during a session. */ class RuntimeRoleState { private state: RuntimeRoleStateData = { activeRole: null, source: null, setAt: null, }; private projectRoot: string | null = null; /** * Initialize with project root directory. * * @param projectRoot - Path to project root (where .wpnav-state.json lives) */ initialize(projectRoot: string): void { this.projectRoot = projectRoot; this.loadFromFile(); } /** * Set the active role. * * @param roleSlug - Role slug to set, or null to clear * @param source - How the role was set * @returns Success result */ setRole( roleSlug: string | null, source: 'cli' | 'tool' | 'state-file' ): { success: boolean; error?: string } { if (roleSlug !== null) { // Validate role exists const role = getRole(roleSlug); if (!role) { return { success: false, error: `Role not found: "${roleSlug}"` }; } } this.state = { activeRole: roleSlug, source: roleSlug ? source : null, setAt: roleSlug ? Date.now() : null, }; // Persist to file for CLI sessions if (source === 'cli' || source === 'state-file') { this.saveToFile(); } return { success: true }; } /** * Get the current role slug. * * @returns Role slug or null if no runtime override */ getRole(): string | null { return this.state.activeRole; } /** * Get the loaded role object. * * @returns LoadedRole or null */ getLoadedRole(): LoadedRole | null { if (!this.state.activeRole) { return null; } return getRole(this.state.activeRole); } /** * Get the source of the current role. * * @returns Source or null if no role */ getSource(): 'cli' | 'tool' | 'state-file' | null { return this.state.source; } /** * Get the full state data. * * @returns Runtime state data */ getState(): RuntimeRoleStateData { return { ...this.state }; } /** * Clear the runtime role override. */ clear(): void { this.state = { activeRole: null, source: null, setAt: null, }; this.deleteFile(); } /** * Override the current role. * Alias for setRole with validation. * * @param roleSlug - Role slug to set * @param source - How the role was set * @returns Result with success status */ overrideRole( roleSlug: string, source: 'cli' | 'tool' = 'tool' ): { success: boolean; error?: string; role?: LoadedRole } { const result = this.setRole(roleSlug, source); if (result.success) { return { success: true, role: this.getLoadedRole() ?? undefined }; } return result; } /** * Load state from .wpnav-state.json file. */ private loadFromFile(): void { if (!this.projectRoot) { return; } const filePath = path.join(this.projectRoot, STATE_FILE_NAME); try { if (!fs.existsSync(filePath)) { return; } const content = fs.readFileSync(filePath, 'utf-8'); const data = JSON.parse(content) as WpnavStateFile; if (data.active_role) { // Validate role still exists const role = getRole(data.active_role); if (role) { this.state = { activeRole: data.active_role, source: 'state-file', setAt: data.modified_at ? new Date(data.modified_at).getTime() : Date.now(), }; } } } catch { // Ignore file read errors, start with clean state } } /** * Save state to .wpnav-state.json file. */ private saveToFile(): void { if (!this.projectRoot) { return; } const filePath = path.join(this.projectRoot, STATE_FILE_NAME); try { const data: WpnavStateFile = { active_role: this.state.activeRole, role_source: this.state.source === 'tool' ? undefined : (this.state.source ?? undefined), modified_at: new Date().toISOString(), }; fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8'); } catch { // Ignore file write errors } } /** * Delete the state file. */ private deleteFile(): void { if (!this.projectRoot) { return; } const filePath = path.join(this.projectRoot, STATE_FILE_NAME); try { if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); } } catch { // Ignore file delete errors } } /** * Reset state for testing. */ _reset(): void { this.state = { activeRole: null, source: null, setAt: null, }; this.projectRoot = null; } } /** * Global runtime role state instance */ export const runtimeRoleState = new RuntimeRoleState();

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/littlebearapps/wp-navigator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server