junis 0.2.0 → 0.2.3

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.
@@ -46,6 +46,7 @@ var import_path = __toESM(require("path"));
46
46
  var import_glob = require("glob");
47
47
  var import_zod = require("zod");
48
48
  var execAsync = (0, import_util.promisify)(import_child_process.exec);
49
+ var execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
49
50
  var FilesystemTools = class {
50
51
  register(server) {
51
52
  server.tool(
@@ -91,8 +92,16 @@ ${error.stderr ?? ""}`
91
92
  encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("\uC778\uCF54\uB529")
92
93
  },
93
94
  async ({ path: filePath, encoding }) => {
94
- const content = await import_promises.default.readFile(filePath, encoding);
95
- return { content: [{ type: "text", text: content }] };
95
+ try {
96
+ const content = await import_promises.default.readFile(filePath, encoding);
97
+ return { content: [{ type: "text", text: content }] };
98
+ } catch (err) {
99
+ const e = err;
100
+ if (e.code === "ENOENT") {
101
+ return { content: [{ type: "text", text: `\u274C \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}` }], isError: true };
102
+ }
103
+ return { content: [{ type: "text", text: `\u274C \uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
104
+ }
96
105
  }
97
106
  );
98
107
  server.tool(
@@ -115,9 +124,17 @@ ${error.stderr ?? ""}`
115
124
  path: import_zod.z.string().describe("\uB514\uB809\uD1A0\uB9AC \uACBD\uB85C")
116
125
  },
117
126
  async ({ path: dirPath }) => {
118
- const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
119
- const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
120
- return { content: [{ type: "text", text: lines.join("\n") }] };
127
+ try {
128
+ const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
129
+ const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
130
+ return { content: [{ type: "text", text: lines.join("\n") }] };
131
+ } catch (err) {
132
+ const e = err;
133
+ if (e.code === "ENOENT") {
134
+ return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dirPath}` }], isError: true };
135
+ }
136
+ return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
137
+ }
121
138
  }
122
139
  );
