@muggleai/works 4.0.1 → 4.1.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.
package/README.md CHANGED
@@ -375,8 +375,8 @@ Data directory structure (~/.muggle-ai/)
375
375
 
376
376
  ```
377
377
  ~/.muggle-ai/
378
- ├── auth.json # OAuth tokens
379
- ├── credentials.json # API key for service calls
378
+ ├── oauth-session.json # OAuth tokens (short-lived, auto-refresh)
379
+ ├── api-key.json # Long-lived API key for service calls
380
380
  ├── projects/ # Local project cache
381
381
  ├── sessions/ # QA sessions
382
382
  │ └── {runId}/
@@ -424,7 +424,7 @@ muggle doctor # Diagnose
424
424
 
425
425
  ```bash
426
426
  muggle logout # Clear all credentials
427
- rm ~/.muggle-ai/auth.json ~/.muggle-ai/credentials.json
427
+ rm ~/.muggle-ai/oauth-session.json ~/.muggle-ai/api-key.json
428
428
  muggle login # Fresh login
429
429
  ```
430
430
 
@@ -1,5 +1,5 @@
1
1
  import * as fs3 from 'fs';
2
- import { readFileSync, existsSync, rmSync, mkdirSync, createWriteStream, readdirSync, statSync, writeFileSync } from 'fs';
2
+ import { readFileSync, existsSync, rmSync, mkdirSync, readdirSync, createWriteStream, writeFileSync, statSync } from 'fs';
3
3
  import * as os3 from 'os';
4
4
  import { platform, arch, homedir } from 'os';
5
5
  import * as path2 from 'path';
@@ -7,7 +7,7 @@ import { dirname, resolve, join } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import winston from 'winston';
9
9
  import axios, { AxiosError } from 'axios';
10
- import { spawn, exec } from 'child_process';
10
+ import { spawn, exec, execFile } from 'child_process';
11
11
  import * as fs5 from 'fs/promises';
12
12
  import { z, ZodError } from 'zod';
13
13
  import * as crypto from 'crypto';
@@ -39,12 +39,12 @@ var DEFAULT_PROMPT_SERVICE_PRODUCTION_URL = "https://promptservice.muggle-ai.com
39
39
  var DEFAULT_PROMPT_SERVICE_DEV_URL = "http://localhost:5050";
40
40
  var DEFAULT_WEB_SERVICE_URL = "http://localhost:3001";
41
41
  var ELECTRON_APP_DIR = "electron-app";
42
- var CREDENTIALS_FILE = "credentials.json";
42
+ var API_KEY_FILE = "api-key.json";
43
43
  var DEFAULT_AUTH0_PRODUCTION_DOMAIN = "login.muggle-ai.com";
44
44
  var DEFAULT_AUTH0_PRODUCTION_CLIENT_ID = "UgG5UjoyLksxMciWWKqVpwfWrJ4rFvtT";
45
45
  var DEFAULT_AUTH0_PRODUCTION_AUDIENCE = "https://muggleai.us.auth0.com/api/v2/";
46
46
  var DEFAULT_AUTH0_DEV_DOMAIN = "dev-po4mxmz0rd8a0w8w.us.auth0.com";
47
- var DEFAULT_AUTH0_DEV_CLIENT_ID = "hihMM2cxb40yHaZMH2MMXwO2ZRJQ3MxA";
47
+ var DEFAULT_AUTH0_DEV_CLIENT_ID = "GBvkMdTbCI80XJXnJ90MmbEvXwcWGUtw";
48
48
  var DEFAULT_AUTH0_DEV_AUDIENCE = "https://dev-po4mxmz0rd8a0w8w.us.auth0.com/api/v2/";
49
49
  var DEFAULT_AUTH0_SCOPE = "openid profile email offline_access";
50
50
  var configInstance = null;
@@ -130,8 +130,7 @@ function getDataDir2() {
130
130
  }
131
131
  function getDownloadedElectronAppPath() {
132
132
  const platformName = os3.platform();
133
- const config = getMuggleConfig();
134
- const version = config.electronAppVersion;
133
+ const version = getElectronAppVersion();
135
134
  const baseDir = path2.join(getDataDir2(), ELECTRON_APP_DIR, version);
136
135
  let binaryPath;
137
136
  switch (platformName) {
@@ -240,10 +239,10 @@ function getDefaultAuth0Domain() {
240
239
  }
241
240
  function getDefaultAuth0ClientId() {
242
241
  const runtimeTarget = getPromptServiceRuntimeTarget();
243
- if (runtimeTarget === "dev") {
244
- return DEFAULT_AUTH0_DEV_CLIENT_ID;
242
+ if (runtimeTarget === "production") {
243
+ return DEFAULT_AUTH0_PRODUCTION_CLIENT_ID;
245
244
  }
246
- return DEFAULT_AUTH0_PRODUCTION_CLIENT_ID;
245
+ return DEFAULT_AUTH0_DEV_CLIENT_ID;
247
246
  }
248
247
  function getDefaultAuth0Audience() {
249
248
  const runtimeTarget = getPromptServiceRuntimeTarget();
@@ -285,8 +284,8 @@ function buildLocalQaConfig() {
285
284
  sessionsDir: path2.join(dataDir, "sessions"),
286
285
  projectsDir: path2.join(dataDir, "projects"),
287
286
  tempDir: path2.join(dataDir, "temp"),
288
- credentialsFilePath: path2.join(dataDir, CREDENTIALS_FILE),
289
- authFilePath: path2.join(dataDir, "auth.json"),
287
+ apiKeyFilePath: path2.join(dataDir, API_KEY_FILE),
288
+ oauthSessionFilePath: path2.join(dataDir, "oauth-session.json"),
290
289
  electronAppPath: resolveElectronAppPathOrNull(),
291
290
  webServicePath: resolveWebServicePath(),
292
291
  webServicePidFile: path2.join(dataDir, "web-service.pid"),
@@ -611,8 +610,8 @@ var TestResultStatus = /* @__PURE__ */ ((TestResultStatus2) => {
611
610
  // packages/mcps/src/mcp/local/services/auth-service.ts
612
611
  var DEFAULT_LOGIN_WAIT_TIMEOUT_MS = 12e4;
613
612
  var AuthService = class {
614
- /** Path to the auth file. */
615
- authFilePath;
613
+ /** Path to the OAuth session file. */
614
+ oauthSessionFilePath;
616
615
  /** Path to the pending device code file. */
617
616
  pendingDeviceCodePath;
618
617
  /**
@@ -620,9 +619,9 @@ var AuthService = class {
620
619
  */
621
620
  constructor() {
622
621
  const config = getConfig();
623
- this.authFilePath = config.localQa.authFilePath;
622
+ this.oauthSessionFilePath = config.localQa.oauthSessionFilePath;
624
623
  this.pendingDeviceCodePath = path2.join(
625
- path2.dirname(config.localQa.authFilePath),
624
+ path2.dirname(config.localQa.oauthSessionFilePath),
626
625
  "pending-device-code.json"
627
626
  );
628
627
  }
@@ -932,11 +931,11 @@ var AuthService = class {
932
931
  email,
933
932
  userId
934
933
  };
935
- const dir = path2.dirname(this.authFilePath);
934
+ const dir = path2.dirname(this.oauthSessionFilePath);
936
935
  if (!fs3.existsSync(dir)) {
937
936
  fs3.mkdirSync(dir, { recursive: true });
938
937
  }
939
- fs3.writeFileSync(this.authFilePath, JSON.stringify(storedAuth, null, 2), {
938
+ fs3.writeFileSync(this.oauthSessionFilePath, JSON.stringify(storedAuth, null, 2), {
940
939
  encoding: "utf-8",
941
940
  mode: 384
942
941
  });
@@ -947,11 +946,11 @@ var AuthService = class {
947
946
  */
948
947
  loadStoredAuth() {
949
948
  const logger14 = getLogger();
950
- if (!fs3.existsSync(this.authFilePath)) {
949
+ if (!fs3.existsSync(this.oauthSessionFilePath)) {
951
950
  return null;
952
951
  }
953
952
  try {
954
- const content = fs3.readFileSync(this.authFilePath, "utf-8");
953
+ const content = fs3.readFileSync(this.oauthSessionFilePath, "utf-8");
955
954
  return JSON.parse(content);
956
955
  } catch (error) {
957
956
  logger14.error("Failed to load stored auth", {
@@ -1035,11 +1034,11 @@ var AuthService = class {
1035
1034
  email: storedAuth.email,
1036
1035
  userId: storedAuth.userId
1037
1036
  };
1038
- const dir = path2.dirname(this.authFilePath);
1037
+ const dir = path2.dirname(this.oauthSessionFilePath);
1039
1038
  if (!fs3.existsSync(dir)) {
1040
1039
  fs3.mkdirSync(dir, { recursive: true });
1041
1040
  }
1042
- fs3.writeFileSync(this.authFilePath, JSON.stringify(updatedAuth, null, 2), {
1041
+ fs3.writeFileSync(this.oauthSessionFilePath, JSON.stringify(updatedAuth, null, 2), {
1043
1042
  encoding: "utf-8",
1044
1043
  mode: 384
1045
1044
  });
@@ -1091,12 +1090,12 @@ var AuthService = class {
1091
1090
  */
1092
1091
  logout() {
1093
1092
  const logger14 = getLogger();
1094
- if (!fs3.existsSync(this.authFilePath)) {
1093
+ if (!fs3.existsSync(this.oauthSessionFilePath)) {
1095
1094
  logger14.debug("No auth to clear");
1096
1095
  return false;
1097
1096
  }
1098
1097
  try {
1099
- fs3.unlinkSync(this.authFilePath);
1098
+ fs3.unlinkSync(this.oauthSessionFilePath);
1100
1099
  logger14.info("Auth cleared successfully");
1101
1100
  return true;
1102
1101
  } catch (error) {
@@ -1865,9 +1864,24 @@ function getElectronAppPathOrThrow() {
1865
1864
  const config = getConfig();
1866
1865
  const electronAppPath = config.localQa.electronAppPath;
1867
1866
  if (!electronAppPath || electronAppPath.trim() === "") {
1868
- throw new Error(
1869
- "Electron app binary not found. Run 'muggle setup' or set ELECTRON_APP_PATH."
1870
- );
1867
+ const version = getElectronAppVersion();
1868
+ const versionDir = getElectronAppDir(version);
1869
+ const envPath = process.env.ELECTRON_APP_PATH;
1870
+ const errorLines = [
1871
+ "Electron app binary not found.",
1872
+ "",
1873
+ ` Expected version: ${version}`,
1874
+ ` Checked directory: ${versionDir}`
1875
+ ];
1876
+ if (envPath) {
1877
+ errorLines.push(` ELECTRON_APP_PATH: ${envPath} (not found or invalid)`);
1878
+ } else {
1879
+ errorLines.push(" ELECTRON_APP_PATH: (not set)");
1880
+ }
1881
+ errorLines.push("");
1882
+ errorLines.push("To fix this, run: muggle setup");
1883
+ errorLines.push("Or set ELECTRON_APP_PATH to the path of the MuggleAI executable.");
1884
+ throw new Error(errorLines.join("\n"));
1871
1885
  }
1872
1886
  return electronAppPath;
1873
1887
  }
@@ -2366,9 +2380,9 @@ function listActiveExecutions() {
2366
2380
  status: process2.status
2367
2381
  }));
2368
2382
  }
2369
- var CREDENTIALS_FILE2 = "credentials.json";
2370
- function getCredentialsFilePath() {
2371
- return path2.join(getDataDir(), CREDENTIALS_FILE2);
2383
+ var API_KEY_FILE2 = "api-key.json";
2384
+ function getApiKeyFilePath() {
2385
+ return path2.join(getDataDir(), API_KEY_FILE2);
2372
2386
  }
2373
2387
  function ensureDataDir() {
2374
2388
  const dataDir = getDataDir();
@@ -2376,95 +2390,85 @@ function ensureDataDir() {
2376
2390
  fs3.mkdirSync(dataDir, { recursive: true });
2377
2391
  }
2378
2392
  }
2379
- function loadCredentials() {
2393
+ function loadApiKeyData() {
2380
2394
  const logger14 = getLogger();
2381
- const credentialsPath = getCredentialsFilePath();
2395
+ const apiKeyPath = getApiKeyFilePath();
2382
2396
  try {
2383
- if (!fs3.existsSync(credentialsPath)) {
2384
- logger14.debug("No credentials file found", { path: credentialsPath });
2397
+ if (!fs3.existsSync(apiKeyPath)) {
2398
+ logger14.debug("No API key file found", { path: apiKeyPath });
2385
2399
  return null;
2386
2400
  }
2387
- const content = fs3.readFileSync(credentialsPath, "utf-8");
2388
- const credentials = JSON.parse(content);
2389
- if (!credentials.accessToken || !credentials.expiresAt) {
2390
- logger14.warn("Invalid credentials file - missing required fields");
2391
- return null;
2392
- }
2393
- return credentials;
2401
+ const content = fs3.readFileSync(apiKeyPath, "utf-8");
2402
+ const data = JSON.parse(content);
2403
+ return data;
2394
2404
  } catch (error) {
2395
- logger14.warn("Failed to load credentials", {
2405
+ logger14.warn("Failed to load API key data", {
2396
2406
  error: error instanceof Error ? error.message : String(error)
2397
2407
  });
2398
2408
  return null;
2399
2409
  }
2400
2410
  }
2401
- function saveCredentials(credentials) {
2411
+ function saveApiKeyData(data) {
2402
2412
  const logger14 = getLogger();
2403
- const credentialsPath = getCredentialsFilePath();
2413
+ const apiKeyPath = getApiKeyFilePath();
2404
2414
  try {
2405
2415
  ensureDataDir();
2406
- const content = JSON.stringify(credentials, null, 2);
2407
- fs3.writeFileSync(credentialsPath, content, { mode: 384 });
2408
- logger14.info("Credentials saved", { path: credentialsPath });
2416
+ const content = JSON.stringify(data, null, 2);
2417
+ fs3.writeFileSync(apiKeyPath, content, { mode: 384 });
2418
+ logger14.info("API key saved", { path: apiKeyPath });
2409
2419
  } catch (error) {
2410
- logger14.error("Failed to save credentials", {
2420
+ logger14.error("Failed to save API key", {
2411
2421
  error: error instanceof Error ? error.message : String(error)
2412
2422
  });
2413
2423
  throw error;
2414
2424
  }
2415
2425
  }
2416
- function deleteCredentials() {
2426
+ function deleteApiKeyData() {
2417
2427
  const logger14 = getLogger();
2418
- const credentialsPath = getCredentialsFilePath();
2428
+ const apiKeyPath = getApiKeyFilePath();
2419
2429
  try {
2420
- if (fs3.existsSync(credentialsPath)) {
2421
- fs3.unlinkSync(credentialsPath);
2422
- logger14.info("Credentials deleted", { path: credentialsPath });
2430
+ if (fs3.existsSync(apiKeyPath)) {
2431
+ fs3.unlinkSync(apiKeyPath);
2432
+ logger14.info("API key deleted", { path: apiKeyPath });
2423
2433
  }
2424
2434
  } catch (error) {
2425
- logger14.warn("Failed to delete credentials", {
2435
+ logger14.warn("Failed to delete API key", {
2426
2436
  error: error instanceof Error ? error.message : String(error)
2427
2437
  });
2428
2438
  }
2429
2439
  }
2430
- function isCredentialsExpired(credentials) {
2431
- const expiresAt = new Date(credentials.expiresAt);
2432
- const now = /* @__PURE__ */ new Date();
2433
- const bufferMs = 5 * 60 * 1e3;
2434
- return now.getTime() >= expiresAt.getTime() - bufferMs;
2435
- }
2436
- function getValidCredentials() {
2437
- const credentials = loadCredentials();
2438
- if (!credentials) {
2440
+ function getValidApiKeyData() {
2441
+ const data = loadApiKeyData();
2442
+ if (!data) {
2439
2443
  return null;
2440
2444
  }
2441
- if (credentials.apiKey) {
2442
- return credentials;
2445
+ if (data.apiKey) {
2446
+ return data;
2443
2447
  }
2444
2448
  return null;
2445
2449
  }
2446
2450
  function hasApiKey() {
2447
- const credentials = loadCredentials();
2448
- return !!credentials?.apiKey;
2451
+ const data = loadApiKeyData();
2452
+ return !!data?.apiKey;
2449
2453
  }
2450
2454
  function getApiKey() {
2451
- const credentials = loadCredentials();
2452
- return credentials?.apiKey ?? null;
2455
+ const data = loadApiKeyData();
2456
+ return data?.apiKey ?? null;
2453
2457
  }
2454
2458
  function saveApiKey(params) {
2455
2459
  const logger14 = getLogger();
2456
- const credentialsPath = getCredentialsFilePath();
2460
+ const apiKeyPath = getApiKeyFilePath();
2457
2461
  try {
2458
2462
  ensureDataDir();
2459
- const credentials = {
2463
+ const data = {
2460
2464
  accessToken: "",
2461
2465
  expiresAt: "",
2462
2466
  apiKey: params.apiKey,
2463
2467
  apiKeyId: params.apiKeyId
2464
2468
  };
2465
- const content = JSON.stringify(credentials, null, 2);
2466
- fs3.writeFileSync(credentialsPath, content, { mode: 384 });
2467
- logger14.info("API key saved", { path: credentialsPath });
2469
+ const content = JSON.stringify(data, null, 2);
2470
+ fs3.writeFileSync(apiKeyPath, content, { mode: 384 });
2471
+ logger14.info("API key saved", { path: apiKeyPath });
2468
2472
  } catch (error) {
2469
2473
  logger14.error("Failed to save API key", {
2470
2474
  error: error instanceof Error ? error.message : String(error)
@@ -2472,6 +2476,11 @@ function saveApiKey(params) {
2472
2476
  throw error;
2473
2477
  }
2474
2478
  }
2479
+ var loadCredentials = loadApiKeyData;
2480
+ var saveCredentials = saveApiKeyData;
2481
+ var deleteCredentials = deleteApiKeyData;
2482
+ var getValidCredentials = getValidApiKeyData;
2483
+ var getCredentialsFilePath = getApiKeyFilePath;
2475
2484
 
2476
2485
  // packages/mcps/src/shared/auth.ts
2477
2486
  var logger4 = getLogger();
@@ -2680,7 +2689,7 @@ async function performLogin(keyName, keyExpiry = "90d", timeoutMs = 12e4) {
2680
2689
  );
2681
2690
  credentials.apiKey = apiKeyResult.key;
2682
2691
  credentials.apiKeyId = apiKeyResult.id;
2683
- saveCredentials(credentials);
2692
+ saveApiKeyData(credentials);
2684
2693
  }
2685
2694
  return {
2686
2695
  success: true,
@@ -2717,13 +2726,13 @@ async function performLogin(keyName, keyExpiry = "90d", timeoutMs = 12e4) {
2717
2726
  function performLogout() {
2718
2727
  const authService = getAuthService();
2719
2728
  authService.logout();
2720
- deleteCredentials();
2729
+ deleteApiKeyData();
2721
2730
  logger4.info("[Auth] Logged out successfully");
2722
2731
  }
2723
2732
  function getCallerCredentials() {
2724
- const credentials = getValidCredentials();
2725
- if (credentials?.apiKey) {
2726
- return { apiKey: credentials.apiKey };
2733
+ const apiKeyData = getValidApiKeyData();
2734
+ if (apiKeyData?.apiKey) {
2735
+ return { apiKey: apiKeyData.apiKey };
2727
2736
  }
2728
2737
  const authService = getAuthService();
2729
2738
  const accessToken = authService.getAccessToken();
@@ -2733,9 +2742,9 @@ function getCallerCredentials() {
2733
2742
  return {};
2734
2743
  }
2735
2744
  async function getCallerCredentialsAsync() {
2736
- const credentials = getValidCredentials();
2737
- if (credentials?.apiKey) {
2738
- return { apiKey: credentials.apiKey };
2745
+ const apiKeyData = getValidApiKeyData();
2746
+ if (apiKeyData?.apiKey) {
2747
+ return { apiKey: apiKeyData.apiKey };
2739
2748
  }
2740
2749
  const authService = getAuthService();
2741
2750
  const accessToken = await authService.getValidAccessToken();
@@ -3159,17 +3168,17 @@ var PromptServiceClient = class {
3159
3168
  * @param path - Path to validate.
3160
3169
  * @throws GatewayError if path is not allowed.
3161
3170
  */
3162
- validatePath(path13) {
3163
- const isAllowed = ALLOWED_UPSTREAM_PREFIXES.some((prefix) => path13.startsWith(prefix));
3171
+ validatePath(path15) {
3172
+ const isAllowed = ALLOWED_UPSTREAM_PREFIXES.some((prefix) => path15.startsWith(prefix));
3164
3173
  if (!isAllowed) {
3165
3174
  const logger14 = getLogger();
3166
3175
  logger14.error("Path not in allowlist", {
3167
- path: path13,
3176
+ path: path15,
3168
3177
  allowedPrefixes: ALLOWED_UPSTREAM_PREFIXES
3169
3178
  });
3170
3179
  throw new GatewayError({
3171
3180
  code: "FORBIDDEN" /* FORBIDDEN */,
3172
- message: `Path '${path13}' is not allowed`
3181
+ message: `Path '${path15}' is not allowed`
3173
3182
  });
3174
3183
  }
3175
3184
  }
@@ -5452,8 +5461,10 @@ __export(src_exports, {
5452
5461
  calculateFileChecksum: () => calculateFileChecksum,
5453
5462
  createApiKeyWithToken: () => createApiKeyWithToken,
5454
5463
  createChildLogger: () => createChildLogger,
5464
+ deleteApiKeyData: () => deleteApiKeyData,
5455
5465
  deleteCredentials: () => deleteCredentials,
5456
5466
  getApiKey: () => getApiKey,
5467
+ getApiKeyFilePath: () => getApiKeyFilePath,
5457
5468
  getAuthService: () => getAuthService,
5458
5469
  getBundledElectronAppVersion: () => getBundledElectronAppVersion,
5459
5470
  getCallerCredentials: () => getCallerCredentials,
@@ -5471,10 +5482,11 @@ __export(src_exports, {
5471
5482
  getLogger: () => getLogger,
5472
5483
  getPlatformKey: () => getPlatformKey,
5473
5484
  getQaTools: () => getQaTools,
5485
+ getValidApiKeyData: () => getValidApiKeyData,
5474
5486
  getValidCredentials: () => getValidCredentials,
5475
5487
  hasApiKey: () => hasApiKey,
5476
- isCredentialsExpired: () => isCredentialsExpired,
5477
5488
  isElectronAppInstalled: () => isElectronAppInstalled,
5489
+ loadApiKeyData: () => loadApiKeyData,
5478
5490
  loadCredentials: () => loadCredentials,
5479
5491
  localQa: () => local_exports2,
5480
5492
  mcp: () => mcp_exports,
@@ -5486,6 +5498,7 @@ __export(src_exports, {
5486
5498
  resetConfig: () => resetConfig,
5487
5499
  resetLogger: () => resetLogger,
5488
5500
  saveApiKey: () => saveApiKey,
5501
+ saveApiKeyData: () => saveApiKeyData,
5489
5502
  saveCredentials: () => saveCredentials,
5490
5503
  startDeviceCodeFlow: () => startDeviceCodeFlow,
5491
5504
  toolRequiresAuth: () => toolRequiresAuth,
@@ -5796,8 +5809,8 @@ function createUnifiedMcpServer(options) {
5796
5809
  errors: error.issues
5797
5810
  });
5798
5811
  const issueMessages = error.issues.slice(0, 3).map((issue) => {
5799
- const path13 = issue.path.join(".");
5800
- return path13 ? `'${path13}': ${issue.message}` : issue.message;
5812
+ const path15 = issue.path.join(".");
5813
+ return path15 ? `'${path15}': ${issue.message}` : issue.message;
5801
5814
  });
5802
5815
  return {
5803
5816
  content: [
@@ -6065,6 +6078,72 @@ var logger8 = getLogger();
6065
6078
  function getCursorMcpConfigPath() {
6066
6079
  return join(homedir(), ".cursor", "mcp.json");
6067
6080
  }
6081
+ function getExpectedExecutablePath(versionDir) {
6082
+ const os4 = platform();
6083
+ switch (os4) {
6084
+ case "darwin":
6085
+ return path2.join(versionDir, "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
6086
+ case "win32":
6087
+ return path2.join(versionDir, "MuggleAI.exe");
6088
+ case "linux":
6089
+ return path2.join(versionDir, "MuggleAI");
6090
+ default:
6091
+ throw new Error(`Unsupported platform: ${os4}`);
6092
+ }
6093
+ }
6094
+ function verifyElectronAppInstallation() {
6095
+ const version = getElectronAppVersion();
6096
+ const versionDir = getElectronAppDir(version);
6097
+ const executablePath = getExpectedExecutablePath(versionDir);
6098
+ const metadataPath = path2.join(versionDir, ".install-metadata.json");
6099
+ const result = {
6100
+ valid: false,
6101
+ versionDir,
6102
+ executablePath,
6103
+ executableExists: false,
6104
+ executableIsFile: false,
6105
+ metadataExists: false,
6106
+ hasPartialArchive: false
6107
+ };
6108
+ if (!fs3.existsSync(versionDir)) {
6109
+ result.errorDetail = "Version directory does not exist";
6110
+ return result;
6111
+ }
6112
+ const archivePatterns = ["MuggleAI-darwin", "MuggleAI-win32", "MuggleAI-linux"];
6113
+ try {
6114
+ const files = fs3.readdirSync(versionDir);
6115
+ for (const file of files) {
6116
+ if (archivePatterns.some((pattern) => file.startsWith(pattern)) && (file.endsWith(".zip") || file.endsWith(".tar.gz"))) {
6117
+ result.hasPartialArchive = true;
6118
+ break;
6119
+ }
6120
+ }
6121
+ } catch {
6122
+ }
6123
+ result.executableExists = fs3.existsSync(executablePath);
6124
+ if (!result.executableExists) {
6125
+ if (result.hasPartialArchive) {
6126
+ result.errorDetail = "Download incomplete: archive found but not extracted";
6127
+ } else {
6128
+ result.errorDetail = "Executable not found at expected path";
6129
+ }
6130
+ return result;
6131
+ }
6132
+ try {
6133
+ const stats = fs3.statSync(executablePath);
6134
+ result.executableIsFile = stats.isFile();
6135
+ if (!result.executableIsFile) {
6136
+ result.errorDetail = "Executable path exists but is not a file";
6137
+ return result;
6138
+ }
6139
+ } catch {
6140
+ result.errorDetail = "Cannot stat executable (broken symlink?)";
6141
+ return result;
6142
+ }
6143
+ result.metadataExists = fs3.existsSync(metadataPath);
6144
+ result.valid = true;
6145
+ return result;
6146
+ }
6068
6147
  function validateCursorMcpConfig() {
6069
6148
  const cursorMcpConfigPath = getCursorMcpConfigPath();
6070
6149
  if (!existsSync(cursorMcpConfigPath)) {
@@ -6140,12 +6219,13 @@ function runDiagnostics() {
6140
6219
  description: existsSync(dataDir) ? `Found at ${dataDir}` : `Not found at ${dataDir}`,
6141
6220
  suggestion: "Run 'muggle login' to create the data directory"
6142
6221
  });
6143
- const electronInstalled = isElectronAppInstalled();
6144
6222
  const electronVersion = getElectronAppVersion();
6145
6223
  const bundledVersion = getBundledElectronAppVersion();
6146
6224
  const versionSource = getElectronAppVersionSource();
6225
+ const installVerification = verifyElectronAppInstallation();
6147
6226
  let electronDescription;
6148
- if (electronInstalled) {
6227
+ let electronSuggestion;
6228
+ if (installVerification.valid) {
6149
6229
  electronDescription = `Installed (v${electronVersion})`;
6150
6230
  switch (versionSource) {
6151
6231
  case "env":
@@ -6155,16 +6235,30 @@ function runDiagnostics() {
6155
6235
  electronDescription += ` [overridden from bundled v${bundledVersion}]`;
6156
6236
  break;
6157
6237
  }
6238
+ if (!installVerification.metadataExists) {
6239
+ electronDescription += " [missing metadata]";
6240
+ }
6158
6241
  } else {
6159
6242
  electronDescription = `Not installed (expected v${electronVersion})`;
6243
+ if (installVerification.errorDetail) {
6244
+ electronDescription += `
6245
+ \u2514\u2500 ${installVerification.errorDetail}`;
6246
+ electronDescription += `
6247
+ \u2514\u2500 Checked: ${installVerification.versionDir}`;
6248
+ }
6249
+ if (installVerification.hasPartialArchive) {
6250
+ electronSuggestion = "Run 'muggle setup --force' to re-download and extract";
6251
+ } else {
6252
+ electronSuggestion = "Run 'muggle setup' to download the Electron app";
6253
+ }
6160
6254
  }
6161
6255
  results.push({
6162
6256
  name: "Electron App",
6163
- passed: electronInstalled,
6257
+ passed: installVerification.valid,
6164
6258
  description: electronDescription,
6165
- suggestion: "Run 'muggle setup' to download the Electron app"
6259
+ suggestion: electronSuggestion
6166
6260
  });
6167
- if (electronInstalled) {
6261
+ if (installVerification.valid) {
6168
6262
  results.push({
6169
6263
  name: "Electron App Updates",
6170
6264
  passed: true,
@@ -6266,8 +6360,8 @@ ${title}`, COLORS.bold + COLORS.cyan);
6266
6360
  function cmd(cmd2) {
6267
6361
  return colorize(cmd2, COLORS.green);
6268
6362
  }
