@blogic-cz/agent-tools 1.5.7 → 1.5.8

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 (62) hide show
  1. package/README.md +9 -1
  2. package/dist/az-tool/service.d.ts.map +1 -1
  3. package/dist/azdo-tool/service.d.ts.map +1 -1
  4. package/dist/credential-guard/index.d.ts +2 -0
  5. package/dist/credential-guard/index.d.ts.map +1 -1
  6. package/dist/db-tool/service.d.ts.map +1 -1
  7. package/dist/gh-tool/gist.d.ts +13 -0
  8. package/dist/gh-tool/gist.d.ts.map +1 -1
  9. package/dist/gh-tool/pr/core.d.ts.map +1 -1
  10. package/dist/gh-tool/pr/review.d.ts.map +1 -1
  11. package/dist/gh-tool/release.d.ts +46 -1
  12. package/dist/gh-tool/release.d.ts.map +1 -1
  13. package/dist/gh-tool/text-input.d.ts +5 -3
  14. package/dist/gh-tool/text-input.d.ts.map +1 -1
  15. package/dist/k8s-tool/service.d.ts.map +1 -1
  16. package/dist/logs-tool/main.d.ts +1 -2
  17. package/dist/logs-tool/main.d.ts.map +1 -1
  18. package/dist/observability-tool/shared.d.ts.map +1 -1
  19. package/dist/session-tool/main.d.ts +1 -2
  20. package/dist/session-tool/main.d.ts.map +1 -1
  21. package/dist/shared/audit.d.ts.map +1 -1
  22. package/dist/shared/content-security.d.ts +6 -0
  23. package/dist/shared/content-security.d.ts.map +1 -0
  24. package/dist/shared/credential-patterns.d.ts +11 -0
  25. package/dist/shared/credential-patterns.d.ts.map +1 -0
  26. package/dist/shared/effect-version.d.ts.map +1 -1
  27. package/dist/shared/error-renderer.d.ts.map +1 -1
  28. package/dist/shared/format.d.ts.map +1 -1
  29. package/dist/shared/index.d.ts +1 -0
  30. package/dist/shared/index.d.ts.map +1 -1
  31. package/dist/shared/output-boundary.d.ts +3 -0
  32. package/dist/shared/output-boundary.d.ts.map +1 -0
  33. package/package.json +1 -1
  34. package/src/audit-tool/main.ts +3 -1
  35. package/src/az-tool/main.ts +3 -1
  36. package/src/az-tool/service.ts +9 -0
  37. package/src/azdo-tool/main.ts +3 -1
  38. package/src/azdo-tool/service.ts +17 -0
  39. package/src/credential-guard/claude-hook.ts +2 -1
  40. package/src/credential-guard/index.ts +25 -56
  41. package/src/db-tool/main.ts +3 -1
  42. package/src/db-tool/service.ts +25 -2
  43. package/src/gh-tool/gist.ts +63 -64
  44. package/src/gh-tool/main.ts +3 -1
  45. package/src/gh-tool/pr/core.ts +10 -3
  46. package/src/gh-tool/pr/review.ts +199 -12
  47. package/src/gh-tool/release.ts +104 -29
  48. package/src/gh-tool/text-input.ts +19 -38
  49. package/src/k8s-tool/main.ts +3 -1
  50. package/src/k8s-tool/service.ts +25 -0
  51. package/src/logs-tool/main.ts +7 -2
  52. package/src/observability-tool/main.ts +5 -7
  53. package/src/observability-tool/shared.ts +41 -1
  54. package/src/session-tool/main.ts +18 -3
  55. package/src/shared/audit.ts +11 -10
  56. package/src/shared/content-security.ts +91 -0
  57. package/src/shared/credential-patterns.ts +62 -0
  58. package/src/shared/effect-version.ts +2 -1
  59. package/src/shared/error-renderer.ts +15 -2
  60. package/src/shared/format.ts +33 -6
  61. package/src/shared/index.ts +5 -0
  62. package/src/shared/output-boundary.ts +29 -0
@@ -15,6 +15,11 @@
15
15
  */
16
16
 
