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
This commit is contained in:
Imanol Maiztegui
2026-05-18 14:03:20 +02:00
parent 09923fbe52
commit 580a94aaaf
14 changed files with 1456 additions and 98 deletions
+13
View File
@@ -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,
@@ -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<KiloAuth | undefined>
set(provider: string, auth: Extract<KiloAuth, { type: "oauth" }>): Promise<void>
}
export interface OrganizationDeps {
auth: AuthStore
clear(): void
dispose(): Promise<void>
}
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<KiloProfileResult> {
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<string, string> = {
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<ClawChatCredentials> {
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,
}
}
+38 -97
View File
@@ -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<string, string> = {
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(
@@ -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.",
}),
)
@@ -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<string, string> = {
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)
}),
)
@@ -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<u
app.post(SessionImportPaths.message, (c) => 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
}
@@ -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,
@@ -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)
@@ -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(
@@ -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
}),
)
@@ -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))
+220
View File
@@ -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<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).get<ConfigWarningsResponses, unknown, ThrowOnError>({
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<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
organizationId?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
{ in: "body", key: "organizationId" },
],
},
],
)
return (options?.client ?? this.client).post<KiloOrganizationSetResponses, KiloOrganizationSetErrors, ThrowOnError>(
{
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<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).get<KiloClawStatusResponses, KiloClawStatusErrors, ThrowOnError>({
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<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).get<KiloClawChatCredentialsResponses, unknown, ThrowOnError>({
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<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).get<KiloProfileResponses, KiloProfileErrors, ThrowOnError>({
url: "/kilo/profile",
...options,
...params,
})
}
/**
* Get Kilo notifications
*
* Fetch notifications from Kilo Gateway for CLI display
*/
public notifications<ThrowOnError extends boolean = false>(
parameters?: {
directory?: string
workspace?: string
},
options?: Options<never, ThrowOnError>,
) {
const params = buildClientParams(
[parameters],
[
{
args: [
{ in: "query", key: "directory" },
{ in: "query", key: "workspace" },
],
},
],
)
return (options?.client ?? this.client).get<KiloNotificationsResponses, KiloNotificationsErrors, ThrowOnError>({
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 }))
+212
View File
@@ -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<string>
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
+516
View File
@@ -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."