diff --git a/packages/cli-workflow/__tests__/serve.test.ts b/packages/cli-workflow/__tests__/connect.test.ts similarity index 96% rename from packages/cli-workflow/__tests__/serve.test.ts rename to packages/cli-workflow/__tests__/connect.test.ts index 3910a8a..3559051 100644 --- a/packages/cli-workflow/__tests__/serve.test.ts +++ b/packages/cli-workflow/__tests__/connect.test.ts @@ -2,14 +2,14 @@ import { describe, expect, test } from "bun:test"; import { createContentMerkleNode, serializeMerkleNode } from "@uncaged/workflow-cas"; -import { createApp } from "../src/commands/serve/app.js"; +import { createApp } from "../src/commands/connect/app.js"; function casStoredForm(raw: string): string { return serializeMerkleNode(createContentMerkleNode(raw)); } function buildApp(storageRoot: string) { - const app = createApp(storageRoot); + const app = createApp(storageRoot, null); return { fetch: (path: string, init?: RequestInit) => app.fetch(new Request(`http://localhost${path}`, init)), @@ -115,7 +115,7 @@ describe("serve error handling", () => { }); test("global error handler returns 500 with JSON", async () => { - const app = createApp("/tmp/uncaged-serve-test-nonexistent"); + const app = createApp("/tmp/uncaged-serve-test-nonexistent", null); app.get("/test-error", () => { throw new Error("boom"); }); @@ -128,7 +128,7 @@ describe("serve error handling", () => { describe("serve security", () => { test("CORS headers present on responses", async () => { - const app = createApp("/tmp/uncaged-serve-test-nonexistent"); + const app = createApp("/tmp/uncaged-serve-test-nonexistent", null); const res2 = await app.fetch( new Request("http://localhost/healthz", { headers: { Origin: "http://localhost:5173" }, diff --git a/packages/cli-workflow/src/cli-dispatch.ts b/packages/cli-workflow/src/cli-dispatch.ts index a7974d4..2a420c3 100644 --- a/packages/cli-workflow/src/cli-dispatch.ts +++ b/packages/cli-workflow/src/cli-dispatch.ts @@ -4,7 +4,7 @@ import { getCommandRegistry } from "./cli-registry.js"; import { formatCliUsage as formatCliUsageWithGroups } from "./cli-usage.js"; import { createCasDispatcher } from "./commands/cas/index.js"; import { createInitDispatcher } from "./commands/init/index.js"; -import { dispatchServe } from "./commands/serve/index.js"; +import { dispatchConnect } from "./commands/connect/index.js"; import { dispatchSetup } from "./commands/setup/index.js"; import { createThreadDispatcher, dispatchLive, dispatchRun } from "./commands/thread/index.js"; import { createWorkflowDispatcher } from "./commands/workflow/index.js"; @@ -71,7 +71,7 @@ const COMMAND_TABLE: Record = { skill: dispatchSkill, run: dispatchRun, live: dispatchLive, - serve: dispatchServe, + connect: dispatchConnect, }; export async function runCli(storageRoot: string, argv: string[]): Promise { diff --git a/packages/cli-workflow/src/cli-usage.ts b/packages/cli-workflow/src/cli-usage.ts index 0a3c05f..2bbb2d5 100644 --- a/packages/cli-workflow/src/cli-usage.ts +++ b/packages/cli-workflow/src/cli-usage.ts @@ -59,12 +59,12 @@ export function formatCliUsage( ); lines.push(""); - lines.push("Server:"); + lines.push("Gateway:"); lines.push( ...formatUsageCommandLines([ { - prefix: "serve [--port N] [--host ADDR]", - description: "Start HTTP API server (default: 127.0.0.1:7860)", + prefix: "connect [--name NAME] [--gateway URL]", + description: "Connect to workflow gateway via WebSocket", }, ]), ); diff --git a/packages/cli-workflow/src/commands/serve/app.ts b/packages/cli-workflow/src/commands/connect/app.ts similarity index 81% rename from packages/cli-workflow/src/commands/serve/app.ts rename to packages/cli-workflow/src/commands/connect/app.ts index 4b07306..6bfb434 100644 --- a/packages/cli-workflow/src/commands/serve/app.ts +++ b/packages/cli-workflow/src/commands/connect/app.ts @@ -8,7 +8,7 @@ import { createWorkflowRoutes } from "./routes-workflow.js"; const MAX_BODY_SIZE = 1_048_576; // 1 MB -export function createApp(storageRoot: string, agentToken: string | null): Hono { +export function createApp(storageRoot: string, clientToken: string | null): Hono { const app = new Hono(); app.onError((_err, c) => { @@ -37,11 +37,11 @@ export function createApp(storageRoot: string, agentToken: string | null): Hono await next(); }); - // ── Agent token auth (skip healthz) ─────────────────────────────── - if (agentToken !== null) { + // ── Client token auth (skip healthz) ─────────────────────────────── + if (clientToken !== null) { app.use("/api/*", async (c, next) => { - const token = c.req.header("X-Agent-Token"); - if (token !== agentToken) { + const token = c.req.header("X-Client-Token"); + if (token !== clientToken) { return c.json({ error: "unauthorized" }, 401); } await next(); diff --git a/packages/cli-workflow/src/commands/serve/serve.ts b/packages/cli-workflow/src/commands/connect/connect.ts similarity index 55% rename from packages/cli-workflow/src/commands/serve/serve.ts rename to packages/cli-workflow/src/commands/connect/connect.ts index e7d9bb4..4e72922 100644 --- a/packages/cli-workflow/src/commands/serve/serve.ts +++ b/packages/cli-workflow/src/commands/connect/connect.ts @@ -1,62 +1,30 @@ import { randomUUID } from "node:crypto"; import { hostname as osHostname } from "node:os"; -import { err, ok, type Result } from "@uncaged/workflow-protocol"; +import { ok, type Result } from "@uncaged/workflow-protocol"; import { createLogger } from "@uncaged/workflow-util"; -import { serve } from "bun"; import { printCliLine } from "../../cli-output.js"; import { createApp } from "./app.js"; import { registerWithGateway, startHeartbeat, unregisterFromGateway } from "./gateway.js"; -import type { ServeOptions } from "./types.js"; +import type { ConnectOptions } from "./types.js"; import { startGatewayWsClient } from "./ws-client.js"; const DEFAULT_GATEWAY_URL = "https://workflow-gateway.shazhou.workers.dev"; const HEARTBEAT_INTERVAL_MS = 60_000; -export function startServer( - storageRoot: string, - options: ServeOptions, -): void { - const app = createApp(storageRoot, null); - - const server = serve({ - fetch: app.fetch, - port: options.port, - hostname: options.hostname, - }); - - printCliLine(`uncaged-workflow API server listening on http://${server.hostname}:${server.port}`); -} - -function parsePortValue(value: string | undefined): Result { - if (value === undefined) { - return err("--port requires a value"); - } - const parsed = Number.parseInt(value, 10); - if (!Number.isFinite(parsed) || parsed < 0 || parsed > 65535) { - return err(`invalid port: ${value}`); - } - return ok(parsed); -} - function requireNextArg(argv: string[], i: number, flag: string): Result { const next = argv[i + 1]; if (next === undefined) { - return err(`${flag} requires a value`); + return { ok: false, error: `${flag} requires a value` }; } return ok(next); } -function parseServeArgv(argv: string[]): Result { - let port = 7860; - let hostname = "127.0.0.1"; +function parseConnectArgv(argv: string[]): Result { let name = osHostname().split(".")[0].toLowerCase(); let gatewayUrl = DEFAULT_GATEWAY_URL; const gatewaySecret = process.env.WORKFLOW_GATEWAY_SECRET ?? ""; const stringFlags: Record void> = { - "--host": (v) => { - hostname = v; - }, "--name": (v) => { name = v; }, @@ -67,12 +35,7 @@ function parseServeArgv(argv: string[]): Result { for (let i = 0; i < argv.length; i++) { const arg = argv[i]; - if (arg === "--port" || arg === "-p") { - const portResult = parsePortValue(argv[i + 1]); - if (!portResult.ok) return portResult; - port = portResult.value; - i++; - } else if (arg in stringFlags) { + if (arg in stringFlags) { const r = requireNextArg(argv, i, arg); if (!r.ok) return r; stringFlags[arg](r.value); @@ -80,11 +43,11 @@ function parseServeArgv(argv: string[]): Result { } } - return ok({ port, hostname, name, gatewayUrl, gatewaySecret }); + return ok({ name, gatewayUrl, gatewaySecret }); } -export async function dispatchServe(storageRoot: string, argv: string[]): Promise { - const parsed = parseServeArgv(argv); +export async function dispatchConnect(storageRoot: string, argv: string[]): Promise { + const parsed = parseConnectArgv(argv); if (!parsed.ok) { printCliLine(`error: ${parsed.error}`); return 1; @@ -93,16 +56,12 @@ export async function dispatchServe(storageRoot: string, argv: string[]): Promis const options = parsed.value; if (options.gatewaySecret === "") { - // No gateway — local-only mode - startServer(storageRoot, options); - printCliLine("no WORKFLOW_GATEWAY_SECRET — running in local-only mode"); - await new Promise(() => {}); - return 0; + printCliLine("error: WORKFLOW_GATEWAY_SECRET is required"); + return 1; } - // Gateway mode — no HTTP server, WS client calls app.fetch directly - const agentToken = randomUUID(); - const app = createApp(storageRoot, agentToken); + const clientToken = randomUUID(); + const app = createApp(storageRoot, clientToken); const log = createLogger({ sink: { kind: "stderr" } }); const stopWsClient = startGatewayWsClient({ @@ -113,7 +72,7 @@ export async function dispatchServe(storageRoot: string, argv: string[]): Promis log, }); - printCliLine("connected to gateway via WebSocket (no local HTTP server)"); + printCliLine("connected to gateway via WebSocket"); // Register with gateway for discovery const registered = await registerWithGateway( @@ -121,7 +80,7 @@ export async function dispatchServe(storageRoot: string, argv: string[]): Promis options.name, `ws://${options.name}`, options.gatewaySecret, - agentToken, + clientToken, ); if (registered) { printCliLine(`registered with gateway as "${options.name}"`); @@ -132,7 +91,7 @@ export async function dispatchServe(storageRoot: string, argv: string[]): Promis options.name, `ws://${options.name}`, options.gatewaySecret, - agentToken, + clientToken, HEARTBEAT_INTERVAL_MS, ); diff --git a/packages/cli-workflow/src/commands/serve/gateway.ts b/packages/cli-workflow/src/commands/connect/gateway.ts similarity index 84% rename from packages/cli-workflow/src/commands/serve/gateway.ts rename to packages/cli-workflow/src/commands/connect/gateway.ts index f3a0912..3d8b34b 100644 --- a/packages/cli-workflow/src/commands/serve/gateway.ts +++ b/packages/cli-workflow/src/commands/connect/gateway.ts @@ -5,13 +5,13 @@ export async function registerWithGateway( name: string, localUrl: string, secret: string, - agentToken: string, + clientToken: string, ): Promise { try { const resp = await fetch(`${gatewayUrl}/api/gateway/register`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name, url: localUrl, secret, agentToken }), + body: JSON.stringify({ name, url: localUrl, secret, clientToken }), }); if (!resp.ok) { const body = await resp.text(); @@ -45,10 +45,10 @@ export function startHeartbeat( name: string, localUrl: string, secret: string, - agentToken: string, + clientToken: string, intervalMs: number, ): ReturnType { return setInterval(() => { - registerWithGateway(gatewayUrl, name, localUrl, secret, agentToken).catch(() => {}); + registerWithGateway(gatewayUrl, name, localUrl, secret, clientToken).catch(() => {}); }, intervalMs); } diff --git a/packages/cli-workflow/src/commands/connect/index.ts b/packages/cli-workflow/src/commands/connect/index.ts new file mode 100644 index 0000000..0b00564 --- /dev/null +++ b/packages/cli-workflow/src/commands/connect/index.ts @@ -0,0 +1,2 @@ +export { dispatchConnect } from "./connect.js"; +export type { ConnectOptions } from "./types.js"; diff --git a/packages/cli-workflow/src/commands/serve/routes-cas.ts b/packages/cli-workflow/src/commands/connect/routes-cas.ts similarity index 100% rename from packages/cli-workflow/src/commands/serve/routes-cas.ts rename to packages/cli-workflow/src/commands/connect/routes-cas.ts diff --git a/packages/cli-workflow/src/commands/serve/routes-live.ts b/packages/cli-workflow/src/commands/connect/routes-live.ts similarity index 100% rename from packages/cli-workflow/src/commands/serve/routes-live.ts rename to packages/cli-workflow/src/commands/connect/routes-live.ts diff --git a/packages/cli-workflow/src/commands/serve/routes-thread.ts b/packages/cli-workflow/src/commands/connect/routes-thread.ts similarity index 100% rename from packages/cli-workflow/src/commands/serve/routes-thread.ts rename to packages/cli-workflow/src/commands/connect/routes-thread.ts diff --git a/packages/cli-workflow/src/commands/serve/routes-workflow.ts b/packages/cli-workflow/src/commands/connect/routes-workflow.ts similarity index 100% rename from packages/cli-workflow/src/commands/serve/routes-workflow.ts rename to packages/cli-workflow/src/commands/connect/routes-workflow.ts diff --git a/packages/cli-workflow/src/commands/serve/types.ts b/packages/cli-workflow/src/commands/connect/types.ts similarity index 50% rename from packages/cli-workflow/src/commands/serve/types.ts rename to packages/cli-workflow/src/commands/connect/types.ts index 56b5de3..b5bc4dc 100644 --- a/packages/cli-workflow/src/commands/serve/types.ts +++ b/packages/cli-workflow/src/commands/connect/types.ts @@ -1,6 +1,4 @@ -export type ServeOptions = { - port: number; - hostname: string; +export type ConnectOptions = { name: string; gatewayUrl: string; gatewaySecret: string; diff --git a/packages/cli-workflow/src/commands/serve/ws-client.ts b/packages/cli-workflow/src/commands/connect/ws-client.ts similarity index 100% rename from packages/cli-workflow/src/commands/serve/ws-client.ts rename to packages/cli-workflow/src/commands/connect/ws-client.ts diff --git a/packages/cli-workflow/src/commands/serve/index.ts b/packages/cli-workflow/src/commands/serve/index.ts deleted file mode 100644 index ce37a75..0000000 --- a/packages/cli-workflow/src/commands/serve/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { createApp } from "./app.js"; -export { dispatchServe, startServer } from "./serve.js"; -export type { ServeOptions } from "./types.js"; diff --git a/packages/cli-workflow/src/skill.ts b/packages/cli-workflow/src/skill.ts index f6842ed..06ea030 100644 --- a/packages/cli-workflow/src/skill.ts +++ b/packages/cli-workflow/src/skill.ts @@ -86,11 +86,11 @@ ${commandSections.join("\n\n")} | \`run\` | \`thread run\` | Shortcut to start a thread | | \`live\` | \`thread live\` | Shortcut to attach to a thread | -### serve +### connect | Command | Args | Description | |---------|------|-------------| -| \`serve\` | \`[--port N] [--host ADDR] [--name NAME]\` | Start HTTP API server with WebSocket gateway connection. \`--name\` registers with the gateway. | +| \`connect\` | \`[--name NAME] [--gateway URL]\` | Connect to workflow gateway via WebSocket. \`--name\` registers with the gateway. | ## Typical Workflow diff --git a/packages/workflow-dashboard/src/api.ts b/packages/workflow-dashboard/src/api.ts index ab910b4..c82ea67 100644 --- a/packages/workflow-dashboard/src/api.ts +++ b/packages/workflow-dashboard/src/api.ts @@ -26,11 +26,11 @@ function authHeaders(): Record { return {}; } -function agentBase(agent: string): string { +function clientBase(client: string): string { if (GATEWAY_URL) { - return `${GATEWAY_URL}/api/agents/${agent}`; + return `${GATEWAY_URL}/api/clients/${client}`; } - // Local dev: proxy via vite, no agent prefix + // Local dev: proxy via vite, no client prefix return "/api"; } @@ -57,7 +57,7 @@ async function fetchJson(base: string, path: string): Promise { // ── Endpoint types ────────────────────────────────────────────────── -export type AgentEndpoint = { +export type ClientEndpoint = { name: string; url: string; status: string; @@ -141,61 +141,61 @@ export type WorkflowDetail = { // ── Gateway endpoints ─────────────────────────────────────────────── -export function listAgents(): Promise { +export function listClients(): Promise { const url = GATEWAY_URL || ""; return fetchJson(url, "/api/gateway/endpoints"); } -// ── Agent-scoped endpoints ────────────────────────────────────────── +// ── Client-scoped endpoints ────────────────────────────────────────── -export function listWorkflows(agent: string): Promise<{ workflows: WorkflowSummary[] }> { - return fetchJson(agentBase(agent), "/workflows"); +export function listWorkflows(client: string): Promise<{ workflows: WorkflowSummary[] }> { + return fetchJson(clientBase(client), "/workflows"); } -export async function getWorkflowDetail(agent: string, name: string): Promise { - return fetchJson(agentBase(agent), `/workflows/${encodeURIComponent(name)}`); +export async function getWorkflowDetail(client: string, name: string): Promise { + return fetchJson(clientBase(client), `/workflows/${encodeURIComponent(name)}`); } export async function getWorkflowDescriptor( - agent: string, + client: string, name: string, ): Promise { - const res = await getWorkflowDetail(agent, name); + const res = await getWorkflowDetail(client, name); return res.descriptor; } -export function listThreads(agent: string): Promise<{ threads: ThreadSummary[] }> { - return fetchJson(agentBase(agent), "/threads"); +export function listThreads(client: string): Promise<{ threads: ThreadSummary[] }> { + return fetchJson(clientBase(client), "/threads"); } -export function listRunningThreads(agent: string): Promise<{ threads: ThreadSummary[] }> { - return fetchJson(agentBase(agent), "/threads/running"); +export function listRunningThreads(client: string): Promise<{ threads: ThreadSummary[] }> { + return fetchJson(clientBase(client), "/threads/running"); } -export function getThread(agent: string, id: string): Promise<{ records: ThreadRecord[] }> { - return fetchJson(agentBase(agent), `/threads/${id}`); +export function getThread(client: string, id: string): Promise<{ records: ThreadRecord[] }> { + return fetchJson(clientBase(client), `/threads/${id}`); } export function runThread( - agent: string, + client: string, workflow: string, prompt: string, ): Promise<{ threadId: string }> { - return postJson(agentBase(agent), "/threads", { workflow, prompt }); + return postJson(clientBase(client), "/threads", { workflow, prompt }); } -export function killThread(agent: string, threadId: string): Promise<{ ok: boolean }> { - return postJson(agentBase(agent), `/threads/${threadId}/kill`, {}); +export function killThread(client: string, threadId: string): Promise<{ ok: boolean }> { + return postJson(clientBase(client), `/threads/${threadId}/kill`, {}); } -export function pauseThread(agent: string, threadId: string): Promise<{ ok: boolean }> { - return postJson(agentBase(agent), `/threads/${threadId}/pause`, {}); +export function pauseThread(client: string, threadId: string): Promise<{ ok: boolean }> { + return postJson(clientBase(client), `/threads/${threadId}/pause`, {}); } -export function resumeThread(agent: string, threadId: string): Promise<{ ok: boolean }> { - return postJson(agentBase(agent), `/threads/${threadId}/resume`, {}); +export function resumeThread(client: string, threadId: string): Promise<{ ok: boolean }> { + return postJson(clientBase(client), `/threads/${threadId}/resume`, {}); } -export function getAgentHealth(agent: string): Promise<{ ok: boolean }> { - return fetchJson(agentBase(agent), "/healthz"); +export function getClientHealth(client: string): Promise<{ ok: boolean }> { + return fetchJson(clientBase(client), "/healthz"); } diff --git a/packages/workflow-dashboard/src/app.tsx b/packages/workflow-dashboard/src/app.tsx index 846da5f..c45ba72 100644 --- a/packages/workflow-dashboard/src/app.tsx +++ b/packages/workflow-dashboard/src/app.tsx @@ -11,7 +11,7 @@ import { useHashRoute } from "./use-hash-route.ts"; export function App() { const [authed, setAuthed] = useState(hasApiKey()); - const { view, agent, threadId, setView, setAgent, setThreadId } = useHashRoute(); + const { view, client, threadId, setView, setClient, setThreadId } = useHashRoute(); const [showRun, setShowRun] = useState(false); if (!authed) { @@ -22,36 +22,36 @@ export function App() {
{ clearApiKey(); setAuthed(false); }} />
- setShowRun(true)} /> + setShowRun(true)} />
- {!agent && ( + {!client && (

- Select an agent from the sidebar to get started. + Select an client from the sidebar to get started.

)} - {agent && view === "threads" && threadId === null && ( - + {client && view === "threads" && threadId === null && ( + )} - {agent && view === "threads" && threadId !== null && ( - setThreadId(null)} /> + {client && view === "threads" && threadId !== null && ( + setThreadId(null)} /> )} - {agent && view === "workflows" && } + {client && view === "workflows" && }
- {showRun && agent && ( + {showRun && client && ( setShowRun(false)} onCreated={(id) => { setShowRun(false); diff --git a/packages/workflow-dashboard/src/components/record-card.tsx b/packages/workflow-dashboard/src/components/record-card.tsx index ecbb28d..5127135 100644 --- a/packages/workflow-dashboard/src/components/record-card.tsx +++ b/packages/workflow-dashboard/src/components/record-card.tsx @@ -3,7 +3,7 @@ import { Markdown } from "./markdown.tsx"; const ROLE_COLORS: Record = { preparer: "#8b5cf6", - agent: "#3b82f6", + client: "#3b82f6", extractor: "#f59e0b", }; diff --git a/packages/workflow-dashboard/src/components/run-dialog.tsx b/packages/workflow-dashboard/src/components/run-dialog.tsx index dca976a..843ca13 100644 --- a/packages/workflow-dashboard/src/components/run-dialog.tsx +++ b/packages/workflow-dashboard/src/components/run-dialog.tsx @@ -3,13 +3,13 @@ import { listWorkflows, runThread } from "../api.ts"; import { useFetch } from "../hooks.ts"; type Props = { - agent: string; + client: string; onClose: () => void; onCreated: (threadId: string) => void; }; -export function RunDialog({ agent, onClose, onCreated }: Props) { - const workflows = useFetch(() => listWorkflows(agent), [agent]); +export function RunDialog({ client, onClose, onCreated }: Props) { + const workflows = useFetch(() => listWorkflows(client), [client]); const [workflow, setWorkflow] = useState(""); const [prompt, setPrompt] = useState(""); const [submitting, setSubmitting] = useState(false); @@ -21,7 +21,7 @@ export function RunDialog({ agent, onClose, onCreated }: Props) { setSubmitting(true); setError(null); try { - const result = await runThread(agent, workflow, prompt); + const result = await runThread(client, workflow, prompt); onCreated(result.threadId); } catch (err) { setError(err instanceof Error ? err.message : String(err)); @@ -38,7 +38,7 @@ export function RunDialog({ agent, onClose, onCreated }: Props) { className="w-full max-w-lg p-6 rounded-lg border" style={{ background: "var(--color-surface)", borderColor: "var(--color-border)" }} > -

Run Thread on {agent}

+

Run Thread on {client}

- {/* Agent selector */} + {/* Client selector */}