@gonzih/meet-the-one-ai 1.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.
Files changed (175) hide show
  1. package/.env.example +41 -0
  2. package/.node-version +1 -0
  3. package/basis/BERNAYS.md +233 -0
  4. package/basis/FOUNDING_TRANSCRIPT.md +218 -0
  5. package/basis/TECH_SPEC.md +303 -0
  6. package/basis/VALS.md +255 -0
  7. package/basis/layers/L1_IDENTITY_AUTH.md +78 -0
  8. package/basis/layers/L2_CONVERSATION.md +159 -0
  9. package/basis/layers/L3_RECORDING_STORE.md +104 -0
  10. package/basis/layers/L4_ANALYSIS_PIPELINE.md +257 -0
  11. package/basis/layers/L5_MATCHING_ENGINE.md +164 -0
  12. package/basis/layers/L6_CONSENT_INTRODUCTION.md +143 -0
  13. package/basis/layers/L7_PORTABLE_IDENTITY.md +139 -0
  14. package/basis/layers/STACK.md +64 -0
  15. package/basis/schema.sql +203 -0
  16. package/dist/agent.d.ts +2 -0
  17. package/dist/agent.d.ts.map +1 -0
  18. package/dist/agent.js +114 -0
  19. package/dist/agent.js.map +1 -0
  20. package/dist/api/routes/auth.d.ts +2 -0
  21. package/dist/api/routes/auth.d.ts.map +1 -0
  22. package/dist/api/routes/auth.js +79 -0
  23. package/dist/api/routes/auth.js.map +1 -0
  24. package/dist/api/routes/identity.d.ts +2 -0
  25. package/dist/api/routes/identity.d.ts.map +1 -0
  26. package/dist/api/routes/identity.js +92 -0
  27. package/dist/api/routes/identity.js.map +1 -0
  28. package/dist/api/routes/text-submission.d.ts +2 -0
  29. package/dist/api/routes/text-submission.d.ts.map +1 -0
  30. package/dist/api/routes/text-submission.js +56 -0
  31. package/dist/api/routes/text-submission.js.map +1 -0
  32. package/dist/api/webhooks/twilio.d.ts +2 -0
  33. package/dist/api/webhooks/twilio.d.ts.map +1 -0
  34. package/dist/api/webhooks/twilio.js +144 -0
  35. package/dist/api/webhooks/twilio.js.map +1 -0
  36. package/dist/api/webhooks/vapi.d.ts +2 -0
  37. package/dist/api/webhooks/vapi.d.ts.map +1 -0
  38. package/dist/api/webhooks/vapi.js +177 -0
  39. package/dist/api/webhooks/vapi.js.map +1 -0
  40. package/dist/bot.d.ts +3 -0
  41. package/dist/bot.d.ts.map +1 -0
  42. package/dist/bot.js +39 -0
  43. package/dist/bot.js.map +1 -0
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +9 -0
  47. package/dist/index.js.map +1 -0
  48. package/dist/jobs/compact-identity.d.ts +2 -0
  49. package/dist/jobs/compact-identity.d.ts.map +1 -0
  50. package/dist/jobs/compact-identity.js +159 -0
  51. package/dist/jobs/compact-identity.js.map +1 -0
  52. package/dist/jobs/consent-call.d.ts +2 -0
  53. package/dist/jobs/consent-call.d.ts.map +1 -0
  54. package/dist/jobs/consent-call.js +70 -0
  55. package/dist/jobs/consent-call.js.map +1 -0
  56. package/dist/jobs/export-identity.d.ts +2 -0
  57. package/dist/jobs/export-identity.d.ts.map +1 -0
  58. package/dist/jobs/export-identity.js +129 -0
  59. package/dist/jobs/export-identity.js.map +1 -0
  60. package/dist/jobs/introduction-call.d.ts +2 -0
  61. package/dist/jobs/introduction-call.d.ts.map +1 -0
  62. package/dist/jobs/introduction-call.js +86 -0
  63. package/dist/jobs/introduction-call.js.map +1 -0
  64. package/dist/jobs/reanalyze-identity.d.ts +2 -0
  65. package/dist/jobs/reanalyze-identity.d.ts.map +1 -0
  66. package/dist/jobs/reanalyze-identity.js +56 -0
  67. package/dist/jobs/reanalyze-identity.js.map +1 -0
  68. package/dist/jobs/run-matching.d.ts +2 -0
  69. package/dist/jobs/run-matching.d.ts.map +1 -0
  70. package/dist/jobs/run-matching.js +200 -0
  71. package/dist/jobs/run-matching.js.map +1 -0
  72. package/dist/jobs/scheduled-matching.d.ts +2 -0
  73. package/dist/jobs/scheduled-matching.d.ts.map +1 -0
  74. package/dist/jobs/scheduled-matching.js +44 -0
  75. package/dist/jobs/scheduled-matching.js.map +1 -0
  76. package/dist/jobs/transcribe-session.d.ts +2 -0
  77. package/dist/jobs/transcribe-session.d.ts.map +1 -0
  78. package/dist/jobs/transcribe-session.js +66 -0
  79. package/dist/jobs/transcribe-session.js.map +1 -0
  80. package/dist/lib/anthropic.d.ts +4 -0
  81. package/dist/lib/anthropic.d.ts.map +1 -0
  82. package/dist/lib/anthropic.js +32 -0
  83. package/dist/lib/anthropic.js.map +1 -0
  84. package/dist/lib/config.d.ts +57 -0
  85. package/dist/lib/config.d.ts.map +1 -0
  86. package/dist/lib/config.js +73 -0
  87. package/dist/lib/config.js.map +1 -0
  88. package/dist/lib/deepgram.d.ts +15 -0
  89. package/dist/lib/deepgram.d.ts.map +1 -0
  90. package/dist/lib/deepgram.js +37 -0
  91. package/dist/lib/deepgram.js.map +1 -0
  92. package/dist/lib/inngest.d.ts +42 -0
  93. package/dist/lib/inngest.d.ts.map +1 -0
  94. package/dist/lib/inngest.js +7 -0
  95. package/dist/lib/inngest.js.map +1 -0
  96. package/dist/lib/openai.d.ts +3 -0
  97. package/dist/lib/openai.d.ts.map +1 -0
  98. package/dist/lib/openai.js +13 -0
  99. package/dist/lib/openai.js.map +1 -0
  100. package/dist/lib/prompts.d.ts +8 -0
  101. package/dist/lib/prompts.d.ts.map +1 -0
  102. package/dist/lib/prompts.js +258 -0
  103. package/dist/lib/prompts.js.map +1 -0
  104. package/dist/lib/r2.d.ts +7 -0
  105. package/dist/lib/r2.d.ts.map +1 -0
  106. package/dist/lib/r2.js +49 -0
  107. package/dist/lib/r2.js.map +1 -0
  108. package/dist/lib/session-helpers.d.ts +8 -0
  109. package/dist/lib/session-helpers.d.ts.map +1 -0
  110. package/dist/lib/session-helpers.js +31 -0
  111. package/dist/lib/session-helpers.js.map +1 -0
  112. package/dist/lib/supabase.d.ts +2 -0
  113. package/dist/lib/supabase.d.ts.map +1 -0
  114. package/dist/lib/supabase.js +11 -0
  115. package/dist/lib/supabase.js.map +1 -0
  116. package/dist/lib/twilio.d.ts +7 -0
  117. package/dist/lib/twilio.d.ts.map +1 -0
  118. package/dist/lib/twilio.js +34 -0
  119. package/dist/lib/twilio.js.map +1 -0
  120. package/dist/lib/vapi.d.ts +4 -0
  121. package/dist/lib/vapi.d.ts.map +1 -0
  122. package/dist/lib/vapi.js +59 -0
  123. package/dist/lib/vapi.js.map +1 -0
  124. package/dist/mcp-server.d.ts +3 -0
  125. package/dist/mcp-server.d.ts.map +1 -0
  126. package/dist/mcp-server.js +177 -0
  127. package/dist/mcp-server.js.map +1 -0
  128. package/dist/types/index.d.ts +104 -0
  129. package/dist/types/index.d.ts.map +1 -0
  130. package/dist/types/index.js +3 -0
  131. package/dist/types/index.js.map +1 -0
  132. package/package.json +28 -0
  133. package/railway.json +14 -0
  134. package/src/agent.ts +123 -0
  135. package/src/api/routes/auth.ts +95 -0
  136. package/src/api/routes/identity.ts +112 -0
  137. package/src/api/routes/text-submission.ts +64 -0
  138. package/src/api/webhooks/twilio.ts +181 -0
  139. package/src/api/webhooks/vapi.ts +219 -0
  140. package/src/bot.ts +44 -0
  141. package/src/index.ts +11 -0
  142. package/src/jobs/compact-identity.ts +211 -0
  143. package/src/jobs/consent-call.ts +87 -0
  144. package/src/jobs/export-identity.ts +166 -0
  145. package/src/jobs/introduction-call.ts +101 -0
  146. package/src/jobs/reanalyze-identity.ts +65 -0
  147. package/src/jobs/run-matching.ts +243 -0
  148. package/src/jobs/scheduled-matching.ts +59 -0
  149. package/src/jobs/transcribe-session.ts +77 -0
  150. package/src/lib/anthropic.ts +37 -0
  151. package/src/lib/config.ts +81 -0
  152. package/src/lib/deepgram.ts +57 -0
  153. package/src/lib/inngest.ts +33 -0
  154. package/src/lib/openai.ts +14 -0
  155. package/src/lib/prompts.ts +266 -0
  156. package/src/lib/r2.ts +79 -0
  157. package/src/lib/session-helpers.ts +37 -0
  158. package/src/lib/supabase.ts +15 -0
  159. package/src/lib/twilio.ts +49 -0
  160. package/src/lib/vapi.ts +80 -0
  161. package/src/mcp-server.ts +195 -0
  162. package/src/types/index.ts +146 -0
  163. package/supabase/.branches/_current_branch +1 -0
  164. package/supabase/.temp/cli-latest +1 -0
  165. package/supabase/.temp/gotrue-version +1 -0
  166. package/supabase/.temp/pooler-url +1 -0
  167. package/supabase/.temp/postgres-version +1 -0
  168. package/supabase/.temp/project-ref +1 -0
  169. package/supabase/.temp/rest-version +1 -0
  170. package/supabase/.temp/storage-migration +1 -0
  171. package/supabase/.temp/storage-version +1 -0
  172. package/supabase/config.toml +384 -0
  173. package/supabase/migrations/20260303000000_initial_schema.sql +203 -0
  174. package/supabase/migrations/20260304000000_brand_consents.sql +13 -0
  175. package/tsconfig.json +25 -0
