ei-tui 0.5.4 → 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.
Files changed (86) hide show
  1. package/package.json +1 -1
  2. package/src/core/constants/built-in-identifier-types.ts +24 -0
  3. package/src/core/embedding-service.ts +24 -1
  4. package/src/core/handlers/dedup.ts +34 -4
  5. package/src/core/handlers/heartbeat.ts +16 -0
  6. package/src/core/handlers/human-extraction.ts +201 -7
  7. package/src/core/handlers/human-matching.ts +71 -22
  8. package/src/core/handlers/index.ts +52 -14
  9. package/src/core/handlers/persona-generation.ts +2 -0
  10. package/src/core/handlers/persona-response.ts +37 -22
  11. package/src/core/handlers/persona-topics.ts +35 -271
  12. package/src/core/handlers/rewrite.ts +3 -0
  13. package/src/core/handlers/rooms.ts +41 -20
  14. package/src/core/handlers/utils.ts +10 -8
  15. package/src/core/heartbeat-manager.ts +60 -2
  16. package/src/core/llm-client.ts +1 -1
  17. package/src/core/message-manager.ts +3 -2
  18. package/src/core/orchestrators/ceremony.ts +54 -144
  19. package/src/core/orchestrators/dedup-phase.ts +0 -199
  20. package/src/core/orchestrators/extraction-chunker.ts +8 -3
  21. package/src/core/orchestrators/human-extraction.ts +37 -85
  22. package/src/core/orchestrators/index.ts +4 -8
  23. package/src/core/orchestrators/person-migration.ts +55 -0
  24. package/src/core/orchestrators/persona-topics.ts +64 -89
  25. package/src/core/orchestrators/room-extraction.ts +34 -0
  26. package/src/core/persona-manager.ts +21 -2
  27. package/src/core/personas/opencode-agent.ts +1 -0
  28. package/src/core/processor.ts +51 -14
  29. package/src/core/prompt-context-builder.ts +38 -5
  30. package/src/core/queue-processor.ts +4 -2
  31. package/src/core/room-manager.ts +6 -7
  32. package/src/core/state/human.ts +6 -0
  33. package/src/core/state/personas.ts +35 -10
  34. package/src/core/state/rooms.ts +21 -0
  35. package/src/core/state-manager.ts +61 -0
  36. package/src/core/types/data-items.ts +12 -0
  37. package/src/core/types/entities.ts +3 -0
  38. package/src/core/types/enums.ts +2 -7
  39. package/src/core/types/llm.ts +2 -0
  40. package/src/core/types/rooms.ts +2 -0
  41. package/src/core/utils/identifier-utils.ts +19 -0
  42. package/src/core/utils/index.ts +2 -1
  43. package/src/core/utils/levenshtein.ts +18 -0
  44. package/src/integrations/claude-code/importer.ts +1 -0
  45. package/src/integrations/cursor/importer.ts +1 -0
  46. package/src/prompts/ceremony/index.ts +1 -0
  47. package/src/prompts/ceremony/person-migration.ts +77 -0
  48. package/src/prompts/ceremony/rewrite.ts +1 -1
  49. package/src/prompts/ceremony/user-dedup.ts +15 -1
  50. package/src/prompts/heartbeat/check.ts +28 -12
  51. package/src/prompts/heartbeat/ei.ts +2 -0
  52. package/src/prompts/heartbeat/types.ts +12 -0
  53. package/src/prompts/human/index.ts +0 -2
  54. package/src/prompts/human/person-scan.ts +58 -14
  55. package/src/prompts/human/person-update.ts +171 -96
  56. package/src/prompts/human/topic-update.ts +1 -1
  57. package/src/prompts/human/types.ts +5 -1
  58. package/src/prompts/index.ts +3 -10
  59. package/src/prompts/message-utils.ts +9 -23
  60. package/src/prompts/persona/index.ts +3 -10
  61. package/src/prompts/persona/topics-rate.ts +95 -0
  62. package/src/prompts/persona/types.ts +8 -48
  63. package/src/prompts/response/index.ts +3 -7
  64. package/src/prompts/response/sections.ts +7 -57
  65. package/src/prompts/room/index.ts +1 -1
  66. package/src/prompts/room/sections.ts +8 -31
  67. package/tui/src/commands/me.tsx +14 -7
  68. package/tui/src/commands/persona.tsx +120 -83
  69. package/tui/src/components/MessageList.tsx +9 -4
  70. package/tui/src/components/RoomMessageList.tsx +10 -5
  71. package/tui/src/context/keyboard.tsx +2 -2
  72. package/tui/src/util/cyp-editor.tsx +13 -8
  73. package/tui/src/util/yaml-context.ts +66 -0
  74. package/tui/src/util/yaml-human.ts +274 -0
  75. package/tui/src/util/yaml-persona.ts +479 -0
  76. package/tui/src/util/yaml-provider.ts +215 -0
  77. package/tui/src/util/yaml-queue.ts +81 -0
  78. package/tui/src/util/yaml-quotes.ts +46 -0
  79. package/tui/src/util/yaml-serializers.ts +9 -1417
  80. package/tui/src/util/yaml-settings.ts +223 -0
  81. package/tui/src/util/yaml-shared.ts +32 -0
  82. package/tui/src/util/yaml-toolkit.ts +55 -0
  83. package/src/prompts/human/person-match.ts +0 -65
  84. package/src/prompts/persona/topics-match.ts +0 -70
  85. package/src/prompts/persona/topics-scan.ts +0 -98
  86. package/src/prompts/persona/topics-update.ts +0 -154
