ei-tui 0.6.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1510 +1,9 @@
1
- import YAML from "yaml";
2
- import type {
3
- PersonaEntity,
4
- HumanEntity,
5
- HumanSettings,
6
- CeremonyConfig,
7
- OpenCodeSettings,
8
- BackupConfig,
9
- Fact,
10
- PersonaTrait,
11
- Topic,
12
- Person,
13
- PersonIdentifier,
14
- PersonaTopic,
15
- ProviderAccount,
16
- ProviderType,
17
- Quote,
18
- Message,
19
- LLMRequest,
20
- LLMRequestState,
21
- LLMPriority,
22
- ToolProvider,
23
- ToolDefinition,
24
- } from "../../../src/core/types.js";
25
- import { ContextStatus } from "../../../src/core/types.js";
26
- import type { ClaudeCodeSettings } from "../../../src/integrations/claude-code/types.js";
27
- import type { CursorSettings } from "../../../src/integrations/cursor/types.js";
28
- import { BUILT_IN_FACT_NAMES } from "../../../src/core/constants/built-in-facts.js";
29
- import { BUILT_IN_IDENTIFIER_TYPES } from "../../../src/core/constants/built-in-identifier-types.js";
30
-
31
- // =============================================================================
32
- // TYPES FOR YAML EDITING
33
- // =============================================================================
34
-
35
- interface EditableTopic extends Omit<Topic, 'persona_groups'> {
36
- _delete?: boolean;
37
- persona_groups?: Record<string, boolean>[];
38
- }
39
-
40
- interface EditableFact extends Omit<Fact, 'persona_groups'> {
41
- _delete?: boolean;
42
- persona_groups?: Record<string, boolean>[];
43
- }
44
-
45
- interface YAMLPersonIdentifier {
46
- type: string;
47
- value: string;
48
- primary?: true;
49
- }
50
-
51
- interface EditablePersonYAML extends Omit<Person, 'identifiers' | 'persona_groups'> {
52
- identifiers: YAMLPersonIdentifier[];
53
- _delete?: boolean;
54
- persona_groups?: Record<string, boolean>[];
55
- }
56
-
57
- interface EditablePersonaData {
58
- display_name?: string;
59
- aliases?: string[];
60
- short_description?: string;
61
- long_description?: string;
62
- model?: string | null;
63
- group_primary?: string | null;
64
- groups_visible?: Record<string, boolean>[];
65
- traits: YAMLTrait[];
66
- topics: YAMLPersonaTopic[];
67
- heartbeat_delay_ms?: string | number;
68
- context_window_hours?: number;
69
- is_paused?: boolean;
70
- pause_until?: string;
71
- is_static?: boolean;
72
- include_message_timestamps?: boolean;
73
- tools?: Record<string, Record<string, boolean>>;
74
- }
75
-
76
- interface EditableHumanData {
77
- facts: EditableFact[];
78
- topics: EditableTopic[];
79
- people: EditablePersonYAML[];
80
- }
81
-
82
- // =============================================================================
83
- // PLACEHOLDER MARKERS (stripped on parse if unchanged)
84
- // =============================================================================
85
-
86
- const PLACEHOLDER_LONG_DESC = "Detailed description of this persona's personality, background, and role";
87
-
88
- // Placeholder types without id/_delete - these are for YAML display only
89
- interface YAMLTrait {
90
- name: string;
91
- description: string;
92
- sentiment: number;
93
- strength: number;
94
- }
95
-
96
- interface YAMLPersonaTopic {
97
- name: string;
98
- perspective: string;
99
- approach: string;
100
- personal_stake: string;
101
- exposure_current: number;
102
- exposure_desired: number;
103
- }
104
-
105
- const PLACEHOLDER_TRAIT: YAMLTrait = {
106
- name: "Example Trait",
107
- description: "Delete this placeholder or modify it to define a real trait",
108
- sentiment: 0,
109
- strength: 0.5,
110
- };
111
- const PLACEHOLDER_TOPIC: YAMLPersonaTopic = {
112
- name: "Example Topic",
113
- perspective: "How this persona views or thinks about this topic",
114
- approach: "How this persona prefers to engage with this topic",
115
- personal_stake: "Why this topic matters to this persona personally",
116
- exposure_current: 0.5,
117
- exposure_desired: 0.5,
118
- };
119
-
120
- // =============================================================================
121
- // TOOL MAP HELPERS (for persona YAML — human-readable grouped tool toggles)
122
- // =============================================================================
123
-
124
- /**
125
- * Build the nested tools map for YAML: { providerDisplayName: { toolDisplayName: bool } }
126
- * enabledToolIds = persona.tools (array of ToolDefinition IDs)
127
- */
128
- function buildPersonaToolsMap(
129
- enabledToolIds: string[],
130
- allTools: ToolDefinition[],
131
- allProviders: import('../../../src/core/types.js').ToolProvider[]
132
- ): Record<string, Record<string, boolean>> | undefined {
133
- if (allTools.length === 0) return undefined;
134
- const enabledSet = new Set(enabledToolIds);
135
- const result: Record<string, Record<string, boolean>> = {};
136
- for (const provider of allProviders.filter(p => p.enabled)) {
137
- const providerTools = allTools.filter(t => t.provider_id === provider.id);
138
- if (providerTools.length === 0) continue;
139
- result[provider.display_name] = Object.fromEntries(
140
- providerTools.map(t => [t.display_name, enabledSet.has(t.id)])
141
- );
142
- }
143
- return Object.keys(result).length > 0 ? result : undefined;
144
- }
145
-
146
- /**
147
- * Flatten the nested tools map back to an array of ToolDefinition IDs.
148
- * toolsMap keys are provider display names; inner keys are tool display names.
149
- */
150
- function resolvePersonaToolsFromMap(
151
- toolsMap: Record<string, Record<string, boolean>> | undefined,
152
- allTools: ToolDefinition[],
153
- allProviders: import('../../../src/core/types.js').ToolProvider[]
154
- ): string[] | undefined {
155
- if (!toolsMap) return undefined;
156
- const enabledIds: string[] = [];
157
- for (const [providerDisplayName, toolToggles] of Object.entries(toolsMap)) {
158
- const provider = allProviders.find(p => p.display_name === providerDisplayName);
159
- if (!provider) continue;
160
- for (const [toolDisplayName, enabled] of Object.entries(toolToggles)) {
161
- if (!enabled) continue;
162
- const tool = allTools.find(t => t.provider_id === provider.id && t.display_name === toolDisplayName);
163
- if (tool) enabledIds.push(tool.id);
164
- }
165
- }
166
- return enabledIds.length > 0 ? enabledIds : [];
167
- }
168
-
169
- // =============================================================================
170
- // PERSONA SERIALIZATION
171
- // =============================================================================
172
-
173
- /**
174
- * Generate YAML skeleton for a NEW persona (doesn't exist yet)
175
- */
176
- export function newPersonaToYAML(name: string, allTools?: ToolDefinition[], allProviders?: import('../../../src/core/types.js').ToolProvider[]): string {
177
- const toolsMap = buildPersonaToolsMap([], allTools ?? [], allProviders ?? []);
178
-
179
- const data: EditablePersonaData = {
180
- display_name: name,
181
- long_description: PLACEHOLDER_LONG_DESC,
182
- model: undefined,
183
- group_primary: "General",
184
- groups_visible: [{ General: true }],
185
- traits: [PLACEHOLDER_TRAIT],
186
- topics: [PLACEHOLDER_TOPIC],
187
- tools: toolsMap,
188
- };
189
-
190
- return YAML.stringify(data, {
191
- lineWidth: 0,
192
- });
193
- }
194
-
195
- /**
196
- * Parse YAML for a NEW persona (creates PersonaEntity from scratch)
197
- */
198
- export function newPersonaFromYAML(yamlContent: string, allTools?: ToolDefinition[], allProviders?: import('../../../src/core/types.js').ToolProvider[]): Partial<PersonaEntity> {
199
- const data = YAML.parse(yamlContent) as EditablePersonaData;
200
-
201
- const isTraitPlaceholder = (t: YAMLTrait) =>
202
- t.name === PLACEHOLDER_TRAIT.name &&
203
- t.description === PLACEHOLDER_TRAIT.description;
204
-
205
- const traits: PersonaTrait[] = [];
206
- for (const t of data.traits ?? []) {
207
- if (isTraitPlaceholder(t)) {
208
- continue;
209
- }
210
- traits.push({
211
- id: crypto.randomUUID(),
212
- name: t.name,
213
- description: t.description,
214
- sentiment: t.sentiment ?? 0,
215
- strength: t.strength,
216
- last_updated: new Date().toISOString(),
217
- });
218
- }
219
-
220
- const isTopicPlaceholder = (t: YAMLPersonaTopic) =>
221
- t.name === PLACEHOLDER_TOPIC.name &&
222
- t.perspective === PLACEHOLDER_TOPIC.perspective;
223
-
224
- const topics: PersonaTopic[] = [];
225
- for (const t of data.topics ?? []) {
226
- if (isTopicPlaceholder(t)) {
227
- continue;
228
- }
229
- topics.push({
230
- id: crypto.randomUUID(),
231
- name: t.name,
232
- perspective: t.perspective,
233
- approach: t.approach,
234
- personal_stake: t.personal_stake,
235
- sentiment: 0,
236
- exposure_current: t.exposure_current,
237
- exposure_desired: t.exposure_desired,
238
- last_updated: new Date().toISOString(),
239
- });
240
- }
241
-
242
- const stripPlaceholder = (value: string | undefined, placeholder: string): string | undefined => {
243
- return value === placeholder ? undefined : value;
244
- };
245
-
246
- // Convert Record<string, boolean>[] to string[] - only include groups with true value
247
- const groupsVisible: string[] = [];
248
- for (const groupRecord of data.groups_visible ?? []) {
249
- for (const [groupName, isVisible] of Object.entries(groupRecord)) {
250
- if (isVisible) {
251
- groupsVisible.push(groupName);
252
- }
253
- }
254
- }
255
-
256
- return {
257
- long_description: stripPlaceholder(data.long_description, PLACEHOLDER_LONG_DESC),
258
- model: data.model ?? undefined,
259
- group_primary: data.group_primary ?? "General",
260
- groups_visible: groupsVisible.length > 0 ? groupsVisible : ["General"],
261
- traits,
262
- topics,
263
- heartbeat_delay_ms: data.heartbeat_delay_ms,
264
- context_window_hours: data.context_window_hours,
265
- tools: resolvePersonaToolsFromMap(data.tools, allTools ?? [], allProviders ?? []),
266
- };
267
- }
268
-
269
- export function personaToYAML(persona: PersonaEntity, allGroups?: string[], allTools?: ToolDefinition[], allProviders?: import('../../../src/core/types.js').ToolProvider[], accounts?: ProviderAccount[]): string {
270
- const useTraitPlaceholder = persona.traits.length === 0;
271
- const useTopicPlaceholder = persona.topics.length === 0;
272
-
273
- const groupsForYAML: Record<string, boolean>[] = [];
274
- const visibleSet = new Set(persona.groups_visible ?? []);
275
- const groupsToShow = allGroups ?? persona.groups_visible ?? [];
276
- for (const groupName of groupsToShow) {
277
- groupsForYAML.push({ [groupName]: visibleSet.has(groupName) });
278
- }
279
-
280
- const toolsMap = buildPersonaToolsMap(persona.tools ?? [], allTools ?? [], allProviders ?? []);
281
-
282
- const modelDisplay = (persona.model && accounts && accounts.length > 0)
283
- ? modelGuidToDisplay(persona.model, accounts)
284
- : persona.model;
285
-
286
- const data: EditablePersonaData = {
287
- display_name: persona.display_name,
288
- aliases: persona.aliases,
289
- short_description: persona.short_description,
290
- long_description: persona.long_description || PLACEHOLDER_LONG_DESC,
291
- model: modelDisplay ?? null,
292
- group_primary: persona.group_primary,
293
- groups_visible: groupsForYAML,
294
- traits: useTraitPlaceholder
295
- ? [PLACEHOLDER_TRAIT]
296
- : persona.traits.map(({ name, description, sentiment, strength }) => ({ name, description, sentiment: sentiment ?? 0, strength: strength ?? 0.5 })),
297
- topics: useTopicPlaceholder
298
- ? [PLACEHOLDER_TOPIC]
299
- : persona.topics.map(({ name, perspective, approach, personal_stake, exposure_current, exposure_desired }) => ({
300
- name, perspective, approach, personal_stake, exposure_current, exposure_desired
301
- })),
302
- heartbeat_delay_ms: persona.heartbeat_delay_ms || 'default',
303
- context_window_hours: persona.context_window_hours,
304
- is_paused: persona.is_paused || undefined,
305
- pause_until: persona.pause_until,
306
- is_static: persona.is_static || undefined,
307
- include_message_timestamps: persona.include_message_timestamps || undefined,
308
- tools: toolsMap,
309
- };
310
-
311
- return YAML.stringify(data, {
312
- lineWidth: 0,
313
- });
314
- }
315
-
316
- export interface PersonaYAMLResult {
317
- updates: Partial<PersonaEntity>;
318
- deletedTraitIds: string[];
319
- deletedTopicIds: string[];
320
- }
321
-
322
- export function personaFromYAML(yamlContent: string, original: PersonaEntity, allTools?: ToolDefinition[], allProviders?: import('../../../src/core/types.js').ToolProvider[], accounts?: ProviderAccount[]): PersonaYAMLResult {
323
- const data = YAML.parse(yamlContent) as EditablePersonaData;
324
-
325
- const deletedTraitIds: string[] = [];
326
- const deletedTopicIds: string[] = [];
327
-
328
- const isTraitPlaceholder = (t: YAMLTrait) =>
329
- t.name === PLACEHOLDER_TRAIT.name &&
330
- t.description === PLACEHOLDER_TRAIT.description;
331
-
332
- const traits: PersonaTrait[] = [];
333
- for (const t of data.traits ?? []) {
334
- if (isTraitPlaceholder(t)) {
335
- continue;
336
- }
337
- const existing = original.traits.find(orig => orig.name === t.name);
338
- traits.push({
339
- id: existing?.id ?? crypto.randomUUID(),
340
- name: t.name,
341
- description: t.description,
342
- sentiment: t.sentiment ?? existing?.sentiment ?? 0,
343
- strength: t.strength,
344
- last_updated: new Date().toISOString(),
345
- });
346
- }
347
-
348
- for (const orig of original.traits) {
349
- if (!traits.some(t => t.id === orig.id)) {
350
- deletedTraitIds.push(orig.id);
351
- }
352
- }
353
-
354
- const isTopicPlaceholder = (t: YAMLPersonaTopic) =>
355
- t.name === PLACEHOLDER_TOPIC.name &&
356
- t.perspective === PLACEHOLDER_TOPIC.perspective;
357
-
358
- const topics: PersonaTopic[] = [];
359
- for (const t of data.topics ?? []) {
360
- if (isTopicPlaceholder(t)) {
361
- continue;
362
- }
363
- const existing = original.topics.find(orig => orig.name === t.name);
364
- topics.push({
365
- id: existing?.id ?? crypto.randomUUID(),
366
- name: t.name,
367
- perspective: t.perspective,
368
- approach: t.approach,
369
- personal_stake: t.personal_stake,
370
- sentiment: existing?.sentiment ?? 0,
371
- exposure_current: t.exposure_current,
372
- exposure_desired: t.exposure_desired,
373
- last_updated: new Date().toISOString(),
374
- });
375
- }
376
-
377
- for (const orig of original.topics) {
378
- if (!topics.some(t => t.id === orig.id)) {
379
- deletedTopicIds.push(orig.id);
380
- }
381
- }
382
-
383
- const stripPlaceholder = (value: string | undefined, placeholder: string): string | undefined => {
384
- return value === placeholder ? undefined : value;
385
- };
386
-
387
- const groupsVisible: string[] = [];
388
- for (const groupRecord of data.groups_visible ?? []) {
389
- for (const [groupName, isVisible] of Object.entries(groupRecord)) {
390
- if (isVisible) {
391
- groupsVisible.push(groupName);
392
- }
393
- }
394
- }
395
-
396
- let resolvedModel: string | undefined = data.model ?? undefined;
397
- if (data.model && accounts && accounts.length > 0) {
398
- const guid = displayToModelGuid(data.model, accounts);
399
- if (guid !== undefined) {
400
- resolvedModel = guid;
401
- } else if (data.model.includes(':')) {
402
- throw new Error(`Model "${data.model}" not found. Use "ProviderName:modelName" format with a valid provider and model.`);
403
- }
404
- }
405
-
406
- const updates: Partial<PersonaEntity> = {
407
- display_name: data.display_name,
408
- aliases: data.aliases,
409
- short_description: data.short_description,
410
- long_description: stripPlaceholder(data.long_description, PLACEHOLDER_LONG_DESC),
411
- model: resolvedModel,
412
- group_primary: data.group_primary,
413
- groups_visible: groupsVisible,
414
- traits,
415
- topics,
416
- heartbeat_delay_ms: stripPlaceholder(data.heartbeat_delay_ms as string | undefined, 'default'),
417
- context_window_hours: data.context_window_hours,
418
- is_paused: data.is_paused ?? false,
419
- pause_until: data.pause_until,
420
- is_static: data.is_static ?? false,
421
- include_message_timestamps: data.include_message_timestamps ?? false,
422
- tools: resolvePersonaToolsFromMap(data.tools, allTools ?? [], allProviders ?? []),
423
- last_updated: new Date().toISOString(),
424
- };
425
-
426
- return { updates, deletedTraitIds, deletedTopicIds };
427
- }
428
-
429
- // =============================================================================
430
- // HUMAN SERIALIZATION
431
- // =============================================================================
432
-
433
- function toYAMLIdentifiers(identifiers: PersonIdentifier[], personaLookup?: Map<string, string>): YAMLPersonIdentifier[] {
434
- return identifiers.map(({ type, value, is_primary }) => {
435
- const resolvedValue = type === 'Ei Persona' ? (personaLookup?.get(value) ?? value) : value;
436
- const entry: YAMLPersonIdentifier = { type, value: resolvedValue };
437
- if (is_primary) entry.primary = true;
438
- return entry;
439
- });
440
- }
441
-
442
- function knownTypesComment(personaLookup?: Map<string, string>): string {
443
- const lines = [`# Valid types: ${BUILT_IN_IDENTIFIER_TYPES.join(', ')}`];
444
- if (personaLookup && personaLookup.size > 0) {
445
- lines.push(`# Personas: ${Array.from(personaLookup.values()).join(', ')}`);
446
- }
447
- return lines.join('\n');
448
- }
449
-
450
- type WithReadOnlyFields = {
451
- learned_on?: string;
452
- learned_by?: string;
453
- last_updated: string;
454
- last_changed_by?: string;
455
- last_mentioned?: string;
456
- };
457
-
458
- function readOnlyToEnd<T extends WithReadOnlyFields>(item: T): T {
459
- const { learned_on, learned_by, last_updated, last_changed_by, last_mentioned, ...rest } = item;
460
- return { ...rest, learned_by, learned_on, last_changed_by, last_updated, last_mentioned } as T;
461
- }
462
-
463
- function buildGroupCheckboxMap(itemGroups: string[], allGroups: string[]): Record<string, boolean>[] {
464
- const activeSet = new Set(itemGroups);
465
- return [...new Set([...allGroups, ...itemGroups])].map(g => ({ [g]: activeSet.has(g) }));
466
- }
467
-
468
- export function humanToYAML(human: HumanEntity, personaLookup?: Map<string, string>, allGroups: string[] = []): string {
469
- const data: EditableHumanData = {
470
- facts: human.facts.map(f => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(f); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
471
- topics: human.topics.map(t => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(t); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
472
- people: human.people.map(p => {
473
- const { identifiers, interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(p);
474
- return {
475
- ...rest,
476
- persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups),
477
- identifiers: toYAMLIdentifiers(identifiers ?? [], personaLookup),
478
- _delete: false as const,
479
- };
480
- }),
481
- };
482
-
483
- const personComment = knownTypesComment(personaLookup);
484
-
485
- return YAML.stringify(data, {
486
- lineWidth: 0,
487
- })
488
- .replace(/^(\s+)(learned_by: )(.+)$/mg, (_, indent, key, val) => {
489
- const trimmed = val.trim();
490
- const displayName = personaLookup?.get(trimmed) ?? trimmed;
491
- return `${indent}# [read-only] ${key}${displayName}`;
492
- })
493
- .replace(/^(\s+)(last_changed_by: )(.+)$/mg, (_, indent, key, val) => {
494
- const trimmed = val.trim();
495
- const displayName = personaLookup?.get(trimmed) ?? trimmed;
496
- return `${indent}# [read-only] ${key}${displayName}`;
497
- })
498
- .replace(/^(\s+)(learned_on: .+)$/mg, '$1# [read-only] $2')
499
- .replace(/^(\s+)(last_mentioned: .+)$/mg, '$1# [read-only] $2')
500
- .replace(/^(\s+)(last_updated: .+)$/mg, '$1# [read-only] $2')
501
- .replace(/^(\s+)(identifiers:)/mg, (_, indent, _key) => {
502
- return `${indent}${personComment}\n${indent}identifiers:`;
503
- });
504
- }
505
-
506
- export interface HumanYAMLResult {
507
- facts: Fact[];
508
- topics: Topic[];
509
- people: Person[];
510
- deletedFactIds: string[];
511
- deletedTopicIds: string[];
512
- deletedPersonIds: string[];
513
- }
514
-
515
- function parseGroupCheckboxMap(groups: Record<string, boolean>[] | undefined): string[] {
516
- if (!groups) return [];
517
- const result: string[] = [];
518
- for (const record of groups) {
519
- for (const [name, active] of Object.entries(record)) {
520
- if (active) result.push(name);
521
- }
522
- }
523
- return result;
524
- }
525
-
526
- export function humanFromYAML(yamlContent: string, original?: HumanEntity): HumanYAMLResult {
527
- const stripped = yamlContent
528
- .split('\n')
529
- .filter(line => !/^\s*#\s*\[read-only\]/.test(line))
530
- .join('\n');
531
- const data = YAML.parse(stripped) as EditableHumanData;
532
-
533
- const deletedFactIds: string[] = [];
534
- const deletedTopicIds: string[] = [];
535
- const deletedPersonIds: string[] = [];
536
-
537
- const facts: Fact[] = [];
538
- for (const f of data.facts ?? []) {
539
- if (f._delete && !BUILT_IN_FACT_NAMES.has(f.name)) {
540
- deletedFactIds.push(f.id);
541
- } else {
542
- const { _delete, persona_groups: groupMap, ...parsed } = f;
543
- const originalFact = original?.facts.find(of => of.id === parsed.id);
544
- const fact: Fact = originalFact
545
- ? { ...originalFact, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
546
- : { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
547
- if (fact.description && !fact.validated_date) {
548
- fact.validated_date = new Date().toISOString();
549
- }
550
- facts.push(fact);
551
- }
552
- }
553
-
554
- const topics: Topic[] = [];
555
- for (const t of data.topics ?? []) {
556
- if (t._delete) {
557
- deletedTopicIds.push(t.id);
558
- } else {
559
- const { _delete, persona_groups: groupMap, ...parsed } = t;
560
- const originalTopic = original?.topics.find(ot => ot.id === parsed.id);
561
- const topic: Topic = originalTopic
562
- ? { ...originalTopic, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
563
- : { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
564
- topics.push(topic);
565
- }
566
- }
567
-
568
- const people: Person[] = [];
569
- for (const p of data.people ?? []) {
570
- if (p._delete) {
571
- deletedPersonIds.push(p.id);
572
- } else {
573
- const { _delete, identifiers: yamlIdentifiers, persona_groups: groupMap, ...parsed } = p;
574
- const identifiers: PersonIdentifier[] = (yamlIdentifiers ?? []).map(({ type, value, primary }) => ({
575
- type,
576
- value,
577
- ...(primary ? { is_primary: true } : {}),
578
- }));
579
- const originalPerson = original?.people.find(op => op.id === parsed.id);
580
- const personBase: Person = originalPerson
581
- ? { ...originalPerson, ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) }
582
- : { ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) };
583
- const person: Person = !personBase.validated_date
584
- ? { ...personBase, validated_date: new Date().toISOString() }
585
- : personBase;
586
- people.push(person);
587
- }
588
- }
589
-
590
- return {
591
- facts,
592
- topics,
593
- people,
594
- deletedFactIds,
595
- deletedTopicIds,
596
- deletedPersonIds,
597
- };
598
- }
599
-
600
- // =============================================================================
601
- // SETTINGS SERIALIZATION
602
- // =============================================================================
603
-
604
- interface EditableSettingsData {
605
- default_model?: string | null;
606
- oneshot_model?: string | null;
607
- rewrite_model?: string | null;
608
- time_mode?: "24h" | "12h" | "local" | "utc" | null;
609
- name_display?: string | null;
610
- default_heartbeat_ms?: number | null;
611
- default_context_window_hours?: number | null;
612
- message_min_count?: number | null;
613
- message_max_age_days?: number | null;
614
- ceremony?: {
615
- time: string;
616
- decay_rate?: number | null;
617
- explore_threshold?: number | null;
618
- dedup_threshold?: number | null;
619
- event_window_hours?: number | null;
620
- };
621
- opencode?: {
622
- integration?: boolean | null;
623
- polling_interval_ms?: number | null;
624
- last_sync?: string | null;
625
- extraction_point?: string | null;
626
- extraction_model?: string | null;
627
- };
628
- claudeCode?: {
629
- integration?: boolean | null;
630
- polling_interval_ms?: number | null;
631
- last_sync?: string | null;
632
- extraction_point?: string | null;
633
- extraction_model?: string | null;
634
- };
635
- cursor?: {
636
- integration?: boolean | null;
637
- polling_interval_ms?: number | null;
638
- last_sync?: string | null;
639
- extraction_point?: string | null;
640
- };
641
- backup?: {
642
- enabled?: boolean | null;
643
- max_backups?: number | null;
644
- interval_ms?: number | null;
645
- };
646
- }
647
-
648
- export function settingsToYAML(settings: HumanSettings | undefined, accounts: ProviderAccount[]): string {
649
- const guidToDisplay = (guid: string | undefined | null): string | null => {
650
- if (!guid) return null;
651
- return modelGuidToDisplay(guid, accounts);
652
- };
653
-
654
- // Always show all editable fields, using null for unset values so YAML displays them
655
- const data: EditableSettingsData = {
656
- default_model: guidToDisplay(settings?.default_model),
657
- oneshot_model: guidToDisplay(settings?.oneshot_model),
658
- rewrite_model: guidToDisplay(settings?.rewrite_model),
659
- time_mode: settings?.time_mode ?? null,
660
- name_display: settings?.name_display ?? null,
661
- default_heartbeat_ms: settings?.default_heartbeat_ms ?? 1800000,
662
- default_context_window_hours: settings?.default_context_window_hours ?? 8,
663
- message_min_count: settings?.message_min_count ?? 200,
664
- message_max_age_days: settings?.message_max_age_days ?? 14,
665
- ceremony: {
666
- time: settings?.ceremony?.time ?? "09:00",
667
- decay_rate: settings?.ceremony?.decay_rate ?? null,
668
- explore_threshold: settings?.ceremony?.explore_threshold ?? null,
669
- dedup_threshold: settings?.ceremony?.dedup_threshold ?? null,
670
- event_window_hours: settings?.ceremony?.event_window_hours ?? null,
671
- },
672
- opencode: {
673
- integration: settings?.opencode?.integration ?? false,
674
- polling_interval_ms: settings?.opencode?.polling_interval_ms ?? 60000,
675
- extraction_model: guidToDisplay(settings?.opencode?.extraction_model) ?? 'default',
676
- last_sync: settings?.opencode?.last_sync ?? null,
677
- extraction_point: settings?.opencode?.extraction_point ?? null,
678
- },
679
- claudeCode: {
680
- integration: settings?.claudeCode?.integration ?? false,
681
- polling_interval_ms: settings?.claudeCode?.polling_interval_ms ?? 60000,
682
- extraction_model: guidToDisplay(settings?.claudeCode?.extraction_model) ?? 'default',
683
- last_sync: settings?.claudeCode?.last_sync ?? null,
684
- extraction_point: settings?.claudeCode?.extraction_point ?? null,
685
- },
686
- cursor: {
687
- integration: settings?.cursor?.integration ?? false,
688
- polling_interval_ms: settings?.cursor?.polling_interval_ms ?? 60000,
689
- last_sync: settings?.cursor?.last_sync ?? null,
690
- extraction_point: settings?.cursor?.extraction_point ?? null,
691
- },
692
- backup: {
693
- enabled: settings?.backup?.enabled ?? false,
694
- max_backups: settings?.backup?.max_backups ?? 24,
695
- interval_ms: settings?.backup?.interval_ms ?? 3600000,
696
- },
697
- };
698
-
699
- return YAML.stringify(data, {
700
- lineWidth: 0,
701
- })
702
- .replace(/^(\s+)(last_sync: .+)$/mg, '$1# [read-only] $2')
703
- .replace(/^(\s+)(extraction_point: .+)$/mg, '$1# [read-only] $2')
704
- .replace(/^(\s+)(extraction_model: .+)$/mg, '$1$2 # e.g. Anthropic:claude-haiku-4-5');
705
- }
706
- export function settingsFromYAML(yamlContent: string, original: HumanSettings | undefined, accounts: ProviderAccount[]): HumanSettings {
707
- const data = YAML.parse(yamlContent) as EditableSettingsData;
708
-
709
- const nullToUndefined = <T>(value: T | null | undefined): T | undefined =>
710
- value === null ? undefined : value;
711
-
712
- const displayToGuid = (display: string | null | undefined): string | undefined => {
713
- if (!display || display === 'default') return undefined;
714
- return displayToModelGuid(display, accounts) ?? display;
715
- };
716
-
717
- let ceremony: CeremonyConfig | undefined;
718
- if (data.ceremony) {
719
- ceremony = {
720
- time: data.ceremony.time,
721
- decay_rate: nullToUndefined(data.ceremony.decay_rate),
722
- explore_threshold: nullToUndefined(data.ceremony.explore_threshold),
723
- dedup_threshold: nullToUndefined(data.ceremony.dedup_threshold),
724
- event_window_hours: nullToUndefined(data.ceremony.event_window_hours),
725
- last_ceremony: original?.ceremony?.last_ceremony,
726
- };
727
- }
728
-
729
- let opencode: OpenCodeSettings | undefined;
730
- if (data.opencode) {
731
- opencode = {
732
- integration: nullToUndefined(data.opencode.integration),
733
- polling_interval_ms: nullToUndefined(data.opencode.polling_interval_ms),
734
- last_sync: original?.opencode?.last_sync,
735
- extraction_point: original?.opencode?.extraction_point,
736
- processed_sessions: original?.opencode?.processed_sessions,
737
- extraction_model: displayToGuid(data.opencode.extraction_model),
738
- };
739
- }
740
-
741
- let claudeCode: ClaudeCodeSettings | undefined;
742
- if (data.claudeCode) {
743
- claudeCode = {
744
- integration: nullToUndefined(data.claudeCode.integration),
745
- polling_interval_ms: nullToUndefined(data.claudeCode.polling_interval_ms),
746
- last_sync: original?.claudeCode?.last_sync,
747
- extraction_point: original?.claudeCode?.extraction_point,
748
- processed_sessions: original?.claudeCode?.processed_sessions,
749
- extraction_model: displayToGuid(data.claudeCode.extraction_model),
750
- };
751
- }
752
-
753
- let cursor: CursorSettings | undefined;
754
- if (data.cursor) {
755
- cursor = {
756
- integration: nullToUndefined(data.cursor.integration),
757
- polling_interval_ms: nullToUndefined(data.cursor.polling_interval_ms),
758
- last_sync: original?.cursor?.last_sync,
759
- extraction_point: original?.cursor?.extraction_point,
760
- processed_sessions: original?.cursor?.processed_sessions,
761
- };
762
- }
763
-
764
- let backup: BackupConfig | undefined;
765
- if (data.backup) {
766
- backup = {
767
- enabled: nullToUndefined(data.backup.enabled),
768
- max_backups: nullToUndefined(data.backup.max_backups),
769
- interval_ms: nullToUndefined(data.backup.interval_ms),
770
- last_backup: original?.backup?.last_backup,
771
- };
772
- }
773
-
774
- return {
775
- ...original,
776
- default_model: displayToGuid(data.default_model),
777
- oneshot_model: displayToGuid(data.oneshot_model),
778
- rewrite_model: displayToGuid(data.rewrite_model),
779
- time_mode: nullToUndefined(data.time_mode),
780
- name_display: nullToUndefined(data.name_display),
781
- default_heartbeat_ms: nullToUndefined(data.default_heartbeat_ms),
782
- default_context_window_hours: nullToUndefined(data.default_context_window_hours),
783
- message_min_count: nullToUndefined(data.message_min_count),
784
- message_max_age_days: nullToUndefined(data.message_max_age_days),
785
- ceremony,
786
- opencode,
787
- claudeCode,
788
- cursor,
789
- backup,
790
- };
791
- }
792
-
793
-
794
- /**
795
- * Validate that a model spec (e.g. "Anthropic:sonnet") references a real provider.
796
- * Case-insensitive match — auto-corrects casing to the actual provider name.
797
- * Throws if no matching provider found (caller's catch triggers re-edit).
798
- */
799
- export function validateModelProvider(
800
- modelSpec: string | undefined,
801
- accounts: ProviderAccount[]
802
- ): string | undefined {
803
- if (!modelSpec) return undefined;
804
-
805
- const colonIdx = modelSpec.indexOf(":");
806
- const providerPart = colonIdx >= 0 ? modelSpec.substring(0, colonIdx) : modelSpec;
807
- const modelPart = colonIdx >= 0 ? modelSpec.substring(colonIdx + 1) : undefined;
808
-
809
- const match = accounts.find(a => a.name.toLowerCase() === providerPart.toLowerCase());
810
-
811
- if (!match) {
812
- const available = accounts.map(a => a.name).join(", ");
813
- throw new Error(
814
- available
815
- ? `No provider named "${providerPart}". Available: ${available}`
816
- : `No provider named "${providerPart}". Create one with /provider new`
817
- );
818
- }
819
-
820
- return modelPart ? `${match.name}:${modelPart}` : match.name;
821
- }
822
-
823
- // =============================================================================
824
- // QUOTE SERIALIZATION
825
- // =============================================================================
826
-
827
- interface EditableQuote extends Quote {
828
- _delete?: boolean;
829
- }
830
-
831
- interface EditableQuoteData {
832
- quotes: EditableQuote[];
833
- }
834
-
835
- export function quotesToYAML(quotes: Quote[]): string {
836
- const data: EditableQuoteData = {
837
- quotes: quotes.map(q => ({
838
- ...q,
839
- _delete: false,
840
- })),
841
- };
842
-
843
- return YAML.stringify(data, {
844
- lineWidth: 0,
845
- });
846
- }
847
-
848
- export interface QuotesYAMLResult {
849
- quotes: Quote[];
850
- deletedQuoteIds: string[];
851
- }
852
-
853
- export function quotesFromYAML(yamlContent: string): QuotesYAMLResult {
854
- const data = YAML.parse(yamlContent) as EditableQuoteData;
855
-
856
- const deletedQuoteIds: string[] = [];
857
- const quotes: Quote[] = [];
858
-
859
- for (const q of data.quotes ?? []) {
860
- if (q._delete) {
861
- deletedQuoteIds.push(q.id);
862
- } else {
863
- const { _delete, ...quote } = q;
864
- quotes.push(quote);
865
- }
866
- }
867
-
868
- return {
869
- quotes,
870
- deletedQuoteIds,
871
- };
872
- }
873
-
874
-
875
- // =============================================================================
876
- // PROVIDER ACCOUNT SERIALIZATION
877
- // =============================================================================
878
-
879
- interface EditableModelData {
880
- name: string;
881
- token_limit?: number;
882
- max_output_tokens?: number;
883
- _delete?: boolean;
884
- }
885
-
886
- interface EditableProviderData {
887
- name: string;
888
- type: "llm" | "storage";
889
- url: string;
890
- api_key?: string;
891
- default_model?: string;
892
- token_limit?: number | null;
893
- extra_headers?: Record<string, string>;
894
- enabled?: boolean;
895
- models?: EditableModelData[];
896
- _delete?: boolean;
897
- }
898
-
899
- export interface ProviderYAMLResult {
900
- account: ProviderAccount;
901
- _delete: boolean;
902
- }
903
-
904
- function resolveEnvVar(value: string | undefined): string | undefined {
905
- if (!value || !value.startsWith("$")) return value;
906
- const varName = value.slice(1);
907
- return process.env[varName] || value;
908
- }
909
-
910
- const PLACEHOLDER_PROVIDER_NAME = "My Provider";
911
- const PLACEHOLDER_PROVIDER_URL = "https://api.example.com/v1";
912
- const PLACEHOLDER_PROVIDER_API_KEY = "your-api-key-or-$ENVAR";
913
- const PLACEHOLDER_PROVIDER_DEFAULT_MODEL = "model-name";
914
-
915
- /**
916
- * Generate YAML template for a NEW provider account
917
- */
918
- export function newProviderToYAML(name?: string): string {
919
- const placeholderData = {
920
- name: name ?? PLACEHOLDER_PROVIDER_NAME,
921
- type: "llm",
922
- url: PLACEHOLDER_PROVIDER_URL,
923
- api_key: PLACEHOLDER_PROVIDER_API_KEY,
924
- default_model: PLACEHOLDER_PROVIDER_DEFAULT_MODEL,
925
- token_limit: null,
926
- extra_headers: {},
927
- enabled: true,
928
- };
929
-
930
- const modelsYAML = [
931
- "models:",
932
- " - name: (default)",
933
- " # _delete: true",
934
- "# _delete: true # Delete this entire provider",
935
- ].join("\n");
936
-
937
- return YAML.stringify(placeholderData, { lineWidth: 0 }).trimEnd() + "\n" + modelsYAML + "\n";
938
- }
939
-
940
- /**
941
- * Parse YAML for a NEW provider account
942
- */
943
- export function newProviderFromYAML(yamlContent: string): ProviderAccount {
944
- const cleaned = yamlContent
945
- .split('\n')
946
- .filter(line => !/^\s*#/.test(line))
947
- .join('\n');
948
- const data = YAML.parse(cleaned) as EditableProviderData;
949
-
950
- if (!data.name || data.name === PLACEHOLDER_PROVIDER_NAME) {
951
- throw new Error("Provider name is required");
952
- }
953
- if (!data.url || data.url === PLACEHOLDER_PROVIDER_URL) {
954
- throw new Error("Provider URL is required");
955
- }
956
- if (data.api_key === PLACEHOLDER_PROVIDER_API_KEY) {
957
- data.api_key = undefined;
958
- }
959
- if (data.default_model === PLACEHOLDER_PROVIDER_DEFAULT_MODEL) {
960
- data.default_model = undefined;
961
- }
962
-
963
- if (data.token_limit !== undefined && data.token_limit !== null && (typeof data.token_limit !== "number" || isNaN(data.token_limit))) {
964
- throw new Error(`token_limit must be a number (got: ${JSON.stringify(data.token_limit)}). Note: underscore separators (100_000) are not valid in YAML.`);
965
- }
966
-
967
- // Parse models: filter out _delete:true items
968
- const models = parseModels(data.models ?? []);
969
-
970
- return {
971
- id: crypto.randomUUID(),
972
- name: data.name,
973
- type: (data.type === "storage" ? "storage" : "llm") as ProviderType,
974
- url: data.url,
975
- api_key: resolveEnvVar(data.api_key),
976
- default_model: data.default_model,
977
- token_limit: data.token_limit ?? undefined,
978
- extra_headers: data.extra_headers && Object.keys(data.extra_headers).length > 0 ? data.extra_headers : undefined,
979
- enabled: data.enabled ?? true,
980
- models: models.length > 0 ? models : undefined,
981
- created_at: new Date().toISOString(),
982
- };
983
- }
984
-
985
- /**
986
- * Parse EditableModelData[] into ModelConfig[], generating new GUIDs for models without one.
987
- * Filters out models with _delete: true.
988
- */
989
- function parseModels(editableModels: EditableModelData[]): import('../../../src/core/types.js').ModelConfig[] {
990
- const result: import('../../../src/core/types.js').ModelConfig[] = [];
991
- for (const m of editableModels) {
992
- if (m._delete) continue;
993
- result.push({
994
- id: crypto.randomUUID(),
995
- name: m.name,
996
- token_limit: m.token_limit,
997
- max_output_tokens: m.max_output_tokens,
998
- });
999
- }
1000
- return result;
1001
- }
1002
- /**
1003
- * Serialize existing provider account to YAML for editing.
1004
- * Shows models[] as a nested section with _delete comments.
1005
- * Hides internal fields: id, total_calls, total_tokens_in, total_tokens_out, last_used.
1006
- */
1007
- export function providerToYAML(account: ProviderAccount): string {
1008
- const defaultModelDisplay = account.default_model
1009
- ? modelGuidToDisplay(account.default_model, [account])
1010
- : undefined;
1011
-
1012
- const topData = {
1013
- name: account.name,
1014
- type: account.type as "llm" | "storage",
1015
- url: account.url,
1016
- api_key: account.api_key,
1017
- default_model: defaultModelDisplay,
1018
- token_limit: account.token_limit ?? null,
1019
- extra_headers: account.extra_headers,
1020
- enabled: account.enabled ?? true,
1021
- };
1022
-
1023
- const topYAML = YAML.stringify(topData, { lineWidth: 0 }).trimEnd();
1024
-
1025
- const modelLines: string[] = ["models:"];
1026
- const modelList = account.models ?? [];
1027
- if (modelList.length > 0) {
1028
- for (const m of modelList) {
1029
- modelLines.push(` - name: ${m.name}`);
1030
- if (m.token_limit !== undefined) {
1031
- modelLines.push(` token_limit: ${m.token_limit}`);
1032
- }
1033
- if (m.max_output_tokens !== undefined) {
1034
- modelLines.push(` max_output_tokens: ${m.max_output_tokens}`);
1035
- }
1036
- modelLines.push(` _delete: false`);
1037
- }
1038
- } else {
1039
- modelLines.push(" - name: (default)");
1040
- modelLines.push(" _delete: false");
1041
- }
1042
- modelLines.push("_delete: false # Set to true to delete this entire provider");
1043
-
1044
- return topYAML + "\n" + modelLines.join("\n") + "\n";
1045
- }
1046
-
1047
- /**
1048
- * Parse YAML for an existing provider account (preserves id and created_at).
1049
- * Returns { account, _delete } where _delete signals the entire provider should be removed.
1050
- * Model _delete flags cause individual models to be removed.
1051
- * Preserves model GUIDs on round-trip; generates new GUIDs for new models.
1052
- */
1053
- export function providerFromYAML(yamlContent: string, original: ProviderAccount): ProviderYAMLResult {
1054
- // Strip comment lines before parsing (they encode _delete hints)
1055
- const cleaned = yamlContent
1056
- .split('\n')
1057
- .filter(line => !/^\s*#/.test(line))
1058
- .join('\n');
1059
- const data = YAML.parse(cleaned) as EditableProviderData;
1060
-
1061
- if (!data.name) {
1062
- throw new Error("Provider name is required");
1063
- }
1064
- if (!data.url) {
1065
- throw new Error("Provider URL is required");
1066
- }
1067
-
1068
- if (data.token_limit !== undefined && data.token_limit !== null && (typeof data.token_limit !== "number" || isNaN(data.token_limit))) {
1069
- throw new Error(`token_limit must be a number (got: ${JSON.stringify(data.token_limit)}). Note: underscore separators (100_000) are not valid in YAML.`);
1070
- }
1071
-
1072
- // Root-level _delete → signal deletion of entire provider
1073
- if (data._delete) {
1074
- return {
1075
- account: original,
1076
- _delete: true,
1077
- };
1078
- }
1079
-
1080
- // Parse models: preserve existing GUIDs by name match, generate new GUIDs for new models
1081
- const existingModels = original.models ?? [];
1082
- const parsedModels: import('../../../src/core/types.js').ModelConfig[] = [];
1083
- for (const m of data.models ?? []) {
1084
- if (m._delete) continue;
1085
- const existing = existingModels.find(em => em.name === m.name);
1086
- parsedModels.push({
1087
- id: existing?.id ?? crypto.randomUUID(),
1088
- name: m.name,
1089
- token_limit: m.token_limit,
1090
- max_output_tokens: m.max_output_tokens,
1091
- // Preserve usage counters from original if model matched
1092
- total_calls: existing?.total_calls,
1093
- total_tokens_in: existing?.total_tokens_in,
1094
- total_tokens_out: existing?.total_tokens_out,
1095
- last_used: existing?.last_used,
1096
- });
1097
- }
1098
-
1099
- const account: ProviderAccount = {
1100
- id: original.id,
1101
- name: data.name,
1102
- type: (data.type === "storage" ? "storage" : "llm") as ProviderType,
1103
- url: data.url,
1104
- api_key: resolveEnvVar(data.api_key),
1105
- default_model: data.default_model,
1106
- token_limit: data.token_limit ?? undefined,
1107
- extra_headers: data.extra_headers && Object.keys(data.extra_headers).length > 0 ? data.extra_headers : undefined,
1108
- enabled: data.enabled ?? true,
1109
- models: parsedModels.length > 0 ? parsedModels : undefined,
1110
- created_at: original.created_at,
1111
- };
1112
-
1113
- return { account, _delete: false };
1114
- }
1115
-
1116
- // =============================================================================
1117
- // GUID <-> DISPLAY NAME HELPERS
1118
- // =============================================================================
1119
-
1120
- /**
1121
- * Convert a model GUID to "ProviderName:modelName" display string.
1122
- * Falls back to the raw GUID if the model is not found.
1123
- */
1124
- export function modelGuidToDisplay(guid: string, accounts: ProviderAccount[]): string {
1125
- for (const account of accounts) {
1126
- const model = (account.models ?? []).find(m => m.id === guid);
1127
- if (model) return `${account.name}:${model.name}`;
1128
- }
1129
- return guid; // fallback: return raw GUID if not found
1130
- }
1131
-
1132
- /**
1133
- * Resolve "ProviderName:modelName" display string back to a model GUID.
1134
- * Returns undefined if no matching provider+model is found.
1135
- * Handles colons in model names by treating everything after the first colon as the model name.
1136
- */
1137
- export function displayToModelGuid(display: string, accounts: ProviderAccount[]): string | undefined {
1138
- const colonIdx = display.indexOf(':');
1139
- if (colonIdx < 0) return undefined;
1140
- const providerName = display.substring(0, colonIdx);
1141
- const modelName = display.substring(colonIdx + 1);
1142
- const account = accounts.find(a => a.name === providerName);
1143
- const model = (account?.models ?? []).find(m => m.name === modelName);
1144
- return model?.id;
1145
- }
1146
-
1147
- // =============================================================================
1148
- // CONTEXT / MESSAGE SERIALIZATION
1149
- // =============================================================================
1150
-
1151
- function getContent(m: { content?: string; verbal_response?: string; action_response?: string }): string {
1152
- if (m.content) return m.content;
1153
- const parts: string[] = [];
1154
- if (m.action_response) parts.push(`_${m.action_response}_`);
1155
- if (m.verbal_response) parts.push(m.verbal_response);
1156
- return parts.join('\n\n');
1157
- }
1158
-
1159
- interface EditableMessage {
1160
- id: string;
1161
- role: "human" | "system";
1162
- timestamp: string;
1163
- context_status: ContextStatus;
1164
- _delete?: boolean;
1165
- // content | silence_reason
1166
- content?: string;
1167
- silence_reason?: string;
1168
- }
1169
-
1170
- export function contextToYAML(messages: Message[]): string {
1171
- const header = [
1172
- "# context_status: default | always | never",
1173
- "# _delete: true — permanently removes the message",
1174
- "# content | silence_reason",
1175
- ].join("\n");
1176
-
1177
- const data: EditableMessage[] = messages.map((m) => ({
1178
- id: m.id,
1179
- role: m.role,
1180
- timestamp: m.timestamp,
1181
- context_status: m.context_status,
1182
- _delete: false,
1183
- content: getContent(m) || undefined,
1184
- silence_reason: m.silence_reason,
1185
- }));
1186
-
1187
- return header + "\n" + YAML.stringify(data, { lineWidth: 0 });
1188
- }
1189
-
1190
- export interface ContextYAMLResult {
1191
- messages: Array<{ id: string; context_status: ContextStatus }>;
1192
- deletedMessageIds: string[];
1193
- }
1194
-
1195
- export function contextFromYAML(yamlContent: string): ContextYAMLResult {
1196
- const data = YAML.parse(yamlContent) as EditableMessage[];
1197
-
1198
- const deletedMessageIds: string[] = [];
1199
- const messages: Array<{ id: string; context_status: ContextStatus }> = [];
1200
-
1201
- for (const m of data ?? []) {
1202
- if (m._delete) {
1203
- deletedMessageIds.push(m.id);
1204
- } else {
1205
- const normalized = (m.context_status ?? 'default').toString().toLowerCase() as ContextStatus;
1206
- messages.push({ id: m.id, context_status: normalized });
1207
- }
1208
- }
1209
-
1210
- return { messages, deletedMessageIds };
1211
- }
1212
-
1213
-
1214
- // =============================================================================
1215
- // QUEUE ITEM YAML
1216
- // =============================================================================
1217
-
1218
- interface EditableQueueItem {
1219
- id: string;
1220
- state: LLMRequestState;
1221
- created_at: string;
1222
- attempts: number;
1223
- last_attempt?: string;
1224
- retry_after?: string;
1225
- type?: string;
1226
- priority?: LLMPriority;
1227
- next_step?: string;
1228
- model?: string;
1229
- data?: Record<string, unknown>;
1230
- _delete?: boolean;
1231
- }
1232
-
1233
- export function queueItemsToYAML(items: LLMRequest[], accounts: ProviderAccount[]): string {
1234
- const data: EditableQueueItem[] = items.map(item => ({
1235
- id: item.id,
1236
- _delete: false,
1237
- state: item.state,
1238
- created_at: item.created_at,
1239
- attempts: item.attempts,
1240
- last_attempt: item.last_attempt,
1241
- retry_after: item.retry_after,
1242
- type: item.type,
1243
- priority: item.priority,
1244
- next_step: item.next_step,
1245
- model: item.model ? modelGuidToDisplay(item.model, accounts) : undefined,
1246
- data: item.data,
1247
- // NOTE: system/user prompts omitted (large); to requeue: set state='pending', attempts=0
1248
- }));
1249
- return YAML.stringify(data, { lineWidth: 0 });
1250
- }
1251
-
1252
- export interface QueueItemUpdate {
1253
- id: string;
1254
- state: LLMRequestState;
1255
- attempts: number;
1256
- model?: string;
1257
- priority?: LLMPriority;
1258
- data?: Record<string, unknown>;
1259
- }
1260
-
1261
- export interface QueueItemsYAMLResult {
1262
- updates: QueueItemUpdate[];
1263
- deletedIds: string[];
1264
- }
1265
-
1266
- export function queueItemsFromYAML(yamlContent: string, accounts: ProviderAccount[]): QueueItemsYAMLResult {
1267
- const data = YAML.parse(yamlContent) as EditableQueueItem[];
1268
- if (!Array.isArray(data)) throw new Error("Expected a YAML array of queue items");
1269
-
1270
- const deletedIds: string[] = [];
1271
- const updates: QueueItemUpdate[] = [];
1272
-
1273
- for (const item of data) {
1274
- if (!item.id) throw new Error(`Queue item missing 'id' field`);
1275
- if (item._delete) {
1276
- deletedIds.push(item.id);
1277
- continue;
1278
- }
1279
- if (!item.state) throw new Error(`Queue item ${item.id} missing 'state' field`);
1280
- const validStates: LLMRequestState[] = ["pending", "processing", "dlq"];
1281
- if (!validStates.includes(item.state)) {
1282
- throw new Error(`Queue item ${item.id} has invalid state '${item.state}'. Valid: ${validStates.join(", ")}`);
1283
- }
1284
- updates.push({
1285
- id: item.id,
1286
- state: item.state,
1287
- attempts: typeof item.attempts === "number" ? item.attempts : 0,
1288
- model: item.model ? displayToModelGuid(item.model, accounts) ?? item.model : undefined,
1289
- priority: item.priority,
1290
- data: item.data,
1291
- });
1292
- }
1293
-
1294
- return { updates, deletedIds };
1295
- }
1296
-
1297
- // =============================================================================
1298
- // TOOLKIT (ToolProvider) SERIALIZATION
1299
- // =============================================================================
1300
-
1301
- interface EditableToolkitData {
1302
- display_name: string;
1303
- enabled: boolean;
1304
- config: Record<string, string>;
1305
- tools?: Record<string, boolean>;
1306
- }
1307
-
1308
- /**
1309
- * Serialize a ToolProvider + its tools to YAML for editing.
1310
- * Shows config keys and per-tool enable toggles.
1311
- */
1312
- export function toolkitToYAML(provider: ToolProvider, tools: ToolDefinition[]): string {
1313
- const toolsMap = tools.length > 0
1314
- ? Object.fromEntries(tools.map(t => [t.display_name, t.enabled]))
1315
- : undefined;
1316
- if (provider.builtin) {
1317
- return YAML.stringify({ enabled: provider.enabled, tools: toolsMap }, { lineWidth: 0 });
1318
- }
1319
- const data: EditableToolkitData = {
1320
- display_name: provider.display_name,
1321
- enabled: provider.enabled,
1322
- config: { ...provider.config },
1323
- tools: toolsMap,
1324
- };
1325
- return YAML.stringify(data, { lineWidth: 0 });
1326
- }
1327
-
1328
- export interface ToolkitYAMLResult {
1329
- updates: Partial<Omit<ToolProvider, 'id' | 'created_at'>>;
1330
- toolUpdates: Array<{ id: string; enabled: boolean }>;
1331
- }
1332
-
1333
- /**
1334
- * Parse edited YAML back into ToolProvider updates + per-tool enable changes.
1335
- */
1336
- export function toolkitFromYAML(yamlContent: string, original: ToolProvider, tools: ToolDefinition[]): ToolkitYAMLResult {
1337
- const data = YAML.parse(yamlContent) as EditableToolkitData;
1338
-
1339
- if (!data.display_name) {
1340
- if (!original.display_name) throw new Error("display_name is required");
1341
- data.display_name = original.display_name;
1342
- }
1343
-
1344
- const updates: Partial<Omit<ToolProvider, 'id' | 'created_at'>> = {
1345
- display_name: data.display_name,
1346
- enabled: data.enabled ?? original.enabled,
1347
- config: data.config ?? {},
1348
- };
1349
-
1350
- const toolUpdates: Array<{ id: string; enabled: boolean }> = [];
1351
- if (data.tools) {
1352
- for (const [displayName, enabled] of Object.entries(data.tools)) {
1353
- const tool = tools.find(t => t.display_name === displayName);
1354
- if (tool) toolUpdates.push({ id: tool.id, enabled: Boolean(enabled) });
1355
- }
1356
- }
1357
-
1358
- return { updates, toolUpdates };
1359
- }
1360
-
1361
- // =============================================================================
1362
- // PERSONA PREVIEW SERIALIZATION (from-person generation flow)
1363
- // =============================================================================
1364
-
1365
- /**
1366
- * YAML template for the first editor step in /persona new — description input.
1367
- */
1368
- export function descriptionEntryToYAML(personaName: string): string {
1369
- return `# New Persona: ${personaName}
1370
- # Describe who this persona is. This will be used to generate traits and topics.
1371
- # Save to generate • :q to cancel.
1372
-
1373
- description: |
1374
-
1375
- `;
1376
- }
1377
-
1378
- /**
1379
- * Parse the description entry YAML.
1380
- */
1381
- export function descriptionFromYAML(content: string): { description: string; relationship?: string } {
1382
- const data = YAML.parse(content) as { description?: string; relationship?: string };
1383
- if (!data) throw new Error("Failed to parse YAML");
1384
-
1385
- const description = (data.description ?? "").replace(/\n/g, " ").trim();
1386
- const relationship = data.relationship?.trim() || undefined;
1387
-
1388
- return { description, relationship };
1389
- }
1390
-
1391
- /**
1392
- * YAML template for the traits/topics review editor step.
1393
- */
1394
- export function personaPreviewToYAML(
1395
- preview: import('../../../src/prompts/generation/types.js').PersonaGenerationResult,
1396
- personaName: string,
1397
- personName?: string,
1398
- previousLongDescription?: string
1399
- ): string {
1400
- const normalizeLine = (s: string) => s.replace(/\n/g, ' ').trim();
1401
-
1402
- const headerLines: string[] = [
1403
- `# Persona Preview: ${personaName}`,
1404
- ];
1405
- if (personName) {
1406
- headerLines.push(`# Source: ${personName}`);
1407
- }
1408
- headerLines.push(`# Edit or delete entries. Save or quit to apply • :cq to cancel.`);
1409
- headerLines.push(``);
1410
- if (previousLongDescription) {
1411
- headerLines.push(`# Previously: ${normalizeLine(previousLongDescription)}`);
1412
- }
1413
-
1414
- const longDescYAML = `long_description: ${JSON.stringify(normalizeLine(preview.long_description))}`;
1415
- const shortDescYAML = `short_description: ${JSON.stringify(normalizeLine(preview.short_description ?? ''))}`;
1416
- const aliasesLine = (preview.aliases && preview.aliases.length > 0)
1417
- ? `aliases: ${preview.aliases.join(', ')}`
1418
- : null;
1419
-
1420
- const traitsYAML = YAML.stringify(
1421
- { traits: preview.traits.map(t => ({
1422
- name: t.name,
1423
- description: t.description,
1424
- sentiment: Math.round(t.sentiment * 100) / 100,
1425
- strength: Math.round(t.strength * 100) / 100,
1426
- }))},
1427
- { lineWidth: 0 }
1428
- );
1429
-
1430
- const topicsYAML = YAML.stringify(
1431
- { topics: preview.topics.map(t => ({
1432
- name: t.name,
1433
- perspective: t.perspective,
1434
- approach: t.approach,
1435
- personal_stake: t.personal_stake,
1436
- sentiment: Math.round(t.sentiment * 100) / 100,
1437
- exposure_current: Math.round(t.exposure_current * 100) / 100,
1438
- exposure_desired: Math.round(t.exposure_desired * 100) / 100,
1439
- }))},
1440
- { lineWidth: 0 }
1441
- );
1442
-
1443
- return [
1444
- headerLines.join('\n'),
1445
- longDescYAML,
1446
- shortDescYAML,
1447
- ...(aliasesLine ? [aliasesLine] : []),
1448
- traitsYAML.trimEnd(),
1449
- topicsYAML.trimEnd(),
1450
- '',
1451
- ].join('\n');
1452
- }
1453
-
1454
- interface PreviewYAMLData {
1455
- long_description?: string;
1456
- short_description?: string;
1457
- traits?: Array<{
1458
- name: string;
1459
- description: string;
1460
- sentiment?: number;
1461
- strength?: number;
1462
- }>;
1463
- topics?: Array<{
1464
- name: string;
1465
- perspective: string;
1466
- approach: string;
1467
- personal_stake: string;
1468
- sentiment?: number;
1469
- exposure_current?: number;
1470
- exposure_desired?: number;
1471
- }>;
1472
- }
1473
-
1474
- /**
1475
- * Parse the preview YAML back into traits/topics with generated IDs.
1476
- */
1477
- export function personaPreviewFromYAML(content: string): { long_description: string; short_description?: string; aliases?: string[]; traits: PersonaTrait[]; topics: PersonaTopic[] } {
1478
- const data = YAML.parse(content) as PreviewYAMLData & { aliases?: string };
1479
- if (!data) throw new Error("Failed to parse YAML");
1480
-
1481
- const long_description = (data.long_description ?? "").replace(/\n/g, ' ').trim();
1482
- const short_description = data.short_description?.replace(/\n/g, ' ').trim() || undefined;
1483
-
1484
- const traits: PersonaTrait[] = (data.traits ?? []).map(t => ({
1485
- id: crypto.randomUUID(),
1486
- name: t.name,
1487
- description: t.description,
1488
- sentiment: t.sentiment ?? 0,
1489
- strength: t.strength ?? 0.5,
1490
- last_updated: new Date().toISOString(),
1491
- }));
1492
-
1493
- const topics: PersonaTopic[] = (data.topics ?? []).map(t => ({
1494
- id: crypto.randomUUID(),
1495
- name: t.name,
1496
- perspective: t.perspective,
1497
- approach: t.approach,
1498
- personal_stake: t.personal_stake,
1499
- sentiment: t.sentiment ?? 0,
1500
- exposure_current: t.exposure_current ?? 0.5,
1501
- exposure_desired: t.exposure_desired ?? 0.5,
1502
- last_updated: new Date().toISOString(),
1503
- }));
1504
-
1505
- const aliases = data.aliases
1506
- ? data.aliases.split(',').map((s: string) => s.trim()).filter(Boolean)
1507
- : undefined;
1508
-
1509
- return { long_description, short_description, aliases, traits, topics };
1510
- }
1
+ export * from "./yaml-shared.js";
2
+ export * from "./yaml-human.js";
3
+ export * from "./yaml-persona.js";
4
+ export * from "./yaml-settings.js";
5
+ export * from "./yaml-provider.js";
6
+ export * from "./yaml-quotes.js";
7
+ export * from "./yaml-context.js";
8
+ export * from "./yaml-queue.js";
9
+ export * from "./yaml-toolkit.js";