Commit 9cd49ff4e3ca

Vincent Demeester <vincent@sbr.pm>
2026-02-02 16:11:59
pi: add session-history extension for unified session saving
- Auto-detects tool (pi, claude, copilot, etc.) - /save-session command triggers AI to generate summary - save_session_to_history tool for persisting to disk - Logs session starts to ~/.config/claude/history/sessions/ - Compatible with Claude Code's session-manager plugin - Unified history across all AI coding tools
1 parent 0984010
Changed files (1)
dots
pi
agent
dots/pi/agent/extensions/session-history.ts
@@ -0,0 +1,253 @@
+/**
+ * Session History Extension for Pi
+ *
+ * Integrates pi with Claude Code's history system.
+ * Auto-detects the tool being used (pi, claude, copilot, etc.)
+ *
+ * Features:
+ * - /save-session: Triggers AI to generate and save session summary
+ * - /session-log: Shows today's session activity
+ * - Auto-logs session starts
+ *
+ * Compatible with Claude Code's hook system and history structure.
+ */
+
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import { writeFile, mkdir, appendFile, readFile } from "node:fs/promises";
+import { existsSync } from "node:fs";
+import { join } from "node:path";
+import { homedir, hostname } from "node:os";
+
+export default function (pi: ExtensionAPI) {
+	const HISTORY_DIR = join(homedir(), ".config", "claude", "history");
+	const SESSIONS_DIR = join(HISTORY_DIR, "sessions");
+
+	/**
+	 * Detect which tool is being used
+	 */
+	function detectTool(): string {
+		// Check environment variables
+		if (process.env.CLAUDE_PROJECT_DIR || process.env.CLAUDE_AGENT_TYPE) {
+			return "claude";
+		}
+		if (process.env.PI_VERSION || process.env.PI_PROJECT_DIR) {
+			return "pi";
+		}
+		// Check if running under specific tools
+		const execPath = process.argv0 || "";
+		if (execPath.includes("copilot")) {
+			return "copilot";
+		}
+		if (execPath.includes("cursor")) {
+			return "cursor";
+		}
+		if (execPath.includes("pi")) {
+			return "pi";
+		}
+		if (execPath.includes("claude")) {
+			return "claude";
+		}
+
+		// Default to pi since this is a pi extension
+		return "pi";
+	}
+
+	function getDateInfo() {
+		const now = new Date();
+		const year = now.getFullYear();
+		const month = String(now.getMonth() + 1).padStart(2, "0");
+		const day = String(now.getDate()).padStart(2, "0");
+		const hours = String(now.getHours()).padStart(2, "0");
+		const minutes = String(now.getMinutes()).padStart(2, "0");
+		const seconds = String(now.getSeconds()).padStart(2, "0");
+
+		return {
+			yearMonth: `${year}-${month}`,
+			date: `${year}-${month}-${day}`,
+			timestamp: `${year}-${month}-${day}T${hours}:${minutes}:${seconds}+01:00`,
+			time: `${hours}:${minutes}`,
+		};
+	}
+
+	async function logSessionStart() {
+		const tool = detectTool();
+		const { yearMonth, date, timestamp } = getDateInfo();
+		const sessionDir = join(SESSIONS_DIR, yearMonth);
+		const logFile = join(sessionDir, `${date}_session-log.txt`);
+
+		await mkdir(sessionDir, { recursive: true });
+		await appendFile(logFile, `${timestamp} - Session started (${tool})\n`);
+	}
+
+	async function appendToSessionLog(message: string) {
+		const { yearMonth, date, timestamp } = getDateInfo();
+		const sessionDir = join(SESSIONS_DIR, yearMonth);
+		const logFile = join(sessionDir, `${date}_session-log.txt`);
+
+		await mkdir(sessionDir, { recursive: true });
+		await appendFile(logFile, `${timestamp} - ${message}\n`);
+	}
+
+	// Internal command to actually save the session (called by AI after generating summary)
+	pi.registerTool({
+		name: "save_session_to_history",
+		label: "Save Session to History",
+		description:
+			"Saves a session summary to ~/.config/claude/history/sessions/. Works with any AI coding tool (pi, claude, copilot, cursor). Auto-detects which tool is being used.",
+		parameters: {
+			type: "object",
+			properties: {
+				description: {
+					type: "string",
+					description: "Brief description for the filename (e.g., 'added-hostname-extension')",
+				},
+				content: {
+					type: "string",
+					description: "Full markdown content following the Session Entry template from history-system.md",
+				},
+			},
+			required: ["description", "content"],
+		},
+		async execute(toolCallId, params, signal, onUpdate, ctx) {
+			try {
+				const { description, content } = params;
+				const { yearMonth, date, time } = getDateInfo();
+				const sessionDir = join(SESSIONS_DIR, yearMonth);
+
+				// Sanitize filename
+				const slug = description
+					.toLowerCase()
+					.replace(/[^a-z0-9]+/g, "-")
+					.replace(/^-+|-+$/g, "")
+					.substring(0, 60);
+
+				const filename = `${date}-${slug}.md`;
+				const filepath = join(sessionDir, filename);
+
+				// Create directory
+				await mkdir(sessionDir, { recursive: true });
+
+				// Write file
+				await writeFile(filepath, content, "utf-8");
+
+				// Append to session log
+				await appendToSessionLog(`Session saved: ${filename}`);
+
+				return {
+					content: [
+						{
+							type: "text",
+							text: `โœ“ Session saved to: ${filepath}`,
+						},
+					],
+					details: {},
+				};
+			} catch (error) {
+				return {
+					content: [
+						{
+							type: "text",
+							text: `Error saving session: ${error}`,
+						},
+					],
+					details: { error: String(error) },
+				};
+			}
+		},
+	});
+
+	// Log session start
+	pi.on("session_start", async (_event, ctx) => {
+		try {
+			await logSessionStart();
+			// Show hint
+			ctx.ui.setStatus("session", ctx.ui.theme.fg("dim", "๐Ÿ’พ /save-session"));
+		} catch (error) {
+			// Silent failure
+		}
+	});
+
+	// Register /save-session command
+	pi.registerCommand("save-session", {
+		description: "Generate and save a session summary to history (auto-detects tool)",
+		handler: async (_args, ctx) => {
+			const tool = detectTool();
+
+			// This triggers a message that prompts the AI to generate a summary
+			ctx.ui.notify("Generating session summary...", "info");
+
+			// Insert a prompt for the AI
+			const prompt = `Please generate a session summary for this conversation following the Session Entry template from the CORE skill's history-system.md.
+
+The summary should include:
+- A descriptive title
+- What was accomplished
+- Files that were changed
+- Commands that were run
+- The outcome
+- Any next steps
+
+Use the Session Entry template format:
+
+\`\`\`markdown
+# Session: <Description>
+
+**Date:** ${getDateInfo().date}
+**Time:** ${getDateInfo().time}
+**Host:** ${hostname()}
+**Tool:** ${tool}
+
+## Summary
+Brief description of what was accomplished.
+
+## What Was Accomplished
+- Task 1
+- Task 2
+
+## Files Changed
+- \`path/to/file\` - Description of change
+
+## Commands Run
+\`\`\`bash
+# Key commands executed
+\`\`\`
+
+## Outcome
+Result of the session.
+
+## Next Steps
+- [ ] TODO 1
+- [ ] TODO 2
+
+### Tags
+#${tool} #relevant-tags
+\`\`\`
+
+After generating the summary, use the save_session_to_history tool to save it with an appropriate filename description.`;
+
+			// Set the editor text to trigger the AI
+			ctx.ui.setEditorText(prompt);
+		},
+	});
+
+	// Register /session-log command
+	pi.registerCommand("session-log", {
+		description: "View today's session log",
+		handler: async (_args, ctx) => {
+			const { yearMonth, date } = getDateInfo();
+			const logFile = join(SESSIONS_DIR, yearMonth, `${date}_session-log.txt`);
+
+			if (!existsSync(logFile)) {
+				ctx.ui.notify("No session log for today", "info");
+				return;
+			}
+
+			try {
+				const content = await readFile(logFile, "utf-8");
+				console.log("\n๐Ÿ“‹ Session log:\n" + content);
+			} catch (error) {
+				ctx.ui.notify(`Error: ${error}`, "error");
+			}
+		},
+	});
+}