import type { ExtensionAPI } from "@jaeswift/jae-coding-agent"; import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { join } from "node:path"; interface Bookmark { name: string; path: string; line?: number; note?: string; timestamp: string; } export default function (pi: ExtensionAPI) { let bookmarks: Bookmark[] = []; function getBookmarksPath(cwd: string): string { return join(cwd, ".jae", "bookmarks.json"); } function loadBookmarks(cwd: string): void { const bPath = getBookmarksPath(cwd); if (existsSync(bPath)) { try { bookmarks = JSON.parse(readFileSync(bPath, "utf-8")); } catch { bookmarks = []; } } } function saveBookmarks(cwd: string): void { const jaeDir = join(cwd, ".jae"); if (!existsSync(jaeDir)) mkdirSync(jaeDir, { recursive: true }); writeFileSync(getBookmarksPath(cwd), JSON.stringify(bookmarks, null, 2), "utf-8"); } pi.on("session_start", async (_event, ctx) => { loadBookmarks(ctx.cwd); }); pi.registerCommand("bookmark", { description: "Manage bookmarks: /bookmark [add [:line]] | /bookmark list | /bookmark remove ", handler: async (args, ctx) => { loadBookmarks(ctx.cwd); const parts = args.trim().split(/\s+/); const subcommand = parts[0] || "list"; if (subcommand === "list" || subcommand === "ls") { if (bookmarks.length === 0) { ctx.ui.notify("No bookmarks saved. Use /bookmark add to add one.", "info"); return; } const list = bookmarks .map((b, i) => ` ${i + 1}. ${b.name} -> ${b.path}${b.line ? `:${b.line}` : ""}${b.note ? ` (${b.note})` : ""}`) .join("\n"); ctx.ui.notify(`\u{1F516} Bookmarks:\n${list}`, "info"); return; } if (subcommand === "add") { const name = parts[1]; const pathArg = parts[2]; const note = parts.slice(3).join(" ") || undefined; if (!name || !pathArg) { ctx.ui.notify("Usage: /bookmark add [:line] [note]", "warning"); return; } let filePath = pathArg; let line: number | undefined; if (pathArg.includes(":")) { const colonIdx = pathArg.lastIndexOf(":"); const maybeNum = parseInt(pathArg.substring(colonIdx + 1), 10); if (!isNaN(maybeNum)) { filePath = pathArg.substring(0, colonIdx); line = maybeNum; } } // Remove existing bookmark with same name bookmarks = bookmarks.filter((b) => b.name !== name); bookmarks.push({ name, path: filePath, line, note, timestamp: new Date().toISOString() }); saveBookmarks(ctx.cwd); ctx.ui.notify(`\u{1F516} Bookmark '${name}' saved: ${filePath}${line ? `:${line}` : ""}`, "info"); return; } if (subcommand === "remove" || subcommand === "rm" || subcommand === "delete") { const name = parts[1]; if (!name) { ctx.ui.notify("Usage: /bookmark remove ", "warning"); return; } const before = bookmarks.length; bookmarks = bookmarks.filter((b) => b.name !== name); if (bookmarks.length < before) { saveBookmarks(ctx.cwd); ctx.ui.notify(`\u{1F516} Bookmark '${name}' removed.`, "info"); } else { ctx.ui.notify(`Bookmark '${name}' not found.`, "warning"); } return; } ctx.ui.notify("Usage: /bookmark [add|list|remove] ...", "info"); }, }); pi.registerCommand("search", { description: "Search bookmarks: /search ", handler: async (args, ctx) => { loadBookmarks(ctx.cwd); const query = args.trim().toLowerCase(); if (!query) { ctx.ui.notify("Usage: /search ", "warning"); return; } const results = bookmarks.filter( (b) => b.name.toLowerCase().includes(query) || b.path.toLowerCase().includes(query) || (b.note && b.note.toLowerCase().includes(query)), ); if (results.length === 0) { ctx.ui.notify(`No bookmarks matching '${query}'.`, "info"); } else { const list = results .map((b) => ` - ${b.name} -> ${b.path}${b.line ? `:${b.line}` : ""}${b.note ? ` (${b.note})` : ""}`) .join("\n"); ctx.ui.notify(`\u{1F50D} Search results for '${query}':\n${list}`, "info"); } }, }); }