bunosh 0.5.0 → 0.5.6

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
@@ -427,7 +427,6 @@ export async function checkServices() {
427
427
 
428
428
  - **[Examples](docs/examples.md)** — Real-world examples and workflows
429
429
  - **[AI Integration](docs/ai.md)** — Built-in AI support
430
- - **[MCP Integration](docs/mcp.md)** — Expose commands to AI assistants (Claude, Cursor, etc.)
431
430
  - **[JavaScript Execution](docs/javascript-execution.md)** — Execute JavaScript directly via CLI
432
431
  - **[Bash Migration Guide](docs/bash-migration-guide.md)** — Convert bash scripts to Bunosh
433
432
  - **[Node.js Migration Guide](docs/nodejs-migration-guide.md)** — Migrate from Node.js scripts
package/bunosh.js CHANGED
@@ -124,45 +124,6 @@ async function main() {
124
124
  process.argv.splice(envFileIndex, 1);
125
125
  }
126
126
 
127
- const mcpFlagIndex = process.argv.indexOf('-mcp');
128
- if (mcpFlagIndex !== -1) {
129
- process.argv.splice(mcpFlagIndex, 1);
130
-
131
- process.env.BUNOSH_MCP_MODE = 'true';
132
-
133
- const { createMcpServer, startMcpServer } = await import('./src/mcp-server.js');
134
-
135
- let tasksFile;
136
- let bunoshfileDir;
137
- if (customBunoshfile) {
138
- const resolvedPath = path.isAbsolute(customBunoshfile) ? customBunoshfile : path.resolve(customBunoshfile);
139
- if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
140
- tasksFile = path.join(resolvedPath, BUNOSHFILE);
141
- bunoshfileDir = resolvedPath;
142
- } else {
143
- tasksFile = resolvedPath;
144
- bunoshfileDir = path.dirname(resolvedPath);
145
- }
146
- } else {
147
- tasksFile = path.join(process.cwd(), BUNOSHFILE);
148
- bunoshfileDir = process.cwd();
149
- }
150
-
151
- if (!existsSync(tasksFile)) {
152
- console.error('Bunoshfile not found for MCP mode');
153
- process.exit(1);
154
- }
155
-
156
- loadEnvFiles(bunoshfileDir, customEnvFile);
157
-
158
- const { tasks, sources } = await loadBunoshfiles(tasksFile);
159
-
160
- const server = createMcpServer(tasks, sources);
161
- await startMcpServer(server);
162
-
163
- return;
164
- }
165
-
166
127
  let tasksFile;
167
128
  let bunoshfileDir;
