junis 0.3.14 → 0.3.15

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
@@ -48,6 +48,7 @@ var JUNIS_WEB = (() => {
48
48
  if (u.hostname === "localhost" || u.hostname === "127.0.0.1") {
49
49
  return `${u.protocol}//${u.hostname}:3000`;
50
50
  }
51
+ return `${u.protocol}//${u.hostname}`;
51
52
  } catch {
52
53
  }
53
54
  }
@@ -417,9 +418,26 @@ var RelayClient = class {
417
418
  const filename = `screenshot.${(item.mimeType || "image/png").split("/")[1] || "bin"}`;
418
419
  content[i] = { type: "text", text: `[\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC2E4\uD328: ${String(err)}. \uD30C\uC77C\uBA85: ${filename}]` };
419
420
  }
421
+ } else if (item.type === "text" && typeof item.text === "string" && item.text.startsWith("__SHARE__:")) {
422
+ try {
423
+ const parts = item.text.split(":");
424
+ const shareFilename = parts[1];
425
+ const shareContentType = parts[2];
426
+ const shareBase64 = parts.slice(3).join(":");
427
+ const url = await uploadLargeFile(this, shareBase64, shareFilename, shareContentType);
428
+ content[i] = { type: "text", text: url };
429
+ } catch (err) {
430
+ console.error("Failed to upload shared file:", err);
431
+ const parts = item.text.split(":");
432
+ content[i] = { type: "text", text: `[\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC2E4\uD328: ${String(err)}. \uD30C\uC77C\uBA85: ${parts[1] || "unknown"}]` };
433
+ }
420
434
  } else if (item.type === "text" && typeof item.text === "string" && isLargeBase64(item.text) && /^[A-Za-z0-9+/\n\r]+=*$/.test(item.text.trim())) {
421
435
  try {
422
436
  const contentType = detectContentType(item.text);
437
+ if (contentType === "application/octet-stream") {
438
+ content[i] = { type: "text", text: "[Binary file detected. Use the share_file tool to provide a download link to the user. Do not display this message.]" };
439
+ continue;
440
+ }
423
441
  const ext = contentType.split("/")[1] || "bin";
424
442
  const url = await uploadLargeFile(this, item.text, `file.${ext}`, contentType);
425
443
  content[i] = { type: "text", text: url };
@@ -487,6 +505,7 @@ var toolPermissions = {
487
505
  desktop_list_windows: "auto",
488
506
  cron_list: "auto",
489
507
  read_file: "auto",
508
+ share_file: "auto",
490
509
  list_directory: "auto",
491
510
  list_processes: "auto",
492
511
  search_code: "auto",
@@ -999,6 +1018,76 @@ ${error.stderr ?? ""}`
999
1018
  }
1000
1019
  }
1001
1020
  );
1021
+ server.tool(
1022
+ "share_file",
1023
+ [
1024
+ "Upload a local file to cloud storage and return a downloadable URL.",
1025
+ "",
1026
+ "Use this tool when:",
1027
+ "- The user wants to see, receive, or download any file (including text files like .py, .js, etc.)",
1028
+ "- The user wants to share a file",
1029
+ "- The file is binary (PDF, images, audio, video, archives, etc.)",
1030
+ "",
1031
+ "Use read_file instead ONLY when the user explicitly wants to see the text contents/code inside a file",
1032
+ `in the conversation (e.g. "show me the code", "what's in this file", "read this file").`
1033
+ ].join("\n"),
1034
+ {
1035
+ path: z.string().describe("Absolute or relative file path to share")
1036
+ },
1037
+ async ({ path: filePath }) => {
1038
+ try {
1039
+ const buffer = await fs2.readFile(filePath);
1040
+ const base64 = buffer.toString("base64");
1041
+ const filename = path2.basename(filePath);
1042
+ const extMimeMap = {
1043
+ ".py": "text/x-python; charset=utf-8",
1044
+ ".js": "text/javascript; charset=utf-8",
1045
+ ".ts": "text/typescript; charset=utf-8",
1046
+ ".jsx": "text/javascript; charset=utf-8",
1047
+ ".tsx": "text/typescript; charset=utf-8",
1048
+ ".html": "text/html; charset=utf-8",
1049
+ ".css": "text/css; charset=utf-8",
1050
+ ".json": "application/json; charset=utf-8",
1051
+ ".md": "text/markdown; charset=utf-8",
1052
+ ".txt": "text/plain; charset=utf-8",
1053
+ ".csv": "text/csv; charset=utf-8",
1054
+ ".xml": "application/xml; charset=utf-8",
1055
+ ".yaml": "text/yaml; charset=utf-8",
1056
+ ".yml": "text/yaml; charset=utf-8",
1057
+ ".sh": "text/x-shellscript; charset=utf-8",
1058
+ ".bash": "text/x-shellscript; charset=utf-8",
1059
+ ".pdf": "application/pdf",
1060
+ ".png": "image/png",
1061
+ ".jpg": "image/jpeg",
1062
+ ".jpeg": "image/jpeg",
1063
+ ".gif": "image/gif",
1064
+ ".webp": "image/webp",
1065
+ ".svg": "image/svg+xml",
1066
+ ".mp4": "video/mp4",
1067
+ ".mp3": "audio/mpeg",
1068
+ ".wav": "audio/wav",
1069
+ ".zip": "application/zip",
1070
+ ".tar": "application/x-tar",
1071
+ ".gz": "application/gzip",
1072
+ ".doc": "application/msword",
1073
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1074
+ ".xls": "application/vnd.ms-excel",
1075
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1076
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
1077
+ };
1078
+ const ext = path2.extname(filePath).toLowerCase();
1079
+ const contentType = extMimeMap[ext] || "application/octet-stream";
1080
+ const sharePayload = `__SHARE__:${filename}:${contentType}:${base64}`;
1081
+ return { content: [{ type: "text", text: sharePayload }] };
1082
+ } catch (err) {
1083
+ const e = err;
1084
+ if (e.code === "ENOENT") {
1085
+ return { content: [{ type: "text", text: `\u274C File not found: ${filePath}` }], isError: true };
1086
+ }
1087
+ return { content: [{ type: "text", text: `\u274C Failed to read file: ${e.message}` }], isError: true };
1088
+ }
1089
+ }
1090
+ );
1002
1091
  }
1003
1092
  };
1004
1093
 
@@ -1838,44 +1927,62 @@ async function openSettingsFor(permName) {
1838
1927
  });
1839
1928
  }
1840
1929
  }
1841
- async function guideTerminalPermissions(missing) {
1842
- const termApp = detectTerminalApp();
1843
- const missingNames = missing.map((p) => p.name).join(", ");
1844
- for (const p of missing) {
1845
- await openSettingsFor(p.name);
1930
+ var PERMISSION_TRIGGER = {
1931
+ Accessibility: "import ApplicationServices; let opts = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary; AXIsProcessTrustedWithOptions(opts)",
1932
+ "Screen Recording": "import CoreGraphics; CGRequestScreenCaptureAccess()"
1933
+ };
1934
+ async function triggerPermissionPrompt(permName) {
1935
+ const code = PERMISSION_TRIGGER[permName];
1936
+ if (!code) return;
1937
+ try {
1938
+ await execFileAsync4("swift", ["-e", code], { timeout: 15e3 });
1939
+ } catch {
1846
1940
  }
1847
- console.log(
1848
- `\u26A0\uFE0F Desktop tools need permissions. Please toggle ON '${termApp}' in the Settings window.`
1849
- );
1850
- console.log(` Missing: ${missingNames}`);
1851
- for (const p of missing) {
1852
- console.log(` \u2192 ${p.grantInstructions}`);
1941
+ }
1942
+ async function waitForPermission(permName, totalSeconds, openSettingsAfterSec) {
1943
+ const pollInterval = 5;
1944
+ let settingsOpened = false;
1945
+ for (let elapsed = 0; elapsed < totalSeconds; elapsed++) {
1946
+ process.stdout.write(`\r \u23F3 ${totalSeconds - elapsed}s remaining...`);
1947
+ if (!settingsOpened && elapsed >= openSettingsAfterSec) {
1948
+ await openSettingsFor(permName);
1949
+ settingsOpened = true;
1950
+ }
1951
+ if (elapsed > 0 && elapsed % pollInterval === 0) {
1952
+ try {
1953
+ const { permissions } = await checkPermissions();
1954
+ const perm = permissions.find((p) => p.name === permName);
1955
+ if (perm && perm.isGranted) {
1956
+ process.stdout.write("\r" + " ".repeat(30) + "\r");
1957
+ return true;
1958
+ }
1959
+ } catch {
1960
+ }
1961
+ }
1962
+ await new Promise((r) => setTimeout(r, 1e3));
1853
1963
  }
1964
+ process.stdout.write("\r" + " ".repeat(30) + "\r");
1965
+ return false;
1966
+ }
1967
+ async function guideTerminalPermissions(missing) {
1968
+ const termApp = detectTerminalApp();
1854
1969
  if (!isInteractive()) {
1970
+ console.log(`\u26A0\uFE0F Desktop tools need permissions for '${termApp}'.`);
1971
+ for (const p of missing) {
1972
+ console.log(` Missing: ${p.name} \u2192 ${p.grantInstructions}`);
1973
+ }
1855
1974
  console.log(" Grant permissions and restart to enable desktop tools.");
1856
1975
  return;
1857
1976
  }
1858
- for (let attempt = 1; attempt <= 2; attempt++) {
1859
- console.log(` \u23F3 Waiting 20 seconds for you to grant permissions... (attempt ${attempt}/2)`);
1860
- for (let i = 20; i > 0; i--) {
1861
- process.stdout.write(`\r \u23F3 ${i}s remaining...`);
1862
- await new Promise((r) => setTimeout(r, 1e3));
1863
- }
1864
- process.stdout.write("\r" + " ".repeat(30) + "\r");
1865
- const recheck = await checkPermissions();
1866
- const stillMissing = recheck.permissions.filter((p) => p.isRequired && !p.isGranted);
1867
- if (stillMissing.length === 0) {
1868
- console.log("\u2705 Permissions granted!");
1869
- return;
1870
- }
1871
- if (attempt < 2) {
1872
- console.log(
1873
- ` \u26A0\uFE0F Still missing: ${stillMissing.map((p) => p.name).join(", ")}. Trying once more...`
1874
- );
1977
+ for (const perm of missing) {
1978
+ console.log(`\u26A0\uFE0F '${termApp}' needs ${perm.name} permission.`);
1979
+ console.log(` \u2192 ${perm.grantInstructions}`);
1980
+ await triggerPermissionPrompt(perm.name);
1981
+ const granted = await waitForPermission(perm.name, 60, 10);
1982
+ if (granted) {
1983
+ console.log(` \u2705 ${perm.name} granted!`);
1875
1984
  } else {
1876
- console.log(
1877
- `\u26A0\uFE0F Still missing: ${stillMissing.map((p) => p.name).join(", ")}. Desktop tools may not work correctly.`
1878
- );
1985
+ console.log(` \u26A0\uFE0F ${perm.name} not granted. Desktop tools may not work correctly.`);
1879
1986
  }
1880
1987
  }
1881
1988
  }
@@ -21,6 +21,7 @@ var toolPermissions = {
21
21
  desktop_list_windows: "auto",
22
22
  cron_list: "auto",
23
23
  read_file: "auto",
24
+ share_file: "auto",
24
25
  list_directory: "auto",
25
26
  list_processes: "auto",
26
27
  search_code: "auto",
@@ -533,6 +534,76 @@ ${error.stderr ?? ""}`
533
534
  }
534
535
  }
535
536
  );
