From e8ccd4b573a62aeb4be4e33d2517acf24d48239e Mon Sep 17 00:00:00 2001 From: Bradymck Date: Sun, 17 May 2026 21:13:55 -0600 Subject: [PATCH] fix(assign): send matching_capabilities as single CSV field Server drops all but the last value when multiple form fields with the same name are sent. Fixes openhome assign wiping all but one capability. Also adds --template flag to openhome deploy as a workaround for the cloud router ignoring CLI-deployed abilities (template: null issue). See #14 for context and the proper server-side fix. Fixes #14 (assign bug). References #14 (template workaround). Co-Authored-By: Claude Sonnet 4.6 --- src/api/client.ts | 7 ++++--- src/api/contracts.ts | 1 + src/cli.ts | 5 +++++ src/commands/deploy.ts | 6 ++++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/api/client.ts b/src/api/client.ts index 9268c21..d0c678f 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -267,6 +267,9 @@ export class ApiClient implements IApiClient { if (metadata.personality_id) { form.append("personality_id", metadata.personality_id); } + if (metadata.template !== undefined) { + form.append("template", String(metadata.template)); + } const url = `${this.baseUrl}${ENDPOINTS.uploadCapability}`; @@ -438,9 +441,7 @@ export class ApiClient implements IApiClient { // Uses multipart/form-data — JSON is rejected const form = new FormData(); form.append("personality_id", personalityId); - for (const capId of capabilityIds) { - form.append("matching_capabilities", String(capId)); - } + form.append("matching_capabilities", capabilityIds.join(",")); return this.request( ENDPOINTS.editPersonality, { method: "PUT", body: form }, diff --git a/src/api/contracts.ts b/src/api/contracts.ts index 39d816a..c33d924 100644 --- a/src/api/contracts.ts +++ b/src/api/contracts.ts @@ -29,6 +29,7 @@ export interface UploadAbilityMetadata { category: AbilityCategory; matching_hotwords: string[]; personality_id?: string; + template?: number; } export interface UploadAbilityResponse { diff --git a/src/cli.ts b/src/cli.ts index a5ca3b2..251d832 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -302,6 +302,10 @@ program ) .option("--json", "Output machine-readable JSON") .option("--mock", "Use mock API client (no real network calls)") + .option( + "--template ", + "Template ID to associate with the ability (workaround for cloud router — see issue #14)", + ) .action( async ( path: string | undefined, @@ -314,6 +318,7 @@ program triggers?: string; timeout?: string; json?: boolean; + template?: string; }, ) => { await deployCommand(path, opts); diff --git a/src/commands/deploy.ts b/src/commands/deploy.ts index 5f607c9..9e8c9e1 100644 --- a/src/commands/deploy.ts +++ b/src/commands/deploy.ts @@ -59,6 +59,7 @@ export async function deployCommand( triggers?: string; json?: boolean; timeout?: string; // seconds as string from commander + template?: string; } = {}, ): Promise { if (!opts.json) p.intro("🚀 Upload Ability"); @@ -218,12 +219,17 @@ export async function deployCommand( const personalityId = opts.personality ?? getConfig().default_personality_id; + const templateId = opts.template ? parseInt(opts.template, 10) : undefined; + const metadata: UploadAbilityMetadata = { name, description, category, matching_hotwords: hotwords, personality_id: personalityId, + ...(templateId !== undefined && !Number.isNaN(templateId) + ? { template: templateId } + : {}), }; let zipBuffer: Buffer;