ei-tui 0.4.3 → 0.5.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 (102) hide show
  1. package/README.md +14 -0
  2. package/package.json +1 -1
  3. package/src/cli/README.md +17 -12
  4. package/src/cli/commands/personas.ts +12 -0
  5. package/src/cli/mcp.ts +2 -2
  6. package/src/cli/retrieval.ts +86 -8
  7. package/src/cli.ts +8 -5
  8. package/src/core/constants/seed-traits.ts +29 -0
  9. package/src/core/context-utils.ts +1 -0
  10. package/src/core/handlers/human-matching.ts +86 -56
  11. package/src/core/handlers/index.ts +5 -0
  12. package/src/core/handlers/persona-preview.ts +7 -0
  13. package/src/core/handlers/persona-topics.ts +3 -2
  14. package/src/core/handlers/rooms.ts +176 -0
  15. package/src/core/handlers/utils.ts +55 -3
  16. package/src/core/heartbeat-manager.ts +3 -1
  17. package/src/core/llm-client.ts +1 -1
  18. package/src/core/message-manager.ts +10 -8
  19. package/src/core/orchestrators/human-extraction.ts +15 -2
  20. package/src/core/orchestrators/index.ts +1 -0
  21. package/src/core/orchestrators/persona-generation.ts +4 -0
  22. package/src/core/orchestrators/persona-topics.ts +2 -1
  23. package/src/core/orchestrators/room-extraction.ts +318 -0
  24. package/src/core/persona-manager.ts +16 -5
  25. package/src/core/personas/opencode-agent.ts +12 -2
  26. package/src/core/processor.ts +520 -4
  27. package/src/core/prompt-context-builder.ts +89 -5
  28. package/src/core/queue-processor.ts +68 -8
  29. package/src/core/room-manager.ts +408 -0
  30. package/src/core/state/index.ts +1 -0
  31. package/src/core/state/personas.ts +12 -2
  32. package/src/core/state/queue.ts +2 -2
  33. package/src/core/state/rooms.ts +182 -0
  34. package/src/core/state-manager.ts +124 -2
  35. package/src/core/tool-manager.ts +1 -1
  36. package/src/core/tools/index.ts +15 -0
  37. package/src/core/types/data-items.ts +3 -1
  38. package/src/core/types/enums.ts +11 -0
  39. package/src/core/types/integrations.ts +10 -2
  40. package/src/core/types/llm.ts +3 -0
  41. package/src/core/types/rooms.ts +59 -0
  42. package/src/core/types.ts +1 -0
  43. package/src/core/utils/decay.ts +14 -8
  44. package/src/core/utils/exposure.ts +14 -0
  45. package/src/integrations/claude-code/importer.ts +23 -10
  46. package/src/integrations/cursor/importer.ts +22 -10
  47. package/src/integrations/opencode/importer.ts +30 -13
  48. package/src/prompts/ceremony/dedup.ts +2 -2
  49. package/src/prompts/generation/from-person.ts +85 -0
  50. package/src/prompts/generation/index.ts +2 -0
  51. package/src/prompts/generation/persona.ts +14 -10
  52. package/src/prompts/generation/seeds.ts +4 -29
  53. package/src/prompts/generation/types.ts +13 -0
  54. package/src/prompts/heartbeat/check.ts +1 -1
  55. package/src/prompts/heartbeat/ei.ts +4 -4
  56. package/src/prompts/heartbeat/types.ts +1 -0
  57. package/src/prompts/index.ts +15 -0
  58. package/src/prompts/message-utils.ts +2 -2
  59. package/src/prompts/persona/topics-match.ts +7 -6
  60. package/src/prompts/persona/topics-update.ts +8 -11
  61. package/src/prompts/persona/types.ts +2 -1
  62. package/src/prompts/response/index.ts +1 -1
  63. package/src/prompts/response/sections.ts +20 -8
  64. package/src/prompts/response/types.ts +6 -0
  65. package/src/prompts/room/index.ts +115 -0
  66. package/src/prompts/room/sections.ts +150 -0
  67. package/src/prompts/room/types.ts +93 -0
  68. package/tui/README.md +20 -0
  69. package/tui/src/app.tsx +3 -2
  70. package/tui/src/commands/activate.tsx +98 -0
  71. package/tui/src/commands/archive.tsx +54 -25
  72. package/tui/src/commands/capture.tsx +50 -0
  73. package/tui/src/commands/dedupe.tsx +2 -7
  74. package/tui/src/commands/delete.tsx +48 -0
  75. package/tui/src/commands/details.tsx +7 -0
  76. package/tui/src/commands/persona.tsx +271 -9
  77. package/tui/src/commands/room.tsx +261 -0
  78. package/tui/src/commands/silence.tsx +29 -0
  79. package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
  80. package/tui/src/components/ConfirmOverlay.tsx +6 -0
  81. package/tui/src/components/ConflictOverlay.tsx +6 -0
  82. package/tui/src/components/HelpOverlay.tsx +6 -1
  83. package/tui/src/components/LoadingOverlay.tsx +51 -0
  84. package/tui/src/components/MessageList.tsx +1 -18
  85. package/tui/src/components/PersonPickerOverlay.tsx +121 -0
  86. package/tui/src/components/PersonaListOverlay.tsx +6 -1
  87. package/tui/src/components/PromptInput.tsx +141 -8
  88. package/tui/src/components/ProviderListOverlay.tsx +5 -1
  89. package/tui/src/components/QuotesOverlay.tsx +5 -1
  90. package/tui/src/components/RoomMessageList.tsx +179 -0
  91. package/tui/src/components/Sidebar.tsx +54 -2
  92. package/tui/src/components/StatusBar.tsx +99 -8
  93. package/tui/src/components/ToolkitListOverlay.tsx +5 -1
  94. package/tui/src/components/WelcomeOverlay.tsx +6 -0
  95. package/tui/src/context/ei.tsx +252 -1
  96. package/tui/src/context/keyboard.tsx +48 -12
  97. package/tui/src/util/cyp-editor.tsx +152 -0
  98. package/tui/src/util/quote-utils.ts +19 -0
  99. package/tui/src/util/room-editor.tsx +164 -0
  100. package/tui/src/util/room-logic.ts +8 -0
  101. package/tui/src/util/room-parser.ts +70 -0
  102. package/tui/src/util/yaml-serializers.ts +151 -0
