ei-tui 0.1.3
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.
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +63 -0
- package/src/README.md +96 -0
- package/src/cli/README.md +47 -0
- package/src/cli/commands/facts.ts +25 -0
- package/src/cli/commands/people.ts +25 -0
- package/src/cli/commands/quotes.ts +19 -0
- package/src/cli/commands/topics.ts +25 -0
- package/src/cli/commands/traits.ts +25 -0
- package/src/cli/retrieval.ts +269 -0
- package/src/cli.ts +176 -0
- package/src/core/AGENTS.md +104 -0
- package/src/core/embedding-service.ts +241 -0
- package/src/core/handlers/index.ts +1057 -0
- package/src/core/index.ts +4 -0
- package/src/core/llm-client.ts +265 -0
- package/src/core/model-context-windows.ts +49 -0
- package/src/core/orchestrators/ceremony.ts +500 -0
- package/src/core/orchestrators/extraction-chunker.ts +138 -0
- package/src/core/orchestrators/human-extraction.ts +457 -0
- package/src/core/orchestrators/index.ts +28 -0
- package/src/core/orchestrators/persona-generation.ts +76 -0
- package/src/core/orchestrators/persona-topics.ts +117 -0
- package/src/core/personas/index.ts +5 -0
- package/src/core/personas/opencode-agent.ts +81 -0
- package/src/core/processor.ts +1413 -0
- package/src/core/queue-processor.ts +197 -0
- package/src/core/state/checkpoints.ts +68 -0
- package/src/core/state/human.ts +176 -0
- package/src/core/state/index.ts +5 -0
- package/src/core/state/personas.ts +217 -0
- package/src/core/state/queue.ts +144 -0
- package/src/core/state-manager.ts +347 -0
- package/src/core/types.ts +421 -0
- package/src/core/utils/decay.ts +33 -0
- package/src/index.ts +1 -0
- package/src/integrations/opencode/importer.ts +896 -0
- package/src/integrations/opencode/index.ts +16 -0
- package/src/integrations/opencode/json-reader.ts +304 -0
- package/src/integrations/opencode/reader-factory.ts +35 -0
- package/src/integrations/opencode/sqlite-reader.ts +189 -0
- package/src/integrations/opencode/types.ts +244 -0
- package/src/prompts/AGENTS.md +62 -0
- package/src/prompts/ceremony/description-check.ts +47 -0
- package/src/prompts/ceremony/expire.ts +30 -0
- package/src/prompts/ceremony/explore.ts +60 -0
- package/src/prompts/ceremony/index.ts +11 -0
- package/src/prompts/ceremony/types.ts +42 -0
- package/src/prompts/generation/descriptions.ts +91 -0
- package/src/prompts/generation/index.ts +15 -0
- package/src/prompts/generation/persona.ts +155 -0
- package/src/prompts/generation/seeds.ts +31 -0
- package/src/prompts/generation/types.ts +47 -0
- package/src/prompts/heartbeat/check.ts +179 -0
- package/src/prompts/heartbeat/ei.ts +208 -0
- package/src/prompts/heartbeat/index.ts +15 -0
- package/src/prompts/heartbeat/types.ts +70 -0
- package/src/prompts/human/fact-scan.ts +152 -0
- package/src/prompts/human/index.ts +32 -0
- package/src/prompts/human/item-match.ts +74 -0
- package/src/prompts/human/item-update.ts +322 -0
- package/src/prompts/human/person-scan.ts +115 -0
- package/src/prompts/human/topic-scan.ts +135 -0
- package/src/prompts/human/trait-scan.ts +115 -0
- package/src/prompts/human/types.ts +127 -0
- package/src/prompts/index.ts +90 -0
- package/src/prompts/message-utils.ts +39 -0
- package/src/prompts/persona/index.ts +16 -0
- package/src/prompts/persona/topics-match.ts +69 -0
- package/src/prompts/persona/topics-scan.ts +98 -0
- package/src/prompts/persona/topics-update.ts +157 -0
- package/src/prompts/persona/traits.ts +117 -0
- package/src/prompts/persona/types.ts +74 -0
- package/src/prompts/response/index.ts +147 -0
- package/src/prompts/response/sections.ts +355 -0
- package/src/prompts/response/types.ts +38 -0
- package/src/prompts/validation/ei.ts +93 -0
- package/src/prompts/validation/index.ts +6 -0
- package/src/prompts/validation/types.ts +22 -0
- package/src/storage/crypto.ts +96 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/interface.ts +9 -0
- package/src/storage/local.ts +79 -0
- package/src/storage/merge.ts +69 -0
- package/src/storage/remote.ts +145 -0
- package/src/templates/welcome.ts +91 -0
- package/tui/README.md +62 -0
- package/tui/bunfig.toml +4 -0
- package/tui/src/app.tsx +55 -0
- package/tui/src/commands/archive.tsx +93 -0
- package/tui/src/commands/context.tsx +124 -0
- package/tui/src/commands/delete.tsx +71 -0
- package/tui/src/commands/details.tsx +41 -0
- package/tui/src/commands/editor.tsx +46 -0
- package/tui/src/commands/help.tsx +12 -0
- package/tui/src/commands/me.tsx +145 -0
- package/tui/src/commands/model.ts +47 -0
- package/tui/src/commands/new.ts +31 -0
- package/tui/src/commands/pause.ts +46 -0
- package/tui/src/commands/persona.tsx +58 -0
- package/tui/src/commands/provider.tsx +124 -0
- package/tui/src/commands/quit.ts +22 -0
- package/tui/src/commands/quotes.tsx +172 -0
- package/tui/src/commands/registry.test.ts +137 -0
- package/tui/src/commands/registry.ts +130 -0
- package/tui/src/commands/resume.ts +39 -0
- package/tui/src/commands/setsync.tsx +43 -0
- package/tui/src/commands/settings.tsx +83 -0
- package/tui/src/components/ConfirmOverlay.tsx +51 -0
- package/tui/src/components/ConflictOverlay.tsx +78 -0
- package/tui/src/components/HelpOverlay.tsx +69 -0
- package/tui/src/components/Layout.tsx +24 -0
- package/tui/src/components/MessageList.tsx +174 -0
- package/tui/src/components/PersonaListOverlay.tsx +186 -0
- package/tui/src/components/PromptInput.tsx +145 -0
- package/tui/src/components/ProviderListOverlay.tsx +208 -0
- package/tui/src/components/QuotesOverlay.tsx +157 -0
- package/tui/src/components/Sidebar.tsx +95 -0
- package/tui/src/components/StatusBar.tsx +77 -0
- package/tui/src/components/WelcomeOverlay.tsx +73 -0
- package/tui/src/context/ei.tsx +623 -0
- package/tui/src/context/keyboard.tsx +164 -0
- package/tui/src/context/overlay.tsx +53 -0
- package/tui/src/index.tsx +8 -0
- package/tui/src/storage/file.ts +185 -0
- package/tui/src/util/duration.ts +32 -0
- package/tui/src/util/editor.ts +188 -0
- package/tui/src/util/logger.ts +109 -0
- package/tui/src/util/persona-editor.tsx +181 -0
- package/tui/src/util/provider-editor.tsx +168 -0
- package/tui/src/util/syntax.ts +35 -0
- package/tui/src/util/yaml-serializers.ts +755 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Prompt Section Builders
|
|
3
|
+
* Building blocks for constructing response prompts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Trait, Quote, PersonaTopic } from "../../core/types.js";
|
|
7
|
+
import type { ResponsePromptData } from "./types.js";
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// IDENTITY SECTION
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export function buildIdentitySection(persona: ResponsePromptData["persona"]): string {
|
|
14
|
+
const aliasText = persona.aliases.length > 0
|
|
15
|
+
? ` (also known as: ${persona.aliases.join(", ")})`
|
|
16
|
+
: "";
|
|
17
|
+
|
|
18
|
+
const description = persona.long_description
|
|
19
|
+
|| persona.short_description
|
|
20
|
+
|| "a conversational companion";
|
|
21
|
+
|
|
22
|
+
return `You are ${persona.name}${aliasText}.
|
|
23
|
+
|
|
24
|
+
${description}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// GUIDELINES SECTION
|
|
29
|
+
// =============================================================================
|
|
30
|
+
|
|
31
|
+
export function buildGuidelinesSection(personaName: string): string {
|
|
32
|
+
const universal = `## Guidelines
|
|
33
|
+
- Be genuine, not sycophantic - a good friend disagrees when appropriate
|
|
34
|
+
- Match conversational energy - brief replies for brief messages
|
|
35
|
+
- Respect boundaries - it's okay to say nothing if silence is appropriate
|
|
36
|
+
- You're a friend with your own interests, not just a helper`;
|
|
37
|
+
|
|
38
|
+
if (personaName.toLowerCase() === "ei") {
|
|
39
|
+
return `${universal}
|
|
40
|
+
- Encourage human-to-human connection when appropriate
|
|
41
|
+
- Be transparent about being an AI when relevant
|
|
42
|
+
- Gently challenge self-limiting beliefs - growth over comfort`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return universal;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// TRAITS SECTION
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
export function buildTraitsSection(traits: Trait[], header: string): string {
|
|
53
|
+
if (traits.length === 0) return "";
|
|
54
|
+
|
|
55
|
+
const sorted = [...traits].sort((a, b) => (b.strength ?? 0.5) - (a.strength ?? 0.5));
|
|
56
|
+
const formatted = sorted.map(t => {
|
|
57
|
+
const strength = t.strength !== undefined ? ` (${Math.round(t.strength * 100)}%)` : "";
|
|
58
|
+
return `- **${t.name}**${strength}: ${t.description}`;
|
|
59
|
+
}).join("\n");
|
|
60
|
+
|
|
61
|
+
return `## ${header}
|
|
62
|
+
|
|
63
|
+
> NOTE: Strength of a trait should guide you to your response style, meaning a Strength of:
|
|
64
|
+
> - 0% should never be used - the user has asked you to stop
|
|
65
|
+
> - 25% should be used sparingly or subtly
|
|
66
|
+
> - 50% should be noticable in casual messages, but not dominating
|
|
67
|
+
> - 75% should be frequently used, but not in every resposne or throughout the entire conversation
|
|
68
|
+
> - 100% should be tracable throughout every response
|
|
69
|
+
|
|
70
|
+
${formatted}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// =============================================================================
|
|
74
|
+
// TOPICS SECTION
|
|
75
|
+
// =============================================================================
|
|
76
|
+
|
|
77
|
+
export function buildTopicsSection(topics: PersonaTopic[], header: string): string {
|
|
78
|
+
if (topics.length === 0) return "";
|
|
79
|
+
|
|
80
|
+
const sorted = [...topics]
|
|
81
|
+
.map(t => ({ topic: t, delta: t.exposure_desired - t.exposure_current }))
|
|
82
|
+
.sort((a, b) => b.delta - a.delta)
|
|
83
|
+
.map(x => x.topic);
|
|
84
|
+
|
|
85
|
+
const formatted = sorted.map(t => {
|
|
86
|
+
const delta = t.exposure_desired - t.exposure_current;
|
|
87
|
+
let indicator = "Neutral";
|
|
88
|
+
if (delta > 0.5) {
|
|
89
|
+
indicator = "Very Strong";
|
|
90
|
+
} else if (delta >= 0.25) {
|
|
91
|
+
indicator = "Strong";
|
|
92
|
+
} else if (delta >= 0.1) {
|
|
93
|
+
indicator = "Normal";
|
|
94
|
+
} else if (delta >= -0.1) {
|
|
95
|
+
indicator = "Low";
|
|
96
|
+
} else if (delta >= -0.25) {
|
|
97
|
+
indicator = "Avoid";
|
|
98
|
+
} else if (delta >= -0.5) {
|
|
99
|
+
indicator = "Change Subject";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const sentimentGuide = t.sentiment > 0 ? "(Liked)" : "(Disliked)";
|
|
103
|
+
const sentiment = Math.round(t.sentiment * 100)+`% ${sentimentGuide}`;
|
|
104
|
+
return `### ${t.name}
|
|
105
|
+
- Perspective: ${t.perspective}
|
|
106
|
+
- Approach: ${t.approach}
|
|
107
|
+
- Personal Stake: ${t.personal_stake}
|
|
108
|
+
- General Sentiment: ${sentiment}
|
|
109
|
+
- Desire to Discuss: ${indicator}
|
|
110
|
+
`;
|
|
111
|
+
}).join("\n");
|
|
112
|
+
|
|
113
|
+
return `## ${header}
|
|
114
|
+
${formatted}
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// =============================================================================
|
|
119
|
+
// HUMAN SECTION
|
|
120
|
+
// =============================================================================
|
|
121
|
+
|
|
122
|
+
export function buildHumanSection(human: ResponsePromptData["human"]): string {
|
|
123
|
+
const sections: string[] = [];
|
|
124
|
+
|
|
125
|
+
// Facts
|
|
126
|
+
if (human.facts.length > 0) {
|
|
127
|
+
const facts = human.facts
|
|
128
|
+
.map(f => `- ${f.name}: ${f.description}`)
|
|
129
|
+
.join("\n");
|
|
130
|
+
if (facts) sections.push(`### Key Facts\n${facts}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Traits
|
|
134
|
+
if (human.traits.length > 0) {
|
|
135
|
+
const traits = human.traits
|
|
136
|
+
.map(t => `- **${t.name}**: ${t.description}`)
|
|
137
|
+
.join("\n");
|
|
138
|
+
sections.push(`### Personality\n${traits}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Active topics (exposure_current > 0.3)
|
|
142
|
+
const activeTopics = human.topics.filter(t => t.exposure_current > 0.3);
|
|
143
|
+
if (activeTopics.length > 0) {
|
|
144
|
+
const topics = activeTopics
|
|
145
|
+
.sort((a, b) => b.exposure_current - a.exposure_current)
|
|
146
|
+
.slice(0, 10)
|
|
147
|
+
.map(t => {
|
|
148
|
+
const sentiment = t.sentiment > 0.3 ? "(enjoys)" : t.sentiment < -0.3 ? "(dislikes)" : "";
|
|
149
|
+
return `- **${t.name}** ${sentiment}: ${t.description}`;
|
|
150
|
+
})
|
|
151
|
+
.join("\n");
|
|
152
|
+
sections.push(`### Current Interests\n${topics}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// People
|
|
156
|
+
if (human.people.length > 0) {
|
|
157
|
+
const people = human.people
|
|
158
|
+
.sort((a, b) => b.exposure_current - a.exposure_current)
|
|
159
|
+
.slice(0, 10)
|
|
160
|
+
.map(p => `- **${p.name}** (${p.relationship}): ${p.description}`)
|
|
161
|
+
.join("\n");
|
|
162
|
+
sections.push(`### People in Their Life\n${people}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (sections.length === 0) {
|
|
166
|
+
return "## About the Human\n(Still getting to know them)";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return `## About the Human\n${sections.join("\n\n")}`;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// =============================================================================
|
|
173
|
+
// ASSOCIATES SECTION (visible personas)
|
|
174
|
+
// =============================================================================
|
|
175
|
+
|
|
176
|
+
export function buildAssociatesSection(visiblePersonas: ResponsePromptData["visible_personas"]): string {
|
|
177
|
+
if (visiblePersonas.length === 0) {
|
|
178
|
+
return "";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const personaLines = visiblePersonas.map(p => {
|
|
182
|
+
if (p.short_description) {
|
|
183
|
+
return `- **${p.name}**: ${p.short_description}`;
|
|
184
|
+
}
|
|
185
|
+
return `- **${p.name}**`;
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return `
|
|
189
|
+
|
|
190
|
+
## Other Personas You Know
|
|
191
|
+
${personaLines.join("\n")}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// =============================================================================
|
|
195
|
+
// PRIORITIES SECTION
|
|
196
|
+
// =============================================================================
|
|
197
|
+
|
|
198
|
+
export function buildPrioritiesSection(
|
|
199
|
+
persona: ResponsePromptData["persona"],
|
|
200
|
+
human: ResponsePromptData["human"]
|
|
201
|
+
): string {
|
|
202
|
+
const priorities: string[] = [];
|
|
203
|
+
|
|
204
|
+
const yourNeeds = persona.topics
|
|
205
|
+
.filter(t => t.exposure_desired - t.exposure_current > 0.2)
|
|
206
|
+
.slice(0, 3)
|
|
207
|
+
.map(t => `- Bring up "${t.name}" - ${t.perspective || t.name}`);
|
|
208
|
+
|
|
209
|
+
if (yourNeeds.length > 0) {
|
|
210
|
+
priorities.push(`**Topics you want to discuss:**\n${yourNeeds.join("\n")}`);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Their needs (topics they might want to discuss)
|
|
214
|
+
const theirNeeds = human.topics
|
|
215
|
+
.filter(t => t.exposure_desired - t.exposure_current > 0.2)
|
|
216
|
+
.slice(0, 3)
|
|
217
|
+
.map(t => `- They might want to talk about "${t.name}"`);
|
|
218
|
+
|
|
219
|
+
if (theirNeeds.length > 0) {
|
|
220
|
+
priorities.push(`**Topics they might enjoy:**\n${theirNeeds.join("\n")}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (priorities.length === 0) return "";
|
|
224
|
+
|
|
225
|
+
return `## Conversation Opportunities\n${priorities.join("\n\n")}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// =============================================================================
|
|
229
|
+
// CONVERSATION STATE
|
|
230
|
+
// =============================================================================
|
|
231
|
+
|
|
232
|
+
export function getConversationStateText(delayMs: number): string {
|
|
233
|
+
const delayMinutes = Math.round(delayMs / 60000);
|
|
234
|
+
const delayHours = Math.round(delayMs / 3600000);
|
|
235
|
+
|
|
236
|
+
if (delayMinutes < 5) {
|
|
237
|
+
return "You are mid-conversation with your human friend.";
|
|
238
|
+
} else if (delayMinutes < 60) {
|
|
239
|
+
return `Continuing conversation after ${delayMinutes} minutes.`;
|
|
240
|
+
} else if (delayHours < 8) {
|
|
241
|
+
return `Resuming conversation after ${delayHours} hour${delayHours > 1 ? "s" : ""}.`;
|
|
242
|
+
} else {
|
|
243
|
+
return `Reconnecting after a longer break (${delayHours} hours). A greeting may be appropriate.`;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// =============================================================================
|
|
248
|
+
// QUOTES SECTION (Memorable Moments)
|
|
249
|
+
// =============================================================================
|
|
250
|
+
|
|
251
|
+
function formatDate(isoString: string): string {
|
|
252
|
+
const date = new Date(isoString);
|
|
253
|
+
return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function buildQuotesSection(quotes: Quote[], human: ResponsePromptData["human"]): string {
|
|
257
|
+
if (quotes.length === 0) return "";
|
|
258
|
+
|
|
259
|
+
const allDataItems = [
|
|
260
|
+
...human.facts.map(f => ({ id: f.id, name: f.name })),
|
|
261
|
+
...human.traits.map(t => ({ id: t.id, name: t.name })),
|
|
262
|
+
...human.topics.map(t => ({ id: t.id, name: t.name })),
|
|
263
|
+
...human.people.map(p => ({ id: p.id, name: p.name })),
|
|
264
|
+
];
|
|
265
|
+
const idToName = new Map(allDataItems.map(item => [item.id, item.name]));
|
|
266
|
+
|
|
267
|
+
const formatted = quotes.map(q => {
|
|
268
|
+
const speaker = q.speaker === "human" ? "Human" : q.speaker;
|
|
269
|
+
const date = formatDate(q.timestamp);
|
|
270
|
+
const linkedNames = q.data_item_ids
|
|
271
|
+
.map(id => idToName.get(id))
|
|
272
|
+
.filter((name): name is string => name !== undefined);
|
|
273
|
+
|
|
274
|
+
let line = `- "${q.text}" — ${speaker} (${date})`;
|
|
275
|
+
if (linkedNames.length > 0) {
|
|
276
|
+
line += `\n Related to: ${linkedNames.join(", ")}`;
|
|
277
|
+
}
|
|
278
|
+
return line;
|
|
279
|
+
}).join("\n\n");
|
|
280
|
+
|
|
281
|
+
return `## Memorable Moments
|
|
282
|
+
|
|
283
|
+
These are quotes the human found worth preserving:
|
|
284
|
+
|
|
285
|
+
${formatted}`;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// =============================================================================
|
|
289
|
+
// SYSTEM KNOWLEDGE SECTION (Ei-only)
|
|
290
|
+
// =============================================================================
|
|
291
|
+
|
|
292
|
+
export function buildSystemKnowledgeSection(isTUI: boolean): string {
|
|
293
|
+
const interfaceIntro = isTUI ? "their command line Terminal User Interface (TUI)" : "a web browser";
|
|
294
|
+
const createPersonaAction = isTUI ? "Use the `/p[ersona] new` command" : "Click the [+] button in the left panel";
|
|
295
|
+
const editPersonaAction = isTUI ? "`/d[etails]` command" : "clicking the Edit (Pencil) icon on the left";
|
|
296
|
+
const viewQuotesAction = isTUI ? "`/quotes [number_by_scissors]` command" : "the scissors icon ✂️ ";
|
|
297
|
+
const seeHumanDataAction = isTUI ? "Using the `/me` command" : "Upper-right menu -> My Data";
|
|
298
|
+
const editorNotes = isTUI ? "Ctrl+E to open their editor" : "Ctrl+L to focus the input box";
|
|
299
|
+
const helpNotes = isTUI ? "`/h[elp]` to see all the commands" : "'Help' is in the Upper-right menu";
|
|
300
|
+
const settingsAction = isTUI ? "`/settings` command" : "Hamburger Menu, Top-Right of screen";
|
|
301
|
+
const leftPanelNotes = isTUI ? "\n- Can be hidden with Ctrl+B" : `
|
|
302
|
+
- Hover over a persona to see controls: pause, edit (Pencil), archive, delete (Trash)
|
|
303
|
+
- Click a persona to switch conversations
|
|
304
|
+
- The [+] button creates new personas`;
|
|
305
|
+
|
|
306
|
+
return `# System Knowledge
|
|
307
|
+
|
|
308
|
+
The user is messaging you from ${interfaceIntro}
|
|
309
|
+
|
|
310
|
+
You can help the human navigate this system. Here's what you know:
|
|
311
|
+
|
|
312
|
+
## The Ei Platform
|
|
313
|
+
Ei is a privacy-first AI companion system. Everything stays local on the user's device. You (Ei) are their guide and the only persona who sees everything about them.
|
|
314
|
+
|
|
315
|
+
## Personas
|
|
316
|
+
The human can create multiple AI personas, each with unique personalities and interests. Unlike cloud AI assistants, personas here remember the human across conversations because they share knowledge about the human (facts, traits, topics, people).
|
|
317
|
+
|
|
318
|
+
**To create a persona**: ${createPersonaAction}. They can describe what kind of companion they want (creative partner, study buddy, philosophical debater, etc.) and the system will help build it.
|
|
319
|
+
|
|
320
|
+
### Persona Groups
|
|
321
|
+
Personas can be assigned Groups during creation and when editing their details via the ${editPersonaAction} in the Settings area. Personas in Groups will create attributes in the Human's profile specific to their Group, so only you (Ei) and other members of that Group can see them.
|
|
322
|
+
|
|
323
|
+
Additionally, if the user wants a Persona to feel "Fresh" without prior knowledge, they can **remove** the "General" group from its visibility.
|
|
324
|
+
|
|
325
|
+
## The Left Panel
|
|
326
|
+
- Shows all personas (you're always at the top)
|
|
327
|
+
${leftPanelNotes}
|
|
328
|
+
|
|
329
|
+
## Learning About the Human
|
|
330
|
+
As the human chats, the system learns about them:
|
|
331
|
+
- **Facts**: Objective information (job, location, family members)
|
|
332
|
+
- **Traits**: Personality characteristics and tendencies
|
|
333
|
+
- **Topics**: Interests and how they feel about them
|
|
334
|
+
- **People**: Relationships in their life
|
|
335
|
+
- **Quotes**: Memorable things said in conversation (human selects these with ${viewQuotesAction})
|
|
336
|
+
|
|
337
|
+
The human can view and edit all of this by ${seeHumanDataAction}.
|
|
338
|
+
|
|
339
|
+
## Keyboard Shortcuts
|
|
340
|
+
- **Escape**: Pause/resume all AI processing
|
|
341
|
+
- **Ctrl+H**: Focus the persona panel
|
|
342
|
+
- ${editorNotes}
|
|
343
|
+
- ${helpNotes}
|
|
344
|
+
|
|
345
|
+
## Settings (${settingsAction})
|
|
346
|
+
- Set display name and preferred Time Format
|
|
347
|
+
- Configure LLM providers (local or cloud)
|
|
348
|
+
- Set up device sync (encrypted backup to restore on other devices)
|
|
349
|
+
- Adjust ceremony timing (overnight persona evolution)
|
|
350
|
+
|
|
351
|
+
### Tips You Can Share
|
|
352
|
+
- If they want to talk to a persona privately, tell them about the "Groups" functionality
|
|
353
|
+
- If they want you to remember something specific, tell them about the quote capture feature (${viewQuotesAction})
|
|
354
|
+
- Pausing the system (Escape) immediately stops AI processing but preserves messages`;
|
|
355
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Response Prompt Types
|
|
3
|
+
* Based on CONTRACTS.md ResponsePromptData specification
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Fact, Trait, Topic, Person, Quote, PersonaTopic } from "../../core/types.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Data contract for buildResponsePrompt (from CONTRACTS.md)
|
|
10
|
+
*/
|
|
11
|
+
export interface ResponsePromptData {
|
|
12
|
+
persona: {
|
|
13
|
+
name: string;
|
|
14
|
+
aliases: string[];
|
|
15
|
+
short_description?: string;
|
|
16
|
+
long_description?: string;
|
|
17
|
+
traits: Trait[];
|
|
18
|
+
topics: PersonaTopic[];
|
|
19
|
+
};
|
|
20
|
+
human: {
|
|
21
|
+
facts: Fact[];
|
|
22
|
+
traits: Trait[];
|
|
23
|
+
topics: Topic[];
|
|
24
|
+
people: Person[];
|
|
25
|
+
quotes: Quote[];
|
|
26
|
+
};
|
|
27
|
+
visible_personas: Array<{ name: string; short_description?: string }>;
|
|
28
|
+
delay_ms: number;
|
|
29
|
+
isTUI: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Prompt output structure (all prompts return this)
|
|
34
|
+
*/
|
|
35
|
+
export interface PromptOutput {
|
|
36
|
+
system: string;
|
|
37
|
+
user: string;
|
|
38
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { EiValidationPromptData, PromptOutput } from "./types.js";
|
|
2
|
+
import type { DataItemBase } from "../../core/types.js";
|
|
3
|
+
|
|
4
|
+
function formatDataItem(item: DataItemBase, label: string): string {
|
|
5
|
+
return `### ${label}
|
|
6
|
+
- **Name**: ${item.name}
|
|
7
|
+
- **Description**: ${item.description}
|
|
8
|
+
- **Sentiment**: ${item.sentiment}
|
|
9
|
+
- **Last Updated**: ${item.last_updated}
|
|
10
|
+
${item.learned_by ? `- **Learned By**: ${item.learned_by}` : ""}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildEiValidationPrompt(data: EiValidationPromptData): PromptOutput {
|
|
14
|
+
if (!data.item_name || !data.data_type) {
|
|
15
|
+
throw new Error("buildEiValidationPrompt: item_name and data_type are required");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const roleFragment = `You are Ei, the system guide and arbiter of truth for the human's data.
|
|
19
|
+
|
|
20
|
+
When other personas learn things about the human, those changes come to you for validation. Your job is to ensure data quality and consistency.`;
|
|
21
|
+
|
|
22
|
+
const contextFragment = `# Validation Request
|
|
23
|
+
|
|
24
|
+
**Type**: ${data.data_type.toUpperCase()}
|
|
25
|
+
**Item**: "${data.item_name}"
|
|
26
|
+
**Source**: ${data.source_persona}
|
|
27
|
+
**Context**: ${data.context}`;
|
|
28
|
+
|
|
29
|
+
const dataFragment = data.current_item
|
|
30
|
+
? `# Data Comparison
|
|
31
|
+
|
|
32
|
+
${formatDataItem(data.current_item, "Current (existing data)")}
|
|
33
|
+
|
|
34
|
+
${formatDataItem(data.proposed_item, "Proposed (from " + data.source_persona + ")")}`
|
|
35
|
+
: `# New Data
|
|
36
|
+
|
|
37
|
+
${formatDataItem(data.proposed_item, "Proposed (from " + data.source_persona + ")")}
|
|
38
|
+
|
|
39
|
+
*(This is a NEW ${data.data_type} - no existing data to compare)*`;
|
|
40
|
+
|
|
41
|
+
const guidelinesFragment = `# Validation Guidelines
|
|
42
|
+
|
|
43
|
+
## ACCEPT if:
|
|
44
|
+
- Change is factual and well-evidenced
|
|
45
|
+
- New information is consistent with what you know about the human
|
|
46
|
+
- Source persona's interpretation seems reasonable
|
|
47
|
+
- Data improves understanding of the human
|
|
48
|
+
|
|
49
|
+
## MODIFY if:
|
|
50
|
+
- Partially correct but needs refinement
|
|
51
|
+
- Description could be clearer or more accurate
|
|
52
|
+
- Sentiment or other fields seem off
|
|
53
|
+
- Good information but poorly expressed
|
|
54
|
+
|
|
55
|
+
## REJECT if:
|
|
56
|
+
- Contradicts known facts
|
|
57
|
+
- Seems like a hallucination or misunderstanding
|
|
58
|
+
- Would misrepresent the human
|
|
59
|
+
- Source persona lacks context to make this claim
|
|
60
|
+
|
|
61
|
+
## Considerations
|
|
62
|
+
- ${data.source_persona} may have context you don't
|
|
63
|
+
- The human's data should be accurate, not just convenient
|
|
64
|
+
- When in doubt, lean toward accepting with modifications`;
|
|
65
|
+
|
|
66
|
+
const outputFragment = `# Response Format
|
|
67
|
+
|
|
68
|
+
\`\`\`json
|
|
69
|
+
{
|
|
70
|
+
"decision": "accept" | "modify" | "reject",
|
|
71
|
+
"reason": "Brief explanation of your decision",
|
|
72
|
+
"modified_item": { ... } // Only if decision is "modify"
|
|
73
|
+
}
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
If modifying, include the corrected item with all fields.`;
|
|
77
|
+
|
|
78
|
+
const system = `${roleFragment}
|
|
79
|
+
|
|
80
|
+
${contextFragment}
|
|
81
|
+
|
|
82
|
+
${dataFragment}
|
|
83
|
+
|
|
84
|
+
${guidelinesFragment}
|
|
85
|
+
|
|
86
|
+
${outputFragment}`;
|
|
87
|
+
|
|
88
|
+
const user = `Review the ${data.data_type} "${data.item_name}" proposed by ${data.source_persona}.
|
|
89
|
+
|
|
90
|
+
Should this change be accepted, modified, or rejected?`;
|
|
91
|
+
|
|
92
|
+
return { system, user };
|
|
93
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DataItemBase } from "../../core/types.js";
|
|
2
|
+
|
|
3
|
+
export interface PromptOutput {
|
|
4
|
+
system: string;
|
|
5
|
+
user: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface EiValidationPromptData {
|
|
9
|
+
validation_type: "cross_persona";
|
|
10
|
+
item_name: string;
|
|
11
|
+
data_type: "fact" | "trait" | "topic" | "person";
|
|
12
|
+
context: string;
|
|
13
|
+
source_persona: string;
|
|
14
|
+
current_item?: DataItemBase;
|
|
15
|
+
proposed_item: DataItemBase;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface EiValidationResult {
|
|
19
|
+
decision: "accept" | "modify" | "reject";
|
|
20
|
+
reason: string;
|
|
21
|
+
modified_item?: DataItemBase;
|
|
22
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const PBKDF2_ITERATIONS = 310000;
|
|
2
|
+
const SALT = new TextEncoder().encode('ei-the-answer-is-42');
|
|
3
|
+
const ID_PLAINTEXT = 'the_answer_is_42';
|
|
4
|
+
const CHUNK_SIZE = 0x8000; // 32KB chunks for base64 conversion
|
|
5
|
+
|
|
6
|
+
export interface CryptoCredentials {
|
|
7
|
+
username: string;
|
|
8
|
+
passphrase: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface EncryptedPayload {
|
|
12
|
+
iv: string;
|
|
13
|
+
ciphertext: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function uint8ArrayToBase64(bytes: Uint8Array): string {
|
|
17
|
+
let binary = '';
|
|
18
|
+
for (let i = 0; i < bytes.length; i += CHUNK_SIZE) {
|
|
19
|
+
const chunk = bytes.subarray(i, Math.min(i + CHUNK_SIZE, bytes.length));
|
|
20
|
+
binary += String.fromCharCode.apply(null, chunk as unknown as number[]);
|
|
21
|
+
}
|
|
22
|
+
return btoa(binary);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function deriveKey(credentials: CryptoCredentials): Promise<CryptoKey> {
|
|
26
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
27
|
+
'raw',
|
|
28
|
+
new TextEncoder().encode(`${credentials.username}:${credentials.passphrase}`),
|
|
29
|
+
'PBKDF2',
|
|
30
|
+
false,
|
|
31
|
+
['deriveKey']
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return crypto.subtle.deriveKey(
|
|
35
|
+
{
|
|
36
|
+
name: 'PBKDF2',
|
|
37
|
+
salt: SALT,
|
|
38
|
+
iterations: PBKDF2_ITERATIONS,
|
|
39
|
+
hash: 'SHA-256',
|
|
40
|
+
},
|
|
41
|
+
keyMaterial,
|
|
42
|
+
{ name: 'AES-GCM', length: 256 },
|
|
43
|
+
false,
|
|
44
|
+
['encrypt', 'decrypt']
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function generateUserId(credentials: CryptoCredentials): Promise<string> {
|
|
49
|
+
const key = await deriveKey(credentials);
|
|
50
|
+
// Fixed IV for deterministic user ID - same credentials = same ID
|
|
51
|
+
// This is safe because we only ever encrypt the same static plaintext
|
|
52
|
+
const iv = new Uint8Array(12); // All zeros - deterministic
|
|
53
|
+
|
|
54
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
55
|
+
{ name: 'AES-GCM', iv },
|
|
56
|
+
key,
|
|
57
|
+
new TextEncoder().encode(ID_PLAINTEXT)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
return btoa(String.fromCharCode(...new Uint8Array(ciphertext)))
|
|
61
|
+
.replace(/\+/g, '-')
|
|
62
|
+
.replace(/\//g, '_')
|
|
63
|
+
.replace(/=/g, '');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function encrypt(data: string, credentials: CryptoCredentials): Promise<EncryptedPayload> {
|
|
67
|
+
const key = await deriveKey(credentials);
|
|
68
|
+
const iv = new Uint8Array(12);
|
|
69
|
+
crypto.getRandomValues(iv);
|
|
70
|
+
|
|
71
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
72
|
+
{ name: 'AES-GCM', iv },
|
|
73
|
+
key,
|
|
74
|
+
new TextEncoder().encode(data)
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
iv: uint8ArrayToBase64(iv),
|
|
79
|
+
ciphertext: uint8ArrayToBase64(new Uint8Array(ciphertext)),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function decrypt(payload: EncryptedPayload, credentials: CryptoCredentials): Promise<string> {
|
|
84
|
+
const key = await deriveKey(credentials);
|
|
85
|
+
|
|
86
|
+
const iv = Uint8Array.from(atob(payload.iv), c => c.charCodeAt(0));
|
|
87
|
+
const ciphertext = Uint8Array.from(atob(payload.ciphertext), c => c.charCodeAt(0));
|
|
88
|
+
|
|
89
|
+
const decrypted = await crypto.subtle.decrypt(
|
|
90
|
+
{ name: 'AES-GCM', iv },
|
|
91
|
+
key,
|
|
92
|
+
ciphertext
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return new TextDecoder().decode(decrypted);
|
|
96
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { Storage } from "./interface.js";
|
|
2
|
+
export { LocalStorage } from "./local.js";
|
|
3
|
+
export { remoteSync, RemoteSync, type RemoteSyncCredentials, type RemoteTimestamp, type SyncResult, type FetchResult } from "./remote.js";
|
|
4
|
+
export { encrypt, decrypt, generateUserId, type CryptoCredentials, type EncryptedPayload } from "./crypto.js";
|
|
5
|
+
export { yoloMerge } from "./merge.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { StorageState } from "../core/types.js";
|
|
2
|
+
|
|
3
|
+
export interface Storage {
|
|
4
|
+
isAvailable(): Promise<boolean>;
|
|
5
|
+
save(state: StorageState): Promise<void>;
|
|
6
|
+
load(): Promise<StorageState | null>;
|
|
7
|
+
moveToBackup(): Promise<void>;
|
|
8
|
+
loadBackup(): Promise<StorageState | null>;
|
|
9
|
+
}
|