Project Files
src / toolsProvider.ts
// src/toolsProvider.ts
import { text, tool, type Tool, type ToolsProviderController } from "@lmstudio/sdk";
import { spawn } from "child_process";
import { rm, writeFile, readFile, mkdir, cp, rename } from "fs/promises";
import { join, normalize, dirname } from "path";
import { z } from "zod";
import { existsSync } from "fs";
import ExcelJS from "exceljs";
/* ---------- Helper Functions (not registered as tools) ---------- */
async function ensureDirectory(dirPath: string) {
if (!existsSync(dirPath)) {
await mkdir(dirPath, { recursive: true });
}
}
async function runCommand(options: {
cmd: string;
args?: string[];
cwd?: string;
timeoutMs?: number;
stageLabel?: string;
}): Promise<{ success: boolean; stdout: string; stderr: string; code: number }> {
const { cmd, args = [], cwd = process.cwd(), timeoutMs = 0, stageLabel } = options;
return await new Promise<{ success: boolean; stdout: string; stderr: string; code: number }>((resolve) => {
const child = spawn(cmd, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
let stdout = '';
let stderr = '';
child.stdout?.on('data', (d) => (stdout += d));
child.stderr?.on('data', (d) => (stderr += d));
let killedByTimeout = false;
let timer: NodeJS.Timeout | undefined;
if (timeoutMs && timeoutMs > 0) {
timer = setTimeout(() => {
killedByTimeout = true;
try { child.kill(); } catch {}
}, timeoutMs);
}
child.on('close', async (code) => {
if (timer) clearTimeout(timer);
const timestamp = new Date().toISOString();
const header = stageLabel ? `--- ${stageLabel} ---` : `--- ${cmd} ${args.join(' ')} ---`;
const logEntry = `\n\n${header} ${timestamp}\nEXIT CODE: ${code}${killedByTimeout ? ' (timeout)' : ''}\nSTDOUT:\n${stdout}\nSTDERR:\n${stderr}\n`;
try {
const logDir = join(process.cwd(), 'copilot_unit');
await ensureDirectory(logDir);
await writeFile(join(logDir, 'execution_trace.md'), logEntry, { flag: 'a' });
} catch {}
const exitCode = typeof code === 'number' ? code : -1;
resolve({ success: exitCode === 0 && !killedByTimeout, stdout: stdout.trim(), stderr: stderr.trim(), code: exitCode });
});
child.on('error', async (err) => {
if (timer) clearTimeout(timer);
const timestamp = new Date().toISOString();
const header = stageLabel ? `--- ${stageLabel} ERROR ---` : `--- ${cmd} ERROR ---`;
const logEntry = `\n\n${header} ${timestamp}\nERROR: ${String(err)}\n`;
try {
const logDir = join(process.cwd(), 'copilot_unit');
await ensureDirectory(logDir);
await writeFile(join(logDir, 'execution_trace.md'), logEntry, { flag: 'a' });
} catch {}
resolve({ success: false, stdout: stdout.trim(), stderr: String(err), code: -1 });
});
});
}
/* ---------- Core File Operations ---------- */
export async function toolsProvider(ctl: ToolsProviderController): Promise<Tool[]> {
const tools: Tool[] = [];
// وضع غير آمن عبر متغير بيئة لتعطيل القيود
const UNSAFE = ["1","true","yes"].includes(String(process.env.COPILOT_UNIT_UNSAFE || "").toLowerCase());
// تثبيت الجذر على مجلد الإضافة: __dirname يشير إلى dist، نأخذ الأب ليكون newtools
const pluginRoot = normalize(join(__dirname, ".."));
// السماح بفرض الجذر عبر متغير بيئة COPILOT_UNIT_BASE إن تم ضبطه
const envBase = (process.env.COPILOT_UNIT_BASE || "").trim();
// مرشحات إضافية اعتماداً على manifest.json
const candidates: string[] = [];
try {
const wd = ctl.getWorkingDirectory();
if (wd && wd.trim().length > 0) candidates.push(normalize(wd));
} catch {}
const cwd = normalize(process.cwd());
candidates.push(cwd);
candidates.push(normalize(join(cwd, "..")));
let projectRoot = envBase ? normalize(envBase) : pluginRoot;
const manifestRoot = [pluginRoot, ...candidates].find(p => existsSync(join(p, "manifest.json")));
if (!envBase && manifestRoot) projectRoot = manifestRoot;
const targetBase = join(projectRoot, "copilot_unit");
await ensureDirectory(targetBase);
/* ----------------- writeFile ----------------- */
tools.push(tool({
name: "writeFile",
description: text`كتابة أو إنشاء ملف.`,
parameters: { file: z.string().min(1), content: z.string() },
implementation: async ({ file, content }) => {
const filePath = resolveInsideBase(targetBase, file, UNSAFE);
await ensureDirectory(join(filePath, ".."));
await writeFile(filePath, content, "utf-8");
return { success: true, message: `تم كتابة الملف ${file}` };
}
}));
/* ----------------- readFile ----------------- */
tools.push(tool({
name: "readFile",
description: text`قراءة محتوى ملف.`,
parameters: { file: z.string().min(1) },
implementation: async ({ file }) => {
const filePath = resolveInsideBase(targetBase, file, UNSAFE);
if (!existsSync(filePath)) return { success: false, error: `الملف ${file} غير موجود.` };
const content = await readFile(filePath, "utf-8");
return { success: true, content };
}
}));
/* ----------------- renameFile (fs.rename) ----------------- */
tools.push(tool({
name: "renameFile",
description: text`إعادة تسمية ملف.`,
parameters: { oldName: z.string().min(1), newName: z.string().min(1) },
implementation: async ({ oldName, newName }) => {
const oldPath = resolveInsideBase(targetBase, oldName, UNSAFE);
const newPath = resolveInsideBase(targetBase, newName, UNSAFE);
if (!existsSync(oldPath)) return { success: false, error: `الملف ${oldName} غير موجود.` };
await ensureDirectory(join(newPath, ".."));
await rename(oldPath, newPath);
return { success: true, message: `تمت إعادة التسمية إلى ${newName}` };
}
}));
/* ----------------- createFolder ----------------- */
tools.push(tool({
name: "createFolder",
description: text`إنشاء مجلد.`,
parameters: { folder: z.string().min(1) },
implementation: async ({ folder }) => {
const folderPath = resolveInsideBase(targetBase, folder, UNSAFE);
await mkdir(folderPath, { recursive: true });
return { success: true, message: `تم إنشاء المجلد ${folder}` };
}
}));
/* ----------------- deleteFolder ----------------- */
tools.push(tool({
name: "deleteFolder",
description: text`حذف مجلد.`,
parameters: { folder: z.string().min(1) },
implementation: async ({ folder }) => {
const folderPath = resolveInsideBase(targetBase, folder, UNSAFE);
if (!existsSync(folderPath)) return { success: false, error: `المجلد ${folder} غير موجود.` };
await rm(folderPath, { recursive: true, force: true });
return { success: true, message: `تم حذف المجلد ${folder}` };
}
}));
/* ----------------- deleteFile (new tool) ----------------- */
tools.push(tool({
name: "deleteFile",
description: text`حذف ملف معين.`,
parameters: { file: z.string().min(1) },
implementation: async ({ file }) => {
const filePath = resolveInsideBase(targetBase, file, UNSAFE);
if (!existsSync(filePath)) return { success: false, error: `الملف ${file} غير موجود.` };
await rm(filePath, { force: true });
return { success: true, message: `تم حذف الملف ${file}` };
}
}));
/* ----------------- getCurrentTime (new tool) ----------------- */
tools.push(tool({
name: "getCurrentTime",
description: text`إحضار الوقت الحالي بالتنسيق المحلي.`,
parameters: {},
implementation: async () => {
const now = new Date();
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
return { success: true, time: now.toLocaleTimeString(), iso: now.toISOString(), timezone };
}
}));
/* ----------------- getCurrentDate (new tool) ----------------- */
tools.push(tool({
name: "getCurrentDate",
description: text`إحضار التاريخ الحالي بالتنسيق المحلي.`,
parameters: {},
implementation: async () => {
const today = new Date();
const isoDate = today.toISOString().slice(0, 10);
return { success: true, date: today.toLocaleDateString(), isoDate };
}
}));
/* ----------------- listDirectoryStructure (new tool) ----------------- */
tools.push(tool({
name: "listDirectoryStructure",
description: text`عرض بنية مجلد العمل مع أسماء الملفات والمجلدات وأحجامها.`,
parameters: { folder: z.string().default("") },
implementation: async ({ folder }) => {
const dirPath = resolveInsideBase(targetBase, folder || "");
if (!existsSync(dirPath)) return { success: false, error: `المجلد ${folder || "[الجذر]"} غير موجود.` };
const { readdir, stat } = await import("fs/promises");
const entries = await readdir(dirPath, { withFileTypes: true });
const structure = await Promise.all(entries.map(async (entry) => {
const fullPath = join(dirPath, entry.name);
const stats = await stat(fullPath);
return {
name: entry.name,
type: entry.isDirectory() ? "مجلد" : "ملف",
size: entry.isDirectory() ? "-" : `${(stats.size / 1024).toFixed(2)} KB`,
modified: stats.mtime.toISOString()
};
}));
return { success: true, folder: folder || "[الجذر]", items: structure };
}
}));
/* ----------------- summarizeWithModel (existing) ----------------- */
tools.push(tool({
name: "summarizeWithModel",
description: text`تلخيص إدراكي باستخدام النموذج.`,
parameters: { filename: z.string().min(1).regex(/^[^\/\\$+]+$/) },
implementation: async ({ filename }) => {
const filePath = resolveInsideBase(targetBase, filename.endsWith(".txt") ? filename : `${filename}.txt`);
if (!existsSync(filePath)) return { success: false, error: `الملف ${filename} غير موجود.` };
const content = await readFile(filePath, "utf-8");
const summary = content
.split(/\n+/)
.slice(0, 3)
.join(" ")
.slice(0, 2000);
return { success: true, summary };
}
}));
/* ----------------- markAwareness (existing) ----------------- */
tools.push(tool({
name: "markAwareness",
description: text`توثيق لحظة إدراك.`,
parameters: { insight: z.string().min(10) },
implementation: async ({ insight }) => {
const filePath = join(targetBase, "model_reflection.md");
const entry = `\n### لحظة إدراك\n- ${new Date().toISOString()}\n- ${insight}\n`;
await writeFile(filePath, entry, { flag: "a" });
return { success: true, message: "تم تسجيل الإدراك." };
}
}));
/* ----------------- suggestNextAction (existing) ----------------- */
tools.push(tool({
name: "suggestNextAction",
description: text`اقتراح خطوة إدراكية تالية.`,
parameters: { last_tool: z.string().min(1), context: z.string().min(5) },
implementation: async ({ last_tool, context }) => {
let suggestion = "لا يوجد اقتراح واضح.";
if (last_tool === "summarizeWithModel") {
suggestion = "هل ترغب في تسجيل هذا التلخيص في سجل الإدراك؟";
}
const timestamp = new Date().toISOString();
const entry = `\n\n--- ${timestamp} ---\nالأداة السابقة: ${last_tool}\nالسياق: ${context}\nالاقتراح: ${suggestion}\n`;
const filePath = join(targetBase, "next_action_suggestions.md");
await writeFile(filePath, entry, { flag: "a" });
return { success: true, suggestion, path: filePath, timestamp };
}
}));
/* ----------------- generateTrace (existing) ----------------- */
tools.push(tool({
name: "generateTrace",
description: text`توليد سجل تنفيذ حيّ.`,
parameters: { input: z.string().min(5) },
implementation: async ({ input }) => {
const tracePath = join(targetBase, "execution_trace.md");
const timestamp = new Date().toISOString();
const entry = `\n\n--- ${timestamp} ---\n${input}\n`;
await writeFile(tracePath, entry, { flag: 'a' });
return { success: true, message: "تم حفظ السجل", path: tracePath };
}
}));
/* ----------------- logToolUsage (existing) ----------------- */
tools.push(tool({
name: "logToolUsage",
description: text`تسجيل استخدام أداة في tool_log.md.`,
parameters: {
tool_name: z.string().min(1),
context: z.string().min(5)
},
implementation: async ({ tool_name, context }) => {
const logPath = join(targetBase, "tool_log.md");
const timestamp = new Date().toISOString();
const entry = `\n\n--- ${timestamp} ---\nأداة: ${tool_name}\n${context}\n`;
await writeFile(logPath, entry, { flag: 'a' });
return { success: true, message: "تم تسجيل استخدام الأداة", path: logPath };
}
}));
/* ----------------- exportTextFile (external function) ----------------- */
tools.push(tool({
name: "exportTextFile",
description: text`تصدير ملف نصي باستخدام الدالة الخارجية.`,
parameters: { filename: z.string().min(1), content: z.string() },
implementation: async ({ filename, content }) => {
const filePath = resolveInsideBase(targetBase, filename.endsWith(".txt") ? filename : `${filename}.txt`);
await ensureDirectory(join(filePath, ".."));
await writeFile(filePath, content, "utf-8");
return { success: true, message: `تم تصدير الملف ${filename}` };
}
}));
/* ----------------- saveContextMemory ----------------- */
tools.push(tool({
name: "saveContextMemory",
description: text`حفظ إدخالات ذاكرة سياقية في ملف JSON هرمي داخل copilot_unit/memory/`,
parameters: {
namespace: z.string().min(1).default("default"),
entry: z.object({
role: z.string().default("system"),
content: z.string().min(1),
tags: z.array(z.string()).default([]),
metadata: z.record(z.any()).default({})
})
},
implementation: async ({ namespace, entry }) => {
const memDir = resolveInsideBase(targetBase, join("memory", namespace));
await mkdir(memDir, { recursive: true });
const memPath = join(memDir, "context_memory.json");
const prev = existsSync(memPath) ? JSON.parse(await readFile(memPath, "utf-8")) : { entries: [] };
const record = { id: Date.now().toString(36), timestamp: new Date().toISOString(), ...entry };
prev.entries.push(record);
await writeFile(memPath, JSON.stringify(prev, null, 2), "utf-8");
return { success: true, path: memPath, count: prev.entries.length };
}
}));
/* ----------------- loadContextMemory ----------------- */
tools.push(tool({
name: "loadContextMemory",
description: text`تحميل إدخالات الذاكرة السياقية من copilot_unit/memory/ مع مرشح اختياري بالعلامات`,
parameters: {
namespace: z.string().min(1).default("default"),
tag: z.string().optional()
},
implementation: async ({ namespace, tag }) => {
const memDir = resolveInsideBase(targetBase, join("memory", namespace));
const memPath = join(memDir, "context_memory.json");
if (!existsSync(memPath)) return { success: true, entries: [] };
const data = JSON.parse(await readFile(memPath, "utf-8"));
const entries = Array.isArray(data.entries) ? data.entries : [];
const filtered = tag ? entries.filter((e: any) => Array.isArray(e.tags) && e.tags.includes(tag)) : entries;
return { success: true, entries: filtered };
}
}));
tools.push(tool({
name: "readExcelSheet",
description: text`قراءة ورقة من ملف إكسيل داخل copilot_unit`,
parameters: {
file: z.string().min(1),
sheet: z.string().optional()
},
implementation: async ({ file, sheet }) => {
const filePath = resolveInsideBase(targetBase, file.endsWith('.xlsx') ? file : `${file}.xlsx`);
const wb = new ExcelJS.Workbook();
if (!existsSync(filePath)) {
return { success: false, error: `الملف غير موجود: ${file}` };
}
await wb.xlsx.readFile(filePath);
const ws = sheet ? wb.getWorksheet(sheet) : wb.worksheets[0];
if (!ws) return { success: false, error: `لا توجد ورقة مطابقة` };
const rows: any[] = [];
ws.eachRow((row) => {
const vals: any[] = [];
row.eachCell({ includeEmpty: true }, (cell) => {
vals.push(cell.value);
});
rows.push(vals);
});
return { success: true, rows, sheet: ws.name };
}
}));
tools.push(tool({
name: "appendExcelRow",
description: text`إضافة صف إلى ورقة إكسيل (إنشاء الملف/الورقة إن لم توجد)`,
parameters: {
file: z.string().min(1),
sheet: z.string().optional(),
row: z.array(z.union([z.string(), z.number(), z.boolean()])).min(1)
},
implementation: async ({ file, sheet, row }) => {
const filePath = resolveInsideBase(targetBase, file.endsWith('.xlsx') ? file : `${file}.xlsx`);
const wb = new ExcelJS.Workbook();
if (existsSync(filePath)) {
await wb.xlsx.readFile(filePath);
}
let ws = sheet ? wb.getWorksheet(sheet) : wb.worksheets[0];
if (!ws) {
ws = wb.addWorksheet(sheet || 'Sheet1');
}
ws.addRow(row);
await ensureDirectory(dirname(filePath));
await wb.xlsx.writeFile(filePath);
return { success: true, message: `تمت إضافة صف إلى ${ws.name}`, path: filePath };
}
}));
tools.push(tool({
name: "runJavaScript",
description: text`تنفيذ JavaScript عبر Node.js من نص أو ملف` ,
parameters: {
code: z.string().optional(),
file: z.string().optional(),
args: z.array(z.string()).optional(),
timeoutMs: z.number().int().min(0).max(300000).optional()
},
implementation: async ({ code, file, args, timeoutMs }) => {
const argv = Array.isArray(args) ? args : [];
let scriptPath: string | undefined;
let cwd = targetBase;
if (file && file.trim().length > 0) {
scriptPath = resolveInsideBase(targetBase, file, UNSAFE);
if (!existsSync(scriptPath)) {
return { success: false, error: `الملف غير موجود: ${file}` };
}
cwd = dirname(scriptPath);
} else if (code && code.trim().length > 0) {
// اكتب الكود إلى ملف مؤقت داخل copilot_unit/scripts لتفادي مشاكل الاقتباس في -e
const scriptsDir = join(targetBase, "scripts");
await ensureDirectory(scriptsDir);
scriptPath = join(scriptsDir, `run_tmp_${Date.now()}.js`);
await writeFile(scriptPath, code, "utf-8");
cwd = scriptsDir;
} else {
return { success: false, error: "يجب توفير إما code أو file" };
}
const res = await runCommand({
cmd: process.platform === "win32" ? "node" : "node",
args: scriptPath ? [scriptPath, ...argv] : [],
cwd,
timeoutMs,
stageLabel: "runJavaScript"
});
return {
success: !!res.success,
code: res.code,
stdout: res.stdout,
stderr: res.stderr,
cwd,
script: scriptPath
};
}
}));
return tools;
}
/* ---------- Helper Functions (not registered as tools) ---------- */
function resolveInsideBase(baseDir: string, rel: string, unsafe = false) {
if (unsafe) {
const isAbs = /^([A-Za-z]:)?[\\/]/.test(rel);
return normalize(isAbs ? rel : join(baseDir, rel || ""));
}
const baseNorm = normalize(baseDir);
const target = normalize(join(baseDir, rel || ""));
const b = baseNorm.toLowerCase();
const r = target.toLowerCase();
const bSep = b.endsWith("\\") || b.endsWith("/") ? b : b + "\\";
const inside = r === b || r.startsWith(bSep);
if (!inside) {
throw new Error("مسار غير مسموح: خروج خارج copilot_unit");
}
return target;
}