@debugg-ai/debugg-ai-mcp 3.7.4 → 3.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -51,10 +51,38 @@ Runs an AI browser agent against your app. The agent navigates, interacts, and r
51
51
  | `credentialRole` | string | Pick a credential by role (e.g. `admin`, `guest`) |
52
52
  | `username` | string | Username for login (ephemeral — not persisted) |
53
53
  | `password` | string | Password for login (ephemeral — not persisted) |
54
+ | `loginCredentials` | array | Accounts for logins the agent hits **during** the task — `[{username, password, label?}]` |
55
+ | `useEnvironmentCredentials` | boolean | Default `true`. `false` forbids auto-filling the environment's stored credentials |
56
+ | `auth` | object | Auth precondition — `{precondition, entryUrl, deepUrl, environmentId, username, password}` |
54
57
  | `repoName` | string | Override auto-detected git repo name (e.g. `my-org/my-repo`) |
55
58
 
56
59
  One focused check per call. The agent has a ~25-step internal budget; split broader suites across multiple calls.
57
60
 
61
+ ##### Credentials: pass them as parameters, not prose
62
+
63
+ Naming an account only in `description` does **not** make the agent use it — it falls back to the environment's stored credential, and the app's rejection of the wrong account comes back looking like an application failure. Anything you pass as a parameter beats the environment default for **every** login in the run, not just the first:
64
+
65
+ - `username` / `password` (or `credentialId` / `credentialRole`) — the run's identity.
66
+ - `auth.username` / `auth.password` — pins the precondition login when you also use `auth.precondition: "login"`.
67
+ - `loginCredentials` — accounts for a login form the agent reaches **part-way through** the task. This is the one for flows like *set a password → get bounced to sign-in → log in as the account you just created*, where splitting into separate calls would lose browser state.
68
+
69
+ Set `useEnvironmentCredentials: false` when a silent fallback to the default test user would invalidate the check. The call is rejected if you opt out without naming an account, since the run would have no way to authenticate.
70
+
71
+ Results report the identity actually used, so a wrong one is visible rather than masquerading as a broken app:
72
+
73
+ ```json
74
+ "logins": [
75
+ { "username": "qa+invitefix@example.com", "source": "task", "submitted": true, "authenticated": true }
76
+ ],
77
+ "credentialWarning": {
78
+ "requested": "qa+invitefix@example.com",
79
+ "used": ["qatest123@example.com"],
80
+ "message": "This run signed in with an environment default credential even though '…' was specified. …"
81
+ }
82
+ ```
83
+
84
+ `source` is `task` | `explicit` | `credential_id` (an account you named) or `env` | `env_default` (the environment's stored account). `credentialWarning` appears only when you named an account and an environment default was used anyway. `loginError` appears when a named account could not be resolved and the run declined to substitute a different one.
85
+
58
86
  Every successful run returns a `browserSession` block alongside the screenshot — presigned S3 URLs for the captured **HAR** (full network trace) and **console log** (every JS console message). Use them to detect refetch loops, hydration errors, and other runtime issues that pass type-checks and unit tests:
59
87
 
60
88
  ```json
@@ -9,7 +9,7 @@ import { handleExternalServiceError } from '../utils/errors.js';
9
9
  import { fetchImageAsBase64, imageContentBlock, resourceLinkBlock, artifactResourceLinks } from '../utils/imageUtils.js';
10
10
  import { DebuggAIServerClient } from '../services/index.js';
11
11
  import { getEvalTemplateSlug } from '../services/workflows.js';
12
- import { adaptVerdict } from '../services/verdictAdapter.js';
12
+ import { adaptVerdict, isEnvironmentDefault } from '../services/verdictAdapter.js';
13
13
  import { TunnelProvisionError } from '../services/tunnels.js';
14
14
  import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
15
15
  import { detectRepoName } from '../utils/gitContext.js';
@@ -55,6 +55,27 @@ function findNgrokErrorMarker(parts) {
55
55
  }
56
56
  return undefined;
57
57
  }
58
+ // Credentials ride in `env` and `contextData.auth` on the way out. Log which
59
+ // accounts a run was given — that is the diagnostic that matters when a run
60
+ // signs in as the wrong user — but never their passwords.
61
+ function redactEnv(env) {
62
+ const out = { ...env };
63
+ if (out.password)
64
+ out.password = '[REDACTED]';
65
+ if (Array.isArray(out.taskCredentials)) {
66
+ out.taskCredentials = out.taskCredentials.map((c) => ({
67
+ username: c.username,
68
+ ...(c.label ? { label: c.label } : {}),
69
+ password: '[REDACTED]',
70
+ }));
71
+ }
72
+ return out;
73
+ }
74
+ function redactAuth(auth) {
75
+ if (!auth)
76
+ return undefined;
77
+ return auth.password ? { ...auth, password: '[REDACTED]' } : auth;
78
+ }
58
79
  // Concurrency control — max 2 simultaneous browser checks.
59
80
  // Additional requests queue and run when a slot opens.
60
81
  const MAX_CONCURRENT = 2;
@@ -344,6 +365,13 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
344
365
  auth.entryUrl = input.auth.entryUrl;
345
366
  if (input.auth.deepUrl)
346
367
  auth.deepUrl = input.auth.deepUrl;
368
+ // WHICH account the precondition logs in as. Absent → the environment's
369
+ // default credential (the pre-existing behaviour, correct for a caller
370
+ // that named nobody).
371
+ if (input.auth.username)
372
+ auth.username = input.auth.username;
373
+ if (input.auth.password)
374
+ auth.password = input.auth.password;
347
375
  if (Object.keys(auth).length > 0)
348
376
  contextData.auth = auth;
349
377
  }
@@ -359,8 +387,29 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
359
387
  env.username = input.username;
360
388
  if (input.password)
361
389
  env.password = input.password;
390
+ // Accounts for logins the agent hits mid-task. The backend keys these by
391
+ // username so the agent can sign in as the one the task names, instead of
392
+ // its only login affordance filling the environment's stored account.
393
+ if (input.loginCredentials && input.loginCredentials.length > 0) {
394
+ env.taskCredentials = input.loginCredentials.map(c => ({
395
+ username: c.username,
396
+ password: c.password,
397
+ ...(c.label ? { label: c.label } : {}),
398
+ }));
399
+ }
400
+ // Only send the opt-out when it's actually an opt-out — omitting it keeps
401
+ // the default (environment credentials remain available as a fallback).
402
+ if (input.useEnvironmentCredentials === false) {
403
+ env.useEnvironmentCredentials = false;
404
+ }
362
405
  // --- Execute ---
363
- logger.info('Sending contextData', { contextData, env: Object.keys(env).length > 0 ? env : undefined });
406
+ // Log the SHAPE of env, never its secrets. It now carries per-account
407
+ // passwords (taskCredentials), and this log line is not run through the
408
+ // logger's shallow top-level redaction.
409
+ logger.info('Sending contextData', {
410
+ contextData: { ...contextData, auth: redactAuth(contextData.auth) },
411
+ env: Object.keys(env).length > 0 ? redactEnv(env) : undefined,
412
+ });
364
413
  if (progressCallback) {
365
414
  await progressCallback({ progress: 3, total: TOTAL_STEPS, message: 'Queuing workflow execution...' });
366
415
  }
@@ -656,6 +705,32 @@ async function testPageChangesHandlerInner(input, context, rawProgressCallback)
656
705
  responsePayload.failureCategory = verdict.failureCategory;
657
706
  if (verdict.reason)
658
707
  responsePayload.reason = verdict.reason;
708
+ // --- Which identity did the run actually sign in as? ---
709
+ // A login as the wrong account and an app rejecting the right one produce
710
+ // the SAME symptom ("login failed") and used to be indistinguishable from
711
+ // the result, so an environment-default substitution read as an application
712
+ // bug. Relay the logins verbatim, and when the caller named an account but
713
+ // an environment default was used anyway, say so explicitly rather than
714
+ // leaving the caller to infer it from a failed check.
715
+ if (verdict.logins)
716
+ responsePayload.logins = verdict.logins;
717
+ if (verdict.loginError)
718
+ responsePayload.loginError = verdict.loginError;
719
+ const requestedIdentity = input.username
720
+ ?? input.auth?.username
721
+ ?? input.loginCredentials?.[0]?.username;
722
+ const substituted = (verdict.logins ?? []).filter(isEnvironmentDefault);
723
+ if (requestedIdentity && substituted.length > 0) {
724
+ responsePayload.credentialWarning = {
725
+ requested: requestedIdentity,
726
+ used: substituted.map(l => l.username).filter(Boolean),
727
+ message: `This run signed in with an environment default credential even though ` +
728
+ `'${requestedIdentity}' was specified. Treat a login failure here as a ` +
729
+ `credential-resolution problem, not an application failure.`,
730
+ };
731
+ logger.warn(`check_app_in_browser: requested identity '${requestedIdentity}' but the run used ` +
732
+ `environment-default credential(s): ${substituted.map(l => l.username).join(', ')}`);
733
+ }
659
734
  // Bug z15n: OUR tunnel died mid-run, so this 'fail' describes our error page,
660
735
  // not the user's app. Relay it as a distinct, retryable infrastructure class
661
736
  // so an automated caller doesn't record a UI regression that never happened.
@@ -20,6 +20,7 @@ import { probeLocalPort, probeTunnelHealth } from '../utils/localReachability.js
20
20
  import { extractLocalhostPort } from '../utils/urlParser.js';
21
21
  import { resolveTargetUrl, buildContext, findExistingTunnel, ensureTunnel, sanitizeResponseUrls, touchTunnelById, } from '../utils/tunnelContext.js';
22
22
  import { getCachedTemplateUuid, invalidateTemplateCache } from '../utils/handlerCaches.js';
23
+ import { detectLocalGitRef } from '../utils/gitContext.js';
23
24
  import { getCrawlTemplateSlug } from '../services/workflows.js';
24
25
  import { isTransientWorkflowError, transientReasonTag } from '../utils/transientErrors.js';
25
26
  import { Telemetry, TelemetryEvents } from '../utils/telemetry.js';
@@ -179,6 +180,18 @@ export async function triggerCrawlHandler(input, context, rawProgressCallback) {
179
180
  contextData.headless = true; // D7: the MCP always runs headless — no opt-out.
180
181
  if (typeof input.timeoutSeconds === 'number')
181
182
  contextData.timeoutSeconds = input.timeoutSeconds;
183
+ // sentinal-lwtaw.13 (MCP side): the TRIGGER POINT owns the git fact. Attach
184
+ // the LOCAL checkout's branch + commit so the backend mints a git-backed
185
+ // Atlas version for this crawl — SAME snake_case contextData keys the
186
+ // PR-webhook path reads (commit_sha, branch). Best-effort + non-fatal:
187
+ // detectLocalGitRef never throws and a non-git target attaches nothing
188
+ // (honest no-git) and still crawls. Keys are set ONLY when present so we
189
+ // never fabricate a ref.
190
+ const gitRef = await detectLocalGitRef();
191
+ if (gitRef.commitSha)
192
+ contextData.commit_sha = gitRef.commitSha;
193
+ if (gitRef.branch)
194
+ contextData.branch = gitRef.branch;
182
195
  const env = {};
183
196
  if (input.environmentId)
184
197
  env.environmentId = input.environmentId;
@@ -25,6 +25,12 @@
25
25
  /** The verdict enum the backend emits (bead sentinal-k8x1f.2). */
26
26
  export const KNOWN_OUTCOMES = ['pass', 'fail', 'inconclusive', 'error', 'timeout'];
27
27
  const KNOWN = new Set(KNOWN_OUTCOMES);
28
+ /** Credential sources that mean "the caller named this account for this run". */
29
+ const CALLER_SPECIFIED_SOURCES = new Set(['task', 'explicit', 'credential_id']);
30
+ /** True when this login used an environment default rather than a named account. */
31
+ export function isEnvironmentDefault(login) {
32
+ return !!login.source && !CALLER_SPECIFIED_SOURCES.has(login.source);
33
+ }
28
34
  /**
29
35
  * Map a workflow execution onto the MCP relay verdict. Never throws.
30
36
  */
@@ -69,5 +75,10 @@ export function adaptVerdict(execution, opts = {}) {
69
75
  relay.screenshot = evidence.screenshot;
70
76
  if (Array.isArray(evidence?.actionTrace))
71
77
  relay.actionTrace = evidence.actionTrace;
78
+ if (Array.isArray(evidence?.logins) && evidence.logins.length > 0)
79
+ relay.logins = evidence.logins;
80
+ if (evidence?.loginError && typeof evidence.loginError === 'object') {
81
+ relay.loginError = evidence.loginError;
82
+ }
72
83
  return relay;
73
84
  }
@@ -10,7 +10,9 @@ const BASE_DESCRIPTION = `Give an AI agent eyes on a live website or app. The ag
10
10
 
11
11
  LOCALHOST SUPPORT: Pass any localhost URL (e.g. http://localhost:3000) and it Just Works. A secure tunnel is automatically created so the remote browser can reach your local dev server — no manual ngrok setup, no port forwarding, no config.
12
12
 
13
- SCOPE PER CALL: Keep each call to ONE focused check — a single page or a short interaction on a single screen (login, submit a form, verify a heading). For anything spanning multiple pages or long multi-step flows, split into SEPARATE calls — the remote browser agent has a ~25-step internal budget per call, and long single calls risk client-side timeouts. Example: instead of "log in, then go to settings, then update profile, then verify," make three calls: (1) log in & verify dashboard, (2) update settings, (3) verify profile change.`;
13
+ SCOPE PER CALL: Keep each call to ONE focused check — a single page or a short interaction on a single screen (login, submit a form, verify a heading). For anything spanning multiple pages or long multi-step flows, split into SEPARATE calls — the remote browser agent has a ~25-step internal budget per call, and long single calls risk client-side timeouts. Example: instead of "log in, then go to settings, then update profile, then verify," make three calls: (1) log in & verify dashboard, (2) update settings, (3) verify profile change.
14
+
15
+ CREDENTIALS: pass them as PARAMETERS, not only in the description. Naming an account in \`description\` alone does not make the agent use it — it falls back to the environment's stored credential. Use \`username\`/\`password\` (or \`credentialId\`) for the run's identity, \`auth.username\`/\`auth.password\` to pin the precondition login, and \`loginCredentials\` for accounts the agent must use at a login form it hits PART-WAY through the task (e.g. set a password → bounced to sign-in → log in as the account you just created). Anything you specify beats the environment's default for every login in the run; the result reports the identity actually used under \`logins\`.`;
14
16
  /**
15
17
  * Build the dynamic tool description including available environments/credentials.
16
18
  */
@@ -76,24 +78,50 @@ export function buildTestPageChangesTool(ctx) {
76
78
  },
77
79
  username: {
78
80
  type: "string",
79
- description: "A real, existing account email for the target app. Do NOT invent or guess credentials — use one from the available credentials listed above, or ask the user. The browser agent will type this into the login form."
81
+ description: "A real, existing account email for the target app. Do NOT invent or guess credentials — use one from the available credentials listed above, or ask the user. The browser agent will type this into the login form. Takes precedence over the environment's default credential for EVERY login in the run."
80
82
  },
81
83
  password: {
82
84
  type: "string",
83
85
  description: "The real password for the username above. Do NOT guess or use placeholder passwords — use credentials from the list above or ask the user."
84
86
  },
87
+ loginCredentials: {
88
+ type: "array",
89
+ description: "Accounts the agent may sign in as when it hits a login form DURING the task — not just the first login. Use this for flows that authenticate part-way through, e.g. set a password, get bounced to sign-in, then log in as the account you just provisioned. Stating credentials only in `description` is not enough: pass them here and the agent uses exactly these values. Overrides the environment's default credential.",
90
+ items: {
91
+ type: "object",
92
+ properties: {
93
+ username: { type: "string", description: "Account email/username to type into the login form." },
94
+ password: { type: "string", description: "That account's password." },
95
+ label: { type: "string", description: "Optional human label (e.g. 'newly invited user') to disambiguate in the task text." }
96
+ },
97
+ required: ["username", "password"],
98
+ additionalProperties: false
99
+ }
100
+ },
101
+ useEnvironmentCredentials: {
102
+ type: "boolean",
103
+ description: "Default true. Set false to forbid the agent from ever auto-filling the environment's stored credentials — it signs in only as an account this call named (username/password, credentialId, credentialRole, loginCredentials, or auth.username), or not at all. Use when a run must prove a SPECIFIC account's experience and a silent fallback to the default test user would invalidate it."
104
+ },
85
105
  repoName: {
86
106
  type: "string",
87
107
  description: "GitHub repository name (e.g. 'my-org/my-repo'). Auto-detected from the current git repo — only provide this if you want to run against a different project than the one you're in."
88
108
  },
89
109
  auth: {
90
110
  type: "object",
91
- description: "Optional auth-precondition for a 'log in THEN deep-navigate' check. Set precondition:'login' to authenticate first (using the environment's credentials), then land on deepUrl. Use this instead of hoping the agent signs itself in at a login wall.",
111
+ description: "Optional auth-precondition for a 'log in THEN deep-navigate' check. Set precondition:'login' to authenticate first, then land on deepUrl. Use this instead of hoping the agent signs itself in at a login wall. Pass username/password here to pin WHICH account it authenticates as; omit them to use the environment's default credential.",
92
112
  properties: {
93
113
  environmentId: {
94
114
  type: "string",
95
115
  description: "UUID of the environment whose credentials to log in with. See available environments in the tool description above."
96
116
  },
117
+ username: {
118
+ type: "string",
119
+ description: "Account to authenticate as for the precondition login. Overrides the environment's default credential."
120
+ },
121
+ password: {
122
+ type: "string",
123
+ description: "Password for auth.username."
124
+ },
97
125
  precondition: {
98
126
  type: "string",
99
127
  enum: ["login", "none"],
@@ -18,6 +18,28 @@ export const AuthPreconditionSchema = z.object({
18
18
  precondition: z.enum(['login', 'none']).optional(),
19
19
  entryUrl: z.preprocess(normalizeUrl, z.string().url('entryUrl must be a valid URL (the login page to authenticate on).').optional()),
20
20
  deepUrl: z.preprocess(normalizeUrl, z.string().url('deepUrl must be a valid URL (the page to evaluate AFTER login).').optional()),
21
+ // WHICH account to authenticate as. Without these the environment's default
22
+ // credential is used — which is correct for a caller that named nobody, and
23
+ // wrong for one that did. Naming the account here makes it authoritative for
24
+ // the precondition login.
25
+ username: z.string().optional(),
26
+ password: z.string().optional(),
27
+ }).strict();
28
+ /**
29
+ * A single account the browser agent may sign in as DURING the task.
30
+ *
31
+ * The auth precondition covers the FIRST login. This covers every login after
32
+ * it: a flow that sets a password, gets bounced to a sign-in page, and must
33
+ * authenticate as the account it just provisioned. Without an explicit channel
34
+ * for those, the agent's only login affordance filled the environment's stored
35
+ * account, so "then sign in as <the new user>" was unachievable however the
36
+ * task was worded — and the app's correct rejection of the wrong account came
37
+ * back as an application failure.
38
+ */
39
+ export const LoginCredentialSchema = z.object({
40
+ username: z.string().min(1, 'username is required for a login credential'),
41
+ password: z.string().min(1, 'password is required for a login credential'),
42
+ label: z.string().optional(),
21
43
  }).strict();
22
44
  /**
23
45
  * Tool input validation schemas
@@ -32,8 +54,20 @@ export const TestPageChangesInputSchema = z.object({
32
54
  username: z.string().optional(),
33
55
  password: z.string().optional(),
34
56
  repoName: z.string().optional(),
57
+ // Accounts for logins the agent hits DURING the task, not just the first one.
58
+ loginCredentials: z.array(LoginCredentialSchema).max(10, 'loginCredentials accepts at most 10 accounts per run.').optional(),
59
+ // Opt out of the environment's stored credentials entirely: the agent signs
60
+ // in only as an account this call named, or not at all.
61
+ useEnvironmentCredentials: z.boolean().optional(),
35
62
  // Auth-precondition deep-link intent (bead 56kd.6) — "log in THEN go to X".
36
63
  auth: AuthPreconditionSchema.optional(),
64
+ }).refine((v) => !(v.useEnvironmentCredentials === false
65
+ && !v.username && !v.credentialId && !v.credentialRole
66
+ && !(v.loginCredentials && v.loginCredentials.length > 0)
67
+ && !v.auth?.username), {
68
+ message: 'useEnvironmentCredentials:false leaves the run with no way to authenticate. '
69
+ + 'Pass username/password, credentialId, credentialRole, or loginCredentials alongside it.',
70
+ path: ['useEnvironmentCredentials'],
37
71
  });
38
72
  export const TriggerCrawlInputSchema = z.object({
39
73
  url: z.preprocess(normalizeUrl, z.string().url('Invalid URL. Pass a full URL like "http://localhost:3000" or "https://example.com". Localhost URLs are auto-tunneled to the remote browser.')),
@@ -3,6 +3,9 @@
3
3
  * Parses the origin remote URL into "owner/repo" format.
4
4
  */
5
5
  import { execSync } from 'child_process';
6
+ import { ProjectAnalyzer } from './projectAnalyzer.js';
7
+ import { Logger } from './logger.js';
8
+ const logger = new Logger({ module: 'gitContext' });
6
9
  let cached; // undefined = not yet checked
7
10
  /**
8
11
  * Detect the repo name (e.g. "debugg-ai/debugg-ai-frontend") from git remote origin.
@@ -25,6 +28,29 @@ export function detectRepoName() {
25
28
  }
26
29
  return cached;
27
30
  }
31
+ /**
32
+ * Detect the LOCAL git ref (branch + commit sha) from the current working
33
+ * directory — the repo whose dev server the caller is crawling.
34
+ *
35
+ * sentinal-lwtaw.13 (MCP side): the TRIGGER POINT owns the git fact. The
36
+ * environment says WHERE to crawl; the caller supplies the ref it's running so
37
+ * the backend can mint a git-backed Atlas version. Delegates to
38
+ * ProjectAnalyzer's existing `.git/HEAD` readers — no new git parsing.
39
+ *
40
+ * Best-effort by contract: returns `{}` when cwd isn't a git repo (or the read
41
+ * fails). NEVER throws and NEVER fabricates a branch/sha — a git-less target
42
+ * must still crawl (honest no-git). NOT cached (unlike detectRepoName): the
43
+ * branch/commit change under a long-lived MCP process, so each crawl re-reads.
44
+ */
45
+ export async function detectLocalGitRef() {
46
+ try {
47
+ return await new ProjectAnalyzer().getGitRef(process.cwd());
48
+ }
49
+ catch (err) {
50
+ logger.debug('Could not determine local git ref', err);
51
+ return {};
52
+ }
53
+ }
28
54
  /**
29
55
  * Parse an origin URL into "owner/repo" format.
30
56
  * Handles SSH (git@github.com:owner/repo.git) and HTTPS (https://github.com/owner/repo.git).
@@ -438,6 +438,29 @@ export class ProjectAnalyzer {
438
438
  extractRepoName(projectPath) {
439
439
  return projectPath.split('/').pop() || 'unknown';
440
440
  }
441
+ /**
442
+ * Best-effort local git ref (branch + full commit sha) for a project dir.
443
+ *
444
+ * Reuses the existing `.git/HEAD` readers (getCurrentBranch /
445
+ * getCurrentCommitHash) — no new git parsing. NEVER throws and NEVER
446
+ * fabricates: a non-git dir (or any read failure) yields `{}` so a git-less
447
+ * crawl target still crawls (honest no-git). Not cached — a long-lived MCP
448
+ * process must re-read after the caller checks out a branch or commits.
449
+ */
450
+ async getGitRef(repoPath) {
451
+ const projectPath = repoPath || process.cwd();
452
+ try {
453
+ const [branch, commitSha] = await Promise.all([
454
+ this.getCurrentBranch(projectPath),
455
+ this.getCurrentCommitHash(projectPath),
456
+ ]);
457
+ return { branch, commitSha };
458
+ }
459
+ catch (error) {
460
+ logger.debug('Could not determine local git ref', error);
461
+ return {};
462
+ }
463
+ }
441
464
  /**
442
465
  * Get current git branch (simplified)
443
466
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@debugg-ai/debugg-ai-mcp",
3
- "version": "3.7.4",
3
+ "version": "3.9.0",
4
4
  "description": "Zero-Config, Fully AI-Managed End-to-End Testing for all code gen platforms.",
5
5
  "type": "module",
6
6
  "bin": {