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
132 lines
4.4 KiB
TypeScript
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");
|
|
}
|
|
},
|
|
});
|
|
}
|