import * as fs from "fs"; import * as path from "path"; // ─── Extension Imports ───────────────────────────────────────────────────── import { run as runIntegrityGuard, } from "./extensions/integrity-guard"; import { run as runProgressTracker, } from "./extensions/progress-tracker"; // ─── Types ───────────────────────────────────────────────────────────────── export type { PiscesConfig } from "./extensions/lib/config"; export { loadConfig, validateConfig, deepMerge, getConfigSearchPaths } from "./extensions/lib/config"; import type { PiscesConfig } from "./extensions/lib/config"; import { loadConfig, getConfigSearchPaths } from "./extensions/lib/config"; export interface SessionState { startTime: Date; skillsUsed: string[]; topicsWorked: string[]; warningIssuedAt: number | null; } export interface SkillCallContext { skillName: string; userInput: string; sessionState: SessionState; } export interface StartupResult { contextInjection: string; config: PiscesConfig; } export interface SkillCallResult { proceed: boolean; injection: string; } export interface SessionEndResult { nudge: string | null; summary: string | null; } // ─── Package Metadata ────────────────────────────────────────────────────── export const PACKAGE_NAME = "pisces"; export const PACKAGE_VERSION = ((): string => { try { const pkg = JSON.parse( fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf-8") ) as { version: string }; return pkg.version; } catch { return "0.0.0"; } })(); export const SKILLS = ["attempt"] as const; export type SkillName = (typeof SKILLS)[number]; // ─── Session State ───────────────────────────────────────────────────────── export function createSessionState(): SessionState { return { startTime: new Date(), skillsUsed: [], topicsWorked: [], warningIssuedAt: null, }; } // ─── Lifecycle Hooks ──────────────────────────────────────────────────────── export function onLoad(): { ok: boolean; message: string } { const nodeVersion = process.version; const major = parseInt(nodeVersion.slice(1).split(".")[0], 10); if (major < 18) { return { ok: false, message: `Pisces requires Node.js >= 18. Current: ${nodeVersion}. Please upgrade.`, }; } const config = loadConfig(); const configSource = getConfigSearchPaths().find((p: string) => { try { return fs.existsSync(p); } catch { return false; } }) ?? "defaults"; return { ok: true, message: `Pisces ${PACKAGE_VERSION} loaded. Config: ${configSource}. Integrity guard: ${ config.integrity?.enabled !== false ? "on" : "off" }.`, }; } export function onStartup(state: SessionState): StartupResult { const config = loadConfig(); const injection = buildConfigInjection(config); // state is retained for future extensions that accumulate context void state; return { contextInjection: injection, config, }; } export function onDirectoryChange(_state: SessionState): { contextInjection: string; message: string | null; } { return { contextInjection: "", message: null }; } export function onSkillCall(context: SkillCallContext): SkillCallResult { const { skillName, userInput, sessionState } = context; const config = loadConfig(); if (!sessionState.skillsUsed.includes(skillName)) { sessionState.skillsUsed.push(skillName); } if (config.integrity?.enabled === false) { return { proceed: true, injection: "" }; } const guardResult = runIntegrityGuard({ skillName, userInput, strictness: config.integrity?.strictness }); return { proceed: true, injection: guardResult.inject, }; } export function onSessionEnd(state: SessionState): SessionEndResult { const config = loadConfig(); const durationMinutes = Math.round( (Date.now() - state.startTime.getTime()) / 60_000 ); const trackerResult = runProgressTracker({ sessionDurationMinutes: durationMinutes, skillsUsed: state.skillsUsed, topicsWorked: state.topicsWorked, }); return { nudge: config.productivity?.burnout_nudges !== false ? trackerResult.nudge : null, summary: config.productivity?.weekly_summary !== false ? trackerResult.weeklySummary : null, }; } export function onMidSession(state: SessionState): { warning: string | null } { const config = loadConfig(); const warnAt = config.productivity?.session_warning_minutes ?? 180; if (!config.productivity?.burnout_nudges) return { warning: null }; const elapsedMinutes = Math.round( (Date.now() - state.startTime.getTime()) / 60_000 ); if (elapsedMinutes >= warnAt && state.warningIssuedAt === null) { state.warningIssuedAt = elapsedMinutes; return { warning: `🐠 You've been at this for ${elapsedMinutes} minutes. Time for a short break — your brain will thank you.`, }; } return { warning: null }; } // ─── Context Injection Helpers ───────────────────────────────────────────── function buildConfigInjection(config: PiscesConfig): string { const hints: string[] = []; if (config.student?.name) { hints.push(`Student name: ${config.student.name}`); } if (config.student?.year_of_study) { const yearLabels: Record = { 1: "Freshman (1st year)", 2: "Sophomore (2nd year)", 3: "Junior (3rd year)", 4: "Senior (4th year)", 5: "Graduate (1st year)", 6: "Graduate (2nd year+)", }; hints.push( `Year of study: ${yearLabels[config.student.year_of_study] ?? config.student.year_of_study}` ); } if (config.explanations?.default_depth) { hints.push(`Explanation depth preference: ${config.explanations.default_depth}`); } if (!hints.length) return ""; return [ "", ...hints.map((h) => ``), ].join("\n"); } // ─── Public Utility API ──────────────────────────────────────────────────── export function isValidSkill(name: string): name is SkillName { return (SKILLS as readonly string[]).includes(name); } // ─── Workspace API (re-exported for packages/cli) ────────────────────────── export { findWorkspace, getWorkspaceState, syncWorkspaceState } from "./workspace-detector"; export function describe(): string { return [ `🐠 Pisces v${PACKAGE_VERSION}`, ` Domain-agnostic learning engine for Pi Coding Agent`, ``, ` Skills: /${SKILLS.join(", /")}`, ` Extensions: workspace-gate, integrity-guard, progress-tracker, input-revamp`, ` Config: ~/.pi/pisces.json`, ``, ` Run any skill with /, e.g. /attempt`, ].join("\n"); }