From 580a94aaaff6d08f1be590f27d741cbdd231e80d Mon Sep 17 00:00:00 2001 From: Imanol Maiztegui Date: Mon, 18 May 2026 14:03:20 +0200 Subject: [PATCH] feat(kilo-gateway): add Effect HttpApi endpoints for kilo gateway operations Extract shared handler logic from Hono routes into reusable functions in kilo-gateway/server/handlers.ts and wire up new Effect HttpApi endpoints for profile, notifications, organization switching, claw status, and claw chat credentials. - Add handlers.ts with pure business logic (getProfile, getNotifications, setOrganization, getClawStatus, getClawChatCredentials) - Define KiloGatewayApi group with typed schemas for all endpoints - Implement Effect-based handler layer in httpapi/handlers/kilo-gateway.ts - Register kilo gateway routes in instance router and HttpApi composition - Add config/warnings endpoint to config group and handler - Regenerate SDK types and client methods for new endpoints - Update OpenAPI spec with /kilo/* and /config/warnings paths - Export new constants (KILO_CHAT_URL, KILO_EVENT_SERVICE_URL) and handler utilities from kilo-gateway package index --- packages/kilo-gateway/src/index.ts | 13 + packages/kilo-gateway/src/server/handlers.ts | 128 +++++ packages/kilo-gateway/src/server/routes.ts | 135 ++--- .../server/httpapi/groups/kilo-gateway.ts | 172 ++++++ .../server/httpapi/handlers/kilo-gateway.ts | 115 ++++ .../src/kilocode/server/httpapi/instance.ts | 6 + .../src/kilocode/server/httpapi/server.ts | 2 + .../src/server/routes/instance/httpapi/api.ts | 2 + .../routes/instance/httpapi/groups/config.ts | 20 + .../instance/httpapi/handlers/config.ts | 12 +- .../src/server/routes/instance/index.ts | 1 + packages/sdk/js/src/v2/gen/sdk.gen.ts | 220 ++++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 212 +++++++ packages/sdk/openapi.json | 516 ++++++++++++++++++ 14 files changed, 1456 insertions(+), 98 deletions(-) create mode 100644 packages/kilo-gateway/src/server/handlers.ts create mode 100644 packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts create mode 100644 packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts diff --git a/packages/kilo-gateway/src/index.ts b/packages/kilo-gateway/src/index.ts index cfd97e06e90..7767919f67a 100644 --- a/packages/kilo-gateway/src/index.ts +++ b/packages/kilo-gateway/src/index.ts @@ -53,6 +53,17 @@ export { fetchKilocodeNotifications, type KilocodeNotification } from "./api/not // Server Routes (optional - requires hono and OpenCode dependencies) // ============================================================================ export { createKiloRoutes } from "./server/routes.js" +export { + GatewayError, + UnauthorizedError, + getOrganizationId, + getClawChatCredentials, + getClawStatus, + getNotifications, + getProfile, + getToken, + setOrganization, +} from "./server/handlers.js" // ============================================================================ // Note: TUI exports moved to separate entry point @@ -88,6 +99,8 @@ export { ENV_KILO_API_URL, DEFAULT_KILO_API_URL, KILO_API_BASE, + KILO_CHAT_URL, + KILO_EVENT_SERVICE_URL, KILO_OPENROUTER_BASE, POLL_INTERVAL_MS, DEFAULT_MODEL, diff --git a/packages/kilo-gateway/src/server/handlers.ts b/packages/kilo-gateway/src/server/handlers.ts new file mode 100644 index 00000000000..3141178655e --- /dev/null +++ b/packages/kilo-gateway/src/server/handlers.ts @@ -0,0 +1,128 @@ +import { fetchBalance, fetchProfile } from "../api/profile.js" +import { fetchKilocodeNotifications } from "../api/notifications.js" +import { clearModesCache } from "../api/modes.js" +import { HEADER_ORGANIZATIONID, KILO_API_BASE, KILO_CHAT_URL, KILO_EVENT_SERVICE_URL } from "../api/constants.js" +import type { KilocodeBalance, KilocodeProfile } from "../types.js" + +export type KiloAuth = + | { type: "api"; key: string } + | { type: "oauth"; access: string; refresh: string; expires: number; accountId?: string } + | { type: "wellknown"; key: string; token: string } + +export interface KiloProfileResult { + profile: KilocodeProfile + balance: KilocodeBalance | null + currentOrgId: string | null +} + +export interface ClawChatCredentials { + token: string + expiresAt: string + kiloChatUrl: string + eventServiceUrl: string +} + +export interface AuthStore { + get(provider: string): Promise + set(provider: string, auth: Extract): Promise +} + +export interface OrganizationDeps { + auth: AuthStore + clear(): void + dispose(): Promise +} + +export class UnauthorizedError extends Error {} + +export class GatewayError extends Error { + constructor( + message: string, + readonly status: number, + ) { + super(message) + } +} + +export function getToken(auth: KiloAuth | undefined) { + if (auth?.type === "api") return auth.key + if (auth?.type === "oauth") return auth.access + return undefined +} + +export function getOrganizationId(auth: KiloAuth | undefined) { + if (auth?.type === "oauth") return auth.accountId + return undefined +} + +export async function getProfile(auth: AuthStore): Promise { + const info = await auth.get("kilo") + if (!info || info.type !== "oauth") throw new UnauthorizedError("Not authenticated with Kilo Gateway") + + const currentOrgId = info.accountId ?? null + const [profile, balance] = await Promise.all([ + fetchProfile(info.access), + fetchBalance(info.access, currentOrgId ?? undefined), + ]) + return { profile, balance, currentOrgId } +} + +export async function getNotifications(auth: AuthStore) { + const info = await auth.get("kilo") + const token = getToken(info) + if (!token) return [] + + return fetchKilocodeNotifications({ + kilocodeToken: token, + kilocodeOrganizationId: getOrganizationId(info), + }) +} + +export async function setOrganization(deps: OrganizationDeps, organizationId: string | null) { + const info = await deps.auth.get("kilo") + if (!info || info.type !== "oauth") throw new UnauthorizedError("Not authenticated with Kilo Gateway") + + await deps.auth.set("kilo", { + type: "oauth", + refresh: info.refresh, + access: info.access, + expires: info.expires, + ...(organizationId && { accountId: organizationId }), + }) + + deps.clear() + clearModesCache() + await deps.dispose() + return true +} + +export async function getClawStatus(auth: AuthStore) { + const info = await auth.get("kilo") + const token = getToken(info) + if (!token) throw new UnauthorizedError("No valid token found") + + const headers: Record = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + } + const org = getOrganizationId(info) + if (org) headers[HEADER_ORGANIZATIONID] = org + + const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/status`, { headers }) + if (!response.ok) throw new GatewayError(await response.text(), response.status) + return response.json() +} + +export async function getClawChatCredentials(auth: AuthStore): Promise { + const info = await auth.get("kilo") + const token = getToken(info) + if (!token) throw new UnauthorizedError("No valid token found") + + const expires = info?.type === "oauth" ? info.expires : Date.now() + 365 * 24 * 60 * 60 * 1000 + return { + token, + expiresAt: new Date(expires).toISOString(), + kiloChatUrl: KILO_CHAT_URL, + eventServiceUrl: KILO_EVENT_SERVICE_URL, + } +} diff --git a/packages/kilo-gateway/src/server/routes.ts b/packages/kilo-gateway/src/server/routes.ts index f2cd17498fa..fa44b651375 100644 --- a/packages/kilo-gateway/src/server/routes.ts +++ b/packages/kilo-gateway/src/server/routes.ts @@ -5,19 +5,21 @@ * This factory function accepts OpenCode dependencies to create Kilo-specific routes */ -import { fetchProfile, fetchBalance } from "../api/profile.js" import { fetchKilocodeNotifications, KilocodeNotificationSchema } from "../api/notifications.js" import { fetchOrganizationModes, clearModesCache } from "../api/modes.js" -import { - KILO_API_BASE, - KILO_CHAT_URL, - KILO_EVENT_SERVICE_URL, - HEADER_FEATURE, - HEADER_ORGANIZATIONID, -} from "../api/constants.js" +import { KILO_API_BASE, HEADER_FEATURE, HEADER_ORGANIZATIONID } from "../api/constants.js" import { buildKiloHeaders } from "../headers.js" import type { ImportDeps, DrizzleDb } from "../cloud-sessions.js" import { fetchCloudSession, fetchCloudSessionForImport, importSessionToDb } from "../cloud-sessions.js" +import { + GatewayError, + UnauthorizedError, + getClawChatCredentials, + getClawStatus, + getNotifications, + getProfile, + setOrganization, +} from "./handlers.js" // Type definitions for OpenCode dependencies (injected at runtime) type Hono = any @@ -167,24 +169,12 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }, }), async (c: any) => { - // Get Kilo auth - const auth = await Auth.get("kilo") - - if (!auth || auth.type !== "oauth") { + try { + return c.json(await getProfile(Auth)) + } catch (err) { + if (!(err instanceof UnauthorizedError)) throw err return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) } - - const token = auth.access - const currentOrgId = auth.accountId ?? null - - // Fetch profile and balance in parallel - // Pass organizationId to fetchBalance to get team balance when in org context - const [profile, balance] = await Promise.all([ - fetchProfile(token), - fetchBalance(token, currentOrgId ?? undefined), - ]) - - return c.json({ profile, balance, currentOrgId }) }, ) .post( @@ -214,27 +204,21 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { async (c: any) => { const { organizationId } = c.req.valid("json") - // Get current Kilo auth - const auth = await Auth.get("kilo") - - if (!auth || auth.type !== "oauth") { + try { + return c.json( + await setOrganization( + { + auth: Auth, + clear: () => ModelCache.clear("kilo"), + dispose: () => InstanceStore.disposeAllInstances(), + }, + organizationId, + ), + ) + } catch (err) { + if (!(err instanceof UnauthorizedError)) throw err return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) } - - // Update auth with new organization ID - await Auth.set("kilo", { - type: "oauth", - refresh: auth.refresh, - access: auth.access, - expires: auth.expires, - ...(organizationId && { accountId: organizationId }), - }) - - ModelCache.clear("kilo") - clearModesCache() - await InstanceStore.disposeAllInstances() - - return c.json(true) }, ) .get( @@ -481,19 +465,7 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }, }), async (c: any) => { - const auth = await Auth.get("kilo") - if (!auth) return c.json([]) - - const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined - if (!token) return c.json([]) - - const organizationId = auth.type === "oauth" ? auth.accountId : undefined - const notifications = await fetchKilocodeNotifications({ - kilocodeToken: token, - kilocodeOrganizationId: organizationId, - }) - - return c.json(notifications) + return c.json(await getNotifications(Auth)) }, ) .get( @@ -642,29 +614,11 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }), async (c: any) => { try { - const auth = await Auth.get("kilo") - if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) - const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined - if (!token) return c.json({ error: "No valid token found" }, 401) - - const organizationId = auth.type === "oauth" ? auth.accountId : undefined - const headers: Record = { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - } - if (organizationId) { - headers[HEADER_ORGANIZATIONID] = organizationId - } - - const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/status`, { headers }) - - if (!response.ok) { - const text = await response.text() - return c.json({ error: `KiloClaw request failed: ${response.status} ${text}` }, response.status as any) - } - - return c.json(await response.json()) + return c.json(await getClawStatus(Auth)) } catch (err: any) { + if (err instanceof GatewayError) { + return c.json({ error: `KiloClaw request failed: ${err.status} ${err.message}` }, err.status as any) + } console.error("[Kilo Gateway] claw/status: error", err?.message ?? err) return c.json({ error: "Failed to reach KiloClaw" }, 502) } @@ -701,25 +655,12 @@ export function createKiloRoutes(deps: KiloRoutesDeps) { }, }), async (c: any) => { - const auth = await Auth.get("kilo") - if (!auth) return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) - const token = auth.type === "api" ? auth.key : auth.type === "oauth" ? auth.access : undefined - if (!token) return c.json({ error: "No valid token found" }, 401) - - // For OAuth, expires is a millisecond epoch we already track. For - // API tokens we don't have a verified expiry locally — the JWT is - // signed by the cloud and validated by kilo-chat/event-service on - // every request. Use a far-future placeholder so the client cache - // doesn't refetch unnecessarily; on 401 the client clears the - // cache and prompts re-auth. - const expiresAtMs = auth.type === "oauth" ? auth.expires : Date.now() + 365 * 24 * 60 * 60 * 1000 - - return c.json({ - token, - expiresAt: new Date(expiresAtMs).toISOString(), - kiloChatUrl: KILO_CHAT_URL, - eventServiceUrl: KILO_EVENT_SERVICE_URL, - }) + try { + return c.json(await getClawChatCredentials(Auth)) + } catch (err) { + if (!(err instanceof UnauthorizedError)) throw err + return c.json({ error: "Not authenticated with Kilo Gateway" }, 401) + } }, ) .get( diff --git a/packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts b/packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts new file mode 100644 index 00000000000..b97a7cf4ef7 --- /dev/null +++ b/packages/opencode/src/kilocode/server/httpapi/groups/kilo-gateway.ts @@ -0,0 +1,172 @@ +import { Schema } from "effect" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { Authorization } from "@/server/routes/instance/httpapi/middleware/authorization" +import { InstanceContextMiddleware } from "@/server/routes/instance/httpapi/middleware/instance-context" +import { WorkspaceRoutingMiddleware } from "@/server/routes/instance/httpapi/middleware/workspace-routing" +import { described } from "@/server/routes/instance/httpapi/groups/metadata" + +const root = "/kilo" + +export const Organization = Schema.Struct({ + id: Schema.String, + name: Schema.String, + role: Schema.String, +}) + +export const Profile = Schema.Struct({ + email: Schema.String, + name: Schema.optional(Schema.String), + organizations: Schema.optional(Schema.Array(Organization)), +}) + +export const Balance = Schema.Struct({ + balance: Schema.Finite, +}) + +export const ProfileWithBalance = Schema.Struct({ + profile: Profile, + balance: Schema.NullOr(Balance), + currentOrgId: Schema.NullOr(Schema.String), +}) + +export const NotificationAction = Schema.Struct({ + actionText: Schema.String, + actionURL: Schema.String, +}) + +export const Notification = Schema.Struct({ + id: Schema.String, + title: Schema.String, + message: Schema.String, + action: Schema.optional(NotificationAction), + showIn: Schema.optional(Schema.Array(Schema.String)), + suggestModelId: Schema.optional(Schema.String), +}) + +export const OrganizationBody = Schema.Struct({ + organizationId: Schema.NullOr(Schema.String), +}) + +export const ClawStatus = Schema.Struct({ + status: Schema.NullOr( + Schema.Literals([ + "provisioned", + "starting", + "restarting", + "recovering", + "running", + "stopped", + "destroying", + "restoring", + ]), + ), + sandboxId: Schema.optional(Schema.String), + flyRegion: Schema.optional(Schema.String), + machineSize: Schema.optional( + Schema.Struct({ + cpus: Schema.Finite, + memory_mb: Schema.Finite, + }), + ), + openclawVersion: Schema.optional(Schema.NullOr(Schema.String)), + lastStartedAt: Schema.optional(Schema.NullOr(Schema.String)), + lastStoppedAt: Schema.optional(Schema.NullOr(Schema.String)), + channelCount: Schema.optional(Schema.Finite), + secretCount: Schema.optional(Schema.Finite), + userId: Schema.optional(Schema.String), + botName: Schema.optional(Schema.NullOr(Schema.String)), +}) + +export const ClawChatCredentials = Schema.NullOr( + Schema.Struct({ + token: Schema.String, + expiresAt: Schema.String, + kiloChatUrl: Schema.String, + eventServiceUrl: Schema.String, + }), +) + +export const KiloGatewayPaths = { + profile: `${root}/profile`, + notifications: `${root}/notifications`, + organization: `${root}/organization`, + clawStatus: `${root}/claw/status`, + clawChatCredentials: `${root}/claw/chat-credentials`, +} as const + +export const KiloGatewayApi = HttpApi.make("kilo") + .add( + HttpApiGroup.make("kilo") + .add( + HttpApiEndpoint.get("profile", KiloGatewayPaths.profile, { + success: described(ProfileWithBalance, "Profile data"), + error: [HttpApiError.BadRequest, HttpApiError.Unauthorized], + }).annotateMerge( + OpenApi.annotations({ + identifier: "kilo.profile", + summary: "Get Kilo Gateway profile", + description: "Fetch user profile and organizations from Kilo Gateway", + }), + ), + HttpApiEndpoint.get("notifications", KiloGatewayPaths.notifications, { + success: described(Schema.Array(Notification), "Notifications list"), + error: [HttpApiError.BadRequest, HttpApiError.Unauthorized], + }).annotateMerge( + OpenApi.annotations({ + identifier: "kilo.notifications", + summary: "Get Kilo notifications", + description: "Fetch notifications from Kilo Gateway for CLI display", + }), + ), + HttpApiEndpoint.post("organization", KiloGatewayPaths.organization, { + payload: OrganizationBody, + success: described(Schema.Boolean, "Organization updated successfully"), + error: [HttpApiError.BadRequest, HttpApiError.Unauthorized], + }).annotateMerge( + OpenApi.annotations({ + identifier: "kilo.organization.set", + summary: "Update Kilo Gateway organization", + description: "Switch to a different Kilo Gateway organization", + }), + ), + HttpApiEndpoint.get("clawStatus", KiloGatewayPaths.clawStatus, { + success: described(ClawStatus, "Instance status"), + error: [HttpApiError.Unauthorized, HttpApiError.ServiceUnavailable], + }).annotateMerge( + OpenApi.annotations({ + identifier: "kilo.claw.status", + summary: "Get KiloClaw instance status", + description: "Fetch the user's KiloClaw instance status via the KiloClaw worker", + }), + ), + HttpApiEndpoint.get("clawChatCredentials", KiloGatewayPaths.clawChatCredentials, { + success: described(ClawChatCredentials, "Kilo Chat credentials or null"), + error: HttpApiError.Unauthorized, + }).annotateMerge( + OpenApi.annotations({ + identifier: "kilo.claw.chatCredentials", + summary: "Get KiloClaw chat credentials", + description: + "Returns the bearer token and endpoint URLs the client uses to talk to the Kilo Chat worker " + + "and the Event Service. The bearer is the user's existing long-lived Kilo JWT — kilo-chat and " + + "event-service both verify it directly with NEXTAUTH_SECRET, so no separate token mint is needed.", + }), + ), + ) + .annotateMerge( + OpenApi.annotations({ + title: "kilo", + description: "Kilo Gateway routes.", + }), + ) + .middleware(InstanceContextMiddleware) + .middleware(WorkspaceRoutingMiddleware) + .middleware(Authorization), + ) + .annotateMerge( + OpenApi.annotations({ + title: "kilo HttpApi", + version: "0.0.1", + description: "Kilo HttpApi surface.", + }), + ) diff --git a/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts b/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts new file mode 100644 index 00000000000..09dd80451a6 --- /dev/null +++ b/packages/opencode/src/kilocode/server/httpapi/handlers/kilo-gateway.ts @@ -0,0 +1,115 @@ +import { GatewayError, UnauthorizedError, getOrganizationId, getToken } from "@kilocode/kilo-gateway" +import { + HEADER_ORGANIZATIONID, + KILO_API_BASE, + KILO_CHAT_URL, + KILO_EVENT_SERVICE_URL, + clearModesCache, + fetchBalance, + fetchKilocodeNotifications, + fetchProfile, +} from "@kilocode/kilo-gateway" +import { Effect } from "effect" +import { HttpApiBuilder, HttpApiError } from "effect/unstable/httpapi" +import { Auth } from "@/auth" +import { InstanceStore } from "@/project/instance-store" +import { ModelCache } from "@/provider/model-cache" +import { InstanceHttpApi } from "@/server/routes/instance/httpapi/api" + +export const kiloGatewayHandlers = HttpApiBuilder.group(InstanceHttpApi, "kilo", (handlers) => + Effect.gen(function* () { + const auth = yield* Auth.Service + const store = yield* InstanceStore.Service + + const profile = Effect.fn("KiloGatewayHttpApi.profile")(function* () { + const info = yield* auth.get("kilo").pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) + if (!info || info.type !== "oauth") return yield* Effect.fail(new HttpApiError.Unauthorized({})) + + const currentOrgId = info.accountId ?? null + const [profile, balance] = yield* Effect.tryPromise({ + try: () => Promise.all([fetchProfile(info.access), fetchBalance(info.access, currentOrgId ?? undefined)]), + catch: () => new HttpApiError.BadRequest({}), + }) + return { profile, balance, currentOrgId } + }) + + const notifications = Effect.fn("KiloGatewayHttpApi.notifications")(function* () { + const info = yield* auth.get("kilo").pipe(Effect.mapError(() => new HttpApiError.BadRequest({}))) + const token = getToken(info) + if (!token) return [] + + return yield* Effect.promise(() => + fetchKilocodeNotifications({ + kilocodeToken: token, + kilocodeOrganizationId: getOrganizationId(info), + }), + ) + }) + + const organization = Effect.fn("KiloGatewayHttpApi.organization")(function* (ctx) { + const info = yield* auth.get("kilo").pipe(Effect.mapError(() => new HttpApiError.Unauthorized({}))) + if (!info || info.type !== "oauth") return yield* Effect.fail(new HttpApiError.Unauthorized({})) + + yield* auth + .set("kilo", { + type: "oauth", + refresh: info.refresh, + access: info.access, + expires: info.expires, + ...(ctx.payload.organizationId && { accountId: ctx.payload.organizationId }), + }) + .pipe(Effect.mapError(() => new HttpApiError.Unauthorized({}))) + + ModelCache.clear("kilo") + clearModesCache() + yield* store.disposeAll().pipe(Effect.mapError(() => new HttpApiError.Unauthorized({}))) + return true + }) + + const clawStatus = Effect.fn("KiloGatewayHttpApi.clawStatus")(function* () { + const info = yield* auth.get("kilo").pipe(Effect.mapError(() => new HttpApiError.ServiceUnavailable({}))) + const token = getToken(info) + if (!token) return yield* Effect.fail(new HttpApiError.Unauthorized({})) + + const headers: Record = { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + } + const org = getOrganizationId(info) + if (org) headers[HEADER_ORGANIZATIONID] = org + + return yield* Effect.tryPromise({ + try: async () => { + const response = await fetch(`${KILO_API_BASE}/api/kiloclaw/status`, { headers }) + if (!response.ok) throw new GatewayError(await response.text(), response.status) + return response.json() + }, + catch: (err) => + err instanceof UnauthorizedError + ? new HttpApiError.Unauthorized({}) + : new HttpApiError.ServiceUnavailable({}), + }) + }) + + const clawChatCredentials = Effect.fn("KiloGatewayHttpApi.clawChatCredentials")(function* () { + const info = yield* auth.get("kilo").pipe(Effect.mapError(() => new HttpApiError.Unauthorized({}))) + const token = getToken(info) + if (!token) return yield* Effect.fail(new HttpApiError.Unauthorized({})) + + const expires = info?.type === "oauth" ? info.expires : Date.now() + 365 * 24 * 60 * 60 * 1000 + return { + token, + expiresAt: new Date(expires).toISOString(), + kiloChatUrl: KILO_CHAT_URL, + eventServiceUrl: KILO_EVENT_SERVICE_URL, + } + }) + + return handlers + .handle("profile", profile) + .handle("notifications", notifications) + .handle("organization", organization) + .handle("clawStatus", clawStatus) + .handle("clawChatCredentials", clawChatCredentials) + }), +) diff --git a/packages/opencode/src/kilocode/server/httpapi/instance.ts b/packages/opencode/src/kilocode/server/httpapi/instance.ts index ff0844b73e9..e3bb160f073 100644 --- a/packages/opencode/src/kilocode/server/httpapi/instance.ts +++ b/packages/opencode/src/kilocode/server/httpapi/instance.ts @@ -1,6 +1,7 @@ import type { Context } from "effect" import type { Hono } from "hono" import { IndexingPaths } from "./groups/indexing" +import { KiloGatewayPaths } from "./groups/kilo-gateway" import { KilocodePaths } from "./groups/kilocode" import { NetworkPaths } from "./groups/network" import { RemotePaths } from "./groups/remote" @@ -33,6 +34,11 @@ export function register(app: Hono, handler: Handler, context: Context.Context handler(c.req.raw, context)) app.post(SessionImportPaths.part, (c) => handler(c.req.raw, context)) app.get(IndexingPaths.status, (c) => handler(c.req.raw, context)) + app.get(KiloGatewayPaths.profile, (c) => handler(c.req.raw, context)) + app.get(KiloGatewayPaths.notifications, (c) => handler(c.req.raw, context)) + app.post(KiloGatewayPaths.organization, (c) => handler(c.req.raw, context)) + app.get(KiloGatewayPaths.clawStatus, (c) => handler(c.req.raw, context)) + app.get(KiloGatewayPaths.clawChatCredentials, (c) => handler(c.req.raw, context)) return app } diff --git a/packages/opencode/src/kilocode/server/httpapi/server.ts b/packages/opencode/src/kilocode/server/httpapi/server.ts index 0b923f7bc3f..ca9b07c47f2 100644 --- a/packages/opencode/src/kilocode/server/httpapi/server.ts +++ b/packages/opencode/src/kilocode/server/httpapi/server.ts @@ -3,6 +3,7 @@ import { Layer } from "effect" import { commitMessageHandlers } from "./handlers/commit-message" import { enhancePromptHandlers } from "./handlers/enhance-prompt" import { indexingHandlers } from "./handlers/indexing" +import { kiloGatewayHandlers } from "./handlers/kilo-gateway" import { kilocodeHandlers } from "./handlers/kilocode" import { networkHandlers } from "./handlers/network" import { remoteHandlers } from "./handlers/remote" @@ -14,6 +15,7 @@ export const provide = Layer.provide([ commitMessageHandlers, enhancePromptHandlers, indexingHandlers, + kiloGatewayHandlers, kilocodeHandlers, networkHandlers, remoteHandlers, diff --git a/packages/opencode/src/server/routes/instance/httpapi/api.ts b/packages/opencode/src/server/routes/instance/httpapi/api.ts index bd3b6942dea..7b7d6b640c2 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/api.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/api.ts @@ -24,6 +24,7 @@ import { V2Api } from "./groups/v2" import { CommitMessageApi } from "@/kilocode/server/httpapi/groups/commit-message" import { EnhancePromptApi } from "@/kilocode/server/httpapi/groups/enhance-prompt" import { IndexingApi } from "@/kilocode/server/httpapi/groups/indexing" +import { KiloGatewayApi } from "@/kilocode/server/httpapi/groups/kilo-gateway" import { KilocodeApi } from "@/kilocode/server/httpapi/groups/kilocode" import { NetworkApi } from "@/kilocode/server/httpapi/groups/network" import { RemoteApi } from "@/kilocode/server/httpapi/groups/remote" @@ -59,6 +60,7 @@ export const InstanceHttpApi = HttpApi.make("opencode-instance") .addHttpApi(CommitMessageApi) .addHttpApi(EnhancePromptApi) .addHttpApi(IndexingApi) + .addHttpApi(KiloGatewayApi) .addHttpApi(KilocodeApi) .addHttpApi(NetworkApi) .addHttpApi(RemoteApi) diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts index fa77785a9bc..c8afcc557fc 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/config.ts @@ -1,5 +1,6 @@ import { Config } from "@/config/config" import { Provider } from "@/provider/provider" +import { Schema } from "effect" // kilocode_change import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { Authorization } from "../middleware/authorization" import { InstanceContextMiddleware } from "../middleware/instance-context" @@ -8,6 +9,14 @@ import { described } from "./metadata" const root = "/config" +// kilocode_change start +const Warning = Schema.Struct({ + path: Schema.String, + message: Schema.String, + detail: Schema.optional(Schema.String), +}) +// kilocode_change end + export const ConfigApi = HttpApi.make("config") .add( HttpApiGroup.make("config") @@ -32,6 +41,17 @@ export const ConfigApi = HttpApi.make("config") description: "Update OpenCode configuration settings and preferences.", }), ), + // kilocode_change start + HttpApiEndpoint.get("warnings", `${root}/warnings`, { + success: described(Schema.Array(Warning), "Config warnings"), + }).annotateMerge( + OpenApi.annotations({ + identifier: "config.warnings", + summary: "Get config warnings", + description: "Get warnings generated during config loading (e.g., invalid JSON, schema errors).", + }), + ), + // kilocode_change end HttpApiEndpoint.get("providers", `${root}/providers`, { success: described(Provider.ConfigProvidersResult, "List of providers"), }).annotateMerge( diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts index 753ba031380..b3526452723 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts @@ -21,6 +21,12 @@ export const configHandlers = HttpApiBuilder.group(InstanceHttpApi, "config", (h return ctx.payload }) + // kilocode_change start + const warnings = Effect.fn("ConfigHttpApi.warnings")(function* () { + return yield* configSvc.warnings() + }) + // kilocode_change end + const providers = Effect.fn("ConfigHttpApi.providers")(function* () { const providers = yield* providerSvc.list() return { @@ -29,6 +35,10 @@ export const configHandlers = HttpApiBuilder.group(InstanceHttpApi, "config", (h } }) - return handlers.handle("get", get).handle("update", update).handle("providers", providers) + return handlers + .handle("get", get) + .handle("update", update) + .handle("warnings", warnings) + .handle("providers", providers) // kilocode_change }), ) diff --git a/packages/opencode/src/server/routes/instance/index.ts b/packages/opencode/src/server/routes/instance/index.ts index 3468983f4db..e60d2880273 100644 --- a/packages/opencode/src/server/routes/instance/index.ts +++ b/packages/opencode/src/server/routes/instance/index.ts @@ -59,6 +59,7 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket, opts?: CorsOptions): H app.post("/permission/:requestID/reply", (c) => handler(c.req.raw, context)) app.get("/config", (c) => handler(c.req.raw, context)) app.patch("/config", (c) => handler(c.req.raw, context)) + app.get("/config/warnings", (c) => handler(c.req.raw, context)) // kilocode_change app.get("/config/providers", (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.console, (c) => handler(c.req.raw, context)) app.get(ExperimentalPaths.consoleOrgs, (c) => handler(c.req.raw, context)) diff --git a/packages/sdk/js/src/v2/gen/sdk.gen.ts b/packages/sdk/js/src/v2/gen/sdk.gen.ts index 0954de079de..28ff3b9615f 100644 --- a/packages/sdk/js/src/v2/gen/sdk.gen.ts +++ b/packages/sdk/js/src/v2/gen/sdk.gen.ts @@ -21,6 +21,7 @@ import type { ConfigProvidersResponses, ConfigUpdateErrors, ConfigUpdateResponses, + ConfigWarningsResponses, EnhancePromptEnhanceErrors, EnhancePromptEnhanceResponses, EventSubscribeResponses, @@ -61,6 +62,9 @@ import type { GlobalUpgradeResponses, IndexingStatusResponses, InstanceDisposeResponses, + KiloClawChatCredentialsResponses, + KiloClawStatusErrors, + KiloClawStatusResponses, KilocodeHeapSnapshotErrors, KilocodeHeapSnapshotResponses, KilocodeRemoveAgentErrors, @@ -75,6 +79,12 @@ import type { KilocodeSessionImportProjectResponses, KilocodeSessionImportSessionErrors, KilocodeSessionImportSessionResponses, + KiloNotificationsErrors, + KiloNotificationsResponses, + KiloOrganizationSetErrors, + KiloOrganizationSetResponses, + KiloProfileErrors, + KiloProfileResponses, LspStatusResponses, McpAddErrors, McpAddResponses, @@ -665,6 +675,36 @@ export class Config2 extends HeyApiClient { }) } + /** + * Get config warnings + * + * Get warnings generated during config loading (e.g., invalid JSON, schema errors). + */ + public warnings( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/config/warnings", + ...options, + ...params, + }) + } + /** * List config providers * @@ -4884,6 +4924,181 @@ export class Indexing extends HeyApiClient { } } +export class Organization extends HeyApiClient { + /** + * Update Kilo Gateway organization + * + * Switch to a different Kilo Gateway organization + */ + public set( + parameters?: { + directory?: string + workspace?: string + organizationId?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + { in: "body", key: "organizationId" }, + ], + }, + ], + ) + return (options?.client ?? this.client).post( + { + url: "/kilo/organization", + ...options, + ...params, + headers: { + "Content-Type": "application/json", + ...options?.headers, + ...params.headers, + }, + }, + ) + } +} + +export class Claw extends HeyApiClient { + /** + * Get KiloClaw instance status + * + * Fetch the user's KiloClaw instance status via the KiloClaw worker + */ + public status( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/kilo/claw/status", + ...options, + ...params, + }) + } + + /** + * Get KiloClaw chat credentials + * + * Returns the bearer token and endpoint URLs the client uses to talk to the Kilo Chat worker and the Event Service. The bearer is the user's existing long-lived Kilo JWT — kilo-chat and event-service both verify it directly with NEXTAUTH_SECRET, so no separate token mint is needed. + */ + public chatCredentials( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/kilo/claw/chat-credentials", + ...options, + ...params, + }) + } +} + +export class Kilo extends HeyApiClient { + /** + * Get Kilo Gateway profile + * + * Fetch user profile and organizations from Kilo Gateway + */ + public profile( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/kilo/profile", + ...options, + ...params, + }) + } + + /** + * Get Kilo notifications + * + * Fetch notifications from Kilo Gateway for CLI display + */ + public notifications( + parameters?: { + directory?: string + workspace?: string + }, + options?: Options, + ) { + const params = buildClientParams( + [parameters], + [ + { + args: [ + { in: "query", key: "directory" }, + { in: "query", key: "workspace" }, + ], + }, + ], + ) + return (options?.client ?? this.client).get({ + url: "/kilo/notifications", + ...options, + ...params, + }) + } + + private _organization?: Organization + get organization(): Organization { + return (this._organization ??= new Organization({ client: this.client })) + } + + private _claw?: Claw + get claw(): Claw { + return (this._claw ??= new Claw({ client: this.client })) + } +} + export class Heap extends HeyApiClient { /** * Write heap snapshot @@ -5925,6 +6140,11 @@ export class KiloClient extends HeyApiClient { return (this._indexing ??= new Indexing({ client: this.client })) } + private _kilo?: Kilo + get kilo(): Kilo { + return (this._kilo ??= new Kilo({ client: this.client })) + } + private _kilocode?: Kilocode get kilocode(): Kilocode { return (this._kilocode ??= new Kilocode({ client: this.client })) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index d1d63c59176..6619b5c812c 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1856,6 +1856,10 @@ export type EffectHttpApiErrorUnauthorized = { _tag: "Unauthorized" } +export type EffectHttpApiErrorInternalServerError = { + _tag: "InternalServerError" +} + export type KilocodeSessionImportResult = { ok: boolean id: string @@ -3794,6 +3798,29 @@ export type ConfigUpdateResponses = { export type ConfigUpdateResponse = ConfigUpdateResponses[keyof ConfigUpdateResponses] +export type ConfigWarningsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/config/warnings" +} + +export type ConfigWarningsResponses = { + /** + * Config warnings + */ + 200: Array<{ + path: string + message: string + detail?: string + }> +} + +export type ConfigWarningsResponse = ConfigWarningsResponses[keyof ConfigWarningsResponses] + export type ConfigProvidersData = { body?: never path?: never @@ -7103,6 +7130,191 @@ export type IndexingStatusResponses = { export type IndexingStatusResponse = IndexingStatusResponses[keyof IndexingStatusResponses] +export type KiloProfileData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/kilo/profile" +} + +export type KiloProfileErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type KiloProfileError = KiloProfileErrors[keyof KiloProfileErrors] + +export type KiloProfileResponses = { + /** + * Profile data + */ + 200: { + profile: { + email: string + name?: string + organizations?: Array<{ + id: string + name: string + role: string + }> + } + balance: { + balance: number + } + currentOrgId: string + } +} + +export type KiloProfileResponse = KiloProfileResponses[keyof KiloProfileResponses] + +export type KiloNotificationsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/kilo/notifications" +} + +export type KiloNotificationsErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type KiloNotificationsError = KiloNotificationsErrors[keyof KiloNotificationsErrors] + +export type KiloNotificationsResponses = { + /** + * Notifications list + */ + 200: Array<{ + id: string + title: string + message: string + action?: { + actionText: string + actionURL: string + } + showIn?: Array + suggestModelId?: string + }> +} + +export type KiloNotificationsResponse = KiloNotificationsResponses[keyof KiloNotificationsResponses] + +export type KiloOrganizationSetData = { + body?: { + organizationId: string + } + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/kilo/organization" +} + +export type KiloOrganizationSetErrors = { + /** + * Bad request + */ + 400: BadRequestError +} + +export type KiloOrganizationSetError = KiloOrganizationSetErrors[keyof KiloOrganizationSetErrors] + +export type KiloOrganizationSetResponses = { + /** + * Organization updated successfully + */ + 200: boolean +} + +export type KiloOrganizationSetResponse = KiloOrganizationSetResponses[keyof KiloOrganizationSetResponses] + +export type KiloClawStatusData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/kilo/claw/status" +} + +export type KiloClawStatusErrors = { + /** + * InternalServerError + */ + 500: EffectHttpApiErrorInternalServerError +} + +export type KiloClawStatusError = KiloClawStatusErrors[keyof KiloClawStatusErrors] + +export type KiloClawStatusResponses = { + /** + * Instance status + */ + 200: { + status: + | "provisioned" + | "starting" + | "restarting" + | "recovering" + | "running" + | "stopped" + | "destroying" + | "restoring" + sandboxId?: string + flyRegion?: string + machineSize?: { + cpus: number + memory_mb: number + } + openclawVersion?: string + lastStartedAt?: string + lastStoppedAt?: string + channelCount?: number + secretCount?: number + userId?: string + botName?: string + } +} + +export type KiloClawStatusResponse = KiloClawStatusResponses[keyof KiloClawStatusResponses] + +export type KiloClawChatCredentialsData = { + body?: never + path?: never + query?: { + directory?: string + workspace?: string + } + url: "/kilo/claw/chat-credentials" +} + +export type KiloClawChatCredentialsResponses = { + /** + * Kilo Chat credentials or null + */ + 200: { + token: string + expiresAt: string + kiloChatUrl: string + eventServiceUrl: string + } +} + +export type KiloClawChatCredentialsResponse = KiloClawChatCredentialsResponses[keyof KiloClawChatCredentialsResponses] + export type KilocodeHeapSnapshotData = { body?: never path?: never diff --git a/packages/sdk/openapi.json b/packages/sdk/openapi.json index f56bca53e46..f54f62de1fa 100644 --- a/packages/sdk/openapi.json +++ b/packages/sdk/openapi.json @@ -587,6 +587,67 @@ ] } }, + "/config/warnings": { + "get": { + "tags": ["config"], + "operationId": "config.warnings", + "parameters": [ + { + "name": "directory", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Config warnings", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "path": { + "type": "string" + }, + "message": { + "type": "string" + }, + "detail": { + "type": "string" + } + }, + "required": ["path", "message"], + "additionalProperties": false + }, + "description": "Config warnings" + } + } + } + } + }, + "description": "Get warnings generated during config loading (e.g., invalid JSON, schema errors).", + "summary": "Get config warnings", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.config.warnings({\n ...\n})" + } + ] + } + }, "/config/providers": { "get": { "tags": ["config"], @@ -8802,6 +8863,446 @@ ] } }, + "/kilo/profile": { + "get": { + "tags": ["kilo"], + "operationId": "kilo.profile", + "parameters": [ + { + "name": "directory", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Profile data", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "profile": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "name": { + "type": "string" + }, + "organizations": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "role": { + "type": "string" + } + }, + "required": ["id", "name", "role"], + "additionalProperties": false + } + } + }, + "required": ["email"], + "additionalProperties": false + }, + "balance": { + "type": "object", + "properties": { + "balance": { + "type": "number" + } + }, + "required": ["balance"], + "additionalProperties": false + }, + "currentOrgId": { + "type": "string" + } + }, + "required": ["profile", "balance", "currentOrgId"], + "additionalProperties": false, + "description": "Profile data" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "description": "Fetch user profile and organizations from Kilo Gateway", + "summary": "Get Kilo Gateway profile", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.kilo.profile({\n ...\n})" + } + ] + } + }, + "/kilo/notifications": { + "get": { + "tags": ["kilo"], + "operationId": "kilo.notifications", + "parameters": [ + { + "name": "directory", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Notifications list", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "message": { + "type": "string" + }, + "action": { + "type": "object", + "properties": { + "actionText": { + "type": "string" + }, + "actionURL": { + "type": "string" + } + }, + "required": ["actionText", "actionURL"], + "additionalProperties": false + }, + "showIn": { + "type": "array", + "items": { + "type": "string" + } + }, + "suggestModelId": { + "type": "string" + } + }, + "required": ["id", "title", "message"], + "additionalProperties": false + }, + "description": "Notifications list" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "description": "Fetch notifications from Kilo Gateway for CLI display", + "summary": "Get Kilo notifications", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.kilo.notifications({\n ...\n})" + } + ] + } + }, + "/kilo/organization": { + "post": { + "tags": ["kilo"], + "operationId": "kilo.organization.set", + "parameters": [ + { + "name": "directory", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Organization updated successfully", + "content": { + "application/json": { + "schema": { + "type": "boolean", + "description": "Organization updated successfully" + } + } + } + }, + "400": { + "description": "Bad request", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BadRequestError" + } + } + } + } + }, + "description": "Switch to a different Kilo Gateway organization", + "summary": "Update Kilo Gateway organization", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "organizationId": { + "type": "string" + } + }, + "required": ["organizationId"], + "additionalProperties": false + } + } + } + }, + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.kilo.organization.set({\n ...\n})" + } + ] + } + }, + "/kilo/claw/status": { + "get": { + "tags": ["kilo"], + "operationId": "kilo.claw.status", + "parameters": [ + { + "name": "directory", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Instance status", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": [ + "provisioned", + "starting", + "restarting", + "recovering", + "running", + "stopped", + "destroying", + "restoring" + ] + }, + "sandboxId": { + "type": "string" + }, + "flyRegion": { + "type": "string" + }, + "machineSize": { + "type": "object", + "properties": { + "cpus": { + "type": "number" + }, + "memory_mb": { + "type": "number" + } + }, + "required": ["cpus", "memory_mb"], + "additionalProperties": false + }, + "openclawVersion": { + "type": "string" + }, + "lastStartedAt": { + "type": "string" + }, + "lastStoppedAt": { + "type": "string" + }, + "channelCount": { + "type": "number" + }, + "secretCount": { + "type": "number" + }, + "userId": { + "type": "string" + }, + "botName": { + "type": "string" + } + }, + "required": ["status"], + "additionalProperties": false, + "description": "Instance status" + } + } + } + }, + "500": { + "description": "InternalServerError", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/effect_HttpApiError_InternalServerError" + } + } + } + } + }, + "description": "Fetch the user's KiloClaw instance status via the KiloClaw worker", + "summary": "Get KiloClaw instance status", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.kilo.claw.status({\n ...\n})" + } + ] + } + }, + "/kilo/claw/chat-credentials": { + "get": { + "tags": ["kilo"], + "operationId": "kilo.claw.chatCredentials", + "parameters": [ + { + "name": "directory", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "workspace", + "in": "query", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Kilo Chat credentials or null", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string" + }, + "expiresAt": { + "type": "string" + }, + "kiloChatUrl": { + "type": "string" + }, + "eventServiceUrl": { + "type": "string" + } + }, + "required": ["token", "expiresAt", "kiloChatUrl", "eventServiceUrl"], + "additionalProperties": false + } + } + } + } + }, + "description": "Returns the bearer token and endpoint URLs the client uses to talk to the Kilo Chat worker and the Event Service. The bearer is the user's existing long-lived Kilo JWT — kilo-chat and event-service both verify it directly with NEXTAUTH_SECRET, so no separate token mint is needed.", + "summary": "Get KiloClaw chat credentials", + "x-codeSamples": [ + { + "lang": "js", + "source": "import { createKiloClient } from \"@kilocode/sdk\n\nconst client = createKiloClient()\nawait client.kilo.claw.chatCredentials({\n ...\n})" + } + ] + } + }, "/kilocode/heap/snapshot": { "post": { "tags": ["kilocode"], @@ -15890,6 +16391,17 @@ "required": ["_tag"], "additionalProperties": false }, + "effect_HttpApiError_InternalServerError": { + "type": "object", + "properties": { + "_tag": { + "type": "string", + "enum": ["InternalServerError"] + } + }, + "required": ["_tag"], + "additionalProperties": false + }, "KilocodeSessionImportResult": { "type": "object", "properties": { @@ -21051,6 +21563,10 @@ "name": "indexing", "description": "Kilo indexing routes." }, + { + "name": "kilo", + "description": "Kilo Gateway routes." + }, { "name": "kilocode", "description": "Kilo-specific routes."