@weppy/roblox-mcp 2.7.4 → 2.7.6

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +25 -5
  3. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/ChangelogDetailPage-BWM0bhGw.js → dashboard/dist/assets/ChangelogDetailPage-DglsIYkW.js} +1 -1
  4. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/ChangelogPage-DOXmMWDc.js → dashboard/dist/assets/ChangelogPage-65B3_w0_.js} +1 -1
  5. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/ConfirmModal-Dak7jVJo.js → dashboard/dist/assets/ConfirmModal-Cpk7SbKb.js} +1 -1
  6. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/ConnectionPage-Cds2YIvI.js → dashboard/dist/assets/ConnectionPage-B-IN5LsC.js} +1 -1
  7. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/GameChangeDetail-MfVogSqQ.js → dashboard/dist/assets/GameChangeDetail-DM3mWsFX.js} +1 -1
  8. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/InfoLabel-BDieYXVe.js → dashboard/dist/assets/InfoLabel-B_fEbHa7.js} +1 -1
  9. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/OverviewPage-CTH7jUzQ.js → dashboard/dist/assets/OverviewPage-B4O0bv4R.js} +1 -1
  10. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/PlaytestPage-BHQoeC4V.js → dashboard/dist/assets/PlaytestPage-BHLRKn8U.js} +1 -1
  11. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/SettingsPage-BL3qgVfn.js → dashboard/dist/assets/SettingsPage-DmIKC_O1.js} +1 -1
  12. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/StatusBadge-MxsMOCZ3.js → dashboard/dist/assets/StatusBadge-DRdnq30k.js} +1 -1
  13. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/SyncPage-IZEcPmIw.js → dashboard/dist/assets/SyncPage-CW_0kNpZ.js} +1 -1
  14. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/Tabs-CuTJuL80.js → dashboard/dist/assets/Tabs-BsTVkBUh.js} +1 -1
  15. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/TierComparison-DvuMctLJ.js → dashboard/dist/assets/TierComparison-poRtDe46.js} +1 -1
  16. package/dashboard/dist/assets/ToolsPage-Bt9vYA7u.css +1 -0
  17. package/dashboard/dist/assets/ToolsPage-D77yJ9jZ.js +1 -0
  18. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/TooltipText-DgxJjq91.js → dashboard/dist/assets/TooltipText-DX5jnyNF.js} +1 -1
  19. package/dashboard/dist/assets/UiStudioPage-YtdlkQzT.js +16 -0
  20. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/WhatsNewPage-CcELayE3.js → dashboard/dist/assets/WhatsNewPage--uCu0xCm.js} +1 -1
  21. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/index-CjBiayQg.js → dashboard/dist/assets/index-BPIBy2lU.js} +29 -29
  22. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/sample-requests-BrVQkhcR.js → dashboard/dist/assets/sample-requests-CwDMfktX.js} +1 -1
  23. package/{plugins/weppy-roblox-mcp/dashboard/dist/assets/useLiveUptime-s_CjVcUM.js → dashboard/dist/assets/useLiveUptime-ElD9lDzh.js} +1 -1
  24. package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/index.html +1 -1
  25. package/{plugins/weppy-roblox-mcp/dist → dist}/index.js +89 -88
  26. package/package.json +5 -5
  27. package/{plugins/weppy-roblox-mcp/roblox-plugin → roblox-plugin}/WeppyRobloxMCP.rbxm +0 -0
  28. package/.claude-plugin/marketplace.json +0 -43
  29. package/CODE_OF_CONDUCT.md +0 -29
  30. package/COMMERCIAL-LICENSE.md +0 -13
  31. package/CONTRIBUTING.md +0 -36
  32. package/SECURITY.md +0 -28
  33. package/SUPPORT.md +0 -25
  34. package/TRADEMARKS.md +0 -18
  35. package/glama.json +0 -7
  36. package/install.ps1 +0 -964
  37. package/install.sh +0 -939
  38. package/llms-full.txt +0 -13
  39. package/llms.txt +0 -69
  40. package/plugins/weppy-roblox-mcp/.claude-plugin/plugin.json +0 -28
  41. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ToolsPage-ByA_a1TP.js +0 -1
  42. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/ToolsPage-Dq7uIer6.css +0 -1
  43. package/plugins/weppy-roblox-mcp/dashboard/dist/assets/UiStudioPage-DVFz_q9v.js +0 -16
  44. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ChangelogDetailPage-D6Tqz7ut.css +0 -0
  45. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ChangelogPage-CNxAGfwG.css +0 -0
  46. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ConnectionPage-CNtjimlm.css +0 -0
  47. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/GameChangeDetail-C1XtdYwk.css +0 -0
  48. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/OverviewPage-Dsfl-NRT.css +0 -0
  49. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/PlaytestPage-DjjsIkke.css +0 -0
  50. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/SettingsPage-Du8-FZAO.css +0 -0
  51. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/StatusBadge-C2zYt5iE.css +0 -0
  52. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/SyncPage-Dm7Ni3j_.css +0 -0
  53. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/Tabs-876h0_zB.css +0 -0
  54. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/TierComparison-DGh9vLz0.css +0 -0
  55. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/UiStudioPage-eSinjpOX.css +0 -0
  56. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/WhatsNewPage-Lxgj0StO.css +0 -0
  57. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/index-CX4MHzNt.css +0 -0
  58. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/sample-requests-CygerZZ_.css +0 -0
  59. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/assets/ui-studio-sample-DrNTD6yi.png +0 -0
  60. /package/{plugins/weppy-roblox-mcp/dashboard → dashboard}/dist/weppy-icon.png +0 -0