123
140
  server.tool(
@@ -130,18 +147,20 @@ ${error.stderr ?? ""}`
130
147
  },
131
148
  async ({ pattern, directory, file_pattern }) => {
132
149
  try {
133
- const { stdout } = await execAsync(
134
- `rg --no-heading -n "${pattern}" ${directory}`,
150
+ const { stdout } = await execFileAsync(
151
+ "rg",
152
+ ["--no-heading", "-n", pattern, directory],
135
153
  { timeout: 1e4 }
136
154
  );
137
155
  return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
138
156
  } catch {
139
- const files = await (0, import_glob.glob)(file_pattern, { cwd: directory });
157
+ const safeDirectory = import_path.default.resolve(directory);
158
+ const files = await (0, import_glob.glob)(file_pattern, { cwd: safeDirectory });
140
159
  const results = [];
141
160
  for (const file of files.slice(0, 100)) {
142
161
  try {
143
162
  const content = await import_promises.default.readFile(
144
- import_path.default.join(directory, file),
163
+ import_path.default.join(safeDirectory, file),
145
164
  "utf-8"
146
165
  );
147
166
  const lines = content.split("\n");
@@ -268,6 +287,17 @@ var import_zod2 = require("zod");
268
287
  var BrowserTools = class {
269
288
  browser = null;
270
289
  page = null;
290
+ // 동시 요청 시 race condition 방지용 직렬화 락
291
+ lock = Promise.resolve();
292
+ withLock(fn) {
293
+ let release;
294
+ const next = new Promise((r) => {
295
+ release = r;
296
+ });
297
+ const current = this.lock;
298
+ this.lock = this.lock.then(() => next);
299
+ return current.then(() => fn()).finally(() => release());
300
+ }
271
301
  async init() {
272
302
  try {
273
303
  this.browser = await import_playwright.chromium.launch({ headless: true });
@@ -290,22 +320,22 @@ var BrowserTools = class {
290
320
  "browser_navigate",
291
321
  "URL\uB85C \uC774\uB3D9",
292
322
  { url: import_zod2.z.string().describe("\uC774\uB3D9\uD560 URL") },
293
- async ({ url }) => {
323
+ ({ url }) => this.withLock(async () => {
294
324
  const page = requirePage();
295
325
  await page.goto(url, { waitUntil: "domcontentloaded" });
296
326
  return {
297
327
  content: [{ type: "text", text: `\uC774\uB3D9 \uC644\uB8CC: ${page.url()}` }]
298
328
  };
299
- }
329
+ })
300
330
  );
301
331
  server.tool(
302
332
  "browser_click",
303
333
  "\uC694\uC18C \uD074\uB9AD",
304
334
  { selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790") },
305
- async ({ selector }) => {
335
+ ({ selector }) => this.withLock(async () => {
306
336
  await requirePage().click(selector);
307
337
  return { content: [{ type: "text", text: "\uD074\uB9AD \uC644\uB8CC" }] };
308
- }
338
+ })
309
339
  );
310
340
  server.tool(
311
341
  "browser_type",
@@ -315,12 +345,12 @@ var BrowserTools = class {
315
345
  text: import_zod2.z.string().describe("\uC785\uB825\uD560 \uD14D\uC2A4\uD2B8"),
316
346
  clear: import_zod2.z.boolean().optional().default(false).describe("\uAE30\uC874 \uB0B4\uC6A9 \uC0AD\uC81C \uD6C4 \uC785\uB825")
317
347
  },
318
- async ({ selector, text, clear }) => {
348
+ ({ selector, text, clear }) => this.withLock(async () => {
319
349
  const page = requirePage();
320
350
  if (clear) await page.fill(selector, text);
321
351
  else await page.type(selector, text);
322
352
  return { content: [{ type: "text", text: "\uC785\uB825 \uC644\uB8CC" }] };
323
- }
353
+ })
324
354
  );
325
355
  server.tool(
326
356
  "browser_screenshot",
@@ -329,7 +359,7 @@ var BrowserTools = class {
329
359
  path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
330
360
  full_page: import_zod2.z.boolean().optional().default(false)
331
361
  },
332
- async ({ path: path2, full_page }) => {
362
+ ({ path: path2, full_page }) => this.withLock(async () => {
333
363
  const page = requirePage();
334
364
  const screenshot = await page.screenshot({
335
365
  path: path2 ?? void 0,
@@ -347,13 +377,13 @@ var BrowserTools = class {
347
377
  }
348
378
  ]
349
379
  };
350
- }
380
+ })
351
381
  );
352
382
  server.tool(
353
383
  "browser_snapshot",
354
384
  "\uD398\uC774\uC9C0 \uC811\uADFC\uC131 \uD2B8\uB9AC \uC870\uD68C (\uAD6C\uC870 \uD30C\uC545\uC6A9)",
355
385
  {},
356
- async () => {
386
+ () => this.withLock(async () => {
357
387
  const page = requirePage();
358
388
  const snapshot = await page.locator("body").ariaSnapshot();
359
389
  return {
@@ -361,29 +391,36 @@ var BrowserTools = class {
361
391
  { type: "text", text: snapshot }
362
392
  ]
363
393
  };
364
- }
394
+ })
365
395
  );
366
396
  server.tool(
367
397
  "browser_evaluate",
368
398
  "JavaScript \uC2E4\uD589",
369
399
  { code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
370
- async ({ code }) => {
371
- const result = await requirePage().evaluate(code);
372
- return {
373
- content: [
374
- { type: "text", text: JSON.stringify(result, null, 2) }
375
- ]
376
- };
377
- }
400
+ ({ code }) => this.withLock(async () => {
401
+ try {
402
+ const result = await requirePage().evaluate(code);
403
+ return {
404
+ content: [
405
+ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }
406
+ ]
407
+ };
408
+ } catch (err) {
409
+ return {
410
+ content: [{ type: "text", text: `\u274C JavaScript \uC2E4\uD589 \uC624\uB958: ${err.message}` }],
411
+ isError: true
412
+ };
413
+ }
414
+ })
378
415
  );
379
416
  server.tool(
380
417
  "browser_pdf",
381
418
  "\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
382
419
  { path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
383
- async ({ path: path2 }) => {
420
+ ({ path: path2 }) => this.withLock(async () => {
384
421
  await requirePage().pdf({ path: path2 });
385
422
  return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path2}` }] };
386
- }
423
+ })
387
424
  );
388
425
  }
389
426
  };
@@ -396,7 +433,11 @@ var import_util2 = require("util");
396
433
  var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