@@ -0,0 +1,164 @@
1
+ import { spawnEditor } from "./editor.js";
2
+ import type { CommandContext } from "../commands/registry.js";
3
+ import type { RoomEntity, PersonaSummary } from "../../../src/core/types.js";
4
+ import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
5
+ import { logger } from "./logger.js";
6
+
7
+ export interface RoomEditorOptions {
8
+ roomId: string;
9
+ ctx: CommandContext;
10
+ }
11
+
12
+ function buildRoomEditYAML(room: RoomEntity, personas: PersonaSummary[]): string {
13
+ const activePersonas = personas.filter((p) => !p.is_archived);
14
+ const personaLines = activePersonas
15
+ .map((p) => ` ${p.display_name}: ${room.persona_ids.includes(p.id) ? "true" : "false"}`)
16
+ .join("\n");
17
+
18
+ const judgePersona = room.judge_persona_id
19
+ ? personas.find((p) => p.id === room.judge_persona_id)
20
+ : null;
21
+ const judgeDisplayName = judgePersona ? judgePersona.display_name : "";
22
+
23
+ return `# Room: ${room.display_name} (read-only info)
24
+ # Mode: ${room.mode} (cannot be changed)
25
+ display_name: "${room.display_name}"
26
+
27
+ # Participants (set true/false to add/remove):
28
+ persona_ids:
29
+ ${personaLines}
30
+
31
+ # judge_persona_id: required for messages_against_persona, use display_name from persona_ids
32
+ judge_persona_id: "${judgeDisplayName}"
33
+ `;
34
+ }
35
+
36
+ function parseRoomEditYAML(content: string, personas: PersonaSummary[]): Partial<RoomEntity> {
37
+ const lines = content.split("\n");
38
+ const updates: Partial<RoomEntity> = {};
39
+
40
+ let inPersonaIds = false;
41
+ const personaMapEntries: Array<{ name: string; enabled: boolean }> = [];
42
+
43
+ for (const line of lines) {
44
+ const trimmed = line.trim();
45
+ if (trimmed.startsWith("#") || trimmed === "") {
46
+ continue;
47
+ }
48
+
49
+ if (inPersonaIds && line.match(/^\s+\S/)) {
50
+ const colonIdx = trimmed.lastIndexOf(":");
51
+ if (colonIdx !== -1) {
52
+ const name = trimmed.slice(0, colonIdx).trim();
53
+ const val = trimmed.slice(colonIdx + 1).trim().toLowerCase();
54
+ personaMapEntries.push({ name, enabled: val === "true" });
55
+ }
56
+ continue;
57
+ }
58
+
59
+ if (inPersonaIds) {
60
+ inPersonaIds = false;
61
+ }
62
+
63
+ if (trimmed.startsWith("display_name:")) {
64
+ const val = trimmed.slice("display_name:".length).trim().replace(/^["']|["']$/g, "");
65
+ if (val) updates.display_name = val;
66
+ } else if (trimmed.startsWith("judge_persona_id:")) {
67
+ const val = trimmed.slice("judge_persona_id:".length).trim().replace(/^["']|["']$/g, "");
68
+ if (val) {
69
+ const found = personas.find((p) => p.display_name === val);
70
+ updates.judge_persona_id = found ? found.id : val;
71
+ } else {
72
+ updates.judge_persona_id = undefined;
73
+ }
74
+ } else if (trimmed.startsWith("persona_ids:")) {
75
+ inPersonaIds = true;
76
+ const inline = trimmed.slice("persona_ids:".length).trim();
77
+ if (inline && inline !== "[]") {
78
+ inPersonaIds = false;
79
+ }
80
+ }
81
+ }
82
+
83
+ if (personaMapEntries.length > 0) {
84
+ updates.persona_ids = personaMapEntries
85
+ .filter((e) => e.enabled)
86
+ .map((e) => {
87
+ const found = personas.find((p) => p.display_name === e.name);
88
+ return found ? found.id : e.name;
89
+ });
90
+ }
91
+
92
+ return updates;
93
+ }
94
+
95
+ export async function openRoomEditor(options: RoomEditorOptions): Promise<void> {
96
+ const { roomId, ctx } = options;
97
+ const room = ctx.ei.getRoom(roomId);
98
+ if (!room) {
99
+ ctx.showNotification("Room not found", "error");
100
+ return;
101
+ }
102
+
103
+ const personas = ctx.ei.personas();
104
+ let yamlContent = buildRoomEditYAML(room, personas);
105
+
106
+ while (true) {
107
+ const result = await spawnEditor({
108
+ initialContent: yamlContent,
109
+ filename: `${roomId}-details.yaml`,
110
+ renderer: ctx.renderer,
111
+ });
112
+
113
+ if (result.aborted) {
114
+ ctx.showNotification("Editor cancelled", "info");
115
+ return;
116
+ }
117
+
118
+ if (!result.success) {
119
+ ctx.showNotification("Editor failed to open", "error");
120
+ return;
121
+ }
122
+
123
+ if (result.content === null) {
124
+ ctx.showNotification("No changes made", "info");
125
+ return;
126
+ }
127
+
128
+ try {
129
+ const updates = parseRoomEditYAML(result.content, personas);
130
+ await ctx.ei.updateRoom(roomId, updates);
131
+ ctx.showNotification(`Updated ${room.display_name}`, "info");
132
+ return;
133
+ } catch (parseError) {
134
+ const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
135
+ logger.debug("[room-editor] YAML parse error", { error: errorMsg });
136
+
137
+ const shouldReEdit = await new Promise<boolean>((resolve) => {
138
+ ctx.showOverlay((hideOverlay, hideForEditor) => (
139
+ <ConfirmOverlay
140
+ message={`YAML parse error:\n${errorMsg}\n\nRe-edit?`}
141
+ onConfirm={() => {
142
+ logger.debug("[room-editor] user confirmed re-edit");
143
+ hideForEditor();
144
+ resolve(true);
145
+ }}
146
+ onCancel={() => {
147
+ logger.debug("[room-editor] user cancelled re-edit");
148
+ hideOverlay();
149
+ resolve(false);
150
+ }}
151
+ />
152
+ ), ctx.renderer);
153
+ });
154
+
155
+ if (shouldReEdit) {
156
+ yamlContent = result.content;
157
+ continue;
158
+ } else {
159
+ ctx.showNotification("Changes discarded", "info");
160
+ return;
161
+ }
162
+ }
163
+ }
164
+ }
@@ -0,0 +1,8 @@
1
+ export function checkAllPersonasResponded(
2
+ personaIds: string[],
3
+ judgePersonaId: string | undefined,
4
+ respondedIds: Set<string>
5
+ ): boolean {
6
+ const nonJudgeIds = personaIds.filter(id => id !== judgePersonaId);
7
+ return nonJudgeIds.length > 0 && nonJudgeIds.every(id => respondedIds.has(id));
8
+ }
@@ -0,0 +1,70 @@
1
+ import YAML from "yaml";
2
+ import { RoomMode } from "../../../src/core/types/enums.js";
3
+ import type { RoomCreationInput, PersonaSummary } from "../../../src/core/types.js";
4
+
5
+ export function buildRoomYAMLTemplate(personas: PersonaSummary[], initialName = ""): string {
6
+ const activePersonas = personas.filter((p) => !p.is_archived);
7
+ const personaLines = activePersonas.map((p) => ` ${p.display_name}: false`).join("\n");
8
+ return `# Room configuration
9
+ # mode: MAP | CYP | FFA
10
+ display_name: "${initialName}"
11
+ mode: FFA
12
+ persona_ids:
13
+ ${personaLines}
14
+ # judge_persona_id: required for messages_against_persona, use display_name from persona_ids above
15
+ judge_persona_id: ""
16
+ initial_message: ""
17
+ `;
18
+ }
19
+
20
+ export function parseRoomYAML(content: string, personas: PersonaSummary[]): RoomCreationInput {
21
+ const parsed = YAML.parse(content) as Record<string, unknown>;
22
+
23
+ const display_name = typeof parsed.display_name === "string" ? parsed.display_name.trim() : "";
24
+ if (!display_name) {
25
+ throw new Error("display_name is required");
26
+ }
27
+
28
+ const rawMode = typeof parsed.mode === "string" ? parsed.mode.trim().toUpperCase() : "";
29
+ const modeMap: Record<string, RoomMode> = {
30
+ MAP: RoomMode.MessagesAgainstPersona,
31
+ CYP: RoomMode.ChooseYourPath,
32
+ FFA: RoomMode.FreeForAll,
33
+ MESSAGES_AGAINST_PERSONA: RoomMode.MessagesAgainstPersona,
34
+ CHOOSE_YOUR_PATH: RoomMode.ChooseYourPath,
35
+ FREE_FOR_ALL: RoomMode.FreeForAll,
36
+ };
37
+ const mode = modeMap[rawMode];
38
+ if (!mode) {
39
+ throw new Error("mode must be MAP, CYP, or FFA");
40
+ }
41
+
42
+ let persona_ids: string[] = [];
43
+ if (parsed.persona_ids && typeof parsed.persona_ids === "object" && !Array.isArray(parsed.persona_ids)) {
44
+ persona_ids = Object.entries(parsed.persona_ids as Record<string, unknown>)
45
+ .filter(([, enabled]) => enabled === true)
46
+ .map(([name]) => {
47
+ const found = personas.find((p) => p.display_name === name);
48
+ return found ? found.id : name;
49
+ });
50
+ } else if (Array.isArray(parsed.persona_ids)) {
51
+ persona_ids = (parsed.persona_ids as unknown[]).map((v) => String(v).trim()).filter(Boolean);
52
+ }
53
+
54
+ let judge_persona_id: string | undefined;
55
+ const rawJudge = typeof parsed.judge_persona_id === "string" ? parsed.judge_persona_id.trim() : "";
56
+ if (rawJudge) {
57
+ const found = personas.find((p) => p.display_name === rawJudge);
58
+ judge_persona_id = found ? found.id : rawJudge;
59
+ }
60
+
61
+ const initial_message = typeof parsed.initial_message === "string" ? parsed.initial_message : "";
62
+
63
+ return {
64
+ display_name,
65
+ mode,
66
+ persona_ids,
67
+ ...(judge_persona_id ? { judge_persona_id } : {}),
68
+ initial_message,
69
+ };
70
+ }
@@ -1078,3 +1078,154 @@ export function toolkitFromYAML(yamlContent: string, original: ToolProvider, too
1078
1078
 
1079
1079
  return { updates, toolUpdates };
1080
1080
  }
1081
+
1082
+ // =============================================================================
1083
+ // PERSONA PREVIEW SERIALIZATION (from-person generation flow)
1084
+ // =============================================================================
1085
+
1086
+ /**
1087
+ * YAML template for the first editor step in /persona new — description input.
1088
+ */
1089
+ export function descriptionEntryToYAML(personaName: string): string {
1090
+ return `# New Persona: ${personaName}
1091
+ # Describe who this persona is. This will be used to generate traits and topics.
1092
+ # Save to generate • :q to cancel.
1093
+
1094
+ description: |
1095
+
1096
+ `;
1097
+ }
1098
+
1099
+ /**
1100
+ * Parse the description entry YAML.
1101
+ */
1102
+ export function descriptionFromYAML(content: string): { description: string; relationship?: string } {
1103
+ const data = YAML.parse(content) as { description?: string; relationship?: string };
1104
+ if (!data) throw new Error("Failed to parse YAML");
1105
+
1106
+ const description = (data.description ?? "").replace(/\n/g, " ").trim();
1107
+ const relationship = data.relationship?.trim() || undefined;
1108
+
1109
+ return { description, relationship };
1110
+ }
1111
+
1112
+ /**
1113
+ * YAML template for the traits/topics review editor step.
1114
+ */
1115
+ export function personaPreviewToYAML(
1116
+ preview: import('../../../src/prompts/generation/types.js').PersonaGenerationResult,
1117
+ personaName: string,
1118
+ personName?: string,
1119
+ previousLongDescription?: string
1120
+ ): string {
1121
+ const normalizeLine = (s: string) => s.replace(/\n/g, ' ').trim();
1122
+
1123
+ const headerLines: string[] = [
1124
+ `# Persona Preview: ${personaName}`,
1125
+ ];
1126
+ if (personName) {
1127
+ headerLines.push(`# Source: ${personName}`);
1128
+ }
1129
+ headerLines.push(`# Edit or delete entries. Save or quit to apply • :cq to cancel.`);
1130
+ headerLines.push(``);
1131
+ if (previousLongDescription) {
1132
+ headerLines.push(`# Previously: ${normalizeLine(previousLongDescription)}`);
1133
+ }
1134
+
1135
+ const longDescYAML = `long_description: ${JSON.stringify(normalizeLine(preview.long_description))}`;
1136
+ const shortDescYAML = `short_description: ${JSON.stringify(normalizeLine(preview.short_description ?? ''))}`;
1137
+ const aliasesLine = (preview.aliases && preview.aliases.length > 0)
1138
+ ? `aliases: ${preview.aliases.join(', ')}`
1139
+ : null;
1140
+
1141
+ const traitsYAML = YAML.stringify(
1142
+ { traits: preview.traits.map(t => ({
1143
+ name: t.name,
1144
+ description: t.description,
1145
+ sentiment: Math.round(t.sentiment * 100) / 100,
1146
+ strength: Math.round(t.strength * 100) / 100,
1147
+ }))},
1148
+ { lineWidth: 0 }
1149
+ );
1150
+
1151
+ const topicsYAML = YAML.stringify(
1152
+ { topics: preview.topics.map(t => ({
1153
+ name: t.name,
1154
+ perspective: t.perspective,
1155
+ approach: t.approach,
1156
+ personal_stake: t.personal_stake,
1157
+ sentiment: Math.round(t.sentiment * 100) / 100,
1158
+ exposure_current: Math.round(t.exposure_current * 100) / 100,
1159
+ exposure_desired: Math.round(t.exposure_desired * 100) / 100,
1160
+ }))},
1161
+ { lineWidth: 0 }
1162
+ );
1163
+
1164
+ return [
1165
+ headerLines.join('\n'),
1166
+ longDescYAML,
1167
+ shortDescYAML,
1168
+ ...(aliasesLine ? [aliasesLine] : []),
1169
+ traitsYAML.trimEnd(),
1170
+ topicsYAML.trimEnd(),
1171
+ '',
1172
+ ].join('\n');
1173
+ }
1174
+
1175
+ interface PreviewYAMLData {
1176
+ long_description?: string;
1177
+ short_description?: string;
1178
+ traits?: Array<{
1179
+ name: string;
1180
+ description: string;
1181
+ sentiment?: number;
1182
+ strength?: number;
1183
+ }>;
1184
+ topics?: Array<{
1185
+ name: string;
1186
+ perspective: string;
1187
+ approach: string;
1188
+ personal_stake: string;
1189
+ sentiment?: number;
1190
+ exposure_current?: number;
1191
+ exposure_desired?: number;
1192
+ }>;
1193
+ }
1194
+
1195
+ /**
1196
+ * Parse the preview YAML back into traits/topics with generated IDs.
1197
+ */
1198
+ export function personaPreviewFromYAML(content: string): { long_description: string; short_description?: string; aliases?: string[]; traits: PersonaTrait[]; topics: PersonaTopic[] } {
1199
+ const data = YAML.parse(content) as PreviewYAMLData & { aliases?: string };
1200
+ if (!data) throw new Error("Failed to parse YAML");
1201
+
1202
+ const long_description = (data.long_description ?? "").replace(/\n/g, ' ').trim();
1203
+ const short_description = data.short_description?.replace(/\n/g, ' ').trim() || undefined;
1204
+
1205
+ const traits: PersonaTrait[] = (data.traits ?? []).map(t => ({
1206
+ id: crypto.randomUUID(),
1207
+ name: t.name,
1208
+ description: t.description,
1209
+ sentiment: t.sentiment ?? 0,
1210
+ strength: t.strength ?? 0.5,
1211
+ last_updated: new Date().toISOString(),
1212
+ }));
1213
+
1214
+ const topics: PersonaTopic[] = (data.topics ?? []).map(t => ({
1215
+ id: crypto.randomUUID(),
1216
+ name: t.name,
1217
+ perspective: t.perspective,
1218
+ approach: t.approach,
1219
+ personal_stake: t.personal_stake,
1220
+ sentiment: t.sentiment ?? 0,
1221
+ exposure_current: t.exposure_current ?? 0.5,
1222
+ exposure_desired: t.exposure_desired ?? 0.5,
1223
+ last_updated: new Date().toISOString(),
1224
+ }));
1225
+
1226
+ const aliases = data.aliases
1227
+ ? data.aliases.split(',').map((s: string) => s.trim()).filter(Boolean)
1228
+ : undefined;
1229
+
1230
+ return { long_description, short_description, aliases, traits, topics };
1231
+ }