package/install.sh DELETED
@@ -1,939 +0,0 @@
1
- #!/usr/bin/env bash
2
- #
3
- # WEPPY — One-line install script (macOS/Linux)
4
- #
5
- # Usage:
6
- # curl -fsSL https://raw.githubusercontent.com/hope1026/weppy-roblox-mcp/main/install.sh | bash
7
- #
8
- # Interactive 2 steps:
9
- # [1/2] Setup — install Roblox Studio Plugin via npx
10
- # [2/2] Register MCP with AI apps (user selection)
11
- #
12
-
13
- set -euo pipefail
14
-
15
- # ── Color definitions ──
16
- GREEN='\033[0;32m'
17
- YELLOW='\033[0;33m'
18
- RED='\033[0;31m'
19
- BLUE='\033[0;34m'
20
- CYAN='\033[0;36m'
21
- DIM='\033[2m'
22
- BOLD='\033[1m'
23
- NC='\033[0m'
24
-
25
- INSTALL_LOG_FILE="$(mktemp "${TMPDIR:-/tmp}/weppy-install-XXXXXX.log" 2>/dev/null || true)"
26
- if [ -z "${INSTALL_LOG_FILE:-}" ]; then
27
- INSTALL_LOG_FILE="${HOME}/weppy-install-error.log"
28
- : > "$INSTALL_LOG_FILE"
29
- fi
30
-
31
- if command -v tee >/dev/null 2>&1; then
32
- exec > >(tee -a "$INSTALL_LOG_FILE") 2>&1
33
- fi
34
-
35
- # ── Utilities ──
36
- # shellcheck disable=SC2059
37
- info() { printf "${BLUE}[INFO]${NC} %s\n" "$1"; }
38
- # shellcheck disable=SC2059
39
- success() { printf "${GREEN} ✓${NC} %s\n" "$1"; }
40
- # shellcheck disable=SC2059
41
- warn() { printf "${YELLOW} ⚠${NC} %s\n" "$1"; }
42
- # shellcheck disable=SC2059
43
- fail() { printf "${RED} ✗${NC} %s\n" "$1"; }
44
- # shellcheck disable=SC2059
45
- step() { printf "\n${BOLD}${CYAN}[%s]${NC} ${BOLD}%s${NC}\n" "$1" "$2"; }
46
-
47
- pause_on_failure_if_interactive() {
48
- if [ -t 1 ] && [ -r /dev/tty ]; then
49
- printf "\nPress Enter to exit..." >/dev/tty
50
- read -r _ </dev/tty || true
51
- fi
52
- }
53
-
54
- handle_install_error() {
55
- local exit_code=$?
56
- local line_no="$1"
57
- local failed_command="$2"
58
-
59
- trap - ERR
60
-
61
- printf "\n${RED}Installation failed.${NC}\n"
62
- printf " Command: %s\n" "$failed_command"
63
- printf " Line : %s\n" "$line_no"
64
- printf " Log : %s\n" "$INSTALL_LOG_FILE"
65
-
66
- pause_on_failure_if_interactive
67
- exit "$exit_code"
68
- }
69
-
70
- trap 'handle_install_error "${LINENO}" "$BASH_COMMAND"' ERR
71
-
72
- # Y/n prompt (default Y)
73
- confirm() {
74
- local prompt="$1"
75
- local reply
76
- if [ "${CI:-}" = "true" ]; then
77
- printf "%s (Y/n): Y\n" "$prompt"
78
- return 0
79
- fi
80
- printf "%s (Y/n): " "$prompt"
81
- read -r reply </dev/tty
82
- reply="${reply:-Y}"
83
- case "$reply" in
84
- [Yy]*) return 0 ;;
85
- *) return 1 ;;
86
- esac
87
- }
88
-
89
- # Add MCP server to JSON config file (path via env var — prevents shell injection)
90
- add_mcp_to_config() {
91
- local config_path="$1"
92
- local parent_dir
93
- parent_dir=$(dirname "$config_path")
94
- mkdir -p "$parent_dir"
95
- MCP_CONFIG_PATH="$config_path" node --input-type=commonjs -e '
96
- const fs = require("fs");
97
- const configPath = process.env.MCP_CONFIG_PATH;
98
- let config = {};
99
- try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch {}
100
- if (!config.mcpServers) config.mcpServers = {};
101
- config.mcpServers["weppy-roblox-mcp"] = { command: "npx", args: ["-y", "@weppy/roblox-mcp@latest"] };
102
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
103
- '
104
- }
105
-
106
- is_json_mcp_configured() {
107
- local config_path="$1"
108
-
109
- [ -f "$config_path" ] || return 1
110
-
111
- MCP_CONFIG_PATH="$config_path" node --input-type=commonjs -e '
112
- const fs = require("fs");
113
- const configPath = process.env.MCP_CONFIG_PATH;
114
- try {
115
- const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
116
- process.exit(config?.mcpServers?.["weppy-roblox-mcp"] ? 0 : 1);
117
- } catch {
118
- process.exit(1);
119
- }
120
- ' >/dev/null 2>&1
121
- }
122
-
123
- # Antigravity 설정에 canonical mcpServers 래퍼로 MCP 서버를 추가하고 legacy flat key를 정리
124
- add_antigravity_mcp_config() {
125
- local config_path="$1"
126
- local parent_dir
127
- parent_dir=$(dirname "$config_path")
128
- mkdir -p "$parent_dir"
129
- MCP_CONFIG_PATH="$config_path" node --input-type=commonjs -e '
130
- const fs = require("fs");
131
- const configPath = process.env.MCP_CONFIG_PATH;
132
- let config = {};
133
- try { config = JSON.parse(fs.readFileSync(configPath, "utf8")); } catch {}
134
- if (!config || typeof config !== "object" || Array.isArray(config)) {
135
- config = {};
136
- }
137
- const mcpServers = config.mcpServers;
138
- if (mcpServers !== undefined && (typeof mcpServers !== "object" || mcpServers === null || Array.isArray(mcpServers))) {
139
- throw new Error("Antigravity mcpServers must be an object");
140
- }
141
- const next = { ...config };
142
- delete next["weppy-roblox-mcp"];
143
- next.mcpServers = {
144
- ...(mcpServers || {}),
145
- "weppy-roblox-mcp": { command: "npx", args: ["-y", "@weppy/roblox-mcp@latest"] }
146
- };
147
- config = next;
148
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
149
- '
150
- }
151
-
152
- is_antigravity_mcp_configured() {
153
- local config_path="$1"
154
-
155
- [ -f "$config_path" ] || return 1
156
-
157
- MCP_CONFIG_PATH="$config_path" node --input-type=commonjs -e '
158
- const fs = require("fs");
159
- const configPath = process.env.MCP_CONFIG_PATH;
160
- function isJsonObject(value) {
161
- return typeof value === "object" && value !== null && !Array.isArray(value);
162
- }
163
- function hasExpectedCommandShape(value) {
164
- // Require an explicit `@<tag>` so the installer can upgrade legacy bare
165
- // entries (`@weppy/roblox-mcp`) — those reuse npx cache and trap users on
166
- // outdated versions. Tagged entries (`@latest`, `@2.6.4`, …) are preserved.
167
- return (
168
- isJsonObject(value) &&
169
- value.command === "npx" &&
170
- Array.isArray(value.args) &&
171
- value.args.length === 2 &&
172
- value.args[0] === "-y" &&
173
- typeof value.args[1] === "string" &&
174
- /^@weppy\/roblox-mcp@.+$/.test(value.args[1])
175
- );
176
- }
177
- try {
178
- const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
179
- const canonical = config?.mcpServers?.["weppy-roblox-mcp"];
180
- const hasLegacyFlatKey = Object.prototype.hasOwnProperty.call(config, "weppy-roblox-mcp");
181
- process.exit(hasExpectedCommandShape(canonical) && !hasLegacyFlatKey ? 0 : 1);
182
- } catch {
183
- process.exit(1);
184
- }
185
- ' >/dev/null 2>&1
186
- }
187
-
188
- is_codex_config_configured() {
189
- local config_path="$1"
190
-
191
- [ -f "$config_path" ] || return 1
192
- MCP_CODEX_CONFIG_PATH="$config_path" node --input-type=commonjs <<'NODE' >/dev/null 2>&1
193
- const fs = require("fs");
194
-
195
- const configPath = process.env.MCP_CODEX_CONFIG_PATH;
196
- const serverName = "weppy-roblox-mcp";
197
- const expectedCommand = "npx";
198
- // Require an explicit `@<tag>` so the installer can upgrade legacy bare entries.
199
- const packageSpecPattern = /^@weppy\/roblox-mcp@.+$/;
200
- const headerPattern = new RegExp(
201
- "^\\s*\\[\\s*mcp_servers\\." + serverName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "\\s*\\]\\s*(?:#.*)?$",
202
- );
203
-
204
- function stripCommentOutsideStrings(line) {
205
- let inSingle = false;
206
- let inDouble = false;
207
- let escaped = false;
208
-
209
- for (let index = 0; index < line.length; index += 1) {
210
- const char = line[index];
211
-
212
- if (char === '"' && !inSingle && !escaped) {
213
- inDouble = !inDouble;
214
- } else if (char === "'" && !inDouble && !escaped) {
215
- inSingle = !inSingle;
216
- } else if (char === "#" && !inSingle && !inDouble) {
217
- return line.slice(0, index).trimEnd();
218
- }
219
-
220
- escaped = char === "\\" && !escaped;
221
- if (char !== "\\") {
222
- escaped = false;
223
- }
224
- }
225
-
226
- return line.trimEnd();
227
- }
228
-
229
- function countTripleQuoteToggles(line, quote) {
230
- let count = 0;
231
- let inSingle = false;
232
- let inDouble = false;
233
- let escaped = false;
234
-
235
- for (let index = 0; index < line.length; index += 1) {
236
- const char = line[index] ?? "";
237
- const nextThree = line.slice(index, index + 3);
238
- const isOutsideStrings = !inSingle && !inDouble;
239
-
240
- if (isOutsideStrings && nextThree === quote.repeat(3)) {
241
- count += 1;
242
- index += 2;
243
- escaped = false;
244
- continue;
245
- }
246
-
247
- if (char === '"' && !inSingle && !escaped) {
248
- inDouble = !inDouble;
249
- } else if (char === "'" && !inDouble && !escaped) {
250
- inSingle = !inSingle;
251
- } else if (char === "#" && !inSingle && !inDouble) {
252
- break;
253
- }
254
-
255
- escaped = char === "\\" && !escaped;
256
- if (char !== "\\") {
257
- escaped = false;
258
- }
259
- }
260
-
261
- return count;
262
- }
263
-
264
- function advanceTripleQuoteState(line, state) {
265
- const next = { ...state };
266
- const tripleDoubleCount = countTripleQuoteToggles(line, '"');
267
- const tripleSingleCount = countTripleQuoteToggles(line, "'");
268
-
269
- if (!next.inTripleSingle && tripleDoubleCount % 2 === 1) {
270
- next.inTripleDouble = !next.inTripleDouble;
271
- }
272
-
273
- if (!next.inTripleDouble && tripleSingleCount % 2 === 1) {
274
- next.inTripleSingle = !next.inTripleSingle;
275
- }
276
-
277
- return next;
278
- }
279
-
280
- function isTomlTableHeaderLine(line) {
281
- const normalized = stripCommentOutsideStrings(line).trim();
282
-
283
- if (normalized.length === 0) {
284
- return false;
285
- }
286
-
287
- return /^\[\[.*\]\]$/.test(normalized) || /^\[.*\]$/.test(normalized);
288
- }
289
-
290
- function findAllCodexBlocks(source) {
291
- const lines = source.split("\n");
292
- const blocks = [];
293
- let activeLines = null;
294
- let state = {
295
- inTripleDouble: false,
296
- inTripleSingle: false,
297
- };
298
-
299
- for (const line of lines) {
300
- const isHeaderCandidate = !state.inTripleDouble && !state.inTripleSingle && isTomlTableHeaderLine(line);
301
- const isCodexHeader = isHeaderCandidate && headerPattern.test(line);
302
-
303
- if (isCodexHeader) {
304
- if (activeLines !== null) {
305
- blocks.push(activeLines.join("\n").trim());
306
- }
307
- activeLines = [line];
308
- } else if (activeLines !== null && isHeaderCandidate) {
309
- blocks.push(activeLines.join("\n").trim());
310
- activeLines = null;
311
- } else if (activeLines !== null) {
312
- activeLines.push(line);
313
- }
314
-
315
- state = advanceTripleQuoteState(line, state);
316
- }
317
-
318
- if (activeLines !== null) {
319
- blocks.push(activeLines.join("\n").trim());
320
- }
321
-
322
- return blocks;
323
- }
324
-
325
- function parseStringAssignment(value, key) {
326
- const match = new RegExp("^\\s*" + key + "\\s*=\\s*([\"'])([^\"']+)\\1\\s*$").exec(value);
327
- return match ? match[2] : null;
328
- }
329
-
330
- function parseTomlStringArray(value) {
331
- const match = /^\s*args\s*=\s*\[(.*)\]\s*$/ms.exec(value.trim());
332
-
333
- if (match === null) {
334
- return null;
335
- }
336
-
337
- const body = match[1] ?? "";
338
- const values = [];
339
- let cursor = 0;
340
- let expectValue = true;
341
-
342
- while (cursor < body.length) {
343
- while (cursor < body.length && /\s/.test(body[cursor] ?? "")) {
344
- cursor += 1;
345
- }
346
-
347
- if (cursor >= body.length) {
348
- break;
349
- }
350
-
351
- if (!expectValue) {
352
- if (body[cursor] !== ",") {
353
- return null;
354
- }
355
- cursor += 1;
356
- expectValue = true;
357
- continue;
358
- }
359
-
360
- const quote = body[cursor];
361
- if (quote !== '"' && quote !== "'") {
362
- return null;
363
- }
364
-
365
- cursor += 1;
366
- let token = "";
367
- let escaped = false;
368
-
369
- while (cursor < body.length) {
370
- const char = body[cursor] ?? "";
371
-
372
- if (char === quote && !escaped) {
373
- cursor += 1;
374
- values.push(token);
375
- break;
376
- }
377
-
378
- token += char;
379
- escaped = char === "\\" && !escaped;
380
- if (char !== "\\") {
381
- escaped = false;
382
- }
383
- cursor += 1;
384
- }
385
-
386
- if (cursor > body.length) {
387
- return null;
388
- }
389
-
390
- expectValue = false;
391
- }
392
-
393
- const leftover = body.slice(cursor).trim();
394
- if (leftover === ",") {
395
- return values;
396
- }
397
-
398
- return leftover.length === 0 ? values : null;
399
- }
400
-
401
- function collectArrayLines(lines, startIndex) {
402
- const collected = [stripCommentOutsideStrings(lines[startIndex] ?? "")];
403
- let bracketDepth = 0;
404
- let inSingle = false;
405
- let inDouble = false;
406
- let escaped = false;
407
-
408
- for (let lineIndex = startIndex; lineIndex < lines.length; lineIndex += 1) {
409
- const sanitized = stripCommentOutsideStrings(lines[lineIndex] ?? "");
410
- if (lineIndex !== startIndex) {
411
- collected.push(sanitized);
412
- }
413
-
414
- for (let index = 0; index < sanitized.length; index += 1) {
415
- const char = sanitized[index] ?? "";
416
-
417
- if (char === '"' && !inSingle && !escaped) {
418
- inDouble = !inDouble;
419
- } else if (char === "'" && !inDouble && !escaped) {
420
- inSingle = !inSingle;
421
- } else if (!inSingle && !inDouble) {
422
- if (char === "[") {
423
- bracketDepth += 1;
424
- } else if (char === "]") {
425
- bracketDepth -= 1;
426
- }
427
- }
428
-
429
- escaped = char === "\\" && !escaped;
430
- if (char !== "\\") {
431
- escaped = false;
432
- }
433
- }
434
-
435
- if (bracketDepth <= 0) {
436
- return {
437
- nextIndex: lineIndex,
438
- text: collected.join("\n"),
439
- };
440
- }
441
- }
442
-
443
- return null;
444
- }
445
-
446
- function parseCodexBlock(blockContent) {
447
- const lines = blockContent.split("\n");
448
- let command = null;
449
- let args = null;
450
- let hasConflict = false;
451
- let inTripleDouble = false;
452
- let inTripleSingle = false;
453
-
454
- for (let index = 1; index < lines.length; index += 1) {
455
- const line = lines[index] ?? "";
456
- const sanitized = stripCommentOutsideStrings(line);
457
- const trimmed = sanitized.trim();
458
-
459
- if (inTripleDouble) {
460
- if (countTripleQuoteToggles(sanitized, '"') % 2 === 1) {
461
- inTripleDouble = false;
462
- }
463
- continue;
464
- }
465
-
466
- if (inTripleSingle) {
467
- if (countTripleQuoteToggles(sanitized, "'") % 2 === 1) {
468
- inTripleSingle = false;
469
- }
470
- continue;
471
- }
472
-
473
- if (countTripleQuoteToggles(sanitized, '"') % 2 === 1) {
474
- inTripleDouble = true;
475
- continue;
476
- }
477
-
478
- if (countTripleQuoteToggles(sanitized, "'") % 2 === 1) {
479
- inTripleSingle = true;
480
- continue;
481
- }
482
-
483
- if (trimmed.length === 0) {
484
- continue;
485
- }
486
-
487
- if (/^command\s*=/.test(trimmed)) {
488
- const parsedCommand = parseStringAssignment(trimmed, "command");
489
- if (command !== null || parsedCommand === null) {
490
- hasConflict = true;
491
- } else {
492
- command = parsedCommand;
493
- }
494
- continue;
495
- }
496
-
497
- if (/^args\s*=/.test(trimmed)) {
498
- const collected = collectArrayLines(lines, index);
499
- const parsedArgs = collected === null ? null : parseTomlStringArray(collected.text);
500
-
501
- if (args !== null || parsedArgs === null || collected === null) {
502
- hasConflict = true;
503
- } else {
504
- args = parsedArgs;
505
- index = collected.nextIndex;
506
- }
507
- }
508
- }
509
-
510
- return {
511
- args,
512
- command,
513
- hasConflict,
514
- };
515
- }
516
-
517
- function isStructurallySafe(source) {
518
- let bracketDepth = 0;
519
- let braceDepth = 0;
520
- let inSingle = false;
521
- let inDouble = false;
522
- let escaped = false;
523
- let tripleState = {
524
- inTripleDouble: false,
525
- inTripleSingle: false,
526
- };
527
-
528
- for (const line of source.split("\n")) {
529
- tripleState = advanceTripleQuoteState(line, tripleState);
530
-
531
- for (let index = 0; index < line.length; index += 1) {
532
- const char = line[index] ?? "";
533
-
534
- if (!inSingle && !inDouble && char === "#") {
535
- break;
536
- }
537
-
538
- if (char === '"' && !inSingle && !escaped) {
539
- inDouble = !inDouble;
540
- } else if (char === "'" && !inDouble && !escaped) {
541
- inSingle = !inSingle;
542
- } else if (!inSingle && !inDouble) {
543
- if (char === "[") {
544
- bracketDepth += 1;
545
- } else if (char === "]") {
546
- bracketDepth -= 1;
547
- if (bracketDepth < 0) {
548
- return false;
549
- }
550
- } else if (char === "{") {
551
- braceDepth += 1;
552
- } else if (char === "}") {
553
- braceDepth -= 1;
554
- if (braceDepth < 0) {
555
- return false;
556
- }
557
- }
558
- }
559
-
560
- escaped = char === "\\" && !escaped;
561
- if (char !== "\\") {
562
- escaped = false;
563
- }
564
- }
565
- }
566
-
567
- return (
568
- !tripleState.inTripleDouble &&
569
- !tripleState.inTripleSingle &&
570
- bracketDepth === 0 &&
571
- braceDepth === 0 &&
572
- !inSingle &&
573
- !inDouble
574
- );
575
- }
576
-
577
- try {
578
- const source = fs.readFileSync(configPath, "utf8");
579
- if (!isStructurallySafe(source)) {
580
- process.exit(1);
581
- }
582
-
583
- const blocks = findAllCodexBlocks(source);
584
- if (blocks.length !== 1) {
585
- process.exit(1);
586
- }
587
-
588
- const parsed = parseCodexBlock(blocks[0]);
589
- const isConfigured =
590
- !parsed.hasConflict &&
591
- parsed.command === expectedCommand &&
592
- Array.isArray(parsed.args) &&
593
- parsed.args.length === 2 &&
594
- parsed.args[0] === "-y" &&
595
- typeof parsed.args[1] === "string" &&
596
- packageSpecPattern.test(parsed.args[1]);
597
-
598
- process.exit(isConfigured ? 0 : 1);
599
- } catch {
600
- process.exit(1);
601
- }
602
- NODE
603
- }
604
-
605
- resolve_optional_cli_command() {
606
- local command_name="$1"
607
- local npm_prefix=""
608
-
609
- if command -v "$command_name" >/dev/null 2>&1; then
610
- command -v "$command_name"
611
- return 0
612
- fi
613
-
614
- npm_prefix=$(npm prefix -g 2>/dev/null || true)
615
- if [ -n "$npm_prefix" ] && [ -x "$npm_prefix/bin/$command_name" ]; then
616
- printf "%s\n" "$npm_prefix/bin/$command_name"
617
- return 0
618
- fi
619
-
620
- return 1
621
- }
622
-
623
- # ── Header ──
624
- # shellcheck disable=SC2059
625
- printf "\n${BOLD}WEPPY Installer${NC}\n"
626
- # shellcheck disable=SC2059
627
- printf "${DIM}AI-powered Roblox Studio integration${NC}\n"
628
- printf "%s\n" "════════════════════════════════════"
629
-
630
- # ── Node.js check ──
631
- if ! command -v node &>/dev/null; then
632
- fail "Node.js is not installed"
633
- printf " Install Node.js 18+: https://nodejs.org\n"
634
- exit 1
635
- fi
636
-
637
- NODE_VERSION=$(node -v | sed 's/v//' | cut -d. -f1)
638
- if [ "$NODE_VERSION" -lt 18 ]; then
639
- fail "Node.js 18 or higher required (current: $(node -v))"
640
- printf " Upgrade: https://nodejs.org\n"
641
- exit 1
642
- fi
643
- success "Node.js $(node -v) detected"
644
-
645
- # ═══════════════════════════════════
646
- # [1/2] Setup — Roblox Studio Plugin
647
- # ═══════════════════════════════════
648
- step "1/2" "Setup Roblox Studio Plugin"
649
-
650
- if confirm " Run npx -y @weppy/roblox-mcp@latest --setup?"; then
651
- setup_tmp_dir=""
652
- if setup_tmp_dir=$(mktemp -d "${TMPDIR:-/tmp}/weppy-setup-XXXXXX" 2>/dev/null); then
653
- :
654
- elif setup_tmp_dir=$(mktemp -d 2>/dev/null); then
655
- :
656
- fi
657
-
658
- if [ -n "${setup_tmp_dir:-}" ] && [ -d "$setup_tmp_dir" ]; then
659
- # stdin을 /dev/null로 격리: curl|bash 파이프 모드에서 stdio MCP 서버가
660
- # bash의 남은 스크립트 바이트를 소비해버리는 문제를 방지한다
661
- # The @latest tag forces npx to resolve from the registry instead of
662
- # reusing an older version pinned in the npm cache.
663
- if (cd "$setup_tmp_dir" && npx -y "@weppy/roblox-mcp@latest" --setup </dev/null); then
664
- success "Setup complete"
665
- else
666
- warn "Setup encountered a warning (non-blocking)"
667
- fi
668
-
669
- rm -rf "$setup_tmp_dir"
670
- else
671
- warn "Setup encountered a warning (failed to create temp working directory)"
672
- fi
673
- else
674
- warn "Setup skipped"
675
- fi
676
-
677
- # ═══════════════════════════════════
678
- # [2/2] Register MCP with AI apps
679
- # ═══════════════════════════════════
680
- step "2/2" "Register MCP with AI apps"
681
- printf " Automatic registration: Claude Code, Claude Desktop, Cursor, Codex CLI/App, Gemini CLI, Antigravity\n"
682
-
683
- MCP_COMMAND='npx -y @weppy/roblox-mcp@latest'
684
-
685
- # AI app detection
686
- declare -a DETECTED_NAMES=()
687
- declare -a DETECTED_TYPES=()
688
- declare -a NOT_DETECTED=()
689
-
690
- # Claude Code
691
- # `claude mcp add` stores entries under ~/.claude.json or in local/user scope,
692
- # so prefer `claude mcp list` as the source of truth when the CLI is available
693
- # (the JSON path checks remain as a fallback).
694
- CLAUDE_PROJECT_MCP_CONFIG="$PWD/.mcp.json"
695
- CLAUDE_GLOBAL_MCP_CONFIG="$HOME/.claude/mcp.json"
696
- CLAUDE_CLI_COMMAND="$(resolve_optional_cli_command claude 2>/dev/null || true)"
697
-
698
- is_claude_cli_configured() {
699
- [ -n "$CLAUDE_CLI_COMMAND" ] || return 1
700
- # The entry counts as configured only when its args carry an explicit `@tag`
701
- # (e.g. `@latest`). Legacy bare entries fall through and get re-registered.
702
- "$CLAUDE_CLI_COMMAND" mcp list 2>/dev/null \
703
- | grep "^weppy-roblox-mcp:" \
704
- | grep -q "@weppy/roblox-mcp@"
705
- }
706
-
707
- if is_claude_cli_configured \
708
- || is_json_mcp_configured "$CLAUDE_PROJECT_MCP_CONFIG" \
709
- || is_json_mcp_configured "$CLAUDE_GLOBAL_MCP_CONFIG"; then
710
- DETECTED_NAMES+=("Claude Code (configured)")
711
- DETECTED_TYPES+=("claude-code")
712
- elif [ -n "$CLAUDE_CLI_COMMAND" ]; then
713
- DETECTED_NAMES+=("Claude Code (CLI)")
714
- DETECTED_TYPES+=("claude-code")
715
- else
716
- NOT_DETECTED+=("Claude Code (not found)")
717
- fi
718
-
719
- # Claude Desktop (macOS)
720
- CLAUDE_DESKTOP_CONFIG="$HOME/Library/Application Support/Claude/claude_desktop_config.json"
721
- if [ -f "$CLAUDE_DESKTOP_CONFIG" ]; then
722
- DETECTED_NAMES+=("Claude Desktop")
723
- DETECTED_TYPES+=("claude-desktop")
724
- elif [ "$(uname)" = "Darwin" ]; then
725
- NOT_DETECTED+=("Claude Desktop (config not found)")
726
- fi
727
-
728
- # Cursor (detect only when mcp.json or binary exists)
729
- CURSOR_MCP_CONFIG="$HOME/.cursor/mcp.json"
730
- if [ -f "$CURSOR_MCP_CONFIG" ] || command -v cursor &>/dev/null; then
731
- DETECTED_NAMES+=("Cursor")
732
- DETECTED_TYPES+=("cursor")
733
- else
734
- NOT_DETECTED+=("Cursor (not found)")
735
- fi
736
-
737
- # Codex CLI / Codex App (share the same ~/.codex/config.toml)
738
- CODEX_CONFIG="$HOME/.codex/config.toml"
739
- CODEX_CLI_COMMAND="$(resolve_optional_cli_command codex 2>/dev/null || true)"
740
- if is_codex_config_configured "$CODEX_CONFIG"; then
741
- DETECTED_NAMES+=("Codex CLI/App (configured)")
742
- DETECTED_TYPES+=("codex-cli")
743
- elif [ -n "$CODEX_CLI_COMMAND" ]; then
744
- DETECTED_NAMES+=("Codex CLI/App")
745
- DETECTED_TYPES+=("codex-cli")
746
- else
747
- NOT_DETECTED+=("Codex CLI/App (not found)")
748
- fi
749
-
750
- # Gemini CLI
751
- # Gemini CLI
752
- GEMINI_CONFIG="$HOME/.gemini/settings.json"
753
- GEMINI_CLI_COMMAND="$(resolve_optional_cli_command gemini 2>/dev/null || true)"
754
- if is_json_mcp_configured "$GEMINI_CONFIG"; then
755
- DETECTED_NAMES+=("Gemini CLI (configured)")
756
- DETECTED_TYPES+=("gemini-cli")
757
- elif [ -n "$GEMINI_CLI_COMMAND" ]; then
758
- DETECTED_NAMES+=("Gemini CLI")
759
- DETECTED_TYPES+=("gemini-cli")
760
- else
761
- NOT_DETECTED+=("Gemini CLI (not found)")
762
- fi
763
-
764
- # Antigravity (unofficial path, auto-register if found)
765
- ANTIGRAVITY_CONFIG="$HOME/.gemini/antigravity/mcp_config.json"
766
- if is_antigravity_mcp_configured "$ANTIGRAVITY_CONFIG"; then
767
- DETECTED_NAMES+=("Antigravity (configured)")
768
- DETECTED_TYPES+=("antigravity")
769
- elif [ -f "$ANTIGRAVITY_CONFIG" ]; then
770
- DETECTED_NAMES+=("Antigravity")
771
- DETECTED_TYPES+=("antigravity")
772
- elif [ -d "$HOME/.gemini/antigravity" ]; then
773
- DETECTED_NAMES+=("Antigravity")
774
- DETECTED_TYPES+=("antigravity")
775
- else
776
- NOT_DETECTED+=("Antigravity (not found)")
777
- fi
778
-
779
- if [ ${#DETECTED_NAMES[@]} -eq 0 ]; then
780
- warn "No AI apps detected"
781
- info "Register MCP server manually: $MCP_COMMAND"
782
- else
783
- # shellcheck disable=SC2059
784
- printf "\n ${BOLD}Detected:${NC}\n"
785
- for i in "${!DETECTED_NAMES[@]}"; do
786
- # shellcheck disable=SC2059
787
- printf " ${GREEN}%d.${NC} %s\n" "$((i + 1))" "${DETECTED_NAMES[$i]}"
788
- done
789
-
790
- if [ ${#NOT_DETECTED[@]} -gt 0 ]; then
791
- # shellcheck disable=SC2059
792
- printf "\n ${DIM}Not detected:${NC}\n"
793
- for item in "${NOT_DETECTED[@]}"; do
794
- # shellcheck disable=SC2059
795
- printf " ${DIM}- %s${NC}\n" "$item"
796
- done
797
- fi
798
-
799
- # shellcheck disable=SC2059
800
- printf "\n Select apps to register ${DIM}(comma-separated, 'a' for all, 'n' to skip)${NC}\n"
801
- printf " → "
802
- if [ "${CI:-}" = "true" ]; then
803
- selection="a"
804
- printf "a\n"
805
- else
806
- read -r selection </dev/tty
807
- fi
808
- selection="${selection:-n}"
809
-
810
- # Parse selection
811
- declare -a SELECTED_INDICES=()
812
- case "$selection" in
813
- [Nn])
814
- warn "MCP registration skipped"
815
- ;;
816
- [Aa])
817
- for i in "${!DETECTED_NAMES[@]}"; do
818
- SELECTED_INDICES+=("$i")
819
- done
820
- ;;
821
- *)
822
- IFS=',' read -ra PARTS <<< "$selection"
823
- for part in "${PARTS[@]}"; do
824
- part=$(echo "$part" | tr -d ' ')
825
- if [[ "$part" =~ ^[0-9]+$ ]]; then
826
- idx=$((part - 1))
827
- if [ "$idx" -ge 0 ] && [ "$idx" -lt ${#DETECTED_NAMES[@]} ]; then
828
- SELECTED_INDICES+=("$idx")
829
- fi
830
- fi
831
- done
832
- ;;
833
- esac
834
-
835
- # Register selected apps
836
- for idx in "${SELECTED_INDICES[@]}"; do
837
- app_type="${DETECTED_TYPES[$idx]}"
838
- app_name="${DETECTED_NAMES[$idx]}"
839
-
840
- case "$app_type" in
841
- claude-code)
842
- if is_claude_cli_configured \
843
- || is_json_mcp_configured "$CLAUDE_PROJECT_MCP_CONFIG" \
844
- || is_json_mcp_configured "$CLAUDE_GLOBAL_MCP_CONFIG"; then
845
- success "Already configured: $app_name"
846
- elif [ -n "$CLAUDE_CLI_COMMAND" ]; then
847
- claude_stderr_file=$(mktemp "${TMPDIR:-/tmp}/weppy-claude-XXXXXX.err" 2>/dev/null || echo "${HOME}/weppy-claude.err")
848
- # Best-effort remove any legacy bare entry so the subsequent add can
849
- # install the canonical `@latest` form. Ignore errors when nothing
850
- # exists.
851
- "$CLAUDE_CLI_COMMAND" mcp remove weppy-roblox-mcp >/dev/null 2>&1 || true
852
- # Capture the CLI exit code immediately so it isn't overwritten by the
853
- # subsequent grep check (which would otherwise leak its own exit code).
854
- claude_exit_code=0
855
- "$CLAUDE_CLI_COMMAND" mcp add weppy-roblox-mcp -- npx -y "@weppy/roblox-mcp@latest" 2>"$claude_stderr_file" || claude_exit_code=$?
856
- if [ "$claude_exit_code" -eq 0 ]; then
857
- success "Registered: $app_name"
858
- elif grep -qi "already exists" "$claude_stderr_file"; then
859
- # Already registered in another scope — not a failure
860
- success "Already configured: $app_name"
861
- else
862
- fail "Failed: $app_name (exit=$claude_exit_code)"
863
- printf " CLI: %s\n" "$CLAUDE_CLI_COMMAND"
864
- printf " stderr:\n"
865
- sed 's/^/ /' "$claude_stderr_file" || true
866
- fi
867
- rm -f "$claude_stderr_file"
868
- else
869
- fail "Failed: $app_name (claude CLI not found)"
870
- fi
871
- ;;
872
- claude-desktop)
873
- if add_mcp_to_config "$CLAUDE_DESKTOP_CONFIG"; then
874
- success "Registered: $app_name"
875
- else
876
- fail "Failed: $app_name"
877
- fi
878
- ;;
879
- cursor)
880
- if add_mcp_to_config "$HOME/.cursor/mcp.json"; then
881
- success "Registered: $app_name"
882
- else
883
- fail "Failed: $app_name"
884
- fi
885
- ;;
886
- codex-cli)
887
- if is_codex_config_configured "$CODEX_CONFIG"; then
888
- success "Already configured: $app_name"
889
- else
890
- [ -n "$CODEX_CLI_COMMAND" ] && "$CODEX_CLI_COMMAND" mcp remove weppy-roblox-mcp >/dev/null 2>&1 || true
891
- fi
892
- if is_codex_config_configured "$CODEX_CONFIG"; then
893
- :
894
- elif [ -n "$CODEX_CLI_COMMAND" ] && "$CODEX_CLI_COMMAND" mcp add weppy-roblox-mcp -- npx -y "@weppy/roblox-mcp@latest" 2>/dev/null; then
895
- success "Registered: $app_name"
896
- elif is_codex_config_configured "$CODEX_CONFIG"; then
897
- success "Already configured: $app_name"
898
- else
899
- fail "Failed: $app_name"
900
- fi
901
- ;;
902
- gemini-cli)
903
- if is_json_mcp_configured "$GEMINI_CONFIG"; then
904
- success "Already configured: $app_name"
905
- elif add_mcp_to_config "$GEMINI_CONFIG"; then
906
- success "Registered: $app_name"
907
- else
908
- fail "Failed: $app_name"
909
- fi
910
- ;;
911
- antigravity)
912
- if is_antigravity_mcp_configured "$ANTIGRAVITY_CONFIG"; then
913
- success "Already configured: $app_name"
914
- elif add_antigravity_mcp_config "$ANTIGRAVITY_CONFIG"; then
915
- success "Registered: $app_name"
916
- else
917
- fail "Failed: $app_name"
918
- fi
919
- ;;
920
- esac
921
- done
922
- fi
923
-
924
- # ═══════════════════════════════════
925
- # Installation summary
926
- # ═══════════════════════════════════
927
- # shellcheck disable=SC2059
928
- printf "\n%s\n" "════════════════════════════════════"
929
- # shellcheck disable=SC2059
930
- printf "${BOLD}Installation complete!${NC}\n\n"
931
- # shellcheck disable=SC2059
932
- printf " ${BOLD}Next steps:${NC}\n"
933
- printf " 1. Restart Roblox Studio\n"
934
- # shellcheck disable=SC2059
935
- printf " 2. Look for the ${BOLD}WEPPY${NC} button in the Plugins tab\n"
936
- printf " 3. Click Connect and start building with AI!\n\n"
937
- printf " Auto registration: Claude Code, Claude Desktop, Cursor, Codex CLI/App, Gemini CLI, Antigravity\n\n"
938
- # shellcheck disable=SC2059
939
- printf " ${DIM}Docs: https://weppyai.com/en/install${NC}\n\n"