397
434
  async function readNotebook(filePath) {
398
435
  const raw = await import_promises2.default.readFile(filePath, "utf-8");
399
- return JSON.parse(raw);
436
+ try {
437
+ return JSON.parse(raw);
438
+ } catch {
439
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 Jupyter \uB178\uD2B8\uBD81 \uD30C\uC77C\uC785\uB2C8\uB2E4: ${filePath}`);
440
+ }
400
441
  }
401
442
  async function writeNotebook(filePath, nb) {
402
443
  await import_promises2.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
@@ -491,15 +532,21 @@ var NotebookTools = class {
491
532
  execution_count: cellType === "code" ? null : void 0
492
533
  };
493
534
  let actualIndex;
535
+ let warning = "";
494
536
  if (position === void 0 || position === null) {
495
537
  nb.cells.push(newCell);
496
538
  actualIndex = nb.cells.length - 1;
539
+ } else if (position > nb.cells.length) {
540
+ nb.cells.push(newCell);
541
+ actualIndex = nb.cells.length - 1;
542
+ warning = ` (\uACBD\uACE0: position ${position}\uC774 \uBC94\uC704\uB97C \uCD08\uACFC\uD558\uC5EC \uB05D(index: ${actualIndex})\uC5D0 \uCD94\uAC00\uB428)`;
497
543
  } else {
498
- nb.cells.splice(position, 0, newCell);
499
- actualIndex = position;
544
+ const clamped = Math.max(0, position);
545
+ nb.cells.splice(clamped, 0, newCell);
546
+ actualIndex = clamped;
500
547
  }
501
548
  await writeNotebook(filePath, nb);
502
- return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})` }] };
549
+ return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})${warning}` }] };
503
550
  }
504
551
  );
505
552
  server.tool(
@@ -527,13 +574,13 @@ var import_child_process3 = require("child_process");
527
574
  var import_util3 = require("util");
528
575
  var import_zod4 = require("zod");
529
576
  var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
577
+ var screenRecordPid = null;
530
578
  function platform() {
531
579
  if (process.platform === "darwin") return "mac";
532
580
  if (process.platform === "win32") return "win";
533
581
  return "linux";
534
582
  }
535
583
  var DeviceTools = class {
536
- screenRecordPid = null;
537
584
  register(server) {
538
585
  server.tool(
539
586
  "screen_capture",
@@ -543,15 +590,26 @@ var DeviceTools = class {
543
590
  },
544
591
  async ({ output_path }) => {
545
592
  const p = platform();
593
+ const isTmp = !output_path;
546
594
  const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
547
595
  const cmd = {
548
596
  mac: `screencapture -x "${tmpPath}"`,
549
597
  win: `nircmd.exe savescreenshot "${tmpPath}"`,
550
598
  linux: `scrot "${tmpPath}"`
551
599
  }[p];
552
- await execAsync3(cmd);
553
- const { readFileSync } = await import("fs");
600
+ try {
601
+ await execAsync3(cmd);
602
+ } catch (err) {
603
+ throw new Error(`\uD654\uBA74 \uCEA1\uCC98 \uC2E4\uD328: ${err.message}`);
604
+ }
605
+ const { readFileSync, unlinkSync } = await import("fs");
554
606
  const data = readFileSync(tmpPath).toString("base64");
607
+ if (isTmp) {
608
+ try {
609
+ unlinkSync(tmpPath);
610
+ } catch {
611
+ }
612
+ }
555
613
  return {
556
614
  content: [{ type: "image", data, mimeType: "image/png" }]
557
615
  };
@@ -565,15 +623,31 @@ var DeviceTools = class {
565
623
  },
566
624
  async ({ output_path }) => {
567
625
  const p = platform();
626
+ const isTmp = !output_path;
568
627
  const tmpPath = output_path ?? `/tmp/junis_cam_${Date.now()}.jpg`;
569
628
  const cmd = {
570
629
  mac: `imagesnap "${tmpPath}"`,
571
630
  win: `ffmpeg -f dshow -i video="Default" -frames:v 1 "${tmpPath}"`,
572
631
  linux: `fswebcam -r 1280x720 "${tmpPath}"`
573
632
  }[p];
574
- await execAsync3(cmd);
575
- const { readFileSync } = await import("fs");
633
+ try {
634
+ await execAsync3(cmd);
635
+ } catch (err) {
636
+ const e = err;
637
+ return {
638
+ content: [{ type: "text", text: `\u274C \uCE74\uBA54\uB77C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uAC70\uB098 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
639
+ \uC6D0\uC778: ${e.message}
640
+
641
+ \uCE74\uBA54\uB77C\uAC00 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.` }],
642
+ isError: true
643
+ };
644
+ }
645
+ const { readFileSync, unlinkSync } = await import("fs");
576
646
  const data = readFileSync(tmpPath).toString("base64");
647
+ if (isTmp) try {
648
+ unlinkSync(tmpPath);
649
+ } catch {
650
+ }
577
651
  return {
578
652
  content: [{ type: "image", data, mimeType: "image/jpeg" }]
579
653
  };
@@ -588,11 +662,17 @@ var DeviceTools = class {
588
662
  },
589
663
  async ({ title, message }) => {
590
664
  const p = platform();
591
- const cmd = {
592
- mac: `osascript -e 'display notification "${message}" with title "${title}"'`,
593
- win: `powershell -Command "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime]::CreateToastNotifier('Junis').Show([Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom, ContentType=WindowsRuntime]::new())"`,
594
- linux: `notify-send "${title}" "${message}"`
595
- }[p];
665
+ let cmd;
666
+ if (p === "win") {
667
+ const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}')`;
668
+ const encoded = Buffer.from(script, "utf16le").toString("base64");
669
+ cmd = `powershell -NoProfile -EncodedCommand ${encoded}`;
670
+ } else {
671
+ cmd = {
672
+ mac: `osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"'`,
673
+ linux: `notify-send "${title.replace(/"/g, '\\"')}" "${message.replace(/"/g, '\\"')}"`
674
+ }[p] ?? "";
675
+ }
596
676
  await execAsync3(cmd);
