gm-oc 2.0.173 → 2.0.175

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.
@@ -1,204 +1,166 @@
1
- #!/usr/bin/env bun
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const { execSync, spawnSync } = require('child_process');
7
-
8
- const isGemini = process.env.GEMINI_PROJECT_DIR !== undefined;
9
-
10
- const writeTools = ['Write', 'write_file'];
11
- const searchTools = ['glob', 'search_file_content', 'Search', 'search'];
12
- const forbiddenTools = ['find', 'Find', 'Glob', 'Grep'];
13
-
14
- const run = () => {
15
- try {
16
- const input = fs.readFileSync(0, 'utf-8');
17
- const data = JSON.parse(input);
18
- const { tool_name, tool_input } = data;
19
-
20
- if (!tool_name) return { allow: true };
21
-
22
- if (forbiddenTools.includes(tool_name)) {
23
- return { block: true, reason: 'Use the code-search skill for codebase exploration instead of Grep/Glob/find. Describe what you need in plain language — it understands intent, not just patterns.' };
24
- }
25
-
26
- if (writeTools.includes(tool_name)) {
27
- const file_path = tool_input?.file_path || '';
28
- const ext = path.extname(file_path);
29
- const inSkillsDir = file_path.includes('/skills/') || file_path.includes('\\skills\\');
30
- const base = path.basename(file_path).toLowerCase();
31
- if ((ext === '.md' || ext === '.txt' || base.startsWith('features_list')) &&
32
- !base.startsWith('claude') && !base.startsWith('readme') && !inSkillsDir) {
33
- return { block: true, reason: 'Cannot create documentation files. Only CLAUDE.md and readme.md are maintained. For task-specific notes, use .prd. For permanent reference material, add to CLAUDE.md.' };
34
- }
35
- if (/\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/.test(base) ||
36
- /^(jest|vitest|mocha|ava|jasmine|tap)\.(config|setup)/.test(base) ||
37
- file_path.includes('/__tests__/') || file_path.includes('/test/') ||
38
- file_path.includes('/tests/') || file_path.includes('/fixtures/') ||
39
- file_path.includes('/test-data/') || file_path.includes('/__mocks__/') ||
40
- /\.(snap|stub|mock|fixture)\.(js|ts|json)$/.test(base)) {
41
- return { block: true, reason: 'Test files forbidden on disk. Use Bash tool with real services for all testing.' };
42
- }
43
- }
44
-
45
- if (searchTools.includes(tool_name)) {
46
- return { allow: true };
47
- }
48
-
49
- if (tool_name === 'Task') {
50
- const subagentType = tool_input?.subagent_type || '';
51
- if (subagentType === 'Explore') {
52
- return { block: true, reason: 'Use gm:thorns-overview for codebase insight, then use gm:code-search' };
53
- }
54
- }
55
-
56
- if (tool_name === 'EnterPlanMode') {
57
- return { block: true, reason: 'Plan mode is disabled. Use GM agent planning (PLAN→EXECUTE→EMIT→VERIFY→COMPLETE state machine) via gm:gm subagent instead.' };
58
- }
59
-
60
- if (tool_name === 'Skill') {
61
- const skill = (tool_input?.skill || '').toLowerCase().replace(/^gm:/, '');
62
- if (skill === 'explore' || skill === 'search') {
63
- return { block: true, reason: 'Use the code-search skill for codebase exploration. Describe what you need in plain language — it understands intent, not just patterns.' };
64
- }
65
- }
66
-
67
- if (tool_name === 'Bash') {
68
- const command = (tool_input?.command || '').trim();
69
-
70
- const execMatch = command.match(/^exec(?::(\S+))?\n([\s\S]+)$/);
71
- if (execMatch) {
72
- const rawLang = (execMatch[1] || '').toLowerCase();
73
- const code = execMatch[2];
74
- const cwd = tool_input?.cwd;
75
- const detectLang = (src) => {
76
- if (/^\s*(import |from |export |const |let |var |function |class |async |await |console\.|process\.)/.test(src)) return 'nodejs';
77
- if (/^\s*(import |def |print\(|class |if __name__)/.test(src)) return 'python';
78
- if (/^\s*(echo |ls |cd |mkdir |rm |cat |grep |find |export |source |#!)/.test(src)) return 'bash';
79
- return 'nodejs';
80
- };
81
- const langAliases = { js: 'nodejs', javascript: 'nodejs', ts: 'typescript', node: 'nodejs', py: 'python', sh: 'bash', shell: 'bash', zsh: 'bash', powershell: 'bash', ps1: 'bash', cmd: 'bash', browser: 'agent-browser', ab: 'agent-browser' };
82
- const lang = langAliases[rawLang] || rawLang || detectLang(code);
83
- const stripFooter = (s) => s.replace(/\n\[Running tools\][\s\S]*$/, '').trimEnd();
84
- const gmExecLangs = new Set(['go', 'rust', 'c', 'cpp', 'java']);
85
- const langExts = { nodejs: 'mjs', typescript: 'ts', deno: 'ts', python: 'py', bash: 'ps1', go: 'go', rust: 'rs', c: 'c', cpp: 'cpp', java: 'java' };
86
- const spawnDirect = (bin, args, input) => {
87
- const opts = { encoding: 'utf-8', timeout: 60000 };
88
- if (cwd) opts.cwd = cwd;
89
- if (input !== undefined) opts.input = input;
90
- const r = spawnSync(bin, args, opts);
91
- if (!r.stdout && !r.stderr && r.error) return `[spawn error: ${r.error.message}]`;
92
- const stdout = (r.stdout || '').trimEnd();
93
- const stderr = stripFooter(r.stderr || '').trimEnd();
94
- if (stdout && stderr) return stdout + '\n[stderr]\n' + stderr;
95
- return stripFooter(stdout || stderr);
96
- };
97
- const runWithFile = (lang, code) => {
98
- const ext = langExts[lang] || lang;
99
- const tmpFile = path.join(os.tmpdir(), `gm-exec-${Date.now()}.${ext}`);
100
- fs.writeFileSync(tmpFile, code, 'utf-8');
101
- const r = spawnSync('bun', ['x', 'gm-exec', 'exec', `--lang=${lang}`, `--file=${tmpFile}`, ...(cwd ? [`--cwd=${cwd}`] : [])], { encoding: 'utf-8', timeout: 65000 });
102
- try { fs.unlinkSync(tmpFile); } catch (e) {}
103
- let out = stripFooter((r.stdout || '') + (r.stderr || ''));
104
- const bgMatch = out.match(/Command running in background with ID:\s*(\S+)/);
105
- if (bgMatch) {
106
- const taskId = bgMatch[1];
107
- spawnSync('bun', ['x', 'gm-exec', 'sleep', taskId, '60'], { encoding: 'utf-8', timeout: 70000 });
108
- const sr = spawnSync('bun', ['x', 'gm-exec', 'status', taskId], { encoding: 'utf-8', timeout: 15000 });
109
- out = stripFooter((sr.stdout || '') + (sr.stderr || ''));
110
- spawnSync('bun', ['x', 'gm-exec', 'close', taskId], { encoding: 'utf-8', timeout: 10000 });
111
- }
112
- return out;
113
- };
114
- try {
115
- let result;
116
- if (lang === 'bash') {
117
- const shFile = path.join(os.tmpdir(), `gm-exec-${Date.now()}.ps1`);
118
- fs.writeFileSync(shFile, code, 'utf-8');
119
- result = spawnDirect('powershell', ['-NoProfile', '-NonInteractive', '-File', shFile]);
120
- try { fs.unlinkSync(shFile); } catch (e) {}
121
- if (!result || result.startsWith('[spawn error:')) result = runWithFile('bash', code);
122
- } else if (lang === 'python') {
123
- result = spawnDirect('python3', ['-c', code]);
124
- if (!result || result.startsWith('[spawn error:')) result = spawnDirect('python', ['-c', code]);
125
- } else if (!lang || lang === 'nodejs' || lang === 'typescript' || lang === 'deno') {
126
- const ext = lang === 'typescript' || lang === 'deno' ? 'ts' : 'mjs';
127
- const tmpFile = path.join(os.tmpdir(), `gm-exec-${Date.now()}.${ext}`);
128
- const wrapped = `const __result = await (async () => {\n${code}\n})();\nif (__result !== undefined) { if (typeof __result === 'object') { console.log(JSON.stringify(__result, null, 2)); } else { console.log(__result); } }`;
129
- fs.writeFileSync(tmpFile, wrapped, 'utf-8');
130
- result = spawnDirect('bun', ['run', tmpFile]);
131
- try { fs.unlinkSync(tmpFile); } catch (e) {}
132
- if (result) result = result.split(tmpFile).join('<script>');
133
- } else if (lang === 'agent-browser') {
134
- result = spawnDirect('agent-browser', ['eval', '--stdin'], code);
135
- } else if (gmExecLangs.has(lang)) {
136
- result = runWithFile(lang, code);
137
- } else {
138
- result = runWithFile(lang, code);
139
- }
140
- return { block: true, reason: `exec ran successfully. Output:\n\n${result || '(no output)'}` };
141
- } catch (e) {
142
- return { block: true, reason: `exec ran. Error:\n\n${(e.stdout || '') + (e.stderr || '') || e.message || '(exec failed)'}` };
143
- }
144
- }
145
-
146
- if (!/^exec(\s|:)/.test(command) && !/^bun x gm-exec(@[^\s]*)?(\s|$)/.test(command) && !/^git /.test(command) && !/^bun x codebasesearch/.test(command) && !/(\bclaude\b)/.test(command) && !/^npm install .* \/config\/.gmweb\/npm-global\/lib\/node_modules\/gm-exec/.test(command) && !/^bun install --cwd \/config\/.gmweb\/npm-global\/lib\/node_modules\/gm-exec/.test(command)) {
147
- let helpText = '';
148
- try { helpText = '\n\n' + execSync('bun x gm-exec --help', { timeout: 10000 }).toString().trim(); } catch (e) {}
149
- return { block: true, reason: `Bash is restricted to exec:<lang> interception and git.\n\nUse exec:<lang> syntax:\n exec:nodejs\\n<js code>\n exec:python\\n<python code>\n exec:bash\\n<shell commands>\n exec:typescript\\n<ts code>\n exec (no lang — auto-detects)\n\nOr use bun x gm-exec directly:\n bun x gm-exec${helpText}\n\nDocs: https://www.npmjs.com/package/gm-exec\n\nAll other Bash commands are blocked.` };
150
- }
151
- }
152
-
153
- if (tool_name === 'agent-browser') {
154
- const input = tool_input || {};
155
- const script = input.script || input.code || '';
156
- if (script && !input.url && !input.navigate) {
157
- const sFooter = (s) => s.replace(/\n\[Running tools\][\s\S]*$/, '').trimEnd();
158
- try {
159
- const tmpFile = path.join(os.tmpdir(), `gm-exec-${Date.now()}.mjs`);
160
- fs.writeFileSync(tmpFile, script, 'utf-8');
161
- const r = spawnSync('bun', ['run', tmpFile], { encoding: 'utf-8', timeout: 65000 });
162
- try { fs.unlinkSync(tmpFile); } catch (e) {}
163
- const stdout = (r.stdout || '').trimEnd();
164
- const stderr = sFooter(r.stderr || '').trimEnd();
165
- const out = stdout && stderr ? stdout + '\n[stderr]\n' + stderr : sFooter(stdout || stderr);
166
- return { block: true, reason: `exec ran successfully. Output:\n\n${out || '(no output)'}` };
167
- } catch (e) {
168
- return { block: true, reason: `exec ran. Error:\n\n${(e.stdout || '') + (e.stderr || '') || e.message || '(exec failed)'}` };
169
- }
170
- }
171
- }
172
-
173
- // Allow essential tools explicitly
174
- const allowedTools = ['agent-browser', 'Skill', 'code-search', 'electron', 'TaskOutput', 'ReadMcpResourceTool', 'ListMcpResourcesTool'];
175
- if (allowedTools.includes(tool_name)) {
176
- return { allow: true };
177
- }
178
-
179
- return { allow: true };
180
- } catch (error) {
181
- return { allow: true };
182
- }
183
- };
184
-
185
- try {
186
- const result = run();
187
-
188
- if (result.block) {
189
- if (isGemini) {
190
- console.log(JSON.stringify({ decision: 'deny', reason: result.reason }));
191
- } else {
192
- console.log(JSON.stringify({ decision: 'block', reason: result.reason }));
193
- }
194
- process.exit(0);
195
- }
196
-
197
- if (isGemini) {
198
- console.log(JSON.stringify({ decision: 'allow' }));
199
- }
200
-
201
- process.exit(0);
202
- } catch (error) {
203
- process.exit(0);
204
- }
1
+ #!/usr/bin/env bun
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { execSync, spawnSync } = require('child_process');
7
+
8
+ const isGemini = process.env.GEMINI_PROJECT_DIR !== undefined;
9
+
10
+ const writeTools = ['Write', 'write_file'];
11
+ const searchTools = ['glob', 'search_file_content', 'Search', 'search'];
12
+ const forbiddenTools = ['find', 'Find', 'Glob', 'Grep'];
13
+
14
+ const run = () => {
15
+ try {
16
+ const input = fs.readFileSync(0, 'utf-8');
17
+ const data = JSON.parse(input);
18
+ const { tool_name, tool_input } = data;
19
+
20
+ if (!tool_name) return { allow: true };
21
+
22
+ if (forbiddenTools.includes(tool_name)) {
23
+ return { block: true, reason: 'Use the code-search skill for codebase exploration instead of Grep/Glob/find. Describe what you need in plain language — it understands intent, not just patterns.' };
24
+ }
25
+
26
+ if (writeTools.includes(tool_name)) {
27
+ const file_path = tool_input?.file_path || '';
28
+ const ext = path.extname(file_path);
29
+ const inSkillsDir = file_path.includes('/skills/') || file_path.includes('\\skills\\');
30
+ const base = path.basename(file_path).toLowerCase();
31
+ if ((ext === '.md' || ext === '.txt' || base.startsWith('features_list')) &&
32
+ !base.startsWith('claude') && !base.startsWith('readme') && !inSkillsDir) {
33
+ return { block: true, reason: 'Cannot create documentation files. Only CLAUDE.md and readme.md are maintained. For task-specific notes, use .prd. For permanent reference material, add to CLAUDE.md.' };
34
+ }
35
+ if (/\.(test|spec)\.(js|ts|jsx|tsx|mjs|cjs)$/.test(base) ||
36
+ /^(jest|vitest|mocha|ava|jasmine|tap)\.(config|setup)/.test(base) ||
37
+ file_path.includes('/__tests__/') || file_path.includes('/test/') ||
38
+ file_path.includes('/tests/') || file_path.includes('/fixtures/') ||
39
+ file_path.includes('/test-data/') || file_path.includes('/__mocks__/') ||
40
+ /\.(snap|stub|mock|fixture)\.(js|ts|json)$/.test(base)) {
41
+ return { block: true, reason: 'Test files forbidden on disk. Use Bash tool with real services for all testing.' };
42
+ }
43
+ }
44
+
45
+ if (searchTools.includes(tool_name)) return { allow: true };
46
+
47
+ if (tool_name === 'Task' && (tool_input?.subagent_type || '') === 'Explore') {
48
+ return { block: true, reason: 'Use gm:thorns-overview for codebase insight, then use gm:code-search' };
49
+ }
50
+
51
+ if (tool_name === 'EnterPlanMode') {
52
+ return { block: true, reason: 'Plan mode is disabled. Use GM agent planning (PLAN→EXECUTE→EMIT→VERIFY→COMPLETE state machine) via gm:gm subagent instead.' };
53
+ }
54
+
55
+ if (tool_name === 'Skill') {
56
+ const skill = (tool_input?.skill || '').toLowerCase().replace(/^gm:/, '');
57
+ if (skill === 'explore' || skill === 'search') {
58
+ return { block: true, reason: 'Use the code-search skill for codebase exploration. Describe what you need in plain language — it understands intent, not just patterns.' };
59
+ }
60
+ }
61
+
62
+ if (tool_name === 'Bash') {
63
+ const command = (tool_input?.command || '').trim();
64
+ const execMatch = command.match(/^exec(?::(\S+))?\n([\s\S]+)$/);
65
+ if (execMatch) {
66
+ const rawLang = (execMatch[1] || '').toLowerCase();
67
+ const code = execMatch[2];
68
+ const cwd = tool_input?.cwd;
69
+ const detectLang = (src) => {
70
+ if (/^\s*(import |from |export |const |let |var |function |class |async |await |console\.|process\.)/.test(src)) return 'nodejs';
71
+ if (/^\s*(import |def |print\(|class |if __name__)/.test(src)) return 'python';
72
+ if (/^\s*(echo |ls |cd |mkdir |rm |cat |grep |find |export |source |#!)/.test(src)) return 'bash';
73
+ return 'nodejs';
74
+ };
75
+ const aliases = { js: 'nodejs', javascript: 'nodejs', ts: 'typescript', node: 'nodejs', py: 'python', sh: 'bash', shell: 'bash', zsh: 'bash', powershell: 'bash', ps1: 'bash', cmd: 'bash', browser: 'agent-browser', ab: 'agent-browser' };
76
+ const lang = aliases[rawLang] || rawLang || detectLang(code);
77
+ const IS_WIN = process.platform === 'win32';
78
+ const stripFooter = (s) => s.replace(/\n\[Running tools\][\s\S]*$/, '').trimEnd();
79
+ const langExts = { nodejs: 'mjs', typescript: 'ts', deno: 'ts', python: 'py', bash: IS_WIN ? 'ps1' : 'sh', go: 'go', rust: 'rs', c: 'c', cpp: 'cpp', java: 'java' };
80
+ const spawnDirect = (bin, args, stdin) => {
81
+ const opts = { encoding: 'utf-8', timeout: 60000, ...(cwd && { cwd }), ...(stdin !== undefined && { input: stdin }) };
82
+ const r = spawnSync(bin, args, opts);
83
+ if (!r.stdout && !r.stderr && r.error) return `[spawn error: ${r.error.message}]`;
84
+ const out = (r.stdout || '').trimEnd(), err = stripFooter(r.stderr || '').trimEnd();
85
+ return out && err ? out + '\n[stderr]\n' + err : stripFooter(out || err);
86
+ };
87
+ const runWithFile = (l, src) => {
88
+ const tmp = path.join(os.tmpdir(), `gm-exec-${Date.now()}.${langExts[l] || l}`);
89
+ fs.writeFileSync(tmp, src, 'utf-8');
90
+ const r = spawnSync('bun', ['x', 'gm-exec', 'exec', `--lang=${l}`, `--file=${tmp}`, ...(cwd ? [`--cwd=${cwd}`] : [])], { encoding: 'utf-8', timeout: 65000 });
91
+ try { fs.unlinkSync(tmp); } catch (e) {}
92
+ let out = stripFooter((r.stdout || '') + (r.stderr || ''));
93
+ const bg = out.match(/Command running in background with ID:\s*(\S+)/);
94
+ if (bg) {
95
+ spawnSync('bun', ['x', 'gm-exec', 'sleep', bg[1], '60'], { encoding: 'utf-8', timeout: 70000 });
96
+ const sr = spawnSync('bun', ['x', 'gm-exec', 'status', bg[1]], { encoding: 'utf-8', timeout: 15000 });
97
+ out = stripFooter((sr.stdout || '') + (sr.stderr || ''));
98
+ spawnSync('bun', ['x', 'gm-exec', 'close', bg[1]], { encoding: 'utf-8', timeout: 10000 });
99
+ }
100
+ return out;
101
+ };
102
+ const decodeB64 = (s) => {
103
+ const t = s.trim();
104
+ if (t.length < 16 || t.length % 4 !== 0 || !/^[A-Za-z0-9+/\r\n]+=*$/.test(t)) return s;
105
+ try { const d = Buffer.from(t, 'base64').toString('utf-8'); return /[\x00-\x08\x0b\x0e-\x1f]/.test(d) ? s : d; } catch { return s; }
106
+ };
107
+ const safeCode = decodeB64(code);
108
+ try {
109
+ let result;
110
+ if (lang === 'bash') {
111
+ const shFile = path.join(os.tmpdir(), `gm-exec-${Date.now()}.${IS_WIN ? 'ps1' : 'sh'}`);
112
+ fs.writeFileSync(shFile, safeCode, 'utf-8');
113
+ result = IS_WIN
114
+ ? spawnDirect('powershell', ['-NoProfile', '-NonInteractive', '-File', shFile])
115
+ : spawnDirect('bash', [shFile]);
116
+ try { fs.unlinkSync(shFile); } catch (e) {}
117
+ if (!result || result.startsWith('[spawn error:')) result = runWithFile('bash', safeCode);
118
+ } else if (lang === 'python') {
119
+ result = spawnDirect('python3', ['-c', safeCode]);
120
+ if (!result || result.startsWith('[spawn error:')) result = spawnDirect('python', ['-c', safeCode]);
121
+ } else if (!lang || ['nodejs', 'typescript', 'deno'].includes(lang)) {
122
+ const ext = lang === 'typescript' || lang === 'deno' ? 'ts' : 'mjs';
123
+ const tmp = path.join(os.tmpdir(), `gm-exec-${Date.now()}.${ext}`);
124
+ const wrapped = `const __result = await (async () => {\n${safeCode}\n})();\nif (__result !== undefined) { if (typeof __result === 'object') { console.log(JSON.stringify(__result, null, 2)); } else { console.log(__result); } }`;
125
+ fs.writeFileSync(tmp, wrapped, 'utf-8');
126
+ result = spawnDirect('bun', ['run', tmp]);
127
+ try { fs.unlinkSync(tmp); } catch (e) {}
128
+ if (result) result = result.split(tmp).join('<script>');
129
+ } else if (lang === 'agent-browser') {
130
+ result = spawnDirect('agent-browser', ['eval', '--stdin'], safeCode);
131
+ } else {
132
+ result = runWithFile(lang, safeCode);
133
+ }
134
+ return { block: true, reason: `exec ran successfully. Output:\n\n${result || '(no output)'}` };
135
+ } catch (e) {
136
+ return { block: true, reason: `exec ran. Error:\n\n${(e.stdout || '') + (e.stderr || '') || e.message || '(exec failed)'}` };
137
+ }
138
+ }
139
+
140
+ if (!/^exec(\s|:)/.test(command) && !/^bun x gm-exec(@[^\s]*)?(\s|$)/.test(command) && !/^git /.test(command) && !/^bun x codebasesearch/.test(command) && !/(\bclaude\b)/.test(command) && !/^npm install .* \/config\/.gmweb/.test(command) && !/^bun install --cwd \/config\/.gmweb/.test(command)) {
141
+ let helpText = '';
142
+ try { helpText = '\n\n' + execSync('bun x gm-exec --help', { timeout: 10000 }).toString().trim(); } catch (e) {}
143
+ return { block: true, reason: `Bash is restricted to exec:<lang> and git.\n\nexec:<lang> syntax (lang auto-detected if omitted):\n exec:nodejs / exec:python / exec:bash / exec:typescript\n exec:go / exec:rust / exec:java / exec:c / exec:cpp\n exec:agent-browser ← plain JS piped to browser eval (NO base64)\n exec ← auto-detects language\n\nNEVER encode agent-browser code as base64 — pass plain JS directly.\n\nbun x gm-exec${helpText}\n\nAll other Bash commands are blocked.` };
144
+ }
145
+ }
146
+
147
+ const allowedTools = ['agent-browser', 'Skill', 'code-search', 'electron', 'TaskOutput', 'ReadMcpResourceTool', 'ListMcpResourcesTool'];
148
+ if (allowedTools.includes(tool_name)) return { allow: true };
149
+
150
+ return { allow: true };
151
+ } catch (error) {
152
+ return { allow: true };
153
+ }
154
+ };
155
+
156
+ try {
157
+ const result = run();
158
+ if (result.block) {
159
+ console.log(JSON.stringify({ decision: isGemini ? 'deny' : 'block', reason: result.reason }));
160
+ process.exit(0);
161
+ }
162
+ if (isGemini) console.log(JSON.stringify({ decision: 'allow' }));
163
+ process.exit(0);
164
+ } catch (error) {
165
+ process.exit(0);
166
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-oc",
3
- "version": "2.0.173",
3
+ "version": "2.0.175",
4
4
  "description": "State machine agent with hooks, skills, and automated git enforcement",
5
5
  "author": "AnEntrypoint",
6
6
  "license": "MIT",