17
17
  import type { CliToolOverride, CredentialGuardConfig } from "#config/types";
18
+ import { redactSensitiveText } from "#shared/content-security";
19
+ import { SECRET_PATTERNS } from "#shared/credential-patterns";
20
+
21
+ export { findSecretMatches } from "#shared/credential-patterns";
22
+ export { redactSensitiveText } from "#shared/content-security";
18
23
 
19
24
  // ============================================================================
20
25
  // TYPES
@@ -84,61 +89,6 @@ const DEFAULT_ALLOWED_PATH_PATTERNS: RegExp[] = [
84
89
  /\.env\.sample$/,
85
90
  ];
86
91
 
87
- /** Patterns to detect secrets in content. */
88
- const SECRET_PATTERNS = [
89
- {
90
- name: "AWS Access Key",
91
- pattern: /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/,
92
- },
93
- {
94
- name: "GitHub Token",
95
- pattern: /gh[ps]_[A-Za-z0-9]{36}/,
96
- },
97
- {
98
- name: "GitHub PAT",
99
- pattern: /github_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}/,
100
- },
101
- { name: "OpenAI Key", pattern: /sk-[A-Za-z0-9]{48}/ },
102
- {
103
- name: "Generic API Key",
104
- pattern: /(?:api[_-]?key|apikey)["\s:=]+["']?([A-Za-z0-9_-]{20,})["']?/i,
105
- },
106
- {
107
- name: "Generic Secret",
108
- pattern:
109
- /(?:secret|token|password|passwd|pwd)[" \t:=]+["']?(?!\$\{|process\.env|z\.|generate|create|read|get|fetch|import|export|const|function|return|Schema)[^\s"']{32,}["']?/i,
110
- },
111
- {
112
- // eslint-disable-next-line eslint/no-useless-concat -- intentionally split to avoid credential guard self-detection
113
- name: "Priv" + "ate Key",
114
- pattern: new RegExp("-----BEGIN.*PRIVATE KEY-----"),
115
- },
116
- {
117
- name: "JWT Token",
118
- pattern: /(?:["'=:\s]|^)eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
119
- },
120
- {
121
- name: "Azure SAS Token",
122
- pattern: /[?&]sig=[A-Za-z0-9%+/=]{20,}/,
123
- },
124
- {
125
- name: "GCP Service Account Key",
126
- pattern: /"type"\s*:\s*"service_account"/,
127
- },
128
- {
129
- name: "Slack Webhook URL",
130
- pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/,
131
- },
132
- {
133
- name: "Discord Webhook URL",
134
- pattern: /https:\/\/discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]+/,
135
- },
136
- {
137
- name: "Database URL",
138
- pattern: /(?:postgres(?:ql)?|mysql|mongodb):\/\/(?!\$\{)[^:]+:(?!\$\{)[^@]+@/,
139
- },
140
- ];
141
-
142
92
  /**
143
93
  * CLI tools that must use wrapper tools for security and audit.
144
94
  */
@@ -1425,6 +1375,25 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
1425
1375
  }
1426
1376
 
1427
1377
  function handleToolExecuteBefore(input: HookInput, output: HookOutput): void {
1378
+ try {
1379
+ enforceToolExecuteBefore(input, output);
1380
+ } catch (error) {
1381
+ const message = error instanceof Error ? error.message : String(error);
1382
+ const sanitized = redactSensitiveText(message);
1383
+ if (error instanceof Error) {
1384
+ if (sanitized !== message) {
1385
+ error.message = sanitized;
1386
+ if (error.stack) error.stack = error.stack.replace(message, sanitized);
1387
+ }
1388
+ throw error;
1389
+ }
1390
+ // Do not preserve an unsanitized non-Error value as the new error's cause.
1391
+ // eslint-disable-next-line eslint/preserve-caught-error
1392
+ throw new Error(sanitized);
1393
+ }
1394
+ }
1395
+
1396
+ function enforceToolExecuteBefore(input: HookInput, output: HookOutput): void {
1428
1397
  // Normalize tool name across platforms:
1429
1398
  // - AI coding agents pass capitalized: "Bash", "Read", "Write", "Edit"
1430
1399
  // - OpenCode MCP tools pass prefixed: "mcp_bash", "mcp_read", "mcp_write", "mcp_edit"
@@ -1453,7 +1422,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
1453
1422
  if (detected) {
1454
1423
  throw new Error(
1455
1424
  `\u{1F6AB} Secret detected: Potential ${detected.name} found in content.\n\n` +
1456
- `Matched: ${detected.match}\n\n` +
1425
+ `Matched: [REDACTED]\n\n` +
1457
1426
  `Never commit secrets to code. Use environment variables or secret managers.\n\n` +
1458
1427
  `Think this is a false positive? Review the project repository, fix the pattern, and submit a PR.`,
1459
1428
  );
@@ -14,6 +14,7 @@ import {
14
14
  VERSION,
15
15
  } from "#shared";
16
16
  import { AuditServiceLayer, withAudit } from "#shared/audit";
17
+ import { withRedactedOutput } from "#shared/output-boundary";
17
18
  import { ConfigService, ConfigServiceLayer, getDefaultEnvironment } from "#config";
18
19
  import { DbConfigService, makeDbConfigLayer } from "./config-service";
19
20
  import { DbConnectionError } from "./errors";
@@ -238,6 +239,7 @@ const mainCommand = Command.make("db-tool", {}).pipe(
238
239
 
239
240
  const cli = Command.run(mainCommand, {
240
241
  version: VERSION,
242
+ renderErrors: false,
241
243
  });
242
244
 
243
245
  const dbConfigLayer = makeDbConfigLayer(profileArg);
@@ -252,7 +254,7 @@ const MainLayer = DbService.layer.pipe(
252
254
  Layer.provideMerge(AuditServiceLayer),
253
255
  );
254
256
 
255
- const program = withAudit("db", cli).pipe(
257
+ const program = withRedactedOutput(withAudit("db", cli)).pipe(
256
258
  Effect.provide(MainLayer),
257
259
  Effect.tapCause(renderCauseToStderr),
258
260
  );
@@ -6,6 +6,7 @@ import type { DbConfig, DbMutationOperation, QueryResult, SchemaMode } from "./t
6
6
  import { ConfigService } from "#config";
7
7
  import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
8
8
  import { resolveEnvTemplate } from "#shared/env-template";
9
+ import { unsafeOutboundTextReason } from "#shared/content-security";
9
10
  import { resolveEnvironmentScopedPrerequisites } from "#shared/prerequisites/config";
10
11
  import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
11
12
  import { buildApiProbeArgs } from "#shared/k8s-probe";
@@ -501,8 +502,18 @@ export class DbService extends Context.Service<
501
502
  readOnly: isFullyReadOnly(config),
502
503
  });
503
504
 
504
- const runSql = (config: DbConfig, password: string, sql: string) =>
505
- sqlClient.run(toConnection(config, password), sql);
505
+ const unsafeSqlError = (reason: string) =>
506
+ new DbQueryError({
507
+ message: `Refusing to send SQL containing ${reason}.`,
508
+ sql: "[redacted]",
509
+ });
510
+
511
+ const runSql = (config: DbConfig, password: string, sql: string) => {
512
+ const unsafeReason = unsafeOutboundTextReason(sql);
513
+ return unsafeReason === null
514
+ ? sqlClient.run(toConnection(config, password), sql)
515
+ : Effect.fail(unsafeSqlError(unsafeReason));
516
+ };
506
517
 
507
518
  const fetchSingleColumn = (config: DbConfig, password: string, sql: string) =>
508
519
  runSql(config, password, sql).pipe(
@@ -772,6 +783,11 @@ export class DbService extends Context.Service<
772
783
  sql: string,
773
784
  limit?: number,
774
785
  ) {
786
+ const unsafeReason = unsafeOutboundTextReason(sql);
787
+ if (unsafeReason !== null) {
788
+ return yield* unsafeSqlError(unsafeReason);
789
+ }
790
+
775
791
  const config = yield* getConfigForEnv(env);
776
792
  const startTimeMs = yield* Clock.currentTimeMillis;
777
793
  const resolvedConfig = yield* resolveDbConfig(config, env);
@@ -827,6 +843,13 @@ export class DbService extends Context.Service<
827
843
  mode: SchemaMode,
828
844
  table?: string,
829
845
  ) {
846
+ if (table !== undefined) {
847
+ const unsafeReason = unsafeOutboundTextReason(table);
848
+ if (unsafeReason !== null) {
849
+ return yield* unsafeSqlError(unsafeReason);
850
+ }
851
+ }
852
+
830
853
  const config = yield* getConfigForEnv(env);
831
854
  const startTimeMs = yield* Clock.currentTimeMillis;
832
855
  const resolvedConfig = yield* resolveDbConfig(config, env);
@@ -1,9 +1,15 @@
1
1
  import { Command, Flag } from "effect/unstable/cli";
2
2
  import { Effect, Option } from "effect";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
4
  import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
4
6
 
5
7
  import { formatOption, logFormatted } from "#shared";
6
- import { isSensitivePath, resolveOptionalTextInput, validateOutboundFile } from "#gh/text-input";
8
+ import {
9
+ isSensitivePath,
10
+ readValidatedOutboundFile,
11
+ resolveOptionalTextInput,
12
+ } from "#gh/text-input";
7
13
  import { GitHubCommandError } from "./errors";
8
14
  import { GitHubService } from "./service";
9
15
 
@@ -118,29 +124,13 @@ const validateFilePath = (path: string, command: string) =>
118
124
  // `gh gist create/edit` read content from files only; inline --body is staged in a temp file
119
125
  // whose basename becomes the gist filename.
120
126
  const stageBody = Effect.fn("gist.stageBody")(function* (opts: {
121
- body: string;
127
+ body: string | Uint8Array;
122
128
  filename: string;
123
129
  command: string;
124
130
  }) {
125
131
  const directory = yield* Effect.acquireRelease(
126
132
  Effect.tryPromise({
127
- try: async () => {
128
- const proc = Bun.spawn(
129
- ["mktemp", "-d", join(process.env.TMPDIR ?? "/tmp", "gh-tool-gist-XXXXXX")],
130
- { stdout: "pipe", stderr: "pipe" },
131
- );
132
- const [exitCode, stdout, stderr] = await Promise.all([
133
- proc.exited,
134
- new Response(proc.stdout).text(),
135
- new Response(proc.stderr).text(),
136
- ]);
137
-
138
- if (exitCode !== 0) {
139
- throw new Error(stderr.trim() || `mktemp exited with code ${exitCode}`);
140
- }
141
-
142
- return stdout.trim();
143
- },
133
+ try: () => mkdtemp(join(process.env.TMPDIR ?? tmpdir(), "gh-tool-gist-")),
144
134
  catch: (error) =>
145
135
  inputError(
146
136
  `Failed to stage gist content: ${error instanceof Error ? error.message : String(error)}`,
@@ -149,20 +139,14 @@ const stageBody = Effect.fn("gist.stageBody")(function* (opts: {
149
139
  }),
150
140
  (tempDirectory) =>
151
141
  Effect.tryPromise({
152
- try: async () => {
153
- const proc = Bun.spawn(["rm", "-rf", tempDirectory], {
154
- stdout: "ignore",
155
- stderr: "ignore",
156
- });
157
- await proc.exited;
158
- },
142
+ try: () => rm(tempDirectory, { recursive: true, force: true }),
159
143
  catch: () => undefined,
160
144
  }).pipe(Effect.ignore),
161
145
  );
162
146
  const path = join(directory, opts.filename);
163
147
 
164
148
  yield* Effect.tryPromise({
165
- try: () => Bun.write(path, opts.body),
149
+ try: () => writeFile(path, opts.body),
166
150
  catch: (error) =>
167
151
  inputError(
168
152
  `Failed to stage gist content: ${error instanceof Error ? error.message : String(error)}`,
@@ -293,45 +277,55 @@ export const createGist = Effect.fn("gist.createGist")(function* (opts: {
293
277
  public: boolean;
294
278
  }) {
295
279
  const gh = yield* GitHubService;
280
+ return yield* Effect.scoped(
281
+ Effect.gen(function* () {
282
+ const snapshots: string[] = [];
283
+ for (const path of opts.paths) {
284
+ const validated = yield* readValidatedOutboundFile(path, "gh-tool gist create");
285
+ const filename = path.split(/[\\/]/).at(-1) ?? path;
286
+ snapshots.push(
287
+ yield* stageBody({ body: validated.bytes, filename, command: "gh-tool gist create" }),
288
+ );
289
+ }
296
290
 
297
- for (const path of opts.paths) yield* validateOutboundFile(path, "gh-tool gist create");
298
-
299
- const args = ["gist", "create", ...opts.paths];
291
+ const args = ["gist", "create", ...snapshots];
300
292
 
301
- if (opts.description !== null) {
302
- args.push("--desc", opts.description);
303
- }
293
+ if (opts.description !== null) {
294
+ args.push("--desc", opts.description);
295
+ }
304
296
 
305
- if (opts.public) {
306
- args.push("--public");
307
- }
297
+ if (opts.public) {
298
+ args.push("--public");
299
+ }
308
300
 
309
- const result = yield* gh.runGh(args);
310
- const url =
311
- result.stdout
312
- .trim()
313
- .split("\n")
314
- .map((line) => line.trim())
315
- .findLast((line) => line.startsWith("https://")) ?? "";
316
-
317
- if (url === "") {
318
- return yield* Effect.fail(
319
- inputError("gh gist create did not return a gist URL", "gh-tool gist create"),
320
- );
321
- }
301
+ const result = yield* gh.runGh(args);
302
+ const url =
303
+ result.stdout
304
+ .trim()
305
+ .split("\n")
306
+ .map((line) => line.trim())
307
+ .findLast((line) => line.startsWith("https://")) ?? "";
308
+
309
+ if (url === "") {
310
+ return yield* Effect.fail(
311
+ inputError("gh gist create did not return a gist URL", "gh-tool gist create"),
312
+ );
313
+ }
322
314
 
323
- const created: GistCreateResult = {
324
- created: true,
325
- id: url.split("/").at(-1) ?? "",
326
- url,
327
- public: opts.public,
328
- files: opts.paths.map((path) => path.split("/").at(-1) ?? path),
329
- };
315
+ const created: GistCreateResult = {
316
+ created: true,
317
+ id: url.split("/").at(-1) ?? "",
318
+ url,
319
+ public: opts.public,
320
+ files: opts.paths.map((path) => path.split(/[\\/]/).at(-1) ?? path),
321
+ };
330
322
 
331
- return created;
323
+ return created;
324
+ }),
325
+ );
332
326
  });
333
327
 
334
- const editGist = Effect.fn("gist.editGist")(function* (opts: {
328
+ export const editGist = Effect.fn("gist.editGist")(function* (opts: {
335
329
  id: string;
336
330
  description: string | null;
337
331
  add: string | null;
@@ -341,15 +335,13 @@ const editGist = Effect.fn("gist.editGist")(function* (opts: {
341
335
  }) {
342
336
  const gh = yield* GitHubService;
343
337
 
344
- for (const path of [opts.sourcePath, opts.add]) {
345
- if (path !== null) yield* validateOutboundFile(path, "gh-tool gist edit");
346
- }
347
-
348
338
  const args = ["gist", "edit", opts.id];
349
339
  const changes: string[] = [];
350
340
 
351
341
  if (opts.sourcePath !== null) {
352
- args.push(opts.sourcePath);
342
+ const validated = yield* readValidatedOutboundFile(opts.sourcePath, "gh-tool gist edit");
343
+ const filename = opts.filename ?? opts.sourcePath.split(/[\\/]/).at(-1) ?? "";
344
+ args.push(yield* stageBody({ body: validated.bytes, filename, command: "gh-tool gist edit" }));
353
345
  changes.push(`content:${opts.filename ?? opts.sourcePath.split("/").at(-1)}`);
354
346
  }
355
347
 
@@ -359,7 +351,14 @@ const editGist = Effect.fn("gist.editGist")(function* (opts: {
359
351
  }
360
352
 
361
353
  if (opts.add !== null) {
362
- args.push("--add", opts.add);
354
+ const validated = yield* readValidatedOutboundFile(opts.add, "gh-tool gist edit");
355
+ const filename = opts.add.split(/[\\/]/).at(-1) ?? opts.add;
356
+ const snapshot = yield* stageBody({
357
+ body: validated.bytes,
358
+ filename,
359
+ command: "gh-tool gist edit",
360
+ });
361
+ args.push("--add", snapshot);
363
362
  changes.push(`add:${opts.add.split("/").at(-1)}`);
364
363
  }
365
364
 
@@ -5,6 +5,7 @@ import { Effect, Layer } from "effect";
5
5
 
6
6
  import { makeSchemaCommand, renderCauseToStderr, VERSION } from "#shared";
7
7
  import { AuditServiceLayer, withAudit } from "#shared/audit";
8
+ import { withRedactedOutput } from "#shared/output-boundary";
8
9
  import { ConfigServiceLayer } from "#config";
9
10
  import {
10
11
  issueCloseCommand,
@@ -240,6 +241,7 @@ WORKFLOW FOR AI AGENTS:
240
241
 
241
242
  const cli = Command.run(mainCommand, {
242
243
  version: VERSION,
244
+ renderErrors: false,
243
245
  });
244
246
 
245
247
  const MainLayer = GitHubService.layer.pipe(
@@ -248,7 +250,7 @@ const MainLayer = GitHubService.layer.pipe(
248
250
  Layer.provideMerge(ConfigServiceLayer),
249
251
  );
250
252
 
251
- const program = withAudit("gh", cli).pipe(
253
+ const program = withRedactedOutput(withAudit("gh", cli)).pipe(
252
254
  Effect.provide(MainLayer),
253
255
  Effect.tapCause(renderCauseToStderr),
254
256
  );
@@ -21,6 +21,7 @@ import { GitHubCommandError, GitHubMergeError } from "#gh/errors";
21
21
  import { GitHubService } from "#gh/service";
22
22
  import type { GhResult } from "#gh/service";
23
23
  import { logText } from "#shared";
24
+ import { redactSensitiveText } from "#shared/content-security";
24
25
 
25
26
  import type { ButStatusJson, PRViewJsonResult } from "./helpers";
26
27
  import { pollUntilResolved } from "#shared/poll-until-resolved";
@@ -114,14 +115,18 @@ const resolveMergeSha = Effect.fn("pr.resolveMergeSha")(function* (
114
115
  Effect.map((sha) => ({ sha, failed: false as const })),
115
116
  Effect.catch((error) =>
116
117
  Console.error(
117
- `Warning: PR #${pr} was merged, but its merge commit SHA could not be read: ${error.message}`,
118
+ redactSensitiveText(
119
+ `Warning: PR #${pr} was merged, but its merge commit SHA could not be read: ${error.message}`,
120
+ ),
118
121
  ).pipe(Effect.as({ sha: null, failed: true as const })),
119
122
  ),
120
123
  );
121
124
 
122
125
  if (result.sha === null && !result.failed) {
123
126
  yield* Console.error(
124
- `Warning: PR #${pr} was merged, but its merge commit SHA is still unknown after ${MERGE_SHA_WAIT_SECONDS}s.`,
127
+ redactSensitiveText(
128
+ `Warning: PR #${pr} was merged, but its merge commit SHA is still unknown after ${MERGE_SHA_WAIT_SECONDS}s.`,
129
+ ),
125
130
  );
126
131
  }
127
132
 
@@ -1088,7 +1093,9 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
1088
1093
  if (error._tag !== "GitHubCommandError") {
1089
1094
  return rollbackNote === ""
1090
1095
  ? Effect.fail(error)
1091
- : Console.error(rollbackNote.trim()).pipe(Effect.andThen(Effect.fail(error)));
1096
+ : Console.error(redactSensitiveText(rollbackNote.trim())).pipe(
1097
+ Effect.andThen(Effect.fail(error)),
1098
+ );
1092
1099
  }
1093
1100
 
1094
1101
  const stderr = error.stderr.toLowerCase();