import type { Me } from "@rakazo/contracts"; import { localTimezone } from "./local-timezone"; import { resolveUiLocale } from "./ui-locale"; import { inferHelpVibeHints, mergeHelpVibeIntoMemory, type OnboardingFocusId, type UserHelpVibeHints, } from "./user-help-vibe"; import { mergeRelationshipContractIntoMemory } from "./user-relationship-contract"; export interface UserProfileSignals { name?: string; email?: string; timezone: string; locale: string; uiLocale?: string; platform?: string; languages: string[]; defaultProvider?: string | null; defaultModel?: string | null; computerHost?: string | null; harvestedAt: string; } /** Local, respectful signals available during web onboarding (no new permissions). */ export function collectLocalProfileSignals( me?: Pick, ): UserProfileSignals { const locale = typeof navigator !== "undefined" && navigator.language ? navigator.language : "en"; const languages = typeof navigator !== "undefined" && navigator.languages?.length ? [...navigator.languages] : [locale]; return { name: me?.name?.trim() || undefined, email: me?.email?.trim() || undefined, timezone: localTimezone(), locale, uiLocale: resolveUiLocale(), platform: typeof navigator !== "undefined" ? navigator.platform : undefined, languages, defaultProvider: me?.defaultProvider ?? null, defaultModel: me?.defaultModel ?? null, computerHost: me?.computerHost ?? null, harvestedAt: new Date().toISOString(), }; } const PROFILE_SECTION = "## User profile (auto-harvested)"; export function mergeUserProfileIntoMemory(existing: string, signals: UserProfileSignals): string { const block = formatProfileBlock(signals); const marker = PROFILE_SECTION; const start = existing.indexOf(marker); if (start === -1) { const trimmed = existing.trimEnd(); const prefix = trimmed.length ? `${trimmed}\n\n` : ""; return `${prefix}${marker}\n\n${block}\n`; } const rest = existing.slice(start + marker.length); const nextSection = rest.search(/\n## /); const tail = nextSection === -1 ? "" : rest.slice(nextSection); return `${existing.slice(0, start).trimEnd()}\n\n${marker}\n\n${block}${tail}`; } function formatProfileBlock(signals: UserProfileSignals): string { const lines = [ `- **Updated:** ${signals.harvestedAt}`, signals.name ? `- **Name:** ${signals.name}` : null, signals.email ? `- **Email:** ${signals.email}` : null, `- **Timezone:** ${signals.timezone}`, `- **Locale:** ${signals.locale}`, signals.uiLocale ? `- **UI locale:** ${signals.uiLocale}` : null, signals.languages.length ? `- **Languages:** ${signals.languages.join(", ")}` : null, signals.platform ? `- **Platform:** ${signals.platform}` : null, signals.defaultProvider || signals.defaultModel ? `- **Default model:** ${signals.defaultProvider ?? "—"} / ${signals.defaultModel ?? "—"}` : null, signals.computerHost ? `- **Computer host:** ${signals.computerHost}` : null, "", "_Seeded during onboarding from local/browser signals. Extend manually in Memory settings._", ]; return lines.filter((line) => line !== null).join("\n"); } export type MemoryListClient = { memory: { list: (input: { scope?: "user" | "bot"; }) => Promise>; update: (input: { documentId: string; content: string }) => Promise; }; }; /** Profile + optional help/vibe + Oo relationship product contract → user MEMORY.md. */ export function mergeFullUserProfileMemory( existing: string, signals: UserProfileSignals, helpVibe?: UserHelpVibeHints, ): string { let content = mergeUserProfileIntoMemory(existing, signals); if (helpVibe) { content = mergeHelpVibeIntoMemory(content, helpVibe); } return mergeRelationshipContractIntoMemory(content); } /** Persist harvest + help/vibe hints into space user `MEMORY.md` (existing memory system). */ export async function persistUserProfileHarvest( client: MemoryListClient, signals: UserProfileSignals, options?: { focusChoiceId?: OnboardingFocusId }, ): Promise { const helpVibe = inferHelpVibeHints(signals, options); const docs = await client.memory.list({ scope: "user" }); const memoryDoc = docs.find((doc) => doc.path === "MEMORY.md") ?? docs[0]; if (!memoryDoc) return helpVibe; const content = mergeFullUserProfileMemory(memoryDoc.content, signals, helpVibe); if (content !== memoryDoc.content) { await client.memory.update({ documentId: memoryDoc.id, content }); } return helpVibe; }