UNPKG

pit-manager

Version:

Centralized prompt management system for Human Behavior AI agents

544 lines (528 loc) 21.1 kB
"use strict"; /** * Core versioning operations for PIT TypeScript. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.VersioningEngine = void 0; const fs = require("fs/promises"); const path = 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 { constructor(repoPath = '.pit') { this.repoPath = repoPath; this.storage = new engine_1.StorageEngine(repoPath); } /** * Initialize a new PIT 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 PIT. ## 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 PIT's simplified API author: PIT 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 PIT's simplified API in TypeScript. * This file was auto-generated by 'pit init'. */ import { prompts, model } from '@hb/pit-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 PIT repository in ${this.repoPath}`); } catch (error) { throw new exceptions_1.StorageError(`Failed to initialize repository: ${error}`); } } /** * Check if the current directory is a PIT 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 PIT repository. Run "pit 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 = 'PIT User', metadata = {}) { if (!(await this.isRepo())) { throw new exceptions_1.RepoNotFoundError('Not a PIT repository. Run "pit 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 PIT repository. Run "pit 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 { let headHash = await this.storage.loadRef('HEAD'); // Resolve symbolic ref if needed if (headHash.startsWith('refs/')) { headHash = await this.storage.loadRef(headHash); } const headCommit = await this.storage.loadObject(headHash); headCommit.commit.hash = headHash; // Set hash from storage key commit2Prompts = headCommit.commit.promptVersions; } catch { // No HEAD commit commit2Prompts = []; } } 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 PIT repository. Run "pit init" first.'); } try { const currentBranch = await this.getCurrentBranch(); const stagedChanges = (await this.getStagingRefs()).length; let lastCommit = undefined; try { const headRef = await this.storage.loadRef('HEAD'); // HEAD might be a symbolic ref (refs/heads/main) or a commit hash if (headRef.startsWith('refs/')) { // It's a symbolic ref, try to load the actual commit try { const branchCommit = await this.storage.loadRef(headRef); // Only set lastCommit if we actually got a commit hash (not another ref) if (branchCommit && !branchCommit.startsWith('refs/')) { lastCommit = branchCommit; } // If branchCommit is also a ref (shouldn't happen), leave lastCommit undefined } catch { // Branch doesn't exist yet (no commits) - leave lastCommit undefined } } else if (!headRef.startsWith('refs/')) { // It's a direct commit hash (not a ref) lastCommit = headRef; } // If headRef is somehow a ref but not starting with 'refs/', ignore it } catch { // No HEAD ref yet - leave lastCommit undefined } const stats = await this.storage.getStats(); // Ensure lastCommit is never a ref (defensive programming) if (lastCommit && lastCommit.startsWith('refs/')) { lastCommit = undefined; } 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;