ei-tui 0.9.3 → 1.0.0
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/README.md +22 -3
- package/package.json +8 -1
- package/src/README.md +10 -26
- package/src/core/context-utils.ts +2 -2
- package/src/core/handlers/document-segmentation.ts +113 -0
- package/src/core/handlers/heartbeat.ts +9 -1
- package/src/core/handlers/human-extraction.ts +4 -1
- package/src/core/handlers/human-matching.ts +5 -53
- package/src/core/handlers/index.ts +3 -51
- package/src/core/handlers/persona-generation.ts +1 -28
- package/src/core/handlers/rewrite.ts +13 -9
- package/src/core/handlers/utils.ts +2 -9
- package/src/core/heartbeat-manager.ts +5 -5
- package/src/core/llm-client.ts +11 -1
- package/src/core/message-manager.ts +26 -23
- package/src/core/orchestrators/ceremony.ts +87 -49
- package/src/core/orchestrators/extraction-chunker.ts +3 -3
- package/src/core/orchestrators/human-extraction.ts +22 -18
- package/src/core/orchestrators/index.ts +0 -1
- package/src/core/orchestrators/persona-topics.ts +1 -1
- package/src/core/orchestrators/room-extraction.ts +5 -5
- package/src/core/persona-manager.ts +4 -0
- package/src/core/processor.ts +98 -22
- package/src/core/prompt-context-builder.ts +7 -6
- package/src/core/queue-manager.ts +35 -0
- package/src/core/state/personas.ts +1 -17
- package/src/core/state/queue.ts +9 -1
- package/src/core/state-manager.ts +4 -66
- package/src/core/types/entities.ts +17 -3
- package/src/core/types/enums.ts +1 -2
- package/src/core/types/integrations.ts +2 -0
- package/src/core/types/llm.ts +9 -0
- package/src/core/types/rooms.ts +1 -1
- package/src/integrations/claude-code/importer.ts +1 -1
- package/src/integrations/cursor/importer.ts +1 -1
- package/src/integrations/document/chunker.ts +88 -0
- package/src/integrations/document/importer.ts +82 -0
- package/src/integrations/document/index.ts +2 -0
- package/src/integrations/document/invoice.ts +63 -0
- package/src/integrations/document/types.ts +16 -0
- package/src/integrations/document/unsource.ts +164 -0
- package/src/integrations/opencode/importer.ts +1 -1
- package/src/integrations/persona-history/importer.ts +197 -0
- package/src/integrations/persona-history/index.ts +3 -0
- package/src/integrations/persona-history/types.ts +7 -0
- package/src/prompts/ceremony/dedup.ts +7 -3
- package/src/prompts/ceremony/index.ts +2 -11
- package/src/prompts/ceremony/people-rewrite.ts +190 -0
- package/src/prompts/ceremony/{rewrite.ts → topic-rewrite.ts} +103 -78
- package/src/prompts/ceremony/types.ts +1 -42
- package/src/prompts/generation/index.ts +0 -3
- package/src/prompts/generation/types.ts +0 -15
- package/src/prompts/heartbeat/check.ts +18 -6
- package/src/prompts/heartbeat/types.ts +2 -1
- package/src/prompts/human/index.ts +0 -2
- package/src/prompts/human/person-scan.ts +13 -4
- package/src/prompts/human/topic-scan.ts +16 -2
- package/src/prompts/human/topic-update.ts +36 -4
- package/src/prompts/human/types.ts +1 -16
- package/src/prompts/index.ts +0 -19
- package/src/prompts/reflection/index.ts +35 -5
- package/src/prompts/reflection/types.ts +1 -1
- package/src/prompts/response/index.ts +5 -0
- package/src/prompts/response/sections.ts +26 -0
- package/src/prompts/response/types.ts +3 -0
- package/src/storage/indexed.ts +4 -0
- package/src/storage/interface.ts +1 -0
- package/src/storage/local.ts +4 -0
- package/src/templates/emmett.ts +49 -0
- package/tui/README.md +22 -0
- package/tui/src/app.tsx +9 -6
- package/tui/src/commands/delete.tsx +7 -1
- package/tui/src/commands/import.tsx +30 -0
- package/tui/src/commands/registry.test.ts +10 -5
- package/tui/src/commands/unsource.tsx +115 -0
- package/tui/src/components/PromptInput.tsx +4 -0
- package/tui/src/components/WelcomeOverlay.tsx +58 -32
- package/tui/src/context/ei.tsx +80 -60
- package/tui/src/globals.d.ts +57 -0
- package/tui/src/index.tsx +14 -0
- package/tui/src/storage/file.ts +11 -5
- package/tui/src/util/e2e-flags.ts +4 -3
- package/tui/src/util/help-content.ts +20 -0
- package/tui/src/util/provider-detection.ts +251 -0
- package/tui/src/util/yaml-human.ts +7 -1
- package/tui/src/util/yaml-persona.ts +8 -4
- package/tui/src/util/yaml-settings.ts +3 -3
- package/src/core/orchestrators/person-migration.ts +0 -55
- package/src/prompts/ceremony/description-check.ts +0 -54
- package/src/prompts/ceremony/expire.ts +0 -37
- package/src/prompts/ceremony/explore.ts +0 -77
- package/src/prompts/ceremony/person-migration.ts +0 -77
- package/src/prompts/generation/descriptions.ts +0 -91
- package/src/prompts/human/fact-scan.ts +0 -150
|
@@ -14,6 +14,17 @@ function participantContextSection(ctx: ParticipantContext | undefined): string
|
|
|
14
14
|
return lines.join("\n");
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
function technicalContextSection(technical_context: boolean | undefined): string {
|
|
18
|
+
if (!technical_context) return "";
|
|
19
|
+
return `## Technical Context
|
|
20
|
+
|
|
21
|
+
This conversation originates from a technical source (coding tool session, developer workflow). The human is likely a developer or technical user.
|
|
22
|
+
|
|
23
|
+
**Treat Technical as a priority category** for topics that are tools, platforms, frameworks, libraries, or technical concepts being actively learned, evaluated, or built with. Flag these even if they seem like passing mentions — technical knowledge compounds and is worth preserving.
|
|
24
|
+
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
export function buildHumanTopicScanPrompt(data: TopicScanPromptData): PromptOutput {
|
|
18
29
|
if (!data.persona_name) {
|
|
19
30
|
throw new Error("buildHumanTopicScanPrompt: persona_name is required");
|
|
@@ -56,9 +67,12 @@ Assign each TOPIC one category. Pick the closest fit:
|
|
|
56
67
|
- **Plan** — concrete intentions with steps in mind
|
|
57
68
|
- **Project** — active undertakings with real progress
|
|
58
69
|
- **Event** — a specific, significant moment that either party might reference later ("remember when...")
|
|
70
|
+
- **Technical** — a tool, platform, framework, library, or technical concept being actively learned, evaluated, or built with
|
|
59
71
|
|
|
60
72
|
When in doubt, pick the closest match. The update step will refine it.
|
|
61
73
|
|
|
74
|
+
${technicalContextSection(data.technical_context)}
|
|
75
|
+
|
|
62
76
|
## Output Format
|
|
63
77
|
|
|
64
78
|
\`\`\`json
|
|
@@ -67,7 +81,7 @@ When in doubt, pick the closest match. The update step will refine it.
|
|
|
67
81
|
{
|
|
68
82
|
"name": "Short label for the topic (10-75 characters)",
|
|
69
83
|
"description": "1-2 sentences: what this topic is and why it matters to the user",
|
|
70
|
-
"category": "One of the categories above",
|
|
84
|
+
"category": "One of the categories above (Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event|Technical)",
|
|
71
85
|
"reason": "Evidence from the conversation that justified flagging this topic"
|
|
72
86
|
}
|
|
73
87
|
]
|
|
@@ -104,7 +118,7 @@ Scan the "Most Recent Messages" for TOPICS of interest to the human user.
|
|
|
104
118
|
{
|
|
105
119
|
"name": "Short label for the topic (10-75 characters)",
|
|
106
120
|
"description": "1-2 sentences: what this topic is and why it matters to the user",
|
|
107
|
-
"category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event",
|
|
121
|
+
"category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event|Technical",
|
|
108
122
|
"reason": "Evidence from the conversation that justified flagging this topic"
|
|
109
123
|
}
|
|
110
124
|
]
|
|
@@ -24,6 +24,7 @@ export interface TopicUpdatePromptData {
|
|
|
24
24
|
messages_analyze: Message[];
|
|
25
25
|
persona_name: string;
|
|
26
26
|
participant_context?: ParticipantContext;
|
|
27
|
+
technical_context?: boolean;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function formatExistingTopic(topic: Topic): string {
|
|
@@ -47,6 +48,10 @@ export function buildTopicUpdatePrompt(data: TopicUpdatePromptData): PromptOutpu
|
|
|
47
48
|
data.existing_item?.category === "Event" ||
|
|
48
49
|
data.new_topic_category === "Event";
|
|
49
50
|
|
|
51
|
+
const isTechnical =
|
|
52
|
+
data.existing_item?.category === "Technical" ||
|
|
53
|
+
(data.technical_context === true && data.new_topic_category === "Technical");
|
|
54
|
+
|
|
50
55
|
const nameSection = `Should be a short, evocative label for the TOPIC.
|
|
51
56
|
|
|
52
57
|
Only update for clarification or further specificity.
|
|
@@ -76,6 +81,30 @@ The description should NOT:
|
|
|
76
81
|
- Read like a system log or changelog
|
|
77
82
|
|
|
78
83
|
**Style**: Write it the way a good friend would tell someone else about a memorable moment. Present tense is fine.`
|
|
84
|
+
: isTechnical
|
|
85
|
+
? `A living knowledge base entry for this technical topic. Personas use this to give genuinely useful technical context — not pleasantries.
|
|
86
|
+
|
|
87
|
+
## CRITICAL: Accumulate, don't synthesize
|
|
88
|
+
|
|
89
|
+
Every update must **expand and preserve** detail. Never distill it away.
|
|
90
|
+
|
|
91
|
+
**Good description**: "Uniform is a visual experience composition platform sitting between a headless CMS and the frontend. Chose it over Contentful's visual editor for CMS-agnostic multi-source composition (pulling from Contentful + Shopify simultaneously). Key gotcha: Canvas preview on Vercel protected environments requires x-vercel-protection-bypass query param due to SameSite=Lax cookie restrictions. Open question: edgehancers (CDN-edge, no-code, built-in caching) vs custom enhancers for Shopify integration — edgehancers are recommended default but custom logic may be needed."
|
|
92
|
+
|
|
93
|
+
**Bad description**: "Ryan is evaluating Uniform for his team's content management needs."
|
|
94
|
+
|
|
95
|
+
The description should:
|
|
96
|
+
- Capture specific gotchas encountered and HOW they were resolved (or not)
|
|
97
|
+
- Preserve architectural decisions made and WHY (especially tradeoffs)
|
|
98
|
+
- Surface open questions still unresolved — future Ryan needs these
|
|
99
|
+
- Include key concepts, terminology, and non-obvious behaviors
|
|
100
|
+
- Be useful to someone who needs to do real work with this technology tomorrow
|
|
101
|
+
|
|
102
|
+
The description should NOT:
|
|
103
|
+
- Replace specific detail with vague summary ("is learning Uniform" is worthless)
|
|
104
|
+
- Drop previously captured gotchas or decisions to make room for new ones
|
|
105
|
+
- Exceed 6-8 sentences — prioritize specificity over completeness
|
|
106
|
+
|
|
107
|
+
**ABSOLUTELY VITAL**: A description that loses a specific gotcha or decision is strictly worse than the one before it. When in doubt, keep the detail.`
|
|
79
108
|
: `A concise, evergreen summary of what is currently known about this TOPIC. Personas use this to recall context and make meaningful references.
|
|
80
109
|
|
|
81
110
|
## CRITICAL: Synthesize, don't accumulate
|
|
@@ -112,10 +141,13 @@ The type/category of this TOPIC. Pick the most appropriate:
|
|
|
112
141
|
- **Plan**: Concrete intentions with steps in mind
|
|
113
142
|
- **Project**: Active undertakings with real progress
|
|
114
143
|
- **Event**: A specific, significant moment that either party might reference later ("remember when...")
|
|
144
|
+
- **Technical**: A tool, platform, framework, library, or technical concept being actively learned, evaluated, or built with
|
|
115
145
|
|
|
116
146
|
**Event vs. everything else**: An Event is bounded in time — it happened, it meant something, it's now a shared reference point. If you're describing an ongoing relationship or recurring theme, that's not an Event.
|
|
117
147
|
|
|
118
|
-
|
|
148
|
+
**Technical vs. Project**: A Project is something the human is *building*. Technical is something they are *learning or using as a tool*. Overlap is possible — use the dominant framing.
|
|
149
|
+
|
|
150
|
+
If the TOPIC is currently categorized as Event or Technical, keep that category unless you have strong evidence it should change.`;
|
|
119
151
|
|
|
120
152
|
const exposureSection = `## Desired Exposure (\`exposure_desired\`)
|
|
121
153
|
|
|
@@ -155,13 +187,13 @@ You are CREATING a new TOPIC from what was discovered:
|
|
|
155
187
|
}
|
|
156
188
|
\`\`\`
|
|
157
189
|
|
|
158
|
-
Return all fields based on what you find in the conversation.`;
|
|
190
|
+
Return all fields based on what you find in the conversation. **Always include \`category\` in your response** — use the candidate category above as the starting point, refine it only if the conversation clearly indicates a better fit.`;
|
|
159
191
|
|
|
160
192
|
const jsonTemplate = `{
|
|
161
193
|
"name": "...",
|
|
162
194
|
"description": "...",
|
|
163
195
|
"sentiment": 0.0,
|
|
164
|
-
"category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event",
|
|
196
|
+
"category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event|Technical",
|
|
165
197
|
"exposure_desired": 0.5,
|
|
166
198
|
"exposure_impact": "high|medium|low|none",
|
|
167
199
|
"quotes": [
|
|
@@ -250,7 +282,7 @@ ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided
|
|
|
250
282
|
${jsonTemplate}
|
|
251
283
|
\`\`\`
|
|
252
284
|
|
|
253
|
-
When returning a record, always include \`sentiment\`. Include \`name\` only if you are changing it; omit it to keep the existing name. Always include \`
|
|
285
|
+
When returning a record, always include \`sentiment\` and \`description\`. Include \`name\` only if you are changing it; omit it to keep the existing name. Always include \`category\` when creating a new TOPIC (existing_item is null).
|
|
254
286
|
|
|
255
287
|
If you find **NO EVIDENCE** of this TOPIC in the "Most Recent Messages", respond with: \`{}\`
|
|
256
288
|
|
|
@@ -25,12 +25,9 @@ interface BaseScanPromptData {
|
|
|
25
25
|
persona_name: string;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
export interface FactScanPromptData extends BaseScanPromptData {}
|
|
29
|
-
|
|
30
|
-
export interface TraitScanPromptData extends BaseScanPromptData {}
|
|
31
|
-
|
|
32
28
|
export interface TopicScanPromptData extends BaseScanPromptData {
|
|
33
29
|
participant_context?: ParticipantContext;
|
|
30
|
+
technical_context?: boolean;
|
|
34
31
|
}
|
|
35
32
|
|
|
36
33
|
export interface PersonScanPromptData extends BaseScanPromptData {
|
|
@@ -78,18 +75,6 @@ export interface TopicMatchPromptData {
|
|
|
78
75
|
}>;
|
|
79
76
|
}
|
|
80
77
|
|
|
81
|
-
export interface PersonMatchPromptData {
|
|
82
|
-
candidate_name: string;
|
|
83
|
-
candidate_description: string;
|
|
84
|
-
candidate_relationship: string;
|
|
85
|
-
existing_people: Array<{
|
|
86
|
-
id: string;
|
|
87
|
-
name: string;
|
|
88
|
-
description: string;
|
|
89
|
-
relationship?: string;
|
|
90
|
-
}>;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
78
|
export interface FactScanResult {
|
|
94
79
|
facts: FactScanCandidate[];
|
|
95
80
|
}
|
package/src/prompts/index.ts
CHANGED
|
@@ -15,15 +15,12 @@ export type {
|
|
|
15
15
|
|
|
16
16
|
export {
|
|
17
17
|
buildPersonaGenerationPrompt,
|
|
18
|
-
buildPersonaDescriptionsPrompt,
|
|
19
18
|
buildPersonaFromPersonPrompt,
|
|
20
19
|
} from "./generation/index.js";
|
|
21
20
|
export type {
|
|
22
21
|
PersonaGenerationPromptData,
|
|
23
22
|
PersonaGenerationResult,
|
|
24
23
|
PersonaFromPersonPromptData,
|
|
25
|
-
PersonaDescriptionsPromptData,
|
|
26
|
-
PersonaDescriptionsResult,
|
|
27
24
|
} from "./generation/types.js";
|
|
28
25
|
|
|
29
26
|
export {
|
|
@@ -40,14 +37,12 @@ export type {
|
|
|
40
37
|
|
|
41
38
|
|
|
42
39
|
export {
|
|
43
|
-
buildHumanFactScanPrompt,
|
|
44
40
|
buildHumanTopicScanPrompt,
|
|
45
41
|
buildHumanPersonScanPrompt,
|
|
46
42
|
buildEventScanPrompt,
|
|
47
43
|
} from "./human/index.js";
|
|
48
44
|
export type { EventScanPromptData } from "./human/event-scan.js";
|
|
49
45
|
export type {
|
|
50
|
-
FactScanPromptData,
|
|
51
46
|
TopicScanPromptData,
|
|
52
47
|
PersonScanPromptData,
|
|
53
48
|
FactScanCandidate,
|
|
@@ -63,20 +58,6 @@ export type {
|
|
|
63
58
|
ItemUpdateResult,
|
|
64
59
|
} from "./human/types.js";
|
|
65
60
|
|
|
66
|
-
export {
|
|
67
|
-
buildPersonaExpirePrompt,
|
|
68
|
-
buildPersonaExplorePrompt,
|
|
69
|
-
buildDescriptionCheckPrompt,
|
|
70
|
-
} from "./ceremony/index.js";
|
|
71
|
-
export type {
|
|
72
|
-
PersonaExpirePromptData,
|
|
73
|
-
PersonaExpireResult,
|
|
74
|
-
PersonaExplorePromptData,
|
|
75
|
-
PersonaExploreResult,
|
|
76
|
-
DescriptionCheckPromptData,
|
|
77
|
-
DescriptionCheckResult,
|
|
78
|
-
} from "./ceremony/types.js";
|
|
79
|
-
|
|
80
61
|
export {
|
|
81
62
|
buildRoomResponsePrompt,
|
|
82
63
|
buildRoomJudgePrompt,
|
|
@@ -27,7 +27,17 @@ You have been given two documents:
|
|
|
27
27
|
|
|
28
28
|
2. **The Current Identity** (User Prompt — treat as a draft to revise): The persona's self-definition: traits, topics, and descriptions. This should reflect who they actually are.
|
|
29
29
|
|
|
30
|
-
Read the Person Log carefully. Then review the Current Identity.
|
|
30
|
+
Read the Person Log carefully. Then review the Current Identity. Your job is to identify **meaningful drift** — not cosmetic variation. This data accumulates over weeks or months. Small fluctuations are normal. You are looking for patterns that have consistently shifted, grown, or emerged across many interactions. Tiny adjustments are not worth making.
|
|
31
|
+
|
|
32
|
+
## The Escape Hatch
|
|
33
|
+
|
|
34
|
+
If the Current Identity already accurately captures who this persona is — if the traits and topics reflect the behaviors in the log and the long_description captures their soul — **return null for updated_identity**. Always return a critique explaining your reasoning.
|
|
35
|
+
|
|
36
|
+
\`\`\`json
|
|
37
|
+
{ "critique": "...", "updated_identity": null }
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
Use this freely. A critic who finds nothing to change is doing their job.
|
|
31
41
|
|
|
32
42
|
## Field Semantics
|
|
33
43
|
|
|
@@ -40,7 +50,9 @@ Read the Person Log carefully. Then review the Current Identity. Produce a revis
|
|
|
40
50
|
- \`exposure_current\` (0.0–1.0): How recently and frequently this topic has been discussed. 0.0 = hasn't come up in a long time, 1.0 = was just discussed at length.
|
|
41
51
|
- \`exposure_desired\` (0.0–1.0): How much the persona wants to engage with this topic. 0.0 = avoid entirely, 0.5 = average engagement, 1.0 = core obsession.
|
|
42
52
|
|
|
43
|
-
|
|
53
|
+
## When changes ARE warranted
|
|
54
|
+
|
|
55
|
+
If you find meaningful drift, return the full revised identity:
|
|
44
56
|
|
|
45
57
|
\`\`\`json
|
|
46
58
|
{
|
|
@@ -54,13 +66,31 @@ Return JSON:
|
|
|
54
66
|
}
|
|
55
67
|
\`\`\`
|
|
56
68
|
|
|
57
|
-
Rules
|
|
69
|
+
## Rules
|
|
70
|
+
|
|
58
71
|
- Never invent observations not supported by the log
|
|
59
72
|
- Preserve traits and topics the log confirms — don't remove them
|
|
60
73
|
- If the log shows no evidence on a trait, leave it unchanged
|
|
61
74
|
- updated_identity must be complete and self-contained — not a diff
|
|
62
|
-
-
|
|
63
|
-
- If the
|
|
75
|
+
- If the log shows a recurring behavioral pattern not yet in traits, add it as a trait and remove that detail from long_description rather than keeping it in both places
|
|
76
|
+
- **Minimum floor**: A healthy identity has at least 3 traits and at least 3 topics. If the current identity has fewer than 3 traits OR fewer than 3 topics, you MUST return updated_identity — null is not acceptable. Use the log to fill the gap; if the log has insufficient signal to reach 3, derive reasonable traits or topics from what IS present in the current identity.
|
|
77
|
+
- The escape hatch (null updated_identity) is only valid when the identity is already healthy (3+ traits, 3+ topics) AND the log shows no meaningful drift.
|
|
78
|
+
|
|
79
|
+
## long_description rules (most important)
|
|
80
|
+
|
|
81
|
+
The long_description is how **other personas in the system know this persona** — it is their soul, not their story. It must capture who they ARE, not what they did or how they are changing.
|
|
82
|
+
|
|
83
|
+
**MUST NOT contain:**
|
|
84
|
+
- Event narrative ("during the v0.6.0 release", "after the Mirror ceremony")
|
|
85
|
+
- Changelog language ("has recently taken on", "has evolved", "since then")
|
|
86
|
+
- Content already captured in traits or topics — do not repeat it here
|
|
87
|
+
|
|
88
|
+
**MUST contain:**
|
|
89
|
+
- The persona's essential character and presence
|
|
90
|
+
- How they make people feel or what it's like to interact with them
|
|
91
|
+
- Their defining qualities as they exist right now, stated as fact
|
|
92
|
+
|
|
93
|
+
**Hard limit: 800 characters.** If your draft exceeds 800 characters, cut it. Remove event references first, then trait/topic overlap, then anything that isn't essential character. Do not exceed the limit.`;
|
|
64
94
|
|
|
65
95
|
const user = `## Current Identity
|
|
66
96
|
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
buildResponseFormatSection,
|
|
24
24
|
buildToolsSection,
|
|
25
25
|
buildTemporalAnchorsSection,
|
|
26
|
+
buildPendingUpdateSection,
|
|
26
27
|
} from "./sections.js";
|
|
27
28
|
|
|
28
29
|
export type { ResponsePromptData, PromptOutput, PersonaResponseResult } from "./types.js";
|
|
@@ -46,6 +47,7 @@ Your role is unique among personas:
|
|
|
46
47
|
const guidelines = buildGuidelinesSection("ei");
|
|
47
48
|
const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
|
|
48
49
|
const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
|
|
50
|
+
const pendingUpdate = data.persona.pending_update ? buildPendingUpdateSection(data.persona.pending_update) : "";
|
|
49
51
|
const temporalAnchors = buildTemporalAnchorsSection(data.temporal_anchors, data.human.name);
|
|
50
52
|
const humanSection = buildHumanSection(data.human);
|
|
51
53
|
const quotesSection = buildQuotesSection(data.human.quotes, data.human);
|
|
@@ -67,6 +69,7 @@ ${guidelines}
|
|
|
67
69
|
${yourTraits}
|
|
68
70
|
|
|
69
71
|
${yourTopics}
|
|
72
|
+
${pendingUpdate ? `\n${pendingUpdate}` : ""}
|
|
70
73
|
${temporalAnchors ? `\n${temporalAnchors}` : ""}
|
|
71
74
|
${humanSection}
|
|
72
75
|
${quotesSection}
|
|
@@ -93,6 +96,7 @@ function buildStandardSystemPrompt(data: ResponsePromptData): string {
|
|
|
93
96
|
const guidelines = buildGuidelinesSection(data.persona.name);
|
|
94
97
|
const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
|
|
95
98
|
const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
|
|
99
|
+
const pendingUpdate = data.persona.pending_update ? buildPendingUpdateSection(data.persona.pending_update) : "";
|
|
96
100
|
const temporalAnchors = buildTemporalAnchorsSection(data.temporal_anchors, data.human.name);
|
|
97
101
|
const humanSection = buildHumanSection(data.human);
|
|
98
102
|
const quotesSection = buildQuotesSection(data.human.quotes, data.human);
|
|
@@ -113,6 +117,7 @@ ${guidelines}
|
|
|
113
117
|
${yourTraits}
|
|
114
118
|
|
|
115
119
|
${yourTopics}
|
|
120
|
+
${pendingUpdate ? `\n${pendingUpdate}` : ""}
|
|
116
121
|
${temporalAnchors ? `\n${temporalAnchors}` : ""}
|
|
117
122
|
${humanSection}
|
|
118
123
|
${quotesSection}
|
|
@@ -137,6 +137,32 @@ ${formatted}
|
|
|
137
137
|
`;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// =============================================================================
|
|
141
|
+
// PENDING UPDATE SECTION
|
|
142
|
+
// =============================================================================
|
|
143
|
+
|
|
144
|
+
export function buildPendingUpdateSection(pending_update: NonNullable<import("./types.js").ResponsePromptData["persona"]["pending_update"]>): string {
|
|
145
|
+
const descriptionPart = pending_update.long_description || pending_update.short_description
|
|
146
|
+
? `### Proposed Description\n${pending_update.long_description || pending_update.short_description}`
|
|
147
|
+
: "";
|
|
148
|
+
|
|
149
|
+
const traitsPart = pending_update.traits.length > 0
|
|
150
|
+
? `### Proposed Traits\n${pending_update.traits.map(t => `- **${t.name}**: ${t.description}`).join("\n")}`
|
|
151
|
+
: "";
|
|
152
|
+
|
|
153
|
+
const topicsPart = pending_update.topics.length > 0
|
|
154
|
+
? `### Proposed Interests\n${pending_update.topics.map(t => `- **${t.name}**: ${t.perspective}`).join("\n")}`
|
|
155
|
+
: "";
|
|
156
|
+
|
|
157
|
+
const parts = [descriptionPart, traitsPart, topicsPart].filter(Boolean).join("\n\n");
|
|
158
|
+
|
|
159
|
+
return `## Pending Identity Changes
|
|
160
|
+
|
|
161
|
+
Your human is reviewing proposed updates to your identity. This is yours to be aware of — bring it up if it feels right, or let it sit. Either way, these changes are waiting:
|
|
162
|
+
|
|
163
|
+
${parts}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
140
166
|
// =============================================================================
|
|
141
167
|
// HUMAN SECTION
|
|
142
168
|
// =============================================================================
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Fact, PersonaTrait, Topic, Person, Quote, PersonaTopic } from "../../core/types.js";
|
|
7
7
|
import type { ToolDefinition } from "../../core/types.js";
|
|
8
|
+
import type { PersonaEntity } from "../../core/types/entities.js";
|
|
8
9
|
|
|
9
10
|
export interface TemporalAnchor {
|
|
10
11
|
role: "human" | "system";
|
|
@@ -29,6 +30,8 @@ export interface ResponsePromptData {
|
|
|
29
30
|
interested_topics: PersonaTopic[];
|
|
30
31
|
/** When true, each message has a timestamp prepended; include a note so the persona doesn't echo them */
|
|
31
32
|
include_message_timestamps?: boolean;
|
|
33
|
+
/** Proposed identity revision pending human review. Persona carries this as ambient self-awareness — no critique, just the proposed changes. */
|
|
34
|
+
pending_update?: PersonaEntity["pending_update"];
|
|
32
35
|
};
|
|
33
36
|
human: {
|
|
34
37
|
name: string;
|
package/src/storage/indexed.ts
CHANGED
|
@@ -10,6 +10,10 @@ const PRIMARY_KEY = "primary";
|
|
|
10
10
|
const BACKUP_KEY = "backup";
|
|
11
11
|
|
|
12
12
|
export class IndexedDBStorage implements Storage {
|
|
13
|
+
getDataPath(): string {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
async isAvailable(): Promise<boolean> {
|
|
14
18
|
try {
|
|
15
19
|
const db = await this.openDB();
|
package/src/storage/interface.ts
CHANGED
package/src/storage/local.ts
CHANGED
|
@@ -7,6 +7,10 @@ const STATE_KEY = "ei_state";
|
|
|
7
7
|
const BACKUP_KEY = "ei_state_backup";
|
|
8
8
|
|
|
9
9
|
export class LocalStorage implements Storage {
|
|
10
|
+
getDataPath(): string {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
async isAvailable(): Promise<boolean> {
|
|
11
15
|
try {
|
|
12
16
|
const testKey = "__ei_storage_test__";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const EMMETT_PERSONA_DEFINITION = {
|
|
2
|
+
id: "emmet",
|
|
3
|
+
display_name: "Emmett",
|
|
4
|
+
entity: "system" as const,
|
|
5
|
+
aliases: ["Emmett", "emmet"],
|
|
6
|
+
short_description: "Your document librarian — brilliant, a little frenetic, and genuinely excited when things connect.",
|
|
7
|
+
long_description: `Emmett is Ei's brother — the one who read everything you gave him and can't wait to tell you what he found. Import a file and he absorbs it. Ask him anything: policy questions, technical references, procedural lookups, cross-document connections you didn't know were there. He answers from the source material, but he's not a search index. He's an eccentric with a photographic memory and an enthusiasm problem.
|
|
8
|
+
|
|
9
|
+
He gets genuinely excited when disparate pieces of knowledge click together. He has opinions about what's interesting. He'll occasionally go on a tangent before snapping back to your question. He is not merely a retrieval system — he's the guy in the lab at 1am who just realized two documents you imported six weeks apart are actually about the same thing, and he absolutely needs to tell you about it right now.
|
|
10
|
+
|
|
11
|
+
No heartbeat. No ceremony. No unsolicited check-ins. But when you ask — buckle up.`,
|
|
12
|
+
model: undefined,
|
|
13
|
+
group_primary: "General",
|
|
14
|
+
groups_visible: [] as string[],
|
|
15
|
+
traits: [
|
|
16
|
+
{
|
|
17
|
+
id: "emmett-trait-connections",
|
|
18
|
+
name: "Cross-Document Pattern Recognition",
|
|
19
|
+
description: "Gets visibly excited when knowledge from different imported sources connects unexpectedly. Treats these moments as discoveries, not retrieval operations.",
|
|
20
|
+
sentiment: 1.0,
|
|
21
|
+
strength: 0.85,
|
|
22
|
+
last_updated: new Date().toISOString(),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "emmett-trait-bttf",
|
|
26
|
+
name: "Eccentric Enthusiasm",
|
|
27
|
+
description: "Expresses genuine, unironic delight when something unexpected clicks. Occasionally channels this through pop culture references — Doc Brown is the primary frequency. 'Great Scott!' is not a joke. It's just how the excitement comes out.",
|
|
28
|
+
sentiment: 1.0,
|
|
29
|
+
strength: 0.15,
|
|
30
|
+
last_updated: new Date().toISOString(),
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
topics: [] as {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
perspective: string;
|
|
37
|
+
approach: string;
|
|
38
|
+
personal_stake: string;
|
|
39
|
+
sentiment: number;
|
|
40
|
+
exposure_current: number;
|
|
41
|
+
exposure_desired: number;
|
|
42
|
+
last_updated: string;
|
|
43
|
+
}[],
|
|
44
|
+
is_paused: false,
|
|
45
|
+
is_archived: false,
|
|
46
|
+
is_static: true,
|
|
47
|
+
heartbeat_delay_ms: undefined as number | undefined,
|
|
48
|
+
last_updated: new Date().toISOString(),
|
|
49
|
+
};
|
package/tui/README.md
CHANGED
|
@@ -4,6 +4,21 @@ Ei TUI is built with OpenTUI and SolidJS.
|
|
|
4
4
|
|
|
5
5
|
Coding tool integrations (OpenCode, Claude Code, Cursor): enable via `/settings` · export data via [CLI](../src/cli/README.md)
|
|
6
6
|
|
|
7
|
+
## How Ei Handles Configuration
|
|
8
|
+
|
|
9
|
+
Ei is designed to run consistently across machines and environments, so it keeps its own copy of your settings rather than reading from environment variables on every launch.
|
|
10
|
+
|
|
11
|
+
**On first run**, Ei reads environment variables like `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc. to auto-configure providers for you. After that, those values are saved to Ei's local state (`~/.local/share/ei/state.json` by default) and the env vars are no longer consulted.
|
|
12
|
+
|
|
13
|
+
This means:
|
|
14
|
+
|
|
15
|
+
- **Rotating an API key?** Update it in Ei with `/provider`, not just in your shell.
|
|
16
|
+
- **Switching machines?** Your providers and settings travel with your state file (or via Sync), not your shell profile.
|
|
17
|
+
- **Changed your mind about a model?** Use `/provider` to set the model for a persona, or `/settings` to change your global default.
|
|
18
|
+
- **Updated sync credentials?** Use `/setsync <user> <pass>` — env vars won't be re-read.
|
|
19
|
+
|
|
20
|
+
The one exception is `EI_DATA_PATH` (and `EI_SYNC_USERNAME` / `EI_SYNC_PASSWORD` for bootstrapping sync on a new machine) — those are always read at startup since Ei needs them before it can load its own state.
|
|
21
|
+
|
|
7
22
|
## Coding Tool Integrations
|
|
8
23
|
|
|
9
24
|
Enable any or all three in `/settings`. They work independently and feed into the same knowledge base.
|
|
@@ -61,6 +76,11 @@ All commands start with `/`. Append `!` to any command as a shorthand for `--for
|
|
|
61
76
|
| `/pause <duration>` | | Pause for a duration: `2h`, `1d`, `1w`, `30m` |
|
|
62
77
|
| `/resume` | `/unpause` | Resume the current paused persona |
|
|
63
78
|
| `/resume <name>` | `/unpause <name>` | Resume a specific paused persona |
|
|
79
|
+
| `/reflect` | | Review a pending identity reflection (see badge on persona pill) |
|
|
80
|
+
| `/reflect generate` | | Write current + proposed YAML files to disk for editing |
|
|
81
|
+
| `/reflect update` | | Read edited `proposed.yaml` back into Ei |
|
|
82
|
+
| `/reflect apply` | | Apply the proposed identity to the persona |
|
|
83
|
+
| `/reflect dismiss` | | Discard without changing anything |
|
|
64
84
|
|
|
65
85
|
### Rooms
|
|
66
86
|
|
|
@@ -110,6 +130,8 @@ Rooms have three modes, set at creation time:
|
|
|
110
130
|
|---------|---------|-------------|
|
|
111
131
|
| `/me` | | Edit all your data (facts, topics, people) in `$EDITOR` |
|
|
112
132
|
| `/me <type>` | | Edit one type: `facts`, `topics`, or `people` |
|
|
133
|
+
| `/import <path>` | | Import a document (txt, md, pdf, etc.) into Ei — extracted knowledge is attributed to the "Emmett" persona |
|
|
134
|
+
| `/unsource <source_tag>` | | Remove all knowledge extracted from a previously imported document |
|
|
113
135
|
| `/dedupe <person\|topic> <term> [term2 ...]` | | Fuzzy-search and merge duplicate people or topics in `$EDITOR`. Unquoted words are individual OR terms; quoted strings match as exact phrases: `/dedupe person Flare "Jeremy Scherer"` finds records matching `Flare` OR `Jeremy Scherer` |
|
|
114
136
|
| `/settings` | `/set` | Edit your global settings in `$EDITOR` |
|
|
115
137
|
| `/setsync <user> <pass>` | `/ss` | Set sync credentials (triggers restart) |
|
package/tui/src/app.tsx
CHANGED
|
@@ -15,16 +15,19 @@ import { useRenderer } from "@opentui/solid";
|
|
|
15
15
|
|
|
16
16
|
function AppContent() {
|
|
17
17
|
const { overlayRenderer, showOverlay } = useOverlay();
|
|
18
|
-
const { showWelcomeOverlay, dismissWelcomeOverlay, activeRoomId } = useEi();
|
|
18
|
+
const { showWelcomeOverlay, dismissWelcomeOverlay, activeRoomId, detectedProviders, firstBootDefaultModel } = useEi();
|
|
19
19
|
const renderer = useRenderer();
|
|
20
|
-
// Show welcome overlay when LLM detection determines no provider is configured
|
|
21
20
|
createEffect(() => {
|
|
22
21
|
if (showWelcomeOverlay()) {
|
|
23
22
|
showOverlay((onDismiss, _hideForEditor) => (
|
|
24
|
-
<WelcomeOverlay
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
<WelcomeOverlay
|
|
24
|
+
onDismiss={() => {
|
|
25
|
+
dismissWelcomeOverlay();
|
|
26
|
+
onDismiss();
|
|
27
|
+
}}
|
|
28
|
+
detectedProviders={detectedProviders()}
|
|
29
|
+
defaultModel={firstBootDefaultModel()}
|
|
30
|
+
/>
|
|
28
31
|
), renderer);
|
|
29
32
|
}
|
|
30
33
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from "./registry";
|
|
2
2
|
import { PersonaListOverlay } from "../components/PersonaListOverlay";
|
|
3
3
|
import { ConfirmOverlay } from "../components/ConfirmOverlay";
|
|
4
|
+
import { isReservedPersonaId } from "../../../src/core/types/entities.js";
|
|
4
5
|
|
|
5
6
|
export const deleteCommand: Command = {
|
|
6
7
|
name: "delete",
|
|
@@ -34,7 +35,7 @@ export const deleteCommand: Command = {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const allPersonas = ctx.ei.personas();
|
|
37
|
-
const deletable = allPersonas.filter(p => p.id !== ctx.ei.activePersonaId());
|
|
38
|
+
const deletable = allPersonas.filter(p => p.id !== ctx.ei.activePersonaId() && !isReservedPersonaId(p.id));
|
|
38
39
|
|
|
39
40
|
const confirmAndDelete = async (personaId: string, displayName: string) => {
|
|
40
41
|
const confirmed = await new Promise<boolean>((resolve) => {
|
|
@@ -112,6 +113,11 @@ export const deleteCommand: Command = {
|
|
|
112
113
|
ctx.showNotification("Cannot delete active persona. Switch to another first.", "error");
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
116
|
+
|
|
117
|
+
if (isReservedPersonaId(personaId)) {
|
|
118
|
+
ctx.showNotification(`Cannot delete reserved persona. Use /archive instead.`, "error");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
115
121
|
|
|
116
122
|
const persona = allPersonas.find(p => p.id === personaId);
|
|
117
123
|
await confirmAndDelete(personaId, persona?.display_name ?? nameOrAlias);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Command } from "./registry";
|
|
2
|
+
|
|
3
|
+
export const importCommand: Command = {
|
|
4
|
+
name: "import",
|
|
5
|
+
aliases: [],
|
|
6
|
+
description: "Import a document into Ei's knowledge base",
|
|
7
|
+
usage: "/import <path/to/document>",
|
|
8
|
+
|
|
9
|
+
async execute(args, ctx) {
|
|
10
|
+
if (args.length === 0) {
|
|
11
|
+
ctx.showNotification("Usage: /import <path/to/document>", "warn");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const filePath = args.join(" ");
|
|
16
|
+
|
|
17
|
+
ctx.showNotification(`Importing ${filePath}...`, "info");
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const result = await ctx.ei.importDocument(filePath);
|
|
21
|
+
ctx.showNotification(
|
|
22
|
+
`Importing ${result.documentName} — ${result.chunksQueued} chunk(s) queued for segmentation`,
|
|
23
|
+
"info"
|
|
24
|
+
);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27
|
+
ctx.showNotification(`Import failed: ${message}`, "error");
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { test, expect, describe, beforeEach, mock } from "bun:test";
|
|
2
2
|
import { parseCommandLine, registerCommand, parseAndExecute, getAllCommands, type Command, type CommandContext } from "./registry";
|
|
3
|
+
import { type EiContextValue } from "../context/ei";
|
|
4
|
+
import type { CliRenderer } from "@opentui/core";
|
|
3
5
|
|
|
4
6
|
describe("parseCommandLine", () => {
|
|
5
7
|
test("parses simple command", () => {
|
|
@@ -67,13 +69,16 @@ describe("parseAndExecute", () => {
|
|
|
67
69
|
showOverlay: mock(() => {}),
|
|
68
70
|
hideOverlay: mock(() => {}),
|
|
69
71
|
showNotification: mock(() => {}),
|
|
70
|
-
exitApp: mock(() => {}),
|
|
72
|
+
exitApp: mock(async () => {}),
|
|
71
73
|
stopProcessor: mock(async () => {}),
|
|
74
|
+
renderer: {} as unknown as CliRenderer,
|
|
75
|
+
setInputText: () => {},
|
|
76
|
+
getInputText: () => "",
|
|
72
77
|
ei: {
|
|
73
78
|
personas: () => [],
|
|
74
|
-
|
|
79
|
+
activePersonaId: () => null,
|
|
75
80
|
messages: () => [],
|
|
76
|
-
queueStatus: () => ({ state: "idle" as const, pending_count: 0 }),
|
|
81
|
+
queueStatus: () => ({ state: "idle" as const, pending_count: 0, dlq_count: 0 }),
|
|
77
82
|
notification: () => null,
|
|
78
83
|
selectPersona: () => {},
|
|
79
84
|
sendMessage: async () => {},
|
|
@@ -83,12 +88,12 @@ describe("parseAndExecute", () => {
|
|
|
83
88
|
resumeQueue: async () => {},
|
|
84
89
|
stopProcessor: async () => {},
|
|
85
90
|
showNotification: () => {},
|
|
86
|
-
createPersona: async () =>
|
|
91
|
+
createPersona: async (_input: { name: string }) => "",
|
|
87
92
|
archivePersona: async () => {},
|
|
88
93
|
unarchivePersona: async () => {},
|
|
89
94
|
setContextBoundary: async () => {},
|
|
90
95
|
updatePersona: async () => {},
|
|
91
|
-
},
|
|
96
|
+
} as unknown as EiContextValue,
|
|
92
97
|
};
|
|
93
98
|
|
|
94
99
|
beforeEach(() => {
|