168
129
  if (customBunoshfile) {
package/index.js CHANGED
@@ -1,5 +1,6 @@
1
- import exec from "./src/tasks/exec.js";
2
1
  import shell from "./src/tasks/shell.js";
2
+ // Deprecated: `exec` is an alias for `shell`, kept for backward compatibility.
3
+ const exec = shell;
3
4
  import fetch from "./src/tasks/fetch.js";
4
5
  import writeToFile from "./src/tasks/writeToFile.js";
5
6
  import copyFile from "./src/tasks/copyFile.js";
@@ -11,7 +12,7 @@ export { exec, shell, fetch, writeToFile, copyFile, ai, ask, yell, say, task, tr
11
12
 
12
13
  export function buildCmd(cmd) {
13
14
  return function (args) {
14
- return exec`${cmd} ${args}`;
15
+ return shell`${cmd} ${args}`;
15
16
  };
16
17
  }
17
18
 
@@ -35,7 +36,7 @@ global.bunosh = {
35
36
  silent,
36
37
  TaskResult,
37
38
  buildCmd,
38
- $: exec,
39
+ $: shell,
39
40
  };
40
41
 
41
42
  export default global.bunosh;
package/package.json CHANGED
@@ -1,8 +1,25 @@
1
1
  {
2
2
  "name": "bunosh",
3
- "version": "0.5.0",
3
+ "version": "0.5.6",
4
+ "description": "Task runner that turns JavaScript functions into CLI commands. Runs on Bun and Node.js.",
4
5
  "type": "module",
5
6
  "module": "index.js",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/DavertMik/bunosh.git"
10
+ },
11
+ "homepage": "https://github.com/DavertMik/bunosh#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/DavertMik/bunosh/issues"
14
+ },
15
+ "author": "davert",
16
+ "keywords": [
17
+ "task-runner",
18
+ "cli",
19
+ "bun",
20
+ "automation",
21
+ "scripts"
22
+ ],
6
23
  "bin": {
7
24
  "bunosh": "./bunosh.js"
8
25
  },
@@ -19,7 +36,6 @@
19
36
  "@ai-sdk/openai": "^2.0.23",
20
37
  "@babel/parser": "^7.27.5",
21
38
  "@babel/traverse": "^7.27.4",
22
- "@modelcontextprotocol/sdk": "^1.19.1",
23
39
  "ai": "^5.0.29",
24
40
  "chalk": "^5.4.1",
25
41
  "commander": "^14.0.0",
@@ -12,7 +12,11 @@ const STATUS_CONFIG = {
12
12
 
13
13
  export class ConsoleFormatter extends BaseFormatter {
14
14
  shouldDelayStart() {
15
- return false;
15
+ return true;
16
+ }
17
+
18
+ getStartDelay() {
19
+ return 1000;
16
20
  }
17
21
  format(taskName, status, taskType, extra = {}) {
18
22
  const config = STATUS_CONFIG[status];
package/src/io.js CHANGED
@@ -7,11 +7,6 @@ export function say(...args) {
7
7
  }
8
8
 
9
9
  export async function ask(question, defaultValueOrOptions = {}, options = {}) {
10
- // Check if we're in MCP mode and should use the interactive ask function
11
- if (globalThis._mcpAskFunction) {
12
- return globalThis._mcpAskFunction(question, defaultValueOrOptions, options);
13
- }
14
-
15
10
  // Track that we're in an ask operation to prevent duplicate exit summaries
16
11
  globalThis._bunoshInAskOperation = true;
17
12
  // Smart parameter detection
package/src/printer.js CHANGED
@@ -85,7 +85,10 @@ export class Printer {
85
85
  const delay = this.formatter.getStartDelay ? this.formatter.getStartDelay() : 50;
86
86
 
87
87
  if (this.formatter.shouldDelayStart && this.formatter.shouldDelayStart()) {
88
+ this.pendingStart = { taskName, extra };
88
89
  this.startTimeout = setTimeout(() => {
90
+ this.startTimeout = null;
91
+ this.pendingStart = null;
89
92
  this.hasStarted = true;
90
93
  this.print(taskName, 'start', extra);
91
94
  }, delay);
@@ -95,19 +98,37 @@ export class Printer {
95
98
  }
96
99
  }
97
100
 
98
- finish(taskName, extra = {}) {
101
+ _flushPendingStart() {
102
+ if (!this.startTimeout) return;
103
+ clearTimeout(this.startTimeout);
104
+ this.startTimeout = null;
105
+ const pending = this.pendingStart;
106
+ this.pendingStart = null;
107
+ if (pending) {
108
+ this.hasStarted = true;
109
+ this.print(pending.taskName, 'start', pending.extra);
110
+ }
111
+ }
112
+
113
+ _cancelPendingStart() {
99
114
  if (this.startTimeout) {
100
115
  clearTimeout(this.startTimeout);
101
116
  this.startTimeout = null;
102
117
  }
118
+ this.pendingStart = null;
119
+ }
120
+
121
+ cancel() {
122
+ this._cancelPendingStart();
123
+ }
124
+
125
+ finish(taskName, extra = {}) {
126
+ this._cancelPendingStart();
103
127
  this.print(taskName, 'finish', extra);
104
128
  }
105
129
 
106
130
  error(taskName, error = null, extra = {}) {
107
- if (this.startTimeout) {
108
- clearTimeout(this.startTimeout);
109
- this.startTimeout = null;
110
- }
131
+ this._cancelPendingStart();
111
132
  if (error) {
112
133
  extra.error = typeof error === 'string' ? error : error.message;
113
134
  }
@@ -115,10 +136,7 @@ export class Printer {
115
136
  }
116
137
 
117
138
  warning(taskName, error = null, extra = {}) {
118
- if (this.startTimeout) {
119
- clearTimeout(this.startTimeout);
120
- this.startTimeout = null;
121
- }
139
+ this._cancelPendingStart();
122
140
  if (error) {
123
141
  extra.error = typeof error === 'string' ? error : error.message;
124
142
  }
@@ -128,6 +146,8 @@ export class Printer {
128
146
  output(line, isError = false) {
129
147
  if (!line.trim()) return;
130
148
 
149
+ this._flushPendingStart();
150
+
131
151
  // Add task prefix for parallel tasks on output lines
132
152
  const prefix = this.taskId ? getTaskPrefix(this.taskId) : '';
133
153
  const prefixedLine = prefix ? `${prefix} ${line}` : line;
package/src/program.js CHANGED
@@ -8,7 +8,7 @@ import { yell } from './io.js';
8
8
  import { formatError } from './error-formatter.js';
9
9
  import cprint from "./font.js";
10
10
  import { handleCompletion, detectCurrentShell, installCompletion, getCompletionPaths } from './completion.js';
11
- import { upgradeCommand } from './upgrade.js';
11
+ import { upgradeCommand, printUpgradeNoticeIfAvailable } from './upgrade.js';
12
12
 
13
13
  export const BUNOSHFILE = `Bunoshfile.js`;
14
14
 
@@ -265,16 +265,22 @@ export default async function bunosh(commands, sources) {
265
265
  }
266
266
  });
267
267
 
268
- const optionsObj = commanderArgs[commanderArgs.length - 1];
269
- if (optionsObj && typeof optionsObj === 'object') {
270
- Object.keys(opts).forEach((optName) => {
271
- const dasherizedOpt = optName.replace(/([A-Z])/g, '-$1').toLowerCase();
272
- if (optionsObj[dasherizedOpt] !== undefined) {
273
- transformedArgs.push(optionsObj[dasherizedOpt]);
268
+ const optNames = Object.keys(opts);
269
+ if (optNames.length > 0) {
270
+ const lastArg = commanderArgs[commanderArgs.length - 1];
271
+ const optionsObj = (lastArg && typeof lastArg.opts === 'function') ? lastArg.opts() : lastArg;
272
+ const mergedOpts = {};
273
+ optNames.forEach((optName) => {
274
+ const camelName = optName.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
275
+ if (optionsObj && optionsObj[camelName] !== undefined) {
276
+ mergedOpts[camelName] = optionsObj[camelName];
277
+ } else if (optionsObj && optionsObj[optName] !== undefined) {
278
+ mergedOpts[camelName] = optionsObj[optName];
274
279
  } else {
275
- transformedArgs.push(opts[optName]);
280
+ mergedOpts[camelName] = opts[optName];
276
281
  }
277
282
  });
283
+ transformedArgs.push(mergedOpts);
278
284
  }
279
285
 
280
286
  try {
@@ -385,6 +391,23 @@ export default async function bunosh(commands, sources) {
385
391
 
386
392
  internalCommands.push(setupCompletionCmd);
387
393
 
394
+ const SKILLS_REPO = 'DavertMik/bunosh-skills';
395
+
396
+ const installSkillsCmd = program.command('install-skills')
397
+ .description('Print the command to install Bunosh AI agent skills.')
398
+ .action(() => {
399
+ console.log();
400
+ console.log(`🤖 Install Bunosh AI agent skills (Claude Code, Cursor, Codex, ...):`);
401
+ console.log();
402
+ console.log(` ${color.bold(`npx skills add ${SKILLS_REPO}`)}`);
403
+ console.log();
404
+ console.log(color.dim(` Skills: bunosh-fundamentals, migrate-to-bunosh`));
405
+ console.log(color.dim(` ${SKILLS_REPO} · https://buno.sh`));
406
+ console.log();
407
+ });
408
+
409
+ internalCommands.push(installSkillsCmd);
410
+
388
411
  const upgradeCmd = program.command('upgrade')
389
412
  .description('Upgrade bunosh to the latest version')
390
413
  .option('-f, --force', 'Force upgrade even if already on latest version')
@@ -410,7 +433,7 @@ export default async function bunosh(commands, sources) {
410
433
  }
411
434
 
412
435
  const lines = description.split('\n');
413
- const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
436
+ const firstLine = ` ${color.white.bold(paddedName)} ${color.dim(lines[0])}`;
414
437
  const indentedLines = lines.slice(1).map(line =>
415
438
  line.trim() ? ` ${line}` : ''
416
439
  ).filter(line => line);
@@ -445,7 +468,7 @@ ${mainCommands}
445
468
  }
446
469
 
447
470
  const lines = description.split('\n');
448
- const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
471
+ const firstLine = ` ${color.white.bold(paddedName)} ${color.dim(lines[0])}`;
449
472
  const indentedLines = lines.slice(1).map(line =>
450
473
  line.trim() ? ` ${line}` : ''
451
474
  ).filter(line => line);
@@ -471,7 +494,7 @@ ${devCommands}
471
494
  }
472
495
 
473
496
  const lines = description.split('\n');
474
- const firstLine = ` ${color.white.bold(paddedName)} ${lines[0]}`;
497
+ const firstLine = ` ${color.white.bold(paddedName)} ${color.dim(lines[0])}`;
475
498
  const indentedLines = lines.slice(1).map(line =>
476
499
  line.trim() ? ` ${line}` : ''
477
500
  ).filter(line => line);
@@ -485,7 +508,10 @@ ${namespaceCommands}
485
508
  }
486
509
  });
487
510
 
488
- helpText += color.dim(`Special Commands:
511
+ const helpFlagRequested = process.argv.includes('--help') || process.argv.includes('-h');
512
+
513
+ if (helpFlagRequested) {
514
+ helpText += color.dim(`Special Commands:
489
515
  ${color.bold('bunosh edit')} 📝 Edit bunosh file with $EDITOR
490
516
  ${color.bold('bunosh export:scripts')} 📥 Export commands to package.json
491
517
  ${color.bold('bunosh upgrade')} 🦾 Upgrade bunosh
@@ -494,6 +520,13 @@ ${namespaceCommands}
494
520
  ${color.bold('bunosh --env-file …')} 🔧 Load custom environment file
495
521
  `);
496
522
 
523
+ helpText += `
524
+ ${color.bold('🤖 AI agent skills')} ${color.dim('(Claude Code, Cursor, Codex, ...)')}
525
+ ${color.bold('npx skills add DavertMik/bunosh-skills')}
526
+ ${color.dim('bunosh-fundamentals · migrate-to-bunosh — see "bunosh install-skills"')}
527
+ `;
528
+ }
529
+
497
530
  program.addHelpText('after', helpText);
498
531
 
499
532
  program.on("command:*", (cmd) => {
@@ -505,6 +538,7 @@ ${namespaceCommands}
505
538
 
506
539
  if (process.argv.length === 2) {
507
540
  program.outputHelp();
541
+ await printUpgradeNoticeIfAvailable();
508
542
  return program;
509
543
  }
510
544
 
package/src/task.js CHANGED
@@ -198,6 +198,8 @@ export async function task(name, fn, isSilent = false) {
198
198
 
199
199
  // Check if result is a TaskResult instance
200
200
  if (result && result.constructor && result.constructor.name === 'TaskResult') {
201
+ printer.cancel();
202
+ runningTasks.delete(taskInfo.id);
201
203
  return result;
202
204
  }
203
205
 
@@ -246,8 +248,13 @@ export async function task(name, fn, isSilent = false) {
246
248
  }
247
249
  }
248
250
 
249
- // Add try method to task function
251
+ // Add methods to task function
250
252
  task.try = tryTask;
253
+ task.stopOnFailures = stopOnFailures;
254
+ task.ignoreFailures = ignoreFailures;
255
+ task.silence = silence;
256
+ task.prints = prints;
257
+ task.silent = silent;
251
258
 
252
259
 
253
260
  export class SilentTaskWrapper {
package/src/tasks/exec.js CHANGED
@@ -1,251 +1,7 @@
1
- import { TaskResult, createTaskInfo, finishTaskInfo, getCurrentTaskId, runningTasks } from '../task.js';
2
- import Printer from '../printer.js';
3
-
4
- const isBun = typeof Bun !== 'undefined';
1
+ // Deprecated: `exec` is now an alias for `shell`.
2
+ // Use `shell` (or `$`) instead. This module is kept for backward compatibility.
3
+ import shell from './shell.js';
5
4
 
6
5
  export default function exec(strings, ...values) {
7
- // Check if called as regular function instead of template literal
8
- if (!Array.isArray(strings)) {
9
- // If first argument is a string, treat it as the command
10
- if (typeof strings === 'string') {
11
- strings = [strings];
12
- values = [];
13
- } else {
14
- throw new Error('exec() must be called as a template literal: exec`command` or exec("command")');
15
- }
16
- }
17
-
18
- const cmd = strings.reduce((accumulator, str, i) => {
19
- return accumulator + str + (values[i] || '');
20
- }, '');
21
-
22
- let envs = null;
23
- let cwd = null;
24
-
25
- const cmdPromise = new Promise(async (resolve, reject) => {
26
- // Wait for the next event loop tick to ensure .env() and .cwd() have been called
27
- await new Promise(resolve => setTimeout(resolve, 0));
28
-
29
- const currentTaskId = getCurrentTaskId();
30
-
31
- // Check if parent task is silent
32
- let isParentSilent = false;
33
- if (currentTaskId) {
34
- const parentTask = runningTasks.get(currentTaskId);
35
- if (parentTask && parentTask.isSilent) {
36
- isParentSilent = true;
37
- }
38
- }
39
-
40
- const extraInfo = {};
41
- if (cwd) extraInfo.cwd = cwd;
42
- if (envs) extraInfo.env = envs;
43
-
44
- const taskInfo = createTaskInfo(cmd, currentTaskId, isParentSilent);
45
- const printer = new Printer('exec', taskInfo.id);
46
- printer.start(cmd, extraInfo);
47
-
48
- try {
49
- if (global.disableBunForTesting || !isBun) {
50
- const result = await nodeExec(cmd, extraInfo, printer, taskInfo);
51
- if (result.status === 'success') {
52
- finishTaskInfo(taskInfo, true, null, result.output);
53
- resolve(result);
54
- } else {
55
- finishTaskInfo(taskInfo, false, new Error(result.output), result.output);
56
- resolve(result);
57
- }
58
- return;
59
- }
60
-
61
- // Bun implementation with real-time streaming
62
- const needsShell = cmd.includes('|') || cmd.includes('>') || cmd.includes('<') || cmd.includes('&&') || cmd.includes('||') || cmd.includes("'") || cmd.includes('"') || cmd.includes(';');
63
-
64
- const { spawn } = Bun;
65
- const proc = spawn({
66
- cmd: needsShell ? ['/bin/sh', '-c', cmd] : cmd.trim().split(/\s+/),
67
- cwd: cwd || process.cwd(),
68
- env: {
69
- ...(envs ? { ...process.env, ...envs } : process.env),
70
- },
71
- stdout: "pipe",
72
- stderr: "pipe",
73
- stdin: "ignore"
74
- });
75
-
76
- const decoder = new TextDecoder();
77
- let output = '';
78
- let stdout = '';
79
- let stderr = '';
80
- let finished = false;
81
-
82
- // Process stdout
83
- const readStdout = async () => {
84
- const reader = proc.stdout.getReader();
85
- let buffer = '';
86
-
87
- try {
88
- while (!finished) {
89
- const { done, value } = await reader.read();
90
- if (done) break;
91
-
92
- const text = decoder.decode(value, { stream: true });
93
- buffer += text;
94
-
95
- const lines = buffer.split('\n');
96
- buffer = lines.pop();
97
-
98
- for (const line of lines) {
99
- if (line.trim()) {
100
- printer.output(line);
101
- output += line + '\n';
102
- stdout += line + '\n';
103
- }
104
- }
105
- }
106
-
107
- if (buffer.trim()) {
108
- printer.output(buffer);
109
- output += buffer + '\n';
110
- stdout += buffer + '\n';
111
- }
112
- } finally {
113
- reader.releaseLock();
114
- }
115
- };
116
-
117
- // Process stderr
118
- const readStderr = async () => {
119
- const reader = proc.stderr.getReader();
120
- let buffer = '';
121
-
122
- try {
123
- while (!finished) {
124
- const { done, value } = await reader.read();
125
- if (done) break;
126
-
127
- const text = decoder.decode(value, { stream: true });
128
- buffer += text;
129
-
130
- const lines = buffer.split('\n');
131
- buffer = lines.pop();
132
-
133
- for (const line of lines) {
134
- if (line.trim()) {
135
- printer.output(line, true);
136
- output += line + '\n';
137
- stderr += line + '\n';
138
- }
139
- }
140
- }
141
-
142
- if (buffer.trim()) {
143
- printer.output(buffer, true);
144
- output += buffer + '\n';
145
- stderr += buffer + '\n';
146
- }
147
- } finally {
148
- reader.releaseLock();
149
- }
150
- };
151
-
152
- // Start reading both streams
153
- const [, , exitResult] = await Promise.all([
154
- readStdout(),
155
- readStderr(),
156
- proc.exited
157
- ]);
158
-
159
- finished = true;
160
- const exitCode = parseInt(exitResult, 10);
161
-
162
- const metadata = {
163
- taskType: 'exec',
164
- exitCode,
165
- stdout: stdout.trim(),
166
- stderr: stderr.trim()
167
- };
168
-
169
- if (exitCode === 0) {
170
- printer.finish(cmd);
171
- finishTaskInfo(taskInfo, true, null, output.trim());
172
- resolve(TaskResult.success(output.trim(), metadata));
173
- } else {
174
- const error = new Error(`Exit code: ${exitCode}`);
175
- printer.error(cmd, null, { exitCode });
176
- finishTaskInfo(taskInfo, false, error, output.trim());
177
- resolve(TaskResult.fail(output.trim(), metadata));
178
- }
179
- } catch (error) {
180
- printer.error(cmd, error);
181
- finishTaskInfo(taskInfo, false, error, error.message);
182
- resolve(TaskResult.fail(error.message, { taskType: 'exec' }));
183
- }
184
- });
185
-
186
- cmdPromise.env = (newEnvs) => {
187
- envs = newEnvs;
188
- return cmdPromise;
189
- };
190
-
191
- cmdPromise.cwd = (newCwd) => {
192
- cwd = newCwd;
193
- return cmdPromise;
194
- };
195
-
196
- return cmdPromise;
197
- }
198
-
199
- async function nodeExec(cmd, extraInfo, printer, taskInfo) {
200
- // Node.js fallback - simple execution without real-time output
201
- const { spawn } = await import('child_process');
202
-
203
- return new Promise((resolve) => {
204
- const proc = spawn('sh', ['-c', cmd], {
205
- cwd: extraInfo.cwd || process.cwd(),
206
- env: extraInfo.env ? { ...process.env, ...extraInfo.env } : process.env,
207
- stdio: ['ignore', 'pipe', 'pipe']
208
- });
209
-
210
- let output = '';
211
- let stdout = '';
212
- let stderr = '';
213
-
214
- proc.stdout.on('data', (data) => {
215
- const text = data.toString();
216
- printer.output(text.trim());
217
- output += text;
218
- stdout += text;
219
- });
220
-
221
- proc.stderr.on('data', (data) => {
222
- const text = data.toString();
223
- printer.output(text.trim(), true);
224
- output += text;
225
- stderr += text;
226
- });
227
-
228
- proc.on('close', (code) => {
229
- const combinedOutput = (output).trim();
230
- const metadata = {
231
- taskType: 'exec',
232
- exitCode: code,
233
- stdout: stdout.trim(),
234
- stderr: stderr.trim()
235
- };
236
-
237
- if (code === 0) {
238
- printer.finish(cmd);
239
- resolve(TaskResult.success(combinedOutput, metadata));
240
- } else {
241
- printer.error(cmd, new Error(`Exit code: ${code}`));
242
- resolve(TaskResult.fail(combinedOutput, metadata));
243
- }
244
- });
245
-
246
- proc.on('error', (error) => {
247
- printer.error(cmd, error);
248
- resolve(TaskResult.fail(error.message, { taskType: 'exec' }));
249
- });
250
- });
6
+ return shell(strings, ...values);
251
7
  }
@@ -23,6 +23,7 @@ export default async function httpFetch() {
23
23
 
24
24
  try {
25
25
  const response = await fetch(...arguments);
26
+ const responseForBody = response.clone(); // Clone before streaming consumes the body
26
27
  const textDecoder = new TextDecoder();
27
28
  let output = '';
28
29
 
@@ -40,7 +41,7 @@ export default async function httpFetch() {
40
41
 
41
42
  const metadata = {
42
43
  taskType: 'fetch',
43
- response: response.clone(), // Clone to allow json() to be called later
44
+ response: responseForBody, // Unconsumed clone so json()/text() work later
44
45
  status: response.status,
45
46
  statusText: response.statusText,
46
47
  headers: Object.fromEntries(response.headers.entries())