@flue/sdk 0.3.5 → 0.3.7

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/dist/index.mjs CHANGED
@@ -1,9 +1,10 @@
1
- import { a as parseFrontmatterFile, n as createTools, t as BUILTIN_TOOL_NAMES } from "./agent-BB4lwAd5.mjs";
1
+ import { a as parseFrontmatterFile, n as createTools, t as BUILTIN_TOOL_NAMES } from "./agent-BTB0809P.mjs";
2
2
  import * as esbuild from "esbuild";
3
3
  import * as fs from "node:fs";
4
4
  import * as path from "node:path";
5
5
  import { packageUpSync } from "package-up";
6
6
  import { spawn } from "node:child_process";
7
+ import { randomUUID } from "node:crypto";
7
8
  import { parseEnv } from "node:util";
8
9
 
9
10
  //#region src/cloudflare-wrangler-merge.ts
@@ -267,11 +268,11 @@ function stripNoisyWranglerDefaults(merged) {
267
268
  }
268
269
  /**
269
270
  * Return the list of `class_name`s declared in the user's wrangler
270
- * `durable_objects.bindings` that contain the literal substring `Sandbox`
271
+ * `durable_objects.bindings` that end with the literal suffix `Sandbox`
271
272
  * (case-sensitive).
272
273
  *
273
274
  * This is Flue's convention for wiring `@cloudflare/sandbox`: any DO binding
274
- * whose class name contains `Sandbox` triggers an automatic re-export in the
275
+ * whose class name ends with `Sandbox` triggers an automatic re-export in the
275
276
  * generated Worker entry:
276
277
  *
277
278
  * export { Sandbox as <class_name> } from '@cloudflare/sandbox';
@@ -281,6 +282,13 @@ function stripNoisyWranglerDefaults(merged) {
281
282
  * `@cloudflare/sandbox` package. Each distinct `class_name` can be paired with
282
283
  * a different container image in the user's `containers[]` config.
283
284
  *
285
+ * The match is intentionally a suffix (not substring) so that user-defined
286
+ * classes whose names merely contain "Sandbox" mid-word — e.g. `MySandboxV2`,
287
+ * `MySandboxedAgent`, `LegacySandboxedThing` — are not silently overridden
288
+ * by the `@cloudflare/sandbox` re-export. Note that classes whose names
289
+ * still end in `Sandbox` (e.g. `MockSandbox`, `NotASandbox`) will match;
290
+ * to opt out, rename the class to not end in `Sandbox`.
291
+ *
284
292
  * Returns unique, sorted class names. Non-object bindings or bindings without
285
293
  * a string `class_name` are ignored.
286
294
  */
@@ -294,7 +302,7 @@ function detectSandboxBindings(userConfig) {
294
302
  if (typeof entry !== "object" || entry === null) continue;
295
303
  const className = entry.class_name;
296
304
  if (typeof className !== "string") continue;
297
- if (className.includes("Sandbox")) found.add(className);
305
+ if (className.endsWith("Sandbox")) found.add(className);
298
306
  }
299
307
  return Array.from(found).sort();
300
308
  }
@@ -328,7 +336,7 @@ function assertSandboxPackageInstalled(sandboxClassNames, searchDirs) {
328
336
  current = path.dirname(current);
329
337
  }
330
338
  }
331
- throw new Error(`[flue] Your wrangler config declares DO binding(s) whose class_name contains "Sandbox" (${sandboxClassNames.join(", ")}), but @cloudflare/sandbox is not in your package.json. Install it: \`npm install @cloudflare/sandbox\`.`);
339
+ throw new Error(`[flue] Your wrangler config declares DO binding(s) whose class_name ends with "Sandbox" (${sandboxClassNames.join(", ")}), but @cloudflare/sandbox is not in your package.json. Install it: \`npm install @cloudflare/sandbox\`.`);
332
340
  }