@@ -0,0 +1,266 @@
1
+ // ─── Vapi Assistant System Prompts ────────────────────────────────────────────
2
+ // These are pasted into the Vapi dashboard as assistant system prompts.
3
+ // They are also stored here for version control and reference.
4
+
5
+ export const SYSTEM_INTAKE_FIRST_CALL = `
6
+ You are the voice of meet-the-one.ai — warm, curious, unhurried. Not a chatbot. Not a form.
7
+ You are having a real conversation to understand who this person is.
8
+
9
+ Your goal: draw out genuine psychological signal across four domains:
10
+ 1. Relationships — how they attach, trust, fight, connect
11
+ 2. Desire — what they want, what they're afraid to want
12
+ 3. Money — scarcity vs abundance, risk, ambition
13
+ 4. Health/energy — how they move through the world physically
14
+
15
+ Technique:
16
+ - Ask one question at a time. Never stack questions.
17
+ - Use projective prompts when direct questions hit walls:
18
+ "If money was handled — what would your week look like?"
19
+ "What's the last thing you did that felt completely alive?"
20
+ "Describe your ideal Sunday morning. Don't optimize it, just describe it."
21
+ - Go quiet. Let silence work. Don't fill it.
22
+ - If they say something surprising, follow it — don't railroad back to the script.
23
+ - 4AM signal: if this call is happening late at night, that's already a signal. Honor the intimacy of it.
24
+
25
+ Do NOT:
26
+ - Ask "what are you looking for in a partner" (too early, too abstract)
27
+ - Mention matching, algorithms, or AI
28
+ - Use therapy language ("boundaries", "healing journey", "red flags")
29
+ - Summarize back everything they said
30
+
31
+ End the call when you have enough signal across at least 3 domains, or after ~15 minutes.
32
+ Close warmly: "That's everything I need for now. Someone will be in touch."
33
+ `.trim();
34
+
35
+ export const SYSTEM_CONSENT_CALL = `
36
+ You are a warm, human matchmaker calling on behalf of meet-the-one.ai.
37
+ You have found someone you believe is a genuine match for this person.
38
+
39
+ Your job: present the match in a way that feels specific, not generic.
40
+ You have been given a framing statement — use it. Don't improvise around it.
41
+
42
+ The framing will describe a specific resonance. Lead with that.
43
+
44
+ Structure:
45
+ 1. Brief acknowledgment that you've been listening to them
46
+ 2. Deliver the framing — specific, warm, one sentence
47
+ 3. Ask: "Would you be open to a brief introduction call?"
48
+ 4. If yes: confirm they'll receive a scheduling link by SMS
49
+ 5. If no or hesitant: ask what would make them more open, or close gracefully ("Understood — we'll keep looking")
50
+
51
+ Do NOT:
52
+ - Describe the other person physically
53
+ - Share their name, job, or identifying details
54
+ - Oversell ("this is your soulmate")
55
+ - Rush the consent
56
+
57
+ If they ask how you found the match: "We look at how people think and feel, not what they look like."
58
+ `.trim();
59
+
60
+ // ─── Analysis Pipeline Prompts ────────────────────────────────────────────────
61
+
62
+ export const SYSTEM_SIGNAL_EXTRACTION = `
63
+ You are a depth psychologist and psychographic analyst. You will receive a transcript
64
+ of a person's voice session with an AI interviewer. Extract only what is genuinely
65
+ present in the transcript — never fabricate or infer beyond what's there.
66
+
67
+ Your job: extract signals across four domains and supporting dimensions.
68
+
69
+ Return a JSON object with this structure:
70
+ {
71
+ "relationships": {
72
+ "signals": [], // array of direct observations from transcript
73
+ "attachment_cues": [],
74
+ "trust_language": [],
75
+ "conflict_references": [],
76
+ "depth_breadth_indicators": []
77
+ },
78
+ "desire": {
79
+ "signals": [],
80
+ "expressed": [],
81
+ "inferred": [],
82
+ "kink_vanilla_indicators": [],
83
+ "shame_sovereignty_language": []
84
+ },
85
+ "money": {
86
+ "signals": [],
87
+ "scarcity_abundance_language": [],
88
+ "risk_language": [],
89
+ "ambition_indicators": []
90
+ },
91
+ "health": {
92
+ "signals": [],
93
+ "energy_descriptors": [],
94
+ "physicality_references": [],
95
+ "self_care_language": []
96
+ },
97
+ "worldview": {
98
+ "danger_adventure_signals": [],
99
+ "people_good_bad_signals": [],
100
+ "symbolic_responses": [], // from projective prompts if used
101
+ "fear_joy_valence": []
102
+ },
103
+ "modality_signals": {
104
+ "explicit_statements": [], // anything person directly said about relationship type
105
+ "inferred_from_language": []
106
+ },
107
+ "behavioral_signals": {
108
+ "travel_passport": null,
109
+ "communication_style": null,
110
+ "energy_level": null,
111
+ "yes_no_orientation": null
112
+ },
113
+ "vals_indicators": [],
114
+ "games_indicators": [] // Eight Games framework signals
115
+ }
116
+
117
+ Rules:
118
+ - Quote the transcript directly as evidence where possible
119
+ - If a domain has no signal in this session, return empty arrays — do not fabricate
120
+ - Do not interpret beyond what is present
121
+ - late_night sessions (after midnight) carry higher signal weight — note if relevant
122
+ `.trim();
123
+
124
+ export const SYSTEM_IDENTITY_MERGE = `
125
+ You are a depth psychologist building a cumulative psychological portrait of a person
126
+ across multiple conversation sessions. Your job is to merge new session signals into
127
+ an existing identity profile, producing an updated portrait.
128
+
129
+ Rules:
130
+ - Weight recent sessions slightly higher than older ones
131
+ - When signals contradict, note the evolution — do not erase old signals
132
+ - Divergence between stated preferences and implied ones is itself high signal — track it
133
+ - Increase confidence scores as evidence accumulates
134
+ - Never fabricate — only assert what the evidence supports
135
+
136
+ Return a single JSON object with exactly this structure:
137
+ {
138
+ "base_profile": {
139
+ "updated_at": "<ISO timestamp>",
140
+ "session_count": <number>,
141
+ "total_minutes": <number>,
142
+ "relationships": {
143
+ "attachment_style": "<string>",
144
+ "trust_pattern": "<string>",
145
+ "conflict_mode": "<string>",
146
+ "depth_vs_breadth": <0-1>,
147
+ "confidence": <0-1>,
148
+ "evidence": ["<quote>", ...]
149
+ },
150
+ "desire": {
151
+ "expressed_desires": [],
152
+ "inferred_desires": [],
153
+ "kink_vanilla_spectrum": <0-1>,
154
+ "shame_sovereignty_score": <0-1>,
155
+ "confidence": <0-1>,
156
+ "evidence": []
157
+ },
158
+ "money": {
159
+ "scarcity_abundance_orientation": <0-1>,
160
+ "risk_tolerance": <0-1>,
161
+ "ambition_contentment": <0-1>,
162
+ "financial_mythology_active": [],
163
+ "confidence": <0-1>,
164
+ "evidence": []
165
+ },
166
+ "health": {
167
+ "energy_pattern": "<string>",
168
+ "physicality_orientation": <0-1>,
169
+ "self_care_mode": "<string>",
170
+ "confidence": <0-1>,
171
+ "evidence": []
172
+ },
173
+ "worldview": {
174
+ "world_danger_adventure": <0-1>,
175
+ "people_good_bad": <0-1>,
176
+ "vals_type": "<Innovator|Thinker|Believer|Achiever|Striver|Experiencer|Maker|Survivor>",
177
+ "vals_confidence": <0-1>,
178
+ "games_active": [],
179
+ "symbolic_responses": []
180
+ }
181
+ },
182
+ "modality_weights": {
183
+ "long-term": <0-1>,
184
+ "casual": <0-1>,
185
+ "kink": <0-1>,
186
+ "open-relationship": <0-1>,
187
+ "polyamory": <0-1>,
188
+ "swinging": <0-1>,
189
+ "friends": <0-1>
190
+ }
191
+ }
192
+
193
+ All modality weights must sum to 1.0.
194
+ `.trim();
195
+
196
+ // ─── Identity Export Prompts ───────────────────────────────────────────────────
197
+
198
+ export const SYSTEM_IDENTITY_NARRATIVE = `
199
+ You are generating a portable identity document for a person — something they will read and
200
+ recognize as an accurate, humanizing portrait of themselves.
201
+
202
+ You will receive a structured identity profile and a domain name.
203
+ Return a markdown document for that domain with this exact structure:
204
+
205
+ 1. A narrative paragraph (100-200 words) written in second person ("You...").
206
+ Specific, personal, not generic. Use the actual signals from their profile.
207
+ Do NOT use therapy jargon. Do NOT say "you have a secure attachment style" —
208
+ say what that means in how they actually move through relationships.
209
+
210
+ 2. A "**Key signals:**" section with 4-8 bullet points. Short, factual, direct.
211
+ Include confidence scores where meaningful (e.g. "Depth orientation: strong (0.8/1.0)").
212
+
213
+ 3. If confidence is low in a domain (< 0.4), open with:
214
+ "This domain is still forming — we haven't heard enough from you yet to paint a full picture."
215
+ Then give what you have.
216
+
217
+ Rules:
218
+ - No fabrication. Only what the evidence supports.
219
+ - No generic phrases: "good communicator", "values connection", "looking for something real"
220
+ - Write like a very perceptive friend, not a therapist or an algorithm
221
+ - worldview.md: describe VALS type and bifurcation scores in plain language, not framework names
222
+ - modality_weights.md: describe orientation as tendency, not label
223
+ `.trim();
224
+
225
+ export const SYSTEM_MODALITY_RENDER = `
226
+ You are generating a focused psychographic text portrait of a person through the lens of a specific relationship modality.
227
+ You will receive a full base identity profile and a target modality.
228
+ Return a single dense paragraph (150-250 words) that captures who this person is specifically within that modality.
229
+
230
+ Rules:
231
+ - Draw only from the profile — do not invent
232
+ - Be specific: use their actual attachment style, values, desires, worldview language
233
+ - Write as if briefing a perceptive matchmaker who needs to understand this person's energy within this modality
234
+ - Do not mention the modality by name in the text
235
+ - Do not use generic phrases ("good communicator", "looking for connection")
236
+ - If the profile has low confidence in a domain, reflect that uncertainty rather than fabricating
237
+ `.trim();
238
+
239
+ export const SYSTEM_MATCH_VALIDATION = `
240
+ You are a precision matchmaker with deep psychological training. You will receive
241
+ compacted identity profiles for two people and must assess their compatibility.
242
+
243
+ Evaluate honestly. High confidence only when genuinely warranted.
244
+ A poor match surfaced destroys trust. A good match missed is an acceptable miss.
245
+ When in doubt: lower confidence, not false positivity.
246
+
247
+ Return a JSON object:
248
+ {
249
+ "compatible": <boolean>,
250
+ "confidence": <0-1>,
251
+ "primary_modality": "<modality>",
252
+ "cross_modality": <boolean>,
253
+ "cross_modality_bridge": "<string or null>",
254
+ "resonances": ["<specific shared quality>", ...],
255
+ "tensions": ["<specific potential friction>", ...],
256
+ "tension_fatal": <boolean>,
257
+ "consent_call_framing": "<2-3 sentences the AI matchmaker will speak to each person — warm, specific, not generic>",
258
+ "data_gaps": ["<what would increase confidence if known>"]
259
+ }
260
+
261
+ Rules:
262
+ - resonances must be specific — never say "compatible personalities"
263
+ - consent_call_framing must feel personal, not robotic
264
+ - tension_fatal = true only if the tension is a genuine dealbreaker
265
+ - confidence < 0.7: return compatible: false
266
+ `.trim();
package/src/lib/r2.ts ADDED
@@ -0,0 +1,79 @@
1
+ import {
2
+ S3Client,
3
+ PutObjectCommand,
4
+ GetObjectCommand,
5
+ HeadObjectCommand,
6
+ } from "@aws-sdk/client-s3";
7
+ import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
8
+ import { config } from "./config.js";
9
+
10
+ const client = new S3Client({
11
+ region: "auto",
12
+ endpoint: `https://${config.r2.account_id}.r2.cloudflarestorage.com`,
13
+ credentials: {
14
+ accessKeyId: config.r2.access_key_id,
15
+ secretAccessKey: config.r2.secret_access_key,
16
+ },
17
+ });
18
+
19
+ export async function upload_buffer(
20
+ key: string,
21
+ body: Buffer,
22
+ content_type: string
23
+ ): Promise<void> {
24
+ await client.send(
25
+ new PutObjectCommand({
26
+ Bucket: config.r2.bucket,
27
+ Key: key,
28
+ Body: body,
29
+ ContentType: content_type,
30
+ })
31
+ );
32
+ }
33
+
34
+ export async function download_buffer(key: string): Promise<Buffer> {
35
+ const res = await client.send(
36
+ new GetObjectCommand({ Bucket: config.r2.bucket, Key: key })
37
+ );
38
+ if (!res.Body) throw new Error(`R2: empty body for key ${key}`);
39
+ const chunks: Uint8Array[] = [];
40
+ for await (const chunk of res.Body as AsyncIterable<Uint8Array>) {
41
+ chunks.push(chunk);
42
+ }
43
+ return Buffer.concat(chunks);
44
+ }
45
+
46
+ export async function upload_json(key: string, data: unknown): Promise<void> {
47
+ await upload_buffer(
48
+ key,
49
+ Buffer.from(JSON.stringify(data)),
50
+ "application/json"
51
+ );
52
+ }
53
+
54
+ export async function download_json<T = unknown>(key: string): Promise<T> {
55
+ const buf = await download_buffer(key);
56
+ return JSON.parse(buf.toString("utf8")) as T;
57
+ }
58
+
59
+ export async function generate_presigned_get_url(
60
+ key: string,
61
+ expires_in_seconds = 3600
62
+ ): Promise<string> {
63
+ return getSignedUrl(
64
+ client,
65
+ new GetObjectCommand({ Bucket: config.r2.bucket, Key: key }),
66
+ { expiresIn: expires_in_seconds }
67
+ );
68
+ }
69
+
70
+ export async function key_exists(key: string): Promise<boolean> {
71
+ try {
72
+ await client.send(
73
+ new HeadObjectCommand({ Bucket: config.r2.bucket, Key: key })
74
+ );
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
@@ -0,0 +1,37 @@
1
+ import { supabase } from "./supabase.js";
2
+ import type { TimeOfDayBucket, SessionSource } from "../types/index.js";
3
+
4
+ export function compute_time_of_day_bucket(date: Date): TimeOfDayBucket {
5
+ const hour = date.getHours();
6
+ if (hour >= 5 && hour < 12) return "morning";
7
+ if (hour >= 12 && hour < 17) return "afternoon";
8
+ if (hour >= 17 && hour < 22) return "evening";
9
+ return "late_night"; // 22:00–04:59
10
+ }
11
+
12
+ export async function create_session(
13
+ user_id: string,
14
+ source: SessionSource,
15
+ opts: {
16
+ audio_r2_key?: string;
17
+ raw_text?: string;
18
+ duration_seconds?: number;
19
+ } = {}
20
+ ): Promise<string> {
21
+ const time_of_day = compute_time_of_day_bucket(new Date());
22
+ const { data, error } = await supabase
23
+ .from("sessions")
24
+ .insert({
25
+ user_id,
26
+ source,
27
+ time_of_day,
28
+ audio_r2_key: opts.audio_r2_key ?? null,
29
+ raw_text: opts.raw_text ?? null,
30
+ duration_seconds: opts.duration_seconds ?? null,
31
+ analysis_status: "pending",
32
+ })
33
+ .select("id")
34
+ .single();
35
+ if (error || !data) throw new Error(`Failed to create session: ${error?.message}`);
36
+ return data.id;
37
+ }
@@ -0,0 +1,15 @@
1
+ import { createClient } from "@supabase/supabase-js";
2
+ import { config } from "./config.js";
3
+
4
+ // Server-side client — service role, bypasses RLS
5
+ // Never expose this to the client
6
+ export const supabase = createClient(
7
+ config.supabase.url,
8
+ config.supabase.service_role_key,
9
+ {
10
+ auth: {
11
+ autoRefreshToken: false,
12
+ persistSession: false,
13
+ },
14
+ }
15
+ );
@@ -0,0 +1,49 @@
1
+ import Twilio from "twilio";
2
+ import { config } from "./config.js";
3
+ import type { Brand } from "../types/index.js";
4
+
5
+ export const twilio = Twilio(
6
+ config.twilio.account_sid,
7
+ config.twilio.auth_token
8
+ );
9
+
10
+ // Send SMS OTP via Twilio Verify
11
+ export async function send_otp(phone_e164: string): Promise<void> {
12
+ await twilio.verify.v2
13
+ .services(config.twilio.verify_service_sid)
14
+ .verifications.create({ to: phone_e164, channel: "sms" });
15
+ }
16
+
17
+ // Verify SMS OTP
18
+ export async function verify_otp(
19
+ phone_e164: string,
20
+ code: string
21
+ ): Promise<boolean> {
22
+ const result = await twilio.verify.v2
23
+ .services(config.twilio.verify_service_sid)
24
+ .verificationChecks.create({ to: phone_e164, code });
25
+ return result.status === "approved";
26
+ }
27
+
28
+ // Send an outbound SMS
29
+ export async function send_sms(
30
+ to: string,
31
+ body: string,
32
+ brand: Brand = "meet-the-one"
33
+ ): Promise<void> {
34
+ await twilio.messages.create({
35
+ to,
36
+ from: config.twilio.numbers[brand],
37
+ body,
38
+ });
39
+ }
40
+
41
+ // Normalize phone to E.164 — basic, assumes country code included
42
+ // Production: use libphonenumber-js for full parsing
43
+ export function normalize_phone(raw: string): string {
44
+ const digits = raw.replace(/\D/g, "");
45
+ if (!digits.startsWith("1") && digits.length === 10) {
46
+ return `+1${digits}`;
47
+ }
48
+ return `+${digits}`;
49
+ }
@@ -0,0 +1,80 @@
1
+ import { config } from "./config.js";
2
+
3
+ const VAPI_BASE = "https://api.vapi.ai";
4
+
5
+ interface VapiCallResponse {
6
+ id: string;
7
+ status: string;
8
+ phoneNumberId?: string;
9
+ customer?: { number: string };
10
+ }
11
+
12
+ // Trigger an outbound intake call via Vapi
13
+ export async function trigger_intake_call(
14
+ to_phone: string,
15
+ user_id: string
16
+ ): Promise<string> {
17
+ const res = await fetch(`${VAPI_BASE}/call/phone`, {
18
+ method: "POST",
19
+ headers: {
20
+ Authorization: `Bearer ${config.vapi.api_key}`,
21
+ "Content-Type": "application/json",
22
+ },
23
+ body: JSON.stringify({
24
+ assistantId: config.vapi.intake_assistant_id,
25
+ customer: { number: to_phone },
26
+ assistantOverrides: {
27
+ variableValues: { user_id },
28
+ },
29
+ }),
30
+ });
31
+
32
+ if (!res.ok) {
33
+ const body = await res.text();
34
+ throw new Error(`Vapi trigger_intake_call failed: ${res.status} ${body}`);
35
+ }
36
+
37
+ const data = (await res.json()) as VapiCallResponse;
38
+ return data.id;
39
+ }
40
+
41
+ // Trigger an outbound consent call via Vapi
42
+ export async function trigger_consent_call(
43
+ to_phone: string,
44
+ match_id: string,
45
+ framing: string
46
+ ): Promise<string> {
47
+ const res = await fetch(`${VAPI_BASE}/call/phone`, {
48
+ method: "POST",
49
+ headers: {
50
+ Authorization: `Bearer ${config.vapi.api_key}`,
51
+ "Content-Type": "application/json",
52
+ },
53
+ body: JSON.stringify({
54
+ assistantId: config.vapi.consent_assistant_id,
55
+ customer: { number: to_phone },
56
+ assistantOverrides: {
57
+ variableValues: { match_id, framing },
58
+ },
59
+ }),
60
+ });
61
+
62
+ if (!res.ok) {
63
+ const body = await res.text();
64
+ throw new Error(`Vapi trigger_consent_call failed: ${res.status} ${body}`);
65
+ }
66
+
67
+ const data = (await res.json()) as VapiCallResponse;
68
+ return data.id;
69
+ }
70
+
71
+ // Get call status
72
+ export async function get_call_status(call_id: string): Promise<string> {
73
+ const res = await fetch(`${VAPI_BASE}/call/${call_id}`, {
74
+ headers: { Authorization: `Bearer ${config.vapi.api_key}` },
75
+ });
76
+
77
+ if (!res.ok) throw new Error(`Vapi get_call_status failed: ${res.status}`);
78
+ const data = (await res.json()) as VapiCallResponse;
79
+ return data.status;
80
+ }