6269
- function path11(path13) {
6270
- return colorize(path13, COLORS.yellow);
6363
+ function path12(path15) {
6364
+ return colorize(path15, COLORS.yellow);
6271
6365
  }
6272
6366
  function getHelpGuidance() {
6273
6367
  const lines = [
@@ -6282,14 +6376,14 @@ function getHelpGuidance() {
6282
6376
  " assistants with tools to perform automated QA testing of web applications.",
6283
6377
  "",
6284
6378
  " It supports both:",
6285
- ` ${colorize("\u2022", COLORS.green)} Cloud QA - Test remote production/staging sites`,
6379
+ ` ${colorize("\u2022", COLORS.green)} Cloud QA - Test remote production/staging sites with a public URL`,
6286
6380
  ` ${colorize("\u2022", COLORS.green)} Local QA - Test localhost development servers`,
6287
6381
  "",
6288
6382
  header("Setup Instructions"),
6289
6383
  "",
6290
6384
  ` ${colorize("Step 1:", COLORS.bold)} Configure your MCP client`,
6291
6385
  "",
6292
- ` For ${colorize("Cursor", COLORS.bold)}, edit ${path11("~/.cursor/mcp.json")}:`,
6386
+ ` For ${colorize("Cursor", COLORS.bold)}, edit ${path12("~/.cursor/mcp.json")}:`,
6293
6387
  "",
6294
6388
  ` ${colorize("{", COLORS.dim)}`,
6295
6389
  ` ${colorize('"mcpServers"', COLORS.yellow)}: {`,
@@ -6308,7 +6402,6 @@ function getHelpGuidance() {
6308
6402
  header("CLI Commands"),
6309
6403
  "",
6310
6404
  ` ${colorize("Server Commands:", COLORS.bold)}`,
6311
- ` ${cmd("muggle")} Start MCP server (default)`,
6312
6405
  ` ${cmd("muggle serve")} Start MCP server with all tools`,
6313
6406
  ` ${cmd("muggle serve --qa")} Start with Cloud QA tools only`,
6314
6407
  ` ${cmd("muggle serve --local")} Start with Local QA tools only`,
@@ -6344,7 +6437,7 @@ function getHelpGuidance() {
6344
6437
  ` 2. ${colorize("Log in", COLORS.bold)} with your Muggle AI account`,
6345
6438
  ` 3. ${colorize("The tool call continues", COLORS.bold)} with your credentials`,
6346
6439
  "",
6347
- ` Credentials are stored in ${path11("~/.muggle-ai/credentials.json")}`,
6440
+ ` API keys are stored in ${path12("~/.muggle-ai/api-key.json")}`,
6348
6441
  "",
6349
6442
  header("Available MCP Tools"),
6350
6443
  "",
@@ -6359,12 +6452,12 @@ function getHelpGuidance() {
6359
6452
  "",
6360
6453
  header("Data Directory"),
6361
6454
  "",
6362
- ` All data is stored in ${path11("~/.muggle-ai/")}:`,
6455
+ ` All data is stored in ${path12("~/.muggle-ai/")}:`,
6363
6456
  "",
6364
- ` ${path11("credentials.json")} Auth credentials (auto-generated)`,
6365
- ` ${path11("projects/")} Local test projects`,
6366
- ` ${path11("sessions/")} Test execution sessions`,
6367
- ` ${path11("electron-app/")} Downloaded Electron app binaries`,
6457
+ ` ${path12("api-key.json")} Long-lived API key (auto-generated)`,
6458
+ ` ${path12("projects/")} Local test projects`,
6459
+ ` ${path12("sessions/")} Test execution sessions`,
6460
+ ` ${path12("electron-app/")} Downloaded Electron app binaries`,
6368
6461
  "",
6369
6462
  header("Troubleshooting"),
6370
6463
  "",
@@ -6499,6 +6592,9 @@ async function serveCommand(options) {
6499
6592
  }
6500
6593
  }
6501
6594
  var logger11 = getLogger();
6595
+ var MAX_RETRY_ATTEMPTS = 3;
6596
+ var RETRY_BASE_DELAY_MS = 2e3;
6597
+ var INSTALL_METADATA_FILE_NAME = ".install-metadata.json";
6502
6598
  function getBinaryName() {
6503
6599
  const os4 = platform();
6504
6600
  const architecture = arch();
@@ -6515,21 +6611,47 @@ function getBinaryName() {
6515
6611
  throw new Error(`Unsupported platform: ${os4}`);
6516
6612
  }
6517
6613
  }
6614
+ function getExpectedExecutablePath2(versionDir) {
6615
+ const os4 = platform();
6616
+ switch (os4) {
6617
+ case "darwin":
6618
+ return path2.join(versionDir, "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
6619
+ case "win32":
6620
+ return path2.join(versionDir, "MuggleAI.exe");
6621
+ case "linux":
6622
+ return path2.join(versionDir, "MuggleAI");
6623
+ default:
6624
+ throw new Error(`Unsupported platform: ${os4}`);
6625
+ }
6626
+ }
6518
6627
  async function extractZip(zipPath, destDir) {
6519
6628
  return new Promise((resolve4, reject) => {
6520
- const cmd2 = platform() === "win32" ? `powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"` : `unzip -o "${zipPath}" -d "${destDir}"`;
6521
- exec(cmd2, (error) => {
6522
- if (error) {
6523
- reject(error);
6524
- } else {
6525
- resolve4();
6526
- }
6527
- });
6629
+ if (platform() === "win32") {
6630
+ execFile(
6631
+ "powershell",
6632
+ ["-command", `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`],
6633
+ (error) => {
6634
+ if (error) {
6635
+ reject(error);
6636
+ } else {
6637
+ resolve4();
6638
+ }
6639
+ }
6640
+ );
6641
+ } else {
6642
+ execFile("unzip", ["-o", zipPath, "-d", destDir], (error) => {
6643
+ if (error) {
6644
+ reject(error);
6645
+ } else {
6646
+ resolve4();
6647
+ }
6648
+ });
6649
+ }
6528
6650
  });
6529
6651
  }
6530
6652
  async function extractTarGz(tarPath, destDir) {
6531
6653
  return new Promise((resolve4, reject) => {
6532
- exec(`tar -xzf "${tarPath}" -C "${destDir}"`, (error) => {
6654
+ execFile("tar", ["-xzf", tarPath, "-C", destDir], (error) => {
6533
6655
  if (error) {
6534
6656
  reject(error);
6535
6657
  } else {
@@ -6538,10 +6660,66 @@ async function extractTarGz(tarPath, destDir) {
6538
6660
  });
6539
6661
  });
6540
6662
  }
6663
+ function sleep2(ms) {
6664
+ return new Promise((resolve4) => setTimeout(resolve4, ms));
6665
+ }
6666
+ async function downloadWithRetry(downloadUrl, destPath) {
6667
+ let lastError = null;
6668
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
6669
+ try {
6670
+ if (attempt > 1) {
6671
+ const delayMs = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 2);
6672
+ console.log(`Retry attempt ${attempt}/${MAX_RETRY_ATTEMPTS} after ${delayMs}ms delay...`);
6673
+ await sleep2(delayMs);
6674
+ }
6675
+ const response = await fetch(downloadUrl);
6676
+ if (!response.ok) {
6677
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
6678
+ }
6679
+ if (!response.body) {
6680
+ throw new Error("No response body received");
6681
+ }
6682
+ const fileStream = createWriteStream(destPath);
6683
+ await pipeline(response.body, fileStream);
6684
+ return true;
6685
+ } catch (error) {
6686
+ lastError = error instanceof Error ? error : new Error(String(error));
6687
+ console.error(`Download attempt ${attempt} failed: ${lastError.message}`);
6688
+ if (existsSync(destPath)) {
6689
+ rmSync(destPath, { force: true });
6690
+ }
6691
+ }
6692
+ }
6693
+ if (lastError) {
6694
+ throw new Error(`Download failed after ${MAX_RETRY_ATTEMPTS} attempts: ${lastError.message}`);
6695
+ }
6696
+ return false;
6697
+ }
6698
+ function writeInstallMetadata(params) {
6699
+ const metadata = {
6700
+ version: params.version,
6701
+ binaryName: params.binaryName,
6702
+ platformKey: params.platformKey,
6703
+ executableChecksum: params.executableChecksum,
6704
+ expectedArchiveChecksum: params.expectedArchiveChecksum,
6705
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
6706
+ };
6707
+ writeFileSync(params.metadataPath, `${JSON.stringify(metadata, null, 2)}
6708
+ `, "utf-8");
6709
+ }
6710
+ function cleanupFailedInstall(versionDir) {
6711
+ if (existsSync(versionDir)) {
6712
+ try {
6713
+ rmSync(versionDir, { recursive: true, force: true });
6714
+ } catch {
6715
+ }
6716
+ }
6717
+ }
6541
6718
  async function setupCommand(options) {
6542
6719
  const version = getElectronAppVersion();
6543
6720
  const baseUrl = getDownloadBaseUrl();
6544
6721
  const versionDir = getElectronAppDir(version);
6722
+ const platformKey = getPlatformKey();
6545
6723
  if (!options.force && isElectronAppInstalled()) {
6546
6724
  console.log(`Electron app v${version} is already installed at ${versionDir}`);
6547
6725
  console.log("Use --force to re-download.");
@@ -6556,22 +6734,14 @@ async function setupCommand(options) {
6556
6734
  rmSync(versionDir, { recursive: true, force: true });
6557
6735
  }
6558
6736
  mkdirSync(versionDir, { recursive: true });
6559
- const response = await fetch(downloadUrl);
6560
- if (!response.ok) {
6561
- throw new Error(`Download failed: ${response.status} ${response.statusText}`);
6562
- }
6563
- const tempFile = `${versionDir}/${binaryName}`;
6564
- const fileStream = createWriteStream(tempFile);
6565
- if (!response.body) {
6566
- throw new Error("No response body");
6567
- }
6568
- await pipeline(response.body, fileStream);
6737
+ const tempFile = path2.join(versionDir, binaryName);
6738
+ await downloadWithRetry(downloadUrl, tempFile);
6569
6739
  console.log("Download complete, verifying checksum...");
6570
6740
  const checksums = getElectronAppChecksums();
6571
6741
  const expectedChecksum = getChecksumForPlatform(checksums);
6572
6742
  const checksumResult = await verifyFileChecksum(tempFile, expectedChecksum);
6573
6743
  if (!checksumResult.valid && expectedChecksum) {
6574
- rmSync(versionDir, { recursive: true, force: true });
6744
+ cleanupFailedInstall(versionDir);
6575
6745
  throw new Error(
6576
6746
  `Checksum verification failed!
6577
6747
  Expected: ${checksumResult.expected}
@@ -6590,6 +6760,25 @@ The downloaded file may be corrupted or tampered with.`
6590
6760
  } else if (binaryName.endsWith(".tar.gz")) {
6591
6761
  await extractTarGz(tempFile, versionDir);
6592
6762
  }
6763
+ const executablePath = getExpectedExecutablePath2(versionDir);
6764
+ if (!existsSync(executablePath)) {
6765
+ cleanupFailedInstall(versionDir);
6766
+ throw new Error(
6767
+ `Extraction failed: executable not found at expected path.
6768
+ Expected: ${executablePath}
6769
+ The archive may be corrupted or in an unexpected format.`
6770
+ );
6771
+ }
6772
+ const executableChecksum = await calculateFileChecksum(executablePath);
6773
+ const metadataPath = path2.join(versionDir, INSTALL_METADATA_FILE_NAME);
6774
+ writeInstallMetadata({
6775
+ metadataPath,
6776
+ version,
6777
+ binaryName,
6778
+ platformKey,
6779
+ executableChecksum,
6780
+ expectedArchiveChecksum: expectedChecksum
6781
+ });
6593
6782
  rmSync(tempFile, { force: true });
6594
6783
  console.log(`Electron app installed to ${versionDir}`);
6595
6784
  logger11.info("Setup complete", { version, path: versionDir });
@@ -6602,6 +6791,7 @@ The downloaded file may be corrupted or tampered with.`
6602
6791
  }
6603
6792
  var logger12 = getLogger();
6604
6793
  var GITHUB_RELEASES_API = "https://api.github.com/repos/multiplex-ai/muggle-ai-works/releases";
6794
+ var INSTALL_METADATA_FILE_NAME2 = ".install-metadata.json";
6605
6795
  var VERSION_OVERRIDE_FILE2 = "electron-app-version-override.json";
6606
6796
  function getBinaryName2() {
6607
6797
  const os4 = platform();
@@ -6704,21 +6894,59 @@ function compareVersions2(a, b) {
6704
6894
  }
6705
6895
  return 0;
6706
6896
  }
6897
+ function getExpectedExecutablePath3(versionDir) {
6898
+ const os4 = platform();
6899
+ switch (os4) {
6900
+ case "darwin":
6901
+ return path2.join(versionDir, "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
6902
+ case "win32":
6903
+ return path2.join(versionDir, "MuggleAI.exe");
6904
+ case "linux":
6905
+ return path2.join(versionDir, "MuggleAI");
6906
+ default:
6907
+ throw new Error(`Unsupported platform: ${os4}`);
6908
+ }
6909
+ }
6910
+ function writeInstallMetadata2(params) {
6911
+ const metadata = {
6912
+ version: params.version,
6913
+ binaryName: params.binaryName,
6914
+ platformKey: params.platformKey,
6915
+ executableChecksum: params.executableChecksum,
6916
+ expectedArchiveChecksum: params.expectedArchiveChecksum,
6917
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
6918
+ };
6919
+ writeFileSync(params.metadataPath, `${JSON.stringify(metadata, null, 2)}
6920
+ `, "utf-8");
6921
+ }
6707
6922
  async function extractZip2(zipPath, destDir) {
6708
6923
  return new Promise((resolve4, reject) => {
6709
- const cmd2 = platform() === "win32" ? `powershell -command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"` : `unzip -o "${zipPath}" -d "${destDir}"`;
6710
- exec(cmd2, (error) => {
6711
- if (error) {
6712
- reject(error);
6713
- } else {
6714
- resolve4();
6715
- }
6716
- });
6924
+ if (platform() === "win32") {
6925
+ execFile(
6926
+ "powershell",
6927
+ ["-command", `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`],
6928
+ (error) => {
6929
+ if (error) {
6930
+ reject(error);
6931
+ } else {
6932
+ resolve4();
6933
+ }
6934
+ }
6935
+ );
6936
+ } else {
6937
+ execFile("unzip", ["-o", zipPath, "-d", destDir], (error) => {
6938
+ if (error) {
6939
+ reject(error);
6940
+ } else {
6941
+ resolve4();
6942
+ }
6943
+ });
6944
+ }
6717
6945
  });
6718
6946
  }
6719
6947
  async function extractTarGz2(tarPath, destDir) {
6720
6948
  return new Promise((resolve4, reject) => {
6721
- exec(`tar -xzf "${tarPath}" -C "${destDir}"`, (error) => {
6949
+ execFile("tar", ["-xzf", tarPath, "-C", destDir], (error) => {
6722
6950
  if (error) {
6723
6951
  reject(error);
6724
6952
  } else {
@@ -6776,6 +7004,7 @@ async function fetchChecksumFromRelease(version) {
6776
7004
  async function downloadAndInstall(version, downloadUrl, checksum) {
6777
7005
  const versionDir = getElectronAppDir(version);
6778
7006
  const binaryName = getBinaryName2();
7007
+ const platformKey = getPlatformKey();
6779
7008
  console.log(`Downloading Muggle Test Electron app v${version}...`);
6780
7009
  console.log(`URL: ${downloadUrl}`);
6781
7010
  if (existsSync(versionDir)) {
@@ -6818,6 +7047,25 @@ The downloaded file may be corrupted or tampered with.`
6818
7047
  } else if (binaryName.endsWith(".tar.gz")) {
6819
7048
  await extractTarGz2(tempFile, versionDir);
6820
7049
  }
7050
+ const executablePath = getExpectedExecutablePath3(versionDir);
7051
+ if (!existsSync(executablePath)) {
7052
+ rmSync(versionDir, { recursive: true, force: true });
7053
+ throw new Error(
7054
+ `Extraction failed: executable not found at expected path.
7055
+ Expected: ${executablePath}
7056
+ The archive may be corrupted or in an unexpected format.`
7057
+ );
7058
+ }
7059
+ const executableChecksum = await calculateFileChecksum(executablePath);
7060
+ const metadataPath = path2.join(versionDir, INSTALL_METADATA_FILE_NAME2);
7061
+ writeInstallMetadata2({
7062
+ metadataPath,
7063
+ version,
7064
+ binaryName,
7065
+ platformKey,
7066
+ executableChecksum,
7067
+ expectedArchiveChecksum: expectedChecksum || ""
7068
+ });
6821
7069
  rmSync(tempFile, { force: true });
6822
7070
  saveVersionOverride(version);
6823
7071
  console.log(`Electron app v${version} installed to ${versionDir}`);
@@ -6900,7 +7148,11 @@ function createProgram() {
6900
7148
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
6901
7149
  program.command("status").description("Show authentication status").action(statusCommand);
6902
7150
  program.action(() => {
6903
- serveCommand({ });
7151
+ helpCommand();
7152
+ });
7153
+ program.on("command:*", () => {
7154
+ helpCommand();
7155
+ process.exit(1);
6904
7156
  });
6905
7157
  return program;
6906
7158
  }
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-AJKZXT7B.js';
2
+ import { runCli } from './chunk-PV76IWEX.js';
3
3
 
4
4
  // src/cli/main.ts
5
5
  runCli().catch((error) => {
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { src_exports2 as commands, createChildLogger, createUnifiedMcpServer, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, qa_exports as qa, server_exports as server, src_exports as shared } from './chunk-AJKZXT7B.js';
1
+ export { src_exports2 as commands, createChildLogger, createUnifiedMcpServer, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, qa_exports as qa, server_exports as server, src_exports as shared } from './chunk-PV76IWEX.js';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muggle",
3
3
  "description": "Run real-browser QA tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
4
- "version": "4.0.1",
4
+ "version": "4.1.0",
5
5
  "author": {
6
6
  "name": "Muggle AI",
7
7
  "email": "support@muggle-ai.com"
@@ -2,7 +2,7 @@
2
2
  "name": "muggle",
3
3
  "displayName": "Muggle AI",
4
4
  "description": "Ship quality products with AI-powered QA that validates your app's user experience — from Claude Code and Cursor to PR.",
5
- "version": "4.0.1",
5
+ "version": "4.1.0",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: muggle-test-feature-local
3
- description: Test a feature's user experience on localhost. Use when user types muggle test-feature-local, test my app, run QA, or validate UI changes.
3
+ description: Run a real-browser QA test against localhost to verify a feature works correctly — signup flows, checkout, form validation, UI interactions, or any user-facing behavior. Launches a browser that executes test steps and captures screenshots. Use this skill whenever the user asks to test, QA, validate, or verify their web app, UI changes, user flows, or frontend behavior on localhost or a dev server — even if they don't mention 'muggle' or 'QA' explicitly.
4
4
  ---
5
5
 
6
6
  # Muggle Test Feature Local
@@ -12,54 +12,111 @@ Run end-to-end feature testing from UI against a local URL:
12
12
 
13
13
  ## Workflow
14
14
 
15
- 1. **Auth**
16
- - `muggle-remote-auth-status`
17
- - If needed: `muggle-remote-auth-login` + `muggle-remote-auth-poll`
18
-
19
- 2. **Select or create the project -> use case -> test case to use**
20
- - Explictly ask user to select each target to proceed.
21
- - `muggle-remote-project-list`
22
- - `muggle-remote-use-case-list`
23
- - `muggle-remote-test-case-list-by-use-case`
24
-
25
- 3. **Resolve local URL**
26
- - Use the URL provided by the user.
27
- - If missing, ask explicitly (do not guess).
28
- - Inform user the local URL does not affect the project's remote test.
29
-
30
- 4. **Check script availability**
31
- - `muggle-remote-test-script-list` filtered by testCaseId
32
- - If script exists, recommend replay unless user-flow changes suggest regeneration.
33
-
34
- 5. **Execute**
35
- - Replay path:
36
- - `muggle-remote-test-script-get`
37
- - `muggle-local-execute-replay`
38
- - Generation path:
39
- - `muggle-remote-test-case-get`
40
- - `muggle-local-execute-test-generation`
41
-
42
- 6. **Approval requirement**
43
- - Before execution, get explicit user approval for launching Electron app.
44
- - Only then set `approveElectronAppLaunch: true`.
45
-
46
- 7. **Publish local generation result to MuggleTest cloud records**
47
- - Use `muggle-local-publish-test-script` after successful generation so user can see the full job details.
48
- - Return the remote URL for user to view the result and script details.
49
-
50
- 8. **Report results**
51
- - `muggle-local-run-result-get` with returned runId.
52
- - Report:
53
- - status
54
- - duration
55
- - pass/fail summary
56
- - steps summary
57
- - artifacts path
58
- - script detail view URL
15
+ ### 1. Auth
16
+
17
+ - `muggle-remote-auth-status`
18
+ - If needed: `muggle-remote-auth-login` + `muggle-remote-auth-poll`
19
+
20
+ ### 2. Select project, use case, and test case
21
+
22
+ - Explicitly ask user to select each target to proceed.
23
+ - `muggle-remote-project-list`
24
+ - `muggle-remote-use-case-list`
25
+ - `muggle-remote-test-case-list-by-use-case`
26
+
27
+ ### 3. Resolve local URL
28
+
29
+ - Use the URL provided by the user.
30
+ - If missing, ask explicitly (do not guess).
31
+ - Inform user the local URL does not affect the project's remote test.
32
+
33
+ ### 4. Check for existing scripts and ask user to choose
34
+
35
+ Check BOTH cloud and local scripts to determine what's available:
36
+
37
+ 1. **Check cloud scripts:** `muggle-remote-test-script-list` filtered by projectId
38
+ 2. **Check local scripts:** `muggle-local-test-script-list` filtered by projectId
39
+
40
+ **Decision logic:**
41
+
42
+ | Cloud Script | Local Script (status: published/generated) | Action |
43
+ |--------------|---------------------------------------------|--------|
44
+ | Exists + ACTIVE | Exists | Ask user: "Replay existing script" or "Regenerate from scratch"? |
45
+ | Exists + ACTIVE | Not found | Sync from cloud first, then ask user |
46
+ | Not found | Exists | Ask user: "Replay local script" or "Regenerate"? |
47
+ | Not found | Not found | Default to generation (no need to ask) |
48
+
49
+ **When asking user, show:**
50
+ - Script name and ID
51
+ - When it was created/updated
52
+ - Number of steps
53
+ - Last run status if available
54
+
55
+ ### 5. Prepare for execution
56
+
57
+ **For Replay:**
58
+
59
+ Local scripts contain the complete `actionScript` with element labels required for replay. Remote scripts only contain metadata.
60
+
61
+ 1. Use `muggle-local-test-script-get` with `testScriptId` to fetch the FULL script including actionScript
62
+ 2. The returned script includes all steps with `operation.label` paths needed for element location
63
+ 3. Pass this complete script to `muggle-local-execute-replay`
64
+
65
+ **IMPORTANT:** Do NOT manually construct or simplify the actionScript. The electron app requires the complete script with all `label` paths intact to locate page elements during replay.
66
+
67
+ **For Generation:**
68
+
69
+ 1. `muggle-remote-test-case-get` to fetch test case details
70
+ 2. `muggle-local-execute-test-generation` with the test case
71
+
72
+ ### 6. Approval requirement
73
+
74
+ - Before execution, get explicit user approval for launching Electron app.
75
+ - Show what will be executed (replay vs generation, test case name, URL).
76
+ - Only then set `approveElectronAppLaunch: true`.
77
+
78
+ ### 7. Execute
79
+
80
+ **Replay:**
81
+ ```
82
+ muggle-local-execute-replay with:
83
+ - testScript: (full script from muggle-local-test-script-get)
84
+ - localUrl: user-provided localhost URL
85
+ - approveElectronAppLaunch: true
86
+ - showUi: true (optional, lets user watch)
87
+ ```
88
+
89
+ **Generation:**
90
+ ```
91
+ muggle-local-execute-test-generation with:
92
+ - testCase: (from muggle-remote-test-case-get)
93
+ - localUrl: user-provided localhost URL
94
+ - approveElectronAppLaunch: true
95
+ - showUi: true (optional)
96
+ ```
97
+
98
+ ### 8. Publish generation results (generation only)
99
+
100
+ - Use `muggle-local-publish-test-script` after successful generation.
101
+ - This uploads the script to cloud so it can be replayed later.
102
+ - Return the remote URL for user to view the result.
103
+
104
+ ### 9. Report results
105
+
106
+ - `muggle-local-run-result-get` with returned runId.
107
+ - Report:
108
+ - status (passed/failed)
109
+ - duration
110
+ - pass/fail summary
111
+ - steps summary (which steps passed/failed)
112
+ - artifacts path (screenshots location)
113
+ - script detail view URL
59
114
 
60
115
  ## Guardrails
61
116
 
62
117
  - Do not silently skip auth.
63
- - Do not silently skip test execution when no script exists; generate one.
118
+ - Do not silently skip asking user when a replayable script exists.
64
119
  - Do not launch Electron without explicit approval.
65
120
  - Do not hide failing run details; include error and artifacts path.
121
+ - Do not simplify or reconstruct actionScript for replay; use the complete script from `muggle-local-test-script-get`.
122
+ - Always check local scripts before defaulting to generation.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@muggleai/works",
3
3
  "mcpName": "io.github.multiplex-ai/muggle",
4
- "version": "4.0.1",
4
+ "version": "4.1.0",
5
5
  "description": "Ship quality products with AI-powered QA that validates your app's user experience — from Claude Code and Cursor to PR.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muggle",
3
3
  "description": "Run real-browser QA tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
4
- "version": "4.0.1",
4
+ "version": "4.1.0",
5
5
  "author": {
6
6
  "name": "Muggle AI",
7
7
  "email": "support@muggle-ai.com"
@@ -2,7 +2,7 @@
2
2
  "name": "muggle",
3
3
  "displayName": "Muggle AI",
4
4
  "description": "Ship quality products with AI-powered QA that validates your app's user experience — from Claude Code and Cursor to PR.",
5
- "version": "4.0.1",
5
+ "version": "4.1.0",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: muggle-test-feature-local
3
- description: Test a feature's user experience on localhost. Use when user types muggle test-feature-local, test my app, run QA, or validate UI changes.
3
+ description: Run a real-browser QA test against localhost to verify a feature works correctly — signup flows, checkout, form validation, UI interactions, or any user-facing behavior. Launches a browser that executes test steps and captures screenshots. Use this skill whenever the user asks to test, QA, validate, or verify their web app, UI changes, user flows, or frontend behavior on localhost or a dev server — even if they don't mention 'muggle' or 'QA' explicitly.
4
4
  ---
5
5
 
6
6
  # Muggle Test Feature Local
@@ -12,54 +12,111 @@ Run end-to-end feature testing from UI against a local URL:
12
12
 
13
13
  ## Workflow
14
14
 
15
- 1. **Auth**
16
- - `muggle-remote-auth-status`
17
- - If needed: `muggle-remote-auth-login` + `muggle-remote-auth-poll`
18
-
19
- 2. **Select or create the project -> use case -> test case to use**
20
- - Explictly ask user to select each target to proceed.
21
- - `muggle-remote-project-list`
22
- - `muggle-remote-use-case-list`
23
- - `muggle-remote-test-case-list-by-use-case`
24
-
25
- 3. **Resolve local URL**
26
- - Use the URL provided by the user.
27
- - If missing, ask explicitly (do not guess).
28
- - Inform user the local URL does not affect the project's remote test.
29
-
30
- 4. **Check script availability**
31
- - `muggle-remote-test-script-list` filtered by testCaseId
32
- - If script exists, recommend replay unless user-flow changes suggest regeneration.
33
-
34
- 5. **Execute**
35
- - Replay path:
36
- - `muggle-remote-test-script-get`
37
- - `muggle-local-execute-replay`
38
- - Generation path:
39
- - `muggle-remote-test-case-get`
40
- - `muggle-local-execute-test-generation`
41
-
42
- 6. **Approval requirement**
43
- - Before execution, get explicit user approval for launching Electron app.
44
- - Only then set `approveElectronAppLaunch: true`.
45
-
46
- 7. **Publish local generation result to MuggleTest cloud records**
47
- - Use `muggle-local-publish-test-script` after successful generation so user can see the full job details.
48
- - Return the remote URL for user to view the result and script details.
49
-
50
- 8. **Report results**
51
- - `muggle-local-run-result-get` with returned runId.
52
- - Report:
53
- - status
54
- - duration
55
- - pass/fail summary
56
- - steps summary
57
- - artifacts path
58
- - script detail view URL
15
+ ### 1. Auth
16
+
17
+ - `muggle-remote-auth-status`
18
+ - If needed: `muggle-remote-auth-login` + `muggle-remote-auth-poll`
19
+
20
+ ### 2. Select project, use case, and test case
21
+
22
+ - Explicitly ask user to select each target to proceed.
23
+ - `muggle-remote-project-list`
24
+ - `muggle-remote-use-case-list`
25
+ - `muggle-remote-test-case-list-by-use-case`
26
+
27
+ ### 3. Resolve local URL
28
+
29
+ - Use the URL provided by the user.
30
+ - If missing, ask explicitly (do not guess).
31
+ - Inform user the local URL does not affect the project's remote test.
32
+
33
+ ### 4. Check for existing scripts and ask user to choose
34
+
35
+ Check BOTH cloud and local scripts to determine what's available:
36
+
37
+ 1. **Check cloud scripts:** `muggle-remote-test-script-list` filtered by projectId
38
+ 2. **Check local scripts:** `muggle-local-test-script-list` filtered by projectId
39
+
40
+ **Decision logic:**
41
+
42
+ | Cloud Script | Local Script (status: published/generated) | Action |
43
+ |--------------|---------------------------------------------|--------|
44
+ | Exists + ACTIVE | Exists | Ask user: "Replay existing script" or "Regenerate from scratch"? |
45
+ | Exists + ACTIVE | Not found | Sync from cloud first, then ask user |
46
+ | Not found | Exists | Ask user: "Replay local script" or "Regenerate"? |
47
+ | Not found | Not found | Default to generation (no need to ask) |
48
+
49
+ **When asking user, show:**
50
+ - Script name and ID
51
+ - When it was created/updated
52
+ - Number of steps
53
+ - Last run status if available
54
+
55
+ ### 5. Prepare for execution
56
+
57
+ **For Replay:**
58
+
59
+ Local scripts contain the complete `actionScript` with element labels required for replay. Remote scripts only contain metadata.
60
+
61
+ 1. Use `muggle-local-test-script-get` with `testScriptId` to fetch the FULL script including actionScript
62
+ 2. The returned script includes all steps with `operation.label` paths needed for element location
63
+ 3. Pass this complete script to `muggle-local-execute-replay`
64
+
65
+ **IMPORTANT:** Do NOT manually construct or simplify the actionScript. The electron app requires the complete script with all `label` paths intact to locate page elements during replay.
66
+
67
+ **For Generation:**
68
+
69
+ 1. `muggle-remote-test-case-get` to fetch test case details
70
+ 2. `muggle-local-execute-test-generation` with the test case
71
+
72
+ ### 6. Approval requirement
73
+
74
+ - Before execution, get explicit user approval for launching Electron app.
75
+ - Show what will be executed (replay vs generation, test case name, URL).
76
+ - Only then set `approveElectronAppLaunch: true`.
77
+
78
+ ### 7. Execute
79
+
80
+ **Replay:**
81
+ ```
82
+ muggle-local-execute-replay with:
83
+ - testScript: (full script from muggle-local-test-script-get)
84
+ - localUrl: user-provided localhost URL
85
+ - approveElectronAppLaunch: true
86
+ - showUi: true (optional, lets user watch)
87
+ ```
88
+
89
+ **Generation:**
90
+ ```
91
+ muggle-local-execute-test-generation with:
92
+ - testCase: (from muggle-remote-test-case-get)
93
+ - localUrl: user-provided localhost URL
94
+ - approveElectronAppLaunch: true
95
+ - showUi: true (optional)
96
+ ```
97
+
98
+ ### 8. Publish generation results (generation only)
99
+
100
+ - Use `muggle-local-publish-test-script` after successful generation.
101
+ - This uploads the script to cloud so it can be replayed later.
102
+ - Return the remote URL for user to view the result.
103
+
104
+ ### 9. Report results
105
+
106
+ - `muggle-local-run-result-get` with returned runId.
107
+ - Report:
108
+ - status (passed/failed)
109
+ - duration
110
+ - pass/fail summary
111
+ - steps summary (which steps passed/failed)
112
+ - artifacts path (screenshots location)
113
+ - script detail view URL
59
114
 
60
115
  ## Guardrails
61
116
 
62
117
  - Do not silently skip auth.
63
- - Do not silently skip test execution when no script exists; generate one.
118
+ - Do not silently skip asking user when a replayable script exists.
64
119
  - Do not launch Electron without explicit approval.
65
120
  - Do not hide failing run details; include error and artifacts path.
121
+ - Do not simplify or reconstruct actionScript for replay; use the complete script from `muggle-local-test-script-get`.
122
+ - Always check local scripts before defaulting to generation.