mcp-fs-shell-windows 0.2.20 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,227 @@
1
+ import { validatePath } from "../helpers/path.js";
2
+ import { launchBrowser, closeBrowserQuiet } from "./launcher.js";
3
+ import { closeSession, getSession, setSession } from "./session.js";
4
+ import { executeBrowserActions } from "./browserActions.js";
5
+ import { rankFuzzyMatches } from "./fuzzySearch.js";
6
+ // In-page candidate collection for fuzzy find (ported from the Beledarian
7
+ // reference): interactive + text nodes, deduped on (text||selector), capped
8
+ // at 400 candidates. Runs inside the page context.
9
+ const collectFuzzyCandidates = (page) => page.evaluate(() => {
10
+ const dedup = new Map();
11
+ const nodes = document.querySelectorAll("a,button,input,textarea,select,[role='button'],[aria-label],h1,h2,h3,h4,h5,h6,p,span");
12
+ const clean = (value) => value.replace(/\s+/g, " ").trim();
13
+ const classSelector = (el) => {
14
+ const classes = Array.from(el.classList).slice(0, 2).map(c => c.replace(/[^a-zA-Z0-9_-]/g, ""));
15
+ return classes.length > 0 ? `.${classes.join(".")}` : "";
16
+ };
17
+ const buildSelector = (el) => {
18
+ if (el.id)
19
+ return `#${el.id}`;
20
+ const name = el.getAttribute("name");
21
+ if (name)
22
+ return `${el.tagName.toLowerCase()}[name="${name.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"]`;
23
+ return `${el.tagName.toLowerCase()}${classSelector(el)}`;
24
+ };
25
+ for (const node of nodes) {
26
+ const element = node;
27
+ const text = clean(element.innerText ||
28
+ element.value ||
29
+ element.getAttribute("aria-label") ||
30
+ "");
31
+ if (!text)
32
+ continue;
33
+ const selector = buildSelector(element);
34
+ const key = `${text}||${selector}`;
35
+ if (!dedup.has(key)) {
36
+ dedup.set(key, { text: text.substring(0, 200), selector });
37
+ }
38
+ if (dedup.size >= 400)
39
+ break;
40
+ }
41
+ return Array.from(dedup.values());
42
+ });
43
+ // browser_session_open: launch (replacing any existing session), navigate,
44
+ // optionally wait for a selector, and optionally return full page text.
45
+ export async function handleBrowserSessionOpen(url, waitForSelector, includePageText) {
46
+ try {
47
+ if (getSession()) {
48
+ await closeSession();
49
+ }
50
+ const browser = await launchBrowser();
51
+ const page = await browser.newPage();
52
+ try {
53
+ await page.goto(url, { waitUntil: "networkidle0", timeout: 30000 });
54
+ if (waitForSelector) {
55
+ await page.waitForSelector(waitForSelector, { timeout: 15000 });
56
+ }
57
+ }
58
+ catch (error) {
59
+ await closeBrowserQuiet(browser);
60
+ throw error;
61
+ }
62
+ setSession({ browser, page, currentUrl: page.url() });
63
+ const pageText = includePageText !== false
64
+ ? await page.evaluate(() => document.body.innerText || "")
65
+ : undefined;
66
+ return JSON.stringify({
67
+ success: true,
68
+ session_active: true,
69
+ url: page.url(),
70
+ title: await page.title(),
71
+ text_content: pageText,
72
+ text_length: pageText ? pageText.length : 0,
73
+ message: "Browser session opened.",
74
+ });
75
+ }
76
+ catch (error) {
77
+ return JSON.stringify({
78
+ error: `Failed to open browser session: ${error instanceof Error ? error.message : String(error)}`,
79
+ });
80
+ }
81
+ }
82
+ // browser_session_control: run actions on the active page, optionally capture
83
+ // a screenshot, return page metadata (full text only on URL change or with
84
+ // full_read), and optionally fuzzy-find in-page text/selectors.
85
+ export async function handleBrowserSessionControl(args, allowedDirectories) {
86
+ const session = getSession();
87
+ if (!session) {
88
+ return JSON.stringify({
89
+ error: "No active browser session. Call 'browser_session_open' first.",
90
+ });
91
+ }
92
+ try {
93
+ const beforeUrl = session.page.url();
94
+ const actionLog = await executeBrowserActions(session.page, args.actions || []);
95
+ const afterUrl = session.page.url();
96
+ const urlChanged = beforeUrl !== afterUrl;
97
+ session.currentUrl = afterUrl;
98
+ let screenshotSaved = false;
99
+ if (args.screenshot_path) {
100
+ const screenshotFilePath = await validatePath(args.screenshot_path, allowedDirectories);
101
+ await session.page.screenshot({ path: screenshotFilePath, fullPage: args.full_page_screenshot ?? false });
102
+ screenshotSaved = true;
103
+ }
104
+ let pageSnapshot = undefined;
105
+ if (args.read_page !== false) {
106
+ const title = await session.page.title();
107
+ if (urlChanged || args.full_read) {
108
+ const textContent = await session.page.evaluate(() => document.body.innerText || "");
109
+ pageSnapshot = {
110
+ url: afterUrl,
111
+ title,
112
+ text_content: textContent,
113
+ text_length: textContent.length,
114
+ };
115
+ }
116
+ else {
117
+ pageSnapshot = {
118
+ url: afterUrl,
119
+ title,
120
+ note: "Full page text omitted (URL unchanged). Set full_read=true to force full output.",
121
+ };
122
+ }
123
+ }
124
+ let fuzzyResults = [];
125
+ if (typeof args.fuzzy_find === "string" && args.fuzzy_find.trim()) {
126
+ const query = args.fuzzy_find;
127
+ const candidates = await collectFuzzyCandidates(session.page);
128
+ const ranked = candidates
129
+ .map(candidate => ({
130
+ ...candidate,
131
+ score: Math.max(rankFuzzyMatches(query, [candidate.text], 1)[0]?.score ?? 0, rankFuzzyMatches(query, [candidate.selector], 1)[0]?.score ?? 0),
132
+ }))
133
+ .sort((a, b) => b.score - a.score)
134
+ .slice(0, args.max_results ?? 5);
135
+ fuzzyResults = ranked.map(item => ({
136
+ text: item.text,
137
+ selector: item.selector,
138
+ score: item.score,
139
+ }));
140
+ }
141
+ return JSON.stringify({
142
+ success: true,
143
+ session_active: true,
144
+ actions_executed: actionLog,
145
+ screenshot_saved: screenshotSaved,
146
+ url_changed: urlChanged,
147
+ url_change_notice: urlChanged ? `Url changed to -> [${afterUrl}]` : undefined,
148
+ page: pageSnapshot,
149
+ fuzzy_find_results: fuzzyResults,
150
+ });
151
+ }
152
+ catch (error) {
153
+ return JSON.stringify({
154
+ error: `Browser session control failed: ${error instanceof Error ? error.message : String(error)}`,
155
+ });
156
+ }
157
+ }
158
+ // browser_session_close: tear down the active session (idempotent).
159
+ export async function handleBrowserSessionClose() {
160
+ if (!getSession()) {
161
+ return JSON.stringify({
162
+ success: true,
163
+ session_active: false,
164
+ message: "No active browser session.",
165
+ });
166
+ }
167
+ try {
168
+ await closeSession();
169
+ return JSON.stringify({
170
+ success: true,
171
+ session_active: false,
172
+ message: "Browser session closed.",
173
+ });
174
+ }
175
+ catch (error) {
176
+ return JSON.stringify({
177
+ error: `Failed to close browser session: ${error instanceof Error ? error.message : String(error)}`,
178
+ });
179
+ }
180
+ }
181
+ // browser_open_page: stateless one-shot render (own browser instance, always
182
+ // closed on return; never touches the persistent session).
183
+ export async function handleBrowserOpenPage(args, allowedDirectories) {
184
+ let browser;
185
+ try {
186
+ browser = await launchBrowser();
187
+ const page = await browser.newPage();
188
+ try {
189
+ await page.goto(args.url, { waitUntil: "networkidle0", timeout: 30000 });
190
+ if (args.wait_for_selector) {
191
+ await page.waitForSelector(args.wait_for_selector, { timeout: 10000 });
192
+ }
193
+ const beforeActionUrl = page.url();
194
+ const actionLog = await executeBrowserActions(page, args.actions || []);
195
+ const currentUrl = page.url();
196
+ const urlChanged = currentUrl !== beforeActionUrl;
197
+ const title = await page.title();
198
+ const textContent = await page.evaluate(() => document.body.innerText || "");
199
+ let screenshotSaved = false;
200
+ if (args.screenshot_path) {
201
+ const screenshotFilePath = await validatePath(args.screenshot_path, allowedDirectories);
202
+ await page.screenshot({ path: screenshotFilePath, fullPage: args.full_page_screenshot ?? false });
203
+ screenshotSaved = true;
204
+ }
205
+ return JSON.stringify({
206
+ url: currentUrl,
207
+ title,
208
+ text_content: textContent.substring(0, 5000),
209
+ screenshot_saved: screenshotSaved,
210
+ actions_executed: actionLog,
211
+ url_changed: urlChanged,
212
+ url_change_notice: urlChanged ? `Url changed to -> [${currentUrl}]` : undefined,
213
+ });
214
+ }
215
+ finally {
216
+ await closeBrowserQuiet(browser);
217
+ }
218
+ }
219
+ catch (error) {
220
+ if (browser) {
221
+ await closeBrowserQuiet(browser);
222
+ }
223
+ return JSON.stringify({
224
+ error: `Browser operation failed: ${error instanceof Error ? error.message : String(error)}`,
225
+ });
226
+ }
227
+ }
@@ -0,0 +1,103 @@
1
+ import fs from "fs";
2
+ import nodePath from "path";
3
+ import os from "os";
4
+ // Chrome-for-Testing build 142.0.7444.175, preinstalled at
5
+ // C:\Users\Gerar\.cache\puppeteer (installed for the Beledarian browser tools;
6
+ // pairs with puppeteer 24.31). puppeteer-core is a pure-JS dependency (no
7
+ // browser download); ALL browser launches in this repo go through this single
8
+ // module so web_search's browser legs (duckduckgo-html / google / bing) and
9
+ // the browser_session_* / browser_open_page tools share one launcher:
10
+ //
11
+ // * getSharedBrowser / closeSharedBrowser - the web_search shared browser
12
+ // (module-level; one search call reuses a single instance across legs and
13
+ // each leg closes it afterwards).
14
+ // * launchBrowser - a fresh standalone instance (used by the persistent
15
+ // browser session and the stateless browser_open_page one-shot).
16
+ //
17
+ // The persistent session (browser/session.ts) deliberately holds its OWN
18
+ // browser instance so web_search legs can never close it mid-session.
19
+ //
20
+ // Executable resolution (v0.2.29 publish portability):
21
+ // 1. env MCP_CHROME_PATH (explicit; must exist)
22
+ // 2. the standard puppeteer cache <home>/.cache/puppeteer/chrome/<build>/,
23
+ // highest version wins (e.g. win64-142.0.7444.175/chrome-win64/chrome.exe)
24
+ // 3. the legacy hardcoded path (the reference machine's preinstall)
25
+ export const CHROME_PATH = "C:\\Users\\Gerar\\.cache\\puppeteer\\chrome\\win64-142.0.7444.175\\chrome-win64\\chrome.exe";
26
+ // Compare two "win64-<a.b.c.d>" build dirs numerically (ascending).
27
+ function compareBuildDirs(a, b) {
28
+ const pa = a.replace(/^win64-/, "").split(".").map((n) => parseInt(n, 10) || 0);
29
+ const pb = b.replace(/^win64-/, "").split(".").map((n) => parseInt(n, 10) || 0);
30
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
31
+ const d = (pb[i] || 0) - (pa[i] || 0);
32
+ if (d !== 0)
33
+ return d;
34
+ }
35
+ return 0;
36
+ }
37
+ // Resolve the Chrome-for-Testing executable (null when nothing is found).
38
+ export function resolveChromePath() {
39
+ const env = process.env.MCP_CHROME_PATH;
40
+ if (env && fs.existsSync(env))
41
+ return env;
42
+ const base = nodePath.join(os.homedir(), ".cache", "puppeteer", "chrome");
43
+ if (fs.existsSync(base)) {
44
+ const builds = fs
45
+ .readdirSync(base, { withFileTypes: true })
46
+ .filter((e) => e.isDirectory() && e.name.startsWith("win64-"))
47
+ .map((e) => e.name)
48
+ .sort(compareBuildDirs);
49
+ for (let i = builds.length - 1; i >= 0; i--) {
50
+ const p = nodePath.join(base, builds[i], "chrome-win64", "chrome.exe");
51
+ if (fs.existsSync(p))
52
+ return p;
53
+ }
54
+ }
55
+ if (fs.existsSync(CHROME_PATH))
56
+ return CHROME_PATH;
57
+ return null;
58
+ }
59
+ const LAUNCH_ARGS = ["--no-sandbox", "--disable-setuid-sandbox"];
60
+ // Launch a fresh headless browser with the standard options.
61
+ export const launchBrowser = async () => {
62
+ const puppeteer = await import("puppeteer-core");
63
+ const chromePath = resolveChromePath();
64
+ if (!chromePath) {
65
+ throw new Error("No Chrome-for-Testing executable found. Set MCP_CHROME_PATH to the path of chrome.exe, " +
66
+ `or install a build under ${nodePath.join(os.homedir(), ".cache", "puppeteer", "chrome")} ` +
67
+ "(standard layout: win64-<version>/chrome-win64/chrome.exe).");
68
+ }
69
+ return puppeteer.launch({
70
+ headless: true,
71
+ executablePath: chromePath,
72
+ args: LAUNCH_ARGS,
73
+ });
74
+ };
75
+ // Close a browser, swallowing already-closed errors.
76
+ export const closeBrowserQuiet = async (browser) => {
77
+ try {
78
+ await browser.close();
79
+ }
80
+ catch {
81
+ // already closed
82
+ }
83
+ };
84
+ let sharedBrowser = null;
85
+ // Shared browser used by the web_search browser legs. Lazily launched so a
86
+ // missing browser never breaks the non-puppeteer legs (errors surface per
87
+ // provider and are collected, not fatal).
88
+ export const getSharedBrowser = async () => {
89
+ if (!sharedBrowser) {
90
+ sharedBrowser = await launchBrowser();
91
+ }
92
+ return sharedBrowser;
93
+ };
94
+ // Close and drop the shared browser so a later provider can relaunch cleanly
95
+ // (the reference kept a stale closed instance; nulling here is strictly safer
96
+ // for manual multi-provider mode).
97
+ export const closeSharedBrowser = async () => {
98
+ if (sharedBrowser) {
99
+ const browser = sharedBrowser;
100
+ sharedBrowser = null;
101
+ await closeBrowserQuiet(browser);
102
+ }
103
+ };
@@ -0,0 +1,37 @@
1
+ import { z } from "zod";
2
+ // Shared actions schema: scripted browser steps executed in order on a page.
3
+ // (Ported from the Beledarian reference; per-action required fields are
4
+ // enforced at execution time by executeBrowserActions with clear errors.)
5
+ export const BrowserActionSchema = z.object({
6
+ type: z.enum(["wait_for_selector", "wait", "click", "type", "press", "select", "hover", "scroll", "evaluate"]),
7
+ selector: z.string().optional().describe("CSS selector used by selector-based actions."),
8
+ text: z.string().optional().describe("Text payload for type action."),
9
+ value: z.string().optional().describe("Value payload for select action."),
10
+ key: z.string().optional().describe("Keyboard key for press action (e.g., Enter, Tab)."),
11
+ milliseconds: z.number().int().min(0).max(30000).optional().describe("Delay in milliseconds for wait action."),
12
+ x: z.number().optional().describe("Horizontal scroll delta for scroll action."),
13
+ y: z.number().optional().describe("Vertical scroll delta for scroll action."),
14
+ script: z.string().optional().describe("JavaScript snippet for evaluate action (executed in page context)."),
15
+ });
16
+ export const BrowserSessionOpenArgsSchema = z.object({
17
+ url: z.string(),
18
+ wait_for_selector: z.string().optional().describe("Optional selector to wait for after navigation."),
19
+ include_page_text: z.boolean().optional().describe("If true (default), returns full page text content after opening."),
20
+ });
21
+ export const BrowserSessionControlArgsSchema = z.object({
22
+ actions: z.array(BrowserActionSchema).optional().describe("Optional scripted browser actions to execute on the active page."),
23
+ read_page: z.boolean().optional().describe("If true (default), returns page metadata. Full text is returned only on URL change or when full_read=true."),
24
+ full_read: z.boolean().optional().describe("If true, forces full page text output even when URL has not changed."),
25
+ screenshot_path: z.string().optional().describe("Optional screenshot output path (must be within an allowed directory)."),
26
+ full_page_screenshot: z.boolean().optional().describe("If true, captures full page screenshot."),
27
+ fuzzy_find: z.string().optional().describe("Optional fuzzy-find query for in-page content/selectors."),
28
+ max_results: z.number().int().min(1).max(20).optional().describe("Max fuzzy results to return (default: 5)."),
29
+ });
30
+ export const BrowserSessionCloseArgsSchema = z.object({});
31
+ export const BrowserOpenPageArgsSchema = z.object({
32
+ url: z.string(),
33
+ screenshot_path: z.string().optional().describe("Path to save a screenshot (e.g., 'screenshot.png'); must be within an allowed directory."),
34
+ wait_for_selector: z.string().optional().describe("CSS selector to wait for before returning."),
35
+ full_page_screenshot: z.boolean().optional().describe("If true, captures the full page when taking a screenshot."),
36
+ actions: z.array(BrowserActionSchema).optional().describe("Optional scripted browser steps to run after navigation."),
37
+ });
@@ -0,0 +1,14 @@
1
+ import { closeBrowserQuiet } from "./launcher.js";
2
+ let session = null;
3
+ export const getSession = () => session;
4
+ export const setSession = (s) => {
5
+ session = s;
6
+ };
7
+ // Close the active session's browser and drop the state. No-op when none.
8
+ export const closeSession = async () => {
9
+ if (session) {
10
+ const s = session;
11
+ session = null;
12
+ await closeBrowserQuiet(s.browser);
13
+ }
14
+ };
@@ -0,0 +1,254 @@
1
+ // compat/handler.ts — Beledarian-compatible alias handlers (MCP filesystem fork v0.2.28).
2
+ //
3
+ // Reference: .beledarians-llm-toolbox/dev-src/beledarians-lm-studio-tools/src/toolsProvider.ts.
4
+ // Every handler either delegates to the fork's own tool handler (so behavior and
5
+ // job-registry lifetime are identical) or reuses the fork's shell-layer spawn
6
+ // machinery (runCaptured + cap) with Beledarian's 60 s timeout cap, so existing
7
+ // prompts that pass timeout_seconds up to 60 behave exactly as under Beledarian.
8
+ //
9
+ // Deliberate deviations (documented per the task spec):
10
+ // * execute_command / run_python / run_javascript keep Beledarian's 60 s cap
11
+ // (the fork's native shell_* tools cap at 28 s). Same bounded synchronous
12
+ // semantics, same cmd.exe /d /c verbatim spawn path.
13
+ // * save_memory has NO enable gate (the fork has no such setting); the memory
14
+ // file path = env MCP_MEMORY_FILE if set, else the Beledarian workspace
15
+ // memory.md, created with a '# Long-Term Memory' header if missing.
16
+ // * run_javascript env = { ...process.env, NO_COLOR: "true" } (Beledarian's
17
+ // reference passed only NO_COLOR, which strips PATH; spreading process.env
18
+ // keeps the confined Deno spawn robust and matches run_javascript_free).
19
+ import fsp from "fs/promises";
20
+ import fs from "fs";
21
+ import nodePath from "path";
22
+ import crypto from "crypto";
23
+ import { cap, CAP_OK, CAP_ERR, SHELL_JOBS_DIR, ensureJobsDir, resolveCwd, runCaptured, handleShellTest, handleShellStart, handleShellCheck, handleShellCancel, handleShellTerminal, } from "../shell/handler.js";
24
+ import { resolveDenoPath } from "../run_javascript/handler.js";
25
+ import { COMPAT_SYNC_TIMEOUT_SEC } from "./schema.js";
26
+ // ---------------------------------------------------------------------------
27
+ // execute_command — bounded synchronous execution (alias of shell_run, 60 s cap)
28
+ // Mirrors handleShellRun exactly (cmd.exe /d /c, verbatim, stdin pipe) with
29
+ // Beledarian's 60 s cap and the same result shape: {command, exitCode, stdout,
30
+ // stderr, timedOut, duration_ms, cwd}; non-zero exit / timeout throw with the
31
+ // captured output embedded (callTool turns that into isError content).
32
+ // ---------------------------------------------------------------------------
33
+ export async function handleExecuteCommand(command, input, timeoutSeconds) {
34
+ const abs = await resolveCwd();
35
+ const timeout = Math.min(Math.max(timeoutSeconds ?? 5, 0.1), COMPAT_SYNC_TIMEOUT_SEC);
36
+ const started = Date.now();
37
+ const r = await runCaptured("cmd.exe", ["/d", "/c", command], {
38
+ cwd: abs,
39
+ input,
40
+ timeoutMs: timeout * 1000,
41
+ verbatim: true,
42
+ });
43
+ const durationMs = Date.now() - started;
44
+ if (r.spawnError) {
45
+ throw new Error(`Failed to launch command: ${r.spawnError}`);
46
+ }
47
+ if (r.timedOut) {
48
+ throw new Error(`Process timed out after ${timeout}s.\nSTDOUT:\n${cap(r.stdout, CAP_ERR)}\nSTDERR:\n${cap(r.stderr, CAP_ERR)}`);
49
+ }
50
+ if (r.code !== 0) {
51
+ throw new Error(`Process exited with code ${r.code ?? "unknown"}.\nSTDOUT:\n${cap(r.stdout, CAP_ERR)}\nSTDERR:\n${cap(r.stderr, CAP_ERR)}`);
52
+ }
53
+ return JSON.stringify({
54
+ command,
55
+ exitCode: 0,
56
+ stdout: cap(r.stdout, CAP_OK),
57
+ stderr: cap(r.stderr, CAP_OK),
58
+ timedOut: false,
59
+ duration_ms: durationMs,
60
+ cwd: abs,
61
+ });
62
+ }
63
+ // ---------------------------------------------------------------------------
64
+ // run_in_terminal — delegates to shell_terminal (visible cmd /k console window).
65
+ // Returns the shell_terminal job JSON (including the job id; kill via
66
+ // shell_cancel / cancel_background_command).
67
+ // ---------------------------------------------------------------------------
68
+ export async function handleRunInTerminal(command) {
69
+ return handleShellTerminal(command);
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // run_test_command — delegates to shell_test ({command, exit_code, stdout,
73
+ // stderr, passed}; never errors on a failing test; CI=true).
74
+ // ---------------------------------------------------------------------------
75
+ export async function handleRunTestCommand(command) {
76
+ return handleShellTest(command);
77
+ }
78
+ // ---------------------------------------------------------------------------
79
+ // run_background_command — delegates to shell_start. Job ids live in the fork's
80
+ // per-process registry (same lifetime semantics as Beledarian's per-process
81
+ // backgroundCommands Map). name is required (as in Beledarian); timeout_hours
82
+ // defaults to 10 (max 10).
83
+ // ---------------------------------------------------------------------------
84
+ export async function handleRunBackgroundCommand(command, name, timeoutHours, cwd) {
85
+ return handleShellStart(command, name, timeoutHours, cwd);
86
+ }
87
+ // ---------------------------------------------------------------------------
88
+ // check_background_command — delegates to shell_check.
89
+ // ---------------------------------------------------------------------------
90
+ export async function handleCheckBackgroundCommand(id) {
91
+ return handleShellCheck(id);
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // cancel_background_command — delegates to shell_cancel (kills the whole tree).
95
+ // ---------------------------------------------------------------------------
96
+ export async function handleCancelBackgroundCommand(id) {
97
+ return handleShellCancel(id);
98
+ }
99
+ // ---------------------------------------------------------------------------
100
+ // run_python — temp .py + system python (alias of shell_python, 60 s cap).
101
+ // Mirrors handleShellPython exactly (temp file in the shell-jobs dir, deleted
102
+ // in finally) with Beledarian's 60 s cap.
103
+ // ---------------------------------------------------------------------------
104
+ export async function handleRunPython(code, timeoutSeconds, cwd) {
105
+ const abs = await resolveCwd(cwd);
106
+ await ensureJobsDir();
107
+ const file = nodePath.join(SHELL_JOBS_DIR, `py-${Date.now().toString(36)}-${crypto.randomBytes(3).toString("hex")}.py`);
108
+ await fsp.writeFile(file, code, "utf-8");
109
+ const timeout = Math.min(Math.max(timeoutSeconds ?? 5, 0.1), COMPAT_SYNC_TIMEOUT_SEC);
110
+ const started = Date.now();
111
+ try {
112
+ const r = await runCaptured("python", [file], {
113
+ cwd: abs,
114
+ timeoutMs: timeout * 1000,
115
+ });
116
+ const durationMs = Date.now() - started;
117
+ if (r.spawnError) {
118
+ throw new Error(`Failed to launch python: ${r.spawnError}`);
119
+ }
120
+ if (r.timedOut) {
121
+ throw new Error(`Process timed out after ${timeout}s.\nSTDOUT:\n${cap(r.stdout, CAP_ERR)}\nSTDERR:\n${cap(r.stderr, CAP_ERR)}`);
122
+ }
123
+ if (r.code !== 0) {
124
+ throw new Error(`Process exited with code ${r.code ?? "unknown"}.\nSTDOUT:\n${cap(r.stdout, CAP_ERR)}\nSTDERR:\n${cap(r.stderr, CAP_ERR)}`);
125
+ }
126
+ return JSON.stringify({
127
+ exitCode: 0,
128
+ stdout: cap(r.stdout, CAP_OK),
129
+ stderr: cap(r.stderr, CAP_OK),
130
+ timedOut: false,
131
+ duration_ms: durationMs,
132
+ });
133
+ }
134
+ finally {
135
+ await fsp.unlink(file).catch(() => undefined);
136
+ }
137
+ }
138
+ // ---------------------------------------------------------------------------
139
+ // run_javascript — the CONFINED variant (replicates Beledarian reference
140
+ // L544-610; NOT an alias of run_javascript_free).
141
+ //
142
+ // Temp .ts written in the working directory (Beledarian's pattern, deleted in
143
+ // finally), Deno flags: --allow-read=. --allow-write=. --no-prompt --deny-net
144
+ // --deny-env --deny-sys --deny-run --deny-ffi (net/env/sys/run/ffi denied;
145
+ // no --allow-import, so external module imports are denied — "you cannot import
146
+ // external modules"). cwd defaults to the fork's shell_cwd default. Default
147
+ // timeout 5 s, max 60 s. Returns {stdout, stderr} (trimmed, per the reference).
148
+ // Non-zero exit throws the reference's error text: "Process exited with code N.
149
+ // Stderr: ...". Reuses the fork's Deno binary resolution (resolveDenoPath).
150
+ // ---------------------------------------------------------------------------
151
+ export async function handleRunJavascript(javascript, timeoutSeconds) {
152
+ const deno = resolveDenoPath();
153
+ if (!deno) {
154
+ throw new Error("No Deno runtime found. Set the DENO_PATH environment variable to a deno binary, " +
155
+ "install deno on PATH, or run this server from inside an LM Studio installation " +
156
+ "(which bundles deno at <home>/.internal/utils/deno.exe).");
157
+ }
158
+ const abs = await resolveCwd();
159
+ const file = nodePath.join(abs, `temp_script_${Date.now()}.ts`);
160
+ const timeout = Math.min(Math.max(timeoutSeconds ?? 5, 0.1), COMPAT_SYNC_TIMEOUT_SEC);
161
+ const started = Date.now();
162
+ try {
163
+ await fsp.writeFile(file, javascript, "utf-8");
164
+ const r = await runCaptured(deno, [
165
+ "run",
166
+ "--allow-read=.",
167
+ "--allow-write=.",
168
+ "--no-prompt",
169
+ "--deny-net",
170
+ "--deny-env",
171
+ "--deny-sys",
172
+ "--deny-run",
173
+ "--deny-ffi",
174
+ file,
175
+ ], {
176
+ cwd: abs,
177
+ timeoutMs: timeout * 1000,
178
+ env: { ...process.env, NO_COLOR: "true" },
179
+ });
180
+ const durationMs = Date.now() - started;
181
+ if (r.spawnError) {
182
+ throw new Error(`Failed to launch deno: ${r.spawnError}`);
183
+ }
184
+ if (r.timedOut) {
185
+ throw new Error(`Process timed out after ${timeout}s.\nSTDOUT:\n${cap(r.stdout, CAP_ERR)}\nSTDERR:\n${cap(r.stderr, CAP_ERR)}`);
186
+ }
187
+ if (r.code !== 0) {
188
+ // Reference error text (toolsProvider.ts L594).
189
+ throw new Error(`Process exited with code ${r.code ?? "unknown"}. Stderr: ${cap(r.stderr, CAP_ERR)}`);
190
+ }
191
+ return JSON.stringify({
192
+ stdout: r.stdout.trim(),
193
+ stderr: r.stderr.trim(),
194
+ duration_ms: durationMs,
195
+ cwd: abs,
196
+ });
197
+ }
198
+ finally {
199
+ // Always cleanup temp file, even on error (reference L606-609).
200
+ await fsp.unlink(file).catch(() => undefined);
201
+ }
202
+ }
203
+ // ---------------------------------------------------------------------------
204
+ // save_memory — STANDALONE (replicates Beledarian reference L509-542, minus
205
+ // the enable gate — the fork has no such setting).
206
+ //
207
+ // Appends "- [ISO timestamp] fact" to the memory file. Memory file path =
208
+ // env MCP_MEMORY_FILE if set, else the reference machine's workspace
209
+ // memory.md if present, else <cwd>/memory.md. If the file is missing it is
210
+ // created with a '# Long-Term Memory' header.
211
+ // Returns {success, message} on success, {error} on failure.
212
+ // ---------------------------------------------------------------------------
213
+ export function resolveMemoryFile() {
214
+ const env = process.env.MCP_MEMORY_FILE;
215
+ if (env && env.trim())
216
+ return env;
217
+ // Reference machine's workspace memory.md (kept for back-compat); on other
218
+ // machines the path does not exist, so fall back to the server's CWD.
219
+ const legacy = "C:\\Users\\Gerar\\.beledarians-llm-toolbox\\workspace\\memory.md";
220
+ if (fs.existsSync(legacy))
221
+ return legacy;
222
+ return nodePath.join(process.cwd(), "memory.md");
223
+ }
224
+ export async function handleSaveMemory(fact) {
225
+ const memoryFile = resolveMemoryFile();
226
+ const timestamp = new Date().toISOString();
227
+ const entry = `\n- [${timestamp}] ${fact}`;
228
+ try {
229
+ // Reference: append; on failure (e.g. missing file) create with header.
230
+ // On POSIX the reference's append fails for a missing file and the write
231
+ // path runs; on Windows appendFile would create the file headerless, so we
232
+ // check existence up front to keep the '# Long-Term Memory' header on all
233
+ // platforms.
234
+ let exists = false;
235
+ try {
236
+ await fsp.access(memoryFile);
237
+ exists = true;
238
+ }
239
+ catch {
240
+ exists = false;
241
+ }
242
+ if (exists) {
243
+ await fsp.appendFile(memoryFile, entry, "utf-8");
244
+ return JSON.stringify({ success: true, message: "Fact saved to memory." });
245
+ }
246
+ await fsp.writeFile(memoryFile, "# Long-Term Memory\n" + entry, "utf-8");
247
+ return JSON.stringify({ success: true, message: "Fact saved to memory (new file created)." });
248
+ }
249
+ catch (error) {
250
+ return JSON.stringify({
251
+ error: `Failed to save memory: ${error instanceof Error ? error.message : String(error)}`,
252
+ });
253
+ }
254
+ }