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.
@@ -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(
@@ -80,8 +81,16 @@ ${error.stderr ?? ""}`
80
81
  encoding: import_zod.z.enum(["utf-8", "base64"]).optional().default("utf-8").describe("\uC778\uCF54\uB529")
81
82
  },
82
83
  async ({ path: filePath, encoding }) => {
83
- const content = await import_promises.default.readFile(filePath, encoding);
84
- return { content: [{ type: "text", text: content }] };
84
+ try {
85
+ const content = await import_promises.default.readFile(filePath, encoding);
86
+ return { content: [{ type: "text", text: content }] };
87
+ } catch (err) {
88
+ const e = err;
89
+ if (e.code === "ENOENT") {
90
+ return { content: [{ type: "text", text: `\u274C \uD30C\uC77C\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${filePath}` }], isError: true };
91
+ }
92
+ return { content: [{ type: "text", text: `\u274C \uD30C\uC77C \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
93
+ }
85
94
  }
86
95
  );
87
96
  server.tool(
@@ -104,9 +113,17 @@ ${error.stderr ?? ""}`
104
113
  path: import_zod.z.string().describe("\uB514\uB809\uD1A0\uB9AC \uACBD\uB85C")
105
114
  },
106
115
  async ({ path: dirPath }) => {
107
- const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
108
- const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
109
- return { content: [{ type: "text", text: lines.join("\n") }] };
116
+ try {
117
+ const entries = await import_promises.default.readdir(dirPath, { withFileTypes: true });
118
+ const lines = entries.map((e) => `${e.isDirectory() ? "\u{1F4C1}" : "\u{1F4C4}"} ${e.name}`);
119
+ return { content: [{ type: "text", text: lines.join("\n") }] };
120
+ } catch (err) {
121
+ const e = err;
122
+ if (e.code === "ENOENT") {
123
+ return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4: ${dirPath}` }], isError: true };
124
+ }
125
+ return { content: [{ type: "text", text: `\u274C \uB514\uB809\uD1A0\uB9AC \uC77D\uAE30 \uC2E4\uD328: ${e.message}` }], isError: true };
126
+ }
110
127
  }
111
128
  );
