From 4f256a6236b09d49d86abe4ce70417cc5aa5c904 Mon Sep 17 00:00:00 2001 From: Boris Starkov Date: Wed, 19 Nov 2025 14:04:26 +0000 Subject: [PATCH 1/2] snake case adjustments --- src/__tests__/utils.test.ts | 91 ++++++++++++++++++++++++++++++------- src/shared/utils.ts | 24 ++++++---- 2 files changed, 88 insertions(+), 27 deletions(-) diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index ed0bf40..b09bdfd 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -294,7 +294,7 @@ describe("Utils", () => { }); }); - it("should preserve secret_id and other keys in request_headers object format", () => { + it("should preserve header names but convert nested object keys in request_headers", () => { const input = { conversation_config: { agent: { @@ -333,11 +333,11 @@ describe("Utils", () => { url: "https://example.com/webhook", method: "GET", requestHeaders: { - "Content-Type": "application/json", - "X-Api-Key": { - secret_id: "abc" // Should NOT be converted to secretId + "Content-Type": "application/json", // Header name preserved + "X-Api-Key": { // Header name preserved + secretId: "abc" // BUT nested key IS converted }, - "foo_bar": "baz" // Should NOT be converted to fooBar + "foo_bar": "baz" // Header name preserved (string value, no nested conversion) } } } @@ -366,7 +366,64 @@ describe("Utils", () => { }); }); - it("should preserve request_headers in toSnakeCaseKeys for symmetry", () => { + it("should preserve header names but convert secretId to snake_case in workspace_overrides", () => { + const input = { + workspace_overrides: { + conversation_initiation_client_data_webhook: { + request_headers: { + "x-elevenlabs-hoxhunt-token": { + secret_id: "test-secret-123" + } + } + } + } + }; + + const result = toCamelCaseKeys(input); + + expect(result).toEqual({ + workspaceOverrides: { + conversationInitiationClientDataWebhook: { + requestHeaders: { + "x-elevenlabs-hoxhunt-token": { // Header name preserved + secretId: "test-secret-123" // Nested key converted to camelCase + } + } + } + } + }); + }); + + it("should preserve header names but convert secretId to snake_case when converting from API", () => { + // Simulating API response (camelCase) + const input = { + workspaceOverrides: { + conversationInitiationClientDataWebhook: { + requestHeaders: { + "x-elevenlabs-hoxhunt-token": { + secretId: "test-secret-123" + } + } + } + } + }; + + const result = toSnakeCaseKeys(input); + + expect(result).toEqual({ + workspace_overrides: { + conversation_initiation_client_data_webhook: { + request_headers: { + "x-elevenlabs-hoxhunt-token": { // Header name preserved + secret_id: "test-secret-123" // Nested key converted to snake_case + } + } + } + } + }); + }); + + it("should preserve header names but convert nested object keys in requestHeaders for toSnakeCaseKeys", () => { const input = { conversationConfig: { agent: { @@ -407,12 +464,12 @@ describe("Utils", () => { url: "https://example.com/webhook", method: "GET", request_headers: { - "Content-Type": "application/json", - "X-Api-Key": { - secretId: "abc" // Should NOT be converted to secret_id + "Content-Type": "application/json", // Header name preserved + "X-Api-Key": { // Header name preserved + secret_id: "abc" // BUT nested key IS converted }, - "Authorization": { - variableName: "auth_token" // Should NOT be converted to variable_name + "Authorization": { // Header name preserved + variable_name: "auth_token" // BUT nested key IS converted } } } @@ -424,7 +481,7 @@ describe("Utils", () => { }); }); - it("should maintain round-trip conversion symmetry for request_headers", () => { + it("should maintain round-trip conversion symmetry for request_headers with proper key conversion", () => { const original = { conversation_config: { agent: { @@ -438,7 +495,7 @@ describe("Utils", () => { request_headers: { "Content-Type": "application/json", "X-Api-Key": { - secretId: "my_secret_123" + secret_id: "my_secret_123" } } } @@ -455,7 +512,7 @@ describe("Utils", () => { // Simulate push (local file → API): snake_case → camelCase const afterPush = toCamelCaseKeys(afterPull); - // After round-trip, request_headers internals should be preserved + // After round-trip, header names preserved but nested keys converted expect(afterPush).toEqual({ conversationConfig: { agent: { @@ -467,9 +524,9 @@ describe("Utils", () => { url: "https://example.com/webhook", method: "POST", requestHeaders: { - "Content-Type": "application/json", - "X-Api-Key": { - secretId: "my_secret_123" // Should be preserved through round-trip + "Content-Type": "application/json", // Header name preserved + "X-Api-Key": { // Header name preserved + secretId: "my_secret_123" // Nested key converted through round-trip } } } diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 656c122..3196cd8 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -118,20 +118,22 @@ function toSnakeCaseKey(key: string): string { .toLowerCase(); } -export function toCamelCaseKeys(value: T, skipHeaderConversion = false): T { +export function toCamelCaseKeys(value: T, skipHeaderConversion: boolean | 'names-only' = false): T { if (Array.isArray(value)) { return (value.map((v) => toCamelCaseKeys(v, skipHeaderConversion)) as unknown) as T; } if (isPlainObject(value)) { const result: Record = {}; for (const [k, v] of Object.entries(value)) { - if (skipHeaderConversion) { - // Inside request_headers: preserve all keys as-is to avoid converting - // header names like "X-Api-Key" or nested keys like "secret_id" + if (skipHeaderConversion === true) { + // Deep inside request_headers: preserve all keys (for backwards compatibility with arrays) result[k] = toCamelCaseKeys(v, true); + } else if (skipHeaderConversion === 'names-only') { + // Inside request_headers object: preserve header names (keys) but convert nested objects + result[k] = toCamelCaseKeys(v, false); } else { // Normal conversion - result[toCamelCaseKey(k)] = toCamelCaseKeys(v, k === 'request_headers'); + result[toCamelCaseKey(k)] = toCamelCaseKeys(v, k === 'request_headers' ? 'names-only' : false); } } return (result as unknown) as T; @@ -139,20 +141,22 @@ export function toCamelCaseKeys(value: T, skipHeaderConversion = fa return value; } -export function toSnakeCaseKeys(value: T, skipHeaderConversion = false): T { +export function toSnakeCaseKeys(value: T, skipHeaderConversion: boolean | 'names-only' = false): T { if (Array.isArray(value)) { return (value.map((v) => toSnakeCaseKeys(v, skipHeaderConversion)) as unknown) as T; } if (isPlainObject(value)) { const result: Record = {}; for (const [k, v] of Object.entries(value)) { - if (skipHeaderConversion) { - // Inside request_headers/requestHeaders: preserve all keys as-is to avoid converting - // header names like "X-Api-Key" or nested keys like "secret_id" + if (skipHeaderConversion === true) { + // Deep inside request_headers: preserve all keys (for backwards compatibility with arrays) result[k] = toSnakeCaseKeys(v, true); + } else if (skipHeaderConversion === 'names-only') { + // Inside request_headers object: preserve header names (keys) but convert nested objects + result[k] = toSnakeCaseKeys(v, false); } else { // Normal conversion - result[toSnakeCaseKey(k)] = toSnakeCaseKeys(v, k === 'request_headers' || k === 'requestHeaders'); + result[toSnakeCaseKey(k)] = toSnakeCaseKeys(v, k === 'request_headers' || k === 'requestHeaders' ? 'names-only' : false); } } return (result as unknown) as T; From 8758b966f91d1bdf18eef8e66e5906bc74459f03 Mon Sep 17 00:00:00 2001 From: Boris Starkov Date: Wed, 19 Nov 2025 14:22:33 +0000 Subject: [PATCH 2/2] pull workflows in interactive mode --- src/agents/ui/PullView.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/agents/ui/PullView.tsx b/src/agents/ui/PullView.tsx index a0ddea6..2ec25b8 100644 --- a/src/agents/ui/PullView.tsx +++ b/src/agents/ui/PullView.tsx @@ -216,21 +216,28 @@ export const PullView: React.FC = ({ conversation_config?: Record; platformSettings?: Record; platform_settings?: Record; + workflow?: unknown; tags?: string[]; }; const conversationConfig = agentDetailsTyped.conversationConfig || agentDetailsTyped.conversation_config || {}; const platformSettings = agentDetailsTyped.platformSettings || agentDetailsTyped.platform_settings || {}; + const workflow = agentDetailsTyped.workflow; const tags = agentDetailsTyped.tags || []; // Create agent config structure (without agent_id - it goes in index file) - const agentConfig = { + const agentConfig: any = { name: agentName, conversation_config: conversationConfig, platform_settings: platformSettings, tags }; + // Only include workflow if it exists + if (workflow !== undefined && workflow !== null) { + agentConfig.workflow = workflow; + } + let configPath: string; if (agent.action === 'update' && existingEntry && existingEntry.config) {