@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,243 @@
1
+ import { inngest } from "../lib/inngest.js";
2
+ import { supabase } from "../lib/supabase.js";
3
+ import { complete, extract_json } from "../lib/anthropic.js";
4
+ import { SYSTEM_MATCH_VALIDATION } from "../lib/prompts.js";
5
+ import type { ModalityWeights, Modality } from "../types/index.js";
6
+
7
+ const WORLD_VIEW_DELTA_MAX = 0.5;
8
+ const CONFIDENCE_THRESHOLD = 0.70;
9
+ const MAX_MATCHES_IN_QUEUE = 10;
10
+ const VECTOR_CANDIDATES_PER_MODALITY = 30;
11
+
12
+ // Triggered when a user's identity is updated
13
+ // 1. Vector search per active modality via pgvector RPC
14
+ // 2. Hard-filter on worldview bifurcations
15
+ // 3. LLM-validate each candidate
16
+ // 4. Insert confirmed matches + fire match/confirmed
17
+ export const run_matching = inngest.createFunction(
18
+ { id: "run-matching", retries: 2 },
19
+ { event: "identity/updated" },
20
+ async ({ event, step }) => {
21
+ const { user_id } = event.data;
22
+
23
+ // ── Load identity ─────────────────────────────────────────────────────────
24
+ const identity = await step.run("load-identity", async () => {
25
+ const { data, error } = await supabase
26
+ .from("identities")
27
+ .select("base_profile, modality_weights, signal_completeness_score, ready_for_matching")
28
+ .eq("user_id", user_id)
29
+ .single();
30
+ if (error || !data) throw new Error("Identity not found");
31
+ return data;
32
+ });
33
+
34
+ if (!identity.ready_for_matching) {
35
+ return { skipped: true, reason: "not ready for matching" };
36
+ }
37
+
38
+ // ── Check queue capacity ──────────────────────────────────────────────────
39
+ const { count: pending_count } = await supabase
40
+ .from("matches")
41
+ .select("*", { count: "exact", head: true })
42
+ .or(`user_a_id.eq.${user_id},user_b_id.eq.${user_id}`)
43
+ .eq("status", "pending_consent");
44
+
45
+ if ((pending_count ?? 0) >= MAX_MATCHES_IN_QUEUE) {
46
+ return { skipped: true, reason: "match queue full" };
47
+ }
48
+
49
+ const slots_available = MAX_MATCHES_IN_QUEUE - (pending_count ?? 0);
50
+
51
+ // ── Load user's modality embeddings ───────────────────────────────────────
52
+ const my_embeddings = await step.run("load-embeddings", async () => {
53
+ const { data } = await supabase
54
+ .from("identity_modality_embeddings")
55
+ .select("modality, embedding")
56
+ .eq("user_id", user_id);
57
+ return data ?? [];
58
+ });
59
+
60
+ if (my_embeddings.length === 0) {
61
+ return { skipped: true, reason: "no embeddings yet" };
62
+ }
63
+
64
+ // ── Build exclusion set (existing matches + declined pairs) ───────────────
65
+ const [existing_matches, declined_pairs] = await Promise.all([
66
+ supabase
67
+ .from("matches")
68
+ .select("user_a_id, user_b_id")
69
+ .or(`user_a_id.eq.${user_id},user_b_id.eq.${user_id}`),
70
+ supabase
71
+ .from("declined_pairs")
72
+ .select("user_a_id, user_b_id")
73
+ .or(`user_a_id.eq.${user_id},user_b_id.eq.${user_id}`),
74
+ ]);
75
+
76
+ const excluded_ids = new Set<string>([user_id]); // always exclude self
77
+ for (const p of existing_matches.data ?? []) {
78
+ excluded_ids.add(p.user_a_id);
79
+ excluded_ids.add(p.user_b_id);
80
+ }
81
+ for (const p of declined_pairs.data ?? []) {
82
+ excluded_ids.add(p.user_a_id);
83
+ excluded_ids.add(p.user_b_id);
84
+ }
85
+
86
+ // ── Vector search per active modality ─────────────────────────────────────
87
+ // Best similarity per candidate across all modality searches
88
+ const candidate_scores = new Map<string, { similarity: number; modality: string }>();
89
+
90
+ const vector_results = await step.run("vector-search", async () => {
91
+ const results: Array<{ user_id: string; modality: string; similarity: number }> = [];
92
+
93
+ for (const { modality, embedding } of my_embeddings) {
94
+ const { data, error } = await supabase.rpc("match_identities", {
95
+ p_user_id: user_id,
96
+ p_modality: modality,
97
+ p_embedding: embedding,
98
+ p_limit: VECTOR_CANDIDATES_PER_MODALITY,
99
+ });
100
+ if (error) {
101
+ console.error(`match_identities RPC error for modality ${modality}:`, error);
102
+ continue;
103
+ }
104
+ for (const row of data ?? []) {
105
+ results.push(row);
106
+ }
107
+ }
108
+ return results;
109
+ });
110
+
111
+ for (const row of vector_results) {
112
+ if (excluded_ids.has(row.user_id)) continue;
113
+ const existing = candidate_scores.get(row.user_id);
114
+ if (!existing || row.similarity > existing.similarity) {
115
+ candidate_scores.set(row.user_id, {
116
+ similarity: row.similarity,
117
+ modality: row.modality,
118
+ });
119
+ }
120
+ }
121
+
122
+ // Sort by similarity descending, take top 20 for LLM validation
123
+ const ranked_candidates = Array.from(candidate_scores.entries())
124
+ .sort((a, b) => b[1].similarity - a[1].similarity)
125
+ .slice(0, 20)
126
+ .map(([cid, meta]) => ({ user_id: cid, ...meta }));
127
+
128
+ if (ranked_candidates.length === 0) {
129
+ return { user_id, confirmed_matches: 0, reason: "no vector candidates" };
130
+ }
131
+
132
+ // ── Load candidate full profiles ──────────────────────────────────────────
133
+ const candidate_ids = ranked_candidates.map((c) => c.user_id);
134
+ const { data: candidate_profiles } = await supabase
135
+ .from("identities")
136
+ .select("user_id, base_profile, modality_weights")
137
+ .in("user_id", candidate_ids);
138
+
139
+ const profile_map = new Map(
140
+ (candidate_profiles ?? []).map((p) => [p.user_id, p])
141
+ );
142
+
143
+ // ── Hard filter: worldview bifurcation gates ──────────────────────────────
144
+ const a_wv = identity.base_profile.worldview;
145
+
146
+ const passing_candidates = ranked_candidates.filter((c) => {
147
+ const profile = profile_map.get(c.user_id);
148
+ if (!profile) return false;
149
+ const b_wv = profile.base_profile.worldview;
150
+ const danger_delta = Math.abs(a_wv.world_danger_adventure - b_wv.world_danger_adventure);
151
+ const people_delta = Math.abs(a_wv.people_good_bad - b_wv.people_good_bad);
152
+ return danger_delta <= WORLD_VIEW_DELTA_MAX && people_delta <= WORLD_VIEW_DELTA_MAX;
153
+ });
154
+
155
+ // ── LLM validation ────────────────────────────────────────────────────────
156
+ const confirmed_matches: string[] = [];
157
+
158
+ for (const candidate of passing_candidates) {
159
+ if (confirmed_matches.length >= slots_available) break;
160
+
161
+ const profile = profile_map.get(candidate.user_id)!;
162
+ const primary_modality = dominant_shared_modality(
163
+ identity.modality_weights,
164
+ profile.modality_weights
165
+ );
166
+
167
+ const assessment_raw = await step.run(
168
+ `validate-${candidate.user_id}`,
169
+ async () =>
170
+ complete(
171
+ "matching",
172
+ SYSTEM_MATCH_VALIDATION,
173
+ [
174
+ `Person A profile (${primary_modality} lens):\n${JSON.stringify(identity.base_profile, null, 2)}`,
175
+ `Person A modality weights: ${JSON.stringify(identity.modality_weights)}`,
176
+ `\nPerson B profile (${primary_modality} lens):\n${JSON.stringify(profile.base_profile, null, 2)}`,
177
+ `Person B modality weights: ${JSON.stringify(profile.modality_weights)}`,
178
+ `\nVector similarity score: ${candidate.similarity.toFixed(3)} (modality: ${candidate.modality})`,
179
+ ].join("\n\n")
180
+ )
181
+ );
182
+
183
+ let assessment: Record<string, unknown>;
184
+ try {
185
+ assessment = JSON.parse(extract_json(assessment_raw));
186
+ } catch {
187
+ console.error(`Failed to parse assessment for ${candidate.user_id}`);
188
+ continue;
189
+ }
190
+
191
+ if (!assessment.compatible || assessment.tension_fatal) continue;
192
+ if ((assessment.confidence as number) < CONFIDENCE_THRESHOLD) continue;
193
+
194
+ // Insert match and capture the generated UUID
195
+ const { data: match_row, error: insert_error } = await supabase
196
+ .from("matches")
197
+ .insert({
198
+ user_a_id: user_id,
199
+ user_b_id: candidate.user_id,
200
+ primary_modality,
201
+ cross_modality: assessment.cross_modality ?? false,
202
+ cross_modality_bridge: assessment.cross_modality_bridge ?? null,
203
+ confidence_score: assessment.confidence,
204
+ resonances: assessment.resonances ?? [],
205
+ tensions: assessment.tensions ?? [],
206
+ tension_fatal: false,
207
+ consent_call_framing: assessment.consent_call_framing ?? "",
208
+ status: "pending_consent",
209
+ created_at: new Date().toISOString(),
210
+ })
211
+ .select("id")
212
+ .single();
213
+
214
+ if (insert_error || !match_row) {
215
+ console.error("Failed to insert match:", insert_error);
216
+ continue;
217
+ }
218
+
219
+ confirmed_matches.push(candidate.user_id);
220
+
221
+ await step.sendEvent(`match-confirmed-${match_row.id}`, {
222
+ name: "match/confirmed",
223
+ data: { match_id: match_row.id },
224
+ });
225
+ }
226
+
227
+ return { user_id, confirmed_matches: confirmed_matches.length };
228
+ }
229
+ );
230
+
231
+ function dominant_shared_modality(a: ModalityWeights, b: ModalityWeights): Modality {
232
+ const modalities = Object.keys(a) as Modality[];
233
+ let best = modalities[0];
234
+ let best_score = 0;
235
+ for (const m of modalities) {
236
+ const score = Math.min(a[m], b[m]);
237
+ if (score > best_score) {
238
+ best_score = score;
239
+ best = m;
240
+ }
241
+ }
242
+ return best;
243
+ }
@@ -0,0 +1,59 @@
1
+ import { inngest } from "../lib/inngest.js";
2
+ import { supabase } from "../lib/supabase.js";
3
+
4
+ const MAX_MATCHES_IN_QUEUE = 10;
5
+
6
+ // Hourly cron — finds all ready users with queue capacity and re-runs matching
7
+ // Ensures new users get found by existing users even without a new session
8
+ export const scheduled_matching = inngest.createFunction(
9
+ { id: "scheduled-matching", retries: 1 },
10
+ { cron: "0 * * * *" }, // every hour on the hour
11
+ async ({ step }) => {
12
+ // Find all users ready for matching
13
+ const eligible_users = await step.run("find-eligible-users", async () => {
14
+ const { data, error } = await supabase
15
+ .from("identities")
16
+ .select("user_id")
17
+ .eq("ready_for_matching", true);
18
+ if (error) throw new Error(`Failed to load eligible users: ${error.message}`);
19
+ return data ?? [];
20
+ });
21
+
22
+ if (eligible_users.length === 0) {
23
+ return { triggered: 0 };
24
+ }
25
+
26
+ // Filter to those with queue capacity
27
+ const user_ids = eligible_users.map((u) => u.user_id);
28
+
29
+ const { data: queue_counts } = await supabase
30
+ .from("matches")
31
+ .select("user_a_id, user_b_id")
32
+ .or(user_ids.map((id) => `user_a_id.eq.${id},user_b_id.eq.${id}`).join(","))
33
+ .eq("status", "pending_consent");
34
+
35
+ // Count pending matches per user
36
+ const pending_per_user = new Map<string, number>();
37
+ for (const row of queue_counts ?? []) {
38
+ pending_per_user.set(row.user_a_id, (pending_per_user.get(row.user_a_id) ?? 0) + 1);
39
+ pending_per_user.set(row.user_b_id, (pending_per_user.get(row.user_b_id) ?? 0) + 1);
40
+ }
41
+
42
+ const users_with_capacity = user_ids.filter(
43
+ (id) => (pending_per_user.get(id) ?? 0) < MAX_MATCHES_IN_QUEUE
44
+ );
45
+
46
+ // Fan out identity/updated events — run_matching handles each
47
+ if (users_with_capacity.length > 0) {
48
+ await step.sendEvent(
49
+ "scheduled-matching-fanout",
50
+ users_with_capacity.map((user_id) => ({
51
+ name: "identity/updated" as const,
52
+ data: { user_id },
53
+ }))
54
+ );
55
+ }
56
+
57
+ return { triggered: users_with_capacity.length, total_eligible: eligible_users.length };
58
+ }
59
+ );
@@ -0,0 +1,77 @@
1
+ import { inngest } from "../lib/inngest.js";
2
+ import { supabase } from "../lib/supabase.js";
3
+ import { transcribe_url, user_speech_only } from "../lib/deepgram.js";
4
+ import { generate_presigned_get_url, upload_json } from "../lib/r2.js";
5
+
6
+ // Triggered when a voice session recording lands in R2
7
+ // Runs transcription, stores result, fires next event
8
+ export const transcribe_session = inngest.createFunction(
9
+ { id: "transcribe-session", retries: 3 },
10
+ { event: "session/completed" },
11
+ async ({ event, step }) => {
12
+ const { user_id, session_id } = event.data;
13
+
14
+ // Mark as processing
15
+ await step.run("mark-processing", async () => {
16
+ await supabase
17
+ .from("sessions")
18
+ .update({ analysis_status: "processing" })
19
+ .eq("id", session_id);
20
+ });
21
+
22
+ // Get session record to find audio key
23
+ const session = await step.run("get-session", async () => {
24
+ const { data, error } = await supabase
25
+ .from("sessions")
26
+ .select("audio_r2_key")
27
+ .eq("id", session_id)
28
+ .single();
29
+ if (error || !data) throw new Error(`Session not found: ${session_id}`);
30
+ if (!data.audio_r2_key) throw new Error(`No audio_r2_key for session: ${session_id}`);
31
+ return data;
32
+ });
33
+
34
+ // Generate presigned URL for Deepgram to fetch audio from R2
35
+ const audio_url = await step.run("presign-url", async () => {
36
+ return generate_presigned_get_url(session.audio_r2_key!, 3600);
37
+ });
38
+
39
+ // Transcribe via Deepgram
40
+ const result = await step.run("transcribe", async () => {
41
+ return transcribe_url(audio_url);
42
+ });
43
+
44
+ // Extract user-only speech (speaker 0 = user)
45
+ const user_transcript = user_speech_only(result);
46
+
47
+ // Store transcript JSON to R2
48
+ const transcript_r2_key = `transcripts/${user_id}/${session_id}.json`;
49
+ await step.run("store-transcript", async () => {
50
+ await upload_json(transcript_r2_key, {
51
+ full: result,
52
+ text: user_transcript,
53
+ stored_at: new Date().toISOString(),
54
+ });
55
+ });
56
+
57
+ // Update session record
58
+ await step.run("update-session", async () => {
59
+ await supabase
60
+ .from("sessions")
61
+ .update({
62
+ transcript_r2_key,
63
+ analysis_status: "pending", // reset so compact-identity picks it up
64
+ duration_seconds: Math.round(result.duration_seconds),
65
+ })
66
+ .eq("id", session_id);
67
+ });
68
+
69
+ // Fire next job
70
+ await step.sendEvent("trigger-analysis", {
71
+ name: "session/transcribed",
72
+ data: { user_id, session_id, transcript_r2_key },
73
+ });
74
+
75
+ return { session_id, duration_seconds: result.duration_seconds };
76
+ }
77
+ );
@@ -0,0 +1,37 @@
1
+ import Anthropic from "@anthropic-ai/sdk";
2
+ import { config } from "./config.js";
3
+
4
+ export const anthropic = new Anthropic({
5
+ apiKey: config.anthropic.api_key,
6
+ });
7
+
8
+ // Strip markdown code fences from LLM output and return raw JSON string
9
+ export function extract_json(raw: string): string {
10
+ const fence = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
11
+ if (fence) return fence[1].trim();
12
+ // Try to find first { or [ and last } or ]
13
+ const start = raw.search(/[{[]/);
14
+ const end = Math.max(raw.lastIndexOf("}"), raw.lastIndexOf("]"));
15
+ if (start !== -1 && end !== -1 && end > start) return raw.slice(start, end + 1);
16
+ return raw.trim();
17
+ }
18
+
19
+ // Thin wrapper — returns text content from a single-turn message
20
+ export async function complete(
21
+ model: "conversation" | "analysis" | "matching",
22
+ system: string,
23
+ user: string,
24
+ max_tokens?: number
25
+ ): Promise<string> {
26
+ const default_tokens = model === "analysis" ? 8192 : 4096;
27
+ const response = await anthropic.messages.create({
28
+ model: config.anthropic.models[model],
29
+ max_tokens: max_tokens ?? default_tokens,
30
+ system,
31
+ messages: [{ role: "user", content: user }],
32
+ });
33
+
34
+ const block = response.content[0];
35
+ if (block.type !== "text") throw new Error("Unexpected non-text response");
36
+ return block.text;
37
+ }
@@ -0,0 +1,81 @@
1
+ import "dotenv/config";
2
+
3
+ function require_env(key: string): string {
4
+ const val = process.env[key];
5
+ if (!val) throw new Error(`Missing required env var: ${key}`);
6
+ return val;
7
+ }
8
+
9
+ export const config = {
10
+ // Supabase
11
+ supabase: {
12
+ url: require_env("SUPABASE_URL"),
13
+ service_role_key: require_env("SUPABASE_SERVICE_ROLE_KEY"),
14
+ },
15
+
16
+ // Twilio
17
+ twilio: {
18
+ account_sid: require_env("TWILIO_ACCOUNT_SID"),
19
+ auth_token: require_env("TWILIO_AUTH_TOKEN"),
20
+ verify_service_sid: require_env("TWILIO_VERIFY_SERVICE_SID"),
21
+ // Phone numbers per brand
22
+ numbers: {
23
+ "meet-the-one": require_env("TWILIO_NUMBER_FLAGSHIP"),
24
+ casual: process.env["TWILIO_NUMBER_CASUAL"] ?? "",
25
+ kink: process.env["TWILIO_NUMBER_KINK"] ?? "",
26
+ adventure: process.env["TWILIO_NUMBER_ADVENTURE"] ?? "",
27
+ "open-poly": process.env["TWILIO_NUMBER_OPEN_POLY"] ?? "",
28
+ },
29
+ },
30
+
31
+ // Vapi (voice AI — v0.1)
32
+ vapi: {
33
+ api_key: require_env("VAPI_API_KEY"),
34
+ intake_assistant_id: require_env("VAPI_INTAKE_ASSISTANT_ID"),
35
+ consent_assistant_id: require_env("VAPI_CONSENT_ASSISTANT_ID"),
36
+ },
37
+
38
+ // Anthropic
39
+ anthropic: {
40
+ api_key: require_env("ANTHROPIC_API_KEY"),
41
+ models: {
42
+ conversation: "claude-sonnet-4-6",
43
+ analysis: "claude-opus-4-6",
44
+ matching: "claude-sonnet-4-6",
45
+ },
46
+ },
47
+
48
+ // OpenAI (embeddings)
49
+ openai: {
50
+ api_key: require_env("OPENAI_API_KEY"),
51
+ embedding_model: "text-embedding-3-large",
52
+ embedding_dimensions: 1536, // truncated — pgvector local limit is 2000
53
+ },
54
+
55
+ // Deepgram (transcription)
56
+ deepgram: {
57
+ api_key: require_env("DEEPGRAM_API_KEY"),
58
+ model: "nova-2",
59
+ },
60
+
61
+ // Cloudflare R2 (audio/transcript storage)
62
+ r2: {
63
+ account_id: require_env("CLOUDFLARE_ACCOUNT_ID"),
64
+ access_key_id: require_env("R2_ACCESS_KEY_ID"),
65
+ secret_access_key: require_env("R2_SECRET_ACCESS_KEY"),
66
+ bucket: require_env("R2_BUCKET_NAME"),
67
+ endpoint: `https://${require_env("CLOUDFLARE_ACCOUNT_ID")}.r2.cloudflarestorage.com`,
68
+ },
69
+
70
+ // Inngest
71
+ inngest: {
72
+ event_key: require_env("INNGEST_EVENT_KEY"),
73
+ signing_key: require_env("INNGEST_SIGNING_KEY"),
74
+ },
75
+
76
+ // App
77
+ app: {
78
+ url: process.env["APP_URL"] ?? "http://localhost:3000",
79
+ port: parseInt(process.env["PORT"] ?? "3000", 10),
80
+ },
81
+ } as const;
@@ -0,0 +1,57 @@
1
+ import { createClient } from "@deepgram/sdk";
2
+ import { config } from "./config.js";
3
+
4
+ export const deepgram = createClient(config.deepgram.api_key);
5
+
6
+ export interface TranscriptResult {
7
+ transcript: string; // full text
8
+ words: Array<{
9
+ word: string;
10
+ start: number;
11
+ end: number;
12
+ confidence: number;
13
+ speaker: number;
14
+ }>;
15
+ duration_seconds: number;
16
+ }
17
+
18
+ // Transcribe audio from a URL (e.g. presigned R2 URL)
19
+ export async function transcribe_url(
20
+ audio_url: string
21
+ ): Promise<TranscriptResult> {
22
+ const { result, error } = await deepgram.listen.prerecorded.transcribeUrl(
23
+ { url: audio_url },
24
+ {
25
+ model: config.deepgram.model,
26
+ diarize: true, // speaker separation
27
+ punctuate: true,
28
+ utterances: true,
29
+ smart_format: true,
30
+ }
31
+ );
32
+
33
+ if (error) throw new Error(`Deepgram error: ${error.message}`);
34
+ if (!result) throw new Error("No result from Deepgram");
35
+
36
+ const channel = result.results.channels[0].alternatives[0];
37
+
38
+ return {
39
+ transcript: channel.transcript,
40
+ words: (channel.words ?? []).map((w) => ({
41
+ word: w.word,
42
+ start: w.start,
43
+ end: w.end,
44
+ confidence: w.confidence,
45
+ speaker: w.speaker ?? 0,
46
+ })),
47
+ duration_seconds: result.metadata.duration,
48
+ };
49
+ }
50
+
51
+ // Extract only user speech (speaker 1) from transcript words
52
+ export function user_speech_only(result: TranscriptResult): string {
53
+ return result.words
54
+ .filter((w) => w.speaker === 1)
55
+ .map((w) => w.word)
56
+ .join(" ");
57
+ }
@@ -0,0 +1,33 @@
1
+ import { Inngest } from "inngest";
2
+ import { config } from "./config.js";
3
+
4
+ export const inngest = new Inngest({
5
+ id: "meet-the-one",
6
+ eventKey: config.inngest.event_key,
7
+ });
8
+
9
+ // ─── Event type definitions ───────────────────────────────────────────────────
10
+
11
+ export type Events = {
12
+ "session/completed": {
13
+ data: { user_id: string; session_id: string };
14
+ };
15
+ "session/transcribed": {
16
+ data: { user_id: string; session_id: string; transcript_r2_key: string };
17
+ };
18
+ "identity/updated": {
19
+ data: { user_id: string };
20
+ };
21
+ "match/confirmed": {
22
+ data: { match_id: string };
23
+ };
24
+ "match/both-accepted": {
25
+ data: { match_id: string };
26
+ };
27
+ "identity/reanalyze": {
28
+ data: { user_id: string };
29
+ };
30
+ "identity/export": {
31
+ data: { user_id: string };
32
+ };
33
+ };
@@ -0,0 +1,14 @@
1
+ import OpenAI from "openai";
2
+ import { config } from "./config.js";
3
+
4
+ export const openai = new OpenAI({ apiKey: config.openai.api_key });
5
+
6
+ // Generate embedding vector for a text string
7
+ export async function embed(text: string): Promise<number[]> {
8
+ const response = await openai.embeddings.create({
9
+ model: config.openai.embedding_model,
10
+ input: text,
11
+ dimensions: config.openai.embedding_dimensions,
12
+ });
13
+ return response.data[0].embedding;
14
+ }