Project Files
src / index.ts
/**
* @file index.ts
* @description Persona Manager plugin — LM Studio plugin entry point.
*
* Registers three tools via toolsProvider:
* - `switch`: Change the active persona
* - `list`: List available persona profiles
* - `get`: Get current persona details and context
*
* Persona profiles are loaded from the `profiles/` directory as JSON files.
* Each profile defines a system prompt, description, and optional metadata.
*/
import {
tool,
Tool,
ToolsProviderController,
PluginContext,
} from "@lmstudio/sdk";
import { z } from "zod";
import { initLoader } from "./loader";
import { initContext } from "./context";
import {
switchToPersona,
listPersonas,
getCurrentPersona,
savePersona,
} from "./manager";
import { resolve, dirname } from "node:path";
// ---------------------------------------------------------------------------
// Plugin initialisation
// ---------------------------------------------------------------------------
const _dirname = dirname(__dirname);
/** Resolved path to the profiles/ directory bundled with the plugin. */
function getProfilesDir(): string {
// When running from source: plugin root is two levels up from src/
// When running from dist: plugin root is one level up from dist/
const srcPath = resolve(_dirname, "..", "profiles");
return srcPath;
}
// ---------------------------------------------------------------------------
// Zod schemas for tool parameters
// ---------------------------------------------------------------------------
const SwitchParams = z.object({
personaId: z
.string()
.min(1)
.describe(
"Persona ID to activate (kebab-case, e.g. 'socrates', 'einstein'). " +
"Use '__default__' to reset to the default assistant persona. " +
"Use the 'list' tool to see available personas.",
),
});
const SaveParams = z.object({
name: z
.string()
.min(1)
.describe("Display name for the persona (e.g. 'Marie Curie')."),
description: z.string().min(1).describe("Persona backstory / description."),
systemPrompt: z
.string()
.min(1)
.describe("System prompt injected when this persona is active."),
greeting: z
.string()
.optional()
.describe("Optional greeting message to start a conversation."),
tags: z
.array(z.string())
.optional()
.describe("Optional tags for categorising this persona."),
overwrite: z
.boolean()
.optional()
.default(false)
.describe("Overwrite an existing profile with the same generated ID."),
});
// ---------------------------------------------------------------------------
// Tools provider
// ---------------------------------------------------------------------------
export async function main(context: PluginContext): Promise<void> {
// Initialise loader and context
const profilesDir = getProfilesDir();
initLoader(profilesDir);
initContext();
context.withToolsProvider(toolsProvider);
}
async function toolsProvider(_ctl: ToolsProviderController): Promise<Tool[]> {
// ---- switch tool ----
const switchTool = tool({
name: "switch",
description:
"Switch the active persona to the specified character profile.\n\n" +
"This changes the system prompt to match the selected persona, " +
"resets the conversation context, and returns the active persona details.\n\n" +
"Use '__default__' as the personaId to return to the default assistant.",
parameters: {
personaId: SwitchParams.shape.personaId,
},
implementation: async (
{ personaId },
{ warn },
): Promise<Record<string, unknown>> => {
try {
const result = switchToPersona(personaId);
return {
previousPersona: result.previousPersona ?? "__none__",
activePersona: {
id: result.activePersona.id,
name: result.activePersona.name,
description: result.activePersona.description,
systemPrompt: result.activePersona.systemPrompt,
greeting: result.activePersona.greeting,
tags: result.activePersona.tags ?? [],
},
contextReset: result.contextReset,
};
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : String(err ?? "unknown error");
warn(`Persona switch error: ${message}`);
return { error: message };
}
},
});
// ---- list tool ----
const listTool = tool({
name: "list",
description:
"List all available persona profiles.\n\n" +
"Returns an array of persona summaries with id, name, description, and tags. " +
"Use the 'switch' tool with a persona ID to activate one.",
parameters: {},
implementation: async (
_params: Record<string, never>,
): Promise<Record<string, unknown>> => {
try {
const result = listPersonas();
return {
personas: result.personas.map((p) => ({
id: p.id,
name: p.name,
description: p.description,
tags: p.tags ?? [],
})),
count: result.count,
};
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : String(err ?? "unknown error");
return { error: message, personas: [], count: 0 };
}
},
});
// ---- get tool ----
const getTool = tool({
name: "get",
description:
"Get the currently active persona details and conversation context.\n\n" +
"Returns whether a persona is active, the full profile, " +
"and context metadata (systemPrompt, messageCount).",
parameters: {},
implementation: async (
_params: Record<string, never>,
): Promise<Record<string, unknown>> => {
try {
const result = getCurrentPersona();
return {
active: result.active,
persona: result.persona
? {
id: result.persona.id,
name: result.persona.name,
description: result.persona.description,
systemPrompt: result.persona.systemPrompt,
greeting: result.persona.greeting,
tags: result.persona.tags ?? [],
}
: null,
context: {
activePersonaId: result.context.activePersonaId,
activePersonaName: result.context.activePersonaName,
systemPrompt: result.context.systemPrompt,
messageCount: result.context.messageCount,
},
};
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : String(err ?? "unknown error");
return { error: message };
}
},
});
// ---- save tool ----
const saveTool = tool({
name: "save",
description:
"Create and save a new persona profile to disk.\n\n" +
"Generates a kebab-case ID from the display name, validates the input, " +
"writes the profile as a JSON file to the profiles directory, " +
"and reloads the cache so the new persona is immediately available " +
"for use with the 'switch' tool.\n\n" +
"Use overwrite=true to replace an existing profile with the same ID.",
parameters: {
name: SaveParams.shape.name,
description: SaveParams.shape.description,
systemPrompt: SaveParams.shape.systemPrompt,
greeting: SaveParams.shape.greeting,
tags: SaveParams.shape.tags,
overwrite: SaveParams.shape.overwrite,
},
implementation: async (
params: z.infer<typeof SaveParams>,
{ warn },
): Promise<Record<string, unknown>> => {
try {
const result = savePersona(params);
return {
id: result.id,
name: result.name,
description: result.description,
systemPrompt: result.systemPrompt,
greeting: result.greeting,
tags: result.tags ?? [],
filePath: result.filePath,
success: result.success,
overwritten: result.overwritten,
message: result.message,
};
} catch (err: unknown) {
const message =
err instanceof Error ? err.message : String(err ?? "unknown error");
warn(`Persona save error: ${message}`);
return { error: message, success: false };
}
},
});
return [switchTool, listTool, getTool, saveTool];
}