@muggleai/works 4.0.3 → 4.2.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
 
@@ -39,7 +39,7 @@ 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/";
@@ -239,10 +239,10 @@ function getDefaultAuth0Domain() {
239
239
  }
240
240
  function getDefaultAuth0ClientId() {
241
241
  const runtimeTarget = getPromptServiceRuntimeTarget();
242
- if (runtimeTarget === "dev") {
243
- return DEFAULT_AUTH0_DEV_CLIENT_ID;
242
+ if (runtimeTarget === "production") {
243
+ return DEFAULT_AUTH0_PRODUCTION_CLIENT_ID;
244
244
  }
245
- return DEFAULT_AUTH0_PRODUCTION_CLIENT_ID;
245
+ return DEFAULT_AUTH0_DEV_CLIENT_ID;
246
246
  }
247
247
  function getDefaultAuth0Audience() {
248
248
  const runtimeTarget = getPromptServiceRuntimeTarget();
@@ -284,8 +284,8 @@ function buildLocalQaConfig() {
284
284
  sessionsDir: path2.join(dataDir, "sessions"),
285
285
  projectsDir: path2.join(dataDir, "projects"),
286
286
  tempDir: path2.join(dataDir, "temp"),
287
- credentialsFilePath: path2.join(dataDir, CREDENTIALS_FILE),
288
- authFilePath: path2.join(dataDir, "auth.json"),
287
+ apiKeyFilePath: path2.join(dataDir, API_KEY_FILE),
288
+ oauthSessionFilePath: path2.join(dataDir, "oauth-session.json"),
289
289
  electronAppPath: resolveElectronAppPathOrNull(),
290
290
  webServicePath: resolveWebServicePath(),
291
291
  webServicePidFile: path2.join(dataDir, "web-service.pid"),
@@ -610,8 +610,8 @@ var TestResultStatus = /* @__PURE__ */ ((TestResultStatus2) => {
610
610
  // packages/mcps/src/mcp/local/services/auth-service.ts
611
611
  var DEFAULT_LOGIN_WAIT_TIMEOUT_MS = 12e4;
612
612
  var AuthService = class {
613
- /** Path to the auth file. */
614
- authFilePath;
613
+ /** Path to the OAuth session file. */
614
+ oauthSessionFilePath;
615
615
  /** Path to the pending device code file. */
616
616
  pendingDeviceCodePath;
617
617
  /**
@@ -619,9 +619,9 @@ var AuthService = class {
619
619
  */
620
620
  constructor() {
621
621
  const config = getConfig();
622
- this.authFilePath = config.localQa.authFilePath;
622
+ this.oauthSessionFilePath = config.localQa.oauthSessionFilePath;
623
623
  this.pendingDeviceCodePath = path2.join(
624
- path2.dirname(config.localQa.authFilePath),
624
+ path2.dirname(config.localQa.oauthSessionFilePath),
625
625
  "pending-device-code.json"
626
626
  );
627
627
  }
@@ -931,11 +931,11 @@ var AuthService = class {
931
931
  email,
932
932
  userId
933
933
  };
934
- const dir = path2.dirname(this.authFilePath);
934
+ const dir = path2.dirname(this.oauthSessionFilePath);
935
935
  if (!fs3.existsSync(dir)) {
936
936
  fs3.mkdirSync(dir, { recursive: true });
937
937
  }
938
- fs3.writeFileSync(this.authFilePath, JSON.stringify(storedAuth, null, 2), {
938
+ fs3.writeFileSync(this.oauthSessionFilePath, JSON.stringify(storedAuth, null, 2), {
939
939
  encoding: "utf-8",
940
940
  mode: 384
941
941
  });
@@ -946,11 +946,11 @@ var AuthService = class {
946
946
  */
947
947
  loadStoredAuth() {
948
948
  const logger14 = getLogger();
949
- if (!fs3.existsSync(this.authFilePath)) {
949
+ if (!fs3.existsSync(this.oauthSessionFilePath)) {
950
950
  return null;
951
951
  }
952
952
  try {
953
- const content = fs3.readFileSync(this.authFilePath, "utf-8");
953
+ const content = fs3.readFileSync(this.oauthSessionFilePath, "utf-8");
954
954
  return JSON.parse(content);
955
955
  } catch (error) {
956
956
  logger14.error("Failed to load stored auth", {
@@ -1034,11 +1034,11 @@ var AuthService = class {
1034
1034
  email: storedAuth.email,
1035
1035
  userId: storedAuth.userId
1036
1036
  };
1037
- const dir = path2.dirname(this.authFilePath);
1037
+ const dir = path2.dirname(this.oauthSessionFilePath);
1038
1038
  if (!fs3.existsSync(dir)) {
1039
1039
  fs3.mkdirSync(dir, { recursive: true });
1040
1040
  }
1041
- fs3.writeFileSync(this.authFilePath, JSON.stringify(updatedAuth, null, 2), {
1041
+ fs3.writeFileSync(this.oauthSessionFilePath, JSON.stringify(updatedAuth, null, 2), {
1042
1042
  encoding: "utf-8",
1043
1043
  mode: 384
1044
1044
  });
@@ -1090,12 +1090,12 @@ var AuthService = class {
1090
1090
  */
1091
1091
  logout() {
1092
1092
  const logger14 = getLogger();
1093
- if (!fs3.existsSync(this.authFilePath)) {
1093
+ if (!fs3.existsSync(this.oauthSessionFilePath)) {
1094
1094
  logger14.debug("No auth to clear");
1095
1095
  return false;
1096
1096
  }
1097
1097
  try {
1098
- fs3.unlinkSync(this.authFilePath);
1098
+ fs3.unlinkSync(this.oauthSessionFilePath);
1099
1099
  logger14.info("Auth cleared successfully");
1100
1100
  return true;
1101
1101
  } catch (error) {
@@ -2380,9 +2380,9 @@ function listActiveExecutions() {
2380
2380
  status: process2.status
2381
2381
  }));
2382
2382
  }
2383
- var CREDENTIALS_FILE2 = "credentials.json";
2384
- function getCredentialsFilePath() {
2385
- 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);
2386
2386
  }
2387
2387
  function ensureDataDir() {
2388
2388
  const dataDir = getDataDir();
@@ -2390,95 +2390,85 @@ function ensureDataDir() {
2390
2390
  fs3.mkdirSync(dataDir, { recursive: true });
2391
2391
  }
2392
2392
  }
2393
- function loadCredentials() {
2393
+ function loadApiKeyData() {
2394
2394
  const logger14 = getLogger();
2395
- const credentialsPath = getCredentialsFilePath();
2395
+ const apiKeyPath = getApiKeyFilePath();
2396
2396
  try {
2397
- if (!fs3.existsSync(credentialsPath)) {
2398
- logger14.debug("No credentials file found", { path: credentialsPath });
2397
+ if (!fs3.existsSync(apiKeyPath)) {
2398
+ logger14.debug("No API key file found", { path: apiKeyPath });
2399
2399
  return null;
2400
2400
  }
2401
- const content = fs3.readFileSync(credentialsPath, "utf-8");
2402
- const credentials = JSON.parse(content);
2403
- if (!credentials.accessToken || !credentials.expiresAt) {
2404
- logger14.warn("Invalid credentials file - missing required fields");
2405
- return null;
2406
- }
2407
- return credentials;
2401
+ const content = fs3.readFileSync(apiKeyPath, "utf-8");
2402
+ const data = JSON.parse(content);
2403
+ return data;
2408
2404
  } catch (error) {
2409
- logger14.warn("Failed to load credentials", {
2405
+ logger14.warn("Failed to load API key data", {
2410
2406
  error: error instanceof Error ? error.message : String(error)
2411
2407
  });
2412
2408
  return null;
2413
2409
  }
2414
2410
  }
2415
- function saveCredentials(credentials) {
2411
+ function saveApiKeyData(data) {
2416
2412
  const logger14 = getLogger();
2417
- const credentialsPath = getCredentialsFilePath();
2413
+ const apiKeyPath = getApiKeyFilePath();
2418
2414
  try {
2419
2415
  ensureDataDir();
2420
- const content = JSON.stringify(credentials, null, 2);
2421
- fs3.writeFileSync(credentialsPath, content, { mode: 384 });
2422
- 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 });
2423
2419
  } catch (error) {
2424
- logger14.error("Failed to save credentials", {
2420
+ logger14.error("Failed to save API key", {
2425
2421
  error: error instanceof Error ? error.message : String(error)
2426
2422
  });
2427
2423
  throw error;
2428
2424
  }
2429
2425
  }
2430
- function deleteCredentials() {
2426
+ function deleteApiKeyData() {
2431
2427
  const logger14 = getLogger();
2432
- const credentialsPath = getCredentialsFilePath();
2428
+ const apiKeyPath = getApiKeyFilePath();
2433
2429
  try {
2434
- if (fs3.existsSync(credentialsPath)) {
2435
- fs3.unlinkSync(credentialsPath);
2436
- logger14.info("Credentials deleted", { path: credentialsPath });
2430
+ if (fs3.existsSync(apiKeyPath)) {
2431
+ fs3.unlinkSync(apiKeyPath);
2432
+ logger14.info("API key deleted", { path: apiKeyPath });
2437
2433
  }
2438
2434
  } catch (error) {
2439
- logger14.warn("Failed to delete credentials", {
2435
+ logger14.warn("Failed to delete API key", {
2440
2436
  error: error instanceof Error ? error.message : String(error)
2441
2437
  });
2442
2438
  }
2443
2439
  }
2444
- function isCredentialsExpired(credentials) {
2445
- const expiresAt = new Date(credentials.expiresAt);
2446
- const now = /* @__PURE__ */ new Date();
2447
- const bufferMs = 5 * 60 * 1e3;
2448
- return now.getTime() >= expiresAt.getTime() - bufferMs;
2449
- }
2450
- function getValidCredentials() {
2451
- const credentials = loadCredentials();
2452
- if (!credentials) {
2440
+ function getValidApiKeyData() {
2441
+ const data = loadApiKeyData();
2442
+ if (!data) {
2453
2443
  return null;
2454
2444
  }
2455
- if (credentials.apiKey) {
2456
- return credentials;
2445
+ if (data.apiKey) {
2446
+ return data;
2457
2447
  }
2458
2448
  return null;
2459
2449
  }
2460
2450
  function hasApiKey() {
2461
- const credentials = loadCredentials();
2462
- return !!credentials?.apiKey;
2451
+ const data = loadApiKeyData();
2452
+ return !!data?.apiKey;
2463
2453
  }
2464
2454
  function getApiKey() {
2465
- const credentials = loadCredentials();
2466
- return credentials?.apiKey ?? null;
2455
+ const data = loadApiKeyData();
2456
+ return data?.apiKey ?? null;
2467
2457
  }
2468
2458
  function saveApiKey(params) {
2469
2459
  const logger14 = getLogger();
2470
- const credentialsPath = getCredentialsFilePath();
2460
+ const apiKeyPath = getApiKeyFilePath();
2471
2461
  try {
2472
2462
  ensureDataDir();
2473
- const credentials = {
2463
+ const data = {
2474
2464
  accessToken: "",
2475
2465
  expiresAt: "",
2476
2466
  apiKey: params.apiKey,
2477
2467
  apiKeyId: params.apiKeyId
2478
2468
  };
2479
- const content = JSON.stringify(credentials, null, 2);
2480
- fs3.writeFileSync(credentialsPath, content, { mode: 384 });
2481
- 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 });
2482
2472
  } catch (error) {
2483
2473
  logger14.error("Failed to save API key", {
2484
2474
  error: error instanceof Error ? error.message : String(error)
@@ -2486,6 +2476,11 @@ function saveApiKey(params) {
2486
2476
  throw error;
2487
2477
  }
2488
2478
  }
2479
+ var loadCredentials = loadApiKeyData;
2480
+ var saveCredentials = saveApiKeyData;
2481
+ var deleteCredentials = deleteApiKeyData;
2482
+ var getValidCredentials = getValidApiKeyData;
2483
+ var getCredentialsFilePath = getApiKeyFilePath;
2489
2484
 
2490
2485
  // packages/mcps/src/shared/auth.ts
2491
2486
  var logger4 = getLogger();
@@ -2694,7 +2689,7 @@ async function performLogin(keyName, keyExpiry = "90d", timeoutMs = 12e4) {
2694
2689
  );
2695
2690
  credentials.apiKey = apiKeyResult.key;
2696
2691
  credentials.apiKeyId = apiKeyResult.id;
2697
- saveCredentials(credentials);
2692
+ saveApiKeyData(credentials);
2698
2693
  }
2699
2694
  return {
2700
2695
  success: true,
@@ -2731,13 +2726,13 @@ async function performLogin(keyName, keyExpiry = "90d", timeoutMs = 12e4) {
2731
2726
  function performLogout() {
2732
2727
  const authService = getAuthService();
2733
2728
  authService.logout();
2734
- deleteCredentials();
2729
+ deleteApiKeyData();
2735
2730
  logger4.info("[Auth] Logged out successfully");
2736
2731
  }
2737
2732
  function getCallerCredentials() {
2738
- const credentials = getValidCredentials();
2739
- if (credentials?.apiKey) {
2740
- return { apiKey: credentials.apiKey };
2733
+ const apiKeyData = getValidApiKeyData();
2734
+ if (apiKeyData?.apiKey) {
2735
+ return { apiKey: apiKeyData.apiKey };
2741
2736
  }
2742
2737
  const authService = getAuthService();
2743
2738
  const accessToken = authService.getAccessToken();
@@ -2747,9 +2742,9 @@ function getCallerCredentials() {
2747
2742
  return {};
2748
2743
  }
2749
2744
  async function getCallerCredentialsAsync() {
2750
- const credentials = getValidCredentials();
2751
- if (credentials?.apiKey) {
2752
- return { apiKey: credentials.apiKey };
2745
+ const apiKeyData = getValidApiKeyData();
2746
+ if (apiKeyData?.apiKey) {
2747
+ return { apiKey: apiKeyData.apiKey };
2753
2748
  }
2754
2749
  const authService = getAuthService();
2755
2750
  const accessToken = await authService.getValidAccessToken();
@@ -5466,8 +5461,10 @@ __export(src_exports, {
5466
5461
  calculateFileChecksum: () => calculateFileChecksum,
5467
5462
  createApiKeyWithToken: () => createApiKeyWithToken,
5468
5463
  createChildLogger: () => createChildLogger,
5464
+ deleteApiKeyData: () => deleteApiKeyData,
5469
5465
  deleteCredentials: () => deleteCredentials,
5470
5466
  getApiKey: () => getApiKey,
5467
+ getApiKeyFilePath: () => getApiKeyFilePath,
5471
5468
  getAuthService: () => getAuthService,
5472
5469
  getBundledElectronAppVersion: () => getBundledElectronAppVersion,
5473
5470
  getCallerCredentials: () => getCallerCredentials,
@@ -5485,10 +5482,11 @@ __export(src_exports, {
5485
5482
  getLogger: () => getLogger,
5486
5483
  getPlatformKey: () => getPlatformKey,
5487
5484
  getQaTools: () => getQaTools,
5485
+ getValidApiKeyData: () => getValidApiKeyData,
5488
5486
  getValidCredentials: () => getValidCredentials,
5489
5487
  hasApiKey: () => hasApiKey,
5490
- isCredentialsExpired: () => isCredentialsExpired,
5491
5488
  isElectronAppInstalled: () => isElectronAppInstalled,
5489
+ loadApiKeyData: () => loadApiKeyData,
5492
5490
  loadCredentials: () => loadCredentials,
5493
5491
  localQa: () => local_exports2,
5494
5492
  mcp: () => mcp_exports,
@@ -5500,6 +5498,7 @@ __export(src_exports, {
5500
5498
  resetConfig: () => resetConfig,
5501
5499
  resetLogger: () => resetLogger,
5502
5500
  saveApiKey: () => saveApiKey,
5501
+ saveApiKeyData: () => saveApiKeyData,
5503
5502
  saveCredentials: () => saveCredentials,
5504
5503
  startDeviceCodeFlow: () => startDeviceCodeFlow,
5505
5504
  toolRequiresAuth: () => toolRequiresAuth,
@@ -5893,9 +5892,96 @@ async function startStdioServer(server) {
5893
5892
  }
5894
5893
  var logger7 = getLogger();
5895
5894
  var ELECTRON_APP_DIR2 = "electron-app";
5895
+ var CURSOR_SKILLS_DIR = ".cursor";
5896
+ var CURSOR_SKILLS_SUBDIR = "skills";
5897
+ var MUGGLE_SKILL_PREFIX = "muggle";
5898
+ var INSTALL_MANIFEST_FILE = "install-manifest.json";
5896
5899
  function getElectronAppBaseDir() {
5897
5900
  return path2.join(getDataDir2(), ELECTRON_APP_DIR2);
5898
5901
  }
5902
+ function getCursorSkillsDir() {
5903
+ return path2.join(homedir(), CURSOR_SKILLS_DIR, CURSOR_SKILLS_SUBDIR);
5904
+ }
5905
+ function getInstallManifestPath() {
5906
+ return path2.join(getDataDir2(), INSTALL_MANIFEST_FILE);
5907
+ }
5908
+ function readInstallManifest() {
5909
+ const manifestPath = getInstallManifestPath();
5910
+ if (!existsSync(manifestPath)) {
5911
+ return null;
5912
+ }
5913
+ try {
5914
+ const content = readFileSync(manifestPath, "utf-8");
5915
+ const manifest = JSON.parse(content);
5916
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
5917
+ return null;
5918
+ }
5919
+ return manifest;
5920
+ } catch {
5921
+ return null;
5922
+ }
5923
+ }
5924
+ function listObsoleteSkills() {
5925
+ const skillsDir = getCursorSkillsDir();
5926
+ const manifest = readInstallManifest();
5927
+ const obsoleteSkills = [];
5928
+ if (!existsSync(skillsDir)) {
5929
+ return obsoleteSkills;
5930
+ }
5931
+ const manifestSkills = new Set(manifest?.skills ?? []);
5932
+ try {
5933
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
5934
+ for (const entry of entries) {
5935
+ if (!entry.isDirectory()) {
5936
+ continue;
5937
+ }
5938
+ if (!entry.name.startsWith(MUGGLE_SKILL_PREFIX)) {
5939
+ continue;
5940
+ }
5941
+ if (manifestSkills.has(entry.name)) {
5942
+ continue;
5943
+ }
5944
+ const skillPath = path2.join(skillsDir, entry.name);
5945
+ const sizeBytes = getDirectorySize(skillPath);
5946
+ obsoleteSkills.push({
5947
+ name: entry.name,
5948
+ path: skillPath,
5949
+ sizeBytes
5950
+ });
5951
+ }
5952
+ } catch (error) {
5953
+ const errorMessage = error instanceof Error ? error.message : String(error);
5954
+ logger7.warn("Failed to list obsolete skills", { error: errorMessage });
5955
+ }
5956
+ return obsoleteSkills;
5957
+ }
5958
+ function cleanupObsoleteSkills(options = {}) {
5959
+ const { dryRun = false } = options;
5960
+ const obsoleteSkills = listObsoleteSkills();
5961
+ const removed = [];
5962
+ let freedBytes = 0;
5963
+ for (const skill of obsoleteSkills) {
5964
+ if (!dryRun) {
5965
+ try {
5966
+ rmSync(skill.path, { recursive: true, force: true });
5967
+ logger7.info("Removed obsolete skill", {
5968
+ skill: skill.name,
5969
+ freedBytes: skill.sizeBytes
5970
+ });
5971
+ } catch (error) {
5972
+ const errorMessage = error instanceof Error ? error.message : String(error);
5973
+ logger7.error("Failed to remove skill", {
5974
+ skill: skill.name,
5975
+ error: errorMessage
5976
+ });
5977
+ continue;
5978
+ }
5979
+ }
5980
+ removed.push(skill);
5981
+ freedBytes += skill.sizeBytes;
5982
+ }
5983
+ return { removed, freedBytes };
5984
+ }
5899
5985
  function getDirectorySize(dirPath) {
5900
5986
  let totalSize = 0;
5901
5987
  try {
@@ -6028,51 +6114,78 @@ async function versionsCommand() {
6028
6114
  console.log("");
6029
6115
  }
6030
6116
  async function cleanupCommand(options) {
6117
+ let totalFreedBytes = 0;
6118
+ let totalRemovedCount = 0;
6031
6119
  console.log("\nElectron App Cleanup");
6032
6120
  console.log("====================\n");
6033
6121
  const versions = listInstalledVersions();
6034
6122
  if (versions.length === 0) {
6035
6123
  console.log("No versions installed. Nothing to clean up.\n");
6036
- return;
6037
- }
6038
- if (versions.length === 1) {
6124
+ } else if (versions.length === 1) {
6039
6125
  console.log("Only the current version is installed. Nothing to clean up.\n");
6040
- return;
6041
- }
6042
- const currentVersion = versions.find((v) => v.isCurrent);
6043
- const oldVersions = versions.filter((v) => !v.isCurrent);
6044
- console.log(`Current version: v${currentVersion?.version || "unknown"}`);
6045
- console.log(`Old versions: ${oldVersions.length}`);
6046
- console.log("");
6047
- if (options.dryRun) {
6048
- console.log("Dry run - showing what would be deleted:\n");
6049
- }
6050
- const result = cleanupOldVersions(options);
6051
- if (result.removed.length === 0) {
6052
- if (options.all) {
6053
- console.log("No old versions to remove.\n");
6126
+ } else {
6127
+ const currentVersion = versions.find((v) => v.isCurrent);
6128
+ const oldVersions = versions.filter((v) => !v.isCurrent);
6129
+ console.log(`Current version: v${currentVersion?.version ?? "unknown"}`);
6130
+ console.log(`Old versions: ${oldVersions.length}`);
6131
+ console.log("");
6132
+ if (options.dryRun) {
6133
+ console.log("Dry run - showing what would be deleted:\n");
6134
+ }
6135
+ const result = cleanupOldVersions(options);
6136
+ if (result.removed.length === 0) {
6137
+ if (options.all) {
6138
+ console.log("No old versions to remove.\n");
6139
+ } else {
6140
+ console.log("Keeping one previous version for rollback.");
6141
+ console.log("Use --all to remove all old versions.\n");
6142
+ }
6054
6143
  } else {
6055
- console.log("Keeping one previous version for rollback.");
6056
- console.log("Use --all to remove all old versions.\n");
6144
+ console.log(options.dryRun ? "Would remove:" : "Removed:");
6145
+ for (const version of result.removed) {
6146
+ console.log(` v${version.version} (${formatBytes(version.sizeBytes)})`);
6147
+ }
6148
+ totalFreedBytes += result.freedBytes;
6149
+ totalRemovedCount += result.removed.length;
6150
+ console.log("");
6057
6151
  }
6058
- return;
6059
6152
  }
6060
- console.log(options.dryRun ? "Would remove:" : "Removed:");
6061
- for (const version of result.removed) {
6062
- console.log(` v${version.version} (${formatBytes(version.sizeBytes)})`);
6153
+ if (options.skills) {
6154
+ console.log("Skills Cleanup");
6155
+ console.log("==============\n");
6156
+ const obsoleteSkills = listObsoleteSkills();
6157
+ if (obsoleteSkills.length === 0) {
6158
+ console.log("No obsolete skills found. Nothing to clean up.\n");
6159
+ } else {
6160
+ console.log(`Found ${obsoleteSkills.length} obsolete skill(s):
6161
+ `);
6162
+ if (options.dryRun) {
6163
+ console.log("Dry run - showing what would be deleted:\n");
6164
+ }
6165
+ const skillResult = cleanupObsoleteSkills({ dryRun: options.dryRun });
6166
+ console.log(options.dryRun ? "Would remove:" : "Removed:");
6167
+ for (const skill of skillResult.removed) {
6168
+ console.log(` ${skill.name} (${formatBytes(skill.sizeBytes)})`);
6169
+ }
6170
+ totalFreedBytes += skillResult.freedBytes;
6171
+ totalRemovedCount += skillResult.removed.length;
6172
+ console.log("");
6173
+ }
6063
6174
  }
6064
- console.log("");
6065
- console.log(
6066
- `${options.dryRun ? "Would free" : "Freed"}: ${formatBytes(result.freedBytes)}`
6067
- );
6068
- console.log("");
6069
- if (options.dryRun) {
6070
- console.log("Run without --dry-run to actually delete.\n");
6175
+ if (totalRemovedCount > 0) {
6176
+ console.log(
6177
+ `${options.dryRun ? "Would free" : "Freed"}: ${formatBytes(totalFreedBytes)} total`
6178
+ );
6179
+ console.log("");
6180
+ if (options.dryRun) {
6181
+ console.log("Run without --dry-run to actually delete.\n");
6182
+ }
6071
6183
  }
6072
6184
  logger7.info("Cleanup completed", {
6073
- removed: result.removed.length,
6074
- freedBytes: result.freedBytes,
6075
- dryRun: options.dryRun
6185
+ removed: totalRemovedCount,
6186
+ freedBytes: totalFreedBytes,
6187
+ dryRun: options.dryRun,
6188
+ includeSkills: options.skills
6076
6189
  });
6077
6190
  }
6078
6191
  var logger8 = getLogger();
@@ -6423,6 +6536,7 @@ function getHelpGuidance() {
6423
6536
  ` ${cmd("muggle versions")} List installed Electron app versions`,
6424
6537
  ` ${cmd("muggle cleanup")} Remove old Electron app versions`,
6425
6538
  ` ${cmd("muggle cleanup --all")} Remove all old versions`,
6539
+ ` ${cmd("muggle cleanup --skills")} Also remove obsolete skills`,
6426
6540
  "",
6427
6541
  ` ${colorize("Help:", COLORS.bold)}`,
6428
6542
  ` ${cmd("muggle help")} Show this guide`,
@@ -6438,7 +6552,7 @@ function getHelpGuidance() {
6438
6552
  ` 2. ${colorize("Log in", COLORS.bold)} with your Muggle AI account`,
6439
6553
  ` 3. ${colorize("The tool call continues", COLORS.bold)} with your credentials`,
6440
6554
  "",
6441
- ` Credentials are stored in ${path12("~/.muggle-ai/credentials.json")}`,
6555
+ ` API keys are stored in ${path12("~/.muggle-ai/api-key.json")}`,
6442
6556
  "",
6443
6557
  header("Available MCP Tools"),
6444
6558
  "",
@@ -6455,7 +6569,7 @@ function getHelpGuidance() {
6455
6569
  "",
6456
6570
  ` All data is stored in ${path12("~/.muggle-ai/")}:`,
6457
6571
  "",
6458
- ` ${path12("credentials.json")} Auth credentials (auto-generated)`,
6572
+ ` ${path12("api-key.json")} Long-lived API key (auto-generated)`,
6459
6573
  ` ${path12("projects/")} Local test projects`,
6460
6574
  ` ${path12("sessions/")} Test execution sessions`,
6461
6575
  ` ${path12("electron-app/")} Downloaded Electron app binaries`,
@@ -7143,7 +7257,7 @@ function createProgram() {
7143
7257
  program.command("setup").description("Download/update the Electron app for local testing").option("--force", "Force re-download even if already installed").action(setupCommand);
7144
7258
  program.command("upgrade").description("Check for and install the latest electron-app version").option("--force", "Force re-download even if already on latest").option("--check", "Check for updates only, don't download").option("--version <version>", "Download a specific version (e.g., 1.0.2)").action(upgradeCommand);
7145
7259
  program.command("versions").description("List installed electron-app versions").action(versionsCommand);
7146
- program.command("cleanup").description("Remove old electron-app versions to free disk space").option("--all", "Remove all old versions (default: keep one previous)").option("--dry-run", "Show what would be deleted without deleting").action(cleanupCommand);
7260
+ program.command("cleanup").description("Remove old electron-app versions and obsolete skills").option("--all", "Remove all old versions (default: keep one previous)").option("--dry-run", "Show what would be deleted without deleting").option("--skills", "Also clean up obsolete skills from ~/.cursor/skills").action(cleanupCommand);
7147
7261
  program.command("doctor").description("Diagnose installation and configuration issues").action(doctorCommand);
7148
7262
  program.command("login").description("Authenticate with Muggle AI (uses device code flow)").option("--key-name <name>", "Name for the API key").option("--key-expiry <expiry>", "API key expiry: 30d, 90d, 1y, never", "90d").action(loginCommand);
7149
7263
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-BQZQDOXI.js';
2
+ import { runCli } from './chunk-CXTJOYWM.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-BQZQDOXI.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-CXTJOYWM.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.3",
4
+ "version": "4.2.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.3",
5
+ "version": "4.2.0",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
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.3",
4
+ "version": "4.2.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.3",
4
+ "version": "4.2.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.3",
5
+ "version": "4.2.0",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -28,6 +28,7 @@ import { fileURLToPath } from "url";
28
28
  const require = createRequire(import.meta.url);
29
29
  const VERSION_DIRECTORY_NAME_PATTERN = /^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?$/;
30
30
  const INSTALL_METADATA_FILE_NAME = ".install-metadata.json";
31
+ const INSTALL_MANIFEST_FILE_NAME = "install-manifest.json";
31
32
  const LOG_FILE_NAME = "postinstall.log";
32
33
  const VERSION_OVERRIDE_FILE_NAME = "electron-app-version-override.json";
33
34
  const CURSOR_SKILLS_DIRECTORY_NAME = ".cursor";
@@ -124,11 +125,101 @@ function getPackageRootDir() {
124
125
  return join(dirname(fileURLToPath(import.meta.url)), "..");
125
126
  }
126
127
 
128
+ /**
129
+ * Get the path to the install manifest file.
130
+ * The manifest tracks what content was installed by this package,
131
+ * enabling cleanup of obsolete content when items are renamed or removed.
132
+ * @returns {string} Path to ~/.muggle-ai/install-manifest.json
133
+ */
134
+ function getInstallManifestPath() {
135
+ return join(getDataDir(), INSTALL_MANIFEST_FILE_NAME);
136
+ }
137
+
138
+ /**
139
+ * Read the install manifest from disk.
140
+ * @returns {{ packageVersion?: string, skills?: string[], installedAt?: string } | null}
141
+ */
142
+ function readInstallManifest() {
143
+ const manifestPath = getInstallManifestPath();
144
+
145
+ if (!existsSync(manifestPath)) {
146
+ return null;
147
+ }
148
+
149
+ try {
150
+ const content = readFileSync(manifestPath, "utf-8");
151
+ const manifest = JSON.parse(content);
152
+
153
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
154
+ return null;
155
+ }
156
+
157
+ return manifest;
158
+ } catch {
159
+ return null;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Write the install manifest to disk.
165
+ * @param {object} params - Manifest fields
166
+ * @param {string} params.packageVersion - The package version being installed
167
+ * @param {string[]} params.skills - List of skill directory names installed
168
+ */
169
+ function writeInstallManifest({ packageVersion, skills }) {
170
+ const manifestPath = getInstallManifestPath();
171
+ const manifest = {
172
+ packageVersion: packageVersion,
173
+ skills: skills,
174
+ installedAt: new Date().toISOString(),
175
+ };
176
+
177
+ mkdirSync(dirname(manifestPath), { recursive: true });
178
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
179
+ }
180
+
181
+ /**
182
+ * Remove obsolete skills that were installed by a previous version but are no longer present.
183
+ * @param {object} params - Cleanup parameters
184
+ * @param {string[]} params.previousSkills - Skills from the previous manifest
185
+ * @param {string[]} params.currentSkills - Skills being installed now
186
+ * @param {string} params.cursorSkillsDirectoryPath - Path to ~/.cursor/skills
187
+ */
188
+ function cleanupObsoleteSkills({ previousSkills, currentSkills, cursorSkillsDirectoryPath }) {
189
+ const currentSkillSet = new Set(currentSkills);
190
+ const obsoleteSkills = previousSkills.filter((skill) => !currentSkillSet.has(skill));
191
+
192
+ if (obsoleteSkills.length === 0) {
193
+ return;
194
+ }
195
+
196
+ for (const skillName of obsoleteSkills) {
197
+ const skillPath = join(cursorSkillsDirectoryPath, skillName);
198
+
199
+ if (!existsSync(skillPath)) {
200
+ continue;
201
+ }
202
+
203
+ try {
204
+ rmSync(skillPath, { recursive: true, force: true });
205
+ log(`Removed obsolete skill: ${skillName}`);
206
+ } catch (error) {
207
+ logError(`Failed to remove obsolete skill ${skillName}: ${error.message}`);
208
+ }
209
+ }
210
+
211
+ log(`Cleaned up ${obsoleteSkills.length} obsolete skill(s)`);
212
+ }
213
+
127
214
  /**
128
215
  * Sync packaged muggle skills into Cursor user skills.
129
216
  * This enables npm installs to refresh locally available `muggle-*` skills.
217
+ * Also cleans up obsolete skills that were removed or renamed in the package.
130
218
  */
131
219
  function syncCursorSkills() {
220
+ const packageJson = require("../package.json");
221
+ const packageVersion = packageJson.version;
222
+
132
223
  const sourceSkillsDirectoryPath = join(getPackageRootDir(), "plugin", "skills");
133
224
  if (!existsSync(sourceSkillsDirectoryPath)) {
134
225
  log("Cursor skill sync skipped: packaged plugin skills directory not found.");
@@ -143,7 +234,7 @@ function syncCursorSkills() {
143
234
  mkdirSync(cursorSkillsDirectoryPath, { recursive: true });
144
235
 
145
236
  const skillEntries = readdirSync(sourceSkillsDirectoryPath, { withFileTypes: true });
146
- let syncedSkillCount = 0;
237
+ const installedSkills = [];
147
238
 
148
239
  for (const skillEntry of skillEntries) {
149
240
  if (!skillEntry.isDirectory()) {
@@ -163,10 +254,26 @@ function syncCursorSkills() {
163
254
  const targetSkillDirectoryPath = join(cursorSkillsDirectoryPath, skillEntry.name);
164
255
  rmSync(targetSkillDirectoryPath, { recursive: true, force: true });
165
256
  cpSync(sourceSkillDirectoryPath, targetSkillDirectoryPath, { recursive: true });
166
- syncedSkillCount += 1;
257
+ installedSkills.push(skillEntry.name);
258
+ }
259
+
260
+ log(`Synced ${installedSkills.length} muggle skill(s) to ${cursorSkillsDirectoryPath}`);
261
+
262
+ // Clean up obsolete skills from previous installation
263
+ const previousManifest = readInstallManifest();
264
+ if (previousManifest && Array.isArray(previousManifest.skills)) {
265
+ cleanupObsoleteSkills({
266
+ previousSkills: previousManifest.skills,
267
+ currentSkills: installedSkills,
268
+ cursorSkillsDirectoryPath: cursorSkillsDirectoryPath,
269
+ });
167
270
  }
168
271
 
169
- log(`Synced ${syncedSkillCount} muggle skill(s) to ${cursorSkillsDirectoryPath}`);
272
+ // Write updated manifest
273
+ writeInstallManifest({
274
+ packageVersion: packageVersion,
275
+ skills: installedSkills,
276
+ });
170
277
  }
171
278
 
172
279
  /**