Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 80
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "always",
"trailingCommas": "all",
"arrowParentheses": "always"
}
},
"linter": {
"enabled": false
},
"files": {
"includes": [
"src/**/*.ts",
"tests/**/*.ts",
"*.json",
"!node_modules/**",
"!resources/**",
"!.vapi-state.*.json",
"!.vapi-state.*.snapshots/**",
"!package-lock.json"
]
}
}
47 changes: 37 additions & 10 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import type { VapiResponse } from "./types.ts";

const DRY_RUN_COUNTS = { POST: 0, PATCH: 0, DELETE: 0 };

export function getDryRunCounts(): { POST: number; PATCH: number; DELETE: number } {
export function getDryRunCounts(): {
POST: number;
PATCH: number;
DELETE: number;
} {
return { ...DRY_RUN_COUNTS };
}

Expand Down Expand Up @@ -54,7 +58,9 @@ function parseApiMessage(body: string): string {
const parsed = JSON.parse(body);
if (typeof parsed.message === "string") return parsed.message;
if (Array.isArray(parsed.message)) return parsed.message.join("; ");
} catch { /* not JSON, use raw body */ }
} catch {
/* not JSON, use raw body */
}
return body;
}

Expand Down Expand Up @@ -87,7 +93,7 @@ function shouldRetry(status: number): boolean {
export async function vapiRequest<T = VapiResponse>(
method: "POST" | "PATCH",
endpoint: string,
body: Record<string, unknown>
body: Record<string, unknown>,
): Promise<T> {
const url = `${VAPI_BASE_URL}${endpoint}`;

Expand Down Expand Up @@ -122,14 +128,25 @@ export async function vapiRequest<T = VapiResponse>(

if (shouldRetry(response.status) && attempt < MAX_RETRIES) {
const delay = INITIAL_DELAY_MS * Math.pow(2, attempt);
const reason = response.status === 429 ? "Rate limited" : `Server error ${response.status}`;
console.log(` ⏳ ${reason}, retrying in ${delay / 1000}s (attempt ${attempt + 1}/${MAX_RETRIES})...`);
const reason =
response.status === 429
? "Rate limited"
: `Server error ${response.status}`;
console.log(
` ⏳ ${reason}, retrying in ${delay / 1000}s (attempt ${attempt + 1}/${MAX_RETRIES})...`,
);
await sleep(delay);
continue;
}

const errorText = await response.text();
throw new VapiApiError(method, endpoint, response.status, parseApiMessage(errorText), errorText);
throw new VapiApiError(
method,
endpoint,
response.status,
parseApiMessage(errorText),
errorText,
);
}

throw new VapiApiError(method, endpoint, 429, "max retries exceeded", "");
Expand Down Expand Up @@ -159,16 +176,26 @@ export async function vapiDelete(endpoint: string): Promise<void> {

if (shouldRetry(response.status) && attempt < MAX_RETRIES) {
const delay = INITIAL_DELAY_MS * Math.pow(2, attempt);
const reason = response.status === 429 ? "Rate limited" : `Server error ${response.status}`;
console.log(` ⏳ ${reason}, retrying in ${delay / 1000}s (attempt ${attempt + 1}/${MAX_RETRIES})...`);
const reason =
response.status === 429
? "Rate limited"
: `Server error ${response.status}`;
console.log(
` ⏳ ${reason}, retrying in ${delay / 1000}s (attempt ${attempt + 1}/${MAX_RETRIES})...`,
);
await sleep(delay);
continue;
}

const errorText = await response.text();
throw new VapiApiError("DELETE", endpoint, response.status, parseApiMessage(errorText), errorText);
throw new VapiApiError(
"DELETE",
endpoint,
response.status,
parseApiMessage(errorText),
errorText,
);
}

throw new VapiApiError("DELETE", endpoint, 429, "max retries exceeded", "");
}

22 changes: 16 additions & 6 deletions src/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export async function runApply(): Promise<void> {
const allArgs = process.argv.slice(3);
const hasForce = allArgs.includes("--force");

const pullArgs = allArgs.filter(a => a !== "--force").join(" ");
const pullArgs = allArgs.filter((a) => a !== "--force").join(" ");
const pushArgs = allArgs.join(" ");

if (!env || !SLUG_RE.test(env)) {
Expand All @@ -40,18 +40,24 @@ export async function runApply(): Promise<void> {
console.error(" Pulls latest platform state (preserving local changes),");
console.error(" then pushes the result back to the platform.");
console.error("");
console.error(" --force Enable deletions: resources you deleted locally");
console.error(
" --force Enable deletions: resources you deleted locally",
);
console.error(" will also be deleted from the platform.");
process.exit(1);
}

console.log("═══════════════════════════════════════════════════════════════");
console.log(
"═══════════════════════════════════════════════════════════════",
);
console.log(`🔄 Vapi GitOps Apply - Environment: ${env}`);
console.log(" Pull → Merge → Push");
if (hasForce) {
console.log(" ⚠️ Deletions enabled (--force)");
}
console.log("═══════════════════════════════════════════════════════════════\n");
console.log(
"═══════════════════════════════════════════════════════════════\n",
);

const pullCmd = `npx tsx src/pull.ts ${env} ${pullArgs}`.trim();
const pullExit = runPassthrough(pullCmd);
Expand All @@ -68,9 +74,13 @@ export async function runApply(): Promise<void> {
process.exit(1);
}

console.log("\n═══════════════════════════════════════════════════════════════");
console.log(
"\n═══════════════════════════════════════════════════════════════",
);
console.log("✅ Apply complete! (Pull → Merge → Push)");
console.log("═══════════════════════════════════════════════════════════════\n");
console.log(
"═══════════════════════════════════════════════════════════════\n",
);
}

// Run when executed directly
Expand Down
30 changes: 21 additions & 9 deletions src/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,7 @@ async function checkMicrophonePermission(): Promise<boolean> {
console.log(
" If prompted, please grant microphone permission in System Settings.",
);
console.log(
" System Settings > Privacy & Security > Microphone\n",
);
console.log(" System Settings > Privacy & Security > Microphone\n");

// Ask user to continue anyway
const shouldContinue = await askUserConfirmation(
Expand Down Expand Up @@ -832,7 +830,9 @@ function handleControlMessage(
"assistant-request-returned-error": "Assistant request error",
"assistant-not-found": "Assistant not found",
};
const label = cm.reason ? (reasonLabels[cm.reason] ?? cm.reason) : "unknown reason";
const label = cm.reason
? (reasonLabels[cm.reason] ?? cm.reason)
: "unknown reason";
clearWrittenLine(process.stdout, state.lastTranscript);
state.lastTranscript = "";
console.log(`📞 Call ended: ${label}`);
Expand Down Expand Up @@ -908,7 +908,10 @@ function handleControlMessage(
}
case "transfer-update": {
const tm = message as TransferUpdateMessage;
printEvent(state, `🔀 Transfer${formatTransferDestination(tm.destination)}`);
printEvent(
state,
`🔀 Transfer${formatTransferDestination(tm.destination)}`,
);
break;
}
default:
Expand Down Expand Up @@ -980,10 +983,17 @@ function createAudioContext(): {
} catch (error) {
const msg = error instanceof Error ? error.message : String(error);
if (msg.includes("Cannot find module")) {
console.warn("⚠️ 'speaker' module not installed. Audio playback disabled.");
console.warn(
"⚠️ 'speaker' module not installed. Audio playback disabled.",
);
console.warn(" Install with: npm install speaker");
} else if (msg.includes("Could not locate the bindings file") || msg.includes("NODE_MODULE_VERSION")) {
console.warn("⚠️ 'speaker' native bindings not built for this Node version.");
} else if (
msg.includes("Could not locate the bindings file") ||
msg.includes("NODE_MODULE_VERSION")
) {
console.warn(
"⚠️ 'speaker' native bindings not built for this Node version.",
);
console.warn(" Rebuild with: npm rebuild speaker");
} else {
console.warn(`⚠️ Could not initialize speaker: ${msg}`);
Expand Down Expand Up @@ -1040,7 +1050,9 @@ function createMicrophoneStream(onData: (data: Buffer) => void): {
console.warn(" Install with: npm install mic");
} else if (msg.includes("sox") || msg.includes("rec")) {
console.warn("⚠️ sox/rec not found. Required for microphone input.");
console.warn(" Install with: brew install sox (macOS) or apt install sox (Linux)");
console.warn(
" Install with: brew install sox (macOS) or apt install sox (Linux)",
);
} else {
console.warn(`⚠️ Could not initialize microphone: ${msg}`);
}
Expand Down
5 changes: 4 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,10 @@ export function loadIgnorePatterns(): string[] {
cachedIgnorePatterns = raw
.split("\n")
.map((line) => line.trim())
.filter((line) => line.length > 0 && !line.startsWith("#") && !line.startsWith("!"));
.filter(
(line) =>
line.length > 0 && !line.startsWith("#") && !line.startsWith("!"),
);

return cachedIgnorePatterns;
}
Expand Down
Loading