@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 +3 -3
- package/dist/{chunk-BQZQDOXI.js → chunk-CXTJOYWM.js} +224 -110
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin/.claude-plugin/plugin.json +1 -1
- package/dist/plugin/.cursor-plugin/plugin.json +1 -1
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.cursor-plugin/plugin.json +1 -1
- package/scripts/postinstall.mjs +110 -3
package/README.md
CHANGED
|
@@ -375,8 +375,8 @@ Data directory structure (~/.muggle-ai/)
|
|
|
375
375
|
|
|
376
376
|
```
|
|
377
377
|
~/.muggle-ai/
|
|
378
|
-
├──
|
|
379
|
-
├──
|
|
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/
|
|
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
|
|
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 === "
|
|
243
|
-
return
|
|
242
|
+
if (runtimeTarget === "production") {
|
|
243
|
+
return DEFAULT_AUTH0_PRODUCTION_CLIENT_ID;
|
|
244
244
|
}
|
|
245
|
-
return
|
|
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
|
-
|
|
288
|
-
|
|
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
|
|
614
|
-
|
|
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.
|
|
622
|
+
this.oauthSessionFilePath = config.localQa.oauthSessionFilePath;
|
|
623
623
|
this.pendingDeviceCodePath = path2.join(
|
|
624
|
-
path2.dirname(config.localQa.
|
|
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.
|
|
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.
|
|
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.
|
|
949
|
+
if (!fs3.existsSync(this.oauthSessionFilePath)) {
|
|
950
950
|
return null;
|
|
951
951
|
}
|
|
952
952
|
try {
|
|
953
|
-
const content = fs3.readFileSync(this.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
2384
|
-
function
|
|
2385
|
-
return path2.join(getDataDir(),
|
|
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
|
|
2393
|
+
function loadApiKeyData() {
|
|
2394
2394
|
const logger14 = getLogger();
|
|
2395
|
-
const
|
|
2395
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
2396
2396
|
try {
|
|
2397
|
-
if (!fs3.existsSync(
|
|
2398
|
-
logger14.debug("No
|
|
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(
|
|
2402
|
-
const
|
|
2403
|
-
|
|
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
|
|
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
|
|
2411
|
+
function saveApiKeyData(data) {
|
|
2416
2412
|
const logger14 = getLogger();
|
|
2417
|
-
const
|
|
2413
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
2418
2414
|
try {
|
|
2419
2415
|
ensureDataDir();
|
|
2420
|
-
const content = JSON.stringify(
|
|
2421
|
-
fs3.writeFileSync(
|
|
2422
|
-
logger14.info("
|
|
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
|
|
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
|
|
2426
|
+
function deleteApiKeyData() {
|
|
2431
2427
|
const logger14 = getLogger();
|
|
2432
|
-
const
|
|
2428
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
2433
2429
|
try {
|
|
2434
|
-
if (fs3.existsSync(
|
|
2435
|
-
fs3.unlinkSync(
|
|
2436
|
-
logger14.info("
|
|
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
|
|
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
|
|
2445
|
-
const
|
|
2446
|
-
|
|
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 (
|
|
2456
|
-
return
|
|
2445
|
+
if (data.apiKey) {
|
|
2446
|
+
return data;
|
|
2457
2447
|
}
|
|
2458
2448
|
return null;
|
|
2459
2449
|
}
|
|
2460
2450
|
function hasApiKey() {
|
|
2461
|
-
const
|
|
2462
|
-
return !!
|
|
2451
|
+
const data = loadApiKeyData();
|
|
2452
|
+
return !!data?.apiKey;
|
|
2463
2453
|
}
|
|
2464
2454
|
function getApiKey() {
|
|
2465
|
-
const
|
|
2466
|
-
return
|
|
2455
|
+
const data = loadApiKeyData();
|
|
2456
|
+
return data?.apiKey ?? null;
|
|
2467
2457
|
}
|
|
2468
2458
|
function saveApiKey(params) {
|
|
2469
2459
|
const logger14 = getLogger();
|
|
2470
|
-
const
|
|
2460
|
+
const apiKeyPath = getApiKeyFilePath();
|
|
2471
2461
|
try {
|
|
2472
2462
|
ensureDataDir();
|
|
2473
|
-
const
|
|
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(
|
|
2480
|
-
fs3.writeFileSync(
|
|
2481
|
-
logger14.info("API key saved", { path:
|
|
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
|
-
|
|
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
|
-
|
|
2729
|
+
deleteApiKeyData();
|
|
2735
2730
|
logger4.info("[Auth] Logged out successfully");
|
|
2736
2731
|
}
|
|
2737
2732
|
function getCallerCredentials() {
|
|
2738
|
-
const
|
|
2739
|
-
if (
|
|
2740
|
-
return { 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
|
|
2751
|
-
if (
|
|
2752
|
-
return { 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
|
-
|
|
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
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
|
|
6044
|
-
|
|
6045
|
-
|
|
6046
|
-
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
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(
|
|
6056
|
-
|
|
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
|
-
|
|
6061
|
-
|
|
6062
|
-
console.log(
|
|
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
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
|
|
6068
|
-
|
|
6069
|
-
|
|
6070
|
-
|
|
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:
|
|
6074
|
-
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
|
-
`
|
|
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("
|
|
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
|
|
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
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5
|
+
"version": "4.2.0",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Muggle AI",
|
|
8
8
|
"email": "support@muggle-ai.com"
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
+
// Write updated manifest
|
|
273
|
+
writeInstallManifest({
|
|
274
|
+
packageVersion: packageVersion,
|
|
275
|
+
skills: installedSkills,
|
|
276
|
+
});
|
|
170
277
|
}
|
|
171
278
|
|
|
172
279
|
/**
|