Download scripts/agentweb-mcp.js from Cnass/Claude: direct link, hf CLI and curl.
- Browser
- Download file 25.7 kB
-
https://huggingface.co/spaces/Cnass/Claude/resolve/main/scripts/agentweb-mcp.js
- Command line
-
hf download hf://spaces/Cnass/Claude/scripts/agentweb-mcp.js
-
curl -L -o agentweb-mcp.js https://huggingface.co/spaces/Cnass/Claude/resolve/main/scripts/agentweb-mcp.js
25.7 kB
| // Agent Web's own MCP server (stdio, newline-delimited JSON-RPC 2.0). | |
| // - share_file: hands files to the user; the web server sees the result and | |
| // shows them in the chat as download cards (see server/chat.js). | |
| // - screen tools: look at and drive the virtual X display (a browser, ...) | |
| // that the user watches in the "Ekran" tab. Only listed when the display is | |
| // on (the web server sets DISPLAY only then, see server/display.js): their | |
| // schemas and screenshots would only cost tokens otherwise. | |
| // No dependencies on purpose: it starts fast and needs nothing from npm. | |
| ; | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const os = require('os'); | |
| const readline = require('readline'); | |
| const { spawn } = require('child_process'); | |
| const SERVER_INFO = { name: 'agentweb', version: '1.0.0' }; | |
| const SCREEN = !!process.env.DISPLAY; | |
| const DISPLAY = process.env.DISPLAY || ':99'; | |
| const MAX_SHARE_FILES = 10; | |
| const MAX_SHARE_BYTES = 50 * 1024 * 1024; | |
| const MAX_SHARE_TOTAL = 100 * 1024 * 1024; | |
| const SYSTEM_PATH_RE = /^\/(proc|sys|dev)(\/|$)/; | |
| // Screenshots are scaled to fit WXGA; coordinates in every tool are in the | |
| // pixels of that (scaled) image and are mapped back to the real screen. | |
| const SHOT_MAX_W = 1280; | |
| const SHOT_MAX_H = 800; | |
| const MAX_PNG_BYTES = 3 * 1024 * 1024; | |
| const MAX_TYPE_CHARS = 5000; | |
| const LOG_DIR = path.join(os.tmpdir(), 'agentweb-logs'); | |
| const APPS = { | |
| browser: { title: 'Tarayıcı', command: 'agentweb-browser', args: [] }, | |
| }; | |
| const COORD = { type: 'integer', minimum: 0, description: 'Pixel in the screenshot image.' }; | |
| const TOOLS = [ | |
| { | |
| name: 'share_file', | |
| title: 'Dosya paylaş', | |
| description: | |
| 'Send files from this machine to the user: they appear in the Agent Web chat as download cards ' + | |
| '(images also get a preview). Use it whenever the user asks you to send, share or give them a file, ' + | |
| 'or when a result is better delivered as a file than as pasted text (long code, logs, CSV, documents, ' + | |
| 'archives, images). Write the file first, then call this with its path. Folders cannot be shared; ' + | |
| 'archive them first (zip/tar). At most 10 files, 50 MB each.', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| paths: { | |
| type: 'array', | |
| items: { type: 'string' }, | |
| minItems: 1, | |
| maxItems: MAX_SHARE_FILES, | |
| description: 'File paths, absolute or relative to the current working directory.', | |
| }, | |
| }, | |
| required: ['paths'], | |
| additionalProperties: false, | |
| }, | |
| annotations: { title: 'Dosya paylaş', readOnlyHint: true, openWorldHint: false }, | |
| }, | |
| { | |
| name: 'screenshot', | |
| title: 'Ekran görüntüsü', | |
| description: | |
| 'Screenshot of the virtual screen (X display) that the user sees in the "Ekran" tab, where GUI apps ' + | |
| 'such as the browser run. Every screen tool takes coordinates in the pixels of this image.', | |
| inputSchema: { type: 'object', properties: {}, additionalProperties: false }, | |
| annotations: { title: 'Ekran görüntüsü', readOnlyHint: true, openWorldHint: false }, | |
| }, | |
| { | |
| name: 'click', | |
| title: 'Tıkla', | |
| description: 'Move the mouse to (x, y) on the virtual screen and click.', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| x: COORD, | |
| y: COORD, | |
| button: { type: 'string', enum: ['left', 'right', 'middle'], default: 'left' }, | |
| double: { type: 'boolean', default: false, description: 'Double-click.' }, | |
| }, | |
| required: ['x', 'y'], | |
| additionalProperties: false, | |
| }, | |
| annotations: { title: 'Tıkla', readOnlyHint: false, destructiveHint: false, openWorldHint: false }, | |
| }, | |
| { | |
| name: 'drag', | |
| title: 'Sürükle', | |
| description: 'Press a mouse button at (from_x, from_y), move to (to_x, to_y) and release.', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| from_x: COORD, | |
| from_y: COORD, | |
| to_x: COORD, | |
| to_y: COORD, | |
| button: { type: 'string', enum: ['left', 'right', 'middle'], default: 'left' }, | |
| }, | |
| required: ['from_x', 'from_y', 'to_x', 'to_y'], | |
| additionalProperties: false, | |
| }, | |
| annotations: { title: 'Sürükle', readOnlyHint: false, destructiveHint: false, openWorldHint: false }, | |
| }, | |
| { | |
| name: 'type_text', | |
| title: 'Yaz', | |
| description: | |
| 'Enter text into the focused window of the virtual screen. ASCII text is typed key by key ("\\n" presses ' + | |
| 'Enter). Text with other characters (Turkish letters, emoji...) is pasted through the clipboard with Ctrl+V, ' + | |
| 'because typing those key by key is unreliable; method "type" or "paste" forces one way.', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| text: { type: 'string', minLength: 1, maxLength: MAX_TYPE_CHARS }, | |
| method: { type: 'string', enum: ['auto', 'type', 'paste'], default: 'auto' }, | |
| }, | |
| required: ['text'], | |
| additionalProperties: false, | |
| }, | |
| annotations: { title: 'Yaz', readOnlyHint: false, destructiveHint: false, openWorldHint: false }, | |
| }, | |
| { | |
| name: 'press_keys', | |
| title: 'Tuşa bas', | |
| description: | |
| 'Press keys on the virtual screen, xdotool style: "Return", "Escape", "Tab", "BackSpace", "Delete", ' + | |
| '"F5", "ctrl+s", "ctrl+shift+p", "alt+F4". Several space-separated combos are pressed in order.', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { keys: { type: 'string', minLength: 1, maxLength: 200 } }, | |
| required: ['keys'], | |
| additionalProperties: false, | |
| }, | |
| annotations: { title: 'Tuşa bas', readOnlyHint: false, destructiveHint: false, openWorldHint: false }, | |
| }, | |
| { | |
| name: 'scroll', | |
| title: 'Kaydır', | |
| description: 'Scroll the mouse wheel at (x, y) on the virtual screen.', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| x: COORD, | |
| y: COORD, | |
| direction: { type: 'string', enum: ['up', 'down', 'left', 'right'] }, | |
| amount: { type: 'integer', minimum: 1, maximum: 20, default: 3 }, | |
| }, | |
| required: ['x', 'y', 'direction'], | |
| additionalProperties: false, | |
| }, | |
| annotations: { title: 'Kaydır', readOnlyHint: false, destructiveHint: false, openWorldHint: false }, | |
| }, | |
| { | |
| name: 'list_windows', | |
| title: 'Pencereler', | |
| description: 'List the visible windows on the virtual screen: title, position and size (in screenshot pixels).', | |
| inputSchema: { type: 'object', properties: {}, additionalProperties: false }, | |
| annotations: { title: 'Pencereler', readOnlyHint: true, openWorldHint: false }, | |
| }, | |
| { | |
| name: 'focus_window', | |
| title: 'Pencereyi öne al', | |
| description: 'Bring the first visible window whose title contains the given text to the front and focus it.', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { title: { type: 'string', minLength: 1, maxLength: 200 } }, | |
| required: ['title'], | |
| additionalProperties: false, | |
| }, | |
| annotations: { title: 'Pencereyi öne al', readOnlyHint: false, destructiveHint: false, openWorldHint: false }, | |
| }, | |
| { | |
| name: 'launch_app', | |
| title: 'Uygulama başlat', | |
| description: | |
| 'Start a GUI app on the virtual screen: "browser" (Chrome, optionally opening url). ' + | |
| 'Check progress with screenshot.', | |
| inputSchema: { | |
| type: 'object', | |
| properties: { | |
| app: { type: 'string', enum: Object.keys(APPS) }, | |
| url: { type: 'string', maxLength: 2000, description: 'Only for "browser": an http(s) address to open.' }, | |
| }, | |
| required: ['app'], | |
| additionalProperties: false, | |
| }, | |
| annotations: { title: 'Uygulama başlat', readOnlyHint: false, destructiveHint: false, openWorldHint: true }, | |
| }, | |
| ]; | |
| class ToolError extends Error {} | |
| function formatBytes(n) { | |
| if (n < 1024) return `${n} B`; | |
| if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`; | |
| return `${(n / (1024 * 1024)).toFixed(1)} MB`; | |
| } | |
| function text(value) { | |
| return { type: 'text', text: value }; | |
| } | |
| function run(cmd, args, { input, timeout = 20000, maxBytes = 64 * 1024 * 1024 } = {}) { | |
| return new Promise((resolve, reject) => { | |
| let child; | |
| try { | |
| // xdotool decodes typed text with the locale; under C/POSIX it rejects | |
| // anything that is not ASCII (Turkish letters, emoji...). | |
| const env = { ...process.env, DISPLAY, LC_ALL: 'C.UTF-8' }; | |
| child = spawn(cmd, args, { env, stdio: ['pipe', 'pipe', 'pipe'] }); | |
| } catch (err) { | |
| reject(err); | |
| return; | |
| } | |
| const out = []; | |
| const errOut = []; | |
| let size = 0; | |
| let settled = false; | |
| const finish = (fn, value) => { | |
| if (settled) return; | |
| settled = true; | |
| clearTimeout(timer); | |
| fn(value); | |
| }; | |
| const timer = setTimeout(() => { | |
| child.kill('SIGKILL'); | |
| finish(reject, new ToolError(`${cmd} zaman aşımına uğradı.`)); | |
| }, timeout); | |
| child.stdout.on('data', (chunk) => { | |
| size += chunk.length; | |
| if (size > maxBytes) { | |
| child.kill('SIGKILL'); | |
| finish(reject, new ToolError(`${cmd} çıktısı çok büyük.`)); | |
| return; | |
| } | |
| out.push(chunk); | |
| }); | |
| child.stderr.on('data', (chunk) => { | |
| if (errOut.length < 50) errOut.push(chunk); | |
| }); | |
| child.on('error', (err) => { | |
| finish(reject, err.code === 'ENOENT' ? new ToolError(`${cmd} kurulu değil.`) : err); | |
| }); | |
| child.on('close', (code) => { | |
| const stderr = Buffer.concat(errOut).toString('utf8').trim(); | |
| if (code === 0) finish(resolve, { stdout: Buffer.concat(out), stderr }); | |
| else finish(reject, new ToolError(`${cmd} başarısız oldu (kod ${code})${stderr ? `: ${stderr.slice(0, 400)}` : ''}`)); | |
| }); | |
| child.stdin.on('error', () => {}); | |
| child.stdin.end(input || undefined); | |
| }); | |
| } | |
| function requireDisplay() { | |
| const match = /^:(\d+)/.exec(DISPLAY); | |
| if (match && !fs.existsSync(`/tmp/.X11-unix/X${match[1]}`)) { | |
| throw new ToolError( | |
| `Sanal ekran (${DISPLAY}) çalışmıyor. Kullanıcıdan Agent Web'de "Ekran" sekmesini açmasını isteyin; ekran orada başlatılır.` | |
| ); | |
| } | |
| } | |
| let geometryCache = null; | |
| async function screenGeometry() { | |
| if (geometryCache && Date.now() - geometryCache.at < 30000) return geometryCache; | |
| const { stdout } = await run('xdotool', ['getdisplaygeometry']); | |
| const [width, height] = stdout.toString().trim().split(/\s+/).map(Number); | |
| if (!(width > 0 && height > 0)) throw new ToolError('Ekran boyutu okunamadı.'); | |
| const scale = Math.min(1, SHOT_MAX_W / width, SHOT_MAX_H / height); | |
| geometryCache = { | |
| at: Date.now(), | |
| width, | |
| height, | |
| scale, | |
| shotWidth: Math.round(width * scale), | |
| shotHeight: Math.round(height * scale), | |
| }; | |
| return geometryCache; | |
| } | |
| async function toScreen(x, y) { | |
| const g = await screenGeometry(); | |
| for (const [name, v] of [['x', x], ['y', y]]) { | |
| if (!Number.isInteger(v) || v < 0) throw new ToolError(`${name} sıfır ya da pozitif bir tam sayı olmalı.`); | |
| } | |
| if (x >= g.shotWidth || y >= g.shotHeight) { | |
| throw new ToolError(`(${x}, ${y}) ekranın dışında; görüntü ${g.shotWidth}x${g.shotHeight} piksel.`); | |
| } | |
| return [Math.min(g.width - 1, Math.round(x / g.scale)), Math.min(g.height - 1, Math.round(y / g.scale))]; | |
| } | |
| function toShot(g, value) { | |
| return Math.round(value * g.scale); | |
| } | |
| const BUTTONS = { left: '1', middle: '2', right: '3' }; | |
| async function screenshot() { | |
| requireDisplay(); | |
| const g = await screenGeometry(); | |
| const resize = g.scale < 1 ? ['-resize', `${g.shotWidth}x${g.shotHeight}!`] : []; | |
| let image; | |
| try { | |
| image = (await run('import', ['-silent', '-window', 'root', ...resize, 'png:-'])).stdout; | |
| } catch (err) { | |
| if (!/kurulu değil/.test(err.message)) throw err; | |
| image = (await run('magick', ['import', '-silent', '-window', 'root', ...resize, 'png:-'])).stdout; | |
| } | |
| let mimeType = 'image/png'; | |
| if (image.length > MAX_PNG_BYTES) { | |
| try { | |
| image = (await run('convert', ['png:-', '-quality', '85', 'jpg:-'], { input: image })).stdout; | |
| mimeType = 'image/jpeg'; | |
| } catch { | |
| // PNG olarak kalsin | |
| } | |
| } | |
| return [ | |
| { type: 'image', data: image.toString('base64'), mimeType }, | |
| text( | |
| `Ekran ${g.width}x${g.height}` + | |
| (g.scale < 1 ? `, görüntü ${g.shotWidth}x${g.shotHeight} olarak küçültüldü` : '') + | |
| '. Araçlardaki koordinatlar bu görüntünün pikselleridir.' | |
| ), | |
| ]; | |
| } | |
| async function click(args) { | |
| requireDisplay(); | |
| const [x, y] = await toScreen(args.x, args.y); | |
| const button = BUTTONS[args.button || 'left']; | |
| if (!button) throw new ToolError('button left, right ya da middle olmalı.'); | |
| const repeat = args.double ? ['--repeat', '2', '--delay', '120'] : []; | |
| await run('xdotool', ['mousemove', '--sync', String(x), String(y), 'click', ...repeat, button]); | |
| return [text(`${args.double ? 'Çift tıklandı' : 'Tıklandı'}: (${args.x}, ${args.y}) ${args.button || 'left'}`)]; | |
| } | |
| async function drag(args) { | |
| requireDisplay(); | |
| const [x1, y1] = await toScreen(args.from_x, args.from_y); | |
| const [x2, y2] = await toScreen(args.to_x, args.to_y); | |
| const button = BUTTONS[args.button || 'left']; | |
| if (!button) throw new ToolError('button left, right ya da middle olmalı.'); | |
| const steps = []; | |
| for (let i = 1; i <= 10; i += 1) { | |
| steps.push('mousemove', String(Math.round(x1 + ((x2 - x1) * i) / 10)), String(Math.round(y1 + ((y2 - y1) * i) / 10)), 'sleep', '0.02'); | |
| } | |
| await run('xdotool', ['mousemove', '--sync', String(x1), String(y1), 'mousedown', button, ...steps, 'mouseup', button]); | |
| return [text(`Sürüklendi: (${args.from_x}, ${args.from_y}) → (${args.to_x}, ${args.to_y})`)]; | |
| } | |
| // xclip keeps serving the selection from a background copy of itself, so only | |
| // the foreground process is waited for (its pipes stay open in the copy). | |
| function setClipboard(value) { | |
| return new Promise((resolve, reject) => { | |
| let child; | |
| try { | |
| child = spawn('xclip', ['-selection', 'clipboard', '-i'], { | |
| env: { ...process.env, DISPLAY, LC_ALL: 'C.UTF-8' }, | |
| stdio: ['pipe', 'ignore', 'ignore'], | |
| }); | |
| } catch (err) { | |
| reject(err); | |
| return; | |
| } | |
| const timer = setTimeout(() => { | |
| child.kill('SIGKILL'); | |
| reject(new ToolError('xclip zaman aşımına uğradı.')); | |
| }, 5000); | |
| child.on('error', (err) => { | |
| clearTimeout(timer); | |
| reject(err.code === 'ENOENT' ? new ToolError('xclip kurulu değil.') : err); | |
| }); | |
| child.on('exit', (code) => { | |
| clearTimeout(timer); | |
| if (code === 0) resolve(); | |
| else reject(new ToolError(`xclip başarısız oldu (kod ${code}).`)); | |
| }); | |
| child.stdin.on('error', () => {}); | |
| child.stdin.end(value); | |
| }); | |
| } | |
| async function typeText(args) { | |
| requireDisplay(); | |
| const value = args.text; | |
| if (typeof value !== 'string' || !value) throw new ToolError('text boş olamaz.'); | |
| if (value.length > MAX_TYPE_CHARS) throw new ToolError(`En fazla ${MAX_TYPE_CHARS} karakter yazılabilir.`); | |
| const method = args.method || 'auto'; | |
| if (!['auto', 'type', 'paste'].includes(method)) throw new ToolError('method auto, type ya da paste olmalı.'); | |
| // xdotool can only type what the keyboard map has: for anything else it maps | |
| // a spare key for a moment and undoes it before the app reads the key. | |
| const plain = /^[\x20-\x7e\n\t]*$/.test(value); | |
| if (method === 'paste' || (method === 'auto' && !plain)) { | |
| try { | |
| await setClipboard(value); | |
| await new Promise((resolve) => setTimeout(resolve, 120)); | |
| await run('xdotool', ['key', '--clearmodifiers', 'ctrl+v']); | |
| return [text(`${value.length} karakter pano ile yapıştırıldı (Ctrl+V).`)]; | |
| } catch (err) { | |
| if (method === 'paste' || !/xclip kurulu değil/.test(err.message)) throw err; | |
| } | |
| } | |
| await run('xdotool', ['type', '--delay', '12', '--', value], { timeout: 120000 }); | |
| return [text(`${value.length} karakter yazıldı.`)]; | |
| } | |
| async function pressKeys(args) { | |
| requireDisplay(); | |
| const combos = String(args.keys || '').trim().split(/\s+/).filter(Boolean); | |
| if (!combos.length || combos.length > 20) throw new ToolError('keys 1-20 tuş kombinasyonu içermeli.'); | |
| for (const combo of combos) { | |
| if (!/^[A-Za-z0-9_]+(\+[A-Za-z0-9_]+)*$/.test(combo)) throw new ToolError(`Geçersiz tuş: ${combo}`); | |
| } | |
| await run('xdotool', ['key', '--delay', '60', '--', ...combos]); | |
| return [text(`Basıldı: ${combos.join(' ')}`)]; | |
| } | |
| async function scroll(args) { | |
| requireDisplay(); | |
| const [x, y] = await toScreen(args.x, args.y); | |
| const button = { up: '4', down: '5', left: '6', right: '7' }[args.direction]; | |
| if (!button) throw new ToolError('direction up, down, left ya da right olmalı.'); | |
| const amount = Number.isInteger(args.amount) ? Math.min(20, Math.max(1, args.amount)) : 3; | |
| await run('xdotool', ['mousemove', '--sync', String(x), String(y), 'click', '--repeat', String(amount), '--delay', '40', button]); | |
| return [text(`Kaydırıldı: ${args.direction} × ${amount}`)]; | |
| } | |
| async function visibleWindows() { | |
| let ids; | |
| try { | |
| ids = (await run('xdotool', ['search', '--onlyvisible', '--name', '.'])).stdout.toString().split(/\s+/).filter(Boolean); | |
| } catch (err) { | |
| // xdotool exits 1 when nothing matches. | |
| if (/kod 1\)/.test(err.message)) return []; | |
| throw err; | |
| } | |
| const g = await screenGeometry(); | |
| const windows = []; | |
| for (const id of ids.slice(0, 50)) { | |
| try { | |
| const name = (await run('xdotool', ['getwindowname', id])).stdout.toString().trim(); | |
| const geo = (await run('xdotool', ['getwindowgeometry', '--shell', id])).stdout.toString(); | |
| const v = (key) => Number((new RegExp(`^${key}=(-?\\d+)`, 'm').exec(geo) || [])[1] || 0); | |
| if (!name || v('WIDTH') < 2 || v('HEIGHT') < 2) continue; | |
| windows.push({ id, title: name, x: toShot(g, v('X')), y: toShot(g, v('Y')), width: toShot(g, v('WIDTH')), height: toShot(g, v('HEIGHT')) }); | |
| } catch { | |
| // pencere bu arada kapanmis olabilir | |
| } | |
| } | |
| return windows; | |
| } | |
| async function listWindows() { | |
| requireDisplay(); | |
| const windows = await visibleWindows(); | |
| if (!windows.length) return [text('Ekranda görünür pencere yok.')]; | |
| const lines = windows.map((w) => `- "${w.title}" konum (${w.x}, ${w.y}) boyut ${w.width}x${w.height}`); | |
| return [text(`Görünür pencereler:\n${lines.join('\n')}`)]; | |
| } | |
| async function focusWindow(args) { | |
| requireDisplay(); | |
| const wanted = String(args.title || '').toLowerCase(); | |
| if (!wanted) throw new ToolError('title boş olamaz.'); | |
| const match = (await visibleWindows()).find((w) => w.title.toLowerCase().includes(wanted)); | |
| if (!match) throw new ToolError(`Başlığında "${args.title}" geçen görünür pencere yok.`); | |
| await run('xdotool', ['windowactivate', '--sync', match.id]).catch(() => run('xdotool', ['windowfocus', match.id])); | |
| return [text(`Öne alındı: "${match.title}"`)]; | |
| } | |
| function findOnPath(command) { | |
| for (const dir of String(process.env.PATH || '').split(':')) { | |
| if (!dir) continue; | |
| const candidate = path.join(dir, command); | |
| try { | |
| fs.accessSync(candidate, fs.constants.X_OK); | |
| return candidate; | |
| } catch { | |
| // yok | |
| } | |
| } | |
| return null; | |
| } | |
| async function launchApp(args) { | |
| requireDisplay(); | |
| const app = Object.prototype.hasOwnProperty.call(APPS, args.app) ? APPS[args.app] : null; | |
| if (!app) throw new ToolError(`app şunlardan biri olmalı: ${Object.keys(APPS).join(', ')}`); | |
| const bin = findOnPath(app.command); | |
| if (!bin) throw new ToolError(`${app.title} bu kurulumda yok (${app.command} bulunamadı).`); | |
| const appArgs = [...app.args]; | |
| if (args.url !== undefined) { | |
| if (args.app !== 'browser') throw new ToolError('url sadece browser için kullanılabilir.'); | |
| if (!/^https?:\/\/\S+$/i.test(String(args.url))) throw new ToolError('url http:// ya da https:// ile başlamalı.'); | |
| appArgs.push(String(args.url)); | |
| } | |
| fs.mkdirSync(LOG_DIR, { recursive: true }); | |
| const logFile = path.join(LOG_DIR, `${args.app}.log`); | |
| const log = fs.openSync(logFile, 'a'); | |
| try { | |
| const child = spawn(bin, appArgs, { env: { ...process.env, DISPLAY }, stdio: ['ignore', log, log], detached: true }); | |
| child.on('error', () => {}); | |
| child.unref(); | |
| } finally { | |
| fs.closeSync(log); | |
| } | |
| return [text(`${app.title} başlatıldı. Durumu screenshot ile kontrol edin. Günlük: ${logFile}`)]; | |
| } | |
| function shareFile(args) { | |
| let raw = args.paths; | |
| if (typeof raw === 'string') raw = [raw]; | |
| if (!Array.isArray(raw) || !raw.length) throw new ToolError('paths en az bir dosya yolu içermeli.'); | |
| if (raw.length > MAX_SHARE_FILES) throw new ToolError(`Tek seferde en fazla ${MAX_SHARE_FILES} dosya paylaşılabilir.`); | |
| const files = []; | |
| let total = 0; | |
| for (const value of raw) { | |
| if (typeof value !== 'string' || !value.trim() || value.includes('\0')) throw new ToolError('Geçersiz dosya yolu.'); | |
| const expanded = value.startsWith('~/') ? path.join(os.homedir(), value.slice(2)) : value; | |
| const abs = path.resolve(process.cwd(), expanded); | |
| let stat; | |
| try { | |
| stat = fs.statSync(abs); | |
| } catch { | |
| throw new ToolError(`Dosya bulunamadı: ${value}`); | |
| } | |
| if (stat.isDirectory()) throw new ToolError(`${value} bir klasör; önce zip ya da tar ile arşivleyip arşivi paylaşın.`); | |
| if (!stat.isFile()) throw new ToolError(`${value} normal bir dosya değil.`); | |
| // /proc and friends look like regular files but hold live process state | |
| // (environment variables, memory maps) that should never leave the box. | |
| if (SYSTEM_PATH_RE.test(fs.realpathSync(abs))) throw new ToolError(`${value} bir sistem dosyası; paylaşılamaz.`); | |
| if (stat.size > MAX_SHARE_BYTES) throw new ToolError(`${value} çok büyük (${formatBytes(stat.size)}; en fazla 50 MB).`); | |
| try { | |
| fs.accessSync(abs, fs.constants.R_OK); | |
| } catch { | |
| throw new ToolError(`${value} okunamıyor (izin yok).`); | |
| } | |
| total += stat.size; | |
| if (total > MAX_SHARE_TOTAL) throw new ToolError('Dosyaların toplamı 100 MB\'ı aşıyor; daha az dosya paylaşın.'); | |
| files.push({ path: fs.realpathSync(abs), name: path.basename(abs), size: stat.size }); | |
| } | |
| const summary = files.map((f) => `${f.name} (${formatBytes(f.size)})`).join(', '); | |
| return [ | |
| text(`Paylaşıldı: ${summary}. Agent Web sohbetinde indirme kartı olarak görünür.`), | |
| text(JSON.stringify({ agentwebShare: files })), | |
| ]; | |
| } | |
| const HANDLERS = { | |
| share_file: shareFile, | |
| screenshot, | |
| click, | |
| drag, | |
| type_text: typeText, | |
| press_keys: pressKeys, | |
| scroll, | |
| list_windows: listWindows, | |
| focus_window: focusWindow, | |
| launch_app: launchApp, | |
| }; | |
| function send(message) { | |
| process.stdout.write(`${JSON.stringify(message)}\n`); | |
| } | |
| function listedTools() { | |
| return SCREEN ? TOOLS : TOOLS.filter((tool) => tool.name === 'share_file'); | |
| } | |
| async function handle(message) { | |
| const { id, method, params } = message; | |
| const isRequest = id !== undefined && id !== null; | |
| if (!method) return; | |
| if (!isRequest) return; // notifications (initialized, cancelled, ...) | |
| try { | |
| switch (method) { | |
| case 'initialize': { | |
| const requested = params && params.protocolVersion; | |
| send({ | |
| jsonrpc: '2.0', | |
| id, | |
| result: { | |
| protocolVersion: /^\d{4}-\d{2}-\d{2}$/.test(requested || '') ? requested : '2025-06-18', | |
| capabilities: { tools: { listChanged: false } }, | |
| serverInfo: SERVER_INFO, | |
| instructions: SCREEN | |
| ? 'Agent Web tools. share_file sends files to the user in the chat. The screen tools drive the ' + | |
| 'virtual display the user watches in the "Ekran" tab.' | |
| : 'Agent Web tools. share_file sends files to the user in the chat.', | |
| }, | |
| }); | |
| return; | |
| } | |
| case 'ping': | |
| send({ jsonrpc: '2.0', id, result: {} }); | |
| return; | |
| case 'tools/list': | |
| send({ jsonrpc: '2.0', id, result: { tools: listedTools() } }); | |
| return; | |
| case 'tools/call': { | |
| const name = params && params.name; | |
| const listed = listedTools().some((tool) => tool.name === name); | |
| const handler = listed && Object.prototype.hasOwnProperty.call(HANDLERS, name) ? HANDLERS[name] : null; | |
| if (!handler) { | |
| send({ jsonrpc: '2.0', id, error: { code: -32602, message: `Unknown tool: ${name}` } }); | |
| return; | |
| } | |
| try { | |
| const content = await handler((params && params.arguments) || {}); | |
| send({ jsonrpc: '2.0', id, result: { content } }); | |
| } catch (err) { | |
| const messageText = err instanceof ToolError ? err.message : `Hata: ${err && err.message ? err.message : String(err)}`; | |
| send({ jsonrpc: '2.0', id, result: { content: [text(messageText)], isError: true } }); | |
| } | |
| return; | |
| } | |
| default: | |
| send({ jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } }); | |
| } | |
| } catch (err) { | |
| send({ jsonrpc: '2.0', id, error: { code: -32603, message: err && err.message ? err.message : String(err) } }); | |
| } | |
| } | |
| if (require.main === module) { | |
| const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity }); | |
| rl.on('line', (line) => { | |
| if (!line.trim()) return; | |
| let message; | |
| try { | |
| message = JSON.parse(line); | |
| } catch { | |
| send({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }); | |
| return; | |
| } | |
| for (const item of Array.isArray(message) ? message : [message]) { | |
| if (item && typeof item === 'object') handle(item); | |
| } | |
| }); | |
| rl.on('close', () => process.exit(0)); | |
| } | |
| module.exports = { TOOLS, HANDLERS }; | |