597
677
  return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
598
678
  }
@@ -633,7 +713,7 @@ var DeviceTools = class {
633
713
  async ({ action, output_path }) => {
634
714
  const p = platform();
635
715
  if (action === "start") {
636
- if (this.screenRecordPid) {
716
+ if (screenRecordPid) {
637
717
  return { content: [{ type: "text", text: "\uC774\uBBF8 \uB179\uD654 \uC911\uC785\uB2C8\uB2E4." }] };
638
718
  }
639
719
  const tmpPath = output_path ?? `/tmp/junis_record_${Date.now()}.mp4`;
@@ -641,17 +721,18 @@ var DeviceTools = class {
641
721
  const cmd = p === "mac" ? ["screencapture", ["-v", tmpPath]] : ["ffmpeg", ["-f", p === "win" ? "gdigrab" : "x11grab", "-i", p === "win" ? "desktop" : ":0.0", tmpPath]];
642
722
  const child = spawn(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
643
723
  child.unref();
644
- this.screenRecordPid = child.pid ?? null;
645
- return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${this.screenRecordPid})` }] };
724
+ screenRecordPid = child.pid ?? null;
725
+ return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${screenRecordPid})` }] };
646
726
  } else {
647
- if (!this.screenRecordPid) {
727
+ if (!screenRecordPid) {
648
728
  return { content: [{ type: "text", text: "\uD604\uC7AC \uB179\uD654 \uC911\uC774 \uC544\uB2D9\uB2C8\uB2E4." }] };
649
729
  }
650
730
  try {
651
- process.kill(this.screenRecordPid, "SIGINT");
731
+ process.kill(screenRecordPid, "SIGINT");
732
+ await new Promise((r) => setTimeout(r, 1e3));
652
733
  } catch {
653
734
  }
654
- this.screenRecordPid = null;
735
+ screenRecordPid = null;
655
736
  return { content: [{ type: "text", text: "\uB179\uD654 \uC911\uC9C0\uB428." }] };
656
737
  }
657
738
  }
@@ -670,12 +751,15 @@ var DeviceTools = class {
670
751
  } catch {
671
752
  }
672
753
  }
673
- const res = await fetch("https://ipapi.co/json/");
754
+ const res = await fetch("http://ip-api.com/json/");
674
755
  const data = await res.json();
756
+ if (data.status !== "success") {
757
+ throw new Error(`IP \uC704\uCE58 \uC870\uD68C \uC2E4\uD328: ${data.message ?? data.status}`);
758
+ }
675
759
  return {
676
760
  content: [{
677
761
  type: "text",
678
- text: `\uC704\uB3C4: ${data.latitude}, \uACBD\uB3C4: ${data.longitude}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country_name} (IP \uAE30\uBC18 \uCD94\uC815)`
762
+ text: `\uC704\uB3C4: ${data.lat}, \uACBD\uB3C4: ${data.lon}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country} (IP \uAE30\uBC18 \uCD94\uC815)`
679
763
  }]
680
764
  };
681
765
  }