mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 17:53:39 +00:00
* feat: use MCP plugin apps across conversations and workspace files Extend the opt-in MCP Apps host with discovery and entrypoints, settings, rich forms, scoped file editing, multimodal context, deep links, and native Codex session preparation. Preserve existing requester, approval, runtime, and file authority owners. Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix: keep app context compact and review large tool payloads Reuse the canonical bounded plugin approval preview without rejecting otherwise valid large App arguments. Keep the same one-shot approval and live-authority gates. Bound context-strip icons and scrolling so attachments leave the composer usable. Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix: reveal pending input above fullscreen MCP apps Keep App fullscreen rendering in the existing browser top layer and return to inline for same-conversation questions or approvals without replacing the iframe. Consolidate host-context, resource, and input notifications under the existing bridge lifetime. Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * test: preserve full attempt inputs in native assignment fixtures Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix: release session access when MCP app launch preparation fails Preserve the upstream optional ifMatch contract and document unconditional-save semantics explicitly. Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(ui): defer question controls and preserve canonical keyboard values Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(mcp): preserve app authority through startup and user turns Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * fix(codex): share native app startup across concurrent discoveries Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * docs(mcp): explain native app prompting and request limits Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * feat: use MCP plugin apps across conversations and workspace files Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: f2f8d735-c1ac-4a0d-84f3-1576113c1baf * fix(mcp): keep form contracts and test helpers with their owners Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * feat: use MCP plugin apps across conversations and workspace files Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: 2513c3ef-4af8-4722-b802-ffa426df486a * fix(codex): revalidate MCP App authority before native retries Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * test(ui): provide chat identity fixture context dependencies Co-authored-by: steipete <58493+steipete@users.noreply.github.com> * feat: use MCP plugin apps across conversations and workspace files Worked on by: - @steipete Co-authored-by: steipete <58493+steipete@users.noreply.github.com> OpenClaw-Publication: 85c132dc-d926-46ca-b57e-2d9e01f15487 * fix: integrate MCP app owners with current main * fix(gateway): accept option labels from installed clients for rich forms * chore(format): anchor the native apps formatter ignore * perf(ui): load the MCP app link parser on demand Keep startup click interception parser-free and capture the normalized href before lazy navigation. Preserve ordinary, modified and download links; drop malformed plugin links and cancel pending navigation on disposal. Router/parser proof: 17 tests passed in 4.50s wall. Startup gzip: 374086 -> 373613 B; 159 B still above the unchanged 373454 B limit. * perf(ui): register MCP app English with lazy consumers Move the MCP App catalog out of startup English and register it synchronously at every consumer. Keep the shared namespace anchor and host catalog composition so all text and source order remain byte-identical. i18n verification passes without baseline changes. Startup gzip: 373613 -> 373103 B, 351 B below the unchanged 373454 B limit. * fix(ui): intercept only well-formed MCP app links Require the plugin/app/tool shape before cancelling browser navigation so ordinary ChatGPT plugin pages keep their default behavior. Preserve lazy strict parsing and accepted native and web app links. * fix(gateway): watch MCP app files directly so loaded macOS hosts still notify Watch the bound file to avoid FSEvents directory event drops under load. Re-arm on atomic replacement, retry ENOENT once on the next immediate turn, and retain subscription authority and cleanup. Cover replacement followed by a plain write through the registered resource routes. * fix(gateway): use the errno guard when rearming MCP app watches Use the shared filesystem error guard instead of an unchecked type assertion. Keep the single immediate ENOENT retry and all subscription behavior unchanged. * fix(codex): read the canonical MCP transport field Main migrates MCP transport aliases before runtime (#162256) and retired the SDK re-export of the alias resolver, so the native app catalog reads the canonical server.transport field the same way the agentsapi plugin does. * fix(gateway): keep MCP app file subscriptions alive across rename gaps Keep resource subscriptions alive while editors move the old file aside before installing its replacement. Poll missing paths at 250 ms, return to the inode watcher when the file reappears, and release polling on subscription close. Recheck after polling registration to cover replacement before its first stat; share the rearm guard so late callbacks cannot leak watchers. * test(codex): type the rooted thread policy support from attempt fixtures The rooted policy support that main added types its lifecycle input from the raw thread signature, while this branch's binding fixtures carry full attempt params. Derive it from the shared attempt-thread fixture type, as the sibling policy-refresh support already does. * test(ui): exercise the MCP app link probe through the click boundary The eager link probe was exported only for its unit test, which the production dead-export scan rejects. Keep it module-private and assert the same accepted and rejected links through real click interception. * style(lint): clear MCP app lint errors CI run 36988865432 jobs check-lint-core-1 and check-lint-core-2 rejected shadowed stat callback variables, a returning Promise executor, and reassigned projection parameters. Rename the inner variables and use local projection bindings and an executor block without changing behavior. * test(agents): align bundle MCP fixture ownership CI run 36988865432 job checks-node-changed-compact-large-14 failed seven merge cases because the fixture omitted the loader-required pluginIdsByServer map. Type the fixture against the producer contract and verify ownership survives only for unshadowed enabled bundle servers, preserving the migrated transport behavior. * fix(auto-reply): register turns before MCP context leasing CI run 36988865432 job checks-node-changed-compact-large-33-2 exposed an asynchronous MCP lease before synchronous run registration. Prepare App context after ownership registration and image admission, revalidate requester authority around the lease, and retain commit and rollback settlement with the execution outcome owner. * test(gateway): admit MCP shutdown requests through current policy CI run 36988865432 job checks-node-changed-compact-large-9 timed out because the fixture ignored an admission rejection before its upstream call. Publish matching Gateway and runtime config, create the session through its RPC owner, and observe early request settlement while preserving all shutdown ordering and cleanup assertions. * refactor(apple): drop the label-only question toggle Periphery flagged toggleOption(questionID🏷️) as dead: production selects by canonical value since the rich-form change, and only tests still called the label overload. Tests now toggle by value; the ambiguous-label case becomes an unknown-value no-op. --------- Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
395 lines
14 KiB
TypeScript
395 lines
14 KiB
TypeScript
import { promises as fs } from "node:fs";
|
|
import path from "node:path";
|
|
import { ProtocolSchemas } from "../packages/gateway-protocol/src/schema/protocol-schemas.js";
|
|
import {
|
|
MIN_NODE_PROTOCOL_VERSION,
|
|
PROTOCOL_VERSION,
|
|
} from "../packages/gateway-protocol/src/version.js";
|
|
import { listCoreGatewayMethodNames } from "../src/gateway/methods/core-method-policy.js";
|
|
import { extractGatewayEventNames } from "./check-protocol-event-coverage.mts";
|
|
import { lowerCamel, upperCamel } from "./lib/protocol-codegen-names.mts";
|
|
import { type JsonSchema, schemaSignature } from "./lib/protocol-codegen-schema.js";
|
|
|
|
type EnumSpec = {
|
|
name: string;
|
|
values: Array<{ name: string; rawValue: string }>;
|
|
namespacePrefix?: string;
|
|
};
|
|
|
|
const protocolSchemas = ProtocolSchemas as Record<string, JsonSchema>;
|
|
|
|
const schemaNames = new Map<string, string>([
|
|
["ErrorShape", "GatewayProtocolError"],
|
|
["RequestFrame", "GatewayRequestFrame"],
|
|
["ResponseFrame", "GatewayResponseFrame"],
|
|
["EventFrame", "GatewayEventFrame"],
|
|
["NodeEventParams", "GatewayNodeEventParams"],
|
|
["NodeInvokeResultParams", "GatewayNodeInvokeResultParams"],
|
|
["NodeInvokeRequestEvent", "GatewayNodeInvokeRequest"],
|
|
["QuestionOption", "QuestionOption"],
|
|
["Question", "Question"],
|
|
["QuestionAnswers", "QuestionAnswers"],
|
|
["QuestionRecord", "QuestionRecord"],
|
|
["QuestionGetResult", "QuestionGetResult"],
|
|
["QuestionListResult", "QuestionListResult"],
|
|
["MessageReactionSummary", "MessageReactionSummary"],
|
|
["SessionReactionsListParams", "SessionReactionsListParams"],
|
|
["SessionReactionsSetParams", "SessionReactionsSetParams"],
|
|
["SessionReactionsListResult", "SessionReactionsListResult"],
|
|
["SessionReactionsSetResult", "SessionReactionsSetResult"],
|
|
["SessionReactionEvent", "SessionReactionEvent"],
|
|
["SessionObserverPlanProgress", "SessionObserverPlanProgress"],
|
|
["SessionObserverDigest", "SessionObserverDigest"],
|
|
["WorkerDesktopObserveParams", "WorkerDesktopObserveParams"],
|
|
["WorkerDesktopObserveResult", "WorkerDesktopObserveResult"],
|
|
["WorkerDesktopLaunchParams", "WorkerDesktopLaunchParams"],
|
|
["WorkerDesktopLaunchResult", "WorkerDesktopLaunchResult"],
|
|
["ProjectsListResult", "ProjectsListResult"],
|
|
["GitHubIdentityFacts", "GitHubIdentityFacts"],
|
|
["GitHubSelectedIdentity", "GitHubSelectedIdentity"],
|
|
["ToolsGitHubStatusParams", "ToolsGitHubStatusParams"],
|
|
["ToolsGitHubStatusResult", "ToolsGitHubStatusResult"],
|
|
["ToolsGitHubAuthorizeStartParams", "ToolsGitHubAuthorizeStartParams"],
|
|
["ToolsGitHubAuthorizeStartResult", "ToolsGitHubAuthorizeStartResult"],
|
|
["ToolsGitHubAuthorizePollParams", "ToolsGitHubAuthorizePollParams"],
|
|
["ToolsGitHubAuthorizePendingResult", "ToolsGitHubAuthorizePendingResult"],
|
|
["ToolsGitHubAuthorizeSlowDownResult", "ToolsGitHubAuthorizeSlowDownResult"],
|
|
["ToolsGitHubAuthorizeAccessDeniedResult", "ToolsGitHubAuthorizeAccessDeniedResult"],
|
|
["ToolsGitHubAuthorizeExpiredResult", "ToolsGitHubAuthorizeExpiredResult"],
|
|
[
|
|
"ToolsGitHubAuthorizeIncorrectDeviceCodeResult",
|
|
"ToolsGitHubAuthorizeIncorrectDeviceCodeResult",
|
|
],
|
|
["ToolsGitHubAuthorizeNetworkErrorResult", "ToolsGitHubAuthorizeNetworkErrorResult"],
|
|
["ToolsGitHubAuthorizeFailedResult", "ToolsGitHubAuthorizeFailedResult"],
|
|
["ToolsGitHubAuthorizeSuccessResult", "ToolsGitHubAuthorizeSuccessResult"],
|
|
["ToolsGitHubAuthorizePollResult", "ToolsGitHubAuthorizePollResult"],
|
|
["ToolsGitHubAuthorizeCancelParams", "ToolsGitHubAuthorizeCancelParams"],
|
|
["ToolsGitHubAuthorizeCancelResult", "ToolsGitHubAuthorizeCancelResult"],
|
|
["SessionGitHubPublicationRequested", "SessionGitHubPublicationRequested"],
|
|
["SessionGitHubPublicationPublishing", "SessionGitHubPublicationPublishing"],
|
|
["SessionGitHubPublicationPublished", "SessionGitHubPublicationPublished"],
|
|
["SessionGitHubPublicationFailed", "SessionGitHubPublicationFailed"],
|
|
["SessionGitHubPublicationNeedsConfirmation", "SessionGitHubPublicationNeedsConfirmation"],
|
|
["SessionGitHubPublicationResult", "SessionGitHubPublicationResult"],
|
|
["TalkSessionCancelOutputResult", "TalkSessionCancelOutputResult"],
|
|
]);
|
|
|
|
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"],
|
|
["VoiceWake", "voiceWake"],
|
|
["MobileUI", "mobileUI"],
|
|
]),
|
|
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"]]),
|
|
enumSpec("OpenClawMobileUiCommand", "mobile.ui.", [
|
|
["Observe", "observe"],
|
|
["Act", "act"],
|
|
]),
|
|
];
|
|
|
|
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 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);
|
|
}
|
|
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<JsonSchema, string>();
|
|
const selectedSignatures = new Map<string, string>();
|
|
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<string, JsonSchema>();
|
|
const unionVariants = new Map<
|
|
string,
|
|
{ discriminator: string; literal: string; unionName: string }
|
|
>();
|
|
const discriminatedUnions = new Map<string, string>();
|
|
for (const [schemaName, kotlinName] of schemaNames) {
|
|
const schema = protocolSchemas[schemaName];
|
|
const branches = schema?.oneOf ?? schema?.anyOf;
|
|
if (!branches || branches.length < 2 || branches.some((branch) => branch.type !== "object")) {
|
|
continue;
|
|
}
|
|
const discriminator = Object.keys(branches[0]?.properties ?? {}).find((property) =>
|
|
branches.every(
|
|
(branch) => typeof literalValue(branch.properties?.[property] ?? {}) === "string",
|
|
),
|
|
);
|
|
if (!discriminator) {
|
|
continue;
|
|
}
|
|
discriminatedUnions.set(kotlinName, discriminator);
|
|
for (const branch of branches) {
|
|
const signature = schemaSignature(branch);
|
|
const literal = literalValue(branch.properties?.[discriminator] ?? {}) as string;
|
|
if (!selectedSignatures.has(signature)) {
|
|
throw new Error(
|
|
`${schemaName} variant ${JSON.stringify(literal)} must be selected for Kotlin generation`,
|
|
);
|
|
}
|
|
unionVariants.set(signature, {
|
|
discriminator,
|
|
literal,
|
|
unionName: kotlinName,
|
|
});
|
|
}
|
|
}
|
|
const kotlinType = (schema: JsonSchema, nestedName: string): string => {
|
|
const selected = selectedSchemas.get(schema) ?? selectedSignatures.get(schemaSignature(schema));
|
|
if (selected) {
|
|
return selected;
|
|
}
|
|
const stringUnion = schema.anyOf?.map(literalValue);
|
|
if (stringUnion?.length && stringUnion.every((value) => typeof value === "string")) {
|
|
return "String";
|
|
}
|
|
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<String, ${kotlinType(valueSchema, `${nestedName}Value`)}>`;
|
|
}
|
|
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 variant = unionVariants.get(schemaSignature(schema));
|
|
const fields = Object.entries(schema.properties)
|
|
.filter(([wireName]) => wireName !== variant?.discriminator)
|
|
.flatMap(([wireName, propertySchema]) => {
|
|
const propertyName = lowerCamel(wireName);
|
|
const type = kotlinType(propertySchema, `${name}${upperCamel(wireName)}`);
|
|
const literal = literalValue(propertySchema);
|
|
const optional = !required.has(wireName);
|
|
const useLiteralDefault =
|
|
literal !== undefined && !optional && typeof literal !== "boolean";
|
|
const lines = [
|
|
` val ${propertyName}: ${type}${optional ? "?" : ""}${
|
|
useLiteralDefault ? ` = ${kotlinLiteral(literal)}` : optional ? " = null" : ""
|
|
},`,
|
|
];
|
|
if (propertyName !== wireName) {
|
|
lines.unshift(` @SerialName(${JSON.stringify(wireName)})`);
|
|
}
|
|
return lines;
|
|
});
|
|
if (fields.length === 0 && variant) {
|
|
return [
|
|
`@SerialName(${JSON.stringify(variant.literal)})`,
|
|
"@Serializable",
|
|
`data object ${name} : ${variant.unionName}`,
|
|
].join("\n");
|
|
}
|
|
return [
|
|
...(variant ? [`@SerialName(${JSON.stringify(variant.literal)})`] : []),
|
|
"@Serializable",
|
|
`data class ${name}(`,
|
|
...fields,
|
|
`)${variant ? ` : ${variant.unionName}` : ""}`,
|
|
].join("\n");
|
|
};
|
|
|
|
const emitUnion = (name: string, discriminator: string): string =>
|
|
[
|
|
"@OptIn(ExperimentalSerializationApi::class)",
|
|
"@Serializable",
|
|
`@JsonClassDiscriminator(${JSON.stringify(discriminator)})`,
|
|
`sealed interface ${name}`,
|
|
].join("\n");
|
|
|
|
const output: string[] = [];
|
|
for (const [schemaName, kotlinName] of schemaNames) {
|
|
const union = discriminatedUnions.get(kotlinName);
|
|
output.push(
|
|
union ? emitUnion(kotlinName, union) : 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<string, string>();
|
|
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 });
|
|
}
|
|
|
|
export async function generateKotlinProtocol(repoRoot: string): Promise<Record<string, string>> {
|
|
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 = listCoreGatewayMethodNames();
|
|
|
|
const gatewayContent = [
|
|
"// Generated by scripts/protocol-gen-kotlin.ts — do not edit by hand.",
|
|
"package ai.openclaw.app.gateway",
|
|
"",
|
|
"import kotlinx.serialization.ExperimentalSerializationApi",
|
|
"import kotlinx.serialization.SerialName",
|
|
"import kotlinx.serialization.Serializable",
|
|
"import kotlinx.serialization.json.JsonClassDiscriminator",
|
|
"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");
|
|
|
|
return {
|
|
"ai/openclaw/app/gateway/GatewayProtocol.kt": gatewayContent,
|
|
"ai/openclaw/app/protocol/OpenClawProtocolConstants.kt": constantsContent,
|
|
};
|
|
}
|