junis 0.1.8 → 0.2.2

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.
package/dist/cli/index.js CHANGED
@@ -31,7 +31,7 @@ var require_package = __commonJS({
31
31
  "package.json"(exports2, module2) {
32
32
  module2.exports = {
33
33
  name: "junis",
34
- version: "0.1.8",
34
+ version: "0.2.2",
35
35
  description: "One-line device control for AI agents",
36
36
  bin: {
37
37
  junis: "dist/cli/index.js"
@@ -87,8 +87,12 @@ function loadConfig() {
87
87
  }
88
88
  }
89
89
  function saveConfig(config) {
90
- import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
91
- import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
90
+ try {
91
+ import_fs.default.mkdirSync(CONFIG_DIR, { recursive: true });
92
+ import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), "utf-8");
93
+ } catch (err) {
94
+ throw new Error(`\uC778\uC99D \uC815\uBCF4 \uC800\uC7A5 \uC2E4\uD328: ${err.message}`);
95
+ }
92
96
  }
93
97
  function clearConfig() {
94
98
  if (import_fs.default.existsSync(CONFIG_FILE)) {
@@ -182,9 +186,11 @@ var RelayClient = class {
182
186
  destroyed = false;
183
187
  async connect() {
184
188
  if (this.destroyed) return;
185
- const url = `${JUNIS_WS}/ws/devices/${this.config.device_key}?token=${this.config.token}`;
189
+ const url = `${JUNIS_WS}/ws/devices/${this.config.device_key}`;
186
190
  console.log(`\u{1F517} \uB9B4\uB808\uC774 \uC11C\uBC84 \uC5F0\uACB0 \uC911...`);
187
- this.ws = new import_ws.default(url);
191
+ this.ws = new import_ws.default(url, {
192
+ headers: { Authorization: `Bearer ${this.config.token}` }
193
+ });
188
194
  this.ws.on("open", () => {
189
195
  console.log("\u2705 \uB9B4\uB808\uC774 \uC11C\uBC84 \uC5F0\uACB0\uB428");
190
196
  this.reconnectDelay = 1e3;
@@ -274,6 +280,7 @@ var import_path2 = __toESM(require("path"));
274
280
  var import_glob = require("glob");
275
281
  var import_zod = require("zod");
276
282
  var execAsync = (0, import_util.promisify)(import_child_process.exec);
283
+ var execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
277
284
  var FilesystemTools = class {
278
285
  register(server) {
279
286
  server.tool(
@@ -358,8 +365,9 @@ ${error.stderr ?? ""}`
358
365
  },
359
366
  async ({ pattern, directory, file_pattern }) => {
360
367
  try {
361
- const { stdout } = await execAsync(
362
- `rg --no-heading -n "${pattern}" ${directory}`,
368
+ const { stdout } = await execFileAsync(
369
+ "rg",
370
+ ["--no-heading", "-n", pattern, directory],
363
371
  { timeout: 1e4 }
364
372
  );
365
373
  return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
@@ -447,6 +455,46 @@ ${error.stderr ?? ""}`
447
455
  };
448
456
  }
449
457
  );
458
+ server.tool(
459
+ "edit_block",
460
+ "\uD30C\uC77C\uC758 \uD2B9\uC815 \uD14D\uC2A4\uD2B8 \uBE14\uB85D\uC744 \uC0C8 \uD14D\uC2A4\uD2B8\uB85C \uAD50\uCCB4 (diff \uAE30\uBC18 \uBD80\uBD84 \uC218\uC815)",
461
+ {
462
+ path: import_zod.z.string().describe("\uD30C\uC77C \uACBD\uB85C"),
463
+ old_string: import_zod.z.string().describe("\uAD50\uCCB4\uD560 \uAE30\uC874 \uD14D\uC2A4\uD2B8 (\uC815\uD655\uD788 \uC77C\uCE58\uD574\uC57C \uD568)"),
464
+ new_string: import_zod.z.string().describe("\uC0C8 \uD14D\uC2A4\uD2B8"),
465
+ replace_all: import_zod.z.boolean().optional().default(false).describe("true\uBA74 \uBAA8\uB4E0 \uB9E4\uCE6D \uAD50\uCCB4, false\uBA74 \uCCAB \uBC88\uC9F8\uB9CC")
466
+ },
467
+ async ({ path: filePath, old_string, new_string, replace_all }) => {
468
+ const content = await import_promises.default.readFile(filePath, "utf-8");
469
+ if (!content.includes(old_string)) {
470
+ throw new Error(`old_string\uC744 \uD30C\uC77C\uC5D0\uC11C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}`);
471
+ }
472
+ let count = 0;
473
+ let pos = 0;
474
+ while ((pos = content.indexOf(old_string, pos)) !== -1) {
475
+ count++;
476
+ pos += old_string.length;
477
+ }
478
+ if (!replace_all && count > 1) {
479
+ throw new Error(
480
+ `\uB9E4\uCE6D\uC774 ${count}\uAC1C\uC785\uB2C8\uB2E4. replace_all\uC744 true\uB85C \uD558\uAC70\uB098 \uB354 \uB113\uC740 \uCEE8\uD14D\uC2A4\uD2B8\uB97C \uD3EC\uD568\uD558\uC138\uC694.`
481
+ );
482
+ }
483
+ let result;
484
+ let replaced;
485
+ if (replace_all) {
486
+ result = content.split(old_string).join(new_string);
487
+ replaced = count;
488
+ } else {
489
+ result = content.replace(old_string, new_string);
490
+ replaced = 1;
491
+ }
492
+ await import_promises.default.writeFile(filePath, result, "utf-8");
493
+ return {
494
+ content: [{ type: "text", text: `\uAD50\uCCB4 \uC644\uB8CC (${replaced}\uAC1C \uBCC0\uACBD\uB428)` }]
495
+ };
496
+ }
497
+ );
450
498
  }
451
499
  };
452
500
 
@@ -456,6 +504,17 @@ var import_zod2 = require("zod");
456
504
  var BrowserTools = class {
457
505
  browser = null;
458
506
  page = null;
507
+ // 동시 요청 시 race condition 방지용 직렬화 락
508
+ lock = Promise.resolve();
509
+ withLock(fn) {
510
+ let release;
511
+ const next = new Promise((r) => {
512
+ release = r;
513
+ });
514
+ const current = this.lock;
515
+ this.lock = this.lock.then(() => next);
516
+ return current.then(() => fn()).finally(() => release());
517
+ }
459
518
  async init() {
460
519
  try {
461
520
  this.browser = await import_playwright.chromium.launch({ headless: true });
@@ -478,22 +537,22 @@ var BrowserTools = class {
478
537
  "browser_navigate",
479
538
  "URL\uB85C \uC774\uB3D9",
480
539
  { url: import_zod2.z.string().describe("\uC774\uB3D9\uD560 URL") },
481
- async ({ url }) => {
540
+ ({ url }) => this.withLock(async () => {
482
541
  const page = requirePage();
483
542
  await page.goto(url, { waitUntil: "domcontentloaded" });
484
543
  return {
485
544
  content: [{ type: "text", text: `\uC774\uB3D9 \uC644\uB8CC: ${page.url()}` }]
486
545
  };
487
- }
546
+ })
488
547
  );
489
548
  server.tool(
490
549
  "browser_click",
491
550
  "\uC694\uC18C \uD074\uB9AD",
492
551
  { selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790") },
493
- async ({ selector }) => {
552
+ ({ selector }) => this.withLock(async () => {
494
553
  await requirePage().click(selector);
495
554
  return { content: [{ type: "text", text: "\uD074\uB9AD \uC644\uB8CC" }] };
496
- }
555
+ })
497
556
  );
498
557
  server.tool(
499
558
  "browser_type",
@@ -503,12 +562,12 @@ var BrowserTools = class {
503
562
  text: import_zod2.z.string().describe("\uC785\uB825\uD560 \uD14D\uC2A4\uD2B8"),
504
563
  clear: import_zod2.z.boolean().optional().default(false).describe("\uAE30\uC874 \uB0B4\uC6A9 \uC0AD\uC81C \uD6C4 \uC785\uB825")
505
564
  },
506
- async ({ selector, text, clear }) => {
565
+ ({ selector, text, clear }) => this.withLock(async () => {
507
566
  const page = requirePage();
508
567
  if (clear) await page.fill(selector, text);
509
568
  else await page.type(selector, text);
510
569
  return { content: [{ type: "text", text: "\uC785\uB825 \uC644\uB8CC" }] };
511
- }
570
+ })
512
571
  );
513
572
  server.tool(
514
573
  "browser_screenshot",
@@ -517,7 +576,7 @@ var BrowserTools = class {
517
576
  path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
518
577
  full_page: import_zod2.z.boolean().optional().default(false)
519
578
  },
520
- async ({ path: path3, full_page }) => {
579
+ ({ path: path3, full_page }) => this.withLock(async () => {
521
580
  const page = requirePage();
522
581
  const screenshot = await page.screenshot({
523
582
  path: path3 ?? void 0,
@@ -535,13 +594,13 @@ var BrowserTools = class {
535
594
  }
536
595
  ]
537
596
  };
538
- }
597
+ })
539
598
  );
540
599
  server.tool(
541
600
  "browser_snapshot",
542
601
  "\uD398\uC774\uC9C0 \uC811\uADFC\uC131 \uD2B8\uB9AC \uC870\uD68C (\uAD6C\uC870 \uD30C\uC545\uC6A9)",
543
602
  {},
544
- async () => {
603
+ () => this.withLock(async () => {
545
604
  const page = requirePage();
546
605
  const snapshot = await page.locator("body").ariaSnapshot();
547
606
  return {
@@ -549,29 +608,29 @@ var BrowserTools = class {
549
608
  { type: "text", text: snapshot }
550
609
  ]
551
610
  };
552
- }
611
+ })
553
612
  );
554
613
  server.tool(
555
614
  "browser_evaluate",
556
615
  "JavaScript \uC2E4\uD589",
557
616
  { code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
558
- async ({ code }) => {
617
+ ({ code }) => this.withLock(async () => {
559
618
  const result = await requirePage().evaluate(code);
560
619
  return {
561
620
  content: [
562
621
  { type: "text", text: JSON.stringify(result, null, 2) }
563
622
  ]
564
623
  };
565
- }
624
+ })
566
625
  );
567
626
  server.tool(
568
627
  "browser_pdf",
569
628
  "\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
570
629
  { path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
571
- async ({ path: path3 }) => {
630
+ ({ path: path3 }) => this.withLock(async () => {
572
631
  await requirePage().pdf({ path: path3 });
573
632
  return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path3}` }] };
574
- }
633
+ })
575
634
  );
576
635
  }
577
636
  };
@@ -584,7 +643,11 @@ var import_util2 = require("util");
584
643
  var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
585
644
  async function readNotebook(filePath) {
586
645
  const raw = await import_promises2.default.readFile(filePath, "utf-8");
587
- return JSON.parse(raw);
646
+ try {
647
+ return JSON.parse(raw);
648
+ } catch {
649
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 .ipynb \uD30C\uC77C: ${filePath}`);
650
+ }
588
651
  }
589
652
  async function writeNotebook(filePath, nb) {
590
653
  await import_promises2.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
@@ -660,6 +723,54 @@ var NotebookTools = class {
660
723
  throw new Error("jupyter\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC124\uCE58 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694: pip install jupyter");
661
724
  }
662
725
  );
726
+ server.tool(
727
+ "notebook_add_cell",
728
+ "\uB178\uD2B8\uBD81\uC5D0 \uC0C8 \uC140 \uCD94\uAC00",
729
+ {
730
+ path: import_zod3.z.string().describe(".ipynb \uD30C\uC77C \uACBD\uB85C"),
731
+ cell_type: import_zod3.z.enum(["code", "markdown"]).describe("\uC140 \uD0C0\uC785"),
732
+ source: import_zod3.z.string().describe("\uC140 \uC18C\uC2A4 \uB0B4\uC6A9"),
733
+ position: import_zod3.z.number().optional().describe("\uC0BD\uC785 \uC704\uCE58(0-based). \uC5C6\uC73C\uBA74 \uB9E8 \uB05D\uC5D0 \uCD94\uAC00")
734
+ },
735
+ async ({ path: filePath, cell_type: cellType, source, position }) => {
736
+ const nb = await readNotebook(filePath);
737
+ const newCell = {
738
+ cell_type: cellType,
739
+ source: source.split("\n").map((l, i, arr) => i < arr.length - 1 ? l + "\n" : l),
740
+ metadata: {},
741
+ outputs: cellType === "code" ? [] : void 0,
742
+ execution_count: cellType === "code" ? null : void 0
743
+ };
744
+ let actualIndex;
745
+ if (position === void 0 || position === null) {
746
+ nb.cells.push(newCell);
747
+ actualIndex = nb.cells.length - 1;
748
+ } else {
749
+ const clamped = Math.max(0, Math.min(position, nb.cells.length));
750
+ nb.cells.splice(clamped, 0, newCell);
751
+ actualIndex = clamped;
752
+ }
753
+ await writeNotebook(filePath, nb);
754
+ return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})` }] };
755
+ }
756
+ );
757
+ server.tool(
758
+ "notebook_delete_cell",
759
+ "\uB178\uD2B8\uBD81 \uD2B9\uC815 \uC140 \uC0AD\uC81C",
760
+ {
761
+ path: import_zod3.z.string().describe(".ipynb \uD30C\uC77C \uACBD\uB85C"),
762
+ cell_index: import_zod3.z.number().describe("\uC0AD\uC81C\uD560 \uC140 \uC778\uB371\uC2A4 (0-based)")
763
+ },
764
+ async ({ path: filePath, cell_index }) => {
765
+ const nb = await readNotebook(filePath);
766
+ if (cell_index < 0 || cell_index >= nb.cells.length) {
767
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC140 \uC778\uB371\uC2A4: ${cell_index}`);
768
+ }
769
+ nb.cells.splice(cell_index, 1);
770
+ await writeNotebook(filePath, nb);
771
+ return { content: [{ type: "text", text: `\uC140 \uC0AD\uC81C \uC644\uB8CC (index: ${cell_index})` }] };
772
+ }
773
+ );
663
774
  }
664
775
  };
665
776
 
@@ -668,13 +779,13 @@ var import_child_process3 = require("child_process");
668
779
  var import_util3 = require("util");
669
780
  var import_zod4 = require("zod");
670
781
  var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
782
+ var screenRecordPid = null;
671
783
  function platform() {
672
784
  if (process.platform === "darwin") return "mac";
673
785
  if (process.platform === "win32") return "win";
674
786
  return "linux";
675
787
  }
676
788
  var DeviceTools = class {
677
- screenRecordPid = null;
678
789
  register(server) {
679
790
  server.tool(
680
791
  "screen_capture",
@@ -684,15 +795,24 @@ var DeviceTools = class {
684
795
  },
685
796
  async ({ output_path }) => {
686
797
  const p = platform();
798
+ const isTmp = !output_path;
687
799
  const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
688
800
  const cmd = {
689
801
  mac: `screencapture -x "${tmpPath}"`,
690
802
  win: `nircmd.exe savescreenshot "${tmpPath}"`,
691
803
  linux: `scrot "${tmpPath}"`
692
804
  }[p];
693
- await execAsync3(cmd);
694
- const { readFileSync } = await import("fs");
805
+ try {
806
+ await execAsync3(cmd);
807
+ } catch (err) {
808
+ throw new Error(`\uD654\uBA74 \uCEA1\uCC98 \uC2E4\uD328: ${err.message}`);
809
+ }
810
+ const { readFileSync, unlinkSync } = await import("fs");
695
811
  const data = readFileSync(tmpPath).toString("base64");
812
+ if (isTmp) try {
813
+ unlinkSync(tmpPath);
814
+ } catch {
815
+ }
696
816
  return {
697
817
  content: [{ type: "image", data, mimeType: "image/png" }]
698
818
  };
@@ -706,15 +826,24 @@ var DeviceTools = class {
706
826
  },
707
827
  async ({ output_path }) => {
708
828
  const p = platform();
829
+ const isTmp = !output_path;
709
830
  const tmpPath = output_path ?? `/tmp/junis_cam_${Date.now()}.jpg`;
710
831
  const cmd = {
711
832
  mac: `imagesnap "${tmpPath}"`,
712
833
  win: `ffmpeg -f dshow -i video="Default" -frames:v 1 "${tmpPath}"`,
713
834
  linux: `fswebcam -r 1280x720 "${tmpPath}"`
714
835
  }[p];
715
- await execAsync3(cmd);
716
- const { readFileSync } = await import("fs");
836
+ try {
837
+ await execAsync3(cmd);
838
+ } catch (err) {
839
+ throw new Error(`\uCE74\uBA54\uB77C \uCD2C\uC601 \uC2E4\uD328: ${err.message}`);
840
+ }
841
+ const { readFileSync, unlinkSync } = await import("fs");
717
842
  const data = readFileSync(tmpPath).toString("base64");
843
+ if (isTmp) try {
844
+ unlinkSync(tmpPath);
845
+ } catch {
846
+ }
718
847
  return {
719
848
  content: [{ type: "image", data, mimeType: "image/jpeg" }]
720
849
  };
@@ -774,7 +903,7 @@ var DeviceTools = class {
774
903
  async ({ action, output_path }) => {
775
904
  const p = platform();
776
905
  if (action === "start") {
777
- if (this.screenRecordPid) {
906
+ if (screenRecordPid) {
778
907
  return { content: [{ type: "text", text: "\uC774\uBBF8 \uB179\uD654 \uC911\uC785\uB2C8\uB2E4." }] };
779
908
  }
780
909
  const tmpPath = output_path ?? `/tmp/junis_record_${Date.now()}.mp4`;
@@ -782,17 +911,17 @@ var DeviceTools = class {
782
911
  const cmd = p === "mac" ? ["screencapture", ["-v", tmpPath]] : ["ffmpeg", ["-f", p === "win" ? "gdigrab" : "x11grab", "-i", p === "win" ? "desktop" : ":0.0", tmpPath]];
783
912
  const child = spawn(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
784
913
  child.unref();
785
- this.screenRecordPid = child.pid ?? null;
786
- return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${this.screenRecordPid})` }] };
914
+ screenRecordPid = child.pid ?? null;
915
+ return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${screenRecordPid})` }] };
787
916
  } else {
788
- if (!this.screenRecordPid) {
917
+ if (!screenRecordPid) {
789
918
  return { content: [{ type: "text", text: "\uD604\uC7AC \uB179\uD654 \uC911\uC774 \uC544\uB2D9\uB2C8\uB2E4." }] };
790
919
  }
791
920
  try {
792
- process.kill(this.screenRecordPid, "SIGINT");
921
+ process.kill(screenRecordPid, "SIGINT");
793
922
  } catch {
794
923
  }
795
- this.screenRecordPid = null;
924
+ screenRecordPid = null;
796
925
  return { content: [{ type: "text", text: "\uB179\uD654 \uC911\uC9C0\uB428." }] };
797
926
  }
798
927
  }
@@ -811,12 +940,15 @@ var DeviceTools = class {
811
940
  } catch {
812
941
  }
813
942
  }
814
- const res = await fetch("https://ipapi.co/json/");
943
+ const res = await fetch("http://ip-api.com/json/");
815
944
  const data = await res.json();
945
+ if (data.status !== "success") {
946
+ throw new Error(`IP \uC704\uCE58 \uC870\uD68C \uC2E4\uD328: ${data.message ?? data.status}`);
947
+ }
816
948
  return {
817
949
  content: [{
818
950
  type: "text",
819
- text: `\uC704\uB3C4: ${data.latitude}, \uACBD\uB3C4: ${data.longitude}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country_name} (IP \uAE30\uBC18 \uCD94\uC815)`
951
+ text: `\uC704\uB3C4: ${data.lat}, \uACBD\uB3C4: ${data.lon}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country} (IP \uAE30\uBC18 \uCD94\uC815)`
820
952
  }]
821
953
  };
822
954
  }
@@ -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(
@@ -130,8 +131,9 @@ ${error.stderr ?? ""}`
130
131
  },
131
132
  async ({ pattern, directory, file_pattern }) => {
132
133
  try {
133
- const { stdout } = await execAsync(
134
- `rg --no-heading -n "${pattern}" ${directory}`,
134
+ const { stdout } = await execFileAsync(
135
+ "rg",
136
+ ["--no-heading", "-n", pattern, directory],
135
137
  { timeout: 1e4 }
136
138
  );
137
139
  return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
@@ -219,6 +221,46 @@ ${error.stderr ?? ""}`
219
221
  };
220
222
  }
221
223
  );
224
+ server.tool(
225
+ "edit_block",
226
+ "\uD30C\uC77C\uC758 \uD2B9\uC815 \uD14D\uC2A4\uD2B8 \uBE14\uB85D\uC744 \uC0C8 \uD14D\uC2A4\uD2B8\uB85C \uAD50\uCCB4 (diff \uAE30\uBC18 \uBD80\uBD84 \uC218\uC815)",
227
+ {
228
+ path: import_zod.z.string().describe("\uD30C\uC77C \uACBD\uB85C"),
229
+ old_string: import_zod.z.string().describe("\uAD50\uCCB4\uD560 \uAE30\uC874 \uD14D\uC2A4\uD2B8 (\uC815\uD655\uD788 \uC77C\uCE58\uD574\uC57C \uD568)"),
230
+ new_string: import_zod.z.string().describe("\uC0C8 \uD14D\uC2A4\uD2B8"),
231
+ replace_all: import_zod.z.boolean().optional().default(false).describe("true\uBA74 \uBAA8\uB4E0 \uB9E4\uCE6D \uAD50\uCCB4, false\uBA74 \uCCAB \uBC88\uC9F8\uB9CC")
232
+ },
233
+ async ({ path: filePath, old_string, new_string, replace_all }) => {
234
+ const content = await import_promises.default.readFile(filePath, "utf-8");
235
+ if (!content.includes(old_string)) {
236
+ throw new Error(`old_string\uC744 \uD30C\uC77C\uC5D0\uC11C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}`);
237
+ }
238
+ let count = 0;
239
+ let pos = 0;
240
+ while ((pos = content.indexOf(old_string, pos)) !== -1) {
241
+ count++;
242
+ pos += old_string.length;
243
+ }
244
+ if (!replace_all && count > 1) {
245
+ throw new Error(
246
+ `\uB9E4\uCE6D\uC774 ${count}\uAC1C\uC785\uB2C8\uB2E4. replace_all\uC744 true\uB85C \uD558\uAC70\uB098 \uB354 \uB113\uC740 \uCEE8\uD14D\uC2A4\uD2B8\uB97C \uD3EC\uD568\uD558\uC138\uC694.`
247
+ );
248
+ }
249
+ let result;
250
+ let replaced;
251
+ if (replace_all) {
252
+ result = content.split(old_string).join(new_string);
253
+ replaced = count;
254
+ } else {
255
+ result = content.replace(old_string, new_string);
256
+ replaced = 1;
257
+ }
258
+ await import_promises.default.writeFile(filePath, result, "utf-8");
259
+ return {
260
+ content: [{ type: "text", text: `\uAD50\uCCB4 \uC644\uB8CC (${replaced}\uAC1C \uBCC0\uACBD\uB428)` }]
261
+ };
262
+ }
263
+ );
222
264
  }
223
265
  };
224
266
 
@@ -228,6 +270,17 @@ var import_zod2 = require("zod");
228
270
  var BrowserTools = class {
229
271
  browser = null;
230
272
  page = null;
273
+ // 동시 요청 시 race condition 방지용 직렬화 락
274
+ lock = Promise.resolve();
275
+ withLock(fn) {
276
+ let release;
277
+ const next = new Promise((r) => {
278
+ release = r;
279
+ });
280
+ const current = this.lock;
281
+ this.lock = this.lock.then(() => next);
282
+ return current.then(() => fn()).finally(() => release());
283
+ }
231
284
  async init() {
232
285
  try {
233
286
  this.browser = await import_playwright.chromium.launch({ headless: true });
@@ -250,22 +303,22 @@ var BrowserTools = class {
250
303
  "browser_navigate",
251
304
  "URL\uB85C \uC774\uB3D9",
252
305
  { url: import_zod2.z.string().describe("\uC774\uB3D9\uD560 URL") },
253
- async ({ url }) => {
306
+ ({ url }) => this.withLock(async () => {
254
307
  const page = requirePage();
255
308
  await page.goto(url, { waitUntil: "domcontentloaded" });
256
309
  return {
257
310
  content: [{ type: "text", text: `\uC774\uB3D9 \uC644\uB8CC: ${page.url()}` }]
258
311
  };
259
- }
312
+ })
260
313
  );
261
314
  server.tool(
262
315
  "browser_click",
263
316
  "\uC694\uC18C \uD074\uB9AD",
264
317
  { selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790") },
265
- async ({ selector }) => {
318
+ ({ selector }) => this.withLock(async () => {
266
319
  await requirePage().click(selector);
267
320
  return { content: [{ type: "text", text: "\uD074\uB9AD \uC644\uB8CC" }] };
268
- }
321
+ })
269
322
  );
270
323
  server.tool(
271
324
  "browser_type",
@@ -275,12 +328,12 @@ var BrowserTools = class {
275
328
  text: import_zod2.z.string().describe("\uC785\uB825\uD560 \uD14D\uC2A4\uD2B8"),
276
329
  clear: import_zod2.z.boolean().optional().default(false).describe("\uAE30\uC874 \uB0B4\uC6A9 \uC0AD\uC81C \uD6C4 \uC785\uB825")
277
330
  },
278
- async ({ selector, text, clear }) => {
331
+ ({ selector, text, clear }) => this.withLock(async () => {
279
332
  const page = requirePage();
280
333
  if (clear) await page.fill(selector, text);
281
334
  else await page.type(selector, text);
282
335
  return { content: [{ type: "text", text: "\uC785\uB825 \uC644\uB8CC" }] };
283
- }
336
+ })
284
337
  );
285
338
  server.tool(
286
339
  "browser_screenshot",
@@ -289,7 +342,7 @@ var BrowserTools = class {
289
342
  path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
290
343
  full_page: import_zod2.z.boolean().optional().default(false)
291
344
  },
292
- async ({ path: path2, full_page }) => {
345
+ ({ path: path2, full_page }) => this.withLock(async () => {
293
346
  const page = requirePage();
294
347
  const screenshot = await page.screenshot({
295
348
  path: path2 ?? void 0,
@@ -307,13 +360,13 @@ var BrowserTools = class {
307
360
  }
308
361
  ]
309
362
  };
310
- }
363
+ })
311
364
  );
312
365
  server.tool(
313
366
  "browser_snapshot",
314
367
  "\uD398\uC774\uC9C0 \uC811\uADFC\uC131 \uD2B8\uB9AC \uC870\uD68C (\uAD6C\uC870 \uD30C\uC545\uC6A9)",
315
368
  {},
316
- async () => {
369
+ () => this.withLock(async () => {
317
370
  const page = requirePage();
318
371
  const snapshot = await page.locator("body").ariaSnapshot();
319
372
  return {
@@ -321,29 +374,29 @@ var BrowserTools = class {
321
374
  { type: "text", text: snapshot }
322
375
  ]
323
376
  };
324
- }
377
+ })
325
378
  );
326
379
  server.tool(
327
380
  "browser_evaluate",
328
381
  "JavaScript \uC2E4\uD589",
329
382
  { code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
330
- async ({ code }) => {
383
+ ({ code }) => this.withLock(async () => {
331
384
  const result = await requirePage().evaluate(code);
332
385
  return {
333
386
  content: [
334
387
  { type: "text", text: JSON.stringify(result, null, 2) }
335
388
  ]
336
389
  };
337
- }
390
+ })
338
391
  );
339
392
  server.tool(
340
393
  "browser_pdf",
341
394
  "\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
342
395
  { path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
343
- async ({ path: path2 }) => {
396
+ ({ path: path2 }) => this.withLock(async () => {
344
397
  await requirePage().pdf({ path: path2 });
345
398
  return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path2}` }] };
346
- }
399
+ })
347
400
  );
348
401
  }
349
402
  };
@@ -356,7 +409,11 @@ var import_util2 = require("util");
356
409
  var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
357
410
  async function readNotebook(filePath) {
358
411
  const raw = await import_promises2.default.readFile(filePath, "utf-8");
359
- return JSON.parse(raw);
412
+ try {
413
+ return JSON.parse(raw);
414
+ } catch {
415
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 .ipynb \uD30C\uC77C: ${filePath}`);
416
+ }
360
417
  }
361
418
  async function writeNotebook(filePath, nb) {
362
419
  await import_promises2.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
@@ -432,6 +489,54 @@ var NotebookTools = class {
432
489
  throw new Error("jupyter\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC124\uCE58 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694: pip install jupyter");
433
490
  }
434
491
  );
492
+ server.tool(
493
+ "notebook_add_cell",
494
+ "\uB178\uD2B8\uBD81\uC5D0 \uC0C8 \uC140 \uCD94\uAC00",
495
+ {
496
+ path: import_zod3.z.string().describe(".ipynb \uD30C\uC77C \uACBD\uB85C"),
497
+ cell_type: import_zod3.z.enum(["code", "markdown"]).describe("\uC140 \uD0C0\uC785"),
498
+ source: import_zod3.z.string().describe("\uC140 \uC18C\uC2A4 \uB0B4\uC6A9"),
499
+ position: import_zod3.z.number().optional().describe("\uC0BD\uC785 \uC704\uCE58(0-based). \uC5C6\uC73C\uBA74 \uB9E8 \uB05D\uC5D0 \uCD94\uAC00")
500
+ },
501
+ async ({ path: filePath, cell_type: cellType, source, position }) => {
502
+ const nb = await readNotebook(filePath);
503
+ const newCell = {
504
+ cell_type: cellType,
505
+ source: source.split("\n").map((l, i, arr) => i < arr.length - 1 ? l + "\n" : l),
506
+ metadata: {},
507
+ outputs: cellType === "code" ? [] : void 0,
508
+ execution_count: cellType === "code" ? null : void 0
509
+ };
510
+ let actualIndex;
511
+ if (position === void 0 || position === null) {
512
+ nb.cells.push(newCell);
513
+ actualIndex = nb.cells.length - 1;
514
+ } else {
515
+ const clamped = Math.max(0, Math.min(position, nb.cells.length));
516
+ nb.cells.splice(clamped, 0, newCell);
517
+ actualIndex = clamped;
518
+ }
519
+ await writeNotebook(filePath, nb);
520
+ return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})` }] };
521
+ }
522
+ );
523
+ server.tool(
524
+ "notebook_delete_cell",
525
+ "\uB178\uD2B8\uBD81 \uD2B9\uC815 \uC140 \uC0AD\uC81C",
526
+ {
527
+ path: import_zod3.z.string().describe(".ipynb \uD30C\uC77C \uACBD\uB85C"),
528
+ cell_index: import_zod3.z.number().describe("\uC0AD\uC81C\uD560 \uC140 \uC778\uB371\uC2A4 (0-based)")
529
+ },
530
+ async ({ path: filePath, cell_index }) => {
531
+ const nb = await readNotebook(filePath);
532
+ if (cell_index < 0 || cell_index >= nb.cells.length) {
533
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC140 \uC778\uB371\uC2A4: ${cell_index}`);
534
+ }
535
+ nb.cells.splice(cell_index, 1);
536
+ await writeNotebook(filePath, nb);
537
+ return { content: [{ type: "text", text: `\uC140 \uC0AD\uC81C \uC644\uB8CC (index: ${cell_index})` }] };
538
+ }
539
+ );
435
540
  }
436
541
  };
437
542
 
@@ -440,13 +545,13 @@ var import_child_process3 = require("child_process");
440
545
  var import_util3 = require("util");
441
546
  var import_zod4 = require("zod");
442
547
  var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
548
+ var screenRecordPid = null;
443
549
  function platform() {
444
550
  if (process.platform === "darwin") return "mac";
445
551
  if (process.platform === "win32") return "win";
446
552
  return "linux";
447
553
  }
448
554
  var DeviceTools = class {
449
- screenRecordPid = null;
450
555
  register(server) {
451
556
  server.tool(
452
557
  "screen_capture",
@@ -456,15 +561,24 @@ var DeviceTools = class {
456
561
  },
457
562
  async ({ output_path }) => {
458
563
  const p = platform();
564
+ const isTmp = !output_path;
459
565
  const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
460
566
  const cmd = {
461
567
  mac: `screencapture -x "${tmpPath}"`,
462
568
  win: `nircmd.exe savescreenshot "${tmpPath}"`,
463
569
  linux: `scrot "${tmpPath}"`
464
570
  }[p];
465
- await execAsync3(cmd);
466
- const { readFileSync } = await import("fs");
571
+ try {
572
+ await execAsync3(cmd);
573
+ } catch (err) {
574
+ throw new Error(`\uD654\uBA74 \uCEA1\uCC98 \uC2E4\uD328: ${err.message}`);
575
+ }
576
+ const { readFileSync, unlinkSync } = await import("fs");
467
577
  const data = readFileSync(tmpPath).toString("base64");
578
+ if (isTmp) try {
579
+ unlinkSync(tmpPath);
580
+ } catch {
581
+ }
468
582
  return {
469
583
  content: [{ type: "image", data, mimeType: "image/png" }]
470
584
  };
@@ -478,15 +592,24 @@ var DeviceTools = class {
478
592
  },
479
593
  async ({ output_path }) => {
480
594
  const p = platform();
595
+ const isTmp = !output_path;
481
596
  const tmpPath = output_path ?? `/tmp/junis_cam_${Date.now()}.jpg`;
482
597
  const cmd = {
483
598
  mac: `imagesnap "${tmpPath}"`,
484
599
  win: `ffmpeg -f dshow -i video="Default" -frames:v 1 "${tmpPath}"`,
485
600
  linux: `fswebcam -r 1280x720 "${tmpPath}"`
486
601
  }[p];
487
- await execAsync3(cmd);
488
- const { readFileSync } = await import("fs");
602
+ try {
603
+ await execAsync3(cmd);
604
+ } catch (err) {
605
+ throw new Error(`\uCE74\uBA54\uB77C \uCD2C\uC601 \uC2E4\uD328: ${err.message}`);
606
+ }
607
+ const { readFileSync, unlinkSync } = await import("fs");
489
608
  const data = readFileSync(tmpPath).toString("base64");
609
+ if (isTmp) try {
610
+ unlinkSync(tmpPath);
611
+ } catch {
612
+ }
490
613
  return {
491
614
  content: [{ type: "image", data, mimeType: "image/jpeg" }]
492
615
  };
@@ -546,7 +669,7 @@ var DeviceTools = class {
546
669
  async ({ action, output_path }) => {
547
670
  const p = platform();
548
671
  if (action === "start") {
549
- if (this.screenRecordPid) {
672
+ if (screenRecordPid) {
550
673
  return { content: [{ type: "text", text: "\uC774\uBBF8 \uB179\uD654 \uC911\uC785\uB2C8\uB2E4." }] };
551
674
  }
552
675
  const tmpPath = output_path ?? `/tmp/junis_record_${Date.now()}.mp4`;
@@ -554,17 +677,17 @@ var DeviceTools = class {
554
677
  const cmd = p === "mac" ? ["screencapture", ["-v", tmpPath]] : ["ffmpeg", ["-f", p === "win" ? "gdigrab" : "x11grab", "-i", p === "win" ? "desktop" : ":0.0", tmpPath]];
555
678
  const child = spawn(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
556
679
  child.unref();
557
- this.screenRecordPid = child.pid ?? null;
558
- return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${this.screenRecordPid})` }] };
680
+ screenRecordPid = child.pid ?? null;
681
+ return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${screenRecordPid})` }] };
559
682
  } else {
560
- if (!this.screenRecordPid) {
683
+ if (!screenRecordPid) {
561
684
  return { content: [{ type: "text", text: "\uD604\uC7AC \uB179\uD654 \uC911\uC774 \uC544\uB2D9\uB2C8\uB2E4." }] };
562
685
  }
563
686
  try {
564
- process.kill(this.screenRecordPid, "SIGINT");
687
+ process.kill(screenRecordPid, "SIGINT");
565
688
  } catch {
566
689
  }
567
- this.screenRecordPid = null;
690
+ screenRecordPid = null;
568
691
  return { content: [{ type: "text", text: "\uB179\uD654 \uC911\uC9C0\uB428." }] };
569
692
  }
570
693
  }
@@ -583,12 +706,15 @@ var DeviceTools = class {
583
706
  } catch {
584
707
  }
585
708
  }
586
- const res = await fetch("https://ipapi.co/json/");
709
+ const res = await fetch("http://ip-api.com/json/");
587
710
  const data = await res.json();
711
+ if (data.status !== "success") {
712
+ throw new Error(`IP \uC704\uCE58 \uC870\uD68C \uC2E4\uD328: ${data.message ?? data.status}`);
713
+ }
588
714
  return {
589
715
  content: [{
590
716
  type: "text",
591
- text: `\uC704\uB3C4: ${data.latitude}, \uACBD\uB3C4: ${data.longitude}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country_name} (IP \uAE30\uBC18 \uCD94\uC815)`
717
+ text: `\uC704\uB3C4: ${data.lat}, \uACBD\uB3C4: ${data.lon}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country} (IP \uAE30\uBC18 \uCD94\uC815)`
592
718
  }]
593
719
  };
594
720
  }
@@ -35,6 +35,7 @@ var import_path = __toESM(require("path"));
35
35
  var import_glob = require("glob");
36
36
  var import_zod = require("zod");
37
37
  var execAsync = (0, import_util.promisify)(import_child_process.exec);
38
+ var execFileAsync = (0, import_util.promisify)(import_child_process.execFile);
38
39
  var FilesystemTools = class {
39
40
  register(server) {
40
41
  server.tool(
@@ -119,8 +120,9 @@ ${error.stderr ?? ""}`
119
120
  },
120
121
  async ({ pattern, directory, file_pattern }) => {
121
122
  try {
122
- const { stdout } = await execAsync(
123
- `rg --no-heading -n "${pattern}" ${directory}`,
123
+ const { stdout } = await execFileAsync(
124
+ "rg",
125
+ ["--no-heading", "-n", pattern, directory],
124
126
  { timeout: 1e4 }
125
127
  );
126
128
  return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
@@ -208,6 +210,46 @@ ${error.stderr ?? ""}`
208
210
  };
209
211
  }
210
212
  );
213
+ server.tool(
214
+ "edit_block",
215
+ "\uD30C\uC77C\uC758 \uD2B9\uC815 \uD14D\uC2A4\uD2B8 \uBE14\uB85D\uC744 \uC0C8 \uD14D\uC2A4\uD2B8\uB85C \uAD50\uCCB4 (diff \uAE30\uBC18 \uBD80\uBD84 \uC218\uC815)",
216
+ {
217
+ path: import_zod.z.string().describe("\uD30C\uC77C \uACBD\uB85C"),
218
+ old_string: import_zod.z.string().describe("\uAD50\uCCB4\uD560 \uAE30\uC874 \uD14D\uC2A4\uD2B8 (\uC815\uD655\uD788 \uC77C\uCE58\uD574\uC57C \uD568)"),
219
+ new_string: import_zod.z.string().describe("\uC0C8 \uD14D\uC2A4\uD2B8"),
220
+ replace_all: import_zod.z.boolean().optional().default(false).describe("true\uBA74 \uBAA8\uB4E0 \uB9E4\uCE6D \uAD50\uCCB4, false\uBA74 \uCCAB \uBC88\uC9F8\uB9CC")
221
+ },
222
+ async ({ path: filePath, old_string, new_string, replace_all }) => {
223
+ const content = await import_promises.default.readFile(filePath, "utf-8");
224
+ if (!content.includes(old_string)) {
225
+ throw new Error(`old_string\uC744 \uD30C\uC77C\uC5D0\uC11C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}`);
226
+ }
227
+ let count = 0;
228
+ let pos = 0;
229
+ while ((pos = content.indexOf(old_string, pos)) !== -1) {
230
+ count++;
231
+ pos += old_string.length;
232
+ }
233
+ if (!replace_all && count > 1) {
234
+ throw new Error(
235
+ `\uB9E4\uCE6D\uC774 ${count}\uAC1C\uC785\uB2C8\uB2E4. replace_all\uC744 true\uB85C \uD558\uAC70\uB098 \uB354 \uB113\uC740 \uCEE8\uD14D\uC2A4\uD2B8\uB97C \uD3EC\uD568\uD558\uC138\uC694.`
236
+ );
237
+ }
238
+ let result;
239
+ let replaced;
240
+ if (replace_all) {
241
+ result = content.split(old_string).join(new_string);
242
+ replaced = count;
243
+ } else {
244
+ result = content.replace(old_string, new_string);
245
+ replaced = 1;
246
+ }
247
+ await import_promises.default.writeFile(filePath, result, "utf-8");
248
+ return {
249
+ content: [{ type: "text", text: `\uAD50\uCCB4 \uC644\uB8CC (${replaced}\uAC1C \uBCC0\uACBD\uB428)` }]
250
+ };
251
+ }
252
+ );
211
253
  }
212
254
  };
213
255
 
@@ -217,6 +259,17 @@ var import_zod2 = require("zod");
217
259
  var BrowserTools = class {
218
260
  browser = null;
219
261
  page = null;
262
+ // 동시 요청 시 race condition 방지용 직렬화 락
263
+ lock = Promise.resolve();
264
+ withLock(fn) {
265
+ let release;
266
+ const next = new Promise((r) => {
267
+ release = r;
268
+ });
269
+ const current = this.lock;
270
+ this.lock = this.lock.then(() => next);
271
+ return current.then(() => fn()).finally(() => release());
272
+ }
220
273
  async init() {
221
274
  try {
222
275
  this.browser = await import_playwright.chromium.launch({ headless: true });
@@ -239,22 +292,22 @@ var BrowserTools = class {
239
292
  "browser_navigate",
240
293
  "URL\uB85C \uC774\uB3D9",
241
294
  { url: import_zod2.z.string().describe("\uC774\uB3D9\uD560 URL") },
242
- async ({ url }) => {
295
+ ({ url }) => this.withLock(async () => {
243
296
  const page = requirePage();
244
297
  await page.goto(url, { waitUntil: "domcontentloaded" });
245
298
  return {
246
299
  content: [{ type: "text", text: `\uC774\uB3D9 \uC644\uB8CC: ${page.url()}` }]
247
300
  };
248
- }
301
+ })
249
302
  );
250
303
  server.tool(
251
304
  "browser_click",
252
305
  "\uC694\uC18C \uD074\uB9AD",
253
306
  { selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790") },
254
- async ({ selector }) => {
307
+ ({ selector }) => this.withLock(async () => {
255
308
  await requirePage().click(selector);
256
309
  return { content: [{ type: "text", text: "\uD074\uB9AD \uC644\uB8CC" }] };
257
- }
310
+ })
258
311
  );
259
312
  server.tool(
260
313
  "browser_type",
@@ -264,12 +317,12 @@ var BrowserTools = class {
264
317
  text: import_zod2.z.string().describe("\uC785\uB825\uD560 \uD14D\uC2A4\uD2B8"),
265
318
  clear: import_zod2.z.boolean().optional().default(false).describe("\uAE30\uC874 \uB0B4\uC6A9 \uC0AD\uC81C \uD6C4 \uC785\uB825")
266
319
  },
267
- async ({ selector, text, clear }) => {
320
+ ({ selector, text, clear }) => this.withLock(async () => {
268
321
  const page = requirePage();
269
322
  if (clear) await page.fill(selector, text);
270
323
  else await page.type(selector, text);
271
324
  return { content: [{ type: "text", text: "\uC785\uB825 \uC644\uB8CC" }] };
272
- }
325
+ })
273
326
  );
274
327
  server.tool(
275
328
  "browser_screenshot",
@@ -278,7 +331,7 @@ var BrowserTools = class {
278
331
  path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
279
332
  full_page: import_zod2.z.boolean().optional().default(false)
280
333
  },
281
- async ({ path: path2, full_page }) => {
334
+ ({ path: path2, full_page }) => this.withLock(async () => {
282
335
  const page = requirePage();
283
336
  const screenshot = await page.screenshot({
284
337
  path: path2 ?? void 0,
@@ -296,13 +349,13 @@ var BrowserTools = class {
296
349
  }
297
350
  ]
298
351
  };
299
- }
352
+ })
300
353
  );
301
354
  server.tool(
302
355
  "browser_snapshot",
303
356
  "\uD398\uC774\uC9C0 \uC811\uADFC\uC131 \uD2B8\uB9AC \uC870\uD68C (\uAD6C\uC870 \uD30C\uC545\uC6A9)",
304
357
  {},
305
- async () => {
358
+ () => this.withLock(async () => {
306
359
  const page = requirePage();
307
360
  const snapshot = await page.locator("body").ariaSnapshot();
308
361
  return {
@@ -310,29 +363,29 @@ var BrowserTools = class {
310
363
  { type: "text", text: snapshot }
311
364
  ]
312
365
  };
313
- }
366
+ })
314
367
  );
315
368
  server.tool(
316
369
  "browser_evaluate",
317
370
  "JavaScript \uC2E4\uD589",
318
371
  { code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
319
- async ({ code }) => {
372
+ ({ code }) => this.withLock(async () => {
320
373
  const result = await requirePage().evaluate(code);
321
374
  return {
322
375
  content: [
323
376
  { type: "text", text: JSON.stringify(result, null, 2) }
324
377
  ]
325
378
  };
326
- }
379
+ })
327
380
  );
328
381
  server.tool(
329
382
  "browser_pdf",
330
383
  "\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
331
384
  { path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
332
- async ({ path: path2 }) => {
385
+ ({ path: path2 }) => this.withLock(async () => {
333
386
  await requirePage().pdf({ path: path2 });
334
387
  return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path2}` }] };
335
- }
388
+ })
336
389
  );
337
390
  }
338
391
  };
@@ -345,7 +398,11 @@ var import_util2 = require("util");
345
398
  var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
346
399
  async function readNotebook(filePath) {
347
400
  const raw = await import_promises2.default.readFile(filePath, "utf-8");
348
- return JSON.parse(raw);
401
+ try {
402
+ return JSON.parse(raw);
403
+ } catch {
404
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 .ipynb \uD30C\uC77C: ${filePath}`);
405
+ }
349
406
  }
350
407
  async function writeNotebook(filePath, nb) {
351
408
  await import_promises2.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
@@ -421,6 +478,54 @@ var NotebookTools = class {
421
478
  throw new Error("jupyter\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4. \uC124\uCE58 \uD6C4 \uB2E4\uC2DC \uC2DC\uB3C4\uD558\uC138\uC694: pip install jupyter");
422
479
  }
423
480
  );
481
+ server.tool(
482
+ "notebook_add_cell",
483
+ "\uB178\uD2B8\uBD81\uC5D0 \uC0C8 \uC140 \uCD94\uAC00",
484
+ {
485
+ path: import_zod3.z.string().describe(".ipynb \uD30C\uC77C \uACBD\uB85C"),
486
+ cell_type: import_zod3.z.enum(["code", "markdown"]).describe("\uC140 \uD0C0\uC785"),
487
+ source: import_zod3.z.string().describe("\uC140 \uC18C\uC2A4 \uB0B4\uC6A9"),
488
+ position: import_zod3.z.number().optional().describe("\uC0BD\uC785 \uC704\uCE58(0-based). \uC5C6\uC73C\uBA74 \uB9E8 \uB05D\uC5D0 \uCD94\uAC00")
489
+ },
490
+ async ({ path: filePath, cell_type: cellType, source, position }) => {
491
+ const nb = await readNotebook(filePath);
492
+ const newCell = {
493
+ cell_type: cellType,
494
+ source: source.split("\n").map((l, i, arr) => i < arr.length - 1 ? l + "\n" : l),
495
+ metadata: {},
496
+ outputs: cellType === "code" ? [] : void 0,
497
+ execution_count: cellType === "code" ? null : void 0
498
+ };
499
+ let actualIndex;
500
+ if (position === void 0 || position === null) {
501
+ nb.cells.push(newCell);
502
+ actualIndex = nb.cells.length - 1;
503
+ } else {
504
+ const clamped = Math.max(0, Math.min(position, nb.cells.length));
505
+ nb.cells.splice(clamped, 0, newCell);
506
+ actualIndex = clamped;
507
+ }
508
+ await writeNotebook(filePath, nb);
509
+ return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})` }] };
510
+ }
511
+ );
512
+ server.tool(
513
+ "notebook_delete_cell",
514
+ "\uB178\uD2B8\uBD81 \uD2B9\uC815 \uC140 \uC0AD\uC81C",
515
+ {
516
+ path: import_zod3.z.string().describe(".ipynb \uD30C\uC77C \uACBD\uB85C"),
517
+ cell_index: import_zod3.z.number().describe("\uC0AD\uC81C\uD560 \uC140 \uC778\uB371\uC2A4 (0-based)")
518
+ },
519
+ async ({ path: filePath, cell_index }) => {
520
+ const nb = await readNotebook(filePath);
521
+ if (cell_index < 0 || cell_index >= nb.cells.length) {
522
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uC140 \uC778\uB371\uC2A4: ${cell_index}`);
523
+ }
524
+ nb.cells.splice(cell_index, 1);
525
+ await writeNotebook(filePath, nb);
526
+ return { content: [{ type: "text", text: `\uC140 \uC0AD\uC81C \uC644\uB8CC (index: ${cell_index})` }] };
527
+ }
528
+ );
424
529
  }
425
530
  };
426
531
 
@@ -429,13 +534,13 @@ var import_child_process3 = require("child_process");
429
534
  var import_util3 = require("util");
430
535
  var import_zod4 = require("zod");
431
536
  var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
537
+ var screenRecordPid = null;
432
538
  function platform() {
433
539
  if (process.platform === "darwin") return "mac";
434
540
  if (process.platform === "win32") return "win";
435
541
  return "linux";
436
542
  }
437
543
  var DeviceTools = class {
438
- screenRecordPid = null;
439
544
  register(server) {
440
545
  server.tool(
441
546
  "screen_capture",
@@ -445,15 +550,24 @@ var DeviceTools = class {
445
550
  },
446
551
  async ({ output_path }) => {
447
552
  const p = platform();
553
+ const isTmp = !output_path;
448
554
  const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
449
555
  const cmd = {
450
556
  mac: `screencapture -x "${tmpPath}"`,
451
557
  win: `nircmd.exe savescreenshot "${tmpPath}"`,
452
558
  linux: `scrot "${tmpPath}"`
453
559
  }[p];
454
- await execAsync3(cmd);
455
- const { readFileSync } = await import("fs");
560
+ try {
561
+ await execAsync3(cmd);
562
+ } catch (err) {
563
+ throw new Error(`\uD654\uBA74 \uCEA1\uCC98 \uC2E4\uD328: ${err.message}`);
564
+ }
565
+ const { readFileSync, unlinkSync } = await import("fs");
456
566
  const data = readFileSync(tmpPath).toString("base64");
567
+ if (isTmp) try {
568
+ unlinkSync(tmpPath);
569
+ } catch {
570
+ }
457
571
  return {
458
572
  content: [{ type: "image", data, mimeType: "image/png" }]
459
573
  };
@@ -467,15 +581,24 @@ var DeviceTools = class {
467
581
  },
468
582
  async ({ output_path }) => {
469
583
  const p = platform();
584
+ const isTmp = !output_path;
470
585
  const tmpPath = output_path ?? `/tmp/junis_cam_${Date.now()}.jpg`;
471
586
  const cmd = {
472
587
  mac: `imagesnap "${tmpPath}"`,
473
588
  win: `ffmpeg -f dshow -i video="Default" -frames:v 1 "${tmpPath}"`,
474
589
  linux: `fswebcam -r 1280x720 "${tmpPath}"`
475
590
  }[p];
476
- await execAsync3(cmd);
477
- const { readFileSync } = await import("fs");
591
+ try {
592
+ await execAsync3(cmd);
593
+ } catch (err) {
594
+ throw new Error(`\uCE74\uBA54\uB77C \uCD2C\uC601 \uC2E4\uD328: ${err.message}`);
595
+ }
596
+ const { readFileSync, unlinkSync } = await import("fs");
478
597
  const data = readFileSync(tmpPath).toString("base64");
598
+ if (isTmp) try {
599
+ unlinkSync(tmpPath);
600
+ } catch {
601
+ }
479
602
  return {
480
603
  content: [{ type: "image", data, mimeType: "image/jpeg" }]
481
604
  };
@@ -535,7 +658,7 @@ var DeviceTools = class {
535
658
  async ({ action, output_path }) => {
536
659
  const p = platform();
537
660
  if (action === "start") {
538
- if (this.screenRecordPid) {
661
+ if (screenRecordPid) {
539
662
  return { content: [{ type: "text", text: "\uC774\uBBF8 \uB179\uD654 \uC911\uC785\uB2C8\uB2E4." }] };
540
663
  }
541
664
  const tmpPath = output_path ?? `/tmp/junis_record_${Date.now()}.mp4`;
@@ -543,17 +666,17 @@ var DeviceTools = class {
543
666
  const cmd = p === "mac" ? ["screencapture", ["-v", tmpPath]] : ["ffmpeg", ["-f", p === "win" ? "gdigrab" : "x11grab", "-i", p === "win" ? "desktop" : ":0.0", tmpPath]];
544
667
  const child = spawn(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
545
668
  child.unref();
546
- this.screenRecordPid = child.pid ?? null;
547
- return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${this.screenRecordPid})` }] };
669
+ screenRecordPid = child.pid ?? null;
670
+ return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${screenRecordPid})` }] };
548
671
  } else {
549
- if (!this.screenRecordPid) {
672
+ if (!screenRecordPid) {
550
673
  return { content: [{ type: "text", text: "\uD604\uC7AC \uB179\uD654 \uC911\uC774 \uC544\uB2D9\uB2C8\uB2E4." }] };
551
674
  }
552
675
  try {
553
- process.kill(this.screenRecordPid, "SIGINT");
676
+ process.kill(screenRecordPid, "SIGINT");
554
677
  } catch {
555
678
  }
556
- this.screenRecordPid = null;
679
+ screenRecordPid = null;
557
680
  return { content: [{ type: "text", text: "\uB179\uD654 \uC911\uC9C0\uB428." }] };
558
681
  }
559
682
  }
@@ -572,12 +695,15 @@ var DeviceTools = class {
572
695
  } catch {
573
696
  }
574
697
  }
575
- const res = await fetch("https://ipapi.co/json/");
698
+ const res = await fetch("http://ip-api.com/json/");
576
699
  const data = await res.json();
700
+ if (data.status !== "success") {
701
+ throw new Error(`IP \uC704\uCE58 \uC870\uD68C \uC2E4\uD328: ${data.message ?? data.status}`);
702
+ }
577
703
  return {
578
704
  content: [{
579
705
  type: "text",
580
- text: `\uC704\uB3C4: ${data.latitude}, \uACBD\uB3C4: ${data.longitude}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country_name} (IP \uAE30\uBC18 \uCD94\uC815)`
706
+ text: `\uC704\uB3C4: ${data.lat}, \uACBD\uB3C4: ${data.lon}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country} (IP \uAE30\uBC18 \uCD94\uC815)`
581
707
  }]
582
708
  };
583
709
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "junis",
3
- "version": "0.1.8",
3
+ "version": "0.2.2",
4
4
  "description": "One-line device control for AI agents",
5
5
  "bin": {
6
6
  "junis": "dist/cli/index.js"