testdriverai 7.7.0-canary.2 → 7.8.0-test.2

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/CHANGELOG.md CHANGED
@@ -1,32 +1,36 @@
1
- ## 7.7.0-canary.2 (2026-03-13)
1
+ ## 7.8.0-test.2 (2026-03-13)
2
+
3
+ ## ✨ Features
4
+
5
+ - Implement automatic cleanup of unused Ably channels to improve resource management [API] (df6181de)
2
6
 
3
7
  ## 🔧 Maintenance
4
8
 
5
- - Eliminate CI race conditions in promotion pipeline by consolidating release workflows and improving conflict resolution [CI] (c21f708d)
6
- - Improve VNC URL printing in CI environments [Runner] (c21f708d)
7
- - Update development container environment configuration [DevOps] (c21f708d)
8
- - Enhance render.yaml configuration for improved deployment [Infrastructure] (c21f708d)
9
+ - Streamline CI/CD workflows with improved test organization and environment configuration [SDK] (df6181de)
10
+ - Enhance development environment setup with better environment variable management (df6181de)
11
+ - Improve connection handling and URL resolution for better reliability [SDK] (df6181de)
9
12
 
10
- ## 🐛 Bug Fixes
13
+ ## 7.6.0-test.8 (2026-03-13)
11
14
 
12
- - Fix AWS manager configuration issue [API] (c21f708d)
13
- - Resolve CLI initialization and base library issues [SDK] (c21f708d)
14
- - Improve Dashcam functionality and SDK core operations [SDK] (c21f708d)
15
+ 🔧 Maintenance
16
+
17
+ - Improve release workflow automation for more reliable package promotions (358bb702)
15
18
 
16
- ## 7.7.0-canary.1 (2026-03-12)
19
+ ## 7.6.0-test.7 (2026-03-13)
17
20
 
18
21
  ## 🔧 Maintenance
19
22
 
20
- - Update development environment configuration and CI/CD workflows [Infrastructure] (634f6d09)
21
- - Improve dashcam recording functionality and agent initialization [SDK] (274fff79)
22
- - Update CLI initialization command behavior [SDK] (274fff79)
23
- - Enhance deployment configuration for better service management [Infrastructure] (274fff79)
23
+ - Eliminate CI race conditions in promotion pipeline and streamline release workflows [CI] (9d83f886)
24
+ - Improve VNC URL display in CI environments [API] (9d83f886)
25
+ - Add dynamic channel resolution system for SDK configuration [SDK] (9d83f886)
26
+ - Update MCP server dependencies and enhance error handling [SDK] (9d83f886)
24
27
 
25
- ## 7.7.0-canary.0 (2026-03-12)
28
+ ## 7.6.0-test.6 (2026-03-13)
26
29
 
27
- 🔧 Maintenance
30
+ ## 🔧 Maintenance
28
31
 
29
- - Improve release workflow automation with enhanced promotion processes [SDK] (bb6be952)
32
+ - Improve CI/CD pipeline reliability by eliminating race conditions in release promotion workflows [c21f708d]
33
+ - Fix VNC URL display in continuous integration environment [API] [c21f708d]
30
34
 
31
35
  ## 7.6.0-test.5 (2026-03-12)
32
36
 
@@ -16,7 +16,7 @@ function parseValue(value) {
16
16
  return value;
17
17
  }
18
18
 
19
- const channelConfig = require("../../channel.json");
19
+ const channelConfig = require("../../lib/resolve-channel.js");
20
20
 
21
21
  // Factory function that creates a config instance
