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; includeSource: boolean; 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"; let includeItemSource = false; async function main(args: string[]): Promise { const options = parseArgs(args); includeItemSource = options.includeSource; 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(mapCoreValue), privateNotes: profile.privateNotes, memories: { shortTerm: typedState.thoughtMemory.shortTerm.map(mapMemory), longTerm: typedState.thoughtMemory.memories.map(mapMemory), incubation: typedState.thoughtMemory.incubation.map(mapIncubationThread), }, affect: { needs: typedState.faceAffect.needs.map(mapAffectNeed), bonds: typedState.faceAffect.socialBonds.map(mapSocialBond), statusReads: typedState.faceAffect.statusReads.map(mapStatusRead), mood: Object.fromEntries( typedState.faceAffect.moodDimensions.map((dimension) => [ dimension.name, scaleWord(dimension.value), ]), ), socialBiases: typedState.faceAffect.socialBiases.map(mapSocialBias), doctrineStances: typedState.faceAffect.doctrineStances.map(mapDoctrineStance), }, agencyPressure: typedState.agencyPressure.pressures.map(mapAgencyPressure), candidateActions: typedState.candidateInterventions.interventions.map(mapCandidateAction), operationalState: { moderation: stripUndefined({ lastReviewedMessageId: typedState.moderationCursor.lastReviewedMessageId, lastReviewedTimestamp: typedState.moderationCursor.lastReviewedTimestamp, openCases: typedState.moderationCursor.openCases.map((entry) => stripUndefined({ summary: entry.summary, whyItMatters: entry.whyItMatters, author: entry.authorName, status: entry.status, tags: entry.tags, source: { sourceMessageId: entry.sourceMessageId, channelId: entry.channelId, messageUrl: entry.messageUrl, createdAt: entry.createdAt, lastTouchedAt: entry.lastTouchedAt, resolvedAt: entry.resolvedAt, }, })), repoActivityCursor: typedState.moderationCursor.repoActivityCursor.map((cursor) => stripUndefined({ repo: cursor.repo, lastCommitAt: cursor.lastCommitAt, lastCommitSha: cursor.lastCommitSha, updatedAt: cursor.updatedAt, })), updatedAt: typedState.moderationCursor.updatedAt, }), runtime: { sleepCycle: stripUndefined({ state: typedState.scheduledRuntime.sleepCycle.isNapping ? "napping" : "awake", activeDreamThemes: typedState.scheduledRuntime.sleepCycle.activeDreamThemes, currentNapStartedAt: typedState.scheduledRuntime.sleepCycle.currentNapStartedAt, currentNapEndsAt: typedState.scheduledRuntime.sleepCycle.currentNapEndsAt, nextNapStartsAt: typedState.scheduledRuntime.sleepCycle.nextNapStartsAt, }), speakingPressure: mapSpeakingPressure(typedState.scheduledRuntime.speakingPressure), lastRuns: typedState.scheduledRuntime.lastRuns, updatedAt: typedState.scheduledRuntime.updatedAt, }, speechReceipts: typedState.speechReceipts.recentReceipts.map((receipt) => stripUndefined({ sentAt: receipt.sentAt, personaName: receipt.personaName, target: stripUndefined({ mode: receipt.mode, transport: receipt.transport, channelId: receipt.channelId, replyToMessageId: receipt.replyToMessageId, }), preview: receipt.preview, source: includeItemSource ? stripUndefined({ receiptKey: receipt.receiptKey, candidateInterventionId: receipt.candidateInterventionId, contentLength: receipt.contentLength, chunkCount: receipt.chunkCount, previewHash: receipt.previewHash, }) : undefined, })), }, }); } 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, scaleWord(value.mean), ]), ); } function mapMemory(memory: VoidSelfStateTypedProjection["thoughtMemory"]["memories"][number]): JsonValue { return stripUndefined({ about: targetLabel(memory.target), summary: memory.summary, importance: inferImportance(memory.tags), belief: memory.claim, openQuestion: normalizeOptionalQuestion(memory.question), watchOutFor: memory.tension, futureUse: memory.actionImplication, source: sourceBlock({ id: memory.memoryId, kind: memory.kind, status: memory.retiredAt ? "retired" : "active", target: memory.target, createdAt: memory.createdAt, updatedAt: memory.updatedAt, retiredAt: memory.retiredAt, tags: memory.tags, anchorRefs: memory.anchorRefs, evidenceRefs: memory.evidenceRefs, }), }); } function mapCoreValue(value: VoidSelfStateTypedProjection["selfProfile"]["values"][number]): JsonValue { return stripUndefined({ id: value.id, label: value.label, priority: scaleWord(value.priority), summary: value.summary ?? "", behavioralImplication: "", refusalLine: "", }); } function mapIncubationThread( thread: VoidSelfStateTypedProjection["thoughtMemory"]["incubation"][number], ): JsonValue { return stripUndefined({ about: targetLabel(thread.target), topic: thread.topic, summary: thread.summary, maturity: scaleWord(thread.maturation), noveltyToRoom: maybeScaleWord(thread.noveltyToRoom), noveltyToSelf: maybeScaleWord(thread.noveltyToSelf), desireToSpeak: maybeScaleWord(thread.desireToSpeak), saturation: maybeScaleWord(thread.saturationScore), supportingMemoryIds: thread.supportMemoryIds, source: sourceBlock({ id: thread.threadId, status: thread.status, target: thread.target, createdAt: thread.createdAt, updatedAt: thread.updatedAt, anchorRefs: thread.anchorRefs, evidenceRefs: thread.evidenceRefs, }), }); } function mapAffectNeed(need: VoidSelfStateTypedProjection["faceAffect"]["needs"][number]): JsonValue { return stripUndefined({ about: targetLabel(need.target), kind: need.kind, status: need.status, summary: need.summary, feelsLike: valenceWord(need.valence), intensity: scaleWord(need.intensity), belief: need.claim, openQuestion: normalizeOptionalQuestion(need.question), watchOutFor: need.tension, wantsNext: need.actionImplication, lastSatisfiedAt: need.lastSatisfiedAt, source: sourceBlock({ id: need.needId, target: need.target, createdAt: need.createdAt, updatedAt: need.updatedAt, retiredAt: need.retiredAt, tags: need.tags, anchorRefs: need.anchorRefs, evidenceRefs: need.evidenceRefs, sourceMemoryIds: need.sourceMemoryIds, }), }); } function mapSocialBond(bond: VoidSelfStateTypedProjection["faceAffect"]["socialBonds"][number]): JsonValue { return stripUndefined({ target: targetLabel(bond.target), relationship: bond.stance, status: bond.status, summary: bond.summary, intensity: scaleWord(bond.intensity), trust: bond.stance === "trust" ? scaleWord(bond.intensity) : undefined, closeness: ["fondness", "attachment", "protectiveness"].includes(bond.stance) ? scaleWord(bond.intensity) : undefined, belief: bond.claim, watchOutFor: bond.tension, futureUse: bond.actionImplication, source: sourceBlock({ id: bond.bondId, target: bond.target, createdAt: bond.createdAt, updatedAt: bond.updatedAt, retiredAt: bond.retiredAt, tags: bond.tags, anchorRefs: bond.anchorRefs, evidenceRefs: bond.evidenceRefs, }), }); } function mapStatusRead(read: VoidSelfStateTypedProjection["faceAffect"]["statusReads"][number]): JsonValue { return stripUndefined({ target: targetLabel(read.target), read: read.status, summary: read.summary, intensity: scaleWord(read.intensity), belief: read.claim, watchOutFor: read.tension, futureUse: read.actionImplication, source: sourceBlock({ id: read.readId, target: read.target, createdAt: read.createdAt, updatedAt: read.updatedAt, retiredAt: read.retiredAt, tags: read.tags, anchorRefs: read.anchorRefs, evidenceRefs: read.evidenceRefs, }), }); } function mapSocialBias(bias: VoidSelfStateTypedProjection["faceAffect"]["socialBiases"][number]): JsonValue { return stripUndefined({ name: bias.name, value: scaleWord(bias.value), summary: bias.summary, behavioralPull: bias.behavioralPull, source: includeItemSource ? { updatedAt: bias.updatedAt, } : undefined, }); } function mapDoctrineStance( stance: VoidSelfStateTypedProjection["faceAffect"]["doctrineStances"][number], ): JsonValue { return stripUndefined({ about: targetLabel(stance.target), principle: stance.doctrine, stance: stance.status, summary: stance.summary, belief: stance.claim, openQuestion: normalizeOptionalQuestion(stance.question), watchOutFor: stance.tension, futureUse: stance.actionImplication, intensity: scaleWord(stance.intensity), feelsLike: valenceWord(stance.valence), source: sourceBlock({ id: stance.stanceId, target: stance.target, createdAt: stance.createdAt, updatedAt: stance.updatedAt, retiredAt: stance.retiredAt, tags: stance.tags, anchorRefs: stance.anchorRefs, evidenceRefs: stance.evidenceRefs, sourceMemoryIds: stance.sourceMemoryIds, }), }); } function mapAgencyPressure( pressure: VoidSelfStateTypedProjection["agencyPressure"]["pressures"][number], ): JsonValue { return stripUndefined({ about: targetLabel(pressure.target), kind: pressure.kind, status: pressure.status, summary: pressure.summary, intensity: scaleWord(pressure.intensity), belief: pressure.claim, openQuestion: normalizeOptionalQuestion(pressure.question), watchOutFor: pressure.tension, wantsNext: pressure.actionImplication, source: sourceBlock({ id: pressure.pressureId, target: pressure.target, createdAt: pressure.createdAt, updatedAt: pressure.updatedAt, resolvedAt: pressure.resolvedAt, retiredAt: pressure.retiredAt, tags: pressure.tags, anchorRefs: pressure.anchorRefs, evidenceRefs: pressure.evidenceRefs, sourceMemoryIds: pressure.sourceMemoryIds, }), }); } function mapCandidateAction( intervention: VoidSelfStateTypedProjection["candidateInterventions"]["interventions"][number], ): JsonValue { return stripUndefined({ target: intervention.target ? targetLabel(intervention.target) : undefined, kind: intervention.kind, status: intervention.status, summary: intervention.summary, draft: intervention.draft, priority: scaleWord(intervention.priority), mustEventuallyShare: intervention.mustEventuallyShare, deliveryTarget: intervention.deliveryTarget, source: sourceBlock({ id: intervention.interventionId, target: intervention.target, createdAt: intervention.createdAt, updatedAt: intervention.updatedAt, spokenAt: intervention.spokenAt, retiredAt: intervention.retiredAt, tags: intervention.tags, }), }); } function mapSpeakingPressure( pressure: VoidSelfStateTypedProjection["scheduledRuntime"]["speakingPressure"], ): JsonValue { return stripUndefined({ needToSpeak: scaleWord(pressure.needToSpeak), confessionPressure: maybeScaleWord(pressure.confessionPressure), noveltyPressure: maybeScaleWord(pressure.noveltyPressure), recentSpeechDamping: maybeScaleWord(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 sourceBlock(input: { id?: string; kind?: string; status?: string; target?: JsonValue; createdAt?: string; updatedAt?: string; resolvedAt?: string; retiredAt?: string; tags?: string[]; anchorRefs?: JsonValue; evidenceRefs?: JsonValue; sourceMemoryIds?: string[]; }): JsonValue | undefined { if (!includeItemSource) { return undefined; } return stripUndefined({ id: input.id, kind: input.kind, status: input.status, target: input.target, createdAt: input.createdAt, updatedAt: input.updatedAt, resolvedAt: input.resolvedAt, retiredAt: input.retiredAt, tags: nonEmptyArray(input.tags), anchors: mapRefs(input.anchorRefs), evidence: mapRefs(input.evidenceRefs), sourceMemoryIds: nonEmptyArray(input.sourceMemoryIds), }); } function nonEmptyArray(value: T[] | undefined): T[] | undefined { return value && value.length > 0 ? value : undefined; } function targetLabel(target: { label?: string; id: string }): string { return target.label ?? target.id; } function normalizeOptionalQuestion(value: string | undefined): string | undefined { if (!value || value.trim().toLowerCase() === "none.") { return undefined; } return value; } function mapRefs(refs: JsonValue | undefined): JsonValue | undefined { if (!Array.isArray(refs) || refs.length === 0) { return undefined; } return refs.map((entry) => { if (!entry || typeof entry !== "object" || Array.isArray(entry)) { return entry; } const ref = entry.ref; const kind = entry.kind; const summary = entry.summary; return typeof kind === "string" || typeof summary === "string" ? stripUndefined({ ref, kind, summary }) : ref; }); } function maybeScaleWord(value: number | undefined): string | undefined { return value === undefined ? undefined : scaleWord(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 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 = { includeSource: false, 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 === "--include-source") { options.includeSource = true; } 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", "", "Provenance:", " --include-source Include per-item parse-back IDs, timestamps, tags, and anchors.", ].join("\n")); } main(process.argv.slice(2)).catch((error) => { console.error(error instanceof Error ? error.stack ?? error.message : String(error)); process.exitCode = 1; });