agent-relay 3.1.10 → 3.1.12

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 (93) hide show
  1. package/bin/agent-relay-broker-darwin-arm64 +0 -0
  2. package/bin/agent-relay-broker-darwin-x64 +0 -0
  3. package/bin/agent-relay-broker-linux-arm64 +0 -0
  4. package/bin/agent-relay-broker-linux-x64 +0 -0
  5. package/dist/index.cjs +2 -2
  6. package/dist/src/cli/bootstrap.d.ts.map +1 -1
  7. package/dist/src/cli/bootstrap.js +2 -0
  8. package/dist/src/cli/bootstrap.js.map +1 -1
  9. package/dist/src/cli/commands/connect.d.ts +3 -0
  10. package/dist/src/cli/commands/connect.d.ts.map +1 -0
  11. package/dist/src/cli/commands/connect.js +18 -0
  12. package/dist/src/cli/commands/connect.js.map +1 -0
  13. package/dist/src/cli/lib/auth-ssh.d.ts.map +1 -1
  14. package/dist/src/cli/lib/auth-ssh.js +22 -270
  15. package/dist/src/cli/lib/auth-ssh.js.map +1 -1
  16. package/dist/src/cli/lib/broker-lifecycle.d.ts.map +1 -1
  17. package/dist/src/cli/lib/broker-lifecycle.js +33 -0
  18. package/dist/src/cli/lib/broker-lifecycle.js.map +1 -1
  19. package/dist/src/cli/lib/connect-daytona.d.ts +15 -0
  20. package/dist/src/cli/lib/connect-daytona.d.ts.map +1 -0
  21. package/dist/src/cli/lib/connect-daytona.js +217 -0
  22. package/dist/src/cli/lib/connect-daytona.js.map +1 -0
  23. package/dist/src/cli/lib/ssh-interactive.d.ts +41 -0
  24. package/dist/src/cli/lib/ssh-interactive.d.ts.map +1 -0
  25. package/dist/src/cli/lib/ssh-interactive.js +320 -0
  26. package/dist/src/cli/lib/ssh-interactive.js.map +1 -0
  27. package/install.sh +2 -1
  28. package/package.json +13 -10
  29. package/packages/acp-bridge/package.json +2 -2
  30. package/packages/config/dist/cli-auth-config.d.ts +2 -0
  31. package/packages/config/dist/cli-auth-config.d.ts.map +1 -1
  32. package/packages/config/dist/cli-auth-config.js +1 -0
  33. package/packages/config/dist/cli-auth-config.js.map +1 -1
  34. package/packages/config/package.json +1 -1
  35. package/packages/config/src/cli-auth-config.ts +3 -0
  36. package/packages/hooks/package.json +4 -4
  37. package/packages/memory/package.json +2 -2
  38. package/packages/openclaw/dist/__tests__/gateway-control.test.js +13 -13
  39. package/packages/openclaw/dist/__tests__/gateway-control.test.js.map +1 -1
  40. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.d.ts +2 -0
  41. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.d.ts.map +1 -0
  42. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.js +369 -0
  43. package/packages/openclaw/dist/__tests__/gateway-poll-fallback.test.js.map +1 -0
  44. package/packages/openclaw/dist/__tests__/gateway-threads.test.js +57 -16
  45. package/packages/openclaw/dist/__tests__/gateway-threads.test.js.map +1 -1
  46. package/packages/openclaw/dist/__tests__/ws-client.test.js +2 -0
  47. package/packages/openclaw/dist/__tests__/ws-client.test.js.map +1 -1
  48. package/packages/openclaw/dist/config.d.ts +2 -0
  49. package/packages/openclaw/dist/config.d.ts.map +1 -1
  50. package/packages/openclaw/dist/config.js +99 -12
  51. package/packages/openclaw/dist/config.js.map +1 -1
  52. package/packages/openclaw/dist/gateway.d.ts +56 -2
  53. package/packages/openclaw/dist/gateway.d.ts.map +1 -1
  54. package/packages/openclaw/dist/gateway.js +819 -127
  55. package/packages/openclaw/dist/gateway.js.map +1 -1
  56. package/packages/openclaw/dist/runtime/openclaw-config.d.ts +2 -0
  57. package/packages/openclaw/dist/runtime/openclaw-config.d.ts.map +1 -1
  58. package/packages/openclaw/dist/runtime/openclaw-config.js +1 -1
  59. package/packages/openclaw/dist/runtime/openclaw-config.js.map +1 -1
  60. package/packages/openclaw/dist/runtime/setup.d.ts +0 -2
  61. package/packages/openclaw/dist/runtime/setup.d.ts.map +1 -1
  62. package/packages/openclaw/dist/runtime/setup.js +53 -8
  63. package/packages/openclaw/dist/runtime/setup.js.map +1 -1
  64. package/packages/openclaw/dist/types.d.ts +28 -0
  65. package/packages/openclaw/dist/types.d.ts.map +1 -1
  66. package/packages/openclaw/package.json +2 -2
  67. package/packages/openclaw/skill/SKILL.md +150 -44
  68. package/packages/openclaw/src/__tests__/gateway-control.test.ts +14 -14
  69. package/packages/openclaw/src/__tests__/gateway-poll-fallback.test.ts +467 -0
  70. package/packages/openclaw/src/__tests__/gateway-threads.test.ts +73 -23
  71. package/packages/openclaw/src/__tests__/ws-client.test.ts +71 -51
  72. package/packages/openclaw/src/config.ts +121 -12
  73. package/packages/openclaw/src/gateway.ts +1155 -252
  74. package/packages/openclaw/src/runtime/openclaw-config.ts +3 -1
  75. package/packages/openclaw/src/runtime/setup.ts +57 -16
  76. package/packages/openclaw/src/types.ts +31 -0
  77. package/packages/openclaw/test/vitest.setup.ts +1 -0
  78. package/packages/policy/package.json +2 -2
  79. package/packages/sdk/dist/__tests__/unit.test.js +131 -129
  80. package/packages/sdk/dist/__tests__/unit.test.js.map +1 -1
  81. package/packages/sdk/dist/relay.js +1 -1
  82. package/packages/sdk/dist/workflows/runner.d.ts.map +1 -1
  83. package/packages/sdk/dist/workflows/runner.js +5 -3
  84. package/packages/sdk/dist/workflows/runner.js.map +1 -1
  85. package/packages/sdk/package.json +2 -2
  86. package/packages/sdk/src/__tests__/unit.test.ts +142 -157
  87. package/packages/sdk/src/relay.ts +1 -1
  88. package/packages/sdk/src/workflows/runner.ts +12 -9
  89. package/packages/sdk-py/pyproject.toml +1 -1
  90. package/packages/telemetry/package.json +1 -1
  91. package/packages/trajectory/package.json +2 -2
  92. package/packages/user-directory/package.json +2 -2
  93. package/packages/utils/package.json +2 -2