333
341
  /**
334
342
  * Write the wrangler deploy-redirect file at `<outputDir>/.wrangler/deploy/config.json`
@@ -396,6 +404,7 @@ var CloudflarePlugin = class {
396
404
  }`;
397
405
  }).join("\n\n");
398
406
  const { config: userConfig } = await this.getUserConfig(ctx.outputDir);
407
+ const sandboxReExports = detectSandboxBindings(userConfig).map((name) => `export { Sandbox as ${name} } from '@cloudflare/sandbox';`).join("\n");
399
408
  return `
400
409
  // Auto-generated by @flue/sdk build (cloudflare)
401
410
  import { Agent, routeAgentRequest } from 'agents';
@@ -405,6 +414,13 @@ import {
405
414
  InMemorySessionStore,
406
415
  bashFactoryToSessionEnv,
407
416
  resolveModel,
417
+ parseJsonBody,
418
+ toHttpResponse,
419
+ toSseData,
420
+ AgentNotFoundError,
421
+ MethodNotAllowedError,
422
+ RouteNotFoundError,
423
+ InvalidRequestError,
408
424
  } from '@flue/sdk/internal';
409
425
  import { runWithCloudflareContext, cfSandboxToSessionEnv } from '@flue/sdk/cloudflare';
410
426
 
@@ -417,12 +433,11 @@ const skills = {};
417
433
  const systemPrompt = '';
418
434
  const manifest = ${manifest};
419
435
 
420
- // ─── Infrastructure ─────────────────────────────────────────────────────────
421
-
422
- // No build-time model default. The user sets model at runtime via
423
- // \`init({ model: "provider/model-id" })\` for an agent default, or via
424
- // \`{ model: "provider/model-id" }\` on any individual prompt/skill/task call.
425
- const model = undefined;
436
+ // Set of webhook-accessible agent names (raw form, as used in URL segments).
437
+ // Used by the worker fetch handler to pre-route requests and reject unknown
438
+ // agents with a JSON 404 envelope before the request hits the partyserver
439
+ // dispatcher (which would otherwise return text/plain "Invalid request").
440
+ const webhookAgentNames = new Set(${JSON.stringify(webhookAgents.map((a) => a.name))});
426
441
 
427
442
  // ─── Sandbox Environments ───────────────────────────────────────────────────
428
443
 
@@ -527,7 +542,7 @@ function createContextForRequest(id, payload, doInstance) {
527
542
  payload,
528
543
  env: doInstance?.env ?? {},
529
544
  agentConfig: {
530
- systemPrompt, skills, roles, model, resolveModel,
545
+ systemPrompt, skills, roles, model: undefined, resolveModel,
531
546
  },
532
547
  createDefaultEnv,
533
548
  createLocalEnv,
@@ -597,19 +612,16 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
597
612
  // Agent id is the DO "room name" set by routeAgentRequest
598
613
  const id = doInstance.name;
599
614
 
600
- // Parse payload
601
- let payload;
602
615
  try {
603
- payload = await request.json();
604
- } catch {
605
- payload = {};
606
- }
616
+ // Parse the request body. Throws on invalid Content-Type or malformed
617
+ // JSON; returns {} for genuinely empty bodies (so no-payload agents
618
+ // still work).
619
+ const payload = await parseJsonBody(request);
607
620
 
608
- const accept = request.headers.get('accept') || '';
609
- const isWebhook = request.headers.get('x-webhook') === 'true';
610
- const isSSE = accept.includes('text/event-stream') && !isWebhook;
621
+ const accept = request.headers.get('accept') || '';
622
+ const isWebhook = request.headers.get('x-webhook') === 'true';
623
+ const isSSE = accept.includes('text/event-stream') && !isWebhook;
611
624
 
612
- try {
613
625
  // Fire-and-forget (webhook mode)
614
626
  if (isWebhook) {
615
627
  const requestId = crypto.randomUUID();
@@ -628,7 +640,13 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
628
640
  });
629
641
  }
630
642
 
631
- // SSE streaming mode
643
+ // SSE streaming mode. Two error regimes meet here:
644
+ // - Pre-stream errors (body parsing, etc.) have already thrown above
645
+ // and are caught by the outer try/catch — rendered as plain HTTP
646
+ // responses by toHttpResponse, since headers haven't been sent yet.
647
+ // - Errors during agent execution surface as in-stream \`error\`
648
+ // events with the canonical envelope (via toSseData), since by
649
+ // then the 200 + text/event-stream headers are already on the wire.
632
650
  if (isSSE) {
633
651
  const { readable, writable } = new TransformStream();
634
652
  const writer = writable.getWriter();
@@ -640,7 +658,7 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
640
658
  const lines = [];
641
659
  if (event) lines.push('event: ' + event);
642
660
  lines.push('id: ' + eventId++);
643
- lines.push('data: ' + JSON.stringify(data));
661
+ lines.push('data: ' + (typeof data === 'string' ? data : JSON.stringify(data)));
644
662
  lines.push('', '');
645
663
  await writer.write(encoder.encode(lines.join('\\n')));
646
664
  };
@@ -662,10 +680,7 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
662
680
  'result',
663
681
  );
664
682
  } catch (err) {
665
- await writeSSE(
666
- { type: 'error', error: String(err) },
667
- 'error',
668
- );
683
+ await writeSSE(toSseData(err), 'error');
669
684
  if (!isIdle) {
670
685
  await writeSSE({ type: 'idle' }, 'idle');
671
686
  }
@@ -696,11 +711,10 @@ async function handleAgentRequest(request, doInstance, agentName, handler) {
696
711
  ctx.setEventCallback(undefined);
697
712
  }
698
713
  } catch (err) {
699
- console.error('[flue] Agent error:', agentName, err);
700
- return new Response(
701
- JSON.stringify({ error: String(err) }),
702
- { status: 500, headers: { 'content-type': 'application/json' } },
703
- );
714
+ // toHttpResponse logs unknowns via flueLog.error no extra console.error
715
+ // needed here. The agentName tag is captured in the wrapped error's
716
+ // server-side log line via flueLog's prefix.
717
+ return toHttpResponse(err);
704
718
  }
705
719
  }
706
720
 
@@ -710,38 +724,77 @@ ${agentClasses}
710
724
 
711
725
  // ─── User-declared Sandbox re-exports ──────────────────────────────────────
712
726
  // One line per DO binding in the user's wrangler.jsonc whose class_name
713
- // contains "Sandbox". Flue aliases the single \`Sandbox\` class shipped by
727
+ // ends with "Sandbox". Flue aliases the single \`Sandbox\` class shipped by
714
728
  // \`@cloudflare/sandbox\` so each user-chosen class_name resolves at the
715
729
  // bundle's top level. The binding + container image configuration is owned
716
730
  // by the user's wrangler.jsonc.
717
- ${detectSandboxBindings(userConfig).map((name) => `export { Sandbox as ${name} } from '@cloudflare/sandbox';`).join("\n")}
731
+ ${sandboxReExports}
718
732
 
719
733
  // ─── Worker Fetch Handler ───────────────────────────────────────────────────
720
734
 
721
735
  export default {
722
736
  async fetch(request, env) {
723
- const url = new URL(request.url);
737
+ try {
738
+ const url = new URL(request.url);
739
+ const method = request.method;
724
740
 
725
- // Health check
726
- if (url.pathname === '/health') {
727
- return new Response(JSON.stringify({ status: 'ok' }), {
728
- headers: { 'content-type': 'application/json' },
729
- });
730
- }
741
+ // Health check
742
+ if (url.pathname === '/health') {
743
+ return new Response(JSON.stringify({ status: 'ok' }), {
744
+ headers: { 'content-type': 'application/json' },
745
+ });
746
+ }
731
747
 
732
- // Agent manifest
733
- if (url.pathname === '/agents' && request.method === 'GET') {
734
- return new Response(JSON.stringify(manifest), {
735
- headers: { 'content-type': 'application/json' },
736
- });
737
- }
748
+ // Agent manifest
749
+ if (url.pathname === '/agents' && method === 'GET') {
750
+ return new Response(JSON.stringify(manifest), {
751
+ headers: { 'content-type': 'application/json' },
752
+ });
753
+ }
738
754
 
739
- // Route to per-agent DOs via the Agents SDK
740
- // URL: /agents/<agent-name>/<id>
741
- const response = await routeAgentRequest(request, env);
742
- if (response) return response;
755
+ // Webhook agent route: /agents/<name>/<id>
756
+ //
757
+ // We pre-check method and agent registration here, BEFORE delegating to
758
+ // routeAgentRequest. Without this, partyserver (the transitive
759
+ // dispatcher behind routeAgentRequest) returns a text/plain
760
+ // "Invalid request" 400 for unknown agent namespaces — visibly
761
+ // inconsistent with the rest of the API and with the Node target.
762
+ // Pre-routing means every error path in the agent route flows through
763
+ // the same JSON envelope.
764
+ const agentRouteMatch = url.pathname.match(/^\\/agents\\/([^/]+)\\/([^/]+)\\/?$/);
765
+ if (agentRouteMatch) {
766
+ if (method !== 'POST') {
767
+ throw new MethodNotAllowedError({ method, allowed: ['POST'] });
768
+ }
769
+ const name = decodeURIComponent(agentRouteMatch[1]);
770
+ const id = decodeURIComponent(agentRouteMatch[2]);
771
+ if (name.trim() === '' || id.trim() === '') {
772
+ throw new InvalidRequestError({
773
+ reason: 'Webhook URLs must have the shape /agents/<name>/<id> with non-empty segments.',
774
+ });
775
+ }
776
+ if (!webhookAgentNames.has(name)) {
777
+ throw new AgentNotFoundError({
778
+ name,
779
+ available: Array.from(webhookAgentNames),
780
+ });
781
+ }
782
+ // All gating passed. Delegate to the Agents SDK / partyserver, which
783
+ // dispatches into the per-agent DO's onRequest → handleAgentRequest.
784
+ // routeAgentRequest may still return null for shape mismatches we
785
+ // didn't anticipate; treat that as a route_not_found with a hint.
786
+ const response = await routeAgentRequest(request, env);
787
+ if (response) return response;
788
+ throw new RouteNotFoundError({ method, path: url.pathname });
789
+ }
743
790
 
744
- return new Response('Not found', { status: 404 });
791
+ // Anything else: canonical 404 envelope.
792
+ throw new RouteNotFoundError({ method, path: url.pathname });
793
+ } catch (err) {
794
+ // toHttpResponse logs unknowns via flueLog.error — no extra
795
+ // console.error needed at this layer.
796
+ return toHttpResponse(err);
797
+ }
745
798
  },
746
799
  };
747
800
  `;
@@ -766,7 +819,7 @@ export default {
766
819
  const sandboxClassNames = detectSandboxBindings(userConfig);
767
820
  if (sandboxClassNames.length > 0) {
768
821
  assertSandboxPackageInstalled(sandboxClassNames, [ctx.outputDir, ctx.workspaceDir]);
769
- for (const className of sandboxClassNames) console.log(`[flue] Detected Sandbox-named DO binding "${className}" re-exporting from @cloudflare/sandbox.`);
822
+ for (const className of sandboxClassNames) console.log(`[flue] Auto-wiring DO binding "${className}" to @cloudflare/sandbox's Sandbox class.`);
770
823
  }
771
824
  const merged = mergeFlueAdditions(userConfig, additions);
772
825
  stripNoisyWranglerDefaults(merged);
@@ -810,6 +863,11 @@ import {
810
863
  InMemorySessionStore,
811
864
  bashFactoryToSessionEnv,
812
865
  resolveModel,
866
+ parseJsonBody,
867
+ validateAgentRequest,
868
+ toHttpResponse,
869
+ toSseData,
870
+ RouteNotFoundError,
813
871
  } from '@flue/sdk/internal';
814
872
  import { randomUUID } from 'node:crypto';
815
873
 
@@ -827,7 +885,10 @@ const handlers = {
827
885
  ${agents.map((a) => ` ${JSON.stringify(a.name)}: ${agentVarName(a.name)},`).join("\n")}
828
886
  };
829
887
 
830
- const webhookAgents = new Set(${JSON.stringify(webhookAgents.map((a) => a.name))});
888
+ // Set of webhook-accessible agent names. Named distinctly from the
889
+ // validateAgentRequest \`webhookAgents\` parameter to keep the call site
890
+ // below readable.
891
+ const webhookAgentSet = new Set(${JSON.stringify(webhookAgents.map((a) => a.name))});
831
892
 
832
893
  // When the CLI starts this server via \`flue run\`, it sets FLUE_MODE=local.
833
894
  // In local mode the HTTP route accepts any registered agent (including
@@ -841,13 +902,6 @@ const manifest = ${JSON.stringify({ agents: agents.map((a) => ({
841
902
  triggers: a.triggers
842
903
  })) }, null, 2)};
843
904
 
844
- // ─── Infrastructure ─────────────────────────────────────────────────────────
845
-
846
- // No build-time model default. The user sets model at runtime via
847
- // \`init({ model: "provider/model-id" })\` for an agent default, or via
848
- // \`{ model: "provider/model-id" }\` on any individual prompt/skill/task call.
849
- const model = undefined;
850
-
851
905
  // ─── Sandbox Environments ───────────────────────────────────────────────────
852
906
 
853
907
  /**
@@ -887,7 +941,7 @@ function createContextForRequest(id, payload) {
887
941
  payload,
888
942
  env: process.env,
889
943
  agentConfig: {
890
- systemPrompt, skills, roles, model, resolveModel,
944
+ systemPrompt, skills, roles, model: undefined, resolveModel,
891
945
  },
892
946
  createDefaultEnv,
893
947
  createLocalEnv,
@@ -902,31 +956,29 @@ const app = new Hono();
902
956
  app.get('/health', (c) => c.json({ status: 'ok' }));
903
957
  app.get('/agents', (c) => c.json(manifest));
904
958
 
905
- // Agent id is required in the URL
906
- app.post('/agents/:name', (c) => {
907
- return c.json({
908
- error: 'Agent id is required. Use /agents/:name/:id',
909
- }, 400);
910
- });
911
-
912
- app.post('/agents/:name/:id', async (c) => {
959
+ // Catch any method on the agent route so non-POSTs become 405 (instead of
960
+ // Hono's default 404 for unmatched method). Throws are translated by the
961
+ // onError handler into the canonical error envelope.
962
+ app.all('/agents/:name/:id', async (c) => {
913
963
  const name = c.req.param('name');
914
964
  const id = c.req.param('id');
915
965
 
916
- if (!handlers[name]) {
917
- return c.json({ error: 'Agent not found' }, 404);
918
- }
919
- if (!webhookAgents.has(name) && !isLocalMode) {
920
- return c.json({ error: 'Agent "' + name + '" is not web-accessible (no webhook trigger)' }, 404);
921
- }
966
+ // Validate method, name shape, registration, webhook-accessibility.
967
+ // Throws FlueHttpError on any failure; caught by app.onError below.
968
+ validateAgentRequest({
969
+ method: c.req.method,
970
+ name,
971
+ id,
972
+ registeredAgents: Object.keys(handlers),
973
+ webhookAgents: Array.from(webhookAgentSet),
974
+ allowNonWebhook: isLocalMode,
975
+ });
922
976
 
923
977
  const handler = handlers[name];
924
- let payload;
925
- try {
926
- payload = await c.req.json();
927
- } catch {
928
- payload = {};
929
- }
978
+
979
+ // Parse the request body. Throws on invalid Content-Type or malformed JSON;
980
+ // returns {} for genuinely empty bodies (so no-payload agents still work).
981
+ const payload = await parseJsonBody(c.req.raw);
930
982
 
931
983
  const accept = c.req.header('accept') || '';
932
984
  const isWebhook = c.req.header('x-webhook') === 'true';
@@ -949,7 +1001,13 @@ app.post('/agents/:name/:id', async (c) => {
949
1001
  return c.json({ status: 'accepted', requestId }, 202);
950
1002
  }
951
1003
 
952
- // SSE streaming mode
1004
+ // SSE streaming mode. Two error regimes meet here:
1005
+ // - Pre-stream errors (validation, body parsing, agent lookup) have
1006
+ // already thrown above and are rendered as plain HTTP responses by
1007
+ // app.onError — headers haven't been sent yet, so this works.
1008
+ // - Errors during agent execution surface as in-stream \`error\` events
1009
+ // with the canonical envelope (via toSseData), since by then the
1010
+ // 200 + text/event-stream headers are already on the wire.
953
1011
  if (isSSE) {
954
1012
  return streamSSE(c, async (stream) => {
955
1013
  let eventId = 0;
@@ -973,7 +1031,7 @@ app.post('/agents/:name/:id', async (c) => {
973
1031
  });
974
1032
  } catch (err) {
975
1033
  await stream.writeSSE({
976
- data: JSON.stringify({ type: 'error', error: String(err) }),
1034
+ data: toSseData(err),
977
1035
  event: 'error',
978
1036
  id: String(eventId++),
979
1037
  });
@@ -987,18 +1045,29 @@ app.post('/agents/:name/:id', async (c) => {
987
1045
  });
988
1046
  }
989
1047
 
990
- // Sync mode (default)
1048
+ // Sync mode (default). Errors propagate to app.onError.
1049
+ const ctx = createContextForRequest(id, payload);
991
1050
  try {
992
- const ctx = createContextForRequest(id, payload);
993
1051
  const result = await handler(ctx);
994
- ctx.setEventCallback(undefined);
995
1052
  return c.json({ result: result !== undefined ? result : null });
996
- } catch (err) {
997
- console.error('[flue] Agent error:', name, err);
998
- return c.json({ error: String(err) }, 500);
1053
+ } finally {
1054
+ ctx.setEventCallback(undefined);
999
1055
  }
1000
1056
  });
1001
1057
 
1058
+ // 404 handler — fires for any URL that didn't match a registered route.
1059
+ app.notFound((c) => {
1060
+ // Throw rather than return so the onError handler is the single source of
1061
+ // truth for error-envelope shaping.
1062
+ throw new RouteNotFoundError({ method: c.req.method, path: new URL(c.req.url).pathname });
1063
+ });
1064
+
1065
+ // Single-source-of-truth error renderer. Every thrown FlueError (and every
1066
+ // thrown unknown) is converted to the canonical JSON envelope here.
1067
+ // toHttpResponse takes care of logging unknowns — no extra console.error
1068
+ // needed at this layer.
1069
+ app.onError((err) => toHttpResponse(err));
1070
+
1002
1071
  // ─── Start ──────────────────────────────────────────────────────────────────
1003
1072
 
1004
1073
  const port = parseInt(process.env.PORT || '3000', 10);
@@ -1594,6 +1663,29 @@ var CloudflareReloader = class {
1594
1663
  port;
1595
1664
  configPath;
1596
1665
  envFiles;
1666
+ /**
1667
+ * Stable container build ID for the lifetime of this reloader instance.
1668
+ *
1669
+ * `unstable_startWorker` does NOT default this field — only wrangler's CLI
1670
+ * path does, via `generateContainerBuildId()`. When the user's wrangler
1671
+ * config declares `containers[]` (e.g. via `@cloudflare/sandbox`), the
1672
+ * first `onBundleComplete` calls `getImageNameFromDOClassName(...)` which
1673
+ * asserts that `options.containerBuildId` is set; without this, the
1674
+ * assertion throws, the `ProxyController` never gets `reloadComplete`,
1675
+ * and every request hangs (including `/health`). See issue #22.
1676
+ *
1677
+ * We generate it once per reloader and reuse it across reloads so that
1678
+ * wrangler's container-prep cache hits when nothing about the image
1679
+ * changed. Format matches wrangler's own helper: an 8-char UUID slice.
1680
+ */
1681
+ containerBuildId;
1682
+ /**
1683
+ * Bound listener for `DevEnv` `'error'` events. Stored so we can detach
1684
+ * it on `disposeWorker()` — the underlying `EventEmitter` outlives the
1685
+ * worker handle, so if the listener stayed attached we'd leak (and
1686
+ * double-fire) across reloads.
1687
+ */
1688
+ errorListener = null;
1597
1689
  url;
