// Protocol Gen Kotlin script supports OpenClaw repository automation. import { promises as fs } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { MIN_NODE_PROTOCOL_VERSION, PROTOCOL_VERSION, ProtocolSchemas, } from "../packages/gateway-protocol/src/schema.js"; import { CORE_GATEWAY_METHOD_SPECS } from "../src/gateway/methods/core-descriptors.js"; import { extractGatewayEventNames } from "./check-protocol-event-coverage.mjs"; type JsonSchema = { type?: string | string[]; const?: boolean | number | string | null; properties?: Record; required?: string[]; items?: JsonSchema; enum?: Array; patternProperties?: Record; anyOf?: JsonSchema[]; oneOf?: JsonSchema[]; }; type EnumSpec = { name: string; values: Array<{ name: string; rawValue: string }>; namespacePrefix?: string; }; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.resolve(scriptDir, ".."); const gatewayOutputPath = path.join( repoRoot, "apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt", ); const constantsOutputPath = path.join( repoRoot, "apps/android/app/src/main/java/ai/openclaw/app/protocol/OpenClawProtocolConstants.kt", ); const protocolSchemas = ProtocolSchemas as unknown as Record; const schemaNames = new Map([ ["ErrorShape", "GatewayProtocolError"], ["RequestFrame", "GatewayRequestFrame"], ["ResponseFrame", "GatewayResponseFrame"], ["EventFrame", "GatewayEventFrame"], ["NodeEventParams", "GatewayNodeEventParams"], ["NodeInvokeResultParams", "GatewayNodeInvokeResultParams"], ["NodeInvokeRequestEvent", "GatewayNodeInvokeRequest"], ]); const androidEnums: EnumSpec[] = [ enumSpec("OpenClawCapability", "", [ ["Canvas", "canvas"], ["Camera", "camera"], ["Sms", "sms"], ["Talk", "talk"], ["Location", "location"], ["Device", "device"], ["Notifications", "notifications"], ["System", "system"], ["Photos", "photos"], ["Contacts", "contacts"], ["Calendar", "calendar"], ["Motion", "motion"], ["CallLog", "callLog"], ]), enumSpec("OpenClawCanvasCommand", "canvas.", [ ["Present", "present"], ["Hide", "hide"], ["Navigate", "navigate"], ["Eval", "eval"], ["Snapshot", "snapshot"], ]), enumSpec("OpenClawCanvasA2UICommand", "canvas.a2ui.", [ ["Push", "push"], ["PushJSONL", "pushJSONL"], ["Reset", "reset"], ]), enumSpec("OpenClawCameraCommand", "camera.", [ ["List", "list"], ["Snap", "snap"], ["Clip", "clip"], ]), enumSpec("OpenClawSmsCommand", "sms.", [ ["Send", "send"], ["Search", "search"], ]), enumSpec("OpenClawTalkCommand", "talk.", [ ["PttStart", "ptt.start"], ["PttStop", "ptt.stop"], ["PttCancel", "ptt.cancel"], ["PttOnce", "ptt.once"], ]), enumSpec("OpenClawLocationCommand", "location.", [["Get", "get"]]), enumSpec("OpenClawDeviceCommand", "device.", [ ["Status", "status"], ["Info", "info"], ["Permissions", "permissions"], ["Health", "health"], ["Apps", "apps"], ]), enumSpec("OpenClawNotificationsCommand", "notifications.", [ ["List", "list"], ["Actions", "actions"], ]), enumSpec("OpenClawSystemCommand", "system.", [["Notify", "notify"]]), enumSpec("OpenClawPhotosCommand", "photos.", [["Latest", "latest"]]), enumSpec("OpenClawContactsCommand", "contacts.", [ ["Search", "search"], ["Add", "add"], ]), enumSpec("OpenClawCalendarCommand", "calendar.", [ ["Events", "events"], ["Add", "add"], ]), enumSpec("OpenClawMotionCommand", "motion.", [ ["Activity", "activity"], ["Pedometer", "pedometer"], ]), enumSpec("OpenClawCallLogCommand", "callLog.", [["Search", "search"]]), ]; function enumSpec( name: string, namespacePrefix: string, values: Array<[name: string, suffix: string]>, ): EnumSpec { return { name, namespacePrefix, values: values.map(([caseName, suffix]) => ({ name: caseName, rawValue: namespacePrefix + suffix, })), }; } function words(value: string): string[] { return ( value.match(/[A-Z]+(?=[A-Z][a-z]|\d|$)|[A-Z]?[a-z]+|\d+/g)?.map((part) => part.toLowerCase()) ?? [] ); } function upperCamel(value: string): string { const parts = words(value); if (parts.length === 0) { throw new Error(`Cannot create Kotlin identifier from ${JSON.stringify(value)}`); } return parts.map((part) => part[0]!.toUpperCase() + part.slice(1)).join(""); } function lowerCamel(value: string): string { const name = upperCamel(value); return name[0]!.toLowerCase() + name.slice(1); } function stableJson(value: unknown): unknown { if (Array.isArray(value)) { return value.map(stableJson); } if (value && typeof value === "object") { const record = value as Record; return Object.fromEntries( Object.keys(record) .toSorted() .map((key) => [key, stableJson(record[key])]), ); } return value; } function schemaSignature(schema: JsonSchema): string { return JSON.stringify(stableJson(schema)); } function literalValue(schema: JsonSchema): boolean | number | string | null | undefined { if ("const" in schema) { return schema.const; } return schema.enum?.length === 1 ? schema.enum[0] : undefined; } function kotlinLiteral(value: boolean | number | string | null): string { if (typeof value === "string") { return JSON.stringify(value); } if (value === null) { return "null"; } return String(value); } function emitEnum(spec: EnumSpec): string { const body = spec.values.map((value) => ` ${value.name}(${JSON.stringify(value.rawValue)}),`); if (spec.namespacePrefix) { body.push( " ;", "", " companion object {", ` const val NamespacePrefix: String = ${JSON.stringify(spec.namespacePrefix)}`, " }", ); } return [`enum class ${spec.name}(`, " val rawValue: String,", ") {", ...body, "}"].join("\n"); } function emitWireModels(): string[] { const selectedSchemas = new Map(); const selectedSignatures = new Map(); for (const [schemaName, kotlinName] of schemaNames) { const schema = protocolSchemas[schemaName]; if (!schema) { throw new Error(`Missing ProtocolSchemas.${schemaName}`); } selectedSchemas.set(schema, kotlinName); selectedSignatures.set(schemaSignature(schema), kotlinName); } const nestedModels = new Map(); const kotlinType = (schema: JsonSchema, nestedName: string): string => { const selected = selectedSchemas.get(schema) ?? selectedSignatures.get(schemaSignature(schema)); if (selected) { return selected; } if (schema.type === "string" || typeof schema.const === "string") { return "String"; } if (schema.type === "integer") { return "Long"; } if (schema.type === "number") { return "Double"; } if (schema.type === "boolean") { return "Boolean"; } if (schema.type === "array") { return `List<${kotlinType(schema.items ?? {}, `${nestedName}Item`)}>`; } if (schema.patternProperties) { const valueSchema = Object.values(schema.patternProperties)[0] ?? {}; return `Map`; } if (schema.type === "object") { nestedModels.set(nestedName, schema); return nestedName; } return "JsonElement"; }; const emitModel = (name: string, schema: JsonSchema): string => { if (schema.type !== "object" || !schema.properties) { throw new Error(`${name} must remain an object schema for Kotlin generation`); } const required = new Set(schema.required ?? []); const properties = Object.entries(schema.properties).map(([wireName, propertySchema]) => { const propertyName = lowerCamel(wireName); const type = kotlinType(propertySchema, `${name}${upperCamel(wireName)}`); const literal = literalValue(propertySchema); const optional = !required.has(wireName); return { annotation: propertyName === wireName ? [] : [` @SerialName(${JSON.stringify(wireName)})`], declaration: ` val ${propertyName}: ${type}${optional ? "?" : ""}${ literal !== undefined ? ` = ${kotlinLiteral(literal)}` : optional ? " = null" : "" },`, }; }); const fields: string[] = []; for (const property of properties) { fields.push(...property.annotation, property.declaration); } return ["@Serializable", `data class ${name}(`, ...fields, ")"].join("\n"); }; const output: string[] = []; for (const [schemaName, kotlinName] of schemaNames) { output.push(emitModel(kotlinName, protocolSchemas[schemaName]!)); } for (const [nestedName, schema] of nestedModels) { if (!output.some((model) => model.startsWith(`@Serializable\ndata class ${nestedName}(`))) { output.push(emitModel(nestedName, schema)); } } return output; } function emitGatewayCatalogEnum(name: string, values: readonly string[]): string { const seenNames = new Map(); const entries = values.map((rawValue) => { const caseName = upperCamel(rawValue); const previous = seenNames.get(caseName); if (previous) { throw new Error( `${name} case collision: ${previous} and ${rawValue} both map to ${caseName}`, ); } seenNames.set(caseName, rawValue); return { name: caseName, rawValue }; }); return emitEnum({ name, values: entries }); } async function generate(): Promise { const gatewayEventSource = await fs.readFile( path.join(repoRoot, "src/gateway/server-methods-list.ts"), "utf8", ); const gatewayEventConstants = await fs.readFile( path.join(repoRoot, "src/gateway/events.ts"), "utf8", ); const gatewayEvents = Array.from( extractGatewayEventNames(gatewayEventSource, gatewayEventConstants), ); const gatewayMethods = CORE_GATEWAY_METHOD_SPECS.map((spec) => spec.name); const gatewayContent = [ "// Generated by scripts/protocol-gen-kotlin.ts — do not edit by hand.", "package ai.openclaw.app.gateway", "", "import kotlinx.serialization.SerialName", "import kotlinx.serialization.Serializable", "import kotlinx.serialization.json.JsonElement", "", `const val GATEWAY_PROTOCOL_VERSION = ${PROTOCOL_VERSION}`, // Android consumes v3 message-only chat deltas and uses the N-1 node transport. `const val GATEWAY_MIN_PROTOCOL_VERSION = ${MIN_NODE_PROTOCOL_VERSION}`, "", ...emitWireModels().flatMap((model) => [model, ""]), emitGatewayCatalogEnum("GatewayMethod", gatewayMethods), "", emitGatewayCatalogEnum("GatewayEvent", gatewayEvents), "", ].join("\n"); const constantsContent = [ "// Generated by scripts/protocol-gen-kotlin.ts — do not edit by hand.", "package ai.openclaw.app.protocol", "", ...androidEnums.flatMap((spec) => [emitEnum(spec), ""]), ].join("\n"); await fs.writeFile(gatewayOutputPath, gatewayContent); await fs.writeFile(constantsOutputPath, constantsContent); console.log(`wrote ${path.relative(repoRoot, gatewayOutputPath)}`); console.log(`wrote ${path.relative(repoRoot, constantsOutputPath)}`); } generate().catch((error: unknown) => { console.error(error); process.exit(1); });