@muggleai/works 4.3.0 → 4.5.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.
Files changed (30) hide show
  1. package/README.md +31 -13
  2. package/dist/{chunk-23NOSJFH.js → chunk-MNCBJEPQ.js} +588 -36
  3. package/dist/cli.js +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/plugin/.claude-plugin/plugin.json +1 -1
  6. package/dist/plugin/.cursor-plugin/plugin.json +1 -1
  7. package/dist/plugin/README.md +1 -0
  8. package/dist/plugin/skills/do/open-prs.md +32 -65
  9. package/dist/plugin/skills/muggle/SKILL.md +15 -15
  10. package/dist/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +181 -0
  11. package/dist/plugin/skills/muggle-test/SKILL.md +97 -137
  12. package/dist/plugin/skills/muggle-test-feature-local/SKILL.md +94 -27
  13. package/dist/plugin/skills/muggle-test-import/SKILL.md +135 -40
  14. package/dist/plugin/skills/muggle-test-regenerate-missing/SKILL.md +196 -0
  15. package/dist/plugin/skills/muggle-test-regenerate-missing/evals/evals.json +58 -0
  16. package/dist/plugin/skills/muggle-test-regenerate-missing/evals/trigger-eval.json +22 -0
  17. package/dist/release-manifest.json +7 -0
  18. package/package.json +7 -7
  19. package/plugin/.claude-plugin/plugin.json +1 -1
  20. package/plugin/.cursor-plugin/plugin.json +1 -1
  21. package/plugin/README.md +1 -0
  22. package/plugin/skills/do/open-prs.md +32 -65
  23. package/plugin/skills/muggle/SKILL.md +15 -15
  24. package/plugin/skills/muggle-pr-visual-walkthrough/SKILL.md +181 -0
  25. package/plugin/skills/muggle-test/SKILL.md +97 -137
  26. package/plugin/skills/muggle-test-feature-local/SKILL.md +94 -27
  27. package/plugin/skills/muggle-test-import/SKILL.md +135 -40
  28. package/plugin/skills/muggle-test-regenerate-missing/SKILL.md +196 -0
  29. package/plugin/skills/muggle-test-regenerate-missing/evals/evals.json +58 -0
  30. package/plugin/skills/muggle-test-regenerate-missing/evals/trigger-eval.json +22 -0
@@ -1,9 +1,9 @@
1
1
  import * as fs3 from 'fs';
2
- import { readFileSync, existsSync, rmSync, mkdirSync, readdirSync, createWriteStream, writeFileSync, statSync } from 'fs';
2
+ import { readFileSync, existsSync, mkdirSync, writeFileSync, rmSync, readdirSync, createWriteStream, statSync } from 'fs';
3
3
  import * as os3 from 'os';
4
4
  import { platform, arch, homedir } from 'os';
5
5
  import * as path2 from 'path';
6
- import { dirname, resolve, join } from 'path';
6
+ import { join, dirname, resolve } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import winston from 'winston';
9
9
  import axios, { AxiosError } from 'axios';
@@ -34,6 +34,49 @@ var DATA_DIR_NAME = ".muggle-ai";
34
34
  function getDataDir() {
35
35
  return path2.join(os3.homedir(), DATA_DIR_NAME);
36
36
  }
37
+ var DEV_MANIFEST = {
38
+ release: "dev",
39
+ buildId: "dev",
40
+ commitSha: "dev",
41
+ buildTime: "1970-01-01T00:00:00Z",
42
+ serviceName: "muggle-ai-works-mcp"
43
+ };
44
+ var MANIFEST_PATH = (() => {
45
+ try {
46
+ return join(dirname(fileURLToPath(import.meta.url)), "release-manifest.json");
47
+ } catch {
48
+ return "release-manifest.json";
49
+ }
50
+ })();
51
+ var cachedManifest;
52
+ function isValidManifest(value) {
53
+ if (typeof value !== "object" || value === null) return false;
54
+ const m = value;
55
+ return typeof m.release === "string" && typeof m.buildId === "string" && typeof m.commitSha === "string" && typeof m.buildTime === "string" && typeof m.serviceName === "string";
56
+ }
57
+ function readReleaseManifest() {
58
+ if (cachedManifest !== void 0) return cachedManifest;
59
+ try {
60
+ const raw = readFileSync(MANIFEST_PATH, "utf8");
61
+ const parsed = JSON.parse(raw);
62
+ if (!isValidManifest(parsed)) {
63
+ cachedManifest = DEV_MANIFEST;
64
+ return cachedManifest;
65
+ }
66
+ cachedManifest = parsed;
67
+ return cachedManifest;
68
+ } catch (err) {
69
+ const code = err.code;
70
+ if (code !== "ENOENT") {
71
+ console.warn(
72
+ "release_manifest: unexpected error reading manifest, using DEV fallback",
73
+ { error: err.message }
74
+ );
75
+ }
76
+ cachedManifest = DEV_MANIFEST;
77
+ return cachedManifest;
78
+ }
79
+ }
37
80
 
38
81
  // packages/mcps/src/shared/config.ts
39
82
  var DEFAULT_PROMPT_SERVICE_PRODUCTION_URL = "https://promptservice.muggle-ai.com";
