Skip to content

Commit 3b19d3c

Browse files
committed
feat: add bootstrap state sync for template pushes
Refresh state and credential mappings without materializing remote org resources, and have push auto-bootstrap when local state is stale or incomplete. Also fix the existing TypeScript build issues needed to keep the repo verifiable.
1 parent 17e1c8e commit 3b19d3c

9 files changed

Lines changed: 500 additions & 144 deletions

File tree

AGENTS.md

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,25 +8,27 @@ This project manages **Vapi voice agent configurations** as code. All resources
88

99
**Environment-scoped resources:** Resources live in `resources/<env>/` (e.g. `resources/dev/`, `resources/prod/`). Each environment directory is isolated — `push:dev` only touches `resources/dev/`, `push:prod` only touches `resources/prod/`. See **`docs/environment-scoped-resources.md`** for the full promotion workflow and rationale.
1010

11+
**Template-safe first run:** In a fresh clone, prefer `npm run pull:dev:bootstrap` (or the matching env) to refresh `.vapi-state.<env>.json` and credential mappings without materializing the target org's resources into `resources/<env>/`. `push:<env>` will auto-run the same bootstrap sync when it detects empty or stale state for the resources being applied.
12+
1113
---
1214

1315
## Quick Reference
1416

15-
| I want to... | What to do |
16-
| ----------------------------------- | ------------------------------------------------------------------------- |
17-
| Edit an assistant's system prompt | Edit the markdown body in `resources/<env>/assistants/<name>.md` |
18-
| Change assistant settings | Edit the YAML frontmatter in the same `.md` file |
19-
| Add a new tool | Create `resources/<env>/tools/<name>.yml` |
20-
| Add a new assistant | Create `resources/<env>/assistants/<name>.md` |
21-
| Create a multi-agent squad | Create `resources/<env>/squads/<name>.yml` |
22-
| Add post-call analysis | Create `resources/<env>/structuredOutputs/<name>.yml` |
23-
| Write test simulations | Create files under `resources/<env>/simulations/` |
24-
| Promote resources across envs | Copy files from `resources/dev/` to `resources/stg/` or `resources/prod/` |
25-
| Test webhook event delivery locally | Run `npm run mock:webhook` and tunnel with ngrok |
26-
| Push changes to Vapi | `npm run push:dev` or `npm run push:prod` |
27-
| Pull latest from Vapi | `npm run pull:dev` or `npm run pull:dev:force` |
28-
| Push only one file | `npm run push:dev resources/dev/assistants/my-agent.md` |
29-
| Test a call | `npm run call:dev -- -a <assistant-name>` |
17+
| I want to... | What to do |
18+
| ----------------------------------- | ----------------------------------------------------------------------------- |
19+
| Edit an assistant's system prompt | Edit the markdown body in `resources/<env>/assistants/<name>.md` |
20+
| Change assistant settings | Edit the YAML frontmatter in the same `.md` file |
21+
| Add a new tool | Create `resources/<env>/tools/<name>.yml` |
22+
| Add a new assistant | Create `resources/<env>/assistants/<name>.md` |
23+
| Create a multi-agent squad | Create `resources/<env>/squads/<name>.yml` |
24+
| Add post-call analysis | Create `resources/<env>/structuredOutputs/<name>.yml` |
25+
| Write test simulations | Create files under `resources/<env>/simulations/` |
26+
| Promote resources across envs | Copy files from `resources/dev/` to `resources/stg/` or `resources/prod/` |
27+
| Test webhook event delivery locally | Run `npm run mock:webhook` and tunnel with ngrok |
28+
| Push changes to Vapi | `npm run push:dev` or `npm run push:prod` |
29+
| Pull latest from Vapi | `npm run pull:dev`, `npm run pull:dev:force`, or `npm run pull:dev:bootstrap` |
30+
| Push only one file | `npm run push:dev resources/dev/assistants/my-agent.md` |
31+
| Test a call | `npm run call:dev -- -a <assistant-name>` |
3032

3133
---
3234

@@ -701,6 +703,7 @@ Concrete example conversations showing expected behavior.
701703
# Sync
702704
npm run pull:dev # Pull from Vapi (preserve local changes)
703705
npm run pull:dev:force # Pull from Vapi (overwrite everything)
706+
npm run pull:dev:bootstrap # Refresh state without writing remote resources locally
704707
npm run push:dev # Push all local changes to Vapi
705708
npm run push:dev assistants # Push only assistants
706709
npm run push:dev resources/dev/assistants/my-agent.md # Push single file

README.md

Lines changed: 45 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Manage Vapi resources via Git using YAML/Markdown as the source-of-truth.
3939

4040
## How to Use This Repo
4141

