prompt-version-manager
Version:
Centralized prompt management system for Human Behavior AI agents
550 lines (535 loc) • 20.9 kB
JavaScript
"use strict";
/**
* Core versioning operations for PVM TypeScript.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.VersioningEngine = void 0;
const fs = __importStar(require("fs/promises"));
const path = __importStar(require("path"));
const uuid_1 = require("uuid");
const engine_1 = require("../storage/engine");
const objects_1 = require("../storage/objects");
const exceptions_1 = require("../core/exceptions");
class VersioningEngine {
storage;
repoPath;
constructor(repoPath = '.pvm') {
this.repoPath = repoPath;
this.storage = new engine_1.StorageEngine(repoPath);
}
/**
* Initialize a new PVM repository.
*/
async init() {
try {
// Create repository directory
await fs.mkdir(this.repoPath, { recursive: true });
// Initialize storage engine directories explicitly
await fs.mkdir(path.join(this.repoPath, 'objects'), { recursive: true });
await fs.mkdir(path.join(this.repoPath, 'refs'), { recursive: true });
await fs.mkdir(path.join(this.repoPath, 'refs', 'heads'), { recursive: true });
await fs.mkdir(path.join(this.repoPath, 'refs', 'tags'), { recursive: true });
// Create prompts directory for tracking prompt templates
const promptsDir = path.join(this.repoPath, 'prompts');
await fs.mkdir(promptsDir, { recursive: true });
// Create README for the prompts directory
const readmePath = path.join(promptsDir, 'README.md');
try {
await fs.access(readmePath);
}
catch {
const readmeContent = `# Prompts Directory
This directory contains all prompt templates tracked by PVM.
## Structure
- Each \`.md\` file is a prompt template
- Templates use YAML frontmatter for metadata
- Variables are denoted with \`{{variable_name}}\`
## Example Template
\`\`\`markdown
---
id: example-prompt
version: 1.0.0
variables: [name, task]
---
You are a helpful assistant. Help {{name}} with {{task}}.
\`\`\`
`;
await fs.writeFile(readmePath, readmeContent);
}
// Create example template.md
const templatePath = path.join(promptsDir, 'template.md');
try {
await fs.access(templatePath);
}
catch {
const templateContent = `---
version: 1.0.0
description: Example template demonstrating PVM's simplified API
author: PVM
tags: [example, demo]
---
You are a {{role}} assistant with expertise in {{domain}}.
Your task is to {{task}}.
Please provide a response that is {{style}} and focuses on practical solutions.
`;
await fs.writeFile(templatePath, templateContent);
}
// Create example.ts in the parent directory
const exampleTsPath = path.join(path.dirname(this.repoPath), 'example.ts');
try {
await fs.access(exampleTsPath);
}
catch {
const exampleTsContent = `/**
* Example script demonstrating PVM's simplified API in TypeScript.
* This file was auto-generated by 'pvm init'.
*/
import { prompts, model } from '@hb/prompt-version-control';
async function main() {
// Load and render the template with variables
const prompt = prompts("template.md", [
"helpful AI", // {{role}}
"TypeScript", // {{domain}}
"explain interfaces", // {{task}}
"beginner-friendly" // {{style}}
]);
console.log("Rendered prompt:");
console.log("-".repeat(50));
console.log(prompt);
console.log("-".repeat(50));
// Example: Execute with a model (requires API key)
// Uncomment the following lines and set your API key to run:
// process.env.OPENAI_API_KEY = "your-api-key-here";
//
// const response = await model.complete(
// "gpt-3.5-turbo",
// prompt,
// "example-task"
// );
//
// console.log("\\nModel response:");
// console.log(response.content);
// console.log(\`\\nTokens used: \${response.metadata.tokens.total}\`);
}
// Run the example
main().catch(console.error);
`;
await fs.writeFile(exampleTsPath, exampleTsContent);
}
// Just set up the initial refs structure like Git
await this.storage.storeRef('HEAD', 'refs/heads/main');
console.log(`Initialized empty PVM repository in ${this.repoPath}`);
}
catch (error) {
throw new exceptions_1.StorageError(`Failed to initialize repository: ${error}`);
}
}
/**
* Check if the current directory is a PVM repository.
*/
async isRepo() {
try {
await fs.access(this.repoPath);
await fs.access(path.join(this.repoPath, 'objects'));
await fs.access(path.join(this.repoPath, 'refs'));
// Also check if HEAD exists (indicating proper initialization)
try {
await this.storage.loadRef('HEAD');
return true;
}
catch {
return false;
}
}
catch {
return false;
}
}
/**
* Add a prompt to the staging area.
*/
async add(content, messages, modelConfig = {}, metadata = {}, tags = []) {
if (!(await this.isRepo())) {
throw new exceptions_1.RepoNotFoundError('Not a PVM repository. Run "pvm init" first.');
}
try {
// Create prompt version
const promptVersion = {
id: (0, uuid_1.v4)(),
hash: '',
content,
messages,
modelConfig,
metadata,
parentId: null,
createdAt: new Date(),
tags
};
// Store prompt version object
const promptObj = new objects_1.PromptVersionObject(promptVersion);
const promptHash = await this.storage.storeObject(promptObj);
// Update the prompt version with the actual hash
promptVersion.hash = promptHash;
// Also store content as blob for easy access
const blobObj = new objects_1.BlobObject(content);
await this.storage.storeObject(blobObj);
// Add to staging area (stored as individual refs like Python)
const tag = tags.length > 0 ? tags[0] : 'unnamed';
await this.addToStaging(promptHash, tag);
console.log(`Added prompt version ${promptHash.substring(0, 8)}`);
return promptHash;
}
catch (error) {
throw new exceptions_1.StorageError(`Failed to add prompt: ${error}`);
}
}
/**
* Commit staged changes.
*/
async commit(message, author = 'PVM User', metadata = {}) {
if (!(await this.isRepo())) {
throw new exceptions_1.RepoNotFoundError('Not a PVM repository. Run "pvm init" first.');
}
try {
// Get staged prompt versions
const stagedPrompts = await this.getStagingRefs();
if (stagedPrompts.length === 0) {
throw new exceptions_1.InvalidCommitError('No changes staged for commit');
}
// Get current HEAD commit
let parentHashes = [];
try {
const headRef = await this.storage.loadRef('HEAD');
if (headRef.startsWith('refs/')) {
// HEAD points to a branch ref, resolve it to get the commit hash
const currentCommit = await this.storage.loadRef(headRef);
parentHashes = [currentCommit];
}
else {
// HEAD points directly to a commit
parentHashes = [headRef];
}
}
catch (error) {
// No previous commits (initial commit)
}
// Create tree object from staged prompts (Git-like structure)
const treeEntries = {};
const stagedPromptsInfo = await this.getStagedPrompts();
for (let i = 0; i < stagedPromptsInfo.length; i++) {
const [promptHash, tag] = stagedPromptsInfo[i];
const filename = tag !== 'unnamed' ? `${tag}_${i}.prompt` : `prompt_${i}.prompt`;
treeEntries[filename] = promptHash;
}
const treeObj = new objects_1.TreeObject(treeEntries);
const treeHash = await this.storage.storeObject(treeObj);
// Create commit object
const commit = {
hash: '',
message,
author,
timestamp: new Date(),
parentHashes,
treeHash,
promptVersions: stagedPrompts, // Keep for backward compatibility
metadata
};
const commitObj = new objects_1.CommitObject(commit);
const commitHash = await this.storage.storeObject(commitObj);
// Update the commit object with the actual hash
commit.hash = commitHash;
// Update current branch to point to new commit
try {
const headRef = await this.storage.loadRef('HEAD');
if (headRef.startsWith('refs/')) {
// HEAD points to a branch, update that branch
await this.storage.storeRef(headRef, commitHash, 'branch');
}
else {
// HEAD points directly to a commit (detached HEAD)
await this.storage.storeRef('HEAD', commitHash, 'head');
}
}
catch (error) {
// First commit, create main branch
await this.storage.storeRef('refs/heads/main', commitHash, 'branch');
await this.storage.storeRef('HEAD', 'refs/heads/main');
}
// Clear staging area
await this.clearStaging();
console.log(`Committed ${stagedPrompts.length} prompt version(s): ${commitHash.substring(0, 8)}`);
return commitHash;
}
catch (error) {
if (error instanceof exceptions_1.InvalidCommitError) {
throw error;
}
throw new exceptions_1.StorageError(`Failed to commit: ${error}`);
}
}
/**
* Show the difference between commits or working directory.
*/
async diff(commitHash1, commitHash2) {
if (!(await this.isRepo())) {
throw new exceptions_1.RepoNotFoundError('Not a PVM repository. Run "pvm init" first.');
}
try {
let commit1Prompts = [];
let commit2Prompts = [];
// If no commits specified, compare staging with HEAD
if (!commitHash1 && !commitHash2) {
// Get what's currently staged (only new additions)
const stagingRefs = await this.getStagingRefs();
// Get what's in HEAD
try {
const headHash = await this.storage.loadRef('HEAD');
const headCommit = await this.storage.loadObject(headHash);
headCommit.commit.hash = headHash; // Set hash from storage key
commit1Prompts = headCommit.commit.promptVersions;
}
catch {
// No previous commits
commit1Prompts = [];
}
// For staging vs HEAD, only show what's staged as "added"
// Everything in HEAD is conceptually still there
commit2Prompts = [...commit1Prompts, ...stagingRefs];
}
else if (commitHash1 && !commitHash2) {
// Compare commit with HEAD
const commit1 = await this.storage.loadObject(commitHash1);
commit1.commit.hash = commitHash1; // Set hash from storage key
commit1Prompts = commit1.commit.promptVersions;
try {
const headHash = await this.storage.loadRef('HEAD');
const headCommit = await this.storage.loadObject(headHash);
headCommit.commit.hash = headHash; // Set hash from storage key
commit2Prompts = headCommit.commit.promptVersions;
}
catch {
// No HEAD commit
}
}
else if (commitHash1 && commitHash2) {
// Compare two specific commits
const commit1 = await this.storage.loadObject(commitHash1);
const commit2 = await this.storage.loadObject(commitHash2);
commit1.commit.hash = commitHash1; // Set hash from storage key
commit2.commit.hash = commitHash2; // Set hash from storage key
commit1Prompts = commit1.commit.promptVersions;
commit2Prompts = commit2.commit.promptVersions;
}
// Calculate differences
const set1 = new Set(commit1Prompts);
const set2 = new Set(commit2Prompts);
const added = Array.from(set2).filter(hash => !set1.has(hash));
const removed = Array.from(set1).filter(hash => !set2.has(hash));
const modified = []; // For now, we don't track modifications
// Get details for each change
const details = [];
for (const hash of added) {
try {
const promptObj = await this.storage.loadObject(hash);
details.push({
hash,
status: 'added',
content: promptObj.promptVersion.content,
messages: promptObj.promptVersion.messages,
});
}
catch (error) {
details.push({
hash,
status: 'added',
});
}
}
for (const hash of removed) {
try {
const promptObj = await this.storage.loadObject(hash);
details.push({
hash,
status: 'removed',
content: promptObj.promptVersion.content,
messages: promptObj.promptVersion.messages,
});
}
catch (error) {
details.push({
hash,
status: 'removed',
});
}
}
return { added, removed, modified, details };
}
catch (error) {
throw new exceptions_1.StorageError(`Failed to calculate diff: ${error}`);
}
}
/**
* Show commit history.
*/
async log(limit = 10) {
if (!(await this.isRepo())) {
return []; // Return empty array for non-repos instead of throwing
}
try {
const commits = [];
const visited = new Set();
// Start from HEAD - resolve HEAD to actual commit hash
let currentHash;
try {
const headRef = await this.storage.loadRef('HEAD');
if (headRef.startsWith('refs/')) {
// HEAD points to a branch ref, resolve it to get the commit hash
currentHash = await this.storage.loadRef(headRef);
}
else {
// HEAD points directly to a commit
currentHash = headRef;
}
}
catch {
return []; // No commits yet
}
// Traverse commit history
while (commits.length < limit && currentHash && !visited.has(currentHash)) {
visited.add(currentHash);
const commitObj = await this.storage.loadObject(currentHash);
// Set the hash from the storage key (Git-like behavior)
commitObj.commit.hash = currentHash;
commits.push(commitObj.commit);
// Move to parent (we only follow first parent for now)
if (commitObj.commit.parentHashes.length > 0) {
currentHash = commitObj.commit.parentHashes[0];
}
else {
break;
}
}
return commits;
}
catch (error) {
throw new exceptions_1.StorageError(`Failed to get log: ${error}`);
}
}
/**
* Get the current branch name.
*/
async getCurrentBranch() {
// For now, we'll just return 'main' as the default branch
// In the future, we could implement proper branch tracking
return 'main';
}
/**
* Add a prompt to the staging area (Python-compatible approach).
*/
async addToStaging(promptHash, tag) {
const stagingRef = `staging/${tag}_${promptHash.substring(0, 8)}`;
await this.storage.storeRef(stagingRef, promptHash, 'staging');
}
/**
* Get all staged prompts (Python-compatible approach).
*/
async getStagedPrompts() {
const refs = await this.storage.listRefs();
const staged = [];
for (const ref of refs) {
if (ref.name.startsWith('staging/')) {
const tag = ref.name.substring(8).split('_')[0]; // Extract tag from ref name
staged.push([ref.hash, tag]);
}
}
return staged;
}
/**
* Get staging area references (legacy method for compatibility).
*/
async getStagingRefs() {
const staged = await this.getStagedPrompts();
return staged.map(([hash, _tag]) => hash);
}
/**
* Get repository status.
*/
async status() {
if (!(await this.isRepo())) {
throw new exceptions_1.RepoNotFoundError('Not a PVM repository. Run "pvm init" first.');
}
try {
const currentBranch = await this.getCurrentBranch();
const stagedChanges = (await this.getStagingRefs()).length;
let lastCommit;
try {
lastCommit = await this.storage.loadRef('HEAD');
}
catch {
// No commits yet
}
const stats = await this.storage.getStats();
return {
currentBranch,
stagedChanges,
lastCommit: lastCommit?.substring(0, 8),
stats,
};
}
catch (error) {
throw new exceptions_1.StorageError(`Failed to get status: ${error}`);
}
}
/**
* Clear the staging area (Python-compatible approach).
*/
async clearStaging() {
const refs = await this.storage.listRefs();
for (const ref of refs) {
if (ref.name.startsWith('staging/')) {
await this.storage.deleteRef(ref.name);
}
}
}
/**
* Close the versioning engine.
*/
async close() {
await this.storage.close();
}
}
exports.VersioningEngine = VersioningEngine;
//# sourceMappingURL=operations.js.map