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
189 lines
6.2 KiB
TypeScript
189 lines
6.2 KiB
TypeScript
import type { ExtensionAPI } from "@jaeswift/jae-coding-agent";
|
|
import { existsSync, writeFileSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { Type } from "@sinclair/typebox";
|
|
|
|
const SKILLS_API = "https://skills.jaeswift.xyz/api/skills";
|
|
|
|
interface SkillInfo {
|
|
name: string;
|
|
description: string;
|
|
version: string;
|
|
author: string;
|
|
}
|
|
|
|
export default function (pi: ExtensionAPI) {
|
|
async function fetchJSON(url: string): Promise<any> {
|
|
const result = await pi.exec("curl", ["-s", "-f", url]);
|
|
if (result.code !== 0) {
|
|
throw new Error(`HTTP request failed: ${result.stderr}`);
|
|
}
|
|
return JSON.parse(result.stdout);
|
|
}
|
|
|
|
function getSkillsDir(ctx: any): string {
|
|
const dir = join(ctx.agentDir, "skills");
|
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
function listLocalSkills(ctx: any): string[] {
|
|
const dir = getSkillsDir(ctx);
|
|
try {
|
|
return readdirSync(dir).filter((f) => f.endsWith(".md") || existsSync(join(dir, f, "SKILL.md")));
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
// /search-skills command
|
|
pi.registerCommand("search-skills", {
|
|
description: "Search the JAE skill marketplace: /search-skills <query>",
|
|
handler: async (args, ctx) => {
|
|
const query = args.trim();
|
|
if (!query) {
|
|
ctx.ui.notify("Usage: /search-skills <query>", "warning");
|
|
return;
|
|
}
|
|
|
|
ctx.ui.notify(`\u{1F50D} Searching skills for: ${query}...`, "info");
|
|
|
|
try {
|
|
const skills = await fetchJSON(`${SKILLS_API}/search?q=${encodeURIComponent(query)}`);
|
|
if (!skills || skills.length === 0) {
|
|
ctx.ui.notify(`No skills found matching '${query}'.`, "info");
|
|
return;
|
|
}
|
|
const list = skills
|
|
.map((s: SkillInfo) => ` - ${s.name} (v${s.version}) by ${s.author}\n ${s.description}`)
|
|
.join("\n");
|
|
ctx.ui.notify(`\u{1F3EA} Skills matching '${query}':\n${list}`, "info");
|
|
} catch (err: any) {
|
|
ctx.ui.notify(`Failed to search skills: ${err.message}\nAPI: ${SKILLS_API}`, "error");
|
|
}
|
|
},
|
|
});
|
|
|
|
// /install-skill command
|
|
pi.registerCommand("install-skill", {
|
|
description: "Install a skill from the marketplace: /install-skill <name>",
|
|
handler: async (args, ctx) => {
|
|
const name = args.trim();
|
|
if (!name) {
|
|
ctx.ui.notify("Usage: /install-skill <name>", "warning");
|
|
return;
|
|
}
|
|
|
|
ctx.ui.notify(`\u{1F4E6} Installing skill: ${name}...`, "info");
|
|
|
|
try {
|
|
const skill = await fetchJSON(`${SKILLS_API}/${encodeURIComponent(name)}`);
|
|
if (!skill || !skill.content) {
|
|
ctx.ui.notify(`Skill '${name}' not found or has no content.`, "error");
|
|
return;
|
|
}
|
|
|
|
const skillsDir = getSkillsDir(ctx);
|
|
const skillDir = join(skillsDir, name);
|
|
if (!existsSync(skillDir)) mkdirSync(skillDir, { recursive: true });
|
|
|
|
writeFileSync(join(skillDir, "SKILL.md"), skill.content, "utf-8");
|
|
|
|
// Write any additional files
|
|
if (skill.files && typeof skill.files === "object") {
|
|
for (const [filename, content] of Object.entries(skill.files)) {
|
|
writeFileSync(join(skillDir, filename), content as string, "utf-8");
|
|
}
|
|
}
|
|
|
|
ctx.ui.notify(
|
|
`\u2705 Skill '${name}' installed to ${skillDir}\nRestart JAE to activate.`,
|
|
"info",
|
|
);
|
|
} catch (err: any) {
|
|
ctx.ui.notify(`Failed to install skill: ${err.message}`, "error");
|
|
}
|
|
},
|
|
});
|
|
|
|
// /publish-skill command
|
|
pi.registerCommand("publish-skill", {
|
|
description: "Publish a local skill to the marketplace: /publish-skill <name>",
|
|
handler: async (args, ctx) => {
|
|
const name = args.trim();
|
|
if (!name) {
|
|
ctx.ui.notify("Usage: /publish-skill <skill-dir-name>", "warning");
|
|
return;
|
|
}
|
|
|
|
const skillsDir = getSkillsDir(ctx);
|
|
const skillDir = join(skillsDir, name);
|
|
const skillMd = join(skillDir, "SKILL.md");
|
|
|
|
if (!existsSync(skillMd)) {
|
|
ctx.ui.notify(`Skill '${name}' not found locally at ${skillDir}`, "error");
|
|
return;
|
|
}
|
|
|
|
const content = readFileSync(skillMd, "utf-8");
|
|
|
|
// Collect extra files
|
|
const files: Record<string, string> = {};
|
|
try {
|
|
for (const f of readdirSync(skillDir)) {
|
|
if (f !== "SKILL.md") {
|
|
files[f] = readFileSync(join(skillDir, f), "utf-8");
|
|
}
|
|
}
|
|
} catch { /* ignore */ }
|
|
|
|
ctx.ui.notify(`\u{1F4E4} Publishing skill: ${name}...`, "info");
|
|
|
|
try {
|
|
const payload = JSON.stringify({ name, content, files });
|
|
const result = await pi.exec(
|
|
"curl",
|
|
["-s", "-f", "-X", "POST", "-H", "Content-Type: application/json", "-d", payload, SKILLS_API],
|
|
);
|
|
if (result.code === 0) {
|
|
ctx.ui.notify(`\u2705 Skill '${name}' published to marketplace!`, "info");
|
|
} else {
|
|
ctx.ui.notify(`Publish failed: ${result.stderr}`, "error");
|
|
}
|
|
} catch (err: any) {
|
|
ctx.ui.notify(`Publish error: ${err.message}`, "error");
|
|
}
|
|
},
|
|
});
|
|
|
|
// /skills command - list local skills
|
|
pi.registerCommand("skills", {
|
|
description: "List locally installed skills",
|
|
handler: async (_args, ctx) => {
|
|
const local = listLocalSkills(ctx);
|
|
if (local.length === 0) {
|
|
ctx.ui.notify("No skills installed. Use /search-skills and /install-skill to get started.", "info");
|
|
return;
|
|
}
|
|
ctx.ui.notify(`\u{1F9E0} Installed Skills (${local.length}):\n${local.map((s) => ` - ${s}`).join("\n")}`, "info");
|
|
},
|
|
});
|
|
|
|
// LLM tool for skill search
|
|
pi.registerTool({
|
|
name: "search_skills",
|
|
label: "Search Skills",
|
|
description: "Search the JAE skill marketplace for installable skills",
|
|
parameters: Type.Object({
|
|
query: Type.String({ description: "Search query for skills" }),
|
|
}),
|
|
execute: async (args, _ctx) => {
|
|
try {
|
|
const skills = await fetchJSON(`${SKILLS_API}/search?q=${encodeURIComponent(args.query)}`);
|
|
return { output: JSON.stringify(skills, null, 2) };
|
|
} catch (err: any) {
|
|
return { error: `Skill search failed: ${err.message}` };
|
|
}
|
|
},
|
|
});
|
|
}
|