Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/local-explorer-agent-hint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"wrangler": patch
Comment thread
NuroDev marked this conversation as resolved.
Outdated
---

Print Local Explorer API details for headless agent-driven `wrangler dev` sessions

When `wrangler dev` is started in a headless AI agent environment, Wrangler now prints the Local Explorer API URL and basic resource routes so agents can inspect local Workers and bindings without relying on the interactive UI.
98 changes: 98 additions & 0 deletions packages/wrangler/src/__tests__/dev/start-dev.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import assert from "node:assert";
import { isInteractive } from "@cloudflare/workers-utils";
import { beforeEach, describe, it, vi } from "vitest";
import registerDevHotKeys from "../../dev/hotkeys";
import { startDev } from "../../dev/start-dev";
import { requireAuth } from "../../user";
import { detectAgent } from "../../utils/detect-agent";
import { mockConsoleMethods } from "../helpers/mock-console";
import type { StartDevWorkerInput } from "../../api";
import type { StartDevOptions } from "../../dev";

Expand Down Expand Up @@ -37,15 +40,22 @@ vi.mock("@cloudflare/workers-utils", async (importOriginal) => ({
openInBrowser: vi.fn(),
}));

vi.mock("../../utils/detect-agent", () => ({
detectAgent: vi.fn(() => ({ isAgent: false, id: null })),
}));

vi.mock("../../user", () => ({
requireApiToken: vi.fn(() => "test-api-token"),
requireAuth: vi.fn(async () => "test-account-id"),
}));

const std = mockConsoleMethods();