42-
1. **Sync from Vapi first** using `pull` so local files reflect platform state.
42+
1. **Bootstrap state first** using `pull:*:bootstrap` when you need fresh platform mappings without downloading the org's resources into your working tree.
4343
2. **Edit declarative resources** in `resources/<env>/` (`.md` assistants, `.yml` tools/squads/etc.).
4444
3. **Push selectively while iterating** (resource type or file path), then run a full push before release.
4545
4. **Promote by environment** (`dev` -> `stg` -> `prod`) by copying files between `resources/dev/`, `resources/stg/`, and `resources/prod/`.
@@ -50,6 +50,8 @@ Use:
5050
- `push` for explicit deploys
5151
- `apply` (`pull -> merge -> push`) when you want one command for sync + deploy
5252

53+
For template-based repos, `push` now auto-runs a bootstrap state sync when local state is missing credential mappings or contains stale IDs for the resources you're applying.
54+
5355
---
5456

5557
## Quick Start
@@ -79,42 +81,58 @@ cp .env.example .env.prod
7981

8082
### Commands
8183

82-
| Command | Description |
83-
| ------------------------------- | --------------------------------------------------- |
84-
| `npm run build` | Type-check the codebase |
85-
| `npm run pull:dev` | Pull platform state, preserve local changes |
86-
| `npm run pull:stg` | Pull staging state, preserve local changes |
87-
| `npm run pull:dev:force` | Pull platform state, overwrite everything |
88-
| `npm run pull:stg:force` | Pull staging state, overwrite everything |
89-
| `npm run pull:prod` | Pull from prod, preserve local changes |
90-
| `npm run pull:prod:force` | Pull from prod, overwrite everything |
91-
| `npm run push:dev` | Push local files to Vapi (dev) |
92-
| `npm run push:stg` | Push local files to Vapi (staging) |
93-
| `npm run push:prod` | Push local files to Vapi (prod) |
94-
| `npm run apply:dev` | Pull → Merge → Push in one shot (dev) |
95-
| `npm run apply:stg` | Pull → Merge → Push in one shot (staging) |
96-
| `npm run apply:prod` | Pull → Merge → Push in one shot (prod) |
97-
| `npm run push:dev assistants` | Push only assistants (dev) |
98-
| `npm run push:dev tools` | Push only tools (dev) |
99-
| `npm run call:dev -- -a <name>` | Start a WebSocket call to an assistant (dev) |
100-
| `npm run call:dev -- -s <name>` | Start a WebSocket call to a squad (dev) |
101-
| `npm run mock:webhook` | Run local webhook receiver for Vapi server messages |
84+
| Command | Description |
85+
| ------------------------------- | -------------------------------------------------------------------------- |
86+
| `npm run build` | Type-check the codebase |
87+
| `npm run pull:dev` | Pull platform state, preserve local changes |
88+
| `npm run pull:stg` | Pull staging state, preserve local changes |
89+
| `npm run pull:dev:force` | Pull platform state, overwrite everything |
90+
| `npm run pull:stg:force` | Pull staging state, overwrite everything |
91+
| `npm run pull:prod` | Pull from prod, preserve local changes |
92+
| `npm run pull:prod:force` | Pull from prod, overwrite everything |
93+
| `npm run pull:dev:bootstrap` | Refresh dev state/credentials without writing remote resources locally |
94+
| `npm run pull:stg:bootstrap` | Refresh staging state/credentials without writing remote resources locally |
95+
| `npm run pull:prod:bootstrap` | Refresh prod state/credentials without writing remote resources locally |
96+
| `npm run push:dev` | Push local files to Vapi (dev) |
97+
| `npm run push:stg` | Push local files to Vapi (staging) |
98+
| `npm run push:prod` | Push local files to Vapi (prod) |
99+
| `npm run apply:dev` | Pull → Merge → Push in one shot (dev) |
100+
| `npm run apply:stg` | Pull → Merge → Push in one shot (staging) |
101+
| `npm run apply:prod` | Pull → Merge → Push in one shot (prod) |
102+
| `npm run push:dev assistants` | Push only assistants (dev) |
103+
| `npm run push:dev tools` | Push only tools (dev) |
104+
| `npm run call:dev -- -a <name>` | Start a WebSocket call to an assistant (dev) |
105+
| `npm run call:dev -- -s <name>` | Start a WebSocket call to a squad (dev) |
106+
| `npm run mock:webhook` | Run local webhook receiver for Vapi server messages |
102107

103108
### Basic Workflow
104109

105110
```bash
106-
# First time: pull all resources from Vapi for your target env
107-
npm run pull:dev:force
108-
109-
# Commit the initial state
110-
git add . && git commit -m "initial pull"
111+
# First time in a template clone: refresh only state and credentials
112+
npm run pull:dev:bootstrap
111113

112-
# Make changes to YAML/MD files under resources/
114+
# Add or edit only the resources you actually want under resources/dev/
113115

114116
# Push your changes (full sync)
115117
npm run push:dev
116118
```
117119