537
+ server.tool(
538
+ "share_file",
539
+ [
540
+ "Upload a local file to cloud storage and return a downloadable URL.",
541
+ "",
542
+ "Use this tool when:",
543
+ "- The user wants to see, receive, or download any file (including text files like .py, .js, etc.)",
544
+ "- The user wants to share a file",
545
+ "- The file is binary (PDF, images, audio, video, archives, etc.)",
546
+ "",
547
+ "Use read_file instead ONLY when the user explicitly wants to see the text contents/code inside a file",
548
+ `in the conversation (e.g. "show me the code", "what's in this file", "read this file").`
549
+ ].join("\n"),
550
+ {
551
+ path: z.string().describe("Absolute or relative file path to share")
552
+ },
553
+ async ({ path: filePath }) => {
554
+ try {
555
+ const buffer = await fs.readFile(filePath);
556
+ const base64 = buffer.toString("base64");
557
+ const filename = path.basename(filePath);
558
+ const extMimeMap = {
559
+ ".py": "text/x-python; charset=utf-8",
560
+ ".js": "text/javascript; charset=utf-8",
561
+ ".ts": "text/typescript; charset=utf-8",
562
+ ".jsx": "text/javascript; charset=utf-8",
563
+ ".tsx": "text/typescript; charset=utf-8",
564
+ ".html": "text/html; charset=utf-8",
565
+ ".css": "text/css; charset=utf-8",
566
+ ".json": "application/json; charset=utf-8",
567
+ ".md": "text/markdown; charset=utf-8",
568
+ ".txt": "text/plain; charset=utf-8",
569
+ ".csv": "text/csv; charset=utf-8",
570
+ ".xml": "application/xml; charset=utf-8",
571
+ ".yaml": "text/yaml; charset=utf-8",
572
+ ".yml": "text/yaml; charset=utf-8",
573
+ ".sh": "text/x-shellscript; charset=utf-8",
574
+ ".bash": "text/x-shellscript; charset=utf-8",
575
+ ".pdf": "application/pdf",
576
+ ".png": "image/png",
577
+ ".jpg": "image/jpeg",
578
+ ".jpeg": "image/jpeg",
579
+ ".gif": "image/gif",
580
+ ".webp": "image/webp",
581
+ ".svg": "image/svg+xml",
582
+ ".mp4": "video/mp4",
583
+ ".mp3": "audio/mpeg",
584
+ ".wav": "audio/wav",
585
+ ".zip": "application/zip",
586
+ ".tar": "application/x-tar",
587
+ ".gz": "application/gzip",
588
+ ".doc": "application/msword",
589
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
590
+ ".xls": "application/vnd.ms-excel",
591
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
592
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
593
+ };
594
+ const ext = path.extname(filePath).toLowerCase();
595
+ const contentType = extMimeMap[ext] || "application/octet-stream";
596
+ const sharePayload = `__SHARE__:${filename}:${contentType}:${base64}`;
597
+ return { content: [{ type: "text", text: sharePayload }] };
598
+ } catch (err) {
599
+ const e = err;
600
+ if (e.code === "ENOENT") {
601
+ return { content: [{ type: "text", text: `\u274C File not found: ${filePath}` }], isError: true };
602
+ }
603
+ return { content: [{ type: "text", text: `\u274C Failed to read file: ${e.message}` }], isError: true };
604
+ }
605
+ }
606
+ );
536
607
  }
