diff --git a/packages/cli/src/__tests__/chat.integration.test.ts b/packages/cli/src/__tests__/chat.integration.test.ts index 278a07f3e..cec9317aa 100644 --- a/packages/cli/src/__tests__/chat.integration.test.ts +++ b/packages/cli/src/__tests__/chat.integration.test.ts @@ -99,7 +99,7 @@ describe("chatCommand example integration", () => { const url = String(input); if ( - url === "http://gateway.test/api/v1/agents" && + url === "http://gateway.test/lobu/api/v1/agents" && init?.method === "POST" ) { const body = JSON.parse(String(init.body)) as Record; @@ -111,7 +111,7 @@ describe("chatCommand example integration", () => { } if ( - url === "http://gateway.test/api/v1/agents/session-1/events" && + url === "http://gateway.test/lobu/api/v1/agents/session-1/events" && !init?.method ) { return createSseResponse([ @@ -156,14 +156,14 @@ describe("chatCommand example integration", () => { } if ( - url === "http://gateway.test/api/v1/agents/session-1/messages" && + url === "http://gateway.test/lobu/api/v1/agents/session-1/messages" && init?.method === "POST" ) { return Response.json({ success: true }); } if ( - url === "http://gateway.test/api/v1/agents/approve" && + url === "http://gateway.test/lobu/api/v1/agents/approve" && init?.method === "POST" ) { const body = JSON.parse(String(init.body)) as Record; @@ -224,7 +224,8 @@ describe("chatCommand example integration", () => { const url = String(input); if ( - url === "http://gateway.test/api/v1/agents/vc-tracking/messages" && + url === + "http://gateway.test/lobu/api/v1/agents/vc-tracking/messages" && init?.method === "POST" ) { return Response.json({ @@ -235,7 +236,7 @@ describe("chatCommand example integration", () => { if ( url === - "http://gateway.test/api/v1/agents/vc-tracking/events?platform=telegram" && + "http://gateway.test/lobu/api/v1/agents/vc-tracking/events?platform=telegram" && !init?.method ) { return createSseResponse([ @@ -288,7 +289,8 @@ describe("chatCommand example integration", () => { const url = String(input); if ( - url === "http://gateway.test/api/v1/agents/vc-tracking/messages" && + url === + "http://gateway.test/lobu/api/v1/agents/vc-tracking/messages" && init?.method === "POST" ) { return Response.json({ @@ -299,7 +301,7 @@ describe("chatCommand example integration", () => { if ( url === - "http://gateway.test/api/v1/agents/vc-tracking/events?platform=telegram" && + "http://gateway.test/lobu/api/v1/agents/vc-tracking/events?platform=telegram" && !init?.method ) { return createSseResponse([ @@ -340,7 +342,8 @@ describe("chatCommand example integration", () => { const url = String(input); if ( - url === "http://gateway.test/api/v1/agents/vc-tracking/messages" && + url === + "http://gateway.test/lobu/api/v1/agents/vc-tracking/messages" && init?.method === "POST" ) { return Response.json({ @@ -351,7 +354,7 @@ describe("chatCommand example integration", () => { if ( url === - "http://gateway.test/api/v1/agents/vc-tracking/events?platform=telegram" && + "http://gateway.test/lobu/api/v1/agents/vc-tracking/events?platform=telegram" && !init?.method ) { return createSseResponse([ @@ -388,7 +391,8 @@ describe("chatCommand example integration", () => { const url = String(input); if ( - url === "http://gateway.test/api/v1/agents/vc-tracking/messages" && + url === + "http://gateway.test/lobu/api/v1/agents/vc-tracking/messages" && init?.method === "POST" ) { const body = JSON.parse(String(init.body)) as Record; @@ -401,7 +405,7 @@ describe("chatCommand example integration", () => { if ( url === - "http://gateway.test/api/v1/agents/vc-tracking/events?platform=telegram" && + "http://gateway.test/lobu/api/v1/agents/vc-tracking/events?platform=telegram" && !init?.method ) { return createSseResponse([ diff --git a/packages/cli/src/commands/chat.ts b/packages/cli/src/commands/chat.ts index d1ea9dc85..a48ab811d 100644 --- a/packages/cli/src/commands/chat.ts +++ b/packages/cli/src/commands/chat.ts @@ -3,6 +3,7 @@ import { join } from "node:path"; import { createInterface } from "node:readline"; import chalk from "chalk"; import { + agentApiBase, apiBaseFromContextUrl, getCurrentContextName, getToken, @@ -80,7 +81,9 @@ export async function chatCommand( } else { gatewayUrl = await resolveGatewayUrl({ cwd }); } - gatewayUrl = gatewayUrl.replace(/\/$/, ""); + // The Agent API lives under `/lobu` on every Lobu deployment; the + // context apiUrl and `.env` PORT only give the origin. + gatewayUrl = agentApiBase(gatewayUrl); const authToken = await getToken(options.context); if (!authToken) { diff --git a/packages/cli/src/commands/eval.ts b/packages/cli/src/commands/eval.ts index 0405289d1..e5c9c6442 100644 --- a/packages/cli/src/commands/eval.ts +++ b/packages/cli/src/commands/eval.ts @@ -9,6 +9,7 @@ import { basename, join } from "node:path"; import chalk from "chalk"; import { parse as parseYaml } from "yaml"; import { + agentApiBase, apiBaseFromContextUrl, getToken, resolveContext, @@ -119,12 +120,12 @@ export async function evalCommand( } // Auth and gateway required from here (not needed for --list) - const gatewayUrl = ( + const gatewayUrl = agentApiBase( options.gateway ?? - (options.context - ? apiBaseFromContextUrl((await resolveContext(options.context)).apiUrl) - : await resolveGatewayUrl({ cwd })) - ).replace(/\/$/, ""); + (options.context + ? apiBaseFromContextUrl((await resolveContext(options.context)).apiUrl) + : await resolveGatewayUrl({ cwd })) + ); const authToken = await getToken(options.context); if (!authToken) { diff --git a/packages/cli/src/internal/gateway-url.ts b/packages/cli/src/internal/gateway-url.ts index 49aa84535..c7552fb05 100644 --- a/packages/cli/src/internal/gateway-url.ts +++ b/packages/cli/src/internal/gateway-url.ts @@ -4,6 +4,27 @@ import { parseEnvContent } from "./env-file.js"; export const GATEWAY_DEFAULT_URL = "http://localhost:8787"; +/** + * The embedded Lobu server mounts its public Agent API (`/api/v1/agents/*`, + * `/api/docs`) under this path prefix — see `packages/server/src/server.ts`'s + * `app.route('/lobu', ...)`. Every deployment (local `lobu run`, app.lobu.ai, + * community.lobu.ai) follows this layout: the org-scoped admin REST API and + * OAuth live at the origin, the Agent API lives at `/lobu`. + */ +export const GATEWAY_AGENT_API_PREFIX = "/lobu"; + +/** + * Normalize a gateway URL (from `--gateway`, a context's apiUrl, or `.env`) to + * the base the Agent API is served from: `/lobu`. Idempotent — passing + * a URL that already ends in `/lobu` returns it unchanged. + */ +export function agentApiBase(gatewayUrl: string): string { + const trimmed = gatewayUrl.replace(/\/+$/, ""); + return trimmed.endsWith(GATEWAY_AGENT_API_PREFIX) + ? trimmed + : trimmed + GATEWAY_AGENT_API_PREFIX; +} + interface ResolveGatewayUrlOptions { cwd?: string; } diff --git a/packages/cli/src/internal/index.ts b/packages/cli/src/internal/index.ts index 3cee5ffec..c47fa3350 100644 --- a/packages/cli/src/internal/index.ts +++ b/packages/cli/src/internal/index.ts @@ -25,4 +25,9 @@ export { listOrganizations, resolveApiClient, } from "./api-client.js"; -export { GATEWAY_DEFAULT_URL, resolveGatewayUrl } from "./gateway-url.js"; +export { + agentApiBase, + GATEWAY_AGENT_API_PREFIX, + GATEWAY_DEFAULT_URL, + resolveGatewayUrl, +} from "./gateway-url.js"; diff --git a/packages/cli/src/templates/TESTING.md.tmpl b/packages/cli/src/templates/TESTING.md.tmpl index 3dcd5583b..544a447df 100644 --- a/packages/cli/src/templates/TESTING.md.tmpl +++ b/packages/cli/src/templates/TESTING.md.tmpl @@ -8,7 +8,7 @@ Send messages to your bot with optional file uploads. ### Endpoint ``` -POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages +POST http://localhost:{{GATEWAY_PORT}}/lobu/api/v1/agents/{agentId}/messages ``` ### Authentication @@ -24,7 +24,7 @@ The bot token must be provided in the `Authorization` header, not in the request #### JSON Request (Simple Message) ```bash -curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages \ +curl -X POST http://localhost:{{GATEWAY_PORT}}/lobu/api/v1/agents/{agentId}/messages \ -H "Authorization: Bearer xoxb-your-bot-token" \ -H "Content-Type: application/json" \ -d '{ @@ -36,7 +36,7 @@ curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages #### Multipart Request (With File Upload) ```bash -curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages \ +curl -X POST http://localhost:{{GATEWAY_PORT}}/lobu/api/v1/agents/{agentId}/messages \ -H "Authorization: Bearer xoxb-your-bot-token" \ -F "platform=slack" \ -F "channel=C12345678" \ @@ -90,7 +90,7 @@ If you don't want to mention the bot, simply omit `@me` from your message. ### Example: Simple Text Message (with @me) ```bash -curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages \ +curl -X POST http://localhost:{{GATEWAY_PORT}}/lobu/api/v1/agents/{agentId}/messages \ -H "Authorization: Bearer xoxb-your-bot-token" \ -H "Content-Type: application/json" \ -d '{ @@ -103,7 +103,7 @@ curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages ### Example: Without Bot Mention ```bash -curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages \ +curl -X POST http://localhost:{{GATEWAY_PORT}}/lobu/api/v1/agents/{agentId}/messages \ -H "Authorization: Bearer xoxb-your-bot-token" \ -H "Content-Type: application/json" \ -d '{ @@ -116,7 +116,7 @@ curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages ### Example: Thread Reply ```bash -curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages \ +curl -X POST http://localhost:{{GATEWAY_PORT}}/lobu/api/v1/agents/{agentId}/messages \ -H "Authorization: Bearer xoxb-your-bot-token" \ -H "Content-Type: application/json" \ -d '{ @@ -130,7 +130,7 @@ curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages ### Example: Single File Upload ```bash -curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages \ +curl -X POST http://localhost:{{GATEWAY_PORT}}/lobu/api/v1/agents/{agentId}/messages \ -H "Authorization: Bearer xoxb-your-bot-token" \ -F "platform=slack" \ -F "channel=dev-channel" \ @@ -141,7 +141,7 @@ curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages ### Example: Multiple File Upload ```bash -curl -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages \ +curl -X POST http://localhost:{{GATEWAY_PORT}}/lobu/api/v1/agents/{agentId}/messages \ -H "Authorization: Bearer xoxb-your-bot-token" \ -F "platform=slack" \ -F "channel=dev-channel" \ @@ -196,7 +196,7 @@ Testing a full conversation: ```bash # Step 1: Send initial message -RESPONSE=$(curl -s -X POST http://localhost:{{GATEWAY_PORT}}/api/v1/agents/{agentId}/messages \ +RESPONSE=$(curl -s -X POST http://localhost:{{GATEWAY_PORT}}/lobu/api/v1/agents/{agentId}/messages \ -H "Authorization: Bearer $SLACK_BOT_TOKEN" \ -H "Content-Type: application/json" \ -d '{