@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,211 @@
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 { embed } from "../lib/openai.js";
5
+ import { download_json, upload_json } from "../lib/r2.js";
6
+ import {
7
+ SYSTEM_SIGNAL_EXTRACTION,
8
+ SYSTEM_IDENTITY_MERGE,
9
+ SYSTEM_MODALITY_RENDER,
10
+ } from "../lib/prompts.js";
11
+ import type { BaseIdentityProfile, ModalityWeights, Modality } from "../types/index.js";
12
+
13
+ // Triggered after transcription completes (or after text submission)
14
+ // Pass 1: Extract signals from session
15
+ // Pass 2: Merge signals into compacted identity
16
+ // Pass 3: Render modality-specific portraits → embed per modality
17
+ export const compact_identity = inngest.createFunction(
18
+ { id: "compact-identity", retries: 2 },
19
+ { event: "session/transcribed" },
20
+ async ({ event, step }) => {
21
+ const { user_id, session_id, transcript_r2_key } = event.data;
22
+
23
+ await step.run("mark-processing", async () => {
24
+ await supabase
25
+ .from("sessions")
26
+ .update({ analysis_status: "processing" })
27
+ .eq("id", session_id);
28
+ });
29
+
30
+ // Load transcript or raw text
31
+ const user_transcript = await step.run("load-transcript", async () => {
32
+ if (transcript_r2_key) {
33
+ const data = await download_json<{ text: string }>(transcript_r2_key);
34
+ return data.text;
35
+ }
36
+ const { data, error } = await supabase
37
+ .from("sessions")
38
+ .select("raw_text")
39
+ .eq("id", session_id)
40
+ .single();
41
+ if (error || !data?.raw_text) throw new Error("No transcript or raw_text found");
42
+ return data.raw_text;
43
+ });
44
+
45
+ const session_meta = await step.run("load-session-meta", async () => {
46
+ const { data, error } = await supabase
47
+ .from("sessions")
48
+ .select("time_of_day, duration_seconds, source")
49
+ .eq("id", session_id)
50
+ .single();
51
+ if (error || !data) throw new Error("Session not found");
52
+ return data;
53
+ });
54
+
55
+ const { count: completed_count } = await supabase
56
+ .from("sessions")
57
+ .select("*", { count: "exact", head: true })
58
+ .eq("user_id", user_id)
59
+ .eq("analysis_status", "complete");
60
+
61
+ const session_number = (completed_count ?? 0) + 1;
62
+
63
+ // ── Pass 1: Signal extraction ─────────────────────────────────────────────
64
+ const session_signals_raw = await step.run("extract-signals", async () => {
65
+ const is_late_night = session_meta.time_of_day === "late_night";
66
+ return complete(
67
+ "analysis",
68
+ SYSTEM_SIGNAL_EXTRACTION,
69
+ [
70
+ `Session #${session_number} (source: ${session_meta.source})`,
71
+ `Time: ${session_meta.time_of_day}${is_late_night ? " [HIGH SIGNAL — 4AM PRINCIPLE]" : ""}`,
72
+ `Duration: ${session_meta.duration_seconds ?? "unknown"}s`,
73
+ `\nTranscript:\n${user_transcript}`,
74
+ ].join("\n")
75
+ );
76
+ });
77
+
78
+ const existing_identity = await step.run("load-identity", async () => {
79
+ const { data } = await supabase
80
+ .from("identities")
81
+ .select("base_profile, modality_weights, session_count")
82
+ .eq("user_id", user_id)
83
+ .single();
84
+ return data;
85
+ });
86
+
87
+ // ── Pass 2: Identity merge ────────────────────────────────────────────────
88
+ const merged_raw = await step.run("merge-identity", async () => {
89
+ const existing_str = existing_identity
90
+ ? JSON.stringify(existing_identity.base_profile, null, 2)
91
+ : "null (first session)";
92
+
93
+ return complete(
94
+ "analysis",
95
+ SYSTEM_IDENTITY_MERGE,
96
+ [
97
+ `Existing identity:\n${existing_str}`,
98
+ `\nNew session signals:\n${session_signals_raw}`,
99
+ `\nSession metadata: ${JSON.stringify(session_meta)}`,
100
+ `\nTotal sessions completed: ${session_number}`,
101
+ ].join("\n\n")
102
+ );
103
+ });
104
+
105
+ const { base_profile, modality_weights } = await step.run(
106
+ "parse-output",
107
+ async () => {
108
+ try {
109
+ return JSON.parse(extract_json(merged_raw)) as {
110
+ base_profile: BaseIdentityProfile;
111
+ modality_weights: ModalityWeights;
112
+ };
113
+ } catch {
114
+ throw new Error(`LLM returned invalid JSON for identity merge: ${merged_raw.slice(0, 200)}`);
115
+ }
116
+ }
117
+ );
118
+
119
+ const completeness = compute_completeness(base_profile);
120
+ const ready =
121
+ completeness >= 0.5 &&
122
+ base_profile.worldview.world_danger_adventure !== undefined;
123
+
124
+ const new_session_count = (existing_identity?.session_count ?? 0) + 1;
125
+
126
+ // ── Pass 3: Modality-specific portraits + embeddings ──────────────────────
127
+ const active_modalities = Object.entries(modality_weights)
128
+ .filter(([, w]) => (w as number) > 0.1)
129
+ .map(([m]) => m as Modality);
130
+
131
+ await step.run("upsert-modality-embeddings", async () => {
132
+ for (const modality of active_modalities) {
133
+ // Render a focused portrait through this modality's lens
134
+ const portrait = await complete(
135
+ "analysis",
136
+ SYSTEM_MODALITY_RENDER,
137
+ [
138
+ `Target modality: ${modality}`,
139
+ `\nFull identity profile:\n${JSON.stringify(base_profile, null, 2)}`,
140
+ `\nModality weights: ${JSON.stringify(modality_weights)}`,
141
+ ].join("\n\n"),
142
+ 512 // short output — dense paragraph only
143
+ );
144
+
145
+ const vec = await embed(portrait);
146
+
147
+ await supabase.rpc("upsert_modality_embedding", {
148
+ p_user_id: user_id,
149
+ p_modality: modality,
150
+ p_embedding: vec,
151
+ });
152
+ }
153
+ });
154
+
155
+ // ── Save identity ─────────────────────────────────────────────────────────
156
+ await step.run("save-identity", async () => {
157
+ await supabase.from("identities").upsert(
158
+ {
159
+ user_id,
160
+ base_profile,
161
+ modality_weights,
162
+ signal_completeness_score: completeness,
163
+ session_count: new_session_count,
164
+ ready_for_matching: ready,
165
+ last_updated: new Date().toISOString(),
166
+ },
167
+ { onConflict: "user_id" }
168
+ );
169
+ });
170
+
171
+ // Audit trail to R2
172
+ await step.run("archive-signals", async () => {
173
+ await upload_json(`signals/${user_id}/${session_id}.json`, {
174
+ session_id,
175
+ signals_raw: session_signals_raw,
176
+ base_profile,
177
+ modality_weights,
178
+ completeness,
179
+ merged_at: new Date().toISOString(),
180
+ });
181
+ });
182
+
183
+ await step.run("mark-complete", async () => {
184
+ await supabase
185
+ .from("sessions")
186
+ .update({ analysis_status: "complete" })
187
+ .eq("id", session_id);
188
+ });
189
+
190
+ await step.sendEvent("trigger-matching", {
191
+ name: "identity/updated",
192
+ data: { user_id },
193
+ });
194
+
195
+ return { user_id, completeness, ready_for_matching: ready, session_count: new_session_count };
196
+ }
197
+ );
198
+
199
+ function compute_completeness(profile: BaseIdentityProfile): number {
200
+ const domain_scores = [
201
+ profile.relationships.confidence,
202
+ profile.desire.confidence,
203
+ profile.money.confidence,
204
+ profile.health.confidence,
205
+ ];
206
+ const domain_avg =
207
+ domain_scores.reduce((a, b) => a + b, 0) / domain_scores.length;
208
+ const has_worldview =
209
+ profile.worldview.world_danger_adventure !== undefined ? 1 : 0;
210
+ return domain_avg * 0.7 + has_worldview * 0.3;
211
+ }
@@ -0,0 +1,87 @@
1
+ import { inngest } from "../lib/inngest.js";
2
+ import { supabase } from "../lib/supabase.js";
3
+ import { trigger_consent_call } from "../lib/vapi.js";
4
+ import { send_sms } from "../lib/twilio.js";
5
+
6
+ // Triggered when a match is confirmed by the matching engine
7
+ // Calls both users via Vapi consent assistant — sequentially with a gap
8
+ // If Vapi call fails, falls back to SMS consent
9
+ export const consent_call = inngest.createFunction(
10
+ { id: "consent-call", retries: 2 },
11
+ { event: "match/confirmed" },
12
+ async ({ event, step }) => {
13
+ const { match_id } = event.data;
14
+
15
+ // Load match + both users' phones
16
+ const match = await step.run("load-match", async () => {
17
+ const { data, error } = await supabase
18
+ .from("matches")
19
+ .select(`
20
+ id,
21
+ user_a_id,
22
+ user_b_id,
23
+ primary_modality,
24
+ confidence_score,
25
+ consent_call_framing,
26
+ status
27
+ `)
28
+ .eq("id", match_id)
29
+ .single();
30
+ if (error || !data) throw new Error(`Match not found: ${match_id}`);
31
+ return data;
32
+ });
33
+
34
+ // Only proceed if still pending consent
35
+ if (match.status !== "pending_consent") {
36
+ return { skipped: true, reason: `match status is ${match.status}` };
37
+ }
38
+
39
+ const users = await step.run("load-users", async () => {
40
+ const { data, error } = await supabase
41
+ .from("users")
42
+ .select("id, phone, brand")
43
+ .in("id", [match.user_a_id, match.user_b_id]);
44
+ if (error || !data || data.length !== 2) {
45
+ throw new Error("Failed to load both users for consent call");
46
+ }
47
+ return data;
48
+ });
49
+
50
+ const user_a = users.find((u) => u.id === match.user_a_id)!;
51
+ const user_b = users.find((u) => u.id === match.user_b_id)!;
52
+ const framing = match.consent_call_framing ?? "";
53
+
54
+ // ── Call user A ───────────────────────────────────────────────────────────
55
+ await step.run("call-user-a", async () => {
56
+ try {
57
+ await trigger_consent_call(user_a.phone, match_id, framing);
58
+ } catch (err) {
59
+ console.error("Vapi consent call failed for user_a, falling back to SMS", err);
60
+ await send_sms(
61
+ user_a.phone,
62
+ `We found someone interesting for you. Reply ACCEPT to connect or DECLINE to pass. [match:${match_id}]`,
63
+ user_a.brand as Parameters<typeof send_sms>[2]
64
+ );
65
+ }
66
+ });
67
+
68
+ // Small delay between calls so they don't land simultaneously
69
+ await step.sleep("gap-between-calls", "2m");
70
+
71
+ // ── Call user B ───────────────────────────────────────────────────────────
72
+ await step.run("call-user-b", async () => {
73
+ try {
74
+ await trigger_consent_call(user_b.phone, match_id, framing);
75
+ } catch (err) {
76
+ console.error("Vapi consent call failed for user_b, falling back to SMS", err);
77
+ await send_sms(
78
+ user_b.phone,
79
+ `We found someone interesting for you. Reply ACCEPT to connect or DECLINE to pass. [match:${match_id}]`,
80
+ user_b.brand as Parameters<typeof send_sms>[2]
81
+ );
82
+ }
83
+ });
84
+
85
+ return { match_id, called: [user_a.id, user_b.id] };
86
+ }
87
+ );
@@ -0,0 +1,166 @@
1
+ import archiver from "archiver";
2
+ import { PassThrough } from "stream";
3
+ import { inngest } from "../lib/inngest.js";
4
+ import { supabase } from "../lib/supabase.js";
5
+ import { complete } from "../lib/anthropic.js";
6
+ import { upload_buffer, generate_presigned_get_url } from "../lib/r2.js";
7
+ import { send_sms } from "../lib/twilio.js";
8
+ import { SYSTEM_IDENTITY_NARRATIVE } from "../lib/prompts.js";
9
+ import type { BaseIdentityProfile, ModalityWeights, Brand } from "../types/index.js";
10
+
11
+ // Triggered by identity/export event (SMS "EXPORT" or web portal)
12
+ // Renders 7 markdown files → zips → uploads to R2 → sends SMS download link
13
+ export const export_identity = inngest.createFunction(
14
+ { id: "export-identity", retries: 1 },
15
+ { event: "identity/export" },
16
+ async ({ event, step }) => {
17
+ const { user_id } = event.data;
18
+
19
+ // Load identity + user
20
+ const { identity, user } = await step.run("load-data", async () => {
21
+ const [id_res, user_res] = await Promise.all([
22
+ supabase
23
+ .from("identities")
24
+ .select("base_profile, modality_weights, signal_completeness_score, session_count")
25
+ .eq("user_id", user_id)
26
+ .single(),
27
+ supabase
28
+ .from("users")
29
+ .select("id, phone, brand")
30
+ .eq("id", user_id)
31
+ .single(),
32
+ ]);
33
+ if (id_res.error || !id_res.data) throw new Error("Identity not found");
34
+ if (user_res.error || !user_res.data) throw new Error("User not found");
35
+ return { identity: id_res.data, user: user_res.data };
36
+ });
37
+
38
+ const profile = identity.base_profile as BaseIdentityProfile;
39
+ const weights = identity.modality_weights as ModalityWeights;
40
+
41
+ // ── Render all narrative files in parallel ────────────────────────────────
42
+ const files = await step.run("render-narratives", async () => {
43
+ const domains = [
44
+ { name: "relationships", data: profile.relationships },
45
+ { name: "desire", data: profile.desire },
46
+ { name: "money", data: profile.money },
47
+ { name: "health", data: profile.health },
48
+ { name: "worldview", data: profile.worldview },
49
+ ];
50
+
51
+ const rendered = await Promise.all(
52
+ domains.map(async ({ name, data }) => {
53
+ const content = await complete(
54
+ "analysis",
55
+ SYSTEM_IDENTITY_NARRATIVE,
56
+ [
57
+ `Domain: ${name}`,
58
+ `\nProfile data:\n${JSON.stringify(data, null, 2)}`,
59
+ `\nFull context (other domains):\n${JSON.stringify(profile, null, 2)}`,
60
+ ].join("\n\n"),
61
+ 1024
62
+ );
63
+ return { filename: `${name}.md`, content: `# My ${capitalize(name)} Identity\n\n${content}` };
64
+ })
65
+ );
66
+
67
+ // Modality weights file
68
+ const modality_content = await complete(
69
+ "analysis",
70
+ SYSTEM_IDENTITY_NARRATIVE,
71
+ [
72
+ `Domain: modality_weights`,
73
+ `\nModality weights:\n${JSON.stringify(weights, null, 2)}`,
74
+ `\nFull profile context:\n${JSON.stringify(profile, null, 2)}`,
75
+ ].join("\n\n"),
76
+ 512
77
+ );
78
+
79
+ rendered.push({
80
+ filename: "modality_weights.md",
81
+ content: `# How I Approach Connection\n\n${modality_content}`,
82
+ });
83
+
84
+ // README
85
+ const completeness_pct = Math.round((identity.signal_completeness_score ?? 0) * 100);
86
+ rendered.push({
87
+ filename: "README.md",
88
+ content: [
89
+ `# My Identity — meet-the-one.ai`,
90
+ ``,
91
+ `Generated: ${new Date().toISOString().slice(0, 10)}`,
92
+ `Sessions: ${identity.session_count ?? 0}`,
93
+ `Completeness: ${completeness_pct}%`,
94
+ ``,
95
+ `## What This Is`,
96
+ ``,
97
+ `This is your portable identity — built from your own words across ${identity.session_count ?? 0} conversation sessions.`,
98
+ `It belongs to you. You can read it, keep it, share it, or import it into compatible platforms.`,
99
+ ``,
100
+ `## Files`,
101
+ ``,
102
+ `- \`relationships.md\` — how you attach, trust, and connect`,
103
+ `- \`desire.md\` — what you want and how you want it`,
104
+ `- \`money.md\` — your relationship with resources and risk`,
105
+ `- \`health.md\` — how you inhabit your body and energy`,
106
+ `- \`worldview.md\` — how you see the world and people in it`,
107
+ `- \`modality_weights.md\` — how you orient toward different kinds of connection`,
108
+ ``,
109
+ `## Privacy`,
110
+ ``,
111
+ `To delete all your data: text DELETE MY DATA to the number you enrolled with.`,
112
+ ``,
113
+ `---`,
114
+ `meet-the-one.ai`,
115
+ ].join("\n"),
116
+ });
117
+
118
+ return rendered;
119
+ });
120
+
121
+ // ── Zip all files + upload to R2 (single step — Buffer can't cross step boundary) ──
122
+ const export_key = `exports/${user_id}/${Date.now()}.zip`;
123
+ await step.run("zip-and-upload", async () => {
124
+ const zip_buf = await new Promise<Buffer>((resolve, reject) => {
125
+ const archive = archiver("zip", { zlib: { level: 6 } });
126
+ const pass = new PassThrough();
127
+ const chunks: Buffer[] = [];
128
+
129
+ pass.on("data", (chunk: Buffer) => chunks.push(chunk));
130
+ pass.on("end", () => resolve(Buffer.concat(chunks)));
131
+ pass.on("error", reject);
132
+ archive.on("error", reject);
133
+
134
+ archive.pipe(pass);
135
+
136
+ for (const { filename, content } of files) {
137
+ archive.append(content, { name: `my-identity/${filename}` });
138
+ }
139
+
140
+ archive.finalize();
141
+ });
142
+
143
+ await upload_buffer(export_key, zip_buf, "application/zip");
144
+ });
145
+
146
+ // ── Generate presigned download URL (24h) ─────────────────────────────────
147
+ const download_url = await step.run("presign-url", async () => {
148
+ return generate_presigned_get_url(export_key, 86400);
149
+ });
150
+
151
+ // ── Send SMS with download link ───────────────────────────────────────────
152
+ await step.run("send-sms", async () => {
153
+ await send_sms(
154
+ user.phone,
155
+ `Your identity export is ready. Download it here (link expires in 24h):\n${download_url}`,
156
+ user.brand as Brand
157
+ );
158
+ });
159
+
160
+ return { user_id, export_key, files: files.length };
161
+ }
162
+ );
163
+
164
+ function capitalize(s: string): string {
165
+ return s.charAt(0).toUpperCase() + s.slice(1);
166
+ }
@@ -0,0 +1,101 @@
1
+ import { inngest } from "../lib/inngest.js";
2
+ import { supabase } from "../lib/supabase.js";
3
+ import { twilio } from "../lib/twilio.js";
4
+ import { send_sms } from "../lib/twilio.js";
5
+ import { config } from "../lib/config.js";
6
+
7
+ // Triggered when both users have accepted the match
8
+ // Creates a Twilio conference and calls both in
9
+ export const introduction_call = inngest.createFunction(
10
+ { id: "introduction-call", retries: 2 },
11
+ { event: "match/both-accepted" },
12
+ async ({ event, step }) => {
13
+ const { match_id } = event.data;
14
+
15
+ const match = await step.run("load-match", async () => {
16
+ const { data, error } = await supabase
17
+ .from("matches")
18
+ .select("id, user_a_id, user_b_id, status")
19
+ .eq("id", match_id)
20
+ .single();
21
+ if (error || !data) throw new Error(`Match not found: ${match_id}`);
22
+ return data;
23
+ });
24
+
25
+ if (match.status !== "both_accepted") {
26
+ return { skipped: true, reason: `match status is ${match.status}` };
27
+ }
28
+
29
+ const users = await step.run("load-users", async () => {
30
+ const { data, error } = await supabase
31
+ .from("users")
32
+ .select("id, phone, brand")
33
+ .in("id", [match.user_a_id, match.user_b_id]);
34
+ if (error || !data || data.length !== 2) {
35
+ throw new Error("Failed to load users for introduction call");
36
+ }
37
+ return data;
38
+ });
39
+
40
+ const user_a = users.find((u) => u.id === match.user_a_id)!;
41
+ const user_b = users.find((u) => u.id === match.user_b_id)!;
42
+
43
+ // Conference room name is deterministic from match_id
44
+ const conference_name = `intro-${match_id}`;
45
+ const from_number = config.twilio.numbers["meet-the-one"];
46
+
47
+ // TwiML to join a named conference
48
+ const conference_twiml = (participant_name: string) =>
49
+ `<?xml version="1.0" encoding="UTF-8"?>
50
+ <Response>
51
+ <Say voice="Polly.Joanna">Please hold while we connect you with your match.</Say>
52
+ <Dial>
53
+ <Conference
54
+ startConferenceOnEnter="false"
55
+ endConferenceOnExit="true"
56
+ waitUrl="https://twimlets.com/holdmusic?Bucket=com.twilio.music.soft-rock"
57
+ statusCallback="${config.app.url}/api/webhooks/twilio/conference"
58
+ statusCallbackEvent="end"
59
+ friendlyName="${participant_name}"
60
+ >${conference_name}</Conference>
61
+ </Dial>
62
+ </Response>`;
63
+
64
+ // Call both users into the conference
65
+ await step.run("dial-both", async () => {
66
+ await Promise.all([
67
+ twilio.calls.create({
68
+ to: user_a.phone,
69
+ from: from_number,
70
+ twiml: conference_twiml("You"),
71
+ }),
72
+ twilio.calls.create({
73
+ to: user_b.phone,
74
+ from: from_number,
75
+ twiml: conference_twiml("You"),
76
+ }),
77
+ ]);
78
+ });
79
+
80
+ // Update match status
81
+ await step.run("mark-introduced", async () => {
82
+ await supabase
83
+ .from("matches")
84
+ .update({ status: "introduced", updated_at: new Date().toISOString() })
85
+ .eq("id", match_id);
86
+ });
87
+
88
+ // Send follow-up SMS to both after the call
89
+ await step.sleep("post-call-delay", "30m");
90
+
91
+ await step.run("send-followup-sms", async () => {
92
+ const msg = "Hope that went well. Let us know how it went — your feedback helps us improve.";
93
+ await Promise.all([
94
+ send_sms(user_a.phone, msg, user_a.brand as Parameters<typeof send_sms>[2]),
95
+ send_sms(user_b.phone, msg, user_b.brand as Parameters<typeof send_sms>[2]),
96
+ ]);
97
+ });
98
+
99
+ return { match_id, introduced: [user_a.id, user_b.id] };
100
+ }
101
+ );
@@ -0,0 +1,65 @@
1
+ import { inngest } from "../lib/inngest.js";
2
+ import { supabase } from "../lib/supabase.js";
3
+
4
+ // Triggered manually or after prompt changes
5
+ // Replays all completed sessions through the current prompt version
6
+ // by re-firing session/transcribed for each session in order
7
+ export const reanalyze_identity = inngest.createFunction(
8
+ { id: "reanalyze-identity", retries: 1 },
9
+ { event: "identity/reanalyze" },
10
+ async ({ event, step }) => {
11
+ const { user_id } = event.data;
12
+
13
+ // Load all completed sessions in chronological order
14
+ const sessions = await step.run("load-sessions", async () => {
15
+ const { data, error } = await supabase
16
+ .from("sessions")
17
+ .select("id, transcript_r2_key, raw_text, analysis_status")
18
+ .eq("user_id", user_id)
19
+ .in("analysis_status", ["complete", "error"])
20
+ .order("created_at", { ascending: true });
21
+ if (error) throw new Error(`Failed to load sessions: ${error.message}`);
22
+ return data ?? [];
23
+ });
24
+
25
+ if (sessions.length === 0) {
26
+ return { skipped: true, reason: "no completed sessions" };
27
+ }
28
+
29
+ // Reset identity
30
+ await step.run("reset-identity", async () => {
31
+ await supabase
32
+ .from("identities")
33
+ .update({
34
+ base_profile: {},
35
+ modality_weights: {},
36
+ signal_completeness_score: 0,
37
+ session_count: 0,
38
+ ready_for_matching: false,
39
+ last_updated: new Date().toISOString(),
40
+ })
41
+ .eq("user_id", user_id);
42
+
43
+ // Clear all modality embeddings
44
+ await supabase
45
+ .from("identity_modality_embeddings")
46
+ .delete()
47
+ .eq("user_id", user_id);
48
+ });
49
+
50
+ // Re-fire each session through compact-identity sequentially
51
+ // Inngest will process them in order via event fan-out
52
+ for (const session of sessions) {
53
+ await step.sendEvent(`replay-${session.id}`, {
54
+ name: "session/transcribed",
55
+ data: {
56
+ user_id,
57
+ session_id: session.id,
58
+ transcript_r2_key: session.transcript_r2_key ?? "",
59
+ },
60
+ });
61
+ }
62
+
63
+ return { user_id, replayed: sessions.length };
64
+ }
65
+ );