@@ -0,0 +1,66 @@
1
+ import YAML from "yaml";
2
+ import type { Message } from "../../../src/core/types.js";
3
+ import { ContextStatus } from "../../../src/core/types.js";
4
+
5
+ interface EditableMessage {
6
+ id: string;
7
+ role: "human" | "system";
8
+ timestamp: string;
9
+ context_status: ContextStatus;
10
+ _delete?: boolean;
11
+ content?: string;
12
+ silence_reason?: string;
13
+ }
14
+
15
+ function getContent(m: { content?: string; verbal_response?: string; action_response?: string }): string {
16
+ if (m.content) return m.content;
17
+ const parts: string[] = [];
18
+ if (m.action_response) parts.push(`_${m.action_response}_`);
19
+ if (m.verbal_response) parts.push(m.verbal_response);
20
+ return parts.join('\n\n');
21
+ }
22
+
23
+ export function contextToYAML(messages: Message[]): string {
24
+ const header = [
25
+ "# context_status: default | always | never",
26
+ "# _delete: true — permanently removes the message",
27
+ "# content | silence_reason",
28
+ ].join("\n");
29
+
30
+ const data: EditableMessage[] = messages.map((m) => ({
31
+ id: m.id,
32
+ role: m.role,
33
+ timestamp: m.timestamp,
34
+ context_status: m.context_status,
35
+ _delete: false,
36
+ content: getContent(m) || undefined,
37
+ silence_reason: m.silence_reason,
38
+ }));
39
+
40
+ return header + "\n" + YAML.stringify(data, { lineWidth: 0 });
41
+ }
42
+
43
+ export interface ContextYAMLResult {
44
+ messages: Array<{ id: string; context_status: ContextStatus }>;
45
+ deletedMessageIds: string[];
46
+ }
47
+
48
+ export function contextFromYAML(yamlContent: string): ContextYAMLResult {
49
+ const data = YAML.parse(yamlContent) as EditableMessage[];
50
+
51
+ const deletedMessageIds: string[] = [];
52
+ const messages: Array<{ id: string; context_status: ContextStatus }> = [];
53
+
54
+ for (const m of data ?? []) {
55
+ if (m._delete) {
56
+ deletedMessageIds.push(m.id);
57
+ } else {
58
+ const normalized = (m.context_status ?? 'default').toString().toLowerCase() as ContextStatus;
59
+ messages.push({ id: m.id, context_status: normalized });
60
+ }
61
+ }
62
+
63
+ return { messages, deletedMessageIds };
64
+ }
65
+
66
+
@@ -0,0 +1,274 @@
1
+ import YAML from "yaml";
2
+ import type {
3
+ HumanEntity,
4
+ Fact,
5
+ Topic,
6
+ Person,
7
+ PersonIdentifier,
8
+ } from "../../../src/core/types.js";
9
+ import { BUILT_IN_FACT_NAMES } from "../../../src/core/constants/built-in-facts.js";
10
+ import { BUILT_IN_IDENTIFIER_TYPES } from "../../../src/core/constants/built-in-identifier-types.js";
11
+
12
+ interface EditableTopic extends Omit<Topic, 'persona_groups'> {
13
+ _delete?: boolean;
14
+ persona_groups?: Record<string, boolean>[];
15
+ }
16
+
17
+ interface EditableFact extends Omit<Fact, 'persona_groups'> {
18
+ _delete?: boolean;
19
+ persona_groups?: Record<string, boolean>[];
20
+ }
21
+
22
+ interface YAMLPersonIdentifier {
23
+ type: string;
24
+ value: string;
25
+ primary?: true;
26
+ }
27
+
28
+ interface EditablePersonYAML extends Omit<Person, 'identifiers' | 'persona_groups'> {
29
+ identifiers: YAMLPersonIdentifier[];
30
+ _delete?: boolean;
31
+ persona_groups?: Record<string, boolean>[];
32
+ }
33
+
34
+ interface EditableHumanData {
35
+ facts: EditableFact[];
36
+ topics: EditableTopic[];
37
+ people: EditablePersonYAML[];
38
+ }
39
+
40
+ type WithReadOnlyFields = {
41
+ learned_on?: string;
42
+ learned_by?: string;
43
+ validated_date?: string;
44
+ last_mentioned?: string;
45
+ last_updated: string;
46
+ last_changed_by?: string;
47
+ };
48
+
49
+ function readOnlyToEnd<T extends WithReadOnlyFields>(item: T): T {
50
+ const { learned_on, learned_by, validated_date, last_mentioned, last_updated, last_changed_by, ...rest } = item;
51
+ return { ...rest, learned_on, learned_by, validated_date, last_mentioned, last_updated, last_changed_by } as T;
52
+ }
53
+
54
+ function buildGroupCheckboxMap(itemGroups: string[], allGroups: string[]): Record<string, boolean>[] {
55
+ const activeSet = new Set(itemGroups);
56
+ return [...new Set([...allGroups, ...itemGroups])].map(g => ({ [g]: activeSet.has(g) }));
57
+ }
58
+
59
+ function toYAMLIdentifiers(identifiers: PersonIdentifier[], personaLookup?: Map<string, string>): YAMLPersonIdentifier[] {
60
+ return identifiers.map(({ type, value, is_primary }) => {
61
+ const resolvedValue = type === 'Ei Persona' ? (personaLookup?.get(value) ?? value) : value;
62
+ const entry: YAMLPersonIdentifier = { type, value: resolvedValue };
63
+ if (is_primary) entry.primary = true;
64
+ return entry;
65
+ });
66
+ }
67
+
68
+ function knownTypesComment(personaLookup?: Map<string, string>): string {
69
+ const lines = [`# Valid types: ${BUILT_IN_IDENTIFIER_TYPES.join(', ')}`];
70
+ if (personaLookup && personaLookup.size > 0) {
71
+ lines.push(`# Personas: ${Array.from(personaLookup.values()).join(', ')}`);
72
+ }
73
+ return lines.join('\n');
74
+ }
75
+
76
+ function parseGroupCheckboxMap(groups: Record<string, boolean>[] | undefined): string[] {
77
+ if (!groups) return [];
78
+ const result: string[] = [];
79
+ for (const record of groups) {
80
+ for (const [name, active] of Object.entries(record)) {
81
+ if (active) result.push(name);
82
+ }
83
+ }
84
+ return result;
85
+ }
86
+
87
+ export function humanToYAML(human: HumanEntity, personaLookup?: Map<string, string>, allGroups: string[] = []): string {
88
+ const data: EditableHumanData = {
89
+ facts: human.facts.map(f => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(f); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
90
+ topics: human.topics.map(t => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(t); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
91
+ people: human.people.map(p => {
92
+ const { identifiers, interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(p);
93
+ return {
94
+ ...rest,
95
+ persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups),
96
+ identifiers: toYAMLIdentifiers(identifiers ?? [], personaLookup),
97
+ _delete: false as const,
98
+ };
99
+ }),
100
+ };
101
+
102
+ const personComment = knownTypesComment(personaLookup);
103
+
104
+ return YAML.stringify(data, {
105
+ lineWidth: 0,
106
+ })
107
+ .replace(/^(\s+)(learned_on: .+)$/mg, '$1# [read-only] $2')
108
+ .replace(/^(\s+)(learned_by: )(.+)$/mg, (_, indent, key, val) => {
109
+ const trimmed = val.trim();
110
+ const displayName = personaLookup?.get(trimmed) ?? trimmed;
111
+ return `${indent}# [read-only] ${key}${displayName}`;
112
+ })
113
+ .replace(/^(\s+)(validated_date: .+)$/mg, '$1# [read-only] $2')
114
+ .replace(/^(\s+)(last_mentioned: .+)$/mg, '$1# [read-only] $2')
115
+ .replace(/^(\s+)(last_updated: .+)$/mg, '$1# [read-only] $2')
116
+ .replace(/^(\s+)(last_changed_by: )(.+)$/mg, (_, indent, key, val) => {
117
+ const trimmed = val.trim();
118
+ const displayName = personaLookup?.get(trimmed) ?? trimmed;
119
+ return `${indent}# [read-only] ${key}${displayName}`;
120
+ })
121
+ .replace(/^(\s+)(identifiers:)/mg, (_, indent, _key) => {
122
+ return `${indent}${personComment}\n${indent}identifiers:`;
123
+ });
124
+ }
125
+
126
+ export interface HumanYAMLResult {
127
+ facts: Fact[];
128
+ topics: Topic[];
129
+ people: Person[];
130
+ deletedFactIds: string[];
131
+ deletedTopicIds: string[];
132
+ deletedPersonIds: string[];
133
+ changedFactIds: Set<string>;
134
+ changedTopicIds: Set<string>;
135
+ changedPersonIds: Set<string>;
136
+ }
137
+
138
+ function identifiersEqual(a: PersonIdentifier[] | undefined, b: PersonIdentifier[] | undefined): boolean {
139
+ const normalize = (ids: PersonIdentifier[] | undefined) =>
140
+ [...(ids ?? [])].sort((x, y) => `${x.type}:${x.value}`.localeCompare(`${y.type}:${y.value}`));
141
+ const na = normalize(a);
142
+ const nb = normalize(b);
143
+ if (na.length !== nb.length) return false;
144
+ return na.every((id, i) =>
145
+ id.type === nb[i].type &&
146
+ id.value === nb[i].value &&
147
+ Boolean(id.is_primary) === Boolean(nb[i].is_primary)
148
+ );
149
+ }
150
+
151
+ function groupsEqual(a: string[] | undefined, b: string[] | undefined): boolean {
152
+ const sa = [...(a ?? [])].sort();
153
+ const sb = [...(b ?? [])].sort();
154
+ if (sa.length !== sb.length) return false;
155
+ return sa.every((v, i) => v === sb[i]);
156
+ }
157
+
158
+ function factChanged(parsed: Fact, original: Fact): boolean {
159
+ const scalarFields: (keyof Fact)[] = [
160
+ 'name', 'description', 'sentiment',
161
+ ];
162
+ for (const field of scalarFields) {
163
+ if (parsed[field] !== original[field]) return true;
164
+ }
165
+ return !groupsEqual(parsed.persona_groups, original.persona_groups);
166
+ }
167
+
168
+ function topicChanged(parsed: Topic, original: Topic): boolean {
169
+ const scalarFields: (keyof Topic)[] = [
170
+ 'name', 'description', 'sentiment', 'exposure_current', 'exposure_desired', 'category',
171
+ ];
172
+ for (const field of scalarFields) {
173
+ if (parsed[field] !== original[field]) return true;
174
+ }
175
+ return !groupsEqual(parsed.persona_groups, original.persona_groups);
176
+ }
177
+
178
+ function personChanged(parsed: Person, original: Person): boolean {
179
+ const scalarFields: (keyof Person)[] = [
180
+ 'name', 'description', 'sentiment', 'relationship',
181
+ 'exposure_current', 'exposure_desired',
182
+ ];
183
+ for (const field of scalarFields) {
184
+ if (parsed[field] !== original[field]) return true;
185
+ }
186
+ if (!groupsEqual(parsed.persona_groups, original.persona_groups)) return true;
187
+ return !identifiersEqual(parsed.identifiers, original.identifiers);
188
+ }
189
+
190
+ export function humanFromYAML(yamlContent: string, original?: HumanEntity): HumanYAMLResult {
191
+ const stripped = yamlContent
192
+ .split('\n')
193
+ .filter(line => !/^\s*#\s*\[read-only\]/.test(line))
194
+ .join('\n');
195
+ const data = YAML.parse(stripped) as EditableHumanData;
196
+
197
+ const deletedFactIds: string[] = [];
198
+ const deletedTopicIds: string[] = [];
199
+ const deletedPersonIds: string[] = [];
200
+ const changedFactIds = new Set<string>();
201
+ const changedTopicIds = new Set<string>();
202
+ const changedPersonIds = new Set<string>();
203
+
204
+ const facts: Fact[] = [];
205
+ for (const f of data.facts ?? []) {
206
+ if (f._delete && !BUILT_IN_FACT_NAMES.has(f.name)) {
207
+ deletedFactIds.push(f.id);
208
+ } else {
209
+ const { _delete, persona_groups: groupMap, ...parsed } = f;
210
+ const originalFact = original?.facts.find(of => of.id === parsed.id);
211
+ const fact: Fact = originalFact
212
+ ? { ...originalFact, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
213
+ : { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
214
+ facts.push(fact);
215
+ if (!originalFact || factChanged(fact, originalFact)) {
216
+ if (fact.description && !originalFact?.validated_date) {
217
+ fact.validated_date = new Date().toISOString();
218
+ }
219
+ changedFactIds.add(fact.id);
220
+ }
221
+ }
222
+ }
223
+
224
+ const topics: Topic[] = [];
225
+ for (const t of data.topics ?? []) {
226
+ if (t._delete) {
227
+ deletedTopicIds.push(t.id);
228
+ } else {
229
+ const { _delete, persona_groups: groupMap, ...parsed } = t;
230
+ const originalTopic = original?.topics.find(ot => ot.id === parsed.id);
231
+ const topic: Topic = originalTopic
232
+ ? { ...originalTopic, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
233
+ : { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
234
+ topics.push(topic);
235
+ if (!originalTopic || topicChanged(topic, originalTopic)) {
236
+ changedTopicIds.add(topic.id);
237
+ }
238
+ }
239
+ }
240
+
241
+ const people: Person[] = [];
242
+ for (const p of data.people ?? []) {
243
+ if (p._delete) {
244
+ deletedPersonIds.push(p.id);
245
+ } else {
246
+ const { _delete, identifiers: yamlIdentifiers, persona_groups: groupMap, ...parsed } = p;
247
+ const identifiers: PersonIdentifier[] = (yamlIdentifiers ?? []).map(({ type, value, primary }) => ({
248
+ type,
249
+ value,
250
+ ...(primary ? { is_primary: true } : {}),
251
+ }));
252
+ const originalPerson = original?.people.find(op => op.id === parsed.id);
253
+ const person: Person = originalPerson
254
+ ? { ...originalPerson, ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) }
255
+ : { ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) };
256
+ people.push(person);
257
+ if (!originalPerson || personChanged(person, originalPerson)) {
258
+ changedPersonIds.add(person.id);
259
+ }
260
+ }
261
+ }
262
+
263
+ return {
264
+ facts,
265
+ topics,
266
+ people,
267
+ deletedFactIds,
268
+ deletedTopicIds,
269
+ deletedPersonIds,
270
+ changedFactIds,
271
+ changedTopicIds,
272
+ changedPersonIds,
273
+ };
274
+ }