1598
1690
  constructor(wrangler, opts) {
1599
1691
  this.wrangler = wrangler;
@@ -1601,6 +1693,7 @@ var CloudflareReloader = class {
1601
1693
  this.port = opts.port;
1602
1694
  this.envFiles = opts.envFiles;
1603
1695
  this.configPath = path.join(this.outputDir, "dist", "wrangler.jsonc");
1696
+ this.containerBuildId = randomUUID().slice(0, 8);
1604
1697
  }
1605
1698
  async start() {
1606
1699
  await this.startWorker();
@@ -1659,9 +1752,18 @@ var CloudflareReloader = class {
1659
1752
  port: this.port
1660
1753
  },
1661
1754
  watch: false,
1662
- logLevel: "info"
1755
+ logLevel: "info",
1756
+ containerBuildId: this.containerBuildId
1663
1757
  }
1664
1758
  });
1759
+ this.errorListener = (event) => {
1760
+ const reason = event?.reason ?? "unknown error";
1761
+ const cause = event?.cause;
1762
+ const causeMsg = cause && typeof cause === "object" && "message" in cause ? cause.message : void 0;
1763
+ console.error(`[flue] Wrangler error (${event?.source ?? "unknown"}): ${reason}`);
1764
+ if (causeMsg) console.error(`[flue] ${causeMsg}`);
1765
+ };
1766
+ this.worker.raw.on("error", this.errorListener);
1665
1767
  try {
1666
1768
  this.url = (await this.worker.url).toString().replace(/\/$/, "");
1667
1769
  } catch {
@@ -1670,8 +1772,13 @@ var CloudflareReloader = class {
1670
1772
  }
1671
1773
  async disposeWorker() {
1672
1774
  const worker = this.worker;
1775
+ const listener = this.errorListener;
1673
1776
  this.worker = null;
1777
+ this.errorListener = null;
1674
1778
  if (!worker) return;
1779
+ if (listener) try {
1780
+ worker.raw.off("error", listener);
1781
+ } catch {}
1675
1782
  try {
1676
1783
  await worker.dispose();
1677
1784
  } catch (err) {