Project Files
src / manager.ts
/**
* @file manager.ts
* @description Persona switching logic — orchestrates profile loading and
* context management for the persona-manager plugin.
*
* Provides the core operations used by the LM Studio tools:
* - switchPersona: activate a persona by ID
* - listPersonas: enumerate available profiles
* - getCurrentPersona: query the active state
*/
import { loadProfile, loadAllProfiles, saveProfileToDisk } from "./loader";
import {
switchPersona as ctxSwitch,
getActivePersona,
getContext,
} from "./context";
import type {
PersonaProfile,
ListPersonasResult,
SavePersonaParams,
SavePersonaResult,
SwitchPersonaResult,
GetPersonaResult,
} from "./types";
/**
* Default persona used when no specific profile is loaded.
*/
export const DEFAULT_PERSONA_ID = "__default__";
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Switch to a persona by ID.
* Returns the result of the switch operation.
* Throws if the persona ID is not found among loaded profiles.
*/
export function switchToPersona(personaId: string): SwitchPersonaResult {
if (personaId === DEFAULT_PERSONA_ID) {
// Switching to default — clear the active persona
const previous = getActivePersona();
// Use a minimal "default" profile
const defaultProfile: PersonaProfile = {
id: DEFAULT_PERSONA_ID,
name: "Default",
description: "Default assistant persona — no character override.",
systemPrompt: "You are a helpful assistant.",
};
const prevId = ctxSwitch(defaultProfile);
return {
previousPersona: prevId,
activePersona: defaultProfile,
contextReset: true,
};
}
const profile = loadProfile(personaId);
if (!profile) {
throw new Error(
`Persona "${personaId}" not found. ` +
"Use the list tool to see available personas.",
);
}
const prevId = ctxSwitch(profile);
return {
previousPersona: prevId,
activePersona: profile,
contextReset: true,
};
}
/**
* List all available persona profiles.
* Returns a summary result suitable for returning as a tool result.
*/
export function listPersonas(): ListPersonasResult {
const profiles = loadAllProfiles();
return {
personas: profiles.map((p) => ({
id: p.id,
name: p.name,
description: p.description,
tags: p.tags,
})),
count: profiles.length,
};
}
/**
* Get the currently active persona details and context.
*/
export function getCurrentPersona(): GetPersonaResult {
const persona = getActivePersona();
return {
active: persona !== null && persona.id !== DEFAULT_PERSONA_ID,
persona,
context: getContext(),
};
}
// ---------------------------------------------------------------------------
// Save tool
// ---------------------------------------------------------------------------
/**
* Generate a kebab-case, filename-safe ID from a display name.
*/
function generateId(name: string): string {
return name
.toLowerCase()
.trim()
.replace(/\s+/g, "-")
.replace(/[^a-z0-9-]/g, "")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
}
/**
* Save a new persona profile to disk.
*
* Generates an ID from the display name, validates required fields,
* writes the profile JSON file, and reloads the in-memory cache.
*/
export function savePersona(params: SavePersonaParams): SavePersonaResult {
const {
name,
description,
systemPrompt,
greeting,
tags,
overwrite = false,
} = params;
// Validate required fields
if (!name || typeof name !== "string" || name.trim().length === 0) {
throw new Error("Field 'name' is required and must be a non-empty string.");
}
if (
!description ||
typeof description !== "string" ||
description.trim().length === 0
) {
throw new Error(
"Field 'description' is required and must be a non-empty string.",
);
}
if (
!systemPrompt ||
typeof systemPrompt !== "string" ||
systemPrompt.trim().length === 0
) {
throw new Error(
"Field 'systemPrompt' is required and must be a non-empty string.",
);
}
const id = generateId(name);
if (!id) {
throw new Error(
`Failed to generate a valid ID from name "${name}". ` +
"Name must contain at least one alphanumeric character.",
);
}
const profile: PersonaProfile = {
id,
name: name.trim(),
description: description.trim(),
systemPrompt: systemPrompt.trim(),
...(greeting ? { greeting: greeting.trim() } : {}),
...(tags && tags.length > 0 ? { tags } : {}),
};
const { filePath, overwritten } = saveProfileToDisk(profile, overwrite);
return {
id: profile.id,
name: profile.name,
description: profile.description,
systemPrompt: profile.systemPrompt,
greeting: profile.greeting,
tags: profile.tags,
filePath,
success: true,
overwritten,
message: overwritten
? `Persona "${profile.name}" (${profile.id}) overwritten successfully.`
: `Persona "${profile.name}" (${profile.id}) created successfully.`,
};
}
/**
* Alias for the active persona used in toolsProvider.
*/
export { getActivePersona, getContext };