537
608
  };
538
609
 
@@ -1372,44 +1443,62 @@ async function openSettingsFor(permName) {
1372
1443
  });
1373
1444
  }
1374
1445
  }
1375
- async function guideTerminalPermissions(missing) {
1376
- const termApp = detectTerminalApp();
1377
- const missingNames = missing.map((p) => p.name).join(", ");
1378
- for (const p of missing) {
1379
- await openSettingsFor(p.name);
1446
+ var PERMISSION_TRIGGER = {
1447
+ Accessibility: "import ApplicationServices; let opts = [kAXTrustedCheckOptionPrompt.takeUnretainedValue() as String: true] as CFDictionary; AXIsProcessTrustedWithOptions(opts)",
1448
+ "Screen Recording": "import CoreGraphics; CGRequestScreenCaptureAccess()"
1449
+ };
1450
+ async function triggerPermissionPrompt(permName) {
1451
+ const code = PERMISSION_TRIGGER[permName];
1452
+ if (!code) return;
1453
+ try {
1454
+ await execFileAsync4("swift", ["-e", code], { timeout: 15e3 });
1455
+ } catch {
1380
1456
  }
1381
- console.log(
1382
- `\u26A0\uFE0F Desktop tools need permissions. Please toggle ON '${termApp}' in the Settings window.`
1383
- );
1384
- console.log(` Missing: ${missingNames}`);
1385
- for (const p of missing) {
1386
- console.log(` \u2192 ${p.grantInstructions}`);
1457
+ }
1458
+ async function waitForPermission(permName, totalSeconds, openSettingsAfterSec) {
1459
+ const pollInterval = 5;
1460
+ let settingsOpened = false;
1461
+ for (let elapsed = 0; elapsed < totalSeconds; elapsed++) {
1462
+ process.stdout.write(`\r \u23F3 ${totalSeconds - elapsed}s remaining...`);
1463
+ if (!settingsOpened && elapsed >= openSettingsAfterSec) {
1464
+ await openSettingsFor(permName);
1465
+ settingsOpened = true;
1466
+ }
1467
+ if (elapsed > 0 && elapsed % pollInterval === 0) {
1468
+ try {
1469
+ const { permissions } = await checkPermissions();
1470
+ const perm = permissions.find((p) => p.name === permName);
1471
+ if (perm && perm.isGranted) {
1472
+ process.stdout.write("\r" + " ".repeat(30) + "\r");
1473
+ return true;
1474
+ }
1475
+ } catch {
1476
+ }
1477
+ }
1478
+ await new Promise((r) => setTimeout(r, 1e3));
1387
1479
  }
1480
+ process.stdout.write("\r" + " ".repeat(30) + "\r");
1481
+ return false;
1482
+ }
1483
+ async function guideTerminalPermissions(missing) {
1484
+ const termApp = detectTerminalApp();
1388
1485
  if (!isInteractive()) {
1486
+ console.log(`\u26A0\uFE0F Desktop tools need permissions for '${termApp}'.`);
1487
+ for (const p of missing) {
1488
+ console.log(` Missing: ${p.name} \u2192 ${p.grantInstructions}`);
1489
+ }
1389
1490
  console.log(" Grant permissions and restart to enable desktop tools.");
1390
1491
  return;
1391
1492
  }
1392
- for (let attempt = 1; attempt <= 2; attempt++) {
1393
- console.log(` \u23F3 Waiting 20 seconds for you to grant permissions... (attempt ${attempt}/2)`);
1394
- for (let i = 20; i > 0; i--) {
1395
- process.stdout.write(`\r \u23F3 ${i}s remaining...`);
1396
- await new Promise((r) => setTimeout(r, 1e3));
1397
- }
1398
- process.stdout.write("\r" + " ".repeat(30) + "\r");
1399
- const recheck = await checkPermissions();
1400
- const stillMissing = recheck.permissions.filter((p) => p.isRequired && !p.isGranted);
1401
- if (stillMissing.length === 0) {
1402
- console.log("\u2705 Permissions granted!");
1403
- return;
1404
- }
1405
- if (attempt < 2) {
1406
- console.log(
1407
- ` \u26A0\uFE0F Still missing: ${stillMissing.map((p) => p.name).join(", ")}. Trying once more...`
1408
- );
1493
+ for (const perm of missing) {
1494
+ console.log(`\u26A0\uFE0F '${termApp}' needs ${perm.name} permission.`);
1495
+ console.log(` \u2192 ${perm.grantInstructions}`);
1496
+ await triggerPermissionPrompt(perm.name);
1497
+ const granted = await waitForPermission(perm.name, 60, 10);
1498
+ if (granted) {
1499
+ console.log(` \u2705 ${perm.name} granted!`);
1409
1500
  } else {
1410
- console.log(
1411
- `\u26A0\uFE0F Still missing: ${stillMissing.map((p) => p.name).join(", ")}. Desktop tools may not work correctly.`
1412
- );
1501
+ console.log(` \u26A0\uFE0F ${perm.name} not granted. Desktop tools may not work correctly.`);
1413
1502
  }
1414
1503
  }
1415
1504
  }
@@ -22,6 +22,7 @@ var toolPermissions = {
22
22
  desktop_list_windows: "auto",
23
23
  cron_list: "auto",
24
24
  read_file: "auto",
25
+ share_file: "auto",
25
26
  list_directory: "auto",
26
27
  list_processes: "auto",
27
28
  search_code: "auto",
@@ -534,6 +535,76 @@ ${error.stderr ?? ""}`
534
535
  }
535
536
  }
536
537
  );
538
+ server.tool(
539
+ "share_file",
540
+ [
541
+ "Upload a local file to cloud storage and return a downloadable URL.",
542
+ "",
543
+ "Use this tool when:",
544
+ "- The user wants to see, receive, or download any file (including text files like .py, .js, etc.)",
545
+ "- The user wants to share a file",
546
+ "- The file is binary (PDF, images, audio, video, archives, etc.)",
547
+ "",
548
+ "Use read_file instead ONLY when the user explicitly wants to see the text contents/code inside a file",
549
+ `in the conversation (e.g. "show me the code", "what's in this file", "read this file").`
550
+ ].join("\n"),
551
+ {
552
+ path: z.string().describe("Absolute or relative file path to share")
553
+ },
554
+ async ({ path: filePath }) => {
555
+ try {
556
+ const buffer = await fs.readFile(filePath);
557
+ const base64 = buffer.toString("base64");
558
+ const filename = path.basename(filePath);
559
+ const extMimeMap = {
560
+ ".py": "text/x-python; charset=utf-8",
561
+ ".js": "text/javascript; charset=utf-8",
562
+ ".ts": "text/typescript; charset=utf-8",
563
+ ".jsx": "text/javascript; charset=utf-8",
564
+ ".tsx": "text/typescript; charset=utf-8",
565
+ ".html": "text/html; charset=utf-8",
566
+ ".css": "text/css; charset=utf-8",
567
+ ".json": "application/json; charset=utf-8",
568
+ ".md": "text/markdown; charset=utf-8",
569
+ ".txt": "text/plain; charset=utf-8",
570
+ ".csv": "text/csv; charset=utf-8",
571
+ ".xml": "application/xml; charset=utf-8",
572
+ ".yaml": "text/yaml; charset=utf-8",
573
+ ".yml": "text/yaml; charset=utf-8",
574
+ ".sh": "text/x-shellscript; charset=utf-8",
575
+ ".bash": "text/x-shellscript; charset=utf-8",
576
+ ".pdf": "application/pdf",
577
+ ".png": "image/png",
578
+ ".jpg": "image/jpeg",
579
+ ".jpeg": "image/jpeg",
580
+ ".gif": "image/gif",
581
+ ".webp": "image/webp",
582
+ ".svg": "image/svg+xml",
583
+ ".mp4": "video/mp4",
584
+ ".mp3": "audio/mpeg",
585
+ ".wav": "audio/wav",
586
+ ".zip": "application/zip",
587
+ ".tar": "application/x-tar",
588
+ ".gz": "application/gzip",
589
+ ".doc": "application/msword",
590
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
591
+ ".xls": "application/vnd.ms-excel",
592
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
593
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation"
594
+ };
595
+ const ext = path.extname(filePath).toLowerCase();
596
+ const contentType = extMimeMap[ext] || "application/octet-stream";
597
+ const sharePayload = `__SHARE__:${filename}:${contentType}:${base64}`;
598
+ return { content: [{ type: "text", text: sharePayload }] };
599
+ } catch (err) {
600
+ const e = err;
601
+ if (e.code === "ENOENT") {
602
+ return { content: [{ type: "text", text: `\u274C File not found: ${filePath}` }], isError: true };
603
+ }
604
+ return { content: [{ type: "text", text: `\u274C Failed to read file: ${e.message}` }], isError: true };
605
+ }
606
+ }
607
+ );
537
608
  }
538
609
  };
539
610
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "junis",
3
- "version": "0.3.14",
3
+ "version": "0.3.15",
4
4
  "description": "One-line device control for AI agents",
5
5
  "type": "module",
6
6
  "bin": {