Commit 992b2ec9f00a
dots/pi/agent/extensions/ai-storage/index.ts
@@ -4,12 +4,16 @@
* Unified storage for AI-generated artifacts across all AI coding tools.
* Auto-detects the tool being used (pi, claude, copilot, cursor, etc.)
*
- * Features:
+ * AI-invokable tools:
+ * - save_session_to_history: Save conversation summaries
+ * - save_research, save_plan, save_learning: Save various documents
+ * - list_saved_sessions: Search and list past sessions by date/content
+ * - read_saved_session: Read full session markdown content
+ *
+ * User commands:
* - /save-session: Generate and save conversation summaries
* - /session-log: View today's session activity
* - /list-sessions: Browse and search session history
- * - Automatic saving of research, plans, and learnings via AI tools
- * - Auto-logs session starts
*
* Storage locations (XDG-compliant):
* - Sessions: ~/.local/share/ai/sessions/YYYY-MM/*.md
@@ -450,6 +454,216 @@ export default function (pi: ExtensionAPI) {
},
});
+ // ========================================================================
+ // Shared session search helpers (used by both tools and commands)
+ // ========================================================================
+
+ interface SessionFile {
+ date: string;
+ file: string;
+ path: string;
+ desc: string;
+ }
+
+ /** Search sessions by content using ripgrep. Returns matching file paths. */
+ async function searchSessionsByQuery(query: string, limit: number = 20): Promise<SessionFile[]> {
+ const { execSync } = await import("node:child_process");
+ const rgResult = execSync(
+ `rg -l -i "${query.replace(/"/g, '\\"')}" "${SESSIONS_DIR}" 2>/dev/null || true`,
+ { encoding: "utf-8", maxBuffer: 1024 * 1024 }
+ ).trim();
+
+ if (!rgResult) return [];
+
+ return rgResult
+ .split("\n")
+ .filter(Boolean)
+ .slice(0, limit)
+ .map((filepath) => {
+ const filename = filepath.split("/").pop() || "";
+ const date = filename.slice(0, 10);
+ const desc = filename.slice(11).replace(".md", "").replace(/-/g, " ");
+ return { date, file: filename, path: filepath, desc };
+ });
+ }
+
+ /** List sessions by date range. Returns matching files sorted newest-first. */
+ async function listSessionsByDateRange(
+ dateRange: string,
+ limit: number = 20,
+ ): Promise<{ files: SessionFile[]; total: number; label: string } | null> {
+ const dateSpec = parseDateSpec(dateRange);
+ if (!dateSpec) return null;
+
+ const { start, end, label } = dateSpec;
+ const dates = getDatesInRange(start, end);
+
+ const allFiles: SessionFile[] = [];
+ const yearMonths = [...new Set(dates.map((d) => d.slice(0, 7)))];
+
+ for (const ym of yearMonths) {
+ const sessionDir = join(SESSIONS_DIR, ym);
+ if (!existsSync(sessionDir)) continue;
+
+ const dirFiles = await readdir(sessionDir);
+ for (const f of dirFiles) {
+ if (!f.endsWith(".md")) continue;
+ const fileDate = f.slice(0, 10);
+ if (dates.includes(fileDate)) {
+ const desc = f.slice(11).replace(".md", "").replace(/-/g, " ");
+ allFiles.push({ date: fileDate, file: f, path: join(sessionDir, f), desc });
+ }
+ }
+ }
+
+ allFiles.sort((a, b) => b.file.localeCompare(a.file));
+ return { files: allFiles.slice(0, limit), total: allFiles.length, label };
+ }
+
+ // Tool to list/search saved sessions
+ pi.registerTool({
+ name: "list_saved_sessions",
+ label: "List Saved Sessions",
+ description:
+ "Search and list saved session summaries from ~/.local/share/ai/sessions/. Use for finding past work by date or content. Sessions are curated markdown summaries with context and learnings.",
+ parameters: {
+ type: "object",
+ properties: {
+ query: {
+ type: "string",
+ description: "Optional search query to filter sessions by content (uses ripgrep). If omitted, lists recent sessions.",
+ },
+ dateRange: {
+ type: "string",
+ description: "Date range: 'today', 'yesterday', 'last week', 'last 7 days', 'YYYY-MM-DD', or 'YYYY-MM-DD..YYYY-MM-DD'. Default: 'last 7 days'",
+ },
+ limit: {
+ type: "number",
+ description: "Maximum number of sessions to return (default: 20)",
+ },
+ },
+ required: [],
+ },
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
+ try {
+ const { query, dateRange = "last 7 days", limit = 20 } = params;
+
+ if (query) {
+ const files = await searchSessionsByQuery(query, limit);
+ if (files.length === 0) {
+ return {
+ content: [{ type: "text", text: `No sessions found matching "${query}"` }],
+ details: { count: 0 },
+ };
+ }
+
+ const lines = [`Found ${files.length} session(s) matching "${query}":\n`];
+ for (const { date, desc, path } of files) {
+ lines.push(`- **${date}**: ${desc}`);
+ lines.push(` Path: ${path}`);
+ }
+ if (files.length === limit) lines.push(`\n(showing first ${limit} results)`);
+
+ return {
+ content: [{ type: "text", text: lines.join("\n") }],
+ details: { count: files.length, files: files.map(f => f.path) },
+ };
+ }
+
+ const result = await listSessionsByDateRange(dateRange, limit);
+ if (!result) {
+ return {
+ content: [{ type: "text", text: `Invalid date range: ${dateRange}. Use: today, yesterday, last week, last N days, YYYY-MM-DD, or YYYY-MM-DD..YYYY-MM-DD` }],
+ details: { error: "invalid_date_range" },
+ };
+ }
+
+ const { files, total, label } = result;
+ if (total === 0) {
+ return {
+ content: [{ type: "text", text: `No sessions found for ${label}` }],
+ details: { count: 0 },
+ };
+ }
+
+ const lines = [`Found ${total} session(s) for ${label}:\n`];
+ for (const { date, desc, path } of files) {
+ lines.push(`- **${date}**: ${desc}`);
+ lines.push(` Path: ${path}`);
+ }
+ if (total > limit) lines.push(`\n(showing ${limit} of ${total})`);
+
+ return {
+ content: [{ type: "text", text: lines.join("\n") }],
+ details: { count: total, files: files.map(f => f.path) },
+ };
+ } catch (error) {
+ return {
+ content: [{ type: "text", text: `Error listing sessions: ${error}` }],
+ details: { error: String(error) },
+ };
+ }
+ },
+ });
+
+ // Tool to read a saved session
+ pi.registerTool({
+ name: "read_saved_session",
+ label: "Read Saved Session",
+ description:
+ "Read the full content of a saved session summary. Use the file path from list_saved_sessions.",
+ parameters: {
+ type: "object",
+ properties: {
+ path: {
+ type: "string",
+ description: "Full file path to the session markdown file (from list_saved_sessions)",
+ },
+ },
+ required: ["path"],
+ },
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
+ try {
+ const { path: filepath } = params;
+
+ if (!existsSync(filepath)) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Session file not found: ${filepath}`,
+ },
+ ],
+ details: { error: "file_not_found" },
+ };
+ }
+
+ const content = await readFile(filepath, "utf-8");
+ const filename = filepath.split("/").pop() || "";
+
+ return {
+ content: [
+ {
+ type: "text",
+ text: `# ${filename}\n\n${content}`,
+ },
+ ],
+ details: { path: filepath },
+ };
+ } catch (error) {
+ return {
+ content: [
+ {
+ type: "text",
+ text: `Error reading session: ${error}`,
+ },
+ ],
+ details: { error: String(error) },
+ };
+ }
+ },
+ });
+
// Log session start, reset session tracking, and check for pending transcripts
pi.on("session_start", async (_event, ctx) => {
try {
@@ -759,12 +973,30 @@ After generating the summary, use the save_session_to_history tool to save it${c
return dates;
}
- // Register /list-sessions command
+ // Register /list-sessions command (uses shared helpers)
pi.registerCommand("list-sessions", {
description: "List sessions. Usage: /list-sessions [today|yesterday|last week|last N days|YYYY-MM-DD|YYYY-MM-DD..YYYY-MM-DD|search <query>]",
handler: async (args, ctx) => {
const argStr = (args || "").trim();
+ /** Format session files as markdown for the custom renderer */
+ function formatSessionList(title: string, files: SessionFile[], total: number, limit: number): string {
+ const lines: string[] = [];
+ lines.push(`## ๐ ${title}`);
+ lines.push("");
+ lines.push(`*${total} result(s)*`);
+ lines.push("");
+ for (const { date, desc, path } of files) {
+ lines.push(`- **${date}** ${desc}`);
+ lines.push(` \`${path}\``);
+ }
+ if (total > limit) {
+ lines.push("");
+ lines.push(`*(showing ${limit} of ${total})*`);
+ }
+ return lines.join("\n");
+ }
+
// Check for search mode
if (argStr.toLowerCase().startsWith("search ")) {
const query = argStr.slice(7).trim();
@@ -774,43 +1006,14 @@ After generating the summary, use the save_session_to_history tool to save it${c
}
try {
- const { execSync } = await import("node:child_process");
- // Use ripgrep to search sessions
- const rgResult = execSync(
- `rg -l -i "${query.replace(/"/g, '\\"')}" "${SESSIONS_DIR}" 2>/dev/null || true`,
- { encoding: "utf-8", maxBuffer: 1024 * 1024 }
- ).trim();
-
- if (!rgResult) {
+ const files = await searchSessionsByQuery(query, 20);
+ if (files.length === 0) {
ctx.ui.notify(`No sessions found matching "${query}"`, "info");
return;
}
-
- const files = rgResult.split("\n").filter(Boolean).slice(0, 20); // Limit to 20 results
-
- // Build markdown content
- const lines: string[] = [];
- lines.push(`## ๐ Sessions matching "${query}"`);
- lines.push("");
- lines.push(`*${files.length} result(s)*`);
- lines.push("");
-
- for (const filepath of files) {
- const filename = filepath.split("/").pop() || "";
- const date = filename.slice(0, 10);
- const desc = filename.slice(11).replace(".md", "").replace(/-/g, " ");
- lines.push(`- **${date}** ${desc}`);
- lines.push(` \`${filepath}\``);
- }
-
- if (files.length === 20) {
- lines.push("");
- lines.push("*(showing first 20 results)*");
- }
-
pi.sendMessage({
customType: "ai-storage-sessions",
- content: lines.join("\n"),
+ content: formatSessionList(`Sessions matching "${query}"`, files, files.length, 20),
display: true,
});
} catch (error) {
@@ -819,73 +1022,26 @@ After generating the summary, use the save_session_to_history tool to save it${c
return;
}
- // Parse date spec (default to today)
- const dateSpec = parseDateSpec(argStr || "today");
- if (!dateSpec) {
- ctx.ui.notify(
- "Invalid date. Use: today, yesterday, last week, last N days, YYYY-MM-DD, or YYYY-MM-DD..YYYY-MM-DD",
- "error"
- );
- return;
- }
-
- const { start, end, label } = dateSpec;
- const dates = getDatesInRange(start, end);
-
+ // Date-based listing (default to today)
try {
- const { readdir } = await import("node:fs/promises");
- const allFiles: { date: string; file: string; path: string }[] = [];
-
- // Get unique year-months to check
- const yearMonths = [...new Set(dates.map((d) => d.slice(0, 7)))];
-
- for (const ym of yearMonths) {
- const sessionDir = join(SESSIONS_DIR, ym);
- if (!existsSync(sessionDir)) continue;
-
- const files = await readdir(sessionDir);
- for (const f of files) {
- if (!f.endsWith(".md")) continue;
- const fileDate = f.slice(0, 10);
- if (dates.includes(fileDate)) {
- allFiles.push({
- date: fileDate,
- file: f,
- path: join(sessionDir, f),
- });
- }
- }
+ const result = await listSessionsByDateRange(argStr || "today", 30);
+ if (!result) {
+ ctx.ui.notify(
+ "Invalid date. Use: today, yesterday, last week, last N days, YYYY-MM-DD, or YYYY-MM-DD..YYYY-MM-DD",
+ "error"
+ );
+ return;
}
- allFiles.sort((a, b) => b.file.localeCompare(a.file)); // Newest first
-
- if (allFiles.length === 0) {
+ const { files, total, label } = result;
+ if (total === 0) {
ctx.ui.notify(`No sessions found for ${label}`, "info");
return;
}
- // Build markdown content
- const lines: string[] = [];
- lines.push(`## ๐ Sessions - ${label}`);
- lines.push("");
- lines.push(`*${allFiles.length} session(s)*`);
- lines.push("");
-
- const displayFiles = allFiles.slice(0, 30);
- for (const { date, file, path } of displayFiles) {
- const desc = file.slice(11).replace(".md", "").replace(/-/g, " ");
- lines.push(`- **${date}** ${desc}`);
- lines.push(` \`${path}\``);
- }
-
- if (allFiles.length > 30) {
- lines.push("");
- lines.push(`*(showing 30 of ${allFiles.length})*`);
- }
-
pi.sendMessage({
customType: "ai-storage-sessions",
- content: lines.join("\n"),
+ content: formatSessionList(`Sessions - ${label}`, files, total, 30),
display: true,
});
} catch (error) {
dots/pi/agent/AGENTS.md
@@ -34,6 +34,16 @@ Skills from `~/.config/claude/skills/` are compatible with pi. Key skills:
- **Analysis vs Action:** If asked to analyze, do analysis only
- **Org files:** NEVER edit `.org` files directly โ use `emacsclient` or `org_todo` tool
+## Session History and Search
+
+When users refer to past work, choose the appropriate tool:
+
+- **Saved sessions** (past tense: "yesterday's session", "last week's work"): Use `list_saved_sessions` and `read_saved_session` tools to search curated markdown summaries in `~/.local/share/ai/sessions/`. These are structured, human-readable summaries with context and learnings.
+
+- **Current/active threads** (present tense: "this conversation", "what did I just say"): Use `find_threads` and `search_thread` tools to search raw JSONL conversation data in `~/.pi/agent/sessions/`. Useful for debugging or finding exact conversation details.
+
+Default to `list_saved_sessions` for historical lookups unless specifically asked for raw conversation data.
+
## Response Patterns
1. **Understand**: Clarify the task and requirements