22
22
  const createConfig = (environment = {}) => {
@@ -511,7 +511,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
511
511
  url = runnerVncUrl;
512
512
  logger.log(`Using runner-provided vncUrl: ${url}`);
513
513
  } else if (runnerIp && noVncPort) {
514
- url = `http://${runnerIp}:${noVncPort}/vnc_lite.html`;
514
+ url = `http://${runnerIp}:${noVncPort}/vnc_lite.html?token=V3b8wG9`;
515
515
  logger.log(`noVNC URL constructed from runner ip+port: ${url}`);
516
516
  } else if (runnerIp) {
517
517
  url = "http://" + runnerIp;
@@ -619,7 +619,7 @@ const createSandbox = function (emitter, analytics, sessionInstance) {
619
619
  }
620
620
 
621
621
  // Construct VNC URL — use port 8080 (nginx noVNC proxy) for Windows instances
622
- var directUrl = message.ip ? "http://" + message.ip + ":8080/vnc_lite.html" : undefined;
622
+ var directUrl = message.ip ? "http://" + message.ip + ":8080/vnc_lite.html?token=V3b8wG9" : undefined;
623
623
 
624
624
  return {
625
625
  success: true,
@@ -12,7 +12,7 @@ const { execSync } = require("child_process");
12
12
  require("dotenv").config();
13
13
 
14
14
  // API configuration
15
- const channelConfig = require("../../../channel.json");
15
+ const channelConfig = require("../../../lib/resolve-channel.js");
16
16
  const API_BASE_URL = process.env.TD_API_ROOT || channelConfig.channels[channelConfig.active];
17
17
  const POLL_INTERVAL = 5000; // 5 seconds
18
18
  const POLL_TIMEOUT = 900000; // 15 minutes
@@ -9,7 +9,7 @@ import { setTestRunInfo } from "./shared-test-state.mjs";
9
9
 
10
10
  // Use createRequire to import CommonJS modules without esbuild processing
11
11
  const require = createRequire(import.meta.url);
12
- const channelConfig = require("../channel.json");
12
+ const channelConfig = require("../lib/resolve-channel.js");
13
13
 
14
14
  // Import Sentry for error reporting
15
15
  const Sentry = require("@sentry/node");
@@ -1249,12 +1249,15 @@ function getConsoleUrl(apiRoot) {
1249
1249
 
1250
1250
  if (!apiRoot) return "https://console.testdriver.ai";
1251
1251
 
1252
- // Production: API on render.com -> Console on testdriver.ai
1253
- if (apiRoot.includes("api.testdriver.ai")) {
1254
- return "https://console.testdriver.ai";
1252
+ // Map known channel API URLs to their console equivalents
1253
+ // e.g. https://api.canary.testdriver.ai -> https://console.canary.testdriver.ai
1254
+ for (const url of Object.values(channelConfig.channels)) {
1255
+ if (url === apiRoot) {
1256
+ return url.replace("api", "console").replace("1337", "3001");
1257
+ }
1255
1258
  }
1256
1259
 
1257
- // Local development: API on localhost:1337 -> Web on localhost:3001
1260
+ // Local development: ngrok/cloudflare tunnels -> localhost:3001
1258
1261
  if (apiRoot.includes("ngrok.io") || apiRoot.includes("trycloudflare.com") || apiRoot.includes("localhost")) {
1259
1262
  return `http://localhost:3001`;
1260
1263
  }
@@ -1265,15 +1268,10 @@ function getConsoleUrl(apiRoot) {
1265
1268
  const renderPrMatch = apiRoot.match(/https:\/\/([\w-]+)-api(-[\w]+)?(-pr-\d+)?\.onrender\.com/);
1266
1269
  if (renderPrMatch) {
1267
1270
  const [, prefix, suffix, prSuffix] = renderPrMatch;
1268
- // Map API naming to Web naming:
1269
- // canary-api -> canary-web
1270
- // testdriver-api-i4m4 -> web-i4m4
1271
1271
  let webPrefix;
1272
1272
  if (prefix === 'testdriver' && suffix) {
1273
- // testdriver-api-i4m4 -> web-i4m4
1274
1273
  webPrefix = 'web' + suffix;
1275
1274
  } else {
1276
- // canary-api -> canary-web
1277
1275
  webPrefix = prefix + '-web';
1278
1276
  }
1279
1277
  return `https://${webPrefix}${prSuffix || ''}.onrender.com`;
@@ -80,7 +80,7 @@ class Dashcam {
80
80
  * @private
81
81
  */
82
82
  _getApiRoot() {
83
- const channelConfig = require("../../channel.json");
83
+ const channelConfig = require("../../lib/resolve-channel.js");
84
84
  return (
85
85
  this.client.config?.TD_API_ROOT || channelConfig.channels[channelConfig.active]
86
86
  );
@@ -92,7 +92,7 @@ class Dashcam {
92
92
  * @param {string} apiRoot - The API root URL
93
93
  * @returns {string} The corresponding console URL
94
94
  */
95
- static getConsoleUrl(apiRoot = (() => { const c = require("../../channel.json"); return c.channels[c.active]; })()) {
95
+ static getConsoleUrl(apiRoot = (() => { const c = require("../../lib/resolve-channel.js"); return c.channels[c.active]; })()) {
96
96
  // Allow explicit override via env (e.g. VITE_DOMAIN from .env)
97
97
  if (process.env.VITE_DOMAIN) return process.env.VITE_DOMAIN;
98
98
 
@@ -448,41 +448,19 @@ class Dashcam {
448
448
 
449
449
  this.recording = false;
450
450
 
451
- // Extract URL from output
451
+ // Extract the /replay/... path from CLI output and reconstruct the URL
452
+ // using getConsoleUrl(). The CLI may return a wrong domain (e.g. canary-web.onrender.com)
453
+ // so we always rewrite the base URL to match the current environment.
452
454
  if (output) {
453
- // Look for replay URL with optional query parameters (most specific)
454
- // Matches: http://localhost:3001/replay/abc123?share=xyz or https://app.dashcam.io/replay/abc123
455
- const replayUrlMatch = output.match(
456
- /https?:\/\/[^\s"',}]+\/replay\/[^\s"',}]+/,
455
+ // Match /replay/{id} with optional query params from any URL or broken prefix
456
+ const replayPathMatch = output.match(
457
+ /(?:https?:\/\/[^\s"',}]+|undefined|null)?(\/replay\/[^\s"',}]+)/,
457
458
  );
458
- if (replayUrlMatch) {
459
- let url = replayUrlMatch[0];
460
- // Remove trailing punctuation but keep query params
461
- url = url.replace(/[.,;:!\)\]]+$/, "").trim();
462
- return url;
463
- }
464
-
465
- // Fallback: any dashcam.io or testdriver.ai URL
466
- const dashcamUrlMatch = output.match(
467
- /https?:\/\/(?:app\.)?(?:dashcam\.io|testdriver\.ai)[^\s"',}]+/,
468
- );
469
- if (dashcamUrlMatch) {
470
- let url = dashcamUrlMatch[0];
471
- url = url.replace(/[.,;:!\?\)\]]+$/, "").trim();
472
- return url;
473
- }
474
-
475
- // Fallback: dashcam CLI may output "undefined/replay/{id}?share={key}"
476
- // when it doesn't know the console URL. Extract the path and reconstruct
477
- // a proper URL using our console URL mapping.
478
- const brokenUrlMatch = output.match(
479
- /(?:undefined|null)?(\/replay\/[^\s"',}]+)/,
480
- );
481
- if (brokenUrlMatch) {
482
- const replayPath = brokenUrlMatch[1].replace(/[.,;:!\)\]]+$/, "").trim();
459
+ if (replayPathMatch) {
460
+ const replayPath = replayPathMatch[1].replace(/[.,;:!\)\]]+$/, "").trim();
483
461
  const consoleUrl = Dashcam.getConsoleUrl(this._getApiRoot());
484
462
  const url = consoleUrl + replayPath;
485
- this._log("debug", "Reconstructed replay URL from broken dashcam output:", url);
463
+ this._log("debug", "Replay URL:", url);
486
464
  return url;
487
465
  }
488
466
 
@@ -382,6 +382,7 @@ jobs:
382
382
  const addMcpResult = require("child_process").spawnSync(
383
383
  "npx",
384
384
  [
385
+ "--yes",
385
386
  "add-mcp",
386
387
  "testdriver",
387
388
  "--command",
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Resolves the active release channel and API URLs.
3
+ *
4
+ * Channel is derived from (in priority order):
5
+ * 1. TD_CHANNEL env var (explicit override)
6
+ * 2. TD_ENV env var (set by envs/<name>.env)
7
+ * 3. SDK package.json version prerelease tag (e.g. "7.6.0-test.5" → "test")
8
+ * 4. "latest" for clean semver versions (stable releases)
9
+ */
10
+
11
+ const semver = require("semver");
12
+
13
+ const CHANNELS = {
14
+ dev: "http://localhost:1337",
15
+ test: "https://api.test.testdriver.ai",
16
+ canary: "https://api.canary.testdriver.ai",
17
+ latest: "https://api.testdriver.ai",
18
+ };
19
+
20
+ function resolveActiveChannel() {
21
+ // 1. Explicit channel override
22
+ if (process.env.TD_CHANNEL && CHANNELS[process.env.TD_CHANNEL]) {
23
+ return process.env.TD_CHANNEL;
24
+ }
25
+
26
+ // 2. Environment name from env file (mapped: stable → latest)
27
+ if (process.env.TD_ENV) {
28
+ const envName = process.env.TD_ENV;
29
+ if (CHANNELS[envName]) return envName;
30
+ if (envName === "stable") return "latest";
31
+ }
32
+
33
+ // 3. Fallback: derive from package.json prerelease tag
34
+ const version = require("../package.json").version;
35
+ const pre = semver.prerelease(version);
36
+ if (pre && pre.length > 0 && CHANNELS[pre[0]]) {
37
+ return pre[0];
38
+ }
39
+
40
+ return "latest";
41
+ }
42
+
43
+ const active = resolveActiveChannel();
44
+
45
+ module.exports = { active, channels: CHANNELS };
@@ -22,7 +22,7 @@ import TestDriverSDK from "../../sdk.js";
22
22
 
23
23
  // Use createRequire to import CommonJS modules
24
24
  const require = createRequire(import.meta.url);
25
- const channelConfig = require("../../channel.json");
25
+ const channelConfig = require("../../lib/resolve-channel.js");
26
26
 
27
27
  /**
28
28
  * Minimum required Vitest major version
@@ -26,8 +26,18 @@ import { sessionManager } from "./session.js";
26
26
  const sdkRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
27
27
  const packageJson = JSON.parse(fs.readFileSync(path.join(sdkRoot, "package.json"), "utf-8"));
28
28
  const version = packageJson.version || "1.0.0";
29
- const channelConfig = JSON.parse(fs.readFileSync(path.join(sdkRoot, "channel.json"), "utf-8"));
30
- const releaseChannel = channelConfig.active || "dev";
29
+ // Derive release channel from package version prerelease tag (e.g. "7.6.0-test.5" "test")
30
+ import semver from "semver";
31
+ const KNOWN_CHANNELS = new Set(["dev", "test", "canary", "latest"]);
32
+ function resolveReleaseChannel(ver) {
33
+ if (process.env.TD_CHANNEL && KNOWN_CHANNELS.has(process.env.TD_CHANNEL))
34
+ return process.env.TD_CHANNEL;
35
+ const pre = semver.prerelease(ver);
36
+ if (pre && pre.length > 0 && KNOWN_CHANNELS.has(String(pre[0])))
37
+ return String(pre[0]);
38
+ return "latest";
39
+ }
40
+ const releaseChannel = resolveReleaseChannel(version);
31
41
  const isSentryEnabled = () => {
32
42
  if (process.env.TD_TELEMETRY === "false") {
33
43
  return false;
@@ -11,10 +11,12 @@
11
11
  "@modelcontextprotocol/ext-apps": "^1.0.0",
12
12
  "@modelcontextprotocol/sdk": "^1.24.0",
13
13
  "@sentry/node": "^9.0.0",
14
+ "semver": "^7.7.4",
14
15
  "zod": "^3.24.0"
15
16
  },
16
17
  "devDependencies": {
17
18
  "@types/node": "^22.0.0",
19
+ "@types/semver": "^7.7.1",
18
20
  "cross-env": "^7.0.3",
19
21
  "tsx": "^4.19.0",
20
22
  "typescript": "^5.6.0",
@@ -1745,6 +1747,13 @@
1745
1747
  "@types/pg": "*"
1746
1748
  }
1747
1749
  },
1750
+ "node_modules/@types/semver": {
1751
+ "version": "7.7.1",
1752
+ "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz",
1753
+ "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==",
1754
+ "dev": true,
1755
+ "license": "MIT"
1756
+ },
1748
1757
  "node_modules/@types/shimmer": {
1749
1758
  "version": "1.2.0",
1750
1759
  "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz",
@@ -3087,9 +3096,9 @@
3087
3096
  "license": "MIT"
3088
3097
  },
3089
3098
  "node_modules/semver": {
3090
- "version": "7.7.3",
3091
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
3092
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
3099
+ "version": "7.7.4",
3100
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
3101
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
3093
3102
  "license": "ISC",
3094
3103
  "bin": {
3095
3104
  "semver": "bin/semver.js"
@@ -16,10 +16,12 @@
16
16
  "@modelcontextprotocol/ext-apps": "^1.0.0",
17
17
  "@modelcontextprotocol/sdk": "^1.24.0",
18
18
  "@sentry/node": "^9.0.0",
19
+ "semver": "^7.7.4",
19
20
  "zod": "^3.24.0"
20
21
  },
21
22
  "devDependencies": {
22
23
  "@types/node": "^22.0.0",
24
+ "@types/semver": "^7.7.1",
23
25
  "cross-env": "^7.0.3",
24
26
  "tsx": "^4.19.0",
25
27
  "typescript": "^5.6.0",
@@ -33,8 +33,17 @@ import { sessionManager, type SessionState } from "./session.js";
33
33
  const sdkRoot = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
34
34
  const packageJson = JSON.parse(fs.readFileSync(path.join(sdkRoot, "package.json"), "utf-8"));
35
35
  const version = packageJson.version || "1.0.0";
36
- const channelConfig = JSON.parse(fs.readFileSync(path.join(sdkRoot, "channel.json"), "utf-8"));
37
- const releaseChannel = channelConfig.active || "dev";
36
+
37
+ // Derive release channel from package version prerelease tag (e.g. "7.6.0-test.5" "test")
38
+ import semver from "semver";
39
+ const KNOWN_CHANNELS = new Set(["dev", "test", "canary", "latest"]);
40
+ function resolveReleaseChannel(ver: string): string {
41
+ if (process.env.TD_CHANNEL && KNOWN_CHANNELS.has(process.env.TD_CHANNEL)) return process.env.TD_CHANNEL;
42
+ const pre = semver.prerelease(ver);
43
+ if (pre && pre.length > 0 && KNOWN_CHANNELS.has(String(pre[0]))) return String(pre[0]);
44
+ return "latest";
45
+ }
46
+ const releaseChannel = resolveReleaseChannel(version);
38
47
 
39
48
  const isSentryEnabled = () => {
40
49
  if (process.env.TD_TELEMETRY === "false") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testdriverai",
3
- "version": "7.7.0-canary.2",
3
+ "version": "7.8.0-test.2",
4
4
  "description": "Next generation autonomous AI agent for end-to-end testing of web & desktop",
5
5
  "main": "sdk.js",
6
6
  "types": "sdk.d.ts",
@@ -85,8 +85,8 @@
85
85
  "@octokit/rest": "^20.1.1",
86
86
  "@sentry/node": "^9.47.1",
87
87
  "@stoplight/yaml-ast-parser": "^0.0.50",
88
- "ajv": "^8.17.1",
89
88
  "ably": "^2.6.0",
89
+ "ajv": "^8.17.1",
90
90
  "arktype": "^2.1.19",
91
91
  "axios": "^1.7.7",
92
92
  "chalk": "^4.1.2",
@@ -105,6 +105,7 @@
105
105
  "pixelmatch": "^7.1.0",
106
106
  "remark-parse": "^11.0.0",
107
107
  "sanitize-filename": "^1.6.3",
108
+ "semver": "^7.7.4",
108
109
  "strip-ansi": "^6.0.1",
109
110
  "terminal-image": "^4.1.0",
110
111
  "tmp": "^0.2.3",
package/sdk.js CHANGED
@@ -1438,7 +1438,7 @@ class TestDriverSDK {
1438
1438
  }
1439
1439
 
1440
1440
  // Set up environment with API key
1441
- const channelConfig = require("./channel.json");
1441
+ const channelConfig = require("./lib/resolve-channel.js");
1442
1442
  const environment = {
1443
1443
  TD_API_KEY: resolvedApiKey,
1444
1444
  TD_API_ROOT: options.apiRoot || process.env.TD_API_ROOT || channelConfig.channels[channelConfig.active],
@@ -3815,9 +3815,27 @@ CAPTCHA_SOLVER_EOF`,
3815
3815
  */
3816
3816
  _logEnvironmentInfo() {
3817
3817
  const apiRoot = this.config?.TD_API_ROOT || 'unknown';
3818
+ const apiKey = this.config?.TD_API_KEY || '';
3819
+ const maskedKey = apiKey.length > 4 ? '***' + apiKey.slice(-4) : '(not set)';
3820
+ const env = process.env.TD_ENV || 'unknown';
3821
+ const os = this.agent?.options?.os || process.env.TD_OS || 'linux';
3818
3822
  const sdkVersion = require('./package.json').version;
3819
- const http = apiRoot.startsWith('https') ? require('https') : require('http');
3820
3823
 
3824
+ // Always print local config immediately
3825
+ const localLines = [
3826
+ '',
3827
+ ` ┌─ TestDriver SDK v${sdkVersion}`,
3828
+ ` │ Environment: ${env}`,
3829
+ ` │ API: ${apiRoot}`,
3830
+ ` │ Key: ${maskedKey}`,
3831
+ ` │ OS: ${os}`,
3832
+ ` └─`,
3833
+ '',
3834
+ ];
3835
+ console.log(localLines.join('\n'));
3836
+
3837
+ // Fetch API version info asynchronously (non-blocking, best-effort)
3838
+ const http = apiRoot.startsWith('https') ? require('https') : require('http');
3821
3839
  const url = apiRoot + '/api/entrance/version';
3822
3840
  const req = http.get(url, { timeout: 5000 }, (res) => {
3823
3841
  let data = '';
@@ -3825,19 +3843,18 @@ CAPTCHA_SOLVER_EOF`,
3825
3843
  res.on('end', () => {
3826
3844
  try {
3827
3845
  const info = JSON.parse(data);
3828
- if (info.channel === 'stable') return; // don't show on stable
3829
3846
  const commit = info.commit || 'unknown';
3830
3847
  const shortCommit = commit.substring(0, 7);
3831
3848
  const commitUrl = commit !== 'unknown'
3832
3849
  ? `https://github.com/testdriverai/mono/commit/${commit}`
3833
3850
  : null;
3834
3851
  const lines = [
3835
- '',
3836
- ` TestDriver SDK v${sdkVersion}`,
3837
- ` API: ${apiRoot} (${info.channel || 'unknown'} v${info.version || '?'})`,
3852
+ ` ┌─ API Server`,
3853
+ ` Channel: ${info.channel || 'unknown'} v${info.version || '?'}`,
3838
3854
  commitUrl
3839
- ? ` Commit: ${shortCommit} → ${commitUrl}`
3840
- : ` Commit: ${shortCommit}`,
3855
+ ? ` Commit: ${shortCommit} → ${commitUrl}`
3856
+ : ` Commit: ${shortCommit}`,
3857
+ ` └─`,
3841
3858
  '',
3842
3859
  ];
3843
3860
  console.log(lines.join('\n'));
package/channel.json DELETED
@@ -1,9 +0,0 @@
1
- {
2
- "active": "canary",
3
- "channels": {
4
- "dev": "http://localhost:1337",
5
- "test": "https://test-api-5rtk.onrender.com",
6
- "canary": "https://canary-api-vpnz.onrender.com",
7
- "latest": "https://api.testdriver.ai"
8
- }
9
- }