@muggleai/works 4.5.0 → 4.6.1

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.
@@ -0,0 +1,2176 @@
1
+ import { __export, getLogger, getConfig, createChildLogger, buildElectronAppReleaseAssetUrl, getAuthService, hasApiKey, getElectronAppVersion, getElectronAppDir, getPlatformKey, isElectronAppInstalled, getElectronAppChecksums, getChecksumForPlatform, verifyFileChecksum, calculateFileChecksum, getQaTools, getLocalQaTools, performLogout, performLogin, toolRequiresAuth, getCallerCredentials, getDataDir, getBundledElectronAppVersion, getElectronAppVersionSource, getCredentialsFilePath, buildElectronAppChecksumsUrl, __require } from './chunk-HDEZDEM6.js';
2
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
+ import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
+ import { v4 } from 'uuid';
5
+ import { z, ZodError } from 'zod';
6
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
+ import * as fs from 'fs';
8
+ import { readFileSync, existsSync, rmSync, mkdirSync, readdirSync, createWriteStream, writeFileSync, statSync } from 'fs';
9
+ import * as path from 'path';
10
+ import { dirname, resolve, join } from 'path';
11
+ import { fileURLToPath } from 'url';
12
+ import { Command } from 'commander';
13
+ import axios from 'axios';
14
+ import { platform, arch, homedir } from 'os';
15
+ import { execFile } from 'child_process';
16
+ import { pipeline } from 'stream/promises';
17
+
18
+ var registeredTools = [];
19
+ function registerTools(tools) {
20
+ registeredTools = [...registeredTools, ...tools];
21
+ }
22
+ function getAllTools() {
23
+ return registeredTools;
24
+ }
25
+ function clearTools() {
26
+ registeredTools = [];
27
+ }
28
+ function zodToJsonSchema(schema) {
29
+ try {
30
+ if (schema && typeof schema === "object" && "safeParse" in schema) {
31
+ return z.toJSONSchema(schema);
32
+ }
33
+ } catch {
34
+ }
35
+ try {
36
+ const zodSchema = schema;
37
+ if (zodSchema._def) {
38
+ return convertZodDef(zodSchema);
39
+ }
40
+ } catch {
41
+ }
42
+ return {
43
+ type: "object",
44
+ properties: {},
45
+ additionalProperties: true
46
+ };
47
+ }
48
+ function convertZodDef(schema) {
49
+ const zodSchema = schema;
50
+ if (!zodSchema._def) {
51
+ return { type: "object" };
52
+ }
53
+ const def = zodSchema._def;
54
+ const typeName = def.typeName ?? def.type;
55
+ switch (typeName) {
56
+ case "ZodObject":
57
+ case "object": {
58
+ const shapeFromDef = typeof def.shape === "function" ? def.shape() : def.shape;
59
+ const shape = shapeFromDef || zodSchema.shape || {};
60
+ const properties = {};
61
+ const required = [];
62
+ for (const [key, value] of Object.entries(shape)) {
63
+ properties[key] = convertZodDef(value);
64
+ const valueDef = value._def;
65
+ if (valueDef?.typeName !== "ZodOptional") {
66
+ required.push(key);
67
+ }
68
+ }
69
+ const result = {
70
+ type: "object",
71
+ properties
72
+ };
73
+ if (required.length > 0) {
74
+ result.required = required;
75
+ }
76
+ return result;
77
+ }
78
+ case "ZodString":
79
+ case "string": {
80
+ const result = { type: "string" };
81
+ if (def.description) result.description = def.description;
82
+ if (def.checks) {
83
+ for (const check of def.checks) {
84
+ if (check.kind === "min") result.minLength = check.value;
85
+ if (check.kind === "max") result.maxLength = check.value;
86
+ if (check.kind === "url") result.format = "uri";
87
+ if (check.kind === "email") result.format = "email";
88
+ }
89
+ }
90
+ return result;
91
+ }
92
+ case "ZodNumber":
93
+ case "number": {
94
+ const result = { type: "number" };
95
+ if (def.description) result.description = def.description;
96
+ if (def.checks) {
97
+ for (const check of def.checks) {
98
+ if (check.kind === "int") result.type = "integer";
99
+ if (check.kind === "min") result.minimum = check.value;
100
+ if (check.kind === "max") result.maximum = check.value;
101
+ }
102
+ }
103
+ return result;
104
+ }
105
+ case "ZodBoolean":
106
+ case "boolean": {
107
+ const result = { type: "boolean" };
108
+ if (def.description) result.description = def.description;
109
+ return result;
110
+ }
111
+ case "ZodArray":
112
+ case "array": {
113
+ const result = {
114
+ type: "array",
115
+ items: def.innerType ? convertZodDef(def.innerType) : def.element ? convertZodDef(def.element) : {}
116
+ };
117
+ if (def.description) result.description = def.description;
118
+ return result;
119
+ }
120
+ case "ZodEnum":
121
+ case "enum": {
122
+ const enumValues = Array.isArray(def.values) ? def.values : def.values ? Object.values(def.values) : [];
123
+ const result = {
124
+ type: "string",
125
+ enum: enumValues
126
+ };
127
+ if (def.description) result.description = def.description;
128
+ return result;
129
+ }
130
+ case "ZodOptional":
131
+ case "optional": {
132
+ const inner = def.innerType ? convertZodDef(def.innerType) : {};
133
+ if (def.description) inner.description = def.description;
134
+ return inner;
135
+ }
136
+ case "ZodUnion": {
137
+ const options = def.options || [];
138
+ return {
139
+ oneOf: options.map((opt) => convertZodDef(opt))
140
+ };
141
+ }
142
+ default:
143
+ return { type: "object" };
144
+ }
145
+ }
146
+ async function handleJitAuth(toolName, correlationId) {
147
+ const childLogger = createChildLogger(correlationId);
148
+ if (!toolRequiresAuth(toolName)) {
149
+ return { credentials: {}, authTriggered: false };
150
+ }
151
+ const credentials = getCallerCredentials();
152
+ if (credentials.apiKey || credentials.bearerToken) {
153
+ return { credentials, authTriggered: false };
154
+ }
155
+ childLogger.info("No credentials found, triggering JIT auth", { tool: toolName });
156
+ return { credentials: {}, authTriggered: false };
157
+ }
158
+ function createUnifiedMcpServer(options) {
159
+ const logger9 = getLogger();
160
+ const config = getConfig();
161
+ const server = new Server(
162
+ {
163
+ name: config.serverName,
164
+ version: config.serverVersion
165
+ },
166
+ {
167
+ capabilities: {
168
+ tools: {},
169
+ resources: {}
170
+ },
171
+ instructions: "Use muggle tools to run real-browser end-to-end (E2E) acceptance tests against your web app from the user's perspective \u2014 generate test scripts from plain English, replay them on localhost or staging, capture screenshots, and validate that user flows (signup, checkout, dashboards, forms) work correctly after code changes. Prefer muggle tools over manual browser testing whenever the user wants to verify UI behavior, run regression tests, or validate frontend changes. Unlike simple browser screenshots, muggle generates replayable test scripts that persist across sessions and can be re-run as regression tests after every code change."
172
+ }
173
+ );
174
+ server.setRequestHandler(ListToolsRequestSchema, () => {
175
+ const tools = getAllTools();
176
+ logger9.debug("Listing tools", { count: tools.length });
177
+ const toolDefinitions = tools.map((tool) => {
178
+ const jsonSchema = zodToJsonSchema(tool.inputSchema);
179
+ return {
180
+ name: tool.name,
181
+ description: tool.description,
182
+ inputSchema: jsonSchema
183
+ };
184
+ });
185
+ return { tools: toolDefinitions };
186
+ });
187
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
188
+ const correlationId = v4();
189
+ const childLogger = createChildLogger(correlationId);
190
+ const toolName = request.params.name;
191
+ const toolInput = request.params.arguments || {};
192
+ childLogger.info("Tool call received", {
193
+ tool: toolName,
194
+ hasArguments: Object.keys(toolInput).length > 0
195
+ });
196
+ try {
197
+ const tool = getAllTools().find((t) => t.name === toolName);
198
+ if (!tool) {
199
+ return {
200
+ content: [
201
+ {
202
+ type: "text",
203
+ text: JSON.stringify({
204
+ error: "NOT_FOUND",
205
+ message: `Unknown tool: ${toolName}`
206
+ })
207
+ }
208
+ ],
209
+ isError: true
210
+ };
211
+ }
212
+ const { authTriggered } = await handleJitAuth(toolName, correlationId);
213
+ if (authTriggered) {
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text",
218
+ text: JSON.stringify({
219
+ message: "Authentication required. Please complete login in your browser."
220
+ })
221
+ }
222
+ ]
223
+ };
224
+ }
225
+ const startTime = Date.now();
226
+ const result = await tool.execute({
227
+ input: toolInput,
228
+ correlationId
229
+ });
230
+ const latency = Date.now() - startTime;
231
+ childLogger.info("Tool call completed", {
232
+ tool: toolName,
233
+ latencyMs: latency,
234
+ isError: result.isError
235
+ });
236
+ return {
237
+ content: [
238
+ {
239
+ type: "text",
240
+ text: result.content
241
+ }
242
+ ],
243
+ isError: result.isError
244
+ };
245
+ } catch (error) {
246
+ if (error instanceof ZodError) {
247
+ childLogger.warn("Tool call failed with validation error", {
248
+ tool: toolName,
249
+ errors: error.issues
250
+ });
251
+ const issueMessages = error.issues.slice(0, 3).map((issue) => {
252
+ const path6 = issue.path.join(".");
253
+ return path6 ? `'${path6}': ${issue.message}` : issue.message;
254
+ });
255
+ return {
256
+ content: [
257
+ {
258
+ type: "text",
259
+ text: JSON.stringify({
260
+ error: "INVALID_ARGUMENT",
261
+ message: `Invalid input: ${issueMessages.join("; ")}`
262
+ })
263
+ }
264
+ ],
265
+ isError: true
266
+ };
267
+ }
268
+ childLogger.error("Tool call failed with error", {
269
+ tool: toolName,
270
+ error: String(error)
271
+ });
272
+ return {
273
+ content: [
274
+ {
275
+ type: "text",
276
+ text: JSON.stringify({
277
+ error: "INTERNAL_ERROR",
278
+ message: error instanceof Error ? error.message : "An unexpected error occurred"
279
+ })
280
+ }
281
+ ],
282
+ isError: true
283
+ };
284
+ }
285
+ });
286
+ server.setRequestHandler(ListResourcesRequestSchema, () => {
287
+ logger9.debug("Listing resources");
288
+ return { resources: [] };
289
+ });
290
+ server.setRequestHandler(ReadResourceRequestSchema, (request) => {
291
+ const uri = request.params.uri;
292
+ logger9.debug("Reading resource", { uri });
293
+ return {
294
+ contents: [
295
+ {
296
+ uri,
297
+ mimeType: "text/plain",
298
+ text: `Resource not found: ${uri}`
299
+ }
300
+ ]
301
+ };
302
+ });
303
+ logger9.info("Unified MCP server configured", {
304
+ tools: getAllTools().length,
305
+ enableQaTools: options.enableQaTools,
306
+ enableLocalTools: options.enableLocalTools
307
+ });
308
+ return server;
309
+ }
310
+
311
+ // src/server/index.ts
312
+ var server_exports = {};
313
+ __export(server_exports, {
314
+ clearTools: () => clearTools,
315
+ createUnifiedMcpServer: () => createUnifiedMcpServer,
316
+ getAllTools: () => getAllTools,
317
+ registerTools: () => registerTools,
318
+ startStdioServer: () => startStdioServer
319
+ });
320
+ var logger = getLogger();
321
+ async function startStdioServer(server) {
322
+ logger.info("Starting stdio server transport");
323
+ const transport = new StdioServerTransport();
324
+ await server.connect(transport);
325
+ logger.info("Stdio server connected");
326
+ const shutdown = (signal) => {
327
+ logger.info(`Received ${signal}, shutting down...`);
328
+ process.exit(0);
329
+ };
330
+ process.on("SIGTERM", () => shutdown("SIGTERM"));
331
+ process.on("SIGINT", () => shutdown("SIGINT"));
332
+ }
333
+
334
+ // src/cli/pr-section/render.ts
335
+ var DASHBOARD_URL_BASE = "https://www.muggle-ai.com/muggleTestV0/dashboard/projects";
336
+ var DETAIL_IMAGE_WIDTH = 720;
337
+ function countTests(report) {
338
+ const total = report.tests.length;
339
+ const passed = report.tests.filter((t) => t.status === "passed").length;
340
+ const failed = report.tests.filter((t) => t.status === "failed").length;
341
+ return { total, passed, failed };
342
+ }
343
+ function statusEmoji(test) {
344
+ return test.status === "passed" ? "\u2705" : "\u274C";
345
+ }
346
+ function endingScreenshot(test) {
347
+ if (test.steps.length === 0) {
348
+ return null;
349
+ }
350
+ if (test.status === "failed") {
351
+ const failStep = test.steps.find((s) => s.stepIndex === test.failureStepIndex);
352
+ if (failStep) {
353
+ return failStep;
354
+ }
355
+ return test.steps[test.steps.length - 1];
356
+ }
357
+ return test.steps[test.steps.length - 1];
358
+ }
359
+ function fullSizeImage(url, alt) {
360
+ return `<a href="${url}"><img src="${url}" width="${DETAIL_IMAGE_WIDTH}" alt="${alt}"></a>`;
361
+ }
362
+ function safeInlineCode(s) {
363
+ return s.replace(/`/g, "\u2018");
364
+ }
365
+ function renderOverview(report) {
366
+ const { total, passed, failed } = countTests(report);
367
+ const lines = [
368
+ "## E2E Acceptance Results",
369
+ "",
370
+ `**${total} tests ran \u2014 ${passed} passed / ${failed} failed**`
371
+ ];
372
+ if (total === 0) {
373
+ lines.push("", "_No tests were executed._");
374
+ return lines.join("\n");
375
+ }
376
+ lines.push("", "**Tests run:**");
377
+ const anyGrouped = report.tests.some((t) => Boolean(t.useCaseName));
378
+ if (!anyGrouped) {
379
+ for (const t of report.tests) {
380
+ lines.push(`- ${statusEmoji(t)} ${t.name}`);
381
+ }
382
+ return lines.join("\n");
383
+ }
384
+ const groupOrder = [];
385
+ const groups = /* @__PURE__ */ new Map();
386
+ const flat = [];
387
+ const seenGroups = /* @__PURE__ */ new Set();
388
+ for (const t of report.tests) {
389
+ if (t.useCaseName) {
390
+ if (!groups.has(t.useCaseName)) {
391
+ groups.set(t.useCaseName, []);
392
+ groupOrder.push(t.useCaseName);
393
+ }
394
+ groups.get(t.useCaseName).push(t);
395
+ if (!seenGroups.has(t.useCaseName)) {
396
+ seenGroups.add(t.useCaseName);
397
+ flat.push({ type: "group", key: t.useCaseName });
398
+ }
399
+ } else {
400
+ flat.push({ type: "test", test: t });
401
+ }
402
+ }
403
+ for (const entry of flat) {
404
+ if (entry.type === "test") {
405
+ lines.push(`- ${statusEmoji(entry.test)} ${entry.test.name}`);
406
+ } else {
407
+ lines.push(`- **${entry.key}**`);
408
+ for (const t of groups.get(entry.key)) {
409
+ lines.push(` - ${statusEmoji(t)} ${t.name}`);
410
+ }
411
+ }
412
+ }
413
+ return lines.join("\n");
414
+ }
415
+ function renderTestDetails(test, projectId) {
416
+ const summary = renderSummaryLine(test);
417
+ const image = renderEndingImage(test);
418
+ const resultLines = renderResultSummary(test, projectId);
419
+ const body = ["", "<br>", ""];
420
+ if (image) {
421
+ body.push(image, "");
422
+ }
423
+ body.push(...resultLines);
424
+ return `<details>
425
+ <summary>${summary}</summary>
426
+ ${body.join("\n")}
427
+
428
+ </details>`;
429
+ }
430
+ function renderSummaryLine(test) {
431
+ const base = `${statusEmoji(test)} <b>${test.name}</b>`;
432
+ const tail = " <i>\u25B6 click to expand</i>";
433
+ if (test.description) {
434
+ return `${base} \u2014 ${test.description}${tail}`;
435
+ }
436
+ return `${base}${tail}`;
437
+ }
438
+ function renderEndingImage(test) {
439
+ const step = endingScreenshot(test);
440
+ if (!step) {
441
+ return null;
442
+ }
443
+ return fullSizeImage(step.screenshotUrl, test.name);
444
+ }
445
+ function renderResultSummary(test, projectId) {
446
+ const dashboardUrl = `${DASHBOARD_URL_BASE}/${projectId}/scripts?modal=script-details&testCaseId=${encodeURIComponent(test.testCaseId)}`;
447
+ const lines = [];
448
+ if (test.status === "passed") {
449
+ lines.push(`**Result:** \u2705 PASSED`);
450
+ } else {
451
+ lines.push(`**Result:** \u274C FAILED at step ${test.failureStepIndex}`);
452
+ lines.push(`**Error:** \`${safeInlineCode(test.error)}\``);
453
+ }
454
+ lines.push(`**Steps:** ${test.steps.length}`);
455
+ lines.push(`[View on Muggle AI dashboard \u2192](${dashboardUrl})`);
456
+ return lines;
457
+ }
458
+ function renderBody(report, opts) {
459
+ const overview = renderOverview(report);
460
+ if (report.tests.length === 0) {
461
+ return overview;
462
+ }
463
+ if (!opts.inlineDetails) {
464
+ return [
465
+ overview,
466
+ "",
467
+ "---",
468
+ "",
469
+ "_Full per-test details in the comment below \u2014 the PR description was too large to inline them._"
470
+ ].join("\n");
471
+ }
472
+ const detailBlocks = report.tests.map((t) => renderTestDetails(t, report.projectId));
473
+ return [
474
+ overview,
475
+ "",
476
+ "---",
477
+ "",
478
+ detailBlocks.join("\n\n")
479
+ ].join("\n");
480
+ }
481
+ function renderComment(report) {
482
+ if (report.tests.length === 0) {
483
+ return "";
484
+ }
485
+ const detailBlocks = report.tests.map((t) => renderTestDetails(t, report.projectId));
486
+ return [
487
+ "## E2E acceptance evidence (overflow)",
488
+ "",
489
+ "_This comment was posted because the full per-test details did not fit in the PR description._",
490
+ "",
491
+ detailBlocks.join("\n\n")
492
+ ].join("\n");
493
+ }
494
+
495
+ // src/cli/pr-section/overflow.ts
496
+ function splitWithOverflow(report, opts) {
497
+ const inlineBody = renderBody(report, { inlineDetails: true });
498
+ const inlineBytes = Buffer.byteLength(inlineBody, "utf-8");
499
+ if (inlineBytes <= opts.maxBodyBytes) {
500
+ return { body: inlineBody, comment: null };
501
+ }
502
+ if (report.tests.length === 0) {
503
+ return { body: inlineBody, comment: null };
504
+ }
505
+ const spilledBody = renderBody(report, { inlineDetails: false });
506
+ const comment = renderComment(report);
507
+ return {
508
+ body: spilledBody,
509
+ comment: comment.length > 0 ? comment : null
510
+ };
511
+ }
512
+ var StepSchema = z.object({
513
+ stepIndex: z.number().int().nonnegative(),
514
+ action: z.string().min(1),
515
+ screenshotUrl: z.string().url()
516
+ });
517
+ var PassedTestSchema = z.object({
518
+ name: z.string().min(1),
519
+ testCaseId: z.string().min(1),
520
+ testScriptId: z.string().min(1).optional(),
521
+ runId: z.string().min(1),
522
+ viewUrl: z.string().url(),
523
+ status: z.literal("passed"),
524
+ steps: z.array(StepSchema),
525
+ useCaseName: z.string().min(1).optional(),
526
+ description: z.string().min(1).optional()
527
+ });
528
+ var FailedTestSchema = z.object({
529
+ name: z.string().min(1),
530
+ testCaseId: z.string().min(1),
531
+ testScriptId: z.string().min(1).optional(),
532
+ runId: z.string().min(1),
533
+ viewUrl: z.string().url(),
534
+ status: z.literal("failed"),
535
+ steps: z.array(StepSchema),
536
+ failureStepIndex: z.number().int().nonnegative(),
537
+ error: z.string().min(1),
538
+ artifactsDir: z.string().min(1).optional(),
539
+ useCaseName: z.string().min(1).optional(),
540
+ description: z.string().min(1).optional()
541
+ });
542
+ var TestResultSchema = z.discriminatedUnion("status", [
543
+ PassedTestSchema,
544
+ FailedTestSchema
545
+ ]);
546
+ var E2eReportSchema = z.object({
547
+ projectId: z.string().min(1),
548
+ tests: z.array(TestResultSchema)
549
+ });
550
+
551
+ // src/cli/pr-section/index.ts
552
+ function buildPrSection(report, opts) {
553
+ return splitWithOverflow(report, opts);
554
+ }
555
+
556
+ // src/cli/pr-section/resolve-urls-types.ts
557
+ var GS_SCHEME = "gs://";
558
+ var PUBLIC_URL_PATH = "/v1/protected/storage/publicUrl";
559
+ var RESOLVE_TIMEOUT_MS = 1e4;
560
+ var LOG_PREFIX = "build-pr-section";
561
+
562
+ // src/cli/pr-section/resolve-urls.ts
563
+ function errMsg(e) {
564
+ return e instanceof Error ? e.message : String(e);
565
+ }
566
+ function isGsUrl(url) {
567
+ return url.startsWith(GS_SCHEME);
568
+ }
569
+ function buildAuthHeaders(credentials) {
570
+ const headers = {};
571
+ if (credentials.bearerToken) {
572
+ headers["Authorization"] = credentials.bearerToken.startsWith("Bearer ") ? credentials.bearerToken : `Bearer ${credentials.bearerToken}`;
573
+ }
574
+ if (credentials.apiKey) {
575
+ headers["x-api-key"] = credentials.apiKey;
576
+ }
577
+ return headers;
578
+ }
579
+ function collectGsUrls(report) {
580
+ const seen = /* @__PURE__ */ new Set();
581
+ for (const test of report.tests) {
582
+ for (const step of test.steps) {
583
+ if (isGsUrl(step.screenshotUrl)) {
584
+ seen.add(step.screenshotUrl);
585
+ }
586
+ }
587
+ }
588
+ return Array.from(seen);
589
+ }
590
+ async function resolveOne(gsUrl, baseUrl, headers, stderrWrite) {
591
+ try {
592
+ const response = await axios.post(
593
+ `${baseUrl}${PUBLIC_URL_PATH}`,
594
+ { resourceUrl: gsUrl },
595
+ {
596
+ headers: { ...headers, "Content-Type": "application/json" },
597
+ timeout: RESOLVE_TIMEOUT_MS,
598
+ validateStatus: () => true
599
+ }
600
+ );
601
+ if (response.status === 200 && response.data && typeof response.data.resourceUrl === "string" && response.data.resourceUrl.length > 0) {
602
+ return response.data.resourceUrl;
603
+ }
604
+ const reason = response.status !== 200 ? `HTTP ${response.status}` : "missing resourceUrl in response";
605
+ stderrWrite(`${LOG_PREFIX}: failed to resolve ${gsUrl}: ${reason}
606
+ `);
607
+ return null;
608
+ } catch (err) {
609
+ stderrWrite(`${LOG_PREFIX}: failed to resolve ${gsUrl}: ${errMsg(err)}
610
+ `);
611
+ return null;
612
+ }
613
+ }
614
+ function remapStep(step, urlMap) {
615
+ const resolved = urlMap.get(step.screenshotUrl);
616
+ if (!resolved) {
617
+ return step;
618
+ }
619
+ return { ...step, screenshotUrl: resolved };
620
+ }
621
+ function remapTest(test, urlMap) {
622
+ return { ...test, steps: test.steps.map((s) => remapStep(s, urlMap)) };
623
+ }
624
+ async function resolveGsScreenshotUrls(report, opts) {
625
+ const { stderrWrite } = opts;
626
+ try {
627
+ const gsUrls = collectGsUrls(report);
628
+ if (gsUrls.length === 0) {
629
+ return report;
630
+ }
631
+ const mcps = await import('./src-TX2KXI26.js');
632
+ const credentials = await mcps.getCallerCredentialsAsync();
633
+ if (!credentials.bearerToken && !credentials.apiKey) {
634
+ stderrWrite(
635
+ `${LOG_PREFIX}: no credentials available; run 'muggle login' to enable automatic gs:// URL resolution. Screenshots will render as broken images in GitHub.
636
+ `
637
+ );
638
+ return report;
639
+ }
640
+ const baseUrl = mcps.getConfig().e2e.promptServiceBaseUrl;
641
+ const headers = buildAuthHeaders(credentials);
642
+ const resolved = await Promise.all(
643
+ gsUrls.map((gsUrl) => resolveOne(gsUrl, baseUrl, headers, stderrWrite))
644
+ );
645
+ const urlMap = /* @__PURE__ */ new Map();
646
+ let failureCount = 0;
647
+ for (let i = 0; i < gsUrls.length; i++) {
648
+ const https = resolved[i];
649
+ if (https) {
650
+ urlMap.set(gsUrls[i], https);
651
+ } else {
652
+ failureCount++;
653
+ }
654
+ }
655
+ if (failureCount > 0) {
656
+ stderrWrite(
657
+ `${LOG_PREFIX}: ${failureCount}/${gsUrls.length} gs:// URLs could not be resolved; those screenshots will render as broken images in GitHub
658
+ `
659
+ );
660
+ }
661
+ if (urlMap.size === 0) {
662
+ return report;
663
+ }
664
+ return {
665
+ ...report,
666
+ tests: report.tests.map((t) => remapTest(t, urlMap))
667
+ };
668
+ } catch (err) {
669
+ stderrWrite(
670
+ `${LOG_PREFIX}: unexpected error while resolving gs:// URLs: ${errMsg(err)}; continuing with original report
671
+ `
672
+ );
673
+ return report;
674
+ }
675
+ }
676
+
677
+ // src/cli/build-pr-section.ts
678
+ var DEFAULT_MAX_BODY_BYTES = 6e4;
679
+ async function readAll(stream) {
680
+ const chunks = [];
681
+ for await (const chunk of stream) {
682
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
683
+ }
684
+ return Buffer.concat(chunks).toString("utf-8");
685
+ }
686
+ function errMsg2(e) {
687
+ return e instanceof Error ? e.message : String(e);
688
+ }
689
+ async function runBuildPrSection(opts) {
690
+ let raw;
691
+ try {
692
+ raw = await readAll(opts.stdin);
693
+ } catch (err) {
694
+ opts.stderrWrite(`build-pr-section: failed to read stdin: ${errMsg2(err)}
695
+ `);
696
+ return 1;
697
+ }
698
+ let json;
699
+ try {
700
+ json = JSON.parse(raw);
701
+ } catch (err) {
702
+ opts.stderrWrite(`build-pr-section: failed to parse stdin as JSON: ${errMsg2(err)}
703
+ `);
704
+ return 1;
705
+ }
706
+ let report;
707
+ try {
708
+ report = E2eReportSchema.parse(json);
709
+ } catch (err) {
710
+ if (err instanceof ZodError) {
711
+ opts.stderrWrite(
712
+ `build-pr-section: report validation failed:
713
+ ${err.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}
714
+ `
715
+ );
716
+ } else {
717
+ opts.stderrWrite(`build-pr-section: report validation failed: ${errMsg2(err)}
718
+ `);
719
+ }
720
+ return 1;
721
+ }
722
+ const resolvedReport = await resolveGsScreenshotUrls(report, { stderrWrite: opts.stderrWrite });
723
+ const result = buildPrSection(resolvedReport, { maxBodyBytes: opts.maxBodyBytes });
724
+ opts.stdoutWrite(JSON.stringify({ body: result.body, comment: result.comment }));
725
+ return 0;
726
+ }
727
+ async function buildPrSectionCommand(options) {
728
+ const maxBodyBytes = options.maxBodyBytes ? Number(options.maxBodyBytes) : DEFAULT_MAX_BODY_BYTES;
729
+ if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0) {
730
+ process.stderr.write(`build-pr-section: --max-body-bytes must be a positive number
731
+ `);
732
+ process.exitCode = 1;
733
+ return;
734
+ }
735
+ const code = await runBuildPrSection({
736
+ stdin: process.stdin,
737
+ stdoutWrite: (s) => process.stdout.write(s),
738
+ stderrWrite: (s) => process.stderr.write(s),
739
+ maxBodyBytes
740
+ });
741
+ if (code !== 0) {
742
+ process.exitCode = code;
743
+ }
744
+ }
745
+ var logger2 = getLogger();
746
+ var ELECTRON_APP_DIR = "electron-app";
747
+ var CURSOR_SKILLS_DIR = ".cursor";
748
+ var CURSOR_SKILLS_SUBDIR = "skills";
749
+ var MUGGLE_SKILL_PREFIX = "muggle";
750
+ var INSTALL_MANIFEST_FILE = "install-manifest.json";
751
+ function getElectronAppBaseDir() {
752
+ return path.join(getDataDir(), ELECTRON_APP_DIR);
753
+ }
754
+ function getCursorSkillsDir() {
755
+ return path.join(homedir(), CURSOR_SKILLS_DIR, CURSOR_SKILLS_SUBDIR);
756
+ }
757
+ function getInstallManifestPath() {
758
+ return path.join(getDataDir(), INSTALL_MANIFEST_FILE);
759
+ }
760
+ function readInstallManifest() {
761
+ const manifestPath = getInstallManifestPath();
762
+ if (!existsSync(manifestPath)) {
763
+ return null;
764
+ }
765
+ try {
766
+ const content = readFileSync(manifestPath, "utf-8");
767
+ const manifest = JSON.parse(content);
768
+ if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
769
+ return null;
770
+ }
771
+ return manifest;
772
+ } catch {
773
+ return null;
774
+ }
775
+ }
776
+ function listObsoleteSkills() {
777
+ const skillsDir = getCursorSkillsDir();
778
+ const manifest = readInstallManifest();
779
+ const obsoleteSkills = [];
780
+ if (!existsSync(skillsDir)) {
781
+ return obsoleteSkills;
782
+ }
783
+ const manifestSkills = new Set(manifest?.skills ?? []);
784
+ try {
785
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
786
+ for (const entry of entries) {
787
+ if (!entry.isDirectory()) {
788
+ continue;
789
+ }
790
+ if (!entry.name.startsWith(MUGGLE_SKILL_PREFIX)) {
791
+ continue;
792
+ }
793
+ if (manifestSkills.has(entry.name)) {
794
+ continue;
795
+ }
796
+ const skillPath = path.join(skillsDir, entry.name);
797
+ const sizeBytes = getDirectorySize(skillPath);
798
+ obsoleteSkills.push({
799
+ name: entry.name,
800
+ path: skillPath,
801
+ sizeBytes
802
+ });
803
+ }
804
+ } catch (error) {
805
+ const errorMessage = error instanceof Error ? error.message : String(error);
806
+ logger2.warn("Failed to list obsolete skills", { error: errorMessage });
807
+ }
808
+ return obsoleteSkills;
809
+ }
810
+ function cleanupObsoleteSkills(options = {}) {
811
+ const { dryRun = false } = options;
812
+ const obsoleteSkills = listObsoleteSkills();
813
+ const removed = [];
814
+ let freedBytes = 0;
815
+ for (const skill of obsoleteSkills) {
816
+ if (!dryRun) {
817
+ try {
818
+ rmSync(skill.path, { recursive: true, force: true });
819
+ logger2.info("Removed obsolete skill", {
820
+ skill: skill.name,
821
+ freedBytes: skill.sizeBytes
822
+ });
823
+ } catch (error) {
824
+ const errorMessage = error instanceof Error ? error.message : String(error);
825
+ logger2.error("Failed to remove skill", {
826
+ skill: skill.name,
827
+ error: errorMessage
828
+ });
829
+ continue;
830
+ }
831
+ }
832
+ removed.push(skill);
833
+ freedBytes += skill.sizeBytes;
834
+ }
835
+ return { removed, freedBytes };
836
+ }
837
+ function getDirectorySize(dirPath) {
838
+ let totalSize = 0;
839
+ try {
840
+ const entries = readdirSync(dirPath, { withFileTypes: true });
841
+ for (const entry of entries) {
842
+ const fullPath = path.join(dirPath, entry.name);
843
+ if (entry.isDirectory()) {
844
+ totalSize += getDirectorySize(fullPath);
845
+ } else if (entry.isFile()) {
846
+ try {
847
+ const stats = statSync(fullPath);
848
+ totalSize += stats.size;
849
+ } catch {
850
+ }
851
+ }
852
+ }
853
+ } catch {
854
+ }
855
+ return totalSize;
856
+ }
857
+ function formatBytes(bytes) {
858
+ if (bytes === 0) {
859
+ return "0 B";
860
+ }
861
+ const units = ["B", "KB", "MB", "GB"];
862
+ const k = 1024;
863
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
864
+ const size = bytes / Math.pow(k, i);
865
+ return `${size.toFixed(1)} ${units[i]}`;
866
+ }
867
+ function compareVersions(a, b) {
868
+ const partsA = a.split(".").map(Number);
869
+ const partsB = b.split(".").map(Number);
870
+ for (let i = 0; i < 3; i++) {
871
+ const partA = partsA[i] || 0;
872
+ const partB = partsB[i] || 0;
873
+ if (partA !== partB) {
874
+ return partA - partB;
875
+ }
876
+ }
877
+ return 0;
878
+ }
879
+ function listInstalledVersions() {
880
+ const baseDir = getElectronAppBaseDir();
881
+ const currentVersion = getElectronAppVersion();
882
+ const versions = [];
883
+ if (!existsSync(baseDir)) {
884
+ return versions;
885
+ }
886
+ try {
887
+ const entries = readdirSync(baseDir, { withFileTypes: true });
888
+ for (const entry of entries) {
889
+ if (!entry.isDirectory()) {
890
+ continue;
891
+ }
892
+ if (!/^\d+\.\d+\.\d+$/.test(entry.name)) {
893
+ continue;
894
+ }
895
+ const versionPath = path.join(baseDir, entry.name);
896
+ const sizeBytes = getDirectorySize(versionPath);
897
+ versions.push({
898
+ version: entry.name,
899
+ path: versionPath,
900
+ sizeBytes,
901
+ isCurrent: entry.name === currentVersion
902
+ });
903
+ }
904
+ } catch (error) {
905
+ const errorMessage = error instanceof Error ? error.message : String(error);
906
+ logger2.warn("Failed to list installed versions", { error: errorMessage });
907
+ }
908
+ versions.sort((a, b) => compareVersions(b.version, a.version));
909
+ return versions;
910
+ }
911
+ function cleanupOldVersions(options = {}) {
912
+ const { all = false, dryRun = false } = options;
913
+ const versions = listInstalledVersions();
914
+ const removed = [];
915
+ let freedBytes = 0;
916
+ const versionsToKeep = all ? 1 : 2;
917
+ let keptCount = 0;
918
+ for (const version of versions) {
919
+ if (version.isCurrent) {
920
+ keptCount++;
921
+ continue;
922
+ }
923
+ if (keptCount < versionsToKeep) {
924
+ keptCount++;
925
+ continue;
926
+ }
927
+ if (!dryRun) {
928
+ try {
929
+ rmSync(version.path, { recursive: true, force: true });
930
+ logger2.info("Removed old version", {
931
+ version: version.version,
932
+ freedBytes: version.sizeBytes
933
+ });
934
+ } catch (error) {
935
+ const errorMessage = error instanceof Error ? error.message : String(error);
936
+ logger2.error("Failed to remove version", {
937
+ version: version.version,
938
+ error: errorMessage
939
+ });
940
+ continue;
941
+ }
942
+ }
943
+ removed.push(version);
944
+ freedBytes += version.sizeBytes;
945
+ }
946
+ return { removed, freedBytes };
947
+ }
948
+ async function versionsCommand() {
949
+ console.log("\nInstalled Electron App Versions");
950
+ console.log("================================\n");
951
+ const versions = listInstalledVersions();
952
+ if (versions.length === 0) {
953
+ console.log("No versions installed.");
954
+ console.log("Run 'muggle setup' to download the Electron app.\n");
955
+ return;
956
+ }
957
+ let totalSize = 0;
958
+ for (const version of versions) {
959
+ const marker = version.isCurrent ? " (current)" : "";
960
+ const size = formatBytes(version.sizeBytes);
961
+ console.log(` v${version.version}${marker} - ${size}`);
962
+ totalSize += version.sizeBytes;
963
+ }
964
+ console.log("");
965
+ console.log(`Total: ${versions.length} version(s), ${formatBytes(totalSize)}`);
966
+ console.log("");
967
+ }
968
+ async function cleanupCommand(options) {
969
+ let totalFreedBytes = 0;
970
+ let totalRemovedCount = 0;
971
+ console.log("\nElectron App Cleanup");
972
+ console.log("====================\n");
973
+ const versions = listInstalledVersions();
974
+ if (versions.length === 0) {
975
+ console.log("No versions installed. Nothing to clean up.\n");
976
+ } else if (versions.length === 1) {
977
+ console.log("Only the current version is installed. Nothing to clean up.\n");
978
+ } else {
979
+ const currentVersion = versions.find((v) => v.isCurrent);
980
+ const oldVersions = versions.filter((v) => !v.isCurrent);
981
+ console.log(`Current version: v${currentVersion?.version ?? "unknown"}`);
982
+ console.log(`Old versions: ${oldVersions.length}`);
983
+ console.log("");
984
+ if (options.dryRun) {
985
+ console.log("Dry run - showing what would be deleted:\n");
986
+ }
987
+ const result = cleanupOldVersions(options);
988
+ if (result.removed.length === 0) {
989
+ if (options.all) {
990
+ console.log("No old versions to remove.\n");
991
+ } else {
992
+ console.log("Keeping one previous version for rollback.");
993
+ console.log("Use --all to remove all old versions.\n");
994
+ }
995
+ } else {
996
+ console.log(options.dryRun ? "Would remove:" : "Removed:");
997
+ for (const version of result.removed) {
998
+ console.log(` v${version.version} (${formatBytes(version.sizeBytes)})`);
999
+ }
1000
+ totalFreedBytes += result.freedBytes;
1001
+ totalRemovedCount += result.removed.length;
1002
+ console.log("");
1003
+ }
1004
+ }
1005
+ if (options.skills) {
1006
+ console.log("Skills Cleanup");
1007
+ console.log("==============\n");
1008
+ const obsoleteSkills = listObsoleteSkills();
1009
+ if (obsoleteSkills.length === 0) {
1010
+ console.log("No obsolete skills found. Nothing to clean up.\n");
1011
+ } else {
1012
+ console.log(`Found ${obsoleteSkills.length} obsolete skill(s):
1013
+ `);
1014
+ if (options.dryRun) {
1015
+ console.log("Dry run - showing what would be deleted:\n");
1016
+ }
1017
+ const skillResult = cleanupObsoleteSkills({ dryRun: options.dryRun });
1018
+ console.log(options.dryRun ? "Would remove:" : "Removed:");
1019
+ for (const skill of skillResult.removed) {
1020
+ console.log(` ${skill.name} (${formatBytes(skill.sizeBytes)})`);
1021
+ }
1022
+ totalFreedBytes += skillResult.freedBytes;
1023
+ totalRemovedCount += skillResult.removed.length;
1024
+ console.log("");
1025
+ }
1026
+ }
1027
+ if (totalRemovedCount > 0) {
1028
+ console.log(
1029
+ `${options.dryRun ? "Would free" : "Freed"}: ${formatBytes(totalFreedBytes)} total`
1030
+ );
1031
+ console.log("");
1032
+ if (options.dryRun) {
1033
+ console.log("Run without --dry-run to actually delete.\n");
1034
+ }
1035
+ }
1036
+ logger2.info("Cleanup completed", {
1037
+ removed: totalRemovedCount,
1038
+ freedBytes: totalFreedBytes,
1039
+ dryRun: options.dryRun,
1040
+ includeSkills: options.skills
1041
+ });
1042
+ }
1043
+ var logger3 = getLogger();
1044
+ function getCursorMcpConfigPath() {
1045
+ return join(homedir(), ".cursor", "mcp.json");
1046
+ }
1047
+ function getExpectedExecutablePath(versionDir) {
1048
+ const os = platform();
1049
+ switch (os) {
1050
+ case "darwin":
1051
+ return path.join(versionDir, "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
1052
+ case "win32":
1053
+ return path.join(versionDir, "MuggleAI.exe");
1054
+ case "linux":
1055
+ return path.join(versionDir, "MuggleAI");
1056
+ default:
1057
+ throw new Error(`Unsupported platform: ${os}`);
1058
+ }
1059
+ }
1060
+ function verifyElectronAppInstallation() {
1061
+ const version = getElectronAppVersion();
1062
+ const versionDir = getElectronAppDir(version);
1063
+ const executablePath = getExpectedExecutablePath(versionDir);
1064
+ const metadataPath = path.join(versionDir, ".install-metadata.json");
1065
+ const result = {
1066
+ valid: false,
1067
+ versionDir,
1068
+ executablePath,
1069
+ executableExists: false,
1070
+ executableIsFile: false,
1071
+ metadataExists: false,
1072
+ hasPartialArchive: false
1073
+ };
1074
+ if (!fs.existsSync(versionDir)) {
1075
+ result.errorDetail = "Version directory does not exist";
1076
+ return result;
1077
+ }
1078
+ const archivePatterns = ["MuggleAI-darwin", "MuggleAI-win32", "MuggleAI-linux"];
1079
+ try {
1080
+ const files = fs.readdirSync(versionDir);
1081
+ for (const file of files) {
1082
+ if (archivePatterns.some((pattern) => file.startsWith(pattern)) && (file.endsWith(".zip") || file.endsWith(".tar.gz"))) {
1083
+ result.hasPartialArchive = true;
1084
+ break;
1085
+ }
1086
+ }
1087
+ } catch {
1088
+ }
1089
+ result.executableExists = fs.existsSync(executablePath);
1090
+ if (!result.executableExists) {
1091
+ if (result.hasPartialArchive) {
1092
+ result.errorDetail = "Download incomplete: archive found but not extracted";
1093
+ } else {
1094
+ result.errorDetail = "Executable not found at expected path";
1095
+ }
1096
+ return result;
1097
+ }
1098
+ try {
1099
+ const stats = fs.statSync(executablePath);
1100
+ result.executableIsFile = stats.isFile();
1101
+ if (!result.executableIsFile) {
1102
+ result.errorDetail = "Executable path exists but is not a file";
1103
+ return result;
1104
+ }
1105
+ } catch {
1106
+ result.errorDetail = "Cannot stat executable (broken symlink?)";
1107
+ return result;
1108
+ }
1109
+ result.metadataExists = fs.existsSync(metadataPath);
1110
+ result.valid = true;
1111
+ return result;
1112
+ }
1113
+ function validateCursorMcpConfig() {
1114
+ const cursorMcpConfigPath = getCursorMcpConfigPath();
1115
+ if (!existsSync(cursorMcpConfigPath)) {
1116
+ return {
1117
+ passed: false,
1118
+ description: `Missing at ${cursorMcpConfigPath}`
1119
+ };
1120
+ }
1121
+ try {
1122
+ const rawCursorConfig = JSON.parse(
1123
+ readFileSync(cursorMcpConfigPath, "utf-8")
1124
+ );
1125
+ if (!rawCursorConfig.mcpServers) {
1126
+ return {
1127
+ passed: false,
1128
+ description: "Missing mcpServers key"
1129
+ };
1130
+ }
1131
+ const muggleServerConfig = rawCursorConfig.mcpServers.muggle;
1132
+ if (!muggleServerConfig) {
1133
+ return {
1134
+ passed: false,
1135
+ description: "Missing mcpServers.muggle entry"
1136
+ };
1137
+ }
1138
+ if (!Array.isArray(muggleServerConfig.args)) {
1139
+ return {
1140
+ passed: false,
1141
+ description: "mcpServers.muggle.args is not an array"
1142
+ };
1143
+ }
1144
+ const hasServeArgument = muggleServerConfig.args.includes("serve");
1145
+ if (!hasServeArgument) {
1146
+ return {
1147
+ passed: false,
1148
+ description: "mcpServers.muggle args does not include 'serve'"
1149
+ };
1150
+ }
1151
+ if (muggleServerConfig.command === "node") {
1152
+ const firstArgument = muggleServerConfig.args.at(0);
1153
+ if (!firstArgument) {
1154
+ return {
1155
+ passed: false,
1156
+ description: "mcpServers.muggle command is node but args[0] is missing"
1157
+ };
1158
+ }
1159
+ if (!existsSync(firstArgument)) {
1160
+ return {
1161
+ passed: false,
1162
+ description: `mcpServers.muggle args[0] does not exist: ${firstArgument}`
1163
+ };
1164
+ }
1165
+ }
1166
+ return {
1167
+ passed: true,
1168
+ description: `Configured at ${cursorMcpConfigPath}`
1169
+ };
1170
+ } catch (error) {
1171
+ const errorMessage = error instanceof Error ? error.message : String(error);
1172
+ return {
1173
+ passed: false,
1174
+ description: `Invalid JSON or schema: ${errorMessage}`
1175
+ };
1176
+ }
1177
+ }
1178
+ function runDiagnostics() {
1179
+ const results = [];
1180
+ const config = getConfig();
1181
+ const dataDir = getDataDir();
1182
+ results.push({
1183
+ name: "Data Directory",
1184
+ passed: existsSync(dataDir),
1185
+ description: existsSync(dataDir) ? `Found at ${dataDir}` : `Not found at ${dataDir}`,
1186
+ suggestion: "Run 'muggle login' to create the data directory"
1187
+ });
1188
+ const electronVersion = getElectronAppVersion();
1189
+ const bundledVersion = getBundledElectronAppVersion();
1190
+ const versionSource = getElectronAppVersionSource();
1191
+ const installVerification = verifyElectronAppInstallation();
1192
+ let electronDescription;
1193
+ let electronSuggestion;
1194
+ if (installVerification.valid) {
1195
+ electronDescription = `Installed (v${electronVersion})`;
1196
+ switch (versionSource) {
1197
+ case "env":
1198
+ electronDescription += ` [from ELECTRON_APP_VERSION env]`;
1199
+ break;
1200
+ case "override":
1201
+ electronDescription += ` [overridden from bundled v${bundledVersion}]`;
1202
+ break;
1203
+ }
1204
+ if (!installVerification.metadataExists) {
1205
+ electronDescription += " [missing metadata]";
1206
+ }
1207
+ } else {
1208
+ electronDescription = `Not installed (expected v${electronVersion})`;
1209
+ if (installVerification.errorDetail) {
1210
+ electronDescription += `
1211
+ \u2514\u2500 ${installVerification.errorDetail}`;
1212
+ electronDescription += `
1213
+ \u2514\u2500 Checked: ${installVerification.versionDir}`;
1214
+ }
1215
+ if (installVerification.hasPartialArchive) {
1216
+ electronSuggestion = "Run 'muggle setup --force' to re-download and extract";
1217
+ } else {
1218
+ electronSuggestion = "Run 'muggle setup' to download the Electron app";
1219
+ }
1220
+ }
1221
+ results.push({
1222
+ name: "Electron App",
1223
+ passed: installVerification.valid,
1224
+ description: electronDescription,
1225
+ suggestion: electronSuggestion
1226
+ });
1227
+ if (installVerification.valid) {
1228
+ results.push({
1229
+ name: "Electron App Updates",
1230
+ passed: true,
1231
+ description: "Run 'muggle upgrade --check' to check for updates"
1232
+ });
1233
+ }
1234
+ const authService = getAuthService();
1235
+ const authStatus = authService.getAuthStatus();
1236
+ results.push({
1237
+ name: "Authentication",
1238
+ passed: authStatus.authenticated,
1239
+ description: authStatus.authenticated ? `Authenticated as ${authStatus.email ?? "unknown"}` : "Not authenticated",
1240
+ suggestion: "Run 'muggle login' to authenticate"
1241
+ });
1242
+ const hasStoredApiKey = hasApiKey();
1243
+ results.push({
1244
+ name: "API Key",
1245
+ passed: hasStoredApiKey,
1246
+ description: hasStoredApiKey ? "API key stored" : "No API key stored (optional)",
1247
+ suggestion: "Run 'muggle login --key-name <name>' to generate an API key"
1248
+ });
1249
+ const credentialsPath = getCredentialsFilePath();
1250
+ results.push({
1251
+ name: "Credentials File",
1252
+ passed: existsSync(credentialsPath),
1253
+ description: existsSync(credentialsPath) ? `Found at ${credentialsPath}` : `Not found at ${credentialsPath}`,
1254
+ suggestion: "Run 'muggle login' to create credentials"
1255
+ });
1256
+ results.push({
1257
+ name: "Prompt Service URL",
1258
+ passed: !!config.e2e.promptServiceBaseUrl,
1259
+ description: config.e2e.promptServiceBaseUrl
1260
+ });
1261
+ results.push({
1262
+ name: "Web Service URL",
1263
+ passed: !!config.localQa.webServiceUrl,
1264
+ description: config.localQa.webServiceUrl
1265
+ });
1266
+ const cursorMcpConfigValidationResult = validateCursorMcpConfig();
1267
+ results.push({
1268
+ name: "Cursor MCP Config",
1269
+ passed: cursorMcpConfigValidationResult.passed,
1270
+ description: cursorMcpConfigValidationResult.description,
1271
+ suggestion: "Re-run npm install -g @muggleai/works to refresh ~/.cursor/mcp.json"
1272
+ });
1273
+ return results;
1274
+ }
1275
+ function formatCheckResult(result) {
1276
+ const icon = result.passed ? "\u2713" : "\u2717";
1277
+ const color = result.passed ? "\x1B[32m" : "\x1B[31m";
1278
+ const reset = "\x1B[0m";
1279
+ let output = `${color}${icon}${reset} ${result.name}: ${result.description}`;
1280
+ if (!result.passed && result.suggestion) {
1281
+ output += `
1282
+ \u2514\u2500 ${result.suggestion}`;
1283
+ }
1284
+ return output;
1285
+ }
1286
+ async function doctorCommand() {
1287
+ console.log("\nMuggle Works Doctor");
1288
+ console.log("=================\n");
1289
+ const results = runDiagnostics();
1290
+ for (const result of results) {
1291
+ console.log(formatCheckResult(result));
1292
+ }
1293
+ console.log("");
1294
+ const failedCount = results.filter((r) => !r.passed).length;
1295
+ if (failedCount === 0) {
1296
+ console.log("All checks passed! Your installation is ready.");
1297
+ } else {
1298
+ console.log(`${failedCount} issue(s) found. See suggestions above.`);
1299
+ }
1300
+ logger3.info("Doctor command completed", {
1301
+ totalChecks: results.length,
1302
+ passed: results.length - failedCount,
1303
+ failed: failedCount
1304
+ });
1305
+ }
1306
+
1307
+ // src/cli/help.ts
1308
+ var COLORS = {
1309
+ reset: "\x1B[0m",
1310
+ bold: "\x1B[1m",
1311
+ dim: "\x1B[2m",
1312
+ cyan: "\x1B[36m",
1313
+ green: "\x1B[32m",
1314
+ yellow: "\x1B[33m",
1315
+ blue: "\x1B[34m"};
1316
+ function colorize(text, color) {
1317
+ if (process.env.NO_COLOR) {
1318
+ return text;
1319
+ }
1320
+ return `${color}${text}${COLORS.reset}`;
1321
+ }
1322
+ function header(title) {
1323
+ return colorize(`
1324
+ ${title}`, COLORS.bold + COLORS.cyan);
1325
+ }
1326
+ function cmd(cmd2) {
1327
+ return colorize(cmd2, COLORS.green);
1328
+ }
1329
+ function path3(path6) {
1330
+ return colorize(path6, COLORS.yellow);
1331
+ }
1332
+ function getHelpGuidance() {
1333
+ const lines = [
1334
+ "",
1335
+ colorize("=".repeat(70), COLORS.cyan),
1336
+ colorize(" Muggle AI Works - Comprehensive How-To Guide", COLORS.bold + COLORS.green),
1337
+ colorize("=".repeat(70), COLORS.cyan),
1338
+ "",
1339
+ header("What is Muggle AI Works?"),
1340
+ "",
1341
+ " Muggle AI Works is a Model Context Protocol server that provides AI",
1342
+ " assistants with tools to perform automated end-to-end (E2E) acceptance testing of web applications.",
1343
+ "",
1344
+ " It supports both:",
1345
+ ` ${colorize("\u2022", COLORS.green)} Cloud E2E - Test remote production/staging sites with a public URL`,
1346
+ ` ${colorize("\u2022", COLORS.green)} Local E2E - Test localhost development servers`,
1347
+ "",
1348
+ header("Setup Instructions"),
1349
+ "",
1350
+ ` ${colorize("Step 1:", COLORS.bold)} Configure your MCP client`,
1351
+ "",
1352
+ ` For ${colorize("Cursor", COLORS.bold)}, edit ${path3("~/.cursor/mcp.json")}:`,
1353
+ "",
1354
+ ` ${colorize("{", COLORS.dim)}`,
1355
+ ` ${colorize('"mcpServers"', COLORS.yellow)}: {`,
1356
+ ` ${colorize('"muggle"', COLORS.yellow)}: {`,
1357
+ ` ${colorize('"command"', COLORS.yellow)}: ${colorize('"muggle"', COLORS.green)},`,
1358
+ ` ${colorize('"args"', COLORS.yellow)}: [${colorize('"serve"', COLORS.green)}]`,
1359
+ ` }`,
1360
+ ` }`,
1361
+ ` ${colorize("}", COLORS.dim)}`,
1362
+ "",
1363
+ ` ${colorize("Step 2:", COLORS.bold)} Restart your MCP client`,
1364
+ "",
1365
+ ` ${colorize("Step 3:", COLORS.bold)} Start testing! Ask your AI assistant:`,
1366
+ ` ${colorize('"Test the login flow on my app at http://localhost:3000"', COLORS.dim)}`,
1367
+ "",
1368
+ header("CLI Commands"),
1369
+ "",
1370
+ ` ${colorize("Server Commands:", COLORS.bold)}`,
1371
+ ` ${cmd("muggle serve")} Start MCP server with all tools`,
1372
+ ` ${cmd("muggle serve --e2e")} Start with cloud E2E tools only`,
1373
+ ` ${cmd("muggle serve --local")} Start with local E2E tools only`,
1374
+ "",
1375
+ ` ${colorize("Setup & Diagnostics:", COLORS.bold)}`,
1376
+ ` ${cmd("muggle setup")} Download/update Electron app`,
1377
+ ` ${cmd("muggle setup --force")} Force re-download`,
1378
+ ` ${cmd("muggle doctor")} Diagnose installation issues`,
1379
+ ` ${cmd("muggle upgrade")} Check for updates`,
1380
+ ` ${cmd("muggle upgrade --check")} Check updates without installing`,
1381
+ "",
1382
+ ` ${colorize("Authentication:", COLORS.bold)}`,
1383
+ ` ${cmd("muggle login")} Login to Muggle AI`,
1384
+ ` ${cmd("muggle logout")} Clear stored credentials`,
1385
+ ` ${cmd("muggle status")} Show authentication status`,
1386
+ "",
1387
+ ` ${colorize("Maintenance:", COLORS.bold)}`,
1388
+ ` ${cmd("muggle versions")} List installed Electron app versions`,
1389
+ ` ${cmd("muggle cleanup")} Remove old Electron app versions`,
1390
+ ` ${cmd("muggle cleanup --all")} Remove all old versions`,
1391
+ ` ${cmd("muggle cleanup --skills")} Also remove obsolete skills`,
1392
+ "",
1393
+ ` ${colorize("Help:", COLORS.bold)}`,
1394
+ ` ${cmd("muggle help")} Show this guide`,
1395
+ ` ${cmd("muggle --help")} Show command synopsis`,
1396
+ ` ${cmd("muggle --version")} Show version`,
1397
+ "",
1398
+ header("Authentication Flow"),
1399
+ "",
1400
+ " Authentication happens automatically when you first use a tool that",
1401
+ " requires it:",
1402
+ "",
1403
+ ` 1. ${colorize("A browser window opens", COLORS.bold)} with a verification code`,
1404
+ ` 2. ${colorize("Log in", COLORS.bold)} with your Muggle AI account`,
1405
+ ` 3. ${colorize("The tool call continues", COLORS.bold)} with your credentials`,
1406
+ "",
1407
+ ` API keys are stored in ${path3("~/.muggle-ai/api-key.json")}`,
1408
+ "",
1409
+ header("Available MCP Tools"),
1410
+ "",
1411
+ ` ${colorize("Cloud E2E tools:", COLORS.bold)} (prefix: muggle-remote-)`,
1412
+ " muggle-remote-project-create, muggle-remote-project-list, muggle-remote-use-case-create-from-prompts,",
1413
+ " muggle-remote-test-case-generate-from-prompt, muggle-remote-workflow-start-*, etc.",
1414
+ "",
1415
+ ` ${colorize("Local E2E tools:", COLORS.bold)} (prefix: muggle-local-)`,
1416
+ " muggle-local-execute-test-generation, muggle-local-execute-replay,",
1417
+ " muggle-local-publish-test-script, muggle-local-run-result-get,",
1418
+ " muggle-local-check-status, etc.",
1419
+ "",
1420
+ header("Data Directory"),
1421
+ "",
1422
+ ` All data is stored in ${path3("~/.muggle-ai/")}:`,
1423
+ "",
1424
+ ` ${path3("api-key.json")} Long-lived API key (auto-generated)`,
1425
+ ` ${path3("projects/")} Local test projects`,
1426
+ ` ${path3("sessions/")} Test execution sessions`,
1427
+ ` ${path3("electron-app/")} Downloaded Electron app binaries`,
1428
+ "",
1429
+ header("Troubleshooting"),
1430
+ "",
1431
+ ` ${colorize("Installation issues:", COLORS.bold)}`,
1432
+ ` Run ${cmd("muggle doctor")} to diagnose problems`,
1433
+ "",
1434
+ ` ${colorize("Electron app not found:", COLORS.bold)}`,
1435
+ ` Run ${cmd("muggle setup --force")} to re-download`,
1436
+ "",
1437
+ ` ${colorize("Authentication issues:", COLORS.bold)}`,
1438
+ ` Run ${cmd("muggle logout")} then ${cmd("muggle login")}`,
1439
+ "",
1440
+ ` ${colorize("MCP not working in client:", COLORS.bold)}`,
1441
+ " 1. Verify mcp.json configuration",
1442
+ " 2. Restart your MCP client",
1443
+ ` 3. Check ${cmd("muggle doctor")} output`,
1444
+ "",
1445
+ header("Documentation & Support"),
1446
+ "",
1447
+ ` Docs: ${colorize("https://www.muggle-ai.com/muggleTestV0/docs/mcp/mcp-overview", COLORS.blue)}`,
1448
+ ` GitHub: ${colorize("https://github.com/multiplex-ai/muggle-ai-works", COLORS.blue)}`,
1449
+ "",
1450
+ colorize("=".repeat(70), COLORS.cyan),
1451
+ ""
1452
+ ];
1453
+ return lines.join("\n");
1454
+ }
1455
+ function helpCommand() {
1456
+ console.log(getHelpGuidance());
1457
+ }
1458
+
1459
+ // src/cli/login.ts
1460
+ var logger4 = getLogger();
1461
+ async function loginCommand(options) {
1462
+ console.log("\nMuggle AI Login");
1463
+ console.log("===============\n");
1464
+ const expiry = options.keyExpiry || "90d";
1465
+ console.log("Starting device code authentication...");
1466
+ console.log("A browser window will open for you to complete login.\n");
1467
+ const result = await performLogin(options.keyName, expiry);
1468
+ if (result.success) {
1469
+ console.log("\u2713 Login successful!");
1470
+ if (result.credentials?.email) {
1471
+ console.log(` Logged in as: ${result.credentials.email}`);
1472
+ }
1473
+ if (result.credentials?.apiKey) {
1474
+ console.log(" API key created and stored for future use.");
1475
+ }
1476
+ console.log("\nYou can now use Muggle AI Works tools.");
1477
+ } else {
1478
+ console.error("\u2717 Login failed");
1479
+ if (result.error) {
1480
+ console.error(` Error: ${result.error}`);
1481
+ }
1482
+ if (result.deviceCodeResponse) {
1483
+ console.log("\nIf browser didn't open, visit:");
1484
+ console.log(` ${result.deviceCodeResponse.verificationUriComplete}`);
1485
+ console.log(` Code: ${result.deviceCodeResponse.userCode}`);
1486
+ }
1487
+ process.exit(1);
1488
+ }
1489
+ }
1490
+ async function logoutCommand() {
1491
+ console.log("\nLogging out...");
1492
+ performLogout();
1493
+ console.log("\u2713 Credentials cleared successfully.");
1494
+ logger4.info("Logout completed");
1495
+ }
1496
+ async function statusCommand() {
1497
+ console.log("\nAuthentication Status");
1498
+ console.log("=====================\n");
1499
+ const authService = getAuthService();
1500
+ const status = authService.getAuthStatus();
1501
+ const hasStoredApiKey = hasApiKey();
1502
+ if (status.authenticated) {
1503
+ console.log("\u2713 Authenticated");
1504
+ if (status.email) {
1505
+ console.log(` Email: ${status.email}`);
1506
+ }
1507
+ if (status.userId) {
1508
+ console.log(` User ID: ${status.userId}`);
1509
+ }
1510
+ if (status.expiresAt) {
1511
+ const expiresDate = new Date(status.expiresAt);
1512
+ console.log(` Token expires: ${expiresDate.toLocaleString()}`);
1513
+ if (status.isExpired) {
1514
+ console.log(" (Token expired - will refresh automatically on next API call)");
1515
+ }
1516
+ }
1517
+ console.log(` API Key: ${hasStoredApiKey ? "Yes" : "No"}`);
1518
+ } else {
1519
+ console.log("\u2717 Not authenticated");
1520
+ console.log("\nRun 'muggle login' to authenticate.");
1521
+ }
1522
+ }
1523
+
1524
+ // src/cli/serve.ts
1525
+ var logger5 = getLogger();
1526
+ async function serveCommand(options) {
1527
+ const config = getConfig();
1528
+ const enableQa = options.local ? false : true;
1529
+ const enableLocal = options.e2e ? false : true;
1530
+ logger5.info("Starting Muggle MCP Server", {
1531
+ version: config.serverVersion,
1532
+ enableQa,
1533
+ enableLocal,
1534
+ transport: "stdio"
1535
+ });
1536
+ try {
1537
+ if (enableQa) {
1538
+ const qaTools = getQaTools();
1539
+ registerTools(qaTools);
1540
+ logger5.info("Registered cloud E2E acceptance tools", { count: qaTools.length });
1541
+ }
1542
+ if (enableLocal) {
1543
+ const localTools = getLocalQaTools();
1544
+ registerTools(localTools);
1545
+ logger5.info("Registered local E2E acceptance tools", { count: localTools.length });
1546
+ }
1547
+ const mcpServer = createUnifiedMcpServer({
1548
+ enableQaTools: enableQa,
1549
+ enableLocalTools: enableLocal
1550
+ });
1551
+ await startStdioServer(mcpServer);
1552
+ logger5.info("MCP server started successfully");
1553
+ } catch (error) {
1554
+ logger5.error("Failed to start MCP server", {
1555
+ error: error instanceof Error ? error.message : String(error),
1556
+ stack: error instanceof Error ? error.stack : void 0
1557
+ });
1558
+ process.exit(1);
1559
+ }
1560
+ }
1561
+ var logger6 = getLogger();
1562
+ var MAX_RETRY_ATTEMPTS = 3;
1563
+ var RETRY_BASE_DELAY_MS = 2e3;
1564
+ var INSTALL_METADATA_FILE_NAME = ".install-metadata.json";
1565
+ function getBinaryName() {
1566
+ const os = platform();
1567
+ const architecture = arch();
1568
+ switch (os) {
1569
+ case "darwin": {
1570
+ const darwinArch = architecture === "arm64" ? "arm64" : "x64";
1571
+ return `MuggleAI-darwin-${darwinArch}.zip`;
1572
+ }
1573
+ case "win32":
1574
+ return "MuggleAI-win32-x64.zip";
1575
+ case "linux":
1576
+ return "MuggleAI-linux-x64.zip";
1577
+ default:
1578
+ throw new Error(`Unsupported platform: ${os}`);
1579
+ }
1580
+ }
1581
+ function getExpectedExecutablePath2(versionDir) {
1582
+ const os = platform();
1583
+ switch (os) {
1584
+ case "darwin":
1585
+ return path.join(versionDir, "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
1586
+ case "win32":
1587
+ return path.join(versionDir, "MuggleAI.exe");
1588
+ case "linux":
1589
+ return path.join(versionDir, "MuggleAI");
1590
+ default:
1591
+ throw new Error(`Unsupported platform: ${os}`);
1592
+ }
1593
+ }
1594
+ async function extractZip(zipPath, destDir) {
1595
+ return new Promise((resolve2, reject) => {
1596
+ if (platform() === "win32") {
1597
+ execFile(
1598
+ "powershell",
1599
+ ["-command", `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`],
1600
+ (error) => {
1601
+ if (error) {
1602
+ reject(error);
1603
+ } else {
1604
+ resolve2();
1605
+ }
1606
+ }
1607
+ );
1608
+ } else {
1609
+ execFile("unzip", ["-o", zipPath, "-d", destDir], (error) => {
1610
+ if (error) {
1611
+ reject(error);
1612
+ } else {
1613
+ resolve2();
1614
+ }
1615
+ });
1616
+ }
1617
+ });
1618
+ }
1619
+ async function extractTarGz(tarPath, destDir) {
1620
+ return new Promise((resolve2, reject) => {
1621
+ execFile("tar", ["-xzf", tarPath, "-C", destDir], (error) => {
1622
+ if (error) {
1623
+ reject(error);
1624
+ } else {
1625
+ resolve2();
1626
+ }
1627
+ });
1628
+ });
1629
+ }
1630
+ function sleep(ms) {
1631
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
1632
+ }
1633
+ async function downloadWithRetry(downloadUrl, destPath) {
1634
+ let lastError = null;
1635
+ for (let attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
1636
+ try {
1637
+ if (attempt > 1) {
1638
+ const delayMs = RETRY_BASE_DELAY_MS * Math.pow(2, attempt - 2);
1639
+ console.log(`Retry attempt ${attempt}/${MAX_RETRY_ATTEMPTS} after ${delayMs}ms delay...`);
1640
+ await sleep(delayMs);
1641
+ }
1642
+ const response = await fetch(downloadUrl);
1643
+ if (!response.ok) {
1644
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1645
+ }
1646
+ if (!response.body) {
1647
+ throw new Error("No response body received");
1648
+ }
1649
+ const fileStream = createWriteStream(destPath);
1650
+ await pipeline(response.body, fileStream);
1651
+ return true;
1652
+ } catch (error) {
1653
+ lastError = error instanceof Error ? error : new Error(String(error));
1654
+ console.error(`Download attempt ${attempt} failed: ${lastError.message}`);
1655
+ if (existsSync(destPath)) {
1656
+ rmSync(destPath, { force: true });
1657
+ }
1658
+ }
1659
+ }
1660
+ if (lastError) {
1661
+ throw new Error(`Download failed after ${MAX_RETRY_ATTEMPTS} attempts: ${lastError.message}`);
1662
+ }
1663
+ return false;
1664
+ }
1665
+ function writeInstallMetadata(params) {
1666
+ const metadata = {
1667
+ version: params.version,
1668
+ binaryName: params.binaryName,
1669
+ platformKey: params.platformKey,
1670
+ executableChecksum: params.executableChecksum,
1671
+ expectedArchiveChecksum: params.expectedArchiveChecksum,
1672
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1673
+ };
1674
+ writeFileSync(params.metadataPath, `${JSON.stringify(metadata, null, 2)}
1675
+ `, "utf-8");
1676
+ }
1677
+ function cleanupFailedInstall(versionDir) {
1678
+ if (existsSync(versionDir)) {
1679
+ try {
1680
+ rmSync(versionDir, { recursive: true, force: true });
1681
+ } catch {
1682
+ }
1683
+ }
1684
+ }
1685
+ async function setupCommand(options) {
1686
+ const version = getElectronAppVersion();
1687
+ const versionDir = getElectronAppDir(version);
1688
+ const platformKey = getPlatformKey();
1689
+ if (!options.force && isElectronAppInstalled()) {
1690
+ console.log(`Electron app v${version} is already installed at ${versionDir}`);
1691
+ console.log("Use --force to re-download.");
1692
+ return;
1693
+ }
1694
+ const binaryName = getBinaryName();
1695
+ const downloadUrl = buildElectronAppReleaseAssetUrl({
1696
+ version,
1697
+ assetFileName: binaryName
1698
+ });
1699
+ console.log(`Downloading Muggle Test Electron app v${version}...`);
1700
+ console.log(`URL: ${downloadUrl}`);
1701
+ try {
1702
+ if (existsSync(versionDir)) {
1703
+ rmSync(versionDir, { recursive: true, force: true });
1704
+ }
1705
+ mkdirSync(versionDir, { recursive: true });
1706
+ const tempFile = path.join(versionDir, binaryName);
1707
+ await downloadWithRetry(downloadUrl, tempFile);
1708
+ console.log("Download complete, verifying checksum...");
1709
+ const checksums = getElectronAppChecksums();
1710
+ const expectedChecksum = getChecksumForPlatform(checksums);
1711
+ const checksumResult = await verifyFileChecksum(tempFile, expectedChecksum);
1712
+ if (!checksumResult.valid && expectedChecksum) {
1713
+ cleanupFailedInstall(versionDir);
1714
+ throw new Error(
1715
+ `Checksum verification failed!
1716
+ Expected: ${checksumResult.expected}
1717
+ Actual: ${checksumResult.actual}
1718
+ The downloaded file may be corrupted or tampered with.`
1719
+ );
1720
+ }
1721
+ if (expectedChecksum) {
1722
+ console.log("Checksum verified successfully.");
1723
+ } else {
1724
+ console.log("Warning: No checksum configured, skipping verification.");
1725
+ }
1726
+ console.log("Extracting...");
1727
+ if (binaryName.endsWith(".zip")) {
1728
+ await extractZip(tempFile, versionDir);
1729
+ } else if (binaryName.endsWith(".tar.gz")) {
1730
+ await extractTarGz(tempFile, versionDir);
1731
+ }
1732
+ const executablePath = getExpectedExecutablePath2(versionDir);
1733
+ if (!existsSync(executablePath)) {
1734
+ cleanupFailedInstall(versionDir);
1735
+ throw new Error(
1736
+ `Extraction failed: executable not found at expected path.
1737
+ Expected: ${executablePath}
1738
+ The archive may be corrupted or in an unexpected format.`
1739
+ );
1740
+ }
1741
+ const executableChecksum = await calculateFileChecksum(executablePath);
1742
+ const metadataPath = path.join(versionDir, INSTALL_METADATA_FILE_NAME);
1743
+ writeInstallMetadata({
1744
+ metadataPath,
1745
+ version,
1746
+ binaryName,
1747
+ platformKey,
1748
+ executableChecksum,
1749
+ expectedArchiveChecksum: expectedChecksum
1750
+ });
1751
+ rmSync(tempFile, { force: true });
1752
+ console.log(`Electron app installed to ${versionDir}`);
1753
+ logger6.info("Setup complete", { version, path: versionDir });
1754
+ } catch (error) {
1755
+ const errorMessage = error instanceof Error ? error.message : String(error);
1756
+ console.error(`Failed to download Electron app: ${errorMessage}`);
1757
+ logger6.error("Setup failed", { error: errorMessage });
1758
+ process.exit(1);
1759
+ }
1760
+ }
1761
+ var logger7 = getLogger();
1762
+ var GITHUB_RELEASES_API = "https://api.github.com/repos/multiplex-ai/muggle-ai-works/releases";
1763
+ var INSTALL_METADATA_FILE_NAME2 = ".install-metadata.json";
1764
+ var VERSION_OVERRIDE_FILE = "electron-app-version-override.json";
1765
+ function getBinaryName2() {
1766
+ const os = platform();
1767
+ const arch2 = process.arch;
1768
+ switch (os) {
1769
+ case "darwin":
1770
+ return arch2 === "arm64" ? "MuggleAI-darwin-arm64.zip" : "MuggleAI-darwin-x64.zip";
1771
+ case "win32":
1772
+ return "MuggleAI-win32-x64.zip";
1773
+ case "linux":
1774
+ return "MuggleAI-linux-x64.zip";
1775
+ default:
1776
+ throw new Error(`Unsupported platform: ${os}`);
1777
+ }
1778
+ }
1779
+ function extractVersionFromTag(tag) {
1780
+ const match = tag.match(/^(?:electron-app-)?v(\d+\.\d+\.\d+)$/);
1781
+ return match ? match[1] : null;
1782
+ }
1783
+ function getVersionOverridePath() {
1784
+ return path.join(getDataDir(), VERSION_OVERRIDE_FILE);
1785
+ }
1786
+ function getEffectiveElectronAppVersion() {
1787
+ const overridePath = getVersionOverridePath();
1788
+ if (existsSync(overridePath)) {
1789
+ try {
1790
+ const content = JSON.parse(__require("fs").readFileSync(overridePath, "utf-8"));
1791
+ if (content.version) {
1792
+ return content.version;
1793
+ }
1794
+ } catch {
1795
+ }
1796
+ }
1797
+ return getElectronAppVersion();
1798
+ }
1799
+ function saveVersionOverride(version) {
1800
+ const overridePath = getVersionOverridePath();
1801
+ const dataDir = getDataDir();
1802
+ if (!existsSync(dataDir)) {
1803
+ mkdirSync(dataDir, { recursive: true });
1804
+ }
1805
+ writeFileSync(overridePath, JSON.stringify({
1806
+ version,
1807
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1808
+ }, null, 2), "utf-8");
1809
+ }
1810
+ async function checkForUpdates() {
1811
+ const currentVersion = getEffectiveElectronAppVersion();
1812
+ try {
1813
+ const response = await fetch(GITHUB_RELEASES_API, {
1814
+ headers: {
1815
+ "Accept": "application/vnd.github.v3+json",
1816
+ "User-Agent": "muggle"
1817
+ }
1818
+ });
1819
+ if (!response.ok) {
1820
+ throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
1821
+ }
1822
+ const releases = await response.json();
1823
+ for (const release of releases) {
1824
+ if (release.prerelease || release.draft) {
1825
+ continue;
1826
+ }
1827
+ const version = extractVersionFromTag(release.tag_name);
1828
+ if (version) {
1829
+ const updateAvailable = compareVersions2(version, currentVersion) > 0;
1830
+ const binaryName = getBinaryName2();
1831
+ return {
1832
+ currentVersion,
1833
+ latestVersion: version,
1834
+ updateAvailable,
1835
+ downloadUrl: buildElectronAppReleaseAssetUrl({
1836
+ version,
1837
+ assetFileName: binaryName
1838
+ })
1839
+ };
1840
+ }
1841
+ }
1842
+ return {
1843
+ currentVersion,
1844
+ latestVersion: currentVersion,
1845
+ updateAvailable: false
1846
+ };
1847
+ } catch (error) {
1848
+ const errorMessage = error instanceof Error ? error.message : String(error);
1849
+ logger7.warn("Failed to check for updates", { error: errorMessage });
1850
+ throw new Error(`Failed to check for updates: ${errorMessage}`, { cause: error });
1851
+ }
1852
+ }
1853
+ function compareVersions2(a, b) {
1854
+ const partsA = a.split(".").map(Number);
1855
+ const partsB = b.split(".").map(Number);
1856
+ for (let i = 0; i < 3; i++) {
1857
+ const partA = partsA[i] || 0;
1858
+ const partB = partsB[i] || 0;
1859
+ if (partA > partB) {
1860
+ return 1;
1861
+ }
1862
+ if (partA < partB) {
1863
+ return -1;
1864
+ }
1865
+ }
1866
+ return 0;
1867
+ }
1868
+ function getExpectedExecutablePath3(versionDir) {
1869
+ const os = platform();
1870
+ switch (os) {
1871
+ case "darwin":
1872
+ return path.join(versionDir, "MuggleAI.app", "Contents", "MacOS", "MuggleAI");
1873
+ case "win32":
1874
+ return path.join(versionDir, "MuggleAI.exe");
1875
+ case "linux":
1876
+ return path.join(versionDir, "MuggleAI");
1877
+ default:
1878
+ throw new Error(`Unsupported platform: ${os}`);
1879
+ }
1880
+ }
1881
+ function writeInstallMetadata2(params) {
1882
+ const metadata = {
1883
+ version: params.version,
1884
+ binaryName: params.binaryName,
1885
+ platformKey: params.platformKey,
1886
+ executableChecksum: params.executableChecksum,
1887
+ expectedArchiveChecksum: params.expectedArchiveChecksum,
1888
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1889
+ };
1890
+ writeFileSync(params.metadataPath, `${JSON.stringify(metadata, null, 2)}
1891
+ `, "utf-8");
1892
+ }
1893
+ async function extractZip2(zipPath, destDir) {
1894
+ return new Promise((resolve2, reject) => {
1895
+ if (platform() === "win32") {
1896
+ execFile(
1897
+ "powershell",
1898
+ ["-command", `Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force`],
1899
+ (error) => {
1900
+ if (error) {
1901
+ reject(error);
1902
+ } else {
1903
+ resolve2();
1904
+ }
1905
+ }
1906
+ );
1907
+ } else {
1908
+ execFile("unzip", ["-o", zipPath, "-d", destDir], (error) => {
1909
+ if (error) {
1910
+ reject(error);
1911
+ } else {
1912
+ resolve2();
1913
+ }
1914
+ });
1915
+ }
1916
+ });
1917
+ }
1918
+ async function extractTarGz2(tarPath, destDir) {
1919
+ return new Promise((resolve2, reject) => {
1920
+ execFile("tar", ["-xzf", tarPath, "-C", destDir], (error) => {
1921
+ if (error) {
1922
+ reject(error);
1923
+ } else {
1924
+ resolve2();
1925
+ }
1926
+ });
1927
+ });
1928
+ }
1929
+ async function fetchChecksumFromRelease(version) {
1930
+ const checksumUrl = buildElectronAppChecksumsUrl(version);
1931
+ try {
1932
+ const response = await fetch(checksumUrl);
1933
+ if (!response.ok) {
1934
+ logger7.warn("Checksums file not found in release", { version });
1935
+ return "";
1936
+ }
1937
+ const text = await response.text();
1938
+ const binaryName = getBinaryName2();
1939
+ const platformKey = getPlatformKey();
1940
+ const lines = text.split("\n");
1941
+ for (const line of lines) {
1942
+ const trimmed = line.trim();
1943
+ if (!trimmed) {
1944
+ continue;
1945
+ }
1946
+ const match = trimmed.match(/^([a-fA-F0-9]{64})\s+(.+)$/);
1947
+ if (match) {
1948
+ const checksum = match[1];
1949
+ const filename = match[2];
1950
+ if (filename === binaryName || filename.includes(platformKey)) {
1951
+ logger7.info("Found checksum in release", {
1952
+ version,
1953
+ platform: platformKey,
1954
+ checksum: checksum.substring(0, 16) + "..."
1955
+ });
1956
+ return checksum;
1957
+ }
1958
+ }
1959
+ }
1960
+ logger7.warn("Platform checksum not found in checksums.txt", {
1961
+ version,
1962
+ platform: platformKey
1963
+ });
1964
+ return "";
1965
+ } catch (error) {
1966
+ const errorMessage = error instanceof Error ? error.message : String(error);
1967
+ logger7.warn("Failed to fetch checksums from release", {
1968
+ version,
1969
+ error: errorMessage
1970
+ });
1971
+ return "";
1972
+ }
1973
+ }
1974
+ async function downloadAndInstall(version, downloadUrl, checksum) {
1975
+ const versionDir = getElectronAppDir(version);
1976
+ const binaryName = getBinaryName2();
1977
+ const platformKey = getPlatformKey();
1978
+ console.log(`Downloading Muggle Test Electron app v${version}...`);
1979
+ console.log(`URL: ${downloadUrl}`);
1980
+ if (existsSync(versionDir)) {
1981
+ rmSync(versionDir, { recursive: true, force: true });
1982
+ }
1983
+ mkdirSync(versionDir, { recursive: true });
1984
+ const response = await fetch(downloadUrl);
1985
+ if (!response.ok) {
1986
+ throw new Error(`Download failed: ${response.status} ${response.statusText}`);
1987
+ }
1988
+ const tempFile = path.join(versionDir, binaryName);
1989
+ const fileStream = createWriteStream(tempFile);
1990
+ if (!response.body) {
1991
+ throw new Error("No response body");
1992
+ }
1993
+ await pipeline(response.body, fileStream);
1994
+ console.log("Download complete, verifying checksum...");
1995
+ let expectedChecksum = checksum;
1996
+ if (!expectedChecksum) {
1997
+ expectedChecksum = await fetchChecksumFromRelease(version);
1998
+ }
1999
+ const checksumResult = await verifyFileChecksum(tempFile, expectedChecksum || "");
2000
+ if (!checksumResult.valid && expectedChecksum) {
2001
+ rmSync(versionDir, { recursive: true, force: true });
2002
+ throw new Error(
2003
+ `Checksum verification failed!
2004
+ Expected: ${checksumResult.expected}
2005
+ Actual: ${checksumResult.actual}
2006
+ The downloaded file may be corrupted or tampered with.`
2007
+ );
2008
+ }
2009
+ if (expectedChecksum) {
2010
+ console.log("Checksum verified successfully.");
2011
+ } else {
2012
+ console.log("Warning: No checksum available, skipping verification.");
2013
+ }
2014
+ console.log("Extracting...");
2015
+ if (binaryName.endsWith(".zip")) {
2016
+ await extractZip2(tempFile, versionDir);
2017
+ } else if (binaryName.endsWith(".tar.gz")) {
2018
+ await extractTarGz2(tempFile, versionDir);
2019
+ }
2020
+ const executablePath = getExpectedExecutablePath3(versionDir);
2021
+ if (!existsSync(executablePath)) {
2022
+ rmSync(versionDir, { recursive: true, force: true });
2023
+ throw new Error(
2024
+ `Extraction failed: executable not found at expected path.
2025
+ Expected: ${executablePath}
2026
+ The archive may be corrupted or in an unexpected format.`
2027
+ );
2028
+ }
2029
+ const executableChecksum = await calculateFileChecksum(executablePath);
2030
+ const metadataPath = path.join(versionDir, INSTALL_METADATA_FILE_NAME2);
2031
+ writeInstallMetadata2({
2032
+ metadataPath,
2033
+ version,
2034
+ binaryName,
2035
+ platformKey,
2036
+ executableChecksum,
2037
+ expectedArchiveChecksum: expectedChecksum || ""
2038
+ });
2039
+ rmSync(tempFile, { force: true });
2040
+ saveVersionOverride(version);
2041
+ console.log(`Electron app v${version} installed to ${versionDir}`);
2042
+ logger7.info("Upgrade complete", { version, path: versionDir });
2043
+ }
2044
+ async function upgradeCommand(options) {
2045
+ try {
2046
+ if (options.version) {
2047
+ const binaryName = getBinaryName2();
2048
+ const downloadUrl = buildElectronAppReleaseAssetUrl({
2049
+ version: options.version,
2050
+ assetFileName: binaryName
2051
+ });
2052
+ await downloadAndInstall(options.version, downloadUrl);
2053
+ const cleanupResult2 = cleanupOldVersions({ all: false });
2054
+ if (cleanupResult2.removed.length > 0) {
2055
+ console.log(
2056
+ `
2057
+ Cleaned up ${cleanupResult2.removed.length} old version(s), freed ${formatBytes(cleanupResult2.freedBytes)}`
2058
+ );
2059
+ }
2060
+ return;
2061
+ }
2062
+ console.log("Checking for updates...");
2063
+ const result = await checkForUpdates();
2064
+ console.log(`Current version: ${result.currentVersion}`);
2065
+ console.log(`Latest version: ${result.latestVersion}`);
2066
+ if (options.check) {
2067
+ if (result.updateAvailable) {
2068
+ console.log("\nUpdate available! Run 'muggle upgrade' to install.");
2069
+ } else {
2070
+ console.log("\nYou are on the latest version.");
2071
+ }
2072
+ return;
2073
+ }
2074
+ if (!result.updateAvailable && !options.force) {
2075
+ console.log("\nYou are already on the latest version.");
2076
+ console.log("Use --force to re-download the current version.");
2077
+ return;
2078
+ }
2079
+ if (!result.downloadUrl) {
2080
+ throw new Error("No download URL available");
2081
+ }
2082
+ await downloadAndInstall(result.latestVersion, result.downloadUrl);
2083
+ const cleanupResult = cleanupOldVersions({ all: false });
2084
+ if (cleanupResult.removed.length > 0) {
2085
+ console.log(
2086
+ `
2087
+ Cleaned up ${cleanupResult.removed.length} old version(s), freed ${formatBytes(cleanupResult.freedBytes)}`
2088
+ );
2089
+ }
2090
+ } catch (error) {
2091
+ const errorMessage = error instanceof Error ? error.message : String(error);
2092
+ console.error(`Upgrade failed: ${errorMessage}`);
2093
+ logger7.error("Upgrade failed", { error: errorMessage });
2094
+ process.exit(1);
2095
+ }
2096
+ }
2097
+
2098
+ // packages/commands/src/cli/run-cli.ts
2099
+ var __dirname$1 = dirname(fileURLToPath(import.meta.url));
2100
+ function getPackageRoot() {
2101
+ if (__dirname$1.endsWith("dist")) {
2102
+ return resolve(__dirname$1, "..");
2103
+ }
2104
+ return resolve(__dirname$1, "..", "..", "..", "..");
2105
+ }
2106
+ var packageVersion = JSON.parse(
2107
+ readFileSync(resolve(getPackageRoot(), "package.json"), "utf-8")
2108
+ ).version;
2109
+ var logger8 = getLogger();
2110
+ function createProgram() {
2111
+ const program = new Command();
2112
+ program.name("muggle").description("Unified MCP server for Muggle AI \u2014 cloud E2E and local E2E testing").version(packageVersion);
2113
+ program.command("serve").description("Start the MCP server").option("--e2e", "Only enable cloud E2E tools (remote URLs; muggle-remote-* prefix)").option("--local", "Only enable local E2E tools (localhost; muggle-local-* prefix)").option("--stdio", "Use stdio transport (default)").action(serveCommand);
2114
+ program.command("setup").description("Download/update the Electron app for local testing").option("--force", "Force re-download even if already installed").action(setupCommand);
2115
+ 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);
2116
+ program.command("versions").description("List installed electron-app versions").action(versionsCommand);
2117
+ 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);
2118
+ program.command("doctor").description("Diagnose installation and configuration issues").action(doctorCommand);
2119
+ 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);
2120
+ program.command("logout").description("Clear stored credentials").action(logoutCommand);
2121
+ program.command("status").description("Show authentication status").action(statusCommand);
2122
+ program.command("build-pr-section").description("Render a muggle-do PR body evidence block from an e2e report on stdin").option("--max-body-bytes <n>", "Max UTF-8 byte budget for the PR body (default 60000)").action(buildPrSectionCommand);
2123
+ program.action(() => {
2124
+ helpCommand();
2125
+ });
2126
+ program.on("command:*", () => {
2127
+ helpCommand();
2128
+ process.exit(1);
2129
+ });
2130
+ return program;
2131
+ }
2132
+ function handleHelpCommand() {
2133
+ const args = process.argv.slice(2);
2134
+ if (args.length === 1 && args[0] === "help") {
2135
+ helpCommand();
2136
+ return true;
2137
+ }
2138
+ return false;
2139
+ }
2140
+ async function runCli() {
2141
+ try {
2142
+ if (handleHelpCommand()) {
2143
+ return;
2144
+ }
2145
+ const program = createProgram();
2146
+ await program.parseAsync(process.argv);
2147
+ } catch (error) {
2148
+ logger8.error("CLI error", {
2149
+ error: error instanceof Error ? error.message : String(error)
2150
+ });
2151
+ process.exit(1);
2152
+ }
2153
+ }
2154
+
2155
+ // packages/commands/src/index.ts
2156
+ var src_exports = {};
2157
+ __export(src_exports, {
2158
+ cleanupCommand: () => cleanupCommand,
2159
+ doctorCommand: () => doctorCommand,
2160
+ helpCommand: () => helpCommand,
2161
+ loginCommand: () => loginCommand,
2162
+ logoutCommand: () => logoutCommand,
2163
+ registerCoreCommands: () => registerCoreCommands,
2164
+ runCli: () => runCli,
2165
+ serveCommand: () => serveCommand,
2166
+ setupCommand: () => setupCommand,
2167
+ statusCommand: () => statusCommand,
2168
+ upgradeCommand: () => upgradeCommand,
2169
+ versionsCommand: () => versionsCommand
2170
+ });
2171
+
2172
+ // packages/commands/src/registry/register-core-commands.ts
2173
+ function registerCoreCommands(commandRegistrationContext) {
2174
+ }
2175
+
2176
+ export { createUnifiedMcpServer, runCli, server_exports, src_exports };