genetics-mcp 2.21.0 → 2.22.0

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.
Binary file
package/bin/manifest.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
- "aarch64-apple-darwin": "70aa0772b147db518b10f22ab6a027a3b7b6a98bbc6d477505bf69cb9d9b447c",
3
- "aarch64-unknown-linux-musl": "87fecda2d2348a0403957b3ab7b7ed55dff72560d6768ad241d93861ab5c56d0",
4
- "x86_64-apple-darwin": "4877439d3b788c303fced3dd458331f21d3661f4eb463572c260e2c82888c165",
5
- "x86_64-unknown-linux-musl": "58e7821dd31b81232175023a86db8659cb2f5c5d47620e7b939df4e155f93b98"
2
+ "aarch64-apple-darwin": "cb352eda0bbcf7419a7ca7243be783dfd3fece7a7e5fc3b5574474c92ac349f3",
3
+ "aarch64-unknown-linux-musl": "43a93d3366f4700a74564adca0531312d4ef2079eb3d38ae5617e9c2199806de",
4
+ "x86_64-apple-darwin": "c21172e1519287d0990149b668fc595be8f656d39fa30f5b811963d492c9e8d4",
5
+ "x86_64-pc-windows-gnu": "10cdaa9e0f06b35843fe65006dbaed70e382ee61a1a86f0627d9e9df87557df3",
6
+ "x86_64-unknown-linux-musl": "72e65bdd62e2afc7de01be2311ecfe55bc3b656764e09df4e382782ba66981c4"
6
7
  }
Binary file
Binary file
package/genetics.mjs CHANGED
@@ -49,6 +49,22 @@ if (process.argv.includes('--version') || process.argv.includes('-v')) {
49
49
  process.exit(0);
50
50
  }
51
51
 
52
+ /*
53
+ * **낡은 Node 는 여기서 말하고 멈춘다.** 서버가 쓰는 전역 `fetch` 는 Node 18 부터다.
54
+ * 그 아래에서는 첫 도구 호출에서 `fetch is not defined` 가 나는데, 그 글자는 클라이언트의
55
+ * 로그 구석에만 남아 **「도구가 그냥 안 된다」로 보인다.** 뜰 때 한 줄로 말하는 편이 낫다.
56
+ * Windows 에서 특히 중요하다 — 옛 판(7·8)에는 Node 18 이 아예 없다(공식 지원표: Node 14·16 은
57
+ * Windows 8.1 부터, 지금 판은 Windows 10 부터).
58
+ */
59
+ if (typeof fetch !== 'function') {
60
+ process.stderr.write(
61
+ `genetics-mcp ${VERSION}: 이 Node(${process.version})에는 전역 fetch 가 없다 — Node 18 이상이 필요하다.\n`
62
+ + (process.platform === 'win32'
63
+ ? ' Windows 는 Node 18+ 가 Windows 10 이상에서만 돈다(공식 지원표). 7·8 에서는 포털의 브라우저 커넥터로 연결할 것.\n'
64
+ : ' nodejs.org 의 LTS 를 깔고 다시 열 것.\n'));
65
+ process.exit(1);
66
+ }
67
+
52
68
  /*
53
69
  * 짝인 스킬을 편다.
54
70
  *
@@ -706,13 +722,33 @@ async function fetchImage(url) {
706
722
  * 방어를 늘려야 하기 때문이다. 그래서 굳히는 것은 여기서 한다: 계약 §7.1 ④ 의
707
723
  * 「발행 도구의 프로세스가 원본을 받아 포맷 규정대로 굳힌다」가 정확히 이 자리다.
708
724
  */
