770 lines
24 KiB
TypeScript
770 lines
24 KiB
TypeScript
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<void> {
|
|
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<string, { mean: number; plasticity: number; current_activation: number }>,
|
|
): 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<T>(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 | undefined>): string {
|
|
return values
|
|
.filter((value): value is string => Boolean(value))
|
|
.sort((left, right) => left.localeCompare(right))
|
|
.at(-1) ?? new Date(0).toISOString();
|
|
}
|
|
|
|
function stripUndefined<T extends JsonValue | undefined>(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<string, JsonValue> = {};
|
|
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<RepoFaceIdentity> {
|
|
const raw = await readFile(path, "utf8");
|
|
return JSON.parse(raw) as RepoFaceIdentity;
|
|
}
|
|
|
|
async function resolveIdentity(options: CliOptions): Promise<RepoFaceIdentity> {
|
|
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;
|
|
});
|