Project Files
dist / loader.js
"use strict";
/**
* @file loader.ts
* @description Persona profile loader — loads persona definitions from JSON files
* in the profiles/ directory and validates them against the schema.
*
* Conventions:
* - Profiles are stored as individual .json files in profiles/
* - Each file must validate against PersonaProfile
* - The loader caches loaded profiles in memory
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.initLoader = initLoader;
exports.loadAllProfiles = loadAllProfiles;
exports.loadProfile = loadProfile;
exports.reloadProfiles = reloadProfiles;
exports.saveProfileToDisk = saveProfileToDisk;
const node_fs_1 = require("node:fs");
const node_path_1 = require("node:path");
const zod_1 = require("zod");
// ---------------------------------------------------------------------------
// Zod schemas for profile validation
// ---------------------------------------------------------------------------
const ExampleDialogSchema = zod_1.z
.object({
role: zod_1.z.enum(["user", "assistant"]),
content: zod_1.z.string().min(1),
})
.strict();
const PersonaProfileLoaderSchema = zod_1.z
.object({
id: zod_1.z.string().min(1),
name: zod_1.z.string().min(1),
description: zod_1.z.string().min(1),
systemPrompt: zod_1.z.string().min(1),
greeting: zod_1.z.string().optional(),
tags: zod_1.z.array(zod_1.z.string()).optional(),
examples: zod_1.z.array(ExampleDialogSchema).optional(),
metadata: zod_1.z.record(zod_1.z.string(), zod_1.z.unknown()).optional(),
})
.strict();
// ---------------------------------------------------------------------------
// Internal state
// ---------------------------------------------------------------------------
let _profilesDir = null;
let _cache = null;
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/**
* Initialise the loader with the path to the profiles directory.
* Must be called before any load operations.
*/
function initLoader(profilesDir) {
_profilesDir = profilesDir;
_cache = null; // invalidate cache on re-init
}
/**
* Load all persona profiles from the profiles directory.
* Caches the result so subsequent calls are fast.
*
* Returns an array of PersonaProfile objects.
* Throws if the profiles directory does not exist or contains invalid files.
*/
function loadAllProfiles() {
if (!_profilesDir) {
throw new Error("Loader not initialised. Call initLoader(profilesDir) first.");
}
if (_cache) {
return Array.from(_cache.values());
}
if (!(0, node_fs_1.existsSync)(_profilesDir)) {
throw new Error(`Profiles directory not found: ${_profilesDir}. ` +
"Create the directory and add at least one persona JSON file.");
}
const files = (0, node_fs_1.readdirSync)(_profilesDir).filter((f) => f.endsWith(".json") && !f.startsWith("_"));
const profiles = new Map();
for (const file of files) {
const filePath = (0, node_path_1.join)(_profilesDir, file);
const raw = (0, node_fs_1.readFileSync)(filePath, "utf-8");
let parsed;
try {
parsed = JSON.parse(raw);
}
catch {
throw new Error(`Failed to parse JSON in "${file}": invalid JSON.`);
}
const validationResult = PersonaProfileLoaderSchema.safeParse(parsed);
if (!validationResult.success) {
const issues = validationResult.error.issues
.map((i) => `${i.path.join(".")}: ${i.message}`)
.join("; ");
throw new Error(`Validation failed for "${file}": ${issues}`);
}
profiles.set(validationResult.data.id, validationResult.data);
}
_cache = profiles;
return Array.from(profiles.values());
}
/**
* Load a single persona profile by ID.
* Returns null if no profile with that ID exists.
*/
function loadProfile(id) {
const profiles = loadAllProfiles();
return profiles.find((p) => p.id === id) ?? null;
}
/**
* Reload profiles from disk (invalidates cache).
*/
function reloadProfiles() {
_cache = null;
return loadAllProfiles();
}
// ---------------------------------------------------------------------------
// Profile persistence
// ---------------------------------------------------------------------------
/**
* Save a persona profile to disk as `{profilesDir}/{id}.json`.
*
* @param profile — The validated persona profile to persist.
* @param overwrite — If false and the file already exists, throws.
* @returns The absolute file path and whether an existing file was overwritten.
*/
function saveProfileToDisk(profile, overwrite) {
if (!_profilesDir) {
throw new Error("Loader not initialised. Call initLoader(profilesDir) first.");
}
const filePath = (0, node_path_1.join)(_profilesDir, `${profile.id}.json`);
const exists = (0, node_fs_1.existsSync)(filePath);
if (exists && !overwrite) {
throw new Error(`Profile "${profile.id}" already exists at ${filePath}. ` +
"Set overwrite=true to replace it.");
}
const json = JSON.stringify(profile, null, 2);
(0, node_fs_1.writeFileSync)(filePath, json, "utf-8");
// Invalidate and reload the cache so subsequent reads are consistent
reloadProfiles();
return { filePath, overwritten: exists };
}
//# sourceMappingURL=loader.js.map