120+
#### Bootstrap State Sync (Template-Safe First Run)
121+
122+
Use bootstrap pull when you need the latest platform IDs and credential mappings but do not want the repo filled with assistants, tools, and other resources from the target Vapi org:
123+
124+
```bash
125+
npm run pull:dev:bootstrap
126+
```
127+
128+
This mode:
129+
130+
- Pulls credentials into `.vapi-state.<env>.json`
131+
- Refreshes remote resource ID mappings in the state file
132+
- Leaves `resources/<env>/` untouched so your working tree stays focused on the resources you actually intend to manage
133+
134+
If you skip this step, `push` will automatically run the same bootstrap sync when it detects empty or stale state for the resources being applied.
135+
118136
Promotion example:
119137

120138
```bash

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
"pull:stg:force": "tsx src/pull.ts stg --force",
2525
"pull:prod": "tsx src/pull.ts prod",
2626
"pull:prod:force": "tsx src/pull.ts prod --force",
27+
"pull:dev:bootstrap": "tsx src/pull.ts dev --bootstrap",
28+
"pull:stg:bootstrap": "tsx src/pull.ts stg --bootstrap",
29+
"pull:prod:bootstrap": "tsx src/pull.ts prod --bootstrap",
2730
"call:dev": "tsx src/call.ts dev",
2831
"call:stg": "tsx src/call.ts stg",
2932
"call:prod": "tsx src/call.ts prod",
@@ -45,4 +48,4 @@
4548
"mic": "^2.1.2",
4649
"speaker": "^0.5.5"
4750
}
48-
}
51+
}

src/call.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,29 @@ function parseArgs(): CallConfig {
6868
console.error("❌ Cannot specify both -a and -s");
6969
process.exit(1);
7070
}
71+
const nextArg = args[i + 1];
72+
if (!nextArg) {
73+
console.error("❌ Missing assistant name after -a/--assistant");
74+
printUsage();
75+
process.exit(1);
76+
}
7177
resourceType = "assistant";
72-
target = args[++i];
78+
target = nextArg;
79+
i++;
7380
} else if (arg === "-s" || arg === "--squad") {
7481
if (resourceType) {
7582
console.error("❌ Cannot specify both -a and -s");
7683
process.exit(1);
7784
}
85+
const nextArg = args[i + 1];
86+
if (!nextArg) {
87+
console.error("❌ Missing squad name after -s/--squad");
88+
printUsage();
89+
process.exit(1);
90+
}
7891
resourceType = "squad";
79-
target = args[++i];
92+
target = nextArg;
93+
i++;
8094
}
8195
}
8296

@@ -536,17 +550,45 @@ function handleControlMessage(
536550
// Audio Utilities (Stubs - require native modules)
537551
// ─────────────────────────────────────────────────────────────────────────────
538552

553+
interface SpeakerInstance {
554+
write: (data: Buffer) => void;
555+
end: () => void;
556+
}
557+
558+
type SpeakerConstructor = new (options: {
559+
channels: number;
560+
bitDepth: number;
561+
sampleRate: number;
562+
}) => SpeakerInstance;
563+
564+
interface MicrophoneAudioStream {
565+
on: (
566+
event: "data" | "error",
567+
listener: ((data: Buffer) => void) | ((error: Error) => void),
568+
) => void;
569+
}
570+
571+
interface MicrophoneInstance {
572+
getAudioStream: () => MicrophoneAudioStream;
573+
start: () => void;
574+
stop: () => void;
575+
}
576+
577+
type MicrophoneFactory = (
578+
options: Record<string, string>,
579+
) => MicrophoneInstance;
580+
539581
function createAudioContext(): {
540582
playAudio: (data: Buffer | ArrayBuffer) => void;
541583
close: () => void;
542584
} {
543585
// Lazy load speaker module
544-
let Speaker: typeof import("speaker") | null = null;
545-
let speakerInstance: InstanceType<typeof import("speaker")> | null = null;
586+
let Speaker: SpeakerConstructor | null = null;
587+
let speakerInstance: SpeakerInstance | null = null;
546588

547589
try {
548590
// Dynamic import for optional dependency
549-
Speaker = require("speaker");
591+
Speaker = require("speaker") as SpeakerConstructor;
550592
speakerInstance = new Speaker!({
551593
channels: 1,
552594
bitDepth: 16,
@@ -577,11 +619,11 @@ function createAudioContext(): {
577619
function createMicrophoneStream(onData: (data: Buffer) => void): {
578620
stop: () => void;
579621
} {
580-
let mic: ReturnType<typeof import("mic")> | null = null;
581-
let micInstance: ReturnType<ReturnType<typeof import("mic")>> | null = null;
622+
let mic: MicrophoneFactory | null = null;
623+
let micInstance: MicrophoneInstance | null = null;
582624

583625
try {
584-
mic = require("mic");
626+
mic = require("mic") as MicrophoneFactory;
585627
micInstance = mic!({
586628
rate: "16000",
587629
channels: "1",

0 commit comments

Comments
 (0)