Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
126 lines
4.6 KiB
TypeScript
126 lines
4.6 KiB
TypeScript
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<Me, "name" | "email" | "defaultProvider" | "defaultModel" | "computerHost">,
|
|
): 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<Array<{ id: string; path: string; content: string }>>;
|
|
update: (input: { documentId: string; content: string }) => Promise<unknown>;
|
|
};
|
|
};
|
|
|
|
/** 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<UserHelpVibeHints> {
|
|
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;
|
|
}
|