import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { loadVoidSelfStateTypedDocuments, type VoidSelfStateTypedProjection, } from "@voidbot/core"; interface CliOptions { identityPath?: string; identityId?: string; displayName?: string; repoName?: string; avatarUri?: string; repoPath?: string; statePath: string; outPath: string; } interface RepoFaceIdentity { identityId?: string; displayName?: string; repoName?: string; avatarUrl?: string; description?: string; repoPath?: string; createdOrRefreshedAt?: string; } type JsonValue = | null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue | undefined }; const DEFAULT_IDENTITY_PATH = "E:/Projects/Sai/.voidbot/voice/identity.json"; const DEFAULT_STATE_PATH = "E:/Projects/Sai/.voidbot/state/sai.cc"; const DEFAULT_OUT_PATH = "docs/persona-intake/sai.persona-intake.yaml"; async function main(args: string[]): Promise { const options = parseArgs(args); const identity = await resolveIdentity(options); const identityDefaults = identity.displayName ? { agentId: identity.identityId ?? identity.displayName, publicName: identity.displayName, publicDescription: identity.description, } : undefined; const typedState = await loadVoidSelfStateTypedDocuments({ canonicalPath: options.statePath, identity: identityDefaults, }); const projection = buildFriendlyPersonaInput({ identity, identityPath: options.identityPath, statePath: options.statePath, typedState, }); const yaml = [ "# Generated by scripts/export-persona-friendly-input.ts.", "# This is a human-friendly projection of typed Persona state, not the canonical state owner.", toYaml(projection), ].join("\n"); const outPath = resolve(options.outPath); await mkdir(dirname(outPath), { recursive: true }); await writeFile(outPath, `${yaml.trimEnd()}\n`, "utf8"); console.log(`Wrote ${outPath}`); } function buildFriendlyPersonaInput(input: { identity: RepoFaceIdentity; identityPath?: string; statePath: string; typedState: VoidSelfStateTypedProjection; }): JsonValue { const { identity, typedState } = input; const profile = typedState.selfProfile; const updatedAt = latestTimestamp([ profile.updatedAt, typedState.thoughtMemory.updatedAt, typedState.faceAffect.updatedAt, typedState.agencyPressure.updatedAt, typedState.candidateInterventions.updatedAt, typedState.scheduledRuntime.updatedAt, typedState.speechReceipts.updatedAt, typedState.moderationCursor.updatedAt, ]); return stripUndefined({ schemaVersion: "gamecult.persona_intake.v0", source: { authority: "projection", sourceSystem: identity.repoPath ? "voidbot.repo_face_state" : "voidbot.native_persona_state", sourceStatePath: resolve(input.statePath), sourceIdentityPath: input.identityPath ? resolve(input.identityPath) : undefined, projectedAt: new Date().toISOString(), sourceUpdatedAt: updatedAt, }, persona: { id: profile.agentId, publicName: profile.publicName, publicDescription: profile.publicDescription ?? "", pronouns: "", avatarUri: identity.avatarUrl ?? "", jurisdiction: identity.repoName ? identity.repoPath ? `Repo Face for ${identity.repoName}.` : `Native VoidBot Persona for ${identity.repoName}.` : identity.repoPath ? "Repo Face." : "Native VoidBot Persona.", sourceRepo: identity.repoPath ? { kind: "repo", id: identity.repoName ?? profile.agentId, label: identity.repoName ?? profile.publicName, path: identity.repoPath, } : undefined, }, presentation: { voiceSummary: inferVoiceSummary(profile.publicDescription), defaultRenderer: "chat", homeContext: { kind: identity.repoPath ? "repo" : "self", id: identity.repoName ?? profile.agentId, label: identity.repoName ?? profile.publicName, }, publicHandles: [], }, personality: mapActivationProfile(profile.activationProfile), coreValues: profile.values.map((value) => ({ id: value.id, label: value.label, priority: scaleWord(value.priority), rawPriority: round(value.priority), summary: value.summary ?? "", behavioralImplication: "", refusalLine: "", })), privateNotes: profile.privateNotes, memories: { shortTerm: typedState.thoughtMemory.shortTerm.map(mapMemory), longTerm: typedState.thoughtMemory.memories.map(mapMemory), incubation: typedState.thoughtMemory.incubation.map((thread) => stripUndefined({ id: thread.threadId, status: thread.status, topic: thread.topic, summary: thread.summary, target: thread.target, maturation: scaleWord(thread.maturation), rawMaturation: round(thread.maturation), noveltyToRoom: maybeScale(thread.noveltyToRoom), noveltyToSelf: maybeScale(thread.noveltyToSelf), desireToSpeak: maybeScale(thread.desireToSpeak), saturation: maybeScale(thread.saturationScore), supportMemoryIds: thread.supportMemoryIds, anchorRefs: thread.anchorRefs, evidenceRefs: thread.evidenceRefs, createdAt: thread.createdAt, updatedAt: thread.updatedAt, })), }, affect: { needs: typedState.faceAffect.needs.map((need) => stripUndefined({ id: need.needId, kind: need.kind, status: need.status, target: need.target, summary: need.summary, claim: need.claim, question: need.question, tension: need.tension, actionImplication: need.actionImplication, intensity: scaleWord(need.intensity), rawIntensity: round(need.intensity), valence: valenceWord(need.valence), rawValence: round(need.valence), anchorRefs: need.anchorRefs, evidenceRefs: need.evidenceRefs, sourceMemoryIds: need.sourceMemoryIds, lastSatisfiedAt: need.lastSatisfiedAt, createdAt: need.createdAt, updatedAt: need.updatedAt, retiredAt: need.retiredAt, tags: need.tags, })), bonds: typedState.faceAffect.socialBonds.map((bond) => stripUndefined({ id: bond.bondId, target: bond.target, relationship: bond.stance, status: bond.status, summary: bond.summary, claim: bond.claim, tension: bond.tension, actionImplication: bond.actionImplication, trust: bond.stance === "trust" ? scaleWord(bond.intensity) : undefined, closeness: ["fondness", "attachment", "protectiveness"].includes(bond.stance) ? scaleWord(bond.intensity) : undefined, intensity: scaleWord(bond.intensity), rawIntensity: round(bond.intensity), anchorRefs: bond.anchorRefs, evidenceRefs: bond.evidenceRefs, createdAt: bond.createdAt, updatedAt: bond.updatedAt, retiredAt: bond.retiredAt, tags: bond.tags, })), statusReads: typedState.faceAffect.statusReads.map((read) => stripUndefined({ id: read.readId, status: read.status, target: read.target, summary: read.summary, claim: read.claim, tension: read.tension, actionImplication: read.actionImplication, intensity: scaleWord(read.intensity), rawIntensity: round(read.intensity), anchorRefs: read.anchorRefs, evidenceRefs: read.evidenceRefs, createdAt: read.createdAt, updatedAt: read.updatedAt, retiredAt: read.retiredAt, tags: read.tags, })), mood: Object.fromEntries( typedState.faceAffect.moodDimensions.map((dimension) => [ dimension.name, stripUndefined({ value: scaleWord(dimension.value), rawValue: round(dimension.value), source: dimension.source, updatedAt: dimension.updatedAt, }), ]), ), socialBiases: typedState.faceAffect.socialBiases.map((bias) => ({ name: bias.name, value: scaleWord(bias.value), rawValue: round(bias.value), summary: bias.summary, behavioralPull: bias.behavioralPull, updatedAt: bias.updatedAt, })), doctrineStances: typedState.faceAffect.doctrineStances.map((stance) => stripUndefined({ id: stance.stanceId, principle: stance.doctrine, stance: stance.status, target: stance.target, summary: stance.summary, claim: stance.claim, question: stance.question, tension: stance.tension, actionImplication: stance.actionImplication, intensity: scaleWord(stance.intensity), rawIntensity: round(stance.intensity), valence: valenceWord(stance.valence), rawValence: round(stance.valence), anchorRefs: stance.anchorRefs, evidenceRefs: stance.evidenceRefs, sourceMemoryIds: stance.sourceMemoryIds, createdAt: stance.createdAt, updatedAt: stance.updatedAt, retiredAt: stance.retiredAt, tags: stance.tags, })), }, agencyPressure: typedState.agencyPressure.pressures.map((pressure) => stripUndefined({ id: pressure.pressureId, kind: pressure.kind, status: pressure.status, target: pressure.target, summary: pressure.summary, claim: pressure.claim, question: pressure.question, tension: pressure.tension, actionImplication: pressure.actionImplication, intensity: scaleWord(pressure.intensity), rawIntensity: round(pressure.intensity), anchorRefs: pressure.anchorRefs, evidenceRefs: pressure.evidenceRefs, sourceMemoryIds: pressure.sourceMemoryIds, createdAt: pressure.createdAt, updatedAt: pressure.updatedAt, resolvedAt: pressure.resolvedAt, retiredAt: pressure.retiredAt, tags: pressure.tags, })), candidateActions: typedState.candidateInterventions.interventions.map((intervention) => stripUndefined({ id: intervention.interventionId, kind: intervention.kind, status: intervention.status, target: intervention.target, summary: intervention.summary, draft: intervention.draft, deliveryTarget: intervention.deliveryTarget, priority: scaleWord(intervention.priority), rawPriority: round(intervention.priority), mustEventuallyShare: intervention.mustEventuallyShare, createdAt: intervention.createdAt, updatedAt: intervention.updatedAt, spokenAt: intervention.spokenAt, retiredAt: intervention.retiredAt, tags: intervention.tags, })), operationalState: { moderation: { lastReviewedMessageId: typedState.moderationCursor.lastReviewedMessageId, lastReviewedTimestamp: typedState.moderationCursor.lastReviewedTimestamp, openCases: typedState.moderationCursor.openCases, repoActivityCursor: typedState.moderationCursor.repoActivityCursor, updatedAt: typedState.moderationCursor.updatedAt, }, runtime: { sleepCycle: typedState.scheduledRuntime.sleepCycle, speakingPressure: mapSpeakingPressure(typedState.scheduledRuntime.speakingPressure), lastRuns: typedState.scheduledRuntime.lastRuns, updatedAt: typedState.scheduledRuntime.updatedAt, }, speechReceipts: typedState.speechReceipts.recentReceipts.map((receipt) => receipt), }, }); } function mapActivationProfile( profile: VoidSelfStateTypedProjection["selfProfile"]["activationProfile"], ): JsonValue { return { underlyingOrganization: mapActivationCategory(profile.underlyingOrganization), stableDispositions: mapActivationCategory(profile.stableDispositions), behavioralDimensions: mapActivationCategory(profile.behavioralDimensions), presentationStrategy: mapActivationCategory(profile.presentationStrategy), voiceStyle: mapActivationCategory(profile.voiceStyle), situationalState: mapActivationCategory(profile.situationalState), }; } function mapActivationCategory( category: Record, ): JsonValue { return Object.fromEntries( Object.entries(category).map(([key, value]) => [ key, { level: scaleWord(value.mean), activation: scaleWord(value.current_activation), plasticity: scaleWord(value.plasticity), raw: { mean: round(value.mean), currentActivation: round(value.current_activation), plasticity: round(value.plasticity), }, }, ]), ); } function mapMemory(memory: VoidSelfStateTypedProjection["thoughtMemory"]["memories"][number]): JsonValue { return stripUndefined({ id: memory.memoryId, kind: memory.kind, status: memory.retiredAt ? "retired" : "active", summary: memory.summary, claim: memory.claim, question: memory.question, tension: memory.tension, actionImplication: memory.actionImplication, importance: inferImportance(memory.tags), target: memory.target, anchorRefs: memory.anchorRefs, evidenceRefs: memory.evidenceRefs, createdAt: memory.createdAt, updatedAt: memory.updatedAt, retiredAt: memory.retiredAt, tags: memory.tags, }); } function mapSpeakingPressure( pressure: VoidSelfStateTypedProjection["scheduledRuntime"]["speakingPressure"], ): JsonValue { return stripUndefined({ needToSpeak: scaleWord(pressure.needToSpeak), rawNeedToSpeak: round(pressure.needToSpeak), confessionPressure: maybeScale(pressure.confessionPressure), noveltyPressure: maybeScale(pressure.noveltyPressure), recentSpeechDamping: maybeScale(pressure.recentSpeechDamping), lastSpokeAt: pressure.lastSpokeAt, lastHeraldAt: pressure.lastHeraldAt, }); } function inferVoiceSummary(description: string | undefined): string { if (!description) { return ""; } const sentence = description .split(/(?<=[.!?])\s+/) .find((line) => /should|voice|speak|herald|pride|tone/i.test(line)); return sentence?.trim() ?? ""; } function inferImportance(tags: string[]): string { if (tags.some((tag) => /identity|value|doctrine/i.test(tag))) { return "high"; } return "medium"; } function maybeScale(value: number | undefined): JsonValue | undefined { return value === undefined ? undefined : { value: scaleWord(value), rawValue: round(value), }; } function scaleWord(value: number): "very_low" | "low" | "medium" | "high" | "very_high" { if (value < 0.2) { return "very_low"; } if (value < 0.4) { return "low"; } if (value < 0.65) { return "medium"; } if (value < 0.85) { return "high"; } return "very_high"; } function valenceWord(value: number): "negative" | "mixed_negative" | "neutral" | "mixed_positive" | "positive" { if (value <= -0.6) { return "negative"; } if (value < -0.15) { return "mixed_negative"; } if (value <= 0.15) { return "neutral"; } if (value < 0.6) { return "mixed_positive"; } return "positive"; } function latestTimestamp(values: Array): string { return values .filter((value): value is string => Boolean(value)) .sort((left, right) => left.localeCompare(right)) .at(-1) ?? new Date(0).toISOString(); } function round(value: number): number { return Number(value.toFixed(3)); } function stripUndefined(value: T): T { if (Array.isArray(value)) { return value.map((entry) => stripUndefined(entry)) as T; } if (!value || typeof value !== "object") { return value; } const output: Record = {}; for (const [key, child] of Object.entries(value)) { if (child !== undefined) { output[key] = stripUndefined(child as JsonValue); } } return output as T; } function toYaml(value: JsonValue, indent = 0): string { const pad = " ".repeat(indent); if (Array.isArray(value)) { if (value.length === 0) { return "[]"; } return value .map((entry) => { if (isScalar(entry)) { return `${pad}- ${formatScalar(entry)}`; } const rendered = toYaml(entry, indent + 2); return `${pad}- ${rendered.trimStart()}`; }) .join("\n"); } if (value && typeof value === "object") { const entries = Object.entries(value).filter(([, child]) => child !== undefined); if (entries.length === 0) { return "{}"; } return entries .map(([key, child]) => { if (isScalar(child)) { return `${pad}${key}: ${formatScalar(child)}`; } const rendered = toYaml(child as JsonValue, indent + 2); if (rendered === "[]" || rendered === "{}") { return `${pad}${key}: ${rendered}`; } return `${pad}${key}:\n${rendered}`; }) .join("\n"); } return formatScalar(value); } function isScalar(value: unknown): value is null | boolean | number | string { return value === null || ["boolean", "number", "string"].includes(typeof value); } function formatScalar(value: null | boolean | number | string): string { if (value === null) { return "null"; } if (typeof value === "boolean" || typeof value === "number") { return String(value); } return JSON.stringify(value); } async function readIdentity(path: string): Promise { const raw = await readFile(path, "utf8"); return JSON.parse(raw) as RepoFaceIdentity; } async function resolveIdentity(options: CliOptions): Promise { const fileIdentity = options.identityPath ? await readIdentity(options.identityPath) : {}; return { ...fileIdentity, identityId: options.identityId ?? fileIdentity.identityId, displayName: options.displayName ?? fileIdentity.displayName, repoName: options.repoName ?? fileIdentity.repoName, avatarUrl: options.avatarUri ?? fileIdentity.avatarUrl, repoPath: options.repoPath ?? fileIdentity.repoPath, }; } function parseArgs(args: string[]): CliOptions { const options: CliOptions = { identityPath: DEFAULT_IDENTITY_PATH, statePath: DEFAULT_STATE_PATH, outPath: DEFAULT_OUT_PATH, }; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; const value = args[index + 1]; if (arg === "--identity" && value) { options.identityPath = value; index += 1; } else if (arg === "--no-identity-file") { options.identityPath = undefined; } else if (arg === "--id" && value) { options.identityId = value; index += 1; } else if (arg === "--name" && value) { options.displayName = value; index += 1; } else if (arg === "--repo-name" && value) { options.repoName = value; index += 1; } else if (arg === "--avatar-uri" && value) { options.avatarUri = value; index += 1; } else if (arg === "--repo-path" && value) { options.repoPath = value; index += 1; } else if (arg === "--state" && value) { options.statePath = value; index += 1; } else if (arg === "--out" && value) { options.outPath = value; index += 1; } else if (arg === "--help" || arg === "-h") { printUsage(); process.exit(0); } else { throw new Error(`Unknown or incomplete argument: ${arg}`); } } return options; } function printUsage(): void { console.log([ "Usage: npx tsx scripts/export-persona-friendly-input.ts [--identity path | --no-identity-file] [--state path] [--out path]", "", `Defaults:`, ` --identity ${DEFAULT_IDENTITY_PATH}`, ` --state ${DEFAULT_STATE_PATH}`, ` --out ${DEFAULT_OUT_PATH}`, "", "Native Persona options:", " --no-identity-file --state .voidbot/private/personas/metame/metame.cc --out docs/persona-intake/metame.persona-intake.yaml", ].join("\n")); } main(process.argv.slice(2)).catch((error) => { console.error(error instanceof Error ? error.stack ?? error.message : String(error)); process.exitCode = 1; });