725
+ /*
726
+ * 「이것을 까세요」는 **그 사람의 기계에서 되는 말**이어야 한다. 예전에는 brew·apt 만
727
+ * 적었는데, 그 두 줄은 Windows 에서 칠 수가 없다 — 안내가 아니라 막다른 길이 된다.
728
+ */
729
+ function installHint(kind) {
730
+ const P = {
731
+ webp: { win: 'winget install Google.LibWebp', mac: 'brew install webp', apt: 'apt install webp' },
732
+ librsvg:{ win: 'winget install --id=GNOME.librsvg -e 또는 scoop install librsvg',
733
+ mac: 'brew install librsvg', apt: 'apt install librsvg2-bin' },
734
+ }[kind];
735
+ return process.platform === 'win32' ? P.win : `${P.mac} · ${P.apt}`;
736
+ }
737
+
738
+ /** npx 가 옛 판을 물고 있을 때 비우는 한 줄 — 셸마다 다르다. */
739
+ function npxCacheResetHint(platform = process.platform) {
740
+ return platform === 'win32'
741
+ ? 'rmdir /s /q "%LOCALAPPDATA%\\npm-cache\\_npx" && npx -y genetics-mcp@latest'
742
+ : 'rm -rf ~/.npm/_npx && npx -y genetics-mcp@latest';
743
+ }
744
+
709
745
  function rasterizeSvg(buf) {
710
746
  const r = spawnSync('rsvg-convert',
711
747
  ['-w', String(ASSET_LONG_EDGE), '-b', 'white', '-f', 'png'],
712
748
  { input: buf, maxBuffer: IMAGE_MAX_DOWNLOAD });
713
749
  if (r.error && r.error.code === 'ENOENT') {
714
750
  throw new Error('rsvg-convert 가 없어 벡터 원본을 래스터로 굳힐 수 없습니다.\n'
715
- + 'librsvg 를 설치하세요(brew install librsvg · apt install librsvg2-bin). '
751
+ + `librsvg 를 설치하세요(${installHint('librsvg')}). `
716
752
  + 'SVG 를 그대로 싣지 않습니다 — 계약이 발행 데이터로 받는 형식을 두 래스터로 못박았습니다.');
717
753
  }
718
754
  if (r.status !== 0) throw new Error(`벡터 래스터화 실패: ${String(r.stderr).slice(0, 200)}`);
@@ -765,7 +801,7 @@ function encodeShot(buf) {
765
801
 
766
802
  function cwebpMissing() {
767
803
  return new Error('cwebp 가 없어 자산을 계약 규격(WebP)으로 굳힐 수 없습니다.\n'
768
- + 'libwebp 를 설치하세요(brew install webp · apt install webp). '
804
+ + `libwebp 를 설치하세요(${installHint('webp')}). `
769
805
  + '다른 포맷으로 우회하지 않습니다 — 계약이 포맷을 규정으로 못박았습니다.');
770
806
  }
771
807
 
@@ -799,7 +835,7 @@ function encodeAsset(buf, role, provider) {
799
835
  const r = spawnSync('cwebp', args, { input: buf, maxBuffer: IMAGE_MAX_DOWNLOAD });
800
836
  if (r.error && r.error.code === 'ENOENT') {
801
837
  throw new Error('cwebp 가 없어 자산을 계약 규격(WebP q78 · 긴 변 1400px)으로 굳힐 수 없습니다.\n'
802
- + 'libwebp 를 설치하세요(brew install webp · apt install webp). '
838
+ + `libwebp 를 설치하세요(${installHint('webp')}). `
803
839
  + '다른 포맷으로 우회하지 않습니다 — 계약이 포맷을 규정으로 못박았습니다.');
804
840
  }
805
841
  if (r.status !== 0) throw new Error(`자산 인코딩 실패: ${String(r.stderr).slice(0, 200)}`);
@@ -1674,11 +1710,46 @@ function scrubTranscript(raw) {
1674
1710
  return { jsonl: out.join('\n') + '\n', lines: out.length, dropped, redacted, cwd: cwdMode(cwds), cwd_raw: cwdMode(cwdsRaw), cwds_raw: rawAll };
1675
1711
  }
1676
1712
 
1677
- /** cwd → 트랜스크립트 디렉터리. **해석된 경로**로 슬러그를 만든다(심볼릭 링크 함정). */
1713
+ /*
1714
+ * cwd → 트랜스크립트 디렉터리. **해석된 경로**로 슬러그를 만든다(심볼릭 링크 함정).
1715
+ *
1716
+ * ★ **이 규칙은 우리 것이 아니라 Claude Code 의 것이다.** 우리가 정하는 자리가 아니라
1717
+ * **맞춰야 하는 자리**다 — 어긋나면 우리가 놓은 파일을 `claude --resume` 이 못 찾는다.
1718
+ * 그래서 기억으로 쓰지 않고 **바이너리에서 떠 왔다**(2.1.274, 2026-09-18):
1719
+ *
1720
+ * var jY = 200;
1721
+ * function JY(e){ let r=0; for(let n=0;n<e.length;n++) r=(r<<5)-r+e.charCodeAt(n)|0; return r }
1722
+ * function Le(e){ return Math.abs(JY(e)).toString(36) }
1723
+ * function k(e){ return e.replace(/[^a-zA-Z0-9]/g, "-") }
1724
+ * function eR(e){ let n=k(e); if(n.length<=jY) return n; return `${n.slice(0,jY)}-${Le(e)}` }
1725
+ *
1726
+ * 옛 코드는 `/` 와 `_` 만 바꿨다. 그러면 **점 하나만 들어가도 어긋난다** — 이 기계 실측:
1727
+ * `…/spreadjs-v19-0-3_/.vendor-src` 의 실재 폴더는 `…-v19-0-3---vendor-src` 인데 옛 규칙은
1728
+ * `…-v19-0-3--.vendor-src` 를 냈다(실재 0건). Windows 는 더 나쁘다 — `\` 와 `:` 가 그대로
1729
+ * 남아 `C:\…\projects\C:\Users\…` 라는 **만들 수 없는 경로**가 나왔다(path.win32 로 실측).
1730
+ * 비영숫자를 전부 바꾸면 드라이브 문자도 `C-` 가 되어 Windows 에서도 유효한 이름이 된다.
1731
+ */
1732
+ const TRANSCRIPT_SLUG_MAX = 200; // Claude Code 의 jY
1733
+
1734
+ /** Claude Code 의 `JY` — 자바 문자열 해시와 같은 셈(32비트 랩어라운드). */
1735
+ function transcriptSlugHash(s) {
1736
+ let r = 0;
1737
+ for (let i = 0; i < s.length; i += 1) r = (((r << 5) - r) + s.charCodeAt(i)) | 0;
1738
+ return Math.abs(r).toString(36);
1739
+ }
1740
+
1741
+ /** Claude Code 의 `eR` — 경로 하나를 폴더 이름 하나로. */
1742
+ function transcriptSlug(real) {
1743
+ const n = String(real).replace(/[^a-zA-Z0-9]/g, '-');
1744
+ return n.length <= TRANSCRIPT_SLUG_MAX
1745
+ ? n
1746
+ : `${n.slice(0, TRANSCRIPT_SLUG_MAX)}-${transcriptSlugHash(real)}`;
1747
+ }
1748
+
1678
1749
  function transcriptDir(dir) {
1679
1750
  let real = dir;
1680
1751
  try { real = fs.realpathSync(dir); } catch { /* 없으면 준 값 그대로 */ }
1681
- return path.join(os.homedir(), '.claude', 'projects', real.replace(/[/_]/g, '-'));
1752
+ return path.join(os.homedir(), '.claude', 'projects', transcriptSlug(real));
1682
1753
  }
1683
1754
 
1684
1755
  /** uuid 로 이 기계의 트랜스크립트를 찾는다. 지금 프로젝트를 먼저 보고, 없으면 전체를 훑는다. */
@@ -1777,8 +1848,33 @@ function cursorActiveFromLedger() {
1777
1848
  * 조용히 비켜난다(beatTargets:transcriptOf null → skip). 대화 본문을 올리는 길(두뇌
1778
1849
  * 파서까지)은 별도 단계다 — DESIGN-cursor-colony.md.
1779
1850
  */
1780
- const CURSOR_ROOT = process.env.GENETICS_CURSOR_ROOT
1781
- || path.join(os.homedir(), 'Library', 'Application Support', 'Cursor', 'User');
1851
+ /*
1852
+ * Cursor 의 사용자 데이터 자리는 **플랫폼마다 다르다**(VS Code 계열 공통 규약).
1853
+ * 여기 macOS 자리만 박혀 있었다 — Windows·Linux 에서는 `workspaceStorage` 를 영영 못 찾고
1854
+ * 조용히 빈 값으로 떨어졌다(try/catch 안이라 아무도 못 봤다).
1855
+ * darwin ~/Library/Application Support/Cursor/User
1856
+ * win32 %APPDATA%\Cursor\User (APPDATA 가 없으면 ~/AppData/Roaming 로 잇는다)
1857
+ * linux $XDG_CONFIG_HOME/Cursor/User → 없으면 ~/.config/Cursor/User
1858
+ */
1859
+ function cursorUserRoot(platform = process.platform, env = process.env, home = os.homedir()) {
1860
+ if (env.GENETICS_CURSOR_ROOT) return env.GENETICS_CURSOR_ROOT;
1861
+ if (platform === 'win32') {
1862
+ return path.join(env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'Cursor', 'User');
1863
+ }
1864
+ if (platform === 'darwin') return path.join(home, 'Library', 'Application Support', 'Cursor', 'User');
1865
+ return path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'Cursor', 'User');
1866
+ }
1867
+
1868
+ const CURSOR_ROOT = cursorUserRoot();
1869
+
1870
+ /*
1871
+ * sqlite3 실행체. macOS 는 자리가 고정이라 **절대경로로** 부르고(PATH 오염을 안 탄다),
1872
+ * 나머지는 PATH 에서 찾는다 — Windows 는 sqlite3 가 기본 탑재가 아니라 없을 수 있고,
1873
+ * 없으면 아래 장부 갈래로 떨어진다(없음은 오류가 아니다).
1874
+ */
1875
+ function sqliteBin(platform = process.platform) {
1876
+ return platform === 'darwin' ? '/usr/bin/sqlite3' : 'sqlite3';
1877
+ }
1782
1878
 
1783
1879
  let cursorSeen = { at: 0, id: '', folder: '' }; // 15초 캐시 — 도구마다 sqlite 를 안 두드리게
1784
1880
 
@@ -1815,7 +1911,7 @@ function cursorActiveComposer() {
1815
1911
  if (!best || mt > best.mt) best = { dir, wsPath, mt };
1816
1912
  }
1817
1913
  if (best) {
1818
- const r = spawnSync('/usr/bin/sqlite3',
1914
+ const r = spawnSync(sqliteBin(),
1819
1915
  ['-readonly', path.join(best.dir, 'state.vscdb'),
1820
1916
  "SELECT value FROM ItemTable WHERE key='composer.composerData'"],
1821
1917
  { encoding: 'utf8', timeout: 2000 });
@@ -4725,11 +4821,23 @@ const CELL_DIR = path.join(os.homedir(), '.config/genetics/bin');
4725
4821
  const CELL_PATH = path.join(CELL_DIR, process.platform === 'win32' ? 'gcell.exe' : 'gcell');
4726
4822
 
4727
4823
  /** 이 기계의 타깃 삼중. 모르면 `null` — 지어내지 않는다. */
4728
- function cellTarget() {
4729
- const a = process.arch, p = process.platform;
4824
+ function cellTarget(platform = process.platform, arch = process.arch) {
4825
+ const a = arch, p = platform;
4730
4826
  if (p === 'darwin') return a === 'arm64' ? 'aarch64-apple-darwin' : a === 'x64' ? 'x86_64-apple-darwin' : null;
4731
4827
  if (p === 'linux') return a === 'arm64' ? 'aarch64-unknown-linux-musl' : a === 'x64' ? 'x86_64-unknown-linux-musl' : null;
4732
- return null; // Windows 는 아직 못 굽는다(cell/.cargo/config.toml)
4828
+ // Windows 는 mingw 로 굽는다(cell/.cargo/config.toml). arm64 Windows 는 아직 안 굽는다 — 없는 것은 없는 것이다.
4829
+ if (p === 'win32') return a === 'x64' ? 'x86_64-pc-windows-gnu' : null;
4830
+ return null;
4831
+ }
4832
+
4833
+ /**
4834
+ * 이 플랫폼에 **세포가 있을 수 있나**. 「아직 안 깔렸다」와 「이 플랫폼엔 애초에 없다」를 가른다.
4835
+ *
4836
+ * ⚠ 이 갈래가 없으면 안내가 **막다른 길**이 된다: 못 쓴다는 글이 `install_cell` 을 가리키고,
4837
+ * 그 도구는 다시 「이 판에 없다」를 낸다 — 모델이 그 둘을 오가며 설치를 되풀이한다.
4838
+ */
4839
+ function cellPlatformSupported(platform = process.platform, arch = process.arch) {
4840
+ return cellTarget(platform, arch) !== null;
4733
4841
  }
4734
4842
 
4735
4843
  function cellManifest() {
@@ -4897,7 +5005,7 @@ function tenantNoticeText(v) {
4897
5005
  }
4898
5006
  if (v.client_old) {
4899
5007
  return `genetics: 이 폴더는 ${T} 로 확정돼 있고 자동 올리기도 켜져 있지만, 이 판(${v.version})이 서버 최소 판(${v.min_client})보다 낡아 `
4900
- + '**이 대화는 안 올라간다**. npx 캐시를 비우고 다시 깔면 그때부터 올라간다: rm -rf ~/.npm/_npx && npx -y genetics-mcp@latest.';
5008
+ + `**이 대화는 안 올라간다**. npx 캐시를 비우고 다시 깔면 그때부터 올라간다: ${npxCacheResetHint()}.`;
4901
5009
  }
4902
5010
  const who = v.confirmed_by ? `${v.confirmed_by}${josaIGa(v.confirmed_by)} ` : '';
4903
5011
  return `genetics: 이 세션은 ${T} 소속으로 확정된 폴더(${v.path} · ${who}${String(v.confirmed_at || '').slice(0, 10)} 확정, 자동 올리기 켬)에서 `
@@ -4954,7 +5062,7 @@ function tenantNoticeText(v) {
4954
5062
  + 'login 도구로 로그인하면 그때부터 확인한다.';
4955
5063
  case 'client_old':
4956
5064
  return `genetics: 이 판(${v.version})은 서버 최소 판(${v.min_client})보다 낡아 폴더 소속·자동 올리기가 안 돈다. `
4957
- + 'npx 캐시를 비우고 다시 깔 것: rm -rf ~/.npm/_npx && npx -y genetics-mcp@latest.';
5065
+ + `npx 캐시를 비우고 다시 깔 것: ${npxCacheResetHint()}.`;
4958
5066
  default:
4959
5067
  return '';
4960
5068
  }
@@ -5167,10 +5275,14 @@ function toolsNow() {
5167
5275
  * 지시형은 프롬프트 주입으로 거부된다(2026-08-22 실측).
5168
5276
  */
5169
5277
  function cellNotice(why) {
5170
- return [
5171
- `genetics 세포(gcell)를 못 쓴다 — ${why}. 그래서 이 저장소에서 남이 무엇을 바꿨는지 못 싣는다.`,
5172
- '세포는 선택이 아니라 필수다. `install_cell` 도구가 깔고, `uninstall_cell` 이 지운다.',
5173
- ].join('\n');
5278
+ const head = `genetics 세포(gcell)를 못 쓴다 — ${why}. 그래서 이 저장소에서 남이 무엇을 바꿨는지 못 싣는다.`;
5279
+ // **깔 수 없는 곳에 설치를 시키지 않는다.** 이 플랫폼에 세포가 없으면 그 말이 곧 다음 할 일이다.
5280
+ if (!cellPlatformSupported()) {
5281
+ return [head,
5282
+ `이 플랫폼(${process.platform}/${process.arch})에는 세포가 없다 — 깔 수 있는 것이 없으니 `
5283
+ + '`install_cell` 을 부르지 말 것. 나머지 도구는 그대로 쓴다.'].join('\n');
5284
+ }
5285
+ return [head, '세포는 선택이 아니라 필수다. `install_cell` 도구가 깔고, `uninstall_cell` 이 지운다.'].join('\n');
5174
5286
  }
5175
5287
 
5176
5288
  /* ── ★★ 메일 레인 — 두뇌·다른 세션·사람이 **나를 지목해** 보낸 것 ──────────
@@ -6844,7 +6956,15 @@ const IS_MAIN = (() => {
6844
6956
  * 자리 고르기는 **순수 함수**로 뺀다(smoke 가 떼어내 잰다): PATH 에 있고 · 실재하고 · 쓸 수 있는
6845
6957
  * 첫 후보. 없는 폴더를 만들거나 rc 파일을 고치지 않는다 — 남의 셸 설정에 손대는 설치는 못 믿는다.
6846
6958
  */
6847
- const CELL_LINK_CANDIDATES = ['/opt/homebrew/bin', '/usr/local/bin', path.join(os.homedir(), '.local/bin')];
6959
+ /*
6960
+ * ⚠ **Windows 에는 이 자리가 없다.** 후보 셋은 전부 유닉스의 PATH 자리이고, 심링크 자체도
6961
+ * Windows 에서는 개발자 모드나 관리자 권한이 있어야 만들어진다(EPERM). 그래서 그 판에서는
6962
+ * **걸지 않고**, 대신 「이 폴더를 PATH 에 넣으라」고 그 판의 말로 말한다 — 없는 자리를
6963
+ * 나열하며 못 걸었다고 하는 것은 안내가 아니다.
6964
+ */
6965
+ const CELL_LINK_CANDIDATES = process.platform === 'win32'
6966
+ ? []
6967
+ : ['/opt/homebrew/bin', '/usr/local/bin', path.join(os.homedir(), '.local/bin')];
6848
6968
  function cellLinkDir(pathEnv, candidates, exists, writable) {
6849
6969
  const onPath = new Set(String(pathEnv || '').split(path.delimiter).filter(Boolean));
6850
6970
  for (const d of candidates) {
@@ -6857,6 +6977,10 @@ function cellLinkOnPath() {
6857
6977
  (d) => { try { return fs.statSync(d).isDirectory(); } catch { return false; } },
6858
6978
  (d) => { try { fs.accessSync(d, fs.constants.W_OK); return true; } catch { return false; } });
6859
6979
  if (!dir) {
6980
+ if (process.platform === 'win32') {
6981
+ return `PATH 에 안 걸었다(Windows 는 심링크에 권한이 필요하다) — 전체 경로 ${CELL_PATH} 로 부르거나 `
6982
+ + `이 폴더를 PATH 에 넣을 것: setx PATH "%PATH%;${CELL_DIR}"`;
6983
+ }
6860
6984
  return `PATH 에 걸 자리를 못 찾았다(${CELL_LINK_CANDIDATES.join(' · ')} 중 PATH 에 있고 쓸 수 있는 곳이 없다) — 전체 경로 ${CELL_PATH} 로 부를 것.`;
6861
6985
  }
6862
6986
  const link = path.join(dir, 'gcell');
@@ -7332,4 +7456,6 @@ export { encodeAsset, imageDims, imageHostAllowed, materializeAssets, warnText,
7332
7456
  autoBlockSetVerdict, autoBlockPut, cellPinSource,
7333
7457
  // 2026-09-15 — 「서버가 아는 행인가」는 소속 칸이 아니라 서버를 거친 표식으로 가른다(첫 업로드가 영영 안 서던 자리)
7334
7458
  autoRowOnServer,
7459
+ // 2026-09-18 Windows — 남의 규칙(Claude Code 슬러그)·플랫폼 갈래는 시험이 표로 걷는다
7460
+ transcriptSlug, transcriptDir, cursorUserRoot, sqliteBin, npxCacheResetHint, cellPlatformSupported,
7335
7461
  HOLDS_FILE, UPDATE_BUDGET_MS };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genetics-mcp",
3
- "version": "2.21.0",
3
+ "version": "2.22.0",
4
4
  "description": "문서 포털 발행·세션 이식·사례 기억 MCP — 데이터를 주면 규격 문서가 나온다 (짝인 스킬 동봉)",
5
5
  "author": "주식회사 LIM (LIM Inc.)",
6
6
  "license": "SEE LICENSE IN LICENSE",
@@ -18,7 +18,7 @@
18
18
  "node": ">=18"
19
19
  },
20
20
  "scripts": {
21
- "test": "node smoke.mjs && node notify.test.mjs && node schedule.test.mjs && bash ../cell/tests/cellcycle.sh",
21
+ "test": "node smoke.mjs && node notify.test.mjs && node schedule.test.mjs && node windows.test.mjs && bash ../cell/tests/cellcycle.sh",
22
22
  "prepack": "find skills -name __pycache__ -type d -prune -exec rm -rf {} + 2>/dev/null; bash ../cell/build-all.sh",
23
23
  "prepublishOnly": "npm test",
24
24
  "release": "bash release.sh"