Agent-JAE/default-extensions/bookmarks.ts
jae a1b6e22c01 feat: add 16 default extensions for Agent JAE
New extensions shipping with Agent JAE:
- jae-rules: Per-project .jae/rules.md injection
- cost-tracker: Token/cost tracking with budget limits
- project-dna: Auto-analyze project fingerprint
- teach-mode: Pedagogical explanation mode
- bookmarks: File bookmark management
- replay: Session recording and asciicast export
- checkpoints: Git-based undo/time-travel
- pr-review: Autonomous PR diff review
- auto-docs: Auto-generate README and docs
- screenshot-context: Vision analysis for screenshots
- deploy: One-command deploy (Docker/Vercel/rsync/static)
- pair-programming: Real-time file watching with feedback
- dashboard: Terminal HUD with live stats
- swarm: Multi-agent parallel task execution
- skill-marketplace: Search/install/publish skills
- widget-api-demo: TUI widget API demonstration
2026-03-23 20:14:41 +01:00

132 lines
4.4 KiB
TypeScript

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 <name> <path>[:line]] | /bookmark list | /bookmark remove <name>",
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 <name> <path> 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 <name> <path>[: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 <name>", "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 <query>",
handler: async (args, ctx) => {
loadBookmarks(ctx.cwd);
const query = args.trim().toLowerCase();
if (!query) {
ctx.ui.notify("Usage: /search <query>", "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");
}
},
});
}