ropilot 0.1.23 → 0.1.25

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 (3) hide show
  1. package/lib/proxy.js +22 -0
  2. package/lib/setup.js +36 -164
  3. package/package.json +1 -1
package/lib/proxy.js CHANGED
@@ -8,6 +8,7 @@
8
8
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
9
9
  import { join } from 'path';
10
10
  import { homedir } from 'os';
11
+ import { spawn } from 'child_process';
11
12
  import { getApiKey, EDGE_URL, loadGlobalConfig, saveGlobalConfig } from './config.js';
12
13
 
13
14
  // Plugin version check URL (served from ropilot.ai main server)
@@ -111,6 +112,24 @@ async function processRequest(apiKey, request) {
111
112
  }
112
113
  }
113
114
 
115
+ /**
116
+ * Run update in background (non-blocking)
117
+ * Claude/Cursor spawn MCP servers from the project root, so process.cwd() is correct
118
+ */
119
+ function runBackgroundUpdate() {
120
+ try {
121
+ const child = spawn('npx', ['ropilot', 'update'], {
122
+ cwd: process.cwd(),
123
+ detached: true,
124
+ stdio: 'ignore',
125
+ shell: true
126
+ });
127
+ child.unref();
128
+ } catch (err) {
129
+ // Silently ignore - update is not critical
130
+ }
131
+ }
132
+
114
133
  /**
115
134
  * Check for plugin updates (runs in background, writes to stderr)
116
135
  */
@@ -161,6 +180,9 @@ export async function serve() {
161
180
  process.exit(1);
162
181
  }
163
182
 
183
+ // Run update in background (non-blocking) - keeps prompts fresh
184
+ runBackgroundUpdate();
185
+
164
186
  // Check for plugin updates in background (non-blocking)
165
187
  checkPluginUpdate();
166
188
 
package/lib/setup.js CHANGED
@@ -1,11 +1,5 @@
1
1
  /**
2
2
  * Setup and initialization for Ropilot CLI
3
- *
4
- * Handles:
5
- * - First-time setup
6
- * - Downloading system prompts
7
- * - Creating project configs
8
- * - Installing Studio plugin
9
3
  */
10
4
 
11
5
  import { existsSync, writeFileSync, mkdirSync, readFileSync, readdirSync } from 'fs';