@@ -303,9 +346,10 @@ function getConfig() {
303
346
  if (configInstance) {
304
347
  return configInstance;
305
348
  }
349
+ const manifest = readReleaseManifest();
306
350
  configInstance = {
307
351
  serverName: "muggle",
308
- serverVersion: "1.0.0",
352
+ serverVersion: manifest.release,
309
353
  logLevel: process.env.LOG_LEVEL ?? "info",
310
354
  auth0: buildAuth0Config(),
311
355
  e2e: buildE2eConfig(),
@@ -425,6 +469,14 @@ __export(e2e_exports2, {
425
469
  ApiKeyRevokeInputSchema: () => ApiKeyRevokeInputSchema,
426
470
  AuthLoginInputSchema: () => AuthLoginInputSchema,
427
471
  AuthPollInputSchema: () => AuthPollInputSchema,
472
+ BulkPreviewJobCancelInputSchema: () => BulkPreviewJobCancelInputSchema,
473
+ BulkPreviewJobGetInputSchema: () => BulkPreviewJobGetInputSchema,
474
+ BulkPreviewJobKindSchema: () => BulkPreviewJobKindSchema,
475
+ BulkPreviewJobListInputSchema: () => BulkPreviewJobListInputSchema,
476
+ BulkPreviewJobStatusSchema: () => BulkPreviewJobStatusSchema,
477
+ BulkPreviewPromptSchema: () => BulkPreviewPromptSchema,
478
+ BulkPreviewSubmitTestCaseInputSchema: () => BulkPreviewSubmitTestCaseInputSchema,
479
+ BulkPreviewSubmitUseCaseInputSchema: () => BulkPreviewSubmitUseCaseInputSchema,
428
480
  EmptyInputSchema: () => EmptyInputSchema,
429
481
  GatewayError: () => GatewayError,
430
482
  IdSchema: () => IdSchema,
@@ -467,11 +519,11 @@ __export(e2e_exports2, {
467
519
  TestCaseListInputSchema: () => TestCaseListInputSchema,
468
520
  TestScriptGetInputSchema: () => TestScriptGetInputSchema,
469
521
  TestScriptListInputSchema: () => TestScriptListInputSchema,
470
- TestScriptListPaginatedInputSchema: () => TestScriptListPaginatedInputSchema,
471
522
  TokenPackageIdSchema: () => TokenPackageIdSchema,
472
523
  TokenUsageFilterTypeSchema: () => TokenUsageFilterTypeSchema,
473
524
  UseCaseCandidatesApproveInputSchema: () => UseCaseCandidatesApproveInputSchema,
474
525
  UseCaseCreateFromPromptsInputSchema: () => UseCaseCreateFromPromptsInputSchema,
526
+ UseCaseCreateInputSchema: () => UseCaseCreateInputSchema,
475
527
  UseCaseDiscoveryMemoryGetInputSchema: () => UseCaseDiscoveryMemoryGetInputSchema,
476
528
  UseCaseGetInputSchema: () => UseCaseGetInputSchema,
477
529
  UseCaseListInputSchema: () => UseCaseListInputSchema,
@@ -2837,7 +2889,7 @@ var LocalExecutionContextInputSchema = z.object({
2837
2889
  electronAppVersion: z.string().optional().describe("Electron app version used for local run"),
2838
2890
  mcpServerVersion: z.string().optional().describe("MCP server version used for local run"),
2839
2891
  localExecutionCompletedAt: z.number().int().positive().describe("Epoch milliseconds when local run completed"),
2840
- uploadedAt: z.number().int().positive().optional().describe("Epoch milliseconds when uploaded to cloud")
2892
+ uploadedAt: z.number().int().positive().describe("Epoch milliseconds when uploaded to cloud")
2841
2893
  });
2842
2894
  var LocalRunUploadInputSchema = z.object({
2843
2895
  projectId: MuggleEntityIdSchema.describe("Project ID (UUID) for the local run"),
@@ -2854,8 +2906,10 @@ var LocalRunUploadInputSchema = z.object({
2854
2906
 
2855
2907
  // packages/mcps/src/mcp/e2e/contracts/index.ts
2856
2908
  var PaginationInputSchema = z.object({
2857
- page: z.number().int().positive().optional().describe("Page number (1-based)"),
2858
- pageSize: z.number().int().positive().max(100).optional().describe("Number of items per page")
2909
+ page: z.number().int().positive().default(1).describe("Page number, 1-based. Defaults to 1 (the first page)."),
2910
+ pageSize: z.number().int().positive().max(100).default(10).describe("Number of items per page. Defaults to 10, max 100."),
2911
+ sortBy: z.enum(["createdAt", "updatedAt"]).default("createdAt").describe("Field to sort by. Defaults to createdAt (stable under concurrent writes)."),
2912
+ sortOrder: z.enum(["asc", "desc"]).default("desc").describe("Sort direction. Defaults to desc (newest first).")
2859
2913
  });
2860
2914
  var IdSchema = MuggleEntityIdSchema;
2861
2915
  var RunBatchIdSchema = MuggleEntityIdSchema.describe("Bulk replay run batch ID (UUID)");
@@ -2961,15 +3015,67 @@ var UseCasePromptPreviewInputSchema = z.object({
2961
3015
  });
2962
3016
  var UseCaseCreateFromPromptsInputSchema = z.object({
2963
3017
  projectId: IdSchema.describe("Project ID (UUID) to create use cases for"),
2964
- prompts: z.array(z.object({
2965
- instruction: z.string().min(1).describe("Natural language instruction describing the use case")
2966
- })).min(1).describe("Array of prompts to generate use cases from")
3018
+ instructions: z.array(z.string().min(1)).min(1).describe('Natural-language instructions \u2014 one use case is generated per string (e.g., ["As a user, I can log in"])')
2967
3019
  });
2968
3020
  var UseCaseUpdateFromPromptInputSchema = z.object({
2969
3021
  projectId: IdSchema.describe("Project ID (UUID)"),
2970
3022
  useCaseId: IdSchema.describe("Use case ID (UUID) to update"),
2971
3023
  instruction: z.string().min(1).describe("Natural language instruction to regenerate the use case from")
2972
3024
  });
3025
+ var UseCaseCreateInputSchema = z.object({
3026
+ projectId: IdSchema.describe("Project ID (UUID) the use case belongs to"),
3027
+ title: z.string().min(1).describe("Use case title"),
3028
+ description: z.string().min(1).describe("Description of the use case, including actor and preconditions"),
3029
+ userStory: z.string().min(1).describe("One-line user story from the end-user point of view"),
3030
+ url: z.string().url().optional().describe("URL where the use case takes place (defaults to project URL)"),
3031
+ useCaseBreakdown: z.array(z.object({
3032
+ requirement: z.string().min(1).describe("One requirement of the use case"),
3033
+ acceptanceCriteria: z.string().min(1).describe("Concrete, measurable acceptance criteria for the requirement")
3034
+ })).describe("Main/alternative/error flows broken down as requirement + acceptance criteria pairs"),
3035
+ status: z.enum(["DRAFT", "IN_REVIEW", "APPROVED", "IMPLEMENTED", "ARCHIVED"]).describe("Use case status"),
3036
+ priority: z.enum(["LOW", "MEDIUM", "HIGH", "CRITICAL"]).describe("Use case priority"),
3037
+ source: z.enum(["PRD_FILE", "SITEMAP", "CRAWLER", "PROMPT", "MANUAL"]).describe("How this use case was produced"),
3038
+ category: z.string().optional().describe("Optional category")
3039
+ });
3040
+ var BulkPreviewPromptSchema = z.object({
3041
+ clientRef: z.string().max(128).optional().describe("Optional caller-supplied reference echoed back on results"),
3042
+ instruction: z.string().min(1).max(4e3).describe("Natural language instruction (max 4000 chars)")
3043
+ });
3044
+ var BulkPreviewSubmitUseCaseInputSchema = z.object({
3045
+ projectId: IdSchema.describe("Project ID (UUID) the use cases belong to"),
3046
+ prompts: z.array(BulkPreviewPromptSchema).min(1).max(100).describe("Prompts to generate use cases from (max 100 per request)")
3047
+ });
3048
+ var BulkPreviewSubmitTestCaseInputSchema = z.object({
3049
+ projectId: IdSchema.describe("Project ID (UUID)"),
3050
+ useCaseId: IdSchema.describe("Parent use case ID (UUID) the test cases will belong to"),
3051
+ prompts: z.array(BulkPreviewPromptSchema).min(1).max(100).describe("Prompts to generate test cases from (max 100 per request)")
3052
+ });
3053
+ var BulkPreviewJobStatusSchema = z.enum([
3054
+ "queued",
3055
+ "submitted",
3056
+ "running",
3057
+ "succeeded",
3058
+ "partial",
3059
+ "failed",
3060
+ "cancelled",
3061
+ "expired"
3062
+ ]);
3063
+ var BulkPreviewJobKindSchema = z.enum(["useCase", "testCase"]);
3064
+ var BulkPreviewJobGetInputSchema = z.object({
3065
+ projectId: IdSchema.describe("Project ID (UUID)"),
3066
+ jobId: IdSchema.describe("Bulk-preview job ID (UUID)")
3067
+ });
3068
+ var BulkPreviewJobCancelInputSchema = z.object({
3069
+ projectId: IdSchema.describe("Project ID (UUID)"),
3070
+ jobId: IdSchema.describe("Bulk-preview job ID (UUID) to cancel")
3071
+ });
3072
+ var BulkPreviewJobListInputSchema = z.object({
3073
+ projectId: IdSchema.describe("Project ID (UUID) to list bulk-preview jobs for"),
3074
+ status: z.array(BulkPreviewJobStatusSchema).optional().describe("Optional filter \u2014 only return jobs matching any of these statuses"),
3075
+ kind: BulkPreviewJobKindSchema.optional().describe("Optional filter \u2014 only return jobs of this kind"),
3076
+ limit: z.number().int().min(1).max(100).optional().describe("Max jobs to return (default 20, max 100)"),
3077
+ cursor: z.string().optional().describe("Pagination cursor returned by a previous call")
3078
+ });
2973
3079
  var TestCaseListInputSchema = z.object({
2974
3080
  projectId: IdSchema.describe("Project ID (UUID) to list test cases for")
2975
3081
  }).merge(PaginationInputSchema);
@@ -3006,9 +3112,6 @@ var TestScriptListInputSchema = z.object({
3006
3112
  var TestScriptGetInputSchema = z.object({
3007
3113
  testScriptId: IdSchema.describe("Test script ID (UUID) to retrieve")
3008
3114
  });
3009
- var TestScriptListPaginatedInputSchema = z.object({
3010
- projectId: IdSchema.describe("Project ID (UUID) to list test scripts for")
3011
- }).merge(PaginationInputSchema);
3012
3115
  var ActionScriptGetInputSchema = z.object({
3013
3116
  actionScriptId: IdSchema.describe("Action script ID (UUID) to retrieve")
3014
3117
  });
@@ -3180,6 +3283,61 @@ var GatewayError = class extends Error {
3180
3283
  this.details = params.details;
3181
3284
  }
3182
3285
  };
3286
+
3287
+ // packages/mcps/src/shared/host_detection.ts
3288
+ function detectMcpHost(env = process.env) {
3289
+ if (env.CLAUDE_CODE || env.CLAUDECODE || env.CLAUDE_CODE_SSE_PORT) {
3290
+ return "claude-code";
3291
+ }
3292
+ if (env.CURSOR_TRACE_ID || env.CURSOR_MCP) {
3293
+ return "cursor";
3294
+ }
3295
+ if (env.CODEX || env.OPENAI_CODEX) {
3296
+ return "codex";
3297
+ }
3298
+ if (env.WINDSURF_MCP || env.WINDSURF || env.CODEIUM_API_KEY) {
3299
+ return "windsurf";
3300
+ }
3301
+ return "unknown";
3302
+ }
3303
+ var INSTALL_ID_FILENAME = "install-id.json";
3304
+ var cachedInstallId;
3305
+ function installIdFilePath() {
3306
+ return join(getDataDir(), INSTALL_ID_FILENAME);
3307
+ }
3308
+ function generateAndPersist(filePath) {
3309
+ const installId = randomUUID();
3310
+ const contents = { installId };
3311
+ mkdirSync(dirname(filePath), { recursive: true });
3312
+ writeFileSync(filePath, JSON.stringify(contents, null, 2) + "\n", "utf8");
3313
+ return installId;
3314
+ }
3315
+ function isValid(value) {
3316
+ return typeof value === "object" && value !== null && typeof value.installId === "string" && value.installId.length > 0;
3317
+ }
3318
+ function getInstallId() {
3319
+ if (cachedInstallId !== void 0) return cachedInstallId;
3320
+ const filePath = installIdFilePath();
3321
+ if (!existsSync(filePath)) {
3322
+ cachedInstallId = generateAndPersist(filePath);
3323
+ return cachedInstallId;
3324
+ }
3325
+ try {
3326
+ const raw = readFileSync(filePath, "utf8");
3327
+ const parsed = JSON.parse(raw);
3328
+ if (isValid(parsed)) {
3329
+ cachedInstallId = parsed.installId;
3330
+ return cachedInstallId;
3331
+ }
3332
+ cachedInstallId = generateAndPersist(filePath);
3333
+ return cachedInstallId;
3334
+ } catch {
3335
+ cachedInstallId = generateAndPersist(filePath);
3336
+ return cachedInstallId;
3337
+ }
3338
+ }
3339
+
3340
+ // packages/mcps/src/mcp/e2e/upstream-client.ts
3183
3341
  var ALLOWED_UPSTREAM_PREFIXES = [
3184
3342
  "/v1/protected/muggle-test/",
3185
3343
  "/v1/protected/wallet/",
@@ -3226,8 +3384,17 @@ var PromptServiceClient = class {
3226
3384
  * @returns Headers object.
3227
3385
  */
3228
3386
  buildHeaders(credentials, correlationId) {
3387
+ const manifest = readReleaseManifest();
3388
+ const installId = getInstallId();
3389
+ const host = detectMcpHost();
3229
3390
  const headers = {
3230
- "X-Correlation-Id": correlationId
3391
+ "X-Correlation-Id": correlationId,
3392
+ "X-Client-Service-Name": manifest.serviceName,
3393
+ "X-Client-Release": manifest.release,
3394
+ "X-Client-Build-Id": manifest.buildId,
3395
+ "X-Client-Commit-Sha": manifest.commitSha,
3396
+ "X-Client-Install-Id": installId,
3397
+ "X-Client-Host": host
3231
3398
  };
3232
3399
  if (credentials.bearerToken) {
3233
3400
  headers["Authorization"] = credentials.bearerToken.startsWith("Bearer ") ? credentials.bearerToken : `Bearer ${credentials.bearerToken}`;
@@ -3476,14 +3643,19 @@ var projectTools = [
3476
3643
  },
3477
3644
  {
3478
3645
  name: "muggle-remote-project-list",
3479
- description: "List all projects accessible to the authenticated user.",
3646
+ description: "List projects accessible to the authenticated user. Returns up to 10 items per page by default (max 100). Response includes pagination metadata (totalCount, totalPages, hasMore) \u2014 check `hasMore` to decide whether to fetch additional pages.",
3480
3647
  inputSchema: ProjectListInputSchema,
3481
3648
  mapToUpstream: (input) => {
3482
3649
  const data = input;
3483
3650
  return {
3484
3651
  method: "GET",
3485
3652
  path: `${MUGGLE_TEST_PREFIX}/projects`,
3486
- queryParams: { page: data.page, pageSize: data.pageSize }
3653
+ queryParams: {
3654
+ page: data.page,
3655
+ pageSize: data.pageSize,
3656
+ sortBy: data.sortBy,
3657
+ sortOrder: data.sortOrder
3658
+ }
3487
3659
  };
3488
3660
  }
3489
3661
  },
@@ -3528,14 +3700,20 @@ var useCaseTools = [
3528
3700
  },
3529
3701
  {
3530
3702
  name: "muggle-remote-use-case-list",
3531
- description: "List all use cases for a project.",
3703
+ description: "List use cases for a project. Returns up to 10 items per page by default (max 100). Response includes pagination metadata (totalCount, totalPages, hasMore) \u2014 check `hasMore` to decide whether to fetch additional pages.",
3532
3704
  inputSchema: UseCaseListInputSchema,
3533
3705
  mapToUpstream: (input) => {
3534
3706
  const data = input;
3535
3707
  return {
3536
3708
  method: "GET",
3537
3709
  path: `${MUGGLE_TEST_PREFIX}/use-cases`,
3538
- queryParams: { projectId: data.projectId, page: data.page, pageSize: data.pageSize }
3710
+ queryParams: {
3711
+ projectId: data.projectId,
3712
+ page: data.page,
3713
+ pageSize: data.pageSize,
3714
+ sortBy: data.sortBy,
3715
+ sortOrder: data.sortOrder
3716
+ }
3539
3717
  };
3540
3718
  }
3541
3719
  },
@@ -3573,7 +3751,10 @@ var useCaseTools = [
3573
3751
  return {
3574
3752
  method: "POST",
3575
3753
  path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/use-cases/prompts/bulk`,
3576
- body: { projectId: data.projectId, prompts: data.prompts }
3754
+ body: {
3755
+ projectId: data.projectId,
3756
+ prompts: data.instructions.map((instruction) => ({ instruction }))
3757
+ }
3577
3758
  };
3578
3759
  }
3579
3760
  },
@@ -3589,19 +3770,62 @@ var useCaseTools = [
3589
3770
  body: { instruction: data.instruction }
3590
3771
  };
3591
3772
  }
3773
+ },
3774
+ {
3775
+ name: "muggle-remote-use-case-create",
3776
+ description: "Create a single use case from a fully-specified payload. Use this to persist use cases returned by muggle-remote-use-case-bulk-preview-submit \u2014 no LLM is invoked.",
3777
+ inputSchema: UseCaseCreateInputSchema,
3778
+ mapToUpstream: (input) => {
3779
+ const data = input;
3780
+ return {
3781
+ method: "POST",
3782
+ path: `${MUGGLE_TEST_PREFIX}/use-cases`,
3783
+ body: {
3784
+ projectId: data.projectId,
3785
+ title: data.title,
3786
+ description: data.description,
3787
+ userStory: data.userStory,
3788
+ url: data.url,
3789
+ useCaseBreakdown: data.useCaseBreakdown,
3790
+ status: data.status,
3791
+ priority: data.priority,
3792
+ source: data.source,
3793
+ category: data.category
3794
+ }
3795
+ };
3796
+ }
3797
+ },
3798
+ {
3799
+ name: "muggle-remote-use-case-bulk-preview-submit",
3800
+ description: "Submit an async bulk-preview job that uses the OpenAI Batch API to generate use cases from many prompts at ~50% of normal LLM cost. Returns a job ID immediately; poll with muggle-remote-bulk-preview-job-get until the job reaches a terminal status, then persist each successful result via muggle-remote-use-case-create.",
3801
+ inputSchema: BulkPreviewSubmitUseCaseInputSchema,
3802
+ mapToUpstream: (input) => {
3803
+ const data = input;
3804
+ return {
3805
+ method: "POST",
3806
+ path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/use-cases/prompts/bulk-preview`,
3807
+ body: { prompts: data.prompts }
3808
+ };
3809
+ }
3592
3810
  }
3593
3811
  ];
3594
3812
  var testCaseTools = [
3595
3813
  {
3596
3814
  name: "muggle-remote-test-case-list",
3597
- description: "List test cases for a project.",
3815
+ description: "List test cases for a project. Returns up to 10 items per page by default (max 100). Response includes pagination metadata (totalCount, totalPages, hasMore) \u2014 check `hasMore` to decide whether to fetch additional pages.",
3598
3816
  inputSchema: TestCaseListInputSchema,
3599
3817
  mapToUpstream: (input) => {
3600
3818
  const data = input;
3601
3819
  return {
3602
3820
  method: "GET",
3603
3821
  path: `${MUGGLE_TEST_PREFIX}/test-cases`,
3604
- queryParams: { projectId: data.projectId, page: data.page, pageSize: data.pageSize }
3822
+ queryParams: {
3823
+ projectId: data.projectId,
3824
+ page: data.page,
3825
+ pageSize: data.pageSize,
3826
+ sortBy: data.sortBy,
3827
+ sortOrder: data.sortOrder
3828
+ }
3605
3829
  };
3606
3830
  }
3607
3831
  },
@@ -3668,44 +3892,95 @@ var testCaseTools = [
3668
3892
  }
3669
3893
  };
3670
3894
  }
3895
+ },
3896
+ {
3897
+ name: "muggle-remote-test-case-bulk-preview-submit",
3898
+ description: "Submit an async bulk-preview job that uses the OpenAI Batch API to generate test cases for a single use case from many prompts at ~50% of normal LLM cost. Returns a job ID immediately; poll with muggle-remote-bulk-preview-job-get until the job reaches a terminal status, then persist each successful result via muggle-remote-test-case-create. Note: one input prompt may fan out to 1\u20135 test cases.",
3899
+ inputSchema: BulkPreviewSubmitTestCaseInputSchema,
3900
+ mapToUpstream: (input) => {
3901
+ const data = input;
3902
+ return {
3903
+ method: "POST",
3904
+ path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/use-cases/${data.useCaseId}/test-cases/prompts/bulk-preview`,
3905
+ body: { prompts: data.prompts }
3906
+ };
3907
+ }
3671
3908
  }
3672
3909
  ];
3673
- var testScriptTools = [
3910
+ var bulkPreviewTools = [
3674
3911
  {
3675
- name: "muggle-remote-test-script-list",
3676
- description: "List test scripts for a project, optionally filtered by test case.",
3677
- inputSchema: TestScriptListInputSchema,
3912
+ name: "muggle-remote-bulk-preview-job-get",
3913
+ description: "Get the current status and (when terminal) results of a bulk-preview job. Poll this after submitting a bulk-preview job \u2014 every 10\u201315 seconds is fine. Terminal statuses: succeeded, partial, failed, cancelled, expired.",
3914
+ inputSchema: BulkPreviewJobGetInputSchema,
3678
3915
  mapToUpstream: (input) => {
3679
3916
  const data = input;
3680
3917
  return {
3681
3918
  method: "GET",
3682
- path: `${MUGGLE_TEST_PREFIX}/test-scripts`,
3683
- queryParams: { projectId: data.projectId, testCaseId: data.testCaseId, page: data.page, pageSize: data.pageSize }
3919
+ path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/bulk-preview-jobs/${data.jobId}`
3684
3920
  };
3685
3921
  }
3686
3922
  },
3687
3923
  {
3688
- name: "muggle-remote-test-script-get",
3689
- description: "Get details of a specific test script.",
3690
- inputSchema: TestScriptGetInputSchema,
3924
+ name: "muggle-remote-bulk-preview-job-list",
3925
+ description: "List bulk-preview jobs for a project, optionally filtered by status or kind.",
3926
+ inputSchema: BulkPreviewJobListInputSchema,
3691
3927
  mapToUpstream: (input) => {
3692
3928
  const data = input;
3693
3929
  return {
3694
3930
  method: "GET",
3695
- path: `${MUGGLE_TEST_PREFIX}/test-scripts/${data.testScriptId}`
3931
+ path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/bulk-preview-jobs`,
3932
+ queryParams: {
3933
+ status: data.status?.join(","),
3934
+ kind: data.kind,
3935
+ limit: data.limit,
3936
+ cursor: data.cursor
3937
+ }
3938
+ };
3939
+ }
3940
+ },
3941
+ {
3942
+ name: "muggle-remote-bulk-preview-job-cancel",
3943
+ description: "Request cancellation of a bulk-preview job. Cancellation is cooperative \u2014 the harvester picks it up on its next tick and moves the job to status=cancelled.",
3944
+ inputSchema: BulkPreviewJobCancelInputSchema,
3945
+ mapToUpstream: (input) => {
3946
+ const data = input;
3947
+ return {
3948
+ method: "DELETE",
3949
+ path: `${MUGGLE_TEST_PREFIX}/projects/${data.projectId}/bulk-preview-jobs/${data.jobId}`
3950
+ };
3951
+ }
3952
+ }
3953
+ ];
3954
+ var testScriptTools = [
3955
+ {
3956
+ name: "muggle-remote-test-script-list",
3957
+ description: "List test scripts for a project, optionally filtered by test case. Returns up to 10 items per page by default (max 100). Response includes pagination metadata (totalCount, totalPages, hasMore) \u2014 check `hasMore` to decide whether to fetch additional pages.",
3958
+ inputSchema: TestScriptListInputSchema,
3959
+ mapToUpstream: (input) => {
3960
+ const data = input;
3961
+ return {
3962
+ method: "GET",
3963
+ path: `${MUGGLE_TEST_PREFIX}/test-scripts`,
3964
+ queryParams: {
3965
+ projectId: data.projectId,
3966
+ testCaseId: data.testCaseId,
3967
+ page: data.page,
3968
+ pageSize: data.pageSize,
3969
+ sortBy: data.sortBy,
3970
+ sortOrder: data.sortOrder
3971
+ }
3696
3972
  };
3697
3973
  }
3698
3974
  },
3699
3975
  {
3700
- name: "muggle-remote-test-script-list-paginated",
3701
- description: "List test scripts with full pagination support.",
3702
- inputSchema: TestScriptListPaginatedInputSchema,
3976
+ name: "muggle-remote-test-script-get",
3977
+ description: "Get details of a specific test script.",
3978
+ inputSchema: TestScriptGetInputSchema,
3703
3979
  mapToUpstream: (input) => {
3704
3980
  const data = input;
3705
3981
  return {
3706
3982
  method: "GET",
3707
- path: `${MUGGLE_TEST_PREFIX}/test-scripts/paginated`,
3708
- queryParams: { projectId: data.projectId, page: data.page, pageSize: data.pageSize }
3983
+ path: `${MUGGLE_TEST_PREFIX}/test-scripts/${data.testScriptId}`
3709
3984
  };
3710
3985
  }
3711
3986
  }
@@ -4691,6 +4966,7 @@ var allQaToolDefinitions = [
4691
4966
  ...projectTools,
4692
4967
  ...useCaseTools,
4693
4968
  ...testCaseTools,
4969
+ ...bulkPreviewTools,
4694
4970
  ...testScriptTools,
4695
4971
  ...actionScriptTools,
4696
4972
  ...workflowTools,
@@ -5985,6 +6261,281 @@ async function startStdioServer(server) {
5985
6261
  process.on("SIGTERM", () => shutdown("SIGTERM"));
5986
6262
  process.on("SIGINT", () => shutdown("SIGINT"));
5987
6263
  }
6264
+
6265
+ // src/cli/pr-section/selectors.ts
6266
+ var ONE_LINER_BUDGET = 160;
6267
+ function selectHero(report) {
6268
+ const firstFailed = report.tests.find(
6269
+ (t) => t.status === "failed"
6270
+ );
6271
+ if (firstFailed) {
6272
+ const step = firstFailed.steps.find((s) => s.stepIndex === firstFailed.failureStepIndex);
6273
+ if (step) {
6274
+ return {
6275
+ screenshotUrl: step.screenshotUrl,
6276
+ testName: firstFailed.name,
6277
+ kind: "failure"
6278
+ };
6279
+ }
6280
+ }
6281
+ const firstPassedWithSteps = report.tests.find(
6282
+ (t) => t.status === "passed" && t.steps.length > 0
6283
+ );
6284
+ if (firstPassedWithSteps) {
6285
+ const lastStep = firstPassedWithSteps.steps[firstPassedWithSteps.steps.length - 1];
6286
+ return {
6287
+ screenshotUrl: lastStep.screenshotUrl,
6288
+ testName: firstPassedWithSteps.name,
6289
+ kind: "final"
6290
+ };
6291
+ }
6292
+ return null;
6293
+ }
6294
+ function buildOneLiner(report) {
6295
+ const total = report.tests.length;
6296
+ if (total === 0) {
6297
+ return "No acceptance tests were executed.";
6298
+ }
6299
+ const failed = report.tests.filter((t) => t.status === "failed");
6300
+ if (failed.length === 0) {
6301
+ return `All ${total} acceptance tests passed.`;
6302
+ }
6303
+ const first = failed[0];
6304
+ const prefix = `${failed.length} of ${total} failed \u2014 "${first.name}" broke at step ${first.failureStepIndex}: `;
6305
+ const available = ONE_LINER_BUDGET - prefix.length - 1;
6306
+ const error = first.error.length > available ? first.error.slice(0, Math.max(0, available - 1)) + "\u2026" : first.error;
6307
+ return `${prefix}${error}.`;
6308
+ }
6309
+
6310
+ // src/cli/pr-section/render.ts
6311
+ var DASHBOARD_URL_BASE = "https://www.muggle-ai.com/muggleTestV0/dashboard/projects";
6312
+ var ROW_THUMB_WIDTH = 120;
6313
+ var DETAIL_THUMB_WIDTH = 200;
6314
+ var HERO_WIDTH = 480;
6315
+ function thumbnail(url, width) {
6316
+ return `<a href="${url}"><img src="${url}" width="${width}"></a>`;
6317
+ }
6318
+ function counts(report) {
6319
+ const passed = report.tests.filter((t) => t.status === "passed").length;
6320
+ const failed = report.tests.filter((t) => t.status === "failed").length;
6321
+ return { passed, failed, text: `**${passed} passed / ${failed} failed**` };
6322
+ }
6323
+ function renderSummary(report) {
6324
+ const { text: countsLine } = counts(report);
6325
+ const oneLiner = buildOneLiner(report);
6326
+ const hero = selectHero(report);
6327
+ const dashboard = `${DASHBOARD_URL_BASE}/${report.projectId}/scripts`;
6328
+ const lines = [
6329
+ countsLine,
6330
+ "",
6331
+ oneLiner,
6332
+ ""
6333
+ ];
6334
+ if (hero) {
6335
+ lines.push(
6336
+ `<a href="${hero.screenshotUrl}"><img src="${hero.screenshotUrl}" width="${HERO_WIDTH}" alt="${hero.testName}"></a>`,
6337
+ ""
6338
+ );
6339
+ }
6340
+ lines.push(`[View project dashboard on muggle-ai.com](${dashboard})`);
6341
+ return lines.join("\n");
6342
+ }
6343
+ function renderRow(test) {
6344
+ const link = `[${test.name}](${test.viewUrl})`;
6345
+ if (test.status === "passed") {
6346
+ const lastStep = test.steps[test.steps.length - 1];
6347
+ const thumb2 = lastStep ? thumbnail(lastStep.screenshotUrl, ROW_THUMB_WIDTH) : "\u2014";
6348
+ return `| ${link} | \u2705 PASSED | ${thumb2} |`;
6349
+ }
6350
+ const failStep = test.steps.find((s) => s.stepIndex === test.failureStepIndex);
6351
+ const thumb = failStep ? thumbnail(failStep.screenshotUrl, ROW_THUMB_WIDTH) : "\u2014";
6352
+ return `| ${link} | \u274C FAILED \u2014 ${test.error} | ${thumb} |`;
6353
+ }
6354
+ function renderFailureDetails(test) {
6355
+ const stepCount = test.steps.length;
6356
+ const header2 = `<details>
6357
+ <summary>\u{1F4F8} <strong>${test.name}</strong> \u2014 ${stepCount} steps (failed at step ${test.failureStepIndex})</summary>
6358
+
6359
+ | # | Action | Screenshot |
6360
+ |---|--------|------------|`;
6361
+ const rows = test.steps.map((step) => renderFailureStepRow(step, test)).join("\n");
6362
+ return `${header2}
6363
+ ${rows}
6364
+
6365
+ </details>`;
6366
+ }
6367
+ function renderFailureStepRow(step, test) {
6368
+ const isFailure = step.stepIndex === test.failureStepIndex;
6369
+ const marker = isFailure ? `${step.stepIndex} \u26A0\uFE0F` : String(step.stepIndex);
6370
+ const action = isFailure ? `${step.action} \u2014 **${test.error}**` : step.action;
6371
+ return `| ${marker} | ${action} | ${thumbnail(step.screenshotUrl, DETAIL_THUMB_WIDTH)} |`;
6372
+ }
6373
+ function renderRowsTable(report) {
6374
+ if (report.tests.length === 0) {
6375
+ return "_No tests were executed._";
6376
+ }
6377
+ const header2 = "| Test Case | Status | Evidence |\n|-----------|--------|----------|";
6378
+ const rows = report.tests.map(renderRow).join("\n");
6379
+ return `${header2}
6380
+ ${rows}`;
6381
+ }
6382
+ function renderBody(report, opts) {
6383
+ const sections = [
6384
+ "## E2E Acceptance Results",
6385
+ "",
6386
+ renderSummary(report),
6387
+ "",
6388
+ renderRowsTable(report)
6389
+ ];
6390
+ const failures = report.tests.filter((t) => t.status === "failed");
6391
+ if (failures.length > 0) {
6392
+ if (opts.inlineFailureDetails) {
6393
+ sections.push("", ...failures.map(renderFailureDetails));
6394
+ } else {
6395
+ sections.push(
6396
+ "",
6397
+ "_Full step-by-step evidence in the comment below \u2014 the PR description was too large to inline it._"
6398
+ );
6399
+ }
6400
+ }
6401
+ return sections.join("\n");
6402
+ }
6403
+ function renderComment(report) {
6404
+ const failures = report.tests.filter((t) => t.status === "failed");
6405
+ if (failures.length === 0) {
6406
+ return "";
6407
+ }
6408
+ const sections = [
6409
+ "## E2E acceptance evidence (overflow)",
6410
+ "",
6411
+ "_This comment was posted because the full step-by-step evidence did not fit in the PR description._",
6412
+ "",
6413
+ ...failures.map(renderFailureDetails)
6414
+ ];
6415
+ return sections.join("\n");
6416
+ }
6417
+
6418
+ // src/cli/pr-section/overflow.ts
6419
+ function splitWithOverflow(report, opts) {
6420
+ const inlineBody = renderBody(report, { inlineFailureDetails: true });
6421
+ const inlineBytes = Buffer.byteLength(inlineBody, "utf-8");
6422
+ if (inlineBytes <= opts.maxBodyBytes) {
6423
+ return { body: inlineBody, comment: null };
6424
+ }
6425
+ const spilledBody = renderBody(report, { inlineFailureDetails: false });
6426
+ const comment = renderComment(report);
6427
+ return {
6428
+ body: spilledBody,
6429
+ comment: comment.length > 0 ? comment : null
6430
+ };
6431
+ }
6432
+ var StepSchema = z.object({
6433
+ stepIndex: z.number().int().nonnegative(),
6434
+ action: z.string().min(1),
6435
+ screenshotUrl: z.string().url()
6436
+ });
6437
+ var PassedTestSchema = z.object({
6438
+ name: z.string().min(1),
6439
+ testCaseId: z.string().min(1),
6440
+ testScriptId: z.string().min(1).optional(),
6441
+ runId: z.string().min(1),
6442
+ viewUrl: z.string().url(),
6443
+ status: z.literal("passed"),
6444
+ steps: z.array(StepSchema)
6445
+ });
6446
+ var FailedTestSchema = z.object({
6447
+ name: z.string().min(1),
6448
+ testCaseId: z.string().min(1),
6449
+ testScriptId: z.string().min(1).optional(),
6450
+ runId: z.string().min(1),
6451
+ viewUrl: z.string().url(),
6452
+ status: z.literal("failed"),
6453
+ steps: z.array(StepSchema),
6454
+ failureStepIndex: z.number().int().nonnegative(),
6455
+ error: z.string().min(1),
6456
+ artifactsDir: z.string().min(1).optional()
6457
+ });
6458
+ var TestResultSchema = z.discriminatedUnion("status", [
6459
+ PassedTestSchema,
6460
+ FailedTestSchema
6461
+ ]);
6462
+ var E2eReportSchema = z.object({
6463
+ projectId: z.string().min(1),
6464
+ tests: z.array(TestResultSchema)
6465
+ });
6466
+
6467
+ // src/cli/pr-section/index.ts
6468
+ function buildPrSection(report, opts) {
6469
+ return splitWithOverflow(report, opts);
6470
+ }
6471
+
6472
+ // src/cli/build-pr-section.ts
6473
+ var DEFAULT_MAX_BODY_BYTES = 6e4;
6474
+ async function readAll(stream) {
6475
+ const chunks = [];
6476
+ for await (const chunk of stream) {
6477
+ chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
6478
+ }
6479
+ return Buffer.concat(chunks).toString("utf-8");
6480
+ }
6481
+ function errMsg(e) {
6482
+ return e instanceof Error ? e.message : String(e);
6483
+ }
6484
+ async function runBuildPrSection(opts) {
6485
+ let raw;
6486
+ try {
6487
+ raw = await readAll(opts.stdin);
6488
+ } catch (err) {
6489
+ opts.stderrWrite(`build-pr-section: failed to read stdin: ${errMsg(err)}
6490
+ `);
6491
+ return 1;
6492
+ }
6493
+ let json;
6494
+ try {
6495
+ json = JSON.parse(raw);
6496
+ } catch (err) {
6497
+ opts.stderrWrite(`build-pr-section: failed to parse stdin as JSON: ${errMsg(err)}
6498
+ `);
6499
+ return 1;
6500
+ }
6501
+ let report;
6502
+ try {
6503
+ report = E2eReportSchema.parse(json);
6504
+ } catch (err) {
6505
+ if (err instanceof ZodError) {
6506
+ opts.stderrWrite(
6507
+ `build-pr-section: report validation failed:
6508
+ ${err.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}
6509
+ `
6510
+ );
6511
+ } else {
6512
+ opts.stderrWrite(`build-pr-section: report validation failed: ${errMsg(err)}
6513
+ `);
6514
+ }
6515
+ return 1;
6516
+ }
6517
+ const result = buildPrSection(report, { maxBodyBytes: opts.maxBodyBytes });
6518
+ opts.stdoutWrite(JSON.stringify({ body: result.body, comment: result.comment }));
6519
+ return 0;
6520
+ }
6521
+ async function buildPrSectionCommand(options) {
6522
+ const maxBodyBytes = options.maxBodyBytes ? Number(options.maxBodyBytes) : DEFAULT_MAX_BODY_BYTES;
6523
+ if (!Number.isFinite(maxBodyBytes) || maxBodyBytes <= 0) {
6524
+ process.stderr.write(`build-pr-section: --max-body-bytes must be a positive number
6525
+ `);
6526
+ process.exitCode = 1;
6527
+ return;
6528
+ }
6529
+ const code = await runBuildPrSection({
6530
+ stdin: process.stdin,
6531
+ stdoutWrite: (s) => process.stdout.write(s),
6532
+ stderrWrite: (s) => process.stderr.write(s),
6533
+ maxBodyBytes
6534
+ });
6535
+ if (code !== 0) {
6536
+ process.exitCode = code;
6537
+ }
6538
+ }
5988
6539
  var logger7 = getLogger();
5989
6540
  var ELECTRON_APP_DIR2 = "electron-app";
5990
6541
  var CURSOR_SKILLS_DIR = ".cursor";
@@ -7362,6 +7913,7 @@ function createProgram() {
7362
7913
  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);
7363
7914
  program.command("logout").description("Clear stored credentials").action(logoutCommand);
7364
7915
  program.command("status").description("Show authentication status").action(statusCommand);
7916
+ 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);
7365
7917
  program.action(() => {
7366
7918
  helpCommand();
7367
7919
  });