describe("startDev", () => {
beforeEach(() => {
vi.clearAllMocks();
mocks.configSet.mockResolvedValue(undefined);
mocks.fakeDevEnv.proxy.ready.promise = new Promise(() => {});
});

it("unregisters the latest hotkey registration after auth re-registers hotkeys", async ({
Expand Down Expand Up @@ -78,4 +88,92 @@ describe("startDev", () => {
expect(unregisterHotKeys[0]).toHaveBeenCalledOnce();
expect(unregisterHotKeys[1]).toHaveBeenCalledOnce();
});

it("prints the Local Explorer API hint for headless agent sessions", async ({
expect,
}) => {
const readyPromise = Promise.resolve({
url: new URL("http://127.0.0.1:8787"),
});
mocks.fakeDevEnv.proxy.ready.promise = readyPromise;
vi.mocked(isInteractive).mockReturnValue(false);
vi.mocked(detectAgent).mockReturnValue({ isAgent: true, id: "test-agent" });

await startDev({
disableDevRegistry: true,
showLocalExplorerAgentHint: true,
} as StartDevOptions);
await readyPromise;
await Promise.resolve();

expect(std.out).toContain(
Comment thread
NuroDev marked this conversation as resolved.
"Wrangler detected this dev session is running in an AI agent."
);
expect(std.out).toContain(
"The Local Explorer API is available at http://127.0.0.1:8787/cdn-cgi/explorer/api"
);
expect(std.out).toContain(
"GET http://127.0.0.1:8787/cdn-cgi/explorer/api/local/workers - local Workers and bindings"
);
});

it("does not print the Local Explorer API hint for interactive agent sessions", async ({
expect,
}) => {
const readyPromise = Promise.resolve({
url: new URL("http://127.0.0.1:8787"),
});
mocks.fakeDevEnv.proxy.ready.promise = readyPromise;
vi.mocked(isInteractive).mockReturnValue(true);
vi.mocked(detectAgent).mockReturnValue({ isAgent: true, id: "test-agent" });

await startDev({
disableDevRegistry: true,
showInteractiveDevSession: true,
showLocalExplorerAgentHint: true,
} as StartDevOptions);
await readyPromise;
await Promise.resolve();

expect(std.out).not.toContain("The Local Explorer API is available");
});

it("does not print the Local Explorer API hint for non-agent sessions", async ({
expect,
}) => {
const readyPromise = Promise.resolve({
url: new URL("http://127.0.0.1:8787"),
});
mocks.fakeDevEnv.proxy.ready.promise = readyPromise;
vi.mocked(isInteractive).mockReturnValue(false);
vi.mocked(detectAgent).mockReturnValue({ isAgent: false, id: null });

await startDev({
disableDevRegistry: true,
showLocalExplorerAgentHint: true,
} as StartDevOptions);
await readyPromise;
await Promise.resolve();

expect(std.out).not.toContain("The Local Explorer API is available");
});

it("does not print the Local Explorer API hint when the caller has not opted in", async ({
expect,
}) => {
const readyPromise = Promise.resolve({
url: new URL("http://127.0.0.1:8787"),
});
mocks.fakeDevEnv.proxy.ready.promise = readyPromise;
vi.mocked(isInteractive).mockReturnValue(false);
vi.mocked(detectAgent).mockReturnValue({ isAgent: true, id: "test-agent" });

await startDev({
disableDevRegistry: true,
} as StartDevOptions);
await readyPromise;
await Promise.resolve();

expect(std.out).not.toContain("The Local Explorer API is available");
});
});
6 changes: 5 additions & 1 deletion packages/wrangler/src/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,10 @@ export const dev = createCommand({
}
},
async handler(args) {
const devInstance = await startDev(args);
const devInstance = await startDev({
...args,
showLocalExplorerAgentHint: true,
Comment thread
NuroDev marked this conversation as resolved.
Outdated
});
assert(devInstance.devEnv !== undefined);
await events.once(devInstance.devEnv, "teardown");
await Promise.all(devInstance.secondary.map((d) => d.teardown()));
Expand Down Expand Up @@ -358,6 +361,7 @@ export type AdditionalDevProps = {
moduleRoot?: string;
rules?: Rule[];
showInteractiveDevSession?: boolean;
showLocalExplorerAgentHint?: boolean;
};

type DevArguments = Omit<(typeof dev)["args"], "installSkills" | "profile">;
Expand Down
44 changes: 41 additions & 3 deletions packages/wrangler/src/dev/start-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ import assert from "node:assert";
import path from "node:path";
import { bold, green } from "@cloudflare/cli-shared-helpers/colors";
import { generateContainerBuildId } from "@cloudflare/containers-shared";
import { getRegistryPath } from "@cloudflare/workers-utils";
import { isInteractive } from "@cloudflare/workers-utils";
import {
getLocalExplorerEnabledFromEnv,
getRegistryPath,
isInteractive,
} from "@cloudflare/workers-utils";
import { CorePaths } from "miniflare";
import dedent from "ts-dedent";
import { DevEnv } from "../api";
import { convertStartDevOptionsToBindings } from "../api/startDevWorker/binding-utils";
Expand All @@ -19,6 +23,7 @@ import {
collectKeyValues,
collectPlainTextVars,
} from "../utils/collectKeyValues";
import { detectAgent } from "../utils/detect-agent";
import type { AsyncHook, StartDevWorkerInput, Trigger } from "../api";
import type { StartDevOptionsBindings } from "../api/startDevWorker/binding-utils";
import type { StartDevOptions } from "../dev";
Expand Down Expand Up @@ -123,7 +128,10 @@ export async function startDev(args: StartDevOptions) {
tunnelManager?.getTunnel()?.dispose();
});

if (isInteractive() && args.showInteractiveDevSession !== false) {
const interactiveDevSession =
isInteractive() && args.showInteractiveDevSession !== false;

if (interactiveDevSession) {
unregisterHotKeys = registerDevHotKeys(devEnvs, args, { tunnelManager });
}

Expand Down Expand Up @@ -155,6 +163,7 @@ export async function startDev(args: StartDevOptions) {
false
);
maybePrintScheduledWorkerWarning(hasCrons, !!args.testScheduled, url);
maybePrintLocalExplorerAgentHint(args, url, !interactiveDevSession);
});

// Start tunnel early, before the proxy is ready.
Expand Down Expand Up @@ -361,6 +370,35 @@ function maybePrintScheduledWorkerWarning(
);
}

function maybePrintLocalExplorerAgentHint(
args: StartDevOptions,
url: URL,
isHeadless: boolean
): void {
if (
!args.showLocalExplorerAgentHint ||
!isHeadless ||
args.remote ||
!getLocalExplorerEnabledFromEnv() ||
!detectAgent().isAgent
) {
return;
}
Comment thread
NuroDev marked this conversation as resolved.
Outdated

const explorerApiUrl = new URL(`${CorePaths.EXPLORER}/api`, url).href;
logger.once.log(dedent`
Wrangler detected this dev session is running in an AI agent.
The Local Explorer API is available at ${explorerApiUrl}
Useful routes:
GET ${explorerApiUrl} - OpenAPI schema
GET ${explorerApiUrl}/d1/database - D1 databases
GET ${explorerApiUrl}/local/workers - local Workers and bindings
GET ${explorerApiUrl}/r2/buckets - R2 buckets
GET ${explorerApiUrl}/storage/kv/namespaces - KV namespaces
GET ${explorerApiUrl}/workers/durable_objects/namespaces - Durable Object namespaces
GET ${explorerApiUrl}/workflows - Workflows`);
}

export function formatHostname(hostname: string): string {
if (hostname === "0.0.0.0" || hostname === "::" || hostname === "*") {
return "localhost";
Expand Down
Loading