retell-sync-cli 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,30 +1,25 @@
1
1
  {
2
2
  "name": "retell-sync-cli",
3
- "author": "zachsents",
4
- "version": "1.1.0",
3
+ "version": "2.0.0",
5
4
  "description": "CLI tool for syncing Retell AI agents between local filesystem and API",
6
- "type": "module",
7
- "main": "cli.ts",
5
+ "keywords": [
6
+ "agents",
7
+ "ai",
8
+ "cli",
9
+ "retell",
10
+ "sync"
11
+ ],
12
+ "author": "zachsents",
8
13
  "bin": {
9
14
  "retell": "./dist/cli.js"
10
15
  },
16
+ "type": "module",
17
+ "main": "cli.ts",
11
18
  "scripts": {
12
19
  "check": "bunx tsc --noEmit && bun fix",
13
- "fix": "biome check --write",
20
+ "fix": "oxlint --type-aware --fix-suggestions && prettier . --write",
14
21
  "build": "bun build ./src/cli.ts --target bun --outdir dist"
15
22
  },
16
- "keywords": [
17
- "retell",
18
- "ai",
19
- "cli",
20
- "sync",
21
- "agents"
22
- ],
23
- "devDependencies": {
24
- "@biomejs/biome": "^2.3.10",
25
- "@types/bun": "latest",
26
- "type-fest": "^5.3.1"
27
- },
28
23
  "dependencies": {
29
24
  "@inquirer/prompts": "^8.1.0",
30
25
  "boxen": "^8.0.1",
@@ -39,5 +34,12 @@
39
34
  "retell-sdk": "^4.66.0",
40
35
  "yaml": "^2.8.2",
41
36
  "zod": "^4.2.1"
37
+ },
38
+ "devDependencies": {
39
+ "@types/bun": "latest",
40
+ "oxlint": "^1.35.0",
41
+ "oxlint-tsgolint": "^0.10.0",
42
+ "prettier-plugin-jsdoc": "^1.8.0",
43
+ "type-fest": "^5.3.1"
42
44
  }
43
45
  }