112
129
  server.tool(
@@ -119,18 +136,20 @@ ${error.stderr ?? ""}`
119
136
  },
120
137
  async ({ pattern, directory, file_pattern }) => {
121
138
  try {
122
- const { stdout } = await execAsync(
123
- `rg --no-heading -n "${pattern}" ${directory}`,
139
+ const { stdout } = await execFileAsync(
140
+ "rg",
141
+ ["--no-heading", "-n", pattern, directory],
124
142
  { timeout: 1e4 }
125
143
  );
126
144
  return { content: [{ type: "text", text: stdout || "\uACB0\uACFC \uC5C6\uC74C" }] };
127
145
  } catch {
128
- const files = await (0, import_glob.glob)(file_pattern, { cwd: directory });
146
+ const safeDirectory = import_path.default.resolve(directory);
147
+ const files = await (0, import_glob.glob)(file_pattern, { cwd: safeDirectory });
129
148
  const results = [];
130
149
  for (const file of files.slice(0, 100)) {
131
150
  try {
132
151
  const content = await import_promises.default.readFile(
133
- import_path.default.join(directory, file),
152
+ import_path.default.join(safeDirectory, file),
134
153
  "utf-8"
135
154
  );
136
155
  const lines = content.split("\n");
@@ -257,6 +276,17 @@ var import_zod2 = require("zod");
257
276
  var BrowserTools = class {
258
277
  browser = null;
259
278
  page = null;
279
+ // 동시 요청 시 race condition 방지용 직렬화 락
280
+ lock = Promise.resolve();
281
+ withLock(fn) {
282
+ let release;
283
+ const next = new Promise((r) => {
284
+ release = r;
285
+ });
286
+ const current = this.lock;
287
+ this.lock = this.lock.then(() => next);
288
+ return current.then(() => fn()).finally(() => release());
289
+ }
260
290
  async init() {
261
291
  try {
262
292
  this.browser = await import_playwright.chromium.launch({ headless: true });
@@ -279,22 +309,22 @@ var BrowserTools = class {
279
309
  "browser_navigate",
280
310
  "URL\uB85C \uC774\uB3D9",
281
311
  { url: import_zod2.z.string().describe("\uC774\uB3D9\uD560 URL") },
282
- async ({ url }) => {
312
+ ({ url }) => this.withLock(async () => {
283
313
  const page = requirePage();
284
314
  await page.goto(url, { waitUntil: "domcontentloaded" });
285
315
  return {
286
316
  content: [{ type: "text", text: `\uC774\uB3D9 \uC644\uB8CC: ${page.url()}` }]
287
317
  };
288
- }
318
+ })
289
319
  );
290
320
  server.tool(
291
321
  "browser_click",
292
322
  "\uC694\uC18C \uD074\uB9AD",
293
323
  { selector: import_zod2.z.string().describe("CSS \uC120\uD0DD\uC790") },
294
- async ({ selector }) => {
324
+ ({ selector }) => this.withLock(async () => {
295
325
  await requirePage().click(selector);
296
326
  return { content: [{ type: "text", text: "\uD074\uB9AD \uC644\uB8CC" }] };
297
- }
327
+ })
298
328
  );
299
329
  server.tool(
300
330
  "browser_type",
@@ -304,12 +334,12 @@ var BrowserTools = class {
304
334
  text: import_zod2.z.string().describe("\uC785\uB825\uD560 \uD14D\uC2A4\uD2B8"),
305
335
  clear: import_zod2.z.boolean().optional().default(false).describe("\uAE30\uC874 \uB0B4\uC6A9 \uC0AD\uC81C \uD6C4 \uC785\uB825")
306
336
  },
307
- async ({ selector, text, clear }) => {
337
+ ({ selector, text, clear }) => this.withLock(async () => {
308
338
  const page = requirePage();
309
339
  if (clear) await page.fill(selector, text);
310
340
  else await page.type(selector, text);
311
341
  return { content: [{ type: "text", text: "\uC785\uB825 \uC644\uB8CC" }] };
312
- }
342
+ })
313
343
  );
314
344
  server.tool(
315
345
  "browser_screenshot",
@@ -318,7 +348,7 @@ var BrowserTools = class {
318
348
  path: import_zod2.z.string().optional().describe("\uC800\uC7A5 \uACBD\uB85C (\uC5C6\uC73C\uBA74 base64 \uBC18\uD658)"),
319
349
  full_page: import_zod2.z.boolean().optional().default(false)
320
350
  },
321
- async ({ path: path2, full_page }) => {
351
+ ({ path: path2, full_page }) => this.withLock(async () => {
322
352
  const page = requirePage();
323
353
  const screenshot = await page.screenshot({
324
354
  path: path2 ?? void 0,
@@ -336,13 +366,13 @@ var BrowserTools = class {
336
366
  }
337
367
  ]
338
368
  };
339
- }
369
+ })
340
370
  );
341
371
  server.tool(
342
372
  "browser_snapshot",
343
373
  "\uD398\uC774\uC9C0 \uC811\uADFC\uC131 \uD2B8\uB9AC \uC870\uD68C (\uAD6C\uC870 \uD30C\uC545\uC6A9)",
344
374
  {},
345
- async () => {
375
+ () => this.withLock(async () => {
346
376
  const page = requirePage();
347
377
  const snapshot = await page.locator("body").ariaSnapshot();
348
378
  return {
@@ -350,29 +380,36 @@ var BrowserTools = class {
350
380
  { type: "text", text: snapshot }
351
381
  ]
352
382
  };
353
- }
383
+ })
354
384
  );
355
385
  server.tool(
356
386
  "browser_evaluate",
357
387
  "JavaScript \uC2E4\uD589",
358
388
  { code: import_zod2.z.string().describe("\uC2E4\uD589\uD560 JavaScript \uCF54\uB4DC") },
359
- async ({ code }) => {
360
- const result = await requirePage().evaluate(code);
361
- return {
362
- content: [
363
- { type: "text", text: JSON.stringify(result, null, 2) }
364
- ]
365
- };
366
- }
389
+ ({ code }) => this.withLock(async () => {
390
+ try {
391
+ const result = await requirePage().evaluate(code);
392
+ return {
393
+ content: [
394
+ { type: "text", text: typeof result === "string" ? result : JSON.stringify(result, null, 2) }
395
+ ]
396
+ };
397
+ } catch (err) {
398
+ return {
399
+ content: [{ type: "text", text: `\u274C JavaScript \uC2E4\uD589 \uC624\uB958: ${err.message}` }],
400
+ isError: true
401
+ };
402
+ }
403
+ })
367
404
  );
368
405
  server.tool(
369
406
  "browser_pdf",
370
407
  "\uD604\uC7AC \uD398\uC774\uC9C0 PDF \uC800\uC7A5",
371
408
  { path: import_zod2.z.string().describe("\uC800\uC7A5 \uACBD\uB85C (.pdf)") },
372
- async ({ path: path2 }) => {
409
+ ({ path: path2 }) => this.withLock(async () => {
373
410
  await requirePage().pdf({ path: path2 });
374
411
  return { content: [{ type: "text", text: `PDF \uC800\uC7A5 \uC644\uB8CC: ${path2}` }] };
375
- }
412
+ })
376
413
  );
377
414
  }
378
415
  };
@@ -385,7 +422,11 @@ var import_util2 = require("util");
385
422
  var execAsync2 = (0, import_util2.promisify)(import_child_process2.exec);
386
423
  async function readNotebook(filePath) {
387
424
  const raw = await import_promises2.default.readFile(filePath, "utf-8");
388
- return JSON.parse(raw);
425
+ try {
426
+ return JSON.parse(raw);
427
+ } catch {
428
+ throw new Error(`\uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 Jupyter \uB178\uD2B8\uBD81 \uD30C\uC77C\uC785\uB2C8\uB2E4: ${filePath}`);
429
+ }
389
430
  }
390
431
  async function writeNotebook(filePath, nb) {
391
432
  await import_promises2.default.writeFile(filePath, JSON.stringify(nb, null, 1), "utf-8");
@@ -480,15 +521,21 @@ var NotebookTools = class {
480
521
  execution_count: cellType === "code" ? null : void 0
481
522
  };
482
523
  let actualIndex;
524
+ let warning = "";
483
525
  if (position === void 0 || position === null) {
484
526
  nb.cells.push(newCell);
485
527
  actualIndex = nb.cells.length - 1;
528
+ } else if (position > nb.cells.length) {
529
+ nb.cells.push(newCell);
530
+ actualIndex = nb.cells.length - 1;
531
+ warning = ` (\uACBD\uACE0: position ${position}\uC774 \uBC94\uC704\uB97C \uCD08\uACFC\uD558\uC5EC \uB05D(index: ${actualIndex})\uC5D0 \uCD94\uAC00\uB428)`;
486
532
  } else {
487
- nb.cells.splice(position, 0, newCell);
488
- actualIndex = position;
533
+ const clamped = Math.max(0, position);
534
+ nb.cells.splice(clamped, 0, newCell);
535
+ actualIndex = clamped;
489
536
  }
490
537
  await writeNotebook(filePath, nb);
491
- return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})` }] };
538
+ return { content: [{ type: "text", text: `\uC140 \uCD94\uAC00 \uC644\uB8CC (index: ${actualIndex})${warning}` }] };
492
539
  }
