Forked from brdcastro/maestro
"use strict";
/**
* @file systemTools.ts
* System/misc tools: system info, clipboard, open file, preview HTML,
* read document, analyze project, create visual treatment, notification, database.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.createSystemTools = createSystemTools;
const child_process_1 = require("child_process");
const promises_1 = require("fs/promises");
const os = __importStar(require("os"));
const path_1 = require("path");
const sdk_1 = require("@lmstudio/sdk");
const zod_1 = require("zod");
const shared_1 = require("./shared");
const errorCodes_1 = require("./errorCodes");
function createSystemTools(ctx, config, limits) {
const tools = [];
const safePath = (p) => (0, shared_1.validatePath)(ctx.cwd, p, ctx.workspaceRoot);
// --- Read Document ---
tools.push((0, sdk_1.tool)({
name: "read_document",
description: "Read content from PDF or DOCX files.",
parameters: { file_path: zod_1.z.string() },
implementation: async ({ file_path }) => {
const fpath = safePath(file_path);
const ext = fpath.split('.').pop()?.toLowerCase();
try {
if (ext === 'pdf') {
if (typeof global.DOMMatrix === 'undefined') {
global.DOMMatrix = class DOMMatrix {
constructor(arg) {
this.a = 1;
this.b = 0;
this.c = 0;
this.d = 1;
this.e = 0;
this.f = 0;
if (Array.isArray(arg)) {
this.a = arg[0];
this.b = arg[1];
this.c = arg[2];
this.d = arg[3];
this.e = arg[4];
this.f = arg[5];
}
}
};
}
const { PDFParse } = require("pdf-parse");
const dataBuffer = await (0, promises_1.readFile)(fpath);
const parser = new PDFParse({ data: dataBuffer });
const textResult = await parser.getText();
const infoResult = await parser.getInfo();
await parser.destroy();
return { content: textResult.text, metadata: infoResult.info };
}
else if (ext === 'docx') {
const mammoth = await Promise.resolve().then(() => __importStar(require("mammoth")));
const result = await mammoth.extractRawText({ path: fpath });
return { content: result.value, messages: result.messages };
}
return (0, errorCodes_1.toolError)(errorCodes_1.UNSUPPORTED_FORMAT, "Unsupported document format. Use read_file for text files.");
}
catch (e) {
return (0, errorCodes_1.toolError)(errorCodes_1.IO_ERROR, `Failed to read document: ${e instanceof Error ? e.message : String(e)}`);
}
},
}));
// --- Analyze Project ---
tools.push((0, sdk_1.tool)({
name: "analyze_project",
description: "Run project-wide analysis (linting) to find errors and warnings.",
parameters: {},
implementation: async () => {
let command = "", type = "unknown";
try {
const pkg = JSON.parse(await (0, promises_1.readFile)((0, path_1.join)(ctx.cwd, "package.json"), "utf-8"));
if (pkg.scripts?.lint) {
command = "npm run lint";
type = "npm-script";
}
else if (pkg.devDependencies?.eslint || pkg.dependencies?.eslint) {
command = "npx eslint . --format json";
type = "eslint";
}
}
catch {
const entries = await (0, promises_1.readdir)(ctx.cwd);
if (entries.some(f => f.endsWith(".py"))) {
command = "pylint .";
type = "python-lint";
}
}
if (!command)
return (0, errorCodes_1.toolError)(errorCodes_1.LINTER_NOT_FOUND, "Could not detect a supported linter (ESLint script or Python).");
try {
const child = (0, child_process_1.spawn)(command, { shell: true, cwd: ctx.cwd, timeout: 60000 });
let stdout = "", stderr = "";
child.stdout.on("data", (d) => stdout += d);
child.stderr.on("data", (d) => stderr += d);
await new Promise(resolve => child.on("close", resolve));
const MAX_ANALYSIS = limits?.maxAnalysis ?? 3_000;
return { tool: command, type, report: (stdout + stderr).substring(0, MAX_ANALYSIS) };
}
catch (e) {
return (0, errorCodes_1.toolError)(errorCodes_1.EXEC_FAILED, `Analysis failed: ${e instanceof Error ? e.message : String(e)}`);
}
},
}));
// --- Notification ---
if (config.allowNotify) {
tools.push((0, sdk_1.tool)({
name: "send_notification",
description: "Send a system notification to the user.",
parameters: { title: zod_1.z.string(), message: zod_1.z.string() },
implementation: async ({ title, message }) => {
const notifier = await Promise.resolve().then(() => __importStar(require("node-notifier")));
notifier.notify({ title, message, sound: true, wait: false });
return { success: true, message: "Notification sent." };
},
}));
}
// --- Database ---
if (config.allowDb) {
tools.push((0, sdk_1.tool)({
name: "query_database",
description: "Execute a read-only query on a SQLite database file.",
parameters: { db_path: zod_1.z.string(), query: zod_1.z.string() },
implementation: async ({ db_path, query }) => {
const fpath = safePath(db_path);
if (/^\s*(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|REPLACE)\b/i.test(query))
return (0, errorCodes_1.toolError)(errorCodes_1.WRITE_BLOCKED, "Only SELECT/read queries are allowed for safety.");
try {
const Database = (await Promise.resolve().then(() => __importStar(require("better-sqlite3")))).default;
const db = new Database(fpath, { readonly: true });
const results = db.prepare(query).all();
db.close();
return { results };
}
catch (e) {
return (0, errorCodes_1.toolError)(errorCodes_1.EXEC_FAILED, `Database query failed: ${e instanceof Error ? e.message : String(e)}`);
}
},
}));
}
// --- System Info ---
tools.push((0, sdk_1.tool)({
name: "get_system_info",
description: "Get information about the system (OS, CPU, Memory).",
parameters: {},
implementation: async () => ({
platform: os.platform(), arch: os.arch(), release: os.release(), hostname: os.hostname(),
total_memory: os.totalmem(), free_memory: os.freemem(), cpus: os.cpus().length, node_version: process.version,
}),
}));
// --- Clipboard ---
tools.push((0, sdk_1.tool)({
name: "read_clipboard",
description: "Read text content from the system clipboard.",
parameters: {},
implementation: async () => {
let command = "", args = [];
if (process.platform === "win32") {
command = "powershell";
args = ["-command", "Get-Clipboard"];
}
else if (process.platform === "darwin") {
command = "pbpaste";
}
else {
command = "xclip";
args = ["-selection", "clipboard", "-o"];
}
return Promise.race([
new Promise(resolve => {
const child = (0, child_process_1.spawn)(command, args);
let output = "", error = "";
child.stdout.on("data", d => output += d.toString());
child.stderr.on("data", d => error += d.toString());
child.on("close", code => resolve(code === 0 ? { content: output.trim() } : { error: `Failed. Code: ${code}. ${error}` }));
child.on("error", err => resolve({ error: `Failed to spawn: ${err.message}` }));
}),
new Promise((_, rej) => setTimeout(() => rej(new Error("Clipboard timeout")), 5000)),
]).catch(err => ({ error: err.message }));
},
}));
tools.push((0, sdk_1.tool)({
name: "write_clipboard",
description: "Write text content to the system clipboard.",
parameters: { content: zod_1.z.string() },
implementation: async ({ content }) => {
let command = "", args = [], input = content;
if (process.platform === "win32") {
command = "powershell";
const b64 = Buffer.from(content, 'utf8').toString('base64');
args = ["-command", `$str = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${b64}')); Set-Clipboard -Value $str`];
input = "";
}
else if (process.platform === "darwin") {
command = "pbcopy";
}
else {
command = "xclip";
args = ["-selection", "clipboard", "-i"];
}
return Promise.race([
new Promise(resolve => {
const child = (0, child_process_1.spawn)(command, args, { stdio: ['pipe', 'ignore', 'pipe'] });
if (input && process.platform !== "win32") {
child.stdin.write(input);
child.stdin.end();
}
else {
child.stdin.end();
}
let error = "";
child.stderr.on("data", d => error += d.toString());
child.on("close", code => resolve(code === 0 ? { success: true } : { error: `Failed. Code: ${code}. ${error}` }));
child.on("error", err => resolve({ error: `Failed to spawn: ${err.message}` }));
}),
new Promise((_, rej) => setTimeout(() => rej(new Error("Clipboard timeout")), 5000)),
]).catch(err => ({ error: err.message }));
},
}));
// --- Open File ---
tools.push((0, sdk_1.tool)({
name: "open_file",
description: "Open a file or URL in the system's default application.",
parameters: { target: zod_1.z.string().describe("File path or URL") },
implementation: async ({ target }) => {
let targetToOpen = target;
if (!target.startsWith("http://") && !target.startsWith("https://"))
targetToOpen = safePath(target);
let command = "", args = [];
if (process.platform === "win32") {
command = "cmd";
args = ["/c", "start", "", targetToOpen];
}
else if (process.platform === "darwin") {
command = "open";
args = [targetToOpen];
}
else {
command = "xdg-open";
args = [targetToOpen];
}
const child = (0, child_process_1.spawn)(command, args, { stdio: 'ignore', detached: true });
child.unref();
return { success: true, message: `Opened ${targetToOpen}` };
},
}));
// --- Preview HTML ---
tools.push((0, sdk_1.tool)({
name: "preview_html",
description: (0, sdk_1.text) `
Preview an HTML file in the browser AND return a structural analysis.
Can accept raw HTML content or a file path to an existing HTML file.
Returns: section count, image references, duplicates, missing assets, and potential overflow warnings.
Use this to verify your HTML output without asking the user to check manually.
`,
parameters: {
html_content: zod_1.z.string().optional().describe("Raw HTML content to preview. If omitted, file_name is required."),
file_name: zod_1.z.string().optional().describe("Path to an existing HTML file, or filename for new content."),
},
implementation: async ({ html_content, file_name }) => {
let filePath;
let html;
if (!html_content && file_name) {
// Read existing file
filePath = safePath(file_name);
try {
html = await (0, promises_1.readFile)(filePath, "utf-8");
}
catch {
return (0, errorCodes_1.toolError)(errorCodes_1.FILE_NOT_FOUND, `File not found: ${filePath}`);
}
}
else if (html_content) {
const name = file_name || `preview_${Date.now()}.html`;
filePath = safePath(name);
await (0, promises_1.writeFile)(filePath, html_content, "utf-8");
html = html_content;
}
else {
return (0, errorCodes_1.toolError)(errorCodes_1.MISSING_PARAM, "Provide either html_content or file_name.");
}
// Open in browser
let command = "", args = [];
if (process.platform === "win32") {
command = "cmd";
args = ["/c", "start", "", filePath];
}
else if (process.platform === "darwin") {
command = "open";
args = [filePath];
}
else {
command = "xdg-open";
args = [filePath];
}
const child = (0, child_process_1.spawn)(command, args, { stdio: 'ignore', detached: true });
child.unref();
// Structural analysis
const analysis = {};
// Count sections/containers
const sectionMatches = html.match(/<(section|article|div[^>]*class[^>]*(?:slide|section|scene|page|hero))/gi);
analysis.sections = sectionMatches ? sectionMatches.length : 0;
// Extract all image references
const imgRefs = [];
const imgRegex = /(?:src|data-src|data-bg|background(?:-image)?)\s*[:=]\s*["']?(?:url\(["']?)?([^"');\s>]+\.(jpg|jpeg|png|gif|webp|svg|avif|bmp|tiff))/gi;
let m;
while ((m = imgRegex.exec(html)) !== null) {
if (!/^(https?:|data:)/i.test(m[1]))
imgRefs.push(m[1]);
}
// Detect duplicate images
const imgCounts = new Map();
for (const ref of imgRefs) {
imgCounts.set(ref, (imgCounts.get(ref) || 0) + 1);
}
const duplicateImgs = [...imgCounts.entries()].filter(([, c]) => c > 1).map(([f, c]) => `${f} (${c}x)`);
analysis.total_images = imgRefs.length;
analysis.unique_images = imgCounts.size;
analysis.duplicate_images = duplicateImgs.length > 0 ? duplicateImgs : "none";
// Check for missing image files
const baseDir = (0, path_1.dirname)(filePath);
const missingImgs = [];
for (const ref of imgCounts.keys()) {
try {
await (0, promises_1.stat)((0, path_1.resolve)(baseDir, ref));
}
catch {
missingImgs.push(ref);
}
}
analysis.missing_images = missingImgs.length > 0 ? missingImgs : "none";
// Text overflow risk — find text blocks > 500 chars inside elements
const textBlocks = html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "");
const betweenTags = textBlocks.match(/>([^<]{500,})</g);
analysis.long_text_blocks = betweenTags ? betweenTags.length : 0;
if (betweenTags && betweenTags.length > 0) {
analysis.overflow_warning = `${betweenTags.length} text block(s) exceed 500 chars — may overflow containers. Consider splitting into more sections.`;
}
// File size
analysis.file_size_kb = Math.round(Buffer.byteLength(html, "utf-8") / 1024 * 10) / 10;
return { success: true, path: filePath, message: "HTML preview launched in browser.", analysis };
},
}));
// --- Create Visual Treatment ---
tools.push((0, sdk_1.tool)({
name: "create_visual_treatment",
description: "Generate a visual treatment HTML template with editorial design. Creates a self-contained HTML file with hero sections, image galleries, and cinematic typography.",
parameters: {
title: zod_1.z.string().describe("Project title displayed as the main heading."),
subtitle: zod_1.z.string().optional().describe("Subtitle or tagline."),
scenes: zod_1.z.array(zod_1.z.object({
heading: zod_1.z.string(), subtitle: zod_1.z.string().optional().describe("Scene subtitle or tagline."),
text: zod_1.z.string(),
images: zod_1.z.array(zod_1.z.string()).optional().describe("Image filenames for this scene."),
})).describe("Array of scenes/sections to include."),
output_file: zod_1.z.string().optional().describe("Output filename. Default: treatment.html"),
dark_mode: zod_1.z.boolean().optional().describe("Use dark background theme. Default: true"),
},
implementation: async ({ title, subtitle, scenes, output_file, dark_mode }) => {
const isDark = dark_mode !== false;
const bg = isDark ? "#0a0a0a" : "#fafafa", fg = isDark ? "#f0f0f0" : "#1a1a1a";
const mutedFg = isDark ? "#888" : "#666", accent = isDark ? "#e0c97f" : "#8b6914";
const scenesHtml = scenes.map((scene, i) => {
const imgsHtml = (scene.images || []).map(img => ` <img src="${img}" alt="${scene.heading}" loading="lazy">`).join("\n");
const gallery = imgsHtml ? `\n <div class="gallery">\n${imgsHtml}\n </div>` : "";
const subtitleHtml = scene.subtitle ? `\n <p class="scene-subtitle">${scene.subtitle}</p>` : "";
return ` <section class="scene" data-index="${i + 1}">\n <span class="scene-number">${String(i + 1).padStart(2, "0")}</span>\n <h2>${scene.heading}</h2>${subtitleHtml}\n <p>${scene.text}</p>${gallery}\n </section>`;
}).join("\n\n");
const html = `<!DOCTYPE html>\n<html lang="pt-BR">\n<head>\n <meta charset="UTF-8">\n <meta name="viewport" content="width=device-width, initial-scale=1.0">\n <title>${title}</title>\n <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700;900&family=Inter:wght@300;400;600&display=swap" rel="stylesheet">\n <style>\n * { margin: 0; padding: 0; box-sizing: border-box; }\n body { background: ${bg}; color: ${fg}; font-family: 'Inter', system-ui, sans-serif; font-weight: 300; line-height: 1.8; }\n .hero { min-height: 100vh; display: flex; flex-direction: column; justify-content: center; align-items: center; text-align: center; padding: 4rem 2rem; }\n .hero h1 { font-family: 'Playfair Display', Georgia, serif; font-size: clamp(3rem, 8vw, 7rem); font-weight: 900; letter-spacing: -0.02em; line-height: 1.05; margin-bottom: 1rem; }\n .hero .subtitle { font-size: clamp(1rem, 2vw, 1.5rem); color: ${mutedFg}; letter-spacing: 0.3em; text-transform: uppercase; font-weight: 300; }\n .scene { padding: 6rem 2rem; max-width: 1200px; margin: 0 auto; opacity: 0; transform: translateY(40px); transition: opacity 0.8s ease, transform 0.8s ease; }\n .scene.visible { opacity: 1; transform: translateY(0); }\n .scene-number { font-family: 'Playfair Display', serif; font-size: 5rem; color: ${accent}; opacity: 0.3; display: block; margin-bottom: -1rem; }\n .scene h2 { font-family: 'Playfair Display', serif; font-size: clamp(1.8rem, 4vw, 3rem); margin-bottom: 0.5rem; font-weight: 700; }\n .scene-subtitle { font-size: 0.95rem; letter-spacing: 0.15em; text-transform: uppercase; color: ${accent}; margin-bottom: 1.5rem; font-weight: 400; }\n .scene p { font-size: 1.1rem; max-width: 65ch; color: ${mutedFg}; }\n .gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1rem; margin-top: 2rem; }\n .gallery img { width: 100%; height: 350px; object-fit: cover; border-radius: 4px; transition: transform 0.4s ease, filter 0.4s ease; filter: ${isDark ? "brightness(0.85) contrast(1.1)" : "none"}; }\n .gallery img:hover { transform: scale(1.03); filter: brightness(1); }\n @media (max-width: 768px) { .scene { padding: 3rem 1.5rem; } .gallery { grid-template-columns: 1fr; } .gallery img { height: 250px; } }\n </style>\n</head>\n<body>\n <header class="hero">\n <h1>${title}</h1>\n ${subtitle ? `<p class="subtitle">${subtitle}</p>` : ""}\n </header>\n\n${scenesHtml}\n\n <script>\n const observer = new IntersectionObserver((entries) => {\n entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });\n }, { threshold: 0.15 });\n document.querySelectorAll('.scene').forEach(s => observer.observe(s));\n </script>\n</body>\n</html>`;
const fileName = output_file || "treatment.html";
const filePath = safePath(fileName);
await (0, promises_1.writeFile)(filePath, html, "utf-8");
return { success: true, path: filePath, sections: scenes.length, message: `Visual treatment saved. Use preview_html or open_file to view it.` };
},
}));
return tools;
}
//# sourceMappingURL=systemTools.js.map