@@ -46,13 +40,11 @@ function isWSL() {
46
40
  * Get Windows username when running in WSL (tries multiple methods)
47
41
  */
48
42
  function getWindowsUsername() {
49
- // Method 1: cmd.exe
50
43
  try {
51
44
  const result = execSync('cmd.exe /c echo %USERNAME%', { encoding: 'utf-8', timeout: 5000 }).trim();
52
45
  if (result && !result.includes('%')) return result;
53
46
  } catch {}
54
47
 
55
- // Method 2: whoami.exe (returns DOMAIN\user)
56
48
  try {
57
49
  const result = execSync('/mnt/c/Windows/System32/whoami.exe', { encoding: 'utf-8', timeout: 5000 }).trim();
58
50
  if (result && result.includes('\\')) {
@@ -60,7 +52,6 @@ function getWindowsUsername() {
60
52
  }
61
53
  } catch {}
62
54
 
63
- // Method 3: powershell
64
55
  try {
65
56
  const result = execSync('powershell.exe -NoProfile -Command "$env:USERNAME"', { encoding: 'utf-8', timeout: 5000 }).trim();
66
57
  if (result) return result;
@@ -86,20 +77,16 @@ function getValidWindowsUsers() {
86
77
 
87
78
  /**
88
79
  * Get Roblox plugins folder path based on OS (with WSL detection)
89
- * Returns { path, needsPrompt, users } - needsPrompt true if user selection needed
90
80
  */
91
81
  function getPluginsFolder() {
92
82
  const os = platform();
93
83
 
94
- // Check for WSL first - need to use Windows path
95
84
  if (os === 'linux' && isWSL()) {
96
- // Try to auto-detect Windows username
97
85
  const windowsUser = getWindowsUsername();
98
86
  if (windowsUser && existsSync(join('/mnt/c/Users', windowsUser))) {
99
87
  return { path: join('/mnt/c/Users', windowsUser, 'AppData', 'Local', 'Roblox', 'Plugins') };
100
88
  }
101
89
 
102
- // Fallback: scan /mnt/c/Users
103
90
  const validUsers = getValidWindowsUsers();
104
91
  if (validUsers.length === 1) {
105
92
  return { path: join('/mnt/c/Users', validUsers[0], 'AppData', 'Local', 'Roblox', 'Plugins') };
@@ -115,7 +102,6 @@ function getPluginsFolder() {
115
102
  } else if (os === 'darwin') {
116
103
  return { path: join(homedir(), 'Documents', 'Roblox', 'Plugins') };
117
104
  } else {
118
- // Linux without WSL - Roblox doesn't officially support Linux
119
105
  return { path: null };
120
106
  }
121
107
  }
@@ -141,33 +127,26 @@ function prompt(question) {
141
127
  * Install Studio plugin to Roblox plugins folder
142
128
  */
143
129
  async function installPlugin() {
144
- console.log('Installing Studio plugin...');
145
-
146
130
  try {
147
- // Check plugin version
148
131
  const versionResponse = await fetch(PLUGIN_VERSION_URL);
149
132
  if (!versionResponse.ok) {
150
133
  throw new Error(`Failed to fetch plugin version: ${versionResponse.status}`);
151
134
  }
152
135
  const versionInfo = await versionResponse.json();
153
- console.log(` Plugin version: ${versionInfo.version}`);
154
136
 
155
- // Download plugin
156
137
  const pluginResponse = await fetch(PLUGIN_DOWNLOAD_URL);
157
138
  if (!pluginResponse.ok) {
158
139
  throw new Error(`Failed to download plugin: ${pluginResponse.status}`);
159
140
  }
160
141
  const pluginSource = await pluginResponse.text();
161
142
 
162
- // Get plugins folder
163
143
  let folderResult = getPluginsFolder();
164
144
  let pluginsFolder = folderResult.path;
165
145
 
166
- // If WSL detected multiple users, prompt for selection
167
146
  if (folderResult.needsPrompt && folderResult.users) {
168
- console.log(' Multiple Windows users detected:');
169
- folderResult.users.forEach((u, i) => console.log(` ${i + 1}. ${u}`));
170
- const choice = await prompt(` Enter number (1-${folderResult.users.length}) or username: `);
147
+ console.log('Multiple Windows users detected:');
148
+ folderResult.users.forEach((u, i) => console.log(` ${i + 1}. ${u}`));
149
+ const choice = await prompt(`Select user (1-${folderResult.users.length}): `);
171
150
 
172
151
  let selectedUser;
173
152
  const num = parseInt(choice);
@@ -184,81 +163,63 @@ async function installPlugin() {
184
163
  }
185
164
  }
186
165
 
187
- // If still no folder and running in WSL, prompt for username directly
188
166
  if (!pluginsFolder && isWSL()) {
189
- const username = await prompt(' Enter your Windows username: ');
190
- if (username && existsSync(join('/mnt/c/Users', username))) {
191
- pluginsFolder = join('/mnt/c/Users', username, 'AppData', 'Local', 'Roblox', 'Plugins');
192
- } else if (username) {
193
- // Trust user input even if path doesn't exist yet
167
+ const username = await prompt('Enter your Windows username: ');
168
+ if (username) {
194
169
  pluginsFolder = join('/mnt/c/Users', username, 'AppData', 'Local', 'Roblox', 'Plugins');
195
170
  }
196
171
  }
197
172
 
198
173
  if (!pluginsFolder) {
199
- console.log(' Could not determine Roblox plugins folder');
200
- console.log(' Manual install: Download from https://ropilot.ai/plugin/StudioPlugin.lua');
174
+ console.log('Could not find Roblox plugins folder');
175
+ console.log('Download manually: https://ropilot.ai/plugin/StudioPlugin.lua');
201
176
  return false;
202
177
  }
203
178
 
204
- // Create plugins folder if it doesn't exist
205
179
  mkdirSync(pluginsFolder, { recursive: true });
206
180
 
207
- // Write plugin file
208
181
  const pluginPath = join(pluginsFolder, 'RopilotPlugin.lua');
209
182
  writeFileSync(pluginPath, pluginSource);
210
183
 
211
- console.log(` Installed to: ${pluginPath}`);
184
+ console.log(`Plugin v${versionInfo.version} installed`);
212
185
 
213
- // Save version to config
214
186
  const config = loadGlobalConfig();
215
187
  config.pluginVersion = versionInfo.version;
216
188
  saveGlobalConfig(config);
217
189
 
218
190
  return true;
219
191
  } catch (err) {
220
- console.error(` Failed to install plugin: ${err.message}`);
221
- console.log(' You can manually download from: https://ropilot.ai/plugin/StudioPlugin.lua');
192
+ console.error(`Plugin install failed: ${err.message}`);
193
+ console.log('Download manually: https://ropilot.ai/plugin/StudioPlugin.lua');
222
194
  return false;
223
195
  }
224
196
  }
225
197
 
226
-
227
198
  /**
228
199
  * Download prompts from edge server
229
200
  */
230
201
  async function downloadPrompts(apiKey) {
231
- console.log('Downloading latest agent package...');
232
-
233
202
  try {
234
- // Fetch from edge server
235
203
  const response = await fetch(`${EDGE_URL}/package`);
236
204
  if (!response.ok) {
237
205
  throw new Error(`Server returned ${response.status}`);
238
206
  }
239
207
 
240
208
  const pkg = await response.json();
241
- console.log(`Package version: ${pkg.version}`);
242
209
 
243
- // Ensure prompts directory exists
244
210
  mkdirSync(PROMPTS_DIR, { recursive: true });
245
-
246
- // Write package info
247
211
  writeFileSync(join(PROMPTS_DIR, 'package.json'), JSON.stringify(pkg, null, 2));
248
212
 
249
- // Update config with version info
250
213
  const config = loadGlobalConfig();
251
214
  config.promptsVersion = pkg.version;
252
215
  config.lastUpdated = new Date().toISOString();
253
216
  saveGlobalConfig(config);
254
217
 
255
- console.log('Agent package downloaded successfully!');
218
+ console.log(`Agent package v${pkg.version} downloaded`);
256
219
  return pkg;
257
220
  } catch (err) {
258
- console.error('Failed to download from server:', err.message);
259
- console.log('Using bundled defaults...');
221
+ console.error('Failed to download package:', err.message);
260
222
 
261
- // Fallback to bundled defaults
262
223
  const prompts = getDefaultPrompts();
263
224
  mkdirSync(PROMPTS_DIR, { recursive: true });
264
225
  for (const [name, content] of Object.entries(prompts)) {
@@ -275,57 +236,12 @@ async function downloadPrompts(apiKey) {
275
236
  }
276
237
 
277
238
  /**
278
- * Get default prompts (bundled)
239
+ * Get default prompts (bundled fallback)
279
240
  */
280
241
  function getDefaultPrompts() {
281
242
  return {
282
- 'CLAUDE.md': `# Ropilot - AI-Powered Roblox Development
283
-
284
- You are connected to Ropilot, an AI assistant for Roblox Studio development.
285
-
286
- ## Available Tools
287
-
288
- Ropilot provides MCP tools for interacting with Roblox Studio:
289
-
290
- ### Reading & Inspection
291
- - \`ropilot_listchildren\` - List children of an instance
292
- - \`ropilot_getproperties\` - Get properties of an instance
293
- - \`ropilot_search\` - Search for instances by name
294
- - \`ropilot_searchbyclass\` - Search for instances by class name
295
-
296
- ### Playtesting
297
- - \`ropilot_test_start_playtest\` - Start a playtest session
298
- - \`ropilot_test_stop_playtest\` - Stop the current playtest
299
- - \`ropilot_test_run_client_lua\` - Run Lua code on the client
300
- - \`ropilot_test_run_server_lua\` - Run Lua code on the server
301
-
302
- ### Data Management
303
- - \`ropilot_reset_data\` - Reset player data in DataStore
304
-
305
- ## Context Targeting
306
-
307
- Commands can target different contexts:
308
- - \`edit\` - Edit mode (default)
309
- - \`server\` - Server context during playtest
310
- - \`client\` - Client context during playtest
311
-
312
- Use the \`target_context\` parameter to specify which context should execute the command.
313
-
314
- ## Best Practices
315
-
316
- 1. **Read before modify** - Always inspect the current state before making changes
317
- 2. **Use specific paths** - Reference instances by their full path (e.g., "Workspace.Map.Building")
318
- 3. **Delegate playtesting** - Use the roblox-tester subagent for playtest operations
319
- 4. **Test incrementally** - Start playtests to verify changes work correctly
320
- `,
321
-
322
- 'subagents.json': JSON.stringify({
323
- "roblox-tester": {
324
- "description": "Fast subagent for executing Roblox playtest steps",
325
- "tools": ["ropilot_test_*", "ropilot_get_*", "ropilot_execute_lua"],
326
- "prompt": "You are a Roblox playtest executor. Run the specified test steps and report results concisely."
327
- }
328
- }, null, 2)
243
+ 'CLAUDE.md': `@import ROPILOT.md\n`,
244
+ 'ROPILOT.md': `# Ropilot\n\nRoblox Studio AI assistant. Use ropilot_* MCP tools to interact with Studio.\n`
329
245
  };
330
246
  }
331
247
 
@@ -334,70 +250,61 @@ Use the \`target_context\` parameter to specify which context should execute the
334
250
  */
335
251
  function setupProjectPrompts(pkg) {
336
252
  const files = pkg.files || {};
337
-
338
- // Files that should NOT be overwritten (user may have customized)
339
253
  const preserveFiles = ['CLAUDE.md'];
340
254
 
255
+ let created = 0, updated = 0, skipped = 0;
256
+
341
257
  for (const [filePath, content] of Object.entries(files)) {
342
258
  const fullPath = filePath;
343
259
  const dir = dirname(fullPath);
344
260
 
345
- // Create directory if needed
346
261
  if (dir && dir !== '.') {
347
262
  mkdirSync(dir, { recursive: true });
348
263
  }
349
264
 
350
- // Check if file exists
351
265
  if (existsSync(fullPath)) {
352
- // For CLAUDE.md, only append @import line (preserve user content)
353
266
  if (fullPath === 'CLAUDE.md') {
354
267
  const existing = readFileSync(fullPath, 'utf-8');
355
268
  if (!existing.includes('@import ROPILOT.md')) {
356
269
  writeFileSync(fullPath, existing.trimEnd() + '\n\n@import ROPILOT.md\n');
357
- console.log(` Updated: ${fullPath} (added @import ROPILOT.md)`);
270
+ updated++;
358
271
  } else {
359
- console.log(` Exists: ${fullPath}`);
272
+ skipped++;
360
273
  }
361
274
  } else if (preserveFiles.includes(fullPath)) {
362
- // Don't overwrite preserved files
363
- console.log(` Exists: ${fullPath}`);
275
+ skipped++;
364
276
  } else {
365
- // Overwrite Ropilot-managed files with latest content
366
277
  writeFileSync(fullPath, content);
367
- console.log(` Updated: ${fullPath}`);
278
+ updated++;
368
279
  }
369
280
  } else {
370
281
  writeFileSync(fullPath, content);
371
- console.log(` Created: ${fullPath}`);
282
+ created++;
372
283
  }
373
284
  }
285
+
286
+ console.log(`Project files: ${created} created, ${updated} updated, ${skipped} unchanged`);
374
287
  }
375
288
 
376
289
  /**
377
290
  * Initialize Ropilot in current project
378
291
  */
379
292
  export async function init(providedApiKey = null) {
380
- console.log('');
381
- console.log('Welcome to Ropilot!');
382
- console.log('===================');
383
- console.log('');
293
+ console.log('\nRopilot Setup\n');
384
294
 
385
- // Always prompt for API key (per-project config)
386
295
  const existingKey = loadProjectConfig()?.apiKey;
387
296
  const defaultHint = existingKey ? ` [${existingKey.slice(0, 20)}...]` : '';
388
297
 
389
- console.log('To use Ropilot, you need an API key.');
390
- console.log('Get one at: https://ropilot.ai');
391
- console.log('');
298
+ if (!existingKey) {
299
+ console.log('Get your API key at: https://ropilot.ai\n');
300
+ }
392
301
 
393
302
  let apiKey = providedApiKey;
394
303
  if (!apiKey) {
395
- apiKey = await prompt(`Enter your API key${defaultHint}: `);
304
+ apiKey = await prompt(`API key${defaultHint}: `);
396
305
 
397
- // If empty and existing key, use existing
398
306
  if (!apiKey && existingKey) {
399
307
  apiKey = existingKey;
400
- console.log(`Using existing key: ${apiKey.slice(0, 20)}...`);
401
308
  }
402
309
  }
403
310
 
@@ -406,45 +313,26 @@ export async function init(providedApiKey = null) {
406
313
  process.exit(1);
407
314
  }
408
315
 
409
- // Save to project config (.ropilot.json)
410
316
  setProjectApiKey(apiKey);
411
- console.log('API key saved to .ropilot.json');
412
- console.log('');
413
-
317
+ console.log('API key saved to .ropilot.json\n');
414
318
 
415
- // Install Studio plugin
416
319
  await installPlugin();
417
- console.log('');
418
-
419
- // Download prompts
420
320
  const pkg = await downloadPrompts(apiKey);
421
- console.log('');
422
-
423
- // Set up project files (includes .mcp.json from package)
424
- console.log('Setting up project files...');
425
321
  setupProjectPrompts(pkg);
426
- console.log('');
427
322
 
428
- // Done!
429
- console.log('Setup complete!');
430
- console.log('');
323
+ console.log('\nSetup complete!\n');
431
324
  console.log('Next steps:');
432
- console.log('1. Open Roblox Studio and check the Plugins tab for "Ropilot AI"');
433
- console.log('2. Click Ropilot AI → Enter your API key Connect');
434
- console.log('3. Run Claude Code, Cursor, or any MCP-compatible AI tool in this directory');
435
- console.log('');
325
+ console.log('1. Open Studio Plugins tab Click "Ropilot AI"');
326
+ console.log('2. Enter your API key and click Connect');
327
+ console.log('3. Start Claude Code or Cursor in this directory\n');
436
328
  }
437
329
 
438
330
  /**
439
331
  * Update prompts to latest version
440
332
  */
441
333
  export async function update() {
442
- console.log('');
443
- console.log('Updating Ropilot...');
444
- console.log('===================');
445
- console.log('');
334
+ console.log('\nUpdating Ropilot...\n');
446
335
 
447
- // Check project config first, then global
448
336
  const projectConfig = loadProjectConfig();
449
337
  const apiKey = projectConfig?.apiKey || getApiKey();
450
338
 
@@ -453,25 +341,9 @@ export async function update() {
453
341
  process.exit(1);
454
342
  }
455
343
 
456
- const source = projectConfig?.apiKey ? '.ropilot.json' : '~/.ropilot/config.json';
457
- console.log(`Using API key from ${source}: ${apiKey.slice(0, 20)}...`);
458
- console.log('');
459
-
460
- // Re-install Studio plugin
461
344
  await installPlugin();
462
- console.log('');
463
-
464
- // Re-download prompts
465
345
  const pkg = await downloadPrompts(apiKey);
466
- console.log('');
467
-
468
- // Re-write project files
469
- console.log('Updating project files...');
470
346
  setupProjectPrompts(pkg);
471
- console.log('');
472
347
 
473
- console.log('Update complete!');
474
- console.log('');
475
- console.log('Note: Check the Studio Plugins tab to verify the updated plugin is loaded.');
476
- console.log('');
348
+ console.log('\nUpdate complete! Restart Studio to reload the plugin.\n');
477
349
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ropilot",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "AI-powered Roblox development assistant - MCP CLI",
5
5
  "author": "whut",
6
6
  "license": "MIT",