493
540
  );
494
541
  server.tool(
@@ -516,13 +563,13 @@ var import_child_process3 = require("child_process");
516
563
  var import_util3 = require("util");
517
564
  var import_zod4 = require("zod");
518
565
  var execAsync3 = (0, import_util3.promisify)(import_child_process3.exec);
566
+ var screenRecordPid = null;
519
567
  function platform() {
520
568
  if (process.platform === "darwin") return "mac";
521
569
  if (process.platform === "win32") return "win";
522
570
  return "linux";
523
571
  }
524
572
  var DeviceTools = class {
525
- screenRecordPid = null;
526
573
  register(server) {
527
574
  server.tool(
528
575
  "screen_capture",
@@ -532,15 +579,26 @@ var DeviceTools = class {
532
579
  },
533
580
  async ({ output_path }) => {
534
581
  const p = platform();
582
+ const isTmp = !output_path;
535
583
  const tmpPath = output_path ?? `/tmp/junis_screen_${Date.now()}.png`;
536
584
  const cmd = {
537
585
  mac: `screencapture -x "${tmpPath}"`,
538
586
  win: `nircmd.exe savescreenshot "${tmpPath}"`,
539
587
  linux: `scrot "${tmpPath}"`
540
588
  }[p];
541
- await execAsync3(cmd);
542
- const { readFileSync } = await import("fs");
589
+ try {
590
+ await execAsync3(cmd);
591
+ } catch (err) {
592
+ throw new Error(`\uD654\uBA74 \uCEA1\uCC98 \uC2E4\uD328: ${err.message}`);
593
+ }
594
+ const { readFileSync, unlinkSync } = await import("fs");
543
595
  const data = readFileSync(tmpPath).toString("base64");
596
+ if (isTmp) {
597
+ try {
598
+ unlinkSync(tmpPath);
599
+ } catch {
600
+ }
601
+ }
544
602
  return {
545
603
  content: [{ type: "image", data, mimeType: "image/png" }]
546
604
  };
@@ -554,15 +612,31 @@ var DeviceTools = class {
554
612
  },
555
613
  async ({ output_path }) => {
556
614
  const p = platform();
615
+ const isTmp = !output_path;
557
616
  const tmpPath = output_path ?? `/tmp/junis_cam_${Date.now()}.jpg`;
558
617
  const cmd = {
559
618
  mac: `imagesnap "${tmpPath}"`,
560
619
  win: `ffmpeg -f dshow -i video="Default" -frames:v 1 "${tmpPath}"`,
561
620
  linux: `fswebcam -r 1280x720 "${tmpPath}"`
562
621
  }[p];
563
- await execAsync3(cmd);
564
- const { readFileSync } = await import("fs");
622
+ try {
623
+ await execAsync3(cmd);
624
+ } catch (err) {
625
+ const e = err;
626
+ return {
627
+ content: [{ type: "text", text: `\u274C \uCE74\uBA54\uB77C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uAC70\uB098 \uC811\uADFC\uD560 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
628
+ \uC6D0\uC778: ${e.message}
629
+
630
+ \uCE74\uBA54\uB77C\uAC00 \uC5F0\uACB0\uB418\uC5B4 \uC788\uB294\uC9C0 \uD655\uC778\uD558\uC138\uC694.` }],
631
+ isError: true
632
+ };
633
+ }
634
+ const { readFileSync, unlinkSync } = await import("fs");
565
635
  const data = readFileSync(tmpPath).toString("base64");
636
+ if (isTmp) try {
637
+ unlinkSync(tmpPath);
638
+ } catch {
639
+ }
566
640
  return {
567
641
  content: [{ type: "image", data, mimeType: "image/jpeg" }]
568
642
  };
@@ -577,11 +651,17 @@ var DeviceTools = class {
577
651
  },
578
652
  async ({ title, message }) => {
579
653
  const p = platform();
580
- const cmd = {
581
- mac: `osascript -e 'display notification "${message}" with title "${title}"'`,
582
- 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())"`,
583
- linux: `notify-send "${title}" "${message}"`
584
- }[p];
654
+ let cmd;
655
+ if (p === "win") {
656
+ const script = `Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.MessageBox]::Show('${message.replace(/'/g, "''")}', '${title.replace(/'/g, "''")}')`;
657
+ const encoded = Buffer.from(script, "utf16le").toString("base64");
658
+ cmd = `powershell -NoProfile -EncodedCommand ${encoded}`;
659
+ } else {
660
+ cmd = {
661
+ mac: `osascript -e 'display notification "${message.replace(/"/g, '\\"')}" with title "${title.replace(/"/g, '\\"')}"'`,
662
+ linux: `notify-send "${title.replace(/"/g, '\\"')}" "${message.replace(/"/g, '\\"')}"`
663
+ }[p] ?? "";
664
+ }
585
665
  await execAsync3(cmd);
586
666
  return { content: [{ type: "text", text: "\uC54C\uB9BC \uC804\uC1A1 \uC644\uB8CC" }] };
587
667
  }
@@ -622,7 +702,7 @@ var DeviceTools = class {
622
702
  async ({ action, output_path }) => {
623
703
  const p = platform();
624
704
  if (action === "start") {
625
- if (this.screenRecordPid) {
705
+ if (screenRecordPid) {
626
706
  return { content: [{ type: "text", text: "\uC774\uBBF8 \uB179\uD654 \uC911\uC785\uB2C8\uB2E4." }] };
627
707
  }
628
708
  const tmpPath = output_path ?? `/tmp/junis_record_${Date.now()}.mp4`;
@@ -630,17 +710,18 @@ var DeviceTools = class {
630
710
  const cmd = p === "mac" ? ["screencapture", ["-v", tmpPath]] : ["ffmpeg", ["-f", p === "win" ? "gdigrab" : "x11grab", "-i", p === "win" ? "desktop" : ":0.0", tmpPath]];
631
711
  const child = spawn(cmd[0], cmd[1], { detached: true, stdio: "ignore" });
632
712
  child.unref();
633
- this.screenRecordPid = child.pid ?? null;
634
- return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${this.screenRecordPid})` }] };
713
+ screenRecordPid = child.pid ?? null;
714
+ return { content: [{ type: "text", text: `\uB179\uD654 \uC2DC\uC791\uB428. \uC800\uC7A5 \uACBD\uB85C: ${tmpPath} (PID: ${screenRecordPid})` }] };
635
715
  } else {
636
- if (!this.screenRecordPid) {
716
+ if (!screenRecordPid) {
637
717
  return { content: [{ type: "text", text: "\uD604\uC7AC \uB179\uD654 \uC911\uC774 \uC544\uB2D9\uB2C8\uB2E4." }] };
638
718
  }
639
719
  try {
640
- process.kill(this.screenRecordPid, "SIGINT");
720
+ process.kill(screenRecordPid, "SIGINT");
721
+ await new Promise((r) => setTimeout(r, 1e3));
641
722
  } catch {
642
723
  }
643
- this.screenRecordPid = null;
724
+ screenRecordPid = null;
644
725
  return { content: [{ type: "text", text: "\uB179\uD654 \uC911\uC9C0\uB428." }] };
645
726
  }
646
727
  }
@@ -659,12 +740,15 @@ var DeviceTools = class {
659
740
  } catch {
660
741
  }
661
742
  }
662
- const res = await fetch("https://ipapi.co/json/");
743
+ const res = await fetch("http://ip-api.com/json/");
663
744
  const data = await res.json();
745
+ if (data.status !== "success") {
746
+ throw new Error(`IP \uC704\uCE58 \uC870\uD68C \uC2E4\uD328: ${data.message ?? data.status}`);
747
+ }
664
748
  return {
665
749
  content: [{
666
750
  type: "text",
667
- text: `\uC704\uB3C4: ${data.latitude}, \uACBD\uB3C4: ${data.longitude}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country_name} (IP \uAE30\uBC18 \uCD94\uC815)`
751
+ text: `\uC704\uB3C4: ${data.lat}, \uACBD\uB3C4: ${data.lon}, \uB3C4\uC2DC: ${data.city}, \uAD6D\uAC00: ${data.country} (IP \uAE30\uBC18 \uCD94\uC815)`
668
752
  }]
669
753
  };
670
754
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "junis",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "One-line device control for AI agents",
5
5
  "bin": {
6
6
  "junis": "dist/cli/index.js"