package/biome.json DELETED
@@ -1,47 +0,0 @@
1
- {
2
- "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json",
3
- "vcs": {
4
- "enabled": true,
5
- "clientKind": "git",
6
- "useIgnoreFile": true
7
- },
8
- "files": {
9
- "ignoreUnknown": true,
10
- "includes": ["**", "!test/*.json"]
11
- },
12
- "formatter": {
13
- "enabled": true,
14
- "indentStyle": "space",
15
- "indentWidth": 2
16
- },
17
- "linter": {
18
- "enabled": true,
19
- "rules": {
20
- "recommended": true,
21
- "suspicious": {
22
- "noArrayIndexKey": "off",
23
- "noConfusingVoidType": "off"
24
- },
25
- "style": {
26
- "noNonNullAssertion": "off"
27
- },
28
- "complexity": {
29
- "noCommaOperator": "off"
30
- }
31
- }
32
- },
33
- "javascript": {
34
- "formatter": {
35
- "quoteStyle": "double",
36
- "semicolons": "asNeeded"
37
- }
38
- },
39
- "assist": {
40
- "enabled": true,
41
- "actions": {
42
- "source": {
43
- "organizeImports": "on"
44
- }
45
- }
46
- }
47
- }
@@ -1,450 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test"
2
- import fs from "node:fs/promises"
3
- import os from "node:os"
4
- import path from "node:path"
5
- import { $ } from "bun"
6
- import { getBaselineState } from "../src/lib/agents"
7
-
8
- describe("getBaselineState", () => {
9
- let tmpDir: string
10
- let originalCwd: string
11
-
12
- beforeEach(async () => {
13
- originalCwd = process.cwd()
14
- tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "retell-sync-test-"))
15
- process.chdir(tmpDir)
16
-
17
- // Initialize git repo
18
- await $`git init`.quiet()
19
- await $`git config user.email "test@test.com"`.quiet()
20
- await $`git config user.name "Test"`.quiet()
21
- })
22
-
23
- afterEach(async () => {
24
- process.chdir(originalCwd)
25
- await fs.rm(tmpDir, { recursive: true, force: true })
26
- })
27
-
28
- test("hydrates retell-llm agent with file placeholders resolved", async () => {
29
- const agentsDir = "agents"
30
- const agentDir = `${agentsDir}/test_llm_agent_abc123`
31
-
32
- // Create agent files
33
- await fs.mkdir(path.join(tmpDir, agentDir), { recursive: true })
34
-
35
- await Bun.write(
36
- path.join(tmpDir, agentDir, ".agent.json"),
37
- JSON.stringify({
38
- id: "agent_abc123456789",
39
- version: 1,
40
- responseEngineVersion: 1,
41
- }),
42
- )
43
-
44
- await Bun.write(
45
- path.join(tmpDir, agentDir, "config.json"),
46
- JSON.stringify(
47
- {
48
- channel: "voice",
49
- agent_name: "Test LLM Agent",
50
- response_engine: {
51
- type: "retell-llm",
52
- llm_id: "llm_test123",
53
- version: 1,
54
- },
55
- voice_id: "11labs-Chloe",
56
- },
57
- null,
58
- 2,
59
- ),
60
- )
61
-
62
- await Bun.write(
63
- path.join(tmpDir, agentDir, "llm.json"),
64
- JSON.stringify(
65
- {
66
- model: "gpt-4.1",
67
- general_prompt: "file://./general_prompt.md",
68
- begin_message: "Hello!",
69
- },
70
- null,
71
- 2,
72
- ),
73
- )
74
-
75
- await Bun.write(
76
- path.join(tmpDir, agentDir, "general_prompt.md"),
77
- `---
78
- some_meta: value
79
- ---
80
-
81
- You are a helpful assistant.
82
-
83
- Be friendly and professional.
84
- `,
85
- )
86
-
87
- // Commit the files
88
- await $`git add .`.quiet()
89
- await $`git commit -m "Add test agent"`.quiet()
90
- const { stdout } = await $`git rev-parse HEAD`.quiet()
91
- const commitHash = stdout.toString().trim()
92
-
93
- // Create .sync.json
94
- await Bun.write(
95
- path.join(tmpDir, ".sync.json"),
96
- JSON.stringify({ baseline: { commitHash } }, null, 2),
97
- )
98
-
99
- // Run getBaselineState
100
- const result = await getBaselineState({ agentsDir })
101
-
102
- // Verify results
103
- expect(result.voiceAgents).toHaveLength(1)
104
- expect(result.llms).toHaveLength(1)
105
- expect(result.conversationFlows).toHaveLength(0)
106
-
107
- const agent = result.voiceAgents[0]!
108
- expect(agent.agent_name).toBe("Test LLM Agent")
109
- expect(agent.response_engine.type).toBe("retell-llm")
110
- expect(agent.voice_id).toBe("11labs-Chloe")
111
-
112
- const llm = result.llms[0]!
113
- expect(llm.model).toBe("gpt-4.1")
114
- expect(llm.begin_message).toBe("Hello!")
115
- // Verify file placeholder was resolved
116
- expect(llm.general_prompt).toContain("You are a helpful assistant.")
117
- expect(llm.general_prompt).toContain("Be friendly and professional.")
118
- // Frontmatter should be stripped
119
- expect(llm.general_prompt).not.toContain("some_meta")
120
- })
121
-
122
- test("hydrates conversation-flow agent with nested file placeholders", async () => {
123
- const agentsDir = "agents"
124
- const agentDir = `${agentsDir}/test_flow_agent_def456`
125
-
126
- // Create agent files
127
- await fs.mkdir(path.join(tmpDir, agentDir, "nodes"), { recursive: true })
128
-
129
- await Bun.write(
130
- path.join(tmpDir, agentDir, ".agent.json"),
131
- JSON.stringify({
132
- id: "agent_def456789012",
133
- version: 2,
134
- responseEngineVersion: 2,
135
- }),
136
- )
137
-
138
- await Bun.write(
139
- path.join(tmpDir, agentDir, "config.json"),
140
- JSON.stringify(
141
- {
142
- channel: "voice",
143
- agent_name: "Test Flow Agent",
144
- response_engine: {
145
- type: "conversation-flow",
146
- conversation_flow_id: "flow_test456",
147
- version: 2,
148
- },
149
- voice_id: "openai-Alloy",
150
- },
151
- null,
152
- 2,
153
- ),
154
- )
155
-
156
- await Bun.write(
157
- path.join(tmpDir, agentDir, "conversation-flow.json"),
158
- JSON.stringify(
159
- {
160
- global_prompt: "file://./global_prompt.md",
161
- nodes: [
162
- {
163
- id: "start-node",
164
- name: "Welcome",
165
- type: "conversation",
166
- instruction: {
167
- type: "static_text",
168
- text: "Hello, welcome!",
169
- },
170
- edges: [],
171
- },
172
- {
173
- id: "node-inquiry",
174
- name: "Handle Inquiry",
175
- type: "conversation",
176
- instruction: {
177
- type: "prompt",
178
- text: "file://./nodes/inquiry_abc123.md",
179
- },
180
- edges: [],
181
- },
182
- ],
183
- },
184
- null,
185
- 2,
186
- ),
187
- )
188
-
189
- await Bun.write(
190
- path.join(tmpDir, agentDir, "global_prompt.md"),
191
- `---
192
- version: 1
193
- ---
194
-
195
- You are a professional receptionist.
196
-
197
- Always be polite and helpful.
198
- `,
199
- )
200
-
201
- await Bun.write(
202
- path.join(tmpDir, agentDir, "nodes", "inquiry_abc123.md"),
203
- `---
204
- nodeId: "node-inquiry"
205
- name: "Handle Inquiry"
206
- ---
207
-
208
- Answer the caller's question to the best of your ability.
209
-
210
- If you don't know, offer to transfer them.
211
- `,
212
- )
213
-
214
- // Commit the files
215
- await $`git add .`.quiet()
216
- await $`git commit -m "Add flow agent"`.quiet()
217
- const { stdout } = await $`git rev-parse HEAD`.quiet()
218
- const commitHash = stdout.toString().trim()
219
-
220
- // Create .sync.json
221
- await Bun.write(
222
- path.join(tmpDir, ".sync.json"),
223
- JSON.stringify({ baseline: { commitHash } }, null, 2),
224
- )
225
-
226
- // Run getBaselineState
227
- const result = await getBaselineState({ agentsDir })
228
-
229
- // Verify results
230
- expect(result.voiceAgents).toHaveLength(1)
231
- expect(result.llms).toHaveLength(0)
232
- expect(result.conversationFlows).toHaveLength(1)
233
-
234
- const agent = result.voiceAgents[0]!
235
- expect(agent.agent_name).toBe("Test Flow Agent")
236
- expect(agent.response_engine.type).toBe("conversation-flow")
237
-
238
- const flow = result.conversationFlows[0]!
239
- // Global prompt placeholder should be resolved
240
- expect(flow.global_prompt).toContain("You are a professional receptionist.")
241
- expect(flow.global_prompt).toContain("Always be polite and helpful.")
242
- expect(flow.global_prompt).not.toContain("version: 1")
243
-
244
- // Node instruction placeholder should be resolved
245
- const inquiryNode = flow.nodes?.find((n) => n.id === "node-inquiry")
246
- expect(inquiryNode).toBeDefined()
247
- expect(
248
- inquiryNode?.type === "conversation" && inquiryNode.instruction.text,
249
- ).toContain("Answer the caller's question")
250
- expect(
251
- inquiryNode?.type === "conversation" && inquiryNode.instruction.text,
252
- ).toContain("offer to transfer them")
253
- // Frontmatter should be stripped from node
254
- expect(
255
- inquiryNode?.type === "conversation" && inquiryNode.instruction.text,
256
- ).not.toContain("nodeId")
257
- })
258
-
259
- test("handles multiple agents of different types", async () => {
260
- const agentsDir = "agents"
261
-
262
- // Create LLM agent
263
- const llmAgentDir = `${agentsDir}/llm_agent_111111`
264
- await fs.mkdir(path.join(tmpDir, llmAgentDir), { recursive: true })
265
- await Bun.write(
266
- path.join(tmpDir, llmAgentDir, ".agent.json"),
267
- JSON.stringify({
268
- id: "agent_llm_1",
269
- version: 1,
270
- responseEngineVersion: 1,
271
- }),
272
- )
273
- await Bun.write(
274
- path.join(tmpDir, llmAgentDir, "config.json"),
275
- JSON.stringify({
276
- agent_name: "LLM Agent",
277
- response_engine: { type: "retell-llm", llm_id: "llm_1", version: 1 },
278
- }),
279
- )
280
- await Bun.write(
281
- path.join(tmpDir, llmAgentDir, "llm.json"),
282
- JSON.stringify({ model: "gpt-4" }),
283
- )
284
-
285
- // Create Flow agent
286
- const flowAgentDir = `${agentsDir}/flow_agent_222222`
287
- await fs.mkdir(path.join(tmpDir, flowAgentDir), { recursive: true })
288
- await Bun.write(
289
- path.join(tmpDir, flowAgentDir, ".agent.json"),
290
- JSON.stringify({
291
- id: "agent_flow_1",
292
- version: 1,
293
- responseEngineVersion: 1,
294
- }),
295
- )
296
- await Bun.write(
297
- path.join(tmpDir, flowAgentDir, "config.json"),
298
- JSON.stringify({
299
- agent_name: "Flow Agent",
300
- response_engine: {
301
- type: "conversation-flow",
302
- conversation_flow_id: "flow_1",
303
- version: 1,
304
- },
305
- }),
306
- )
307
- await Bun.write(
308
- path.join(tmpDir, flowAgentDir, "conversation-flow.json"),
309
- JSON.stringify({ nodes: [] }),
310
- )
311
-
312
- // Commit
313
- await $`git add .`.quiet()
314
- await $`git commit -m "Add multiple agents"`.quiet()
315
- const { stdout } = await $`git rev-parse HEAD`.quiet()
316
- const commitHash = stdout.toString().trim()
317
-
318
- await Bun.write(
319
- path.join(tmpDir, ".sync.json"),
320
- JSON.stringify({ baseline: { commitHash } }),
321
- )
322
-
323
- const result = await getBaselineState({ agentsDir })
324
-
325
- expect(result.voiceAgents).toHaveLength(2)
326
- expect(result.llms).toHaveLength(1)
327
- expect(result.conversationFlows).toHaveLength(1)
328
-
329
- const agentNames = result.voiceAgents.map((a) => a.agent_name).sort()
330
- expect(agentNames).toEqual(["Flow Agent", "LLM Agent"])
331
- })
332
-
333
- test("reads from specific git commit, not working directory", async () => {
334
- const agentsDir = "agents"
335
- const agentDir = `${agentsDir}/versioned_agent_xyz789`
336
-
337
- // Create initial version
338
- await fs.mkdir(path.join(tmpDir, agentDir), { recursive: true })
339
- await Bun.write(
340
- path.join(tmpDir, agentDir, ".agent.json"),
341
- JSON.stringify({
342
- id: "agent_versioned",
343
- version: 1,
344
- responseEngineVersion: 1,
345
- }),
346
- )
347
- await Bun.write(
348
- path.join(tmpDir, agentDir, "config.json"),
349
- JSON.stringify({
350
- agent_name: "Original Name",
351
- response_engine: { type: "retell-llm", llm_id: "llm_v", version: 1 },
352
- }),
353
- )
354
- await Bun.write(
355
- path.join(tmpDir, agentDir, "llm.json"),
356
- JSON.stringify({ model: "gpt-3.5" }),
357
- )
358
-
359
- // Commit v1
360
- await $`git add .`.quiet()
361
- await $`git commit -m "v1"`.quiet()
362
- const { stdout } = await $`git rev-parse HEAD`.quiet()
363
- const v1CommitHash = stdout.toString().trim()
364
-
365
- // Modify files (v2 - not committed or committed separately)
366
- await Bun.write(
367
- path.join(tmpDir, agentDir, "config.json"),
368
- JSON.stringify({
369
- agent_name: "Updated Name",
370
- response_engine: { type: "retell-llm", llm_id: "llm_v", version: 1 },
371
- }),
372
- )
373
- await Bun.write(
374
- path.join(tmpDir, agentDir, "llm.json"),
375
- JSON.stringify({ model: "gpt-4" }),
376
- )
377
- await $`git add .`.quiet()
378
- await $`git commit -m "v2"`.quiet()
379
-
380
- // Point .sync.json to v1 commit
381
- await Bun.write(
382
- path.join(tmpDir, ".sync.json"),
383
- JSON.stringify({ baseline: { commitHash: v1CommitHash } }),
384
- )
385
-
386
- const result = await getBaselineState({ agentsDir })
387
-
388
- // Should get v1 data, not v2
389
- expect(result.voiceAgents[0]!.agent_name).toBe("Original Name")
390
- expect(result.llms[0]!.model as string).toBe("gpt-3.5")
391
- })
392
-
393
- test("throws error when .sync.json is missing", async () => {
394
- const agentsDir = "agents"
395
-
396
- await expect(getBaselineState({ agentsDir })).rejects.toThrow(
397
- ".sync.json file not found",
398
- )
399
- })
400
-
401
- test("skips directories without .agent.json file", async () => {
402
- const agentsDir = "agents"
403
-
404
- // Create valid agent
405
- const validDir = `${agentsDir}/valid_agent_aaa111`
406
- await fs.mkdir(path.join(tmpDir, validDir), { recursive: true })
407
- await Bun.write(
408
- path.join(tmpDir, validDir, ".agent.json"),
409
- JSON.stringify({
410
- id: "agent_valid",
411
- version: 1,
412
- responseEngineVersion: 1,
413
- }),
414
- )
415
- await Bun.write(
416
- path.join(tmpDir, validDir, "config.json"),
417
- JSON.stringify({
418
- agent_name: "Valid Agent",
419
- response_engine: { type: "retell-llm", llm_id: "llm_v", version: 1 },
420
- }),
421
- )
422
- await Bun.write(
423
- path.join(tmpDir, validDir, "llm.json"),
424
- JSON.stringify({ model: "gpt-4" }),
425
- )
426
-
427
- // Create directory without .agent.json (should be skipped)
428
- const invalidDir = `${agentsDir}/not_an_agent`
429
- await fs.mkdir(path.join(tmpDir, invalidDir), { recursive: true })
430
- await Bun.write(
431
- path.join(tmpDir, invalidDir, "config.json"),
432
- JSON.stringify({ agent_name: "Invalid" }),
433
- )
434
-
435
- await $`git add .`.quiet()
436
- await $`git commit -m "mixed dirs"`.quiet()
437
- const { stdout } = await $`git rev-parse HEAD`.quiet()
438
- const commitHash = stdout.toString().trim()
439
-
440
- await Bun.write(
441
- path.join(tmpDir, ".sync.json"),
442
- JSON.stringify({ baseline: { commitHash } }),
443
- )
444
-
445
- const result = await getBaselineState({ agentsDir })
446
-
447
- expect(result.voiceAgents).toHaveLength(1)
448
- expect(result.voiceAgents[0]!.agent_name).toBe("Valid Agent")
449
- })
450
- })