@@ -1,4 +1,4 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
1
+ import { describe, it, expect, vi, afterEach } from 'vitest';
2
2
  import { WebSocketServer, type WebSocket as WsType } from 'ws';
3
3
 
4
4
  // Mock spawn/manager and relaycast SDK to prevent side-effects from gateway.ts module load
@@ -42,6 +42,8 @@ vi.mock('node:fs/promises', () => ({
42
42
  readFile: vi.fn().mockResolvedValue('{"spawns":[]}'),
43
43
  writeFile: vi.fn().mockResolvedValue(undefined),
44
44
  mkdir: vi.fn().mockResolvedValue(undefined),
45
+ rename: vi.fn().mockResolvedValue(undefined),
46
+ chmod: vi.fn().mockResolvedValue(undefined),
45
47
  }));
46
48
 
47
49
  vi.mock('node:fs', () => ({
@@ -114,12 +116,14 @@ class MockOpenClawServer {
114
116
  if (this.acceptAuth) {
115
117
  ws.send(JSON.stringify({ type: 'res', id: 'connect-1', ok: true }));
116
118
  } else {
117
- ws.send(JSON.stringify({
118
- type: 'res',
119
- id: 'connect-1',
120
- ok: false,
121
- error: { code: 'auth_failed', message: 'Invalid token' },
122
- }));
119
+ ws.send(
120
+ JSON.stringify({
121
+ type: 'res',
122
+ id: 'connect-1',
123
+ ok: false,
124
+ error: { code: 'auth_failed', message: 'Invalid token' },
125
+ })
126
+ );
123
127
  }
124
128
  return;
125
129
  }
@@ -128,19 +132,23 @@ class MockOpenClawServer {
128
132
  if (msg.method === 'chat.send') {
129
133
  const respond = () => {
130
134
  if (this.chatOk) {
131
- ws.send(JSON.stringify({
132
- type: 'res',
133
- id: msg.id,
134
- ok: true,
135
- payload: { runId: 'run-1', status: 'accepted' },
136
- }));
135
+ ws.send(
136
+ JSON.stringify({
137
+ type: 'res',
138
+ id: msg.id,
139
+ ok: true,
140
+ payload: { runId: 'run-1', status: 'accepted' },
141
+ })
142
+ );
137
143
  } else {
138
- ws.send(JSON.stringify({
139
- type: 'res',
140
- id: msg.id,
141
- ok: false,
142
- error: { code: 'rate_limited', message: 'Too many requests' },
143
- }));
144
+ ws.send(
145
+ JSON.stringify({
146
+ type: 'res',
147
+ id: msg.id,
148
+ ok: false,
149
+ error: { code: 'rate_limited', message: 'Too many requests' },
150
+ })
151
+ );
144
152
  }
145
153
  };
146
154
 
@@ -308,11 +316,13 @@ describe('OpenClawGatewayClient', () => {
308
316
  origWss.removeAllListeners('connection');
309
317
  origWss.on('connection', (ws) => {
310
318
  // Send challenge
311
- ws.send(JSON.stringify({
312
- type: 'event',
313
- event: 'connect.challenge',
314
- payload: { nonce: 'nonce-1', ts: Date.now() },
315
- }));
319
+ ws.send(
320
+ JSON.stringify({
321
+ type: 'event',
322
+ event: 'connect.challenge',
323
+ payload: { nonce: 'nonce-1', ts: Date.now() },
324
+ })
325
+ );
316
326
  ws.on('message', (data) => {
317
327
  const msg = JSON.parse(data.toString()) as Record<string, unknown>;
318
328
  if (msg.method === 'connect') {
@@ -354,11 +364,13 @@ describe('OpenClawGatewayClient', () => {
354
364
  origWss.on('connection', (ws) => {
355
365
  // Send garbage first, then a proper challenge
356
366
  ws.send('not json at all');
357
- ws.send(JSON.stringify({
358
- type: 'event',
359
- event: 'connect.challenge',
360
- payload: { nonce: 'nonce-2', ts: Date.now() },
361
- }));
367
+ ws.send(
368
+ JSON.stringify({
369
+ type: 'event',
370
+ event: 'connect.challenge',
371
+ payload: { nonce: 'nonce-2', ts: Date.now() },
372
+ })
373
+ );
362
374
  ws.on('message', (data) => {
363
375
  const msg = JSON.parse(data.toString()) as Record<string, unknown>;
364
376
  if (msg.method === 'connect') {
@@ -380,22 +392,26 @@ describe('OpenClawGatewayClient', () => {
380
392
  origWss.on('connection', (ws) => {
381
393
  connectAttempts++;
382
394
  // Send challenge
383
- ws.send(JSON.stringify({
384
- type: 'event',
385
- event: 'connect.challenge',
386
- payload: { nonce: `nonce-fallback-${connectAttempts}`, ts: Date.now() },
387
- }));
395
+ ws.send(
396
+ JSON.stringify({
397
+ type: 'event',
398
+ event: 'connect.challenge',
399
+ payload: { nonce: `nonce-fallback-${connectAttempts}`, ts: Date.now() },
400
+ })
401
+ );
388
402
  ws.on('message', (data) => {
389
403
  const msg = JSON.parse(data.toString()) as Record<string, unknown>;
390
404
  if (msg.method === 'connect') {
391
405
  if (connectAttempts === 1) {
392
406
  // First attempt: reject with signature invalid
393
- ws.send(JSON.stringify({
394
- type: 'res',
395
- id: 'connect-1',
396
- ok: false,
397
- error: { code: 'auth_failed', message: 'device signature invalid' },
398
- }));
407
+ ws.send(
408
+ JSON.stringify({
409
+ type: 'res',
410
+ id: 'connect-1',
411
+ ok: false,
412
+ error: { code: 'auth_failed', message: 'device signature invalid' },
413
+ })
414
+ );
399
415
  } else {
400
416
  // Second attempt (fallback): accept
401
417
  ws.send(JSON.stringify({ type: 'res', id: 'connect-1', ok: true }));
@@ -418,21 +434,25 @@ describe('OpenClawGatewayClient', () => {
418
434
  origWss.removeAllListeners('connection');
419
435
  origWss.on('connection', (ws) => {
420
436
  connectAttempts++;
421
- ws.send(JSON.stringify({
422
- type: 'event',
423
- event: 'connect.challenge',
424
- payload: { nonce: `nonce-nofallback-${connectAttempts}`, ts: Date.now() },
425
- }));
437
+ ws.send(
438
+ JSON.stringify({
439
+ type: 'event',
440
+ event: 'connect.challenge',
441
+ payload: { nonce: `nonce-nofallback-${connectAttempts}`, ts: Date.now() },
442
+ })
443
+ );
426
444
  ws.on('message', (data) => {
427
445
  const msg = JSON.parse(data.toString()) as Record<string, unknown>;
428
446
  if (msg.method === 'connect') {
429
447
  // Always reject with signature invalid
430
- ws.send(JSON.stringify({
431
- type: 'res',
432
- id: 'connect-1',
433
- ok: false,
434
- error: { code: 'auth_failed', message: 'device signature invalid' },
435
- }));
448
+ ws.send(
449
+ JSON.stringify({
450
+ type: 'res',
451
+ id: 'connect-1',
452
+ ok: false,
453
+ error: { code: 'auth_failed', message: 'device signature invalid' },
454
+ })
455
+ );
436
456
  }
437
457
  });
438
458
  });
@@ -5,6 +5,28 @@ import { existsSync, readFileSync } from 'node:fs';
5
5
 
6
6
  import type { GatewayConfig } from './types.js';
7
7
 
8
+ function envValue(vars: Record<string, string>, key: string): string | undefined {
9
+ const processValue = process.env[key]?.trim();
10
+ if (processValue) return processValue;
11
+ const fileValue = vars[key]?.trim();
12
+ return fileValue ? fileValue : undefined;
13
+ }
14
+
15
+ function parseBooleanEnv(vars: Record<string, string>, key: string): boolean | undefined {
16
+ const value = envValue(vars, key);
17
+ if (!value) return undefined;
18
+ if (['1', 'true', 'yes', 'on'].includes(value.toLowerCase())) return true;
19
+ if (['0', 'false', 'no', 'off'].includes(value.toLowerCase())) return false;
20
+ return undefined;
21
+ }
22
+
23
+ function parseNumberEnv(vars: Record<string, string>, key: string): number | undefined {
24
+ const value = envValue(vars, key);
25
+ if (!value) return undefined;
26
+ const parsed = Number(value);
27
+ return Number.isFinite(parsed) ? parsed : undefined;
28
+ }
29
+
8
30
  export interface OpenClawDetection {
9
31
  /** Whether OpenClaw is installed. */
10
32
  installed: boolean;
@@ -61,6 +83,15 @@ export function openclawHome(): string {
61
83
  return openclawHomePath;
62
84
  }
63
85
 
86
+ /** Return the config filename for the resolved OpenClaw home (clawdbot.json or openclaw.json). */
87
+ export function openclawConfigFilename(home?: string): string {
88
+ const dir = home ?? openclawHome();
89
+ if (hasValidConfig(dir, 'clawdbot.json')) return 'clawdbot.json';
90
+ if (hasValidConfig(dir, 'openclaw.json')) return 'openclaw.json';
91
+ // No existing config — infer from directory name
92
+ return dir.endsWith('.clawdbot') ? 'clawdbot.json' : 'openclaw.json';
93
+ }
94
+
64
95
  /**
65
96
  * Detect whether OpenClaw is installed and return paths/config.
66
97
  */
@@ -138,6 +169,7 @@ export async function detectOpenClaw(): Promise<OpenClawDetection> {
138
169
  * Load the gateway config from ~/.openclaw/workspace/relaycast/.env.
139
170
  * Returns null if the file doesn't exist or can't be parsed.
140
171
  */
172
+ // eslint-disable-next-line complexity
141
173
  export async function loadGatewayConfig(): Promise<GatewayConfig | null> {
142
174
  const detection = await detectOpenClaw();
143
175
  const envPath = join(detection.workspaceDir, 'relaycast', '.env');
@@ -163,25 +195,69 @@ export async function loadGatewayConfig(): Promise<GatewayConfig | null> {
163
195
  vars[trimmed.slice(0, eqIdx)] = value;
164
196
  }
165
197
 
166
- const apiKey = vars['RELAY_API_KEY'];
167
- const clawName = vars['RELAY_CLAW_NAME'];
198
+ const apiKey = envValue(vars, 'RELAY_API_KEY');
199
+ const clawName = envValue(vars, 'RELAY_CLAW_NAME');
200
+ const relayChannels = envValue(vars, 'RELAY_CHANNELS');
168
201
 
169
202
  if (!apiKey || !clawName) {
170
203
  return null;
171
204
  }
172
205
 
173
- const portStr = vars['OPENCLAW_GATEWAY_PORT'];
174
- const port = portStr ? Number(portStr) : undefined;
206
+ const port = parseNumberEnv(vars, 'OPENCLAW_GATEWAY_PORT');
207
+ const pollFallbackEnabled = parseBooleanEnv(vars, 'RELAY_TRANSPORT_POLL_FALLBACK_ENABLED');
208
+ const pollFallbackProbeWsEnabled = parseBooleanEnv(
209
+ vars,
210
+ 'RELAY_TRANSPORT_POLL_FALLBACK_PROBE_WS_ENABLED'
211
+ );
212
+ const pollFallbackWsFailureThreshold = parseNumberEnv(
213
+ vars,
214
+ 'RELAY_TRANSPORT_POLL_FALLBACK_WS_FAILURE_THRESHOLD'
215
+ );
216
+ const pollFallbackTimeoutSeconds = parseNumberEnv(vars, 'RELAY_TRANSPORT_POLL_FALLBACK_TIMEOUT_SECONDS');
217
+ const pollFallbackLimit = parseNumberEnv(vars, 'RELAY_TRANSPORT_POLL_FALLBACK_LIMIT');
218
+ const pollFallbackProbeWsIntervalMs = parseNumberEnv(
219
+ vars,
220
+ 'RELAY_TRANSPORT_POLL_FALLBACK_PROBE_WS_INTERVAL_MS'
221
+ );
222
+ const pollFallbackProbeWsStableGraceMs = parseNumberEnv(
223
+ vars,
224
+ 'RELAY_TRANSPORT_POLL_FALLBACK_PROBE_WS_STABLE_GRACE_MS'
225
+ );
226
+ const pollFallbackInitialCursor = envValue(vars, 'RELAY_TRANSPORT_POLL_FALLBACK_INITIAL_CURSOR');
227
+
228
+ const transport =
229
+ pollFallbackEnabled !== undefined ||
230
+ pollFallbackProbeWsEnabled !== undefined ||
231
+ pollFallbackWsFailureThreshold !== undefined ||
232
+ pollFallbackTimeoutSeconds !== undefined ||
233
+ pollFallbackLimit !== undefined ||
234
+ pollFallbackProbeWsIntervalMs !== undefined ||
235
+ pollFallbackProbeWsStableGraceMs !== undefined ||
236
+ pollFallbackInitialCursor !== undefined
237
+ ? {
238
+ pollFallback: {
239
+ enabled: pollFallbackEnabled,
240
+ wsFailureThreshold: pollFallbackWsFailureThreshold,
241
+ timeoutSeconds: pollFallbackTimeoutSeconds,
242
+ limit: pollFallbackLimit,
243
+ initialCursor: pollFallbackInitialCursor,
244
+ probeWs: {
245
+ enabled: pollFallbackProbeWsEnabled,
246
+ intervalMs: pollFallbackProbeWsIntervalMs,
247
+ stableGraceMs: pollFallbackProbeWsStableGraceMs,
248
+ },
249
+ },
250
+ }
251
+ : undefined;
175
252
 
176
253
  return {
177
254
  apiKey,
178
255
  clawName,
179
- baseUrl: vars['RELAY_BASE_URL'] || 'https://api.relaycast.dev',
180
- channels: vars['RELAY_CHANNELS']
181
- ? vars['RELAY_CHANNELS'].split(',').map((c) => c.trim())
182
- : ['general'],
183
- openclawGatewayToken: vars['OPENCLAW_GATEWAY_TOKEN'] || process.env.OPENCLAW_GATEWAY_TOKEN,
256
+ baseUrl: envValue(vars, 'RELAY_BASE_URL') || 'https://api.relaycast.dev',
257
+ channels: relayChannels ? relayChannels.split(',').map((c) => c.trim()) : ['general'],
258
+ openclawGatewayToken: envValue(vars, 'OPENCLAW_GATEWAY_TOKEN'),
184
259
  openclawGatewayPort: Number.isFinite(port) ? port : undefined,
260
+ transport,
185
261
  };
186
262
  } catch {
187
263
  return null;
@@ -207,14 +283,47 @@ export async function saveGatewayConfig(config: GatewayConfig): Promise<void> {
207
283
 
208
284
  if (config.openclawGatewayToken) {
209
285
  lines.push(`OPENCLAW_GATEWAY_TOKEN=${config.openclawGatewayToken}`);
210
- const masked = config.openclawGatewayToken.length > 12
211
- ? config.openclawGatewayToken.slice(0, 8) + '...'
212
- : '***';
286
+ const masked =
287
+ config.openclawGatewayToken.length > 12 ? config.openclawGatewayToken.slice(0, 8) + '...' : '***';
213
288
  console.log(`[config] Persisting OPENCLAW_GATEWAY_TOKEN (${masked})`);
214
289
  }
215
290
  if (config.openclawGatewayPort) {
216
291
  lines.push(`OPENCLAW_GATEWAY_PORT=${config.openclawGatewayPort}`);
217
292
  }
293
+ if (config.transport?.pollFallback?.enabled !== undefined) {
294
+ lines.push(`RELAY_TRANSPORT_POLL_FALLBACK_ENABLED=${config.transport.pollFallback.enabled}`);
295
+ }
296
+ if (config.transport?.pollFallback?.wsFailureThreshold !== undefined) {
297
+ lines.push(
298
+ `RELAY_TRANSPORT_POLL_FALLBACK_WS_FAILURE_THRESHOLD=${config.transport.pollFallback.wsFailureThreshold}`
299
+ );
300
+ }
301
+ if (config.transport?.pollFallback?.timeoutSeconds !== undefined) {
302
+ lines.push(
303
+ `RELAY_TRANSPORT_POLL_FALLBACK_TIMEOUT_SECONDS=${config.transport.pollFallback.timeoutSeconds}`
304
+ );
305
+ }
306
+ if (config.transport?.pollFallback?.limit !== undefined) {
307
+ lines.push(`RELAY_TRANSPORT_POLL_FALLBACK_LIMIT=${config.transport.pollFallback.limit}`);
308
+ }
309
+ if (config.transport?.pollFallback?.initialCursor) {
310
+ lines.push(`RELAY_TRANSPORT_POLL_FALLBACK_INITIAL_CURSOR=${config.transport.pollFallback.initialCursor}`);
311
+ }
312
+ if (config.transport?.pollFallback?.probeWs?.enabled !== undefined) {
313
+ lines.push(
314
+ `RELAY_TRANSPORT_POLL_FALLBACK_PROBE_WS_ENABLED=${config.transport.pollFallback.probeWs.enabled}`
315
+ );
316
+ }
317
+ if (config.transport?.pollFallback?.probeWs?.intervalMs !== undefined) {
318
+ lines.push(
319
+ `RELAY_TRANSPORT_POLL_FALLBACK_PROBE_WS_INTERVAL_MS=${config.transport.pollFallback.probeWs.intervalMs}`
320
+ );
321
+ }
322
+ if (config.transport?.pollFallback?.probeWs?.stableGraceMs !== undefined) {
323
+ lines.push(
324
+ `RELAY_TRANSPORT_POLL_FALLBACK_PROBE_WS_STABLE_GRACE_MS=${config.transport.pollFallback.probeWs.stableGraceMs}`
325
+ );
326
+ }
218
327
 
219
328
  lines.push('');
220
329
  const env = lines.join('\n');