ei-tui 0.4.3 → 0.5.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 +14 -0
- package/package.json +1 -1
- package/src/cli/README.md +17 -12
- package/src/cli/commands/personas.ts +12 -0
- package/src/cli/mcp.ts +2 -2
- package/src/cli/retrieval.ts +86 -8
- package/src/cli.ts +8 -5
- package/src/core/constants/seed-traits.ts +29 -0
- package/src/core/context-utils.ts +1 -0
- package/src/core/handlers/human-matching.ts +53 -35
- package/src/core/handlers/index.ts +5 -0
- package/src/core/handlers/persona-preview.ts +7 -0
- package/src/core/handlers/persona-topics.ts +3 -2
- package/src/core/handlers/rooms.ts +176 -0
- package/src/core/handlers/utils.ts +55 -3
- package/src/core/heartbeat-manager.ts +3 -1
- package/src/core/llm-client.ts +1 -1
- package/src/core/message-manager.ts +10 -8
- package/src/core/orchestrators/human-extraction.ts +15 -2
- package/src/core/orchestrators/index.ts +1 -0
- package/src/core/orchestrators/persona-generation.ts +4 -0
- package/src/core/orchestrators/persona-topics.ts +2 -1
- package/src/core/orchestrators/room-extraction.ts +318 -0
- package/src/core/persona-manager.ts +16 -5
- package/src/core/personas/opencode-agent.ts +12 -2
- package/src/core/processor.ts +520 -4
- package/src/core/prompt-context-builder.ts +89 -5
- package/src/core/queue-processor.ts +68 -8
- package/src/core/room-manager.ts +408 -0
- package/src/core/state/index.ts +1 -0
- package/src/core/state/personas.ts +12 -2
- package/src/core/state/queue.ts +2 -2
- package/src/core/state/rooms.ts +182 -0
- package/src/core/state-manager.ts +124 -2
- package/src/core/tool-manager.ts +1 -1
- package/src/core/tools/index.ts +15 -0
- package/src/core/types/enums.ts +11 -0
- package/src/core/types/integrations.ts +10 -2
- package/src/core/types/llm.ts +3 -0
- package/src/core/types/rooms.ts +59 -0
- package/src/core/types.ts +1 -0
- package/src/core/utils/decay.ts +14 -8
- package/src/core/utils/exposure.ts +14 -0
- package/src/integrations/claude-code/importer.ts +23 -10
- package/src/integrations/cursor/importer.ts +22 -10
- package/src/integrations/opencode/importer.ts +30 -13
- package/src/prompts/ceremony/dedup.ts +2 -2
- package/src/prompts/generation/from-person.ts +85 -0
- package/src/prompts/generation/index.ts +2 -0
- package/src/prompts/generation/persona.ts +14 -10
- package/src/prompts/generation/seeds.ts +4 -29
- package/src/prompts/generation/types.ts +13 -0
- package/src/prompts/heartbeat/check.ts +1 -1
- package/src/prompts/heartbeat/ei.ts +4 -4
- package/src/prompts/heartbeat/types.ts +1 -0
- package/src/prompts/index.ts +15 -0
- package/src/prompts/message-utils.ts +2 -2
- package/src/prompts/persona/topics-match.ts +7 -6
- package/src/prompts/persona/topics-update.ts +8 -11
- package/src/prompts/persona/types.ts +2 -1
- package/src/prompts/response/index.ts +1 -1
- package/src/prompts/response/sections.ts +20 -8
- package/src/prompts/response/types.ts +6 -0
- package/src/prompts/room/index.ts +115 -0
- package/src/prompts/room/sections.ts +150 -0
- package/src/prompts/room/types.ts +93 -0
- package/tui/README.md +20 -0
- package/tui/src/app.tsx +3 -2
- package/tui/src/commands/activate.tsx +98 -0
- package/tui/src/commands/archive.tsx +54 -25
- package/tui/src/commands/capture.tsx +50 -0
- package/tui/src/commands/dedupe.tsx +2 -7
- package/tui/src/commands/delete.tsx +48 -0
- package/tui/src/commands/details.tsx +7 -0
- package/tui/src/commands/persona.tsx +271 -9
- package/tui/src/commands/room.tsx +261 -0
- package/tui/src/commands/silence.tsx +29 -0
- package/tui/src/components/ArchivedItemsOverlay.tsx +144 -0
- package/tui/src/components/ConfirmOverlay.tsx +6 -0
- package/tui/src/components/ConflictOverlay.tsx +6 -0
- package/tui/src/components/HelpOverlay.tsx +6 -1
- package/tui/src/components/LoadingOverlay.tsx +51 -0
- package/tui/src/components/MessageList.tsx +1 -18
- package/tui/src/components/PersonPickerOverlay.tsx +121 -0
- package/tui/src/components/PersonaListOverlay.tsx +6 -1
- package/tui/src/components/PromptInput.tsx +141 -8
- package/tui/src/components/ProviderListOverlay.tsx +5 -1
- package/tui/src/components/QuotesOverlay.tsx +5 -1
- package/tui/src/components/RoomMessageList.tsx +179 -0
- package/tui/src/components/Sidebar.tsx +54 -2
- package/tui/src/components/StatusBar.tsx +99 -8
- package/tui/src/components/ToolkitListOverlay.tsx +5 -1
- package/tui/src/components/WelcomeOverlay.tsx +6 -0
- package/tui/src/context/ei.tsx +252 -1
- package/tui/src/context/keyboard.tsx +48 -12
- package/tui/src/util/cyp-editor.tsx +152 -0
- package/tui/src/util/quote-utils.ts +19 -0
- package/tui/src/util/room-editor.tsx +164 -0
- package/tui/src/util/room-logic.ts +8 -0
- package/tui/src/util/room-parser.ts +70 -0
- package/tui/src/util/yaml-serializers.ts +151 -0
package/README.md
CHANGED
|
@@ -58,6 +58,20 @@ A "Persona" is the combination of these two pieces of data, plus some _personali
|
|
|
58
58
|
|
|
59
59
|
> <sup>1</sup>: By default. You can make them static.
|
|
60
60
|
|
|
61
|
+
## What's a Room?
|
|
62
|
+
|
|
63
|
+
Rooms let you throw multiple personas into the same conversation thread. Chaos ensues. Or collaboration. Sometimes both.
|
|
64
|
+
|
|
65
|
+
Three modes, set at creation:
|
|
66
|
+
|
|
67
|
+
**Free For All (FFA)**: Everyone talks. Every message gets a response from every persona. It's loud. Good for brainstorming or when you want a bunch of perspectives on the same thing.
|
|
68
|
+
|
|
69
|
+
**Choose Your Path (CYP)**: The conversation branches. Each message triggers responses from all personas, but you pick which one continues the thread. Fork in the road, every turn. You're the navigator.
|
|
70
|
+
|
|
71
|
+
**Messages Against Persona (MAP)**: The interesting one. Everyone submits a response, but a Judge persona picks which one actually shows up. The personas have to stay in character and compete for the Judge's approval. The human doesn't have to play by the rules. It's partly a game of "who knows this judge best?" and partly just fun to watch them try.
|
|
72
|
+
|
|
73
|
+
Rooms learn the same way persona conversations do. Quotes, topics, people — all get extracted and persisted. The knowledge base grows no matter which mode you're in.
|
|
74
|
+
|
|
61
75
|
## The Basics
|
|
62
76
|
|
|
63
77
|
Ei can operate with three types of input, and three types of output.
|
package/package.json
CHANGED
package/src/cli/README.md
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
ei # Start the TUI
|
|
6
6
|
ei "query string" # Return up to 10 results across all types
|
|
7
7
|
ei -n 5 "query string" # Return up to 5 results
|
|
8
|
-
ei facts -n 5 "query string"
|
|
9
|
-
ei people -n 5 "query string"
|
|
10
|
-
ei topics -n 5 "query string"
|
|
11
|
-
ei quotes -n 5 "query string"
|
|
8
|
+
ei facts -n 5 "query string" # Return up to 5 facts
|
|
9
|
+
ei people -n 5 "query string" # Return up to 5 people
|
|
10
|
+
ei topics -n 5 "query string" # Return up to 5 topics
|
|
11
|
+
ei quotes -n 5 "query string" # Return up to 5 quotes
|
|
12
|
+
ei personas -n 5 "query string" # Return up to 5 personas (name match)
|
|
12
13
|
ei --persona "Beta" "query string" # Filter results to what Beta has learned
|
|
13
14
|
ei --recent # Most recently mentioned items (no query needed)
|
|
14
15
|
ei --persona "Beta" --recent # Most recently mentioned items Beta has learned
|
|
@@ -18,7 +19,7 @@ ei --install # Register Ei with OpenCode, Claude Code, and Cur
|
|
|
18
19
|
ei mcp # Start the Ei MCP stdio server (for Cursor/Claude Desktop)
|
|
19
20
|
```
|
|
20
21
|
|
|
21
|
-
Type aliases: `fact`, `person`, `topic`, `quote` all work (singular or plural).
|
|
22
|
+
Type aliases: `fact`, `person`, `topic`, `quote`, `persona` all work (singular or plural).
|
|
22
23
|
|
|
23
24
|
# An Agentic Tool
|
|
24
25
|
|
|
@@ -70,9 +71,10 @@ ei "What are the user's current preferences, active projects, and workflow?"
|
|
|
70
71
|
\```
|
|
71
72
|
|
|
72
73
|
Ei is a persistent knowledge base built from the user's conversations — facts, preferences,
|
|
73
|
-
people, topics. Use it when the user references past work, mentions how they like things done,
|
|
74
|
-
or asks "how did we do X." Use `ei --persona "Beta" "walruses"` to scope results to what a specific
|
|
75
|
-
|
|
74
|
+
people, topics, personas. Use it when the user references past work, mentions how they like things done,
|
|
75
|
+
or asks "how did we do X." Use `ei --persona "Beta" "walruses"` to scope results to what a specific
|
|
76
|
+
persona has learned. Use `ei personas "name"` to find personas by name. Query again mid-session when
|
|
77
|
+
they correct you or reference something from a previous session.
|
|
76
78
|
```
|
|
77
79
|
|
|
78
80
|
### Claude Code
|
|
@@ -83,6 +85,7 @@ Add to `~/.claude/CLAUDE.md` (user-level) or `CLAUDE.md` at project root:
|
|
|
83
85
|
At session start, use the **ei** MCP to pull user context: call `ei_search` with a
|
|
84
86
|
natural-language query about the user's preferences, active projects, and workflow.
|
|
85
87
|
A `persona` filter is available to scope results to what a specific persona has learned.
|
|
88
|
+
Use `type: "personas"` to search for personas by name.
|
|
86
89
|
|
|
87
90
|
Use Ei when the user references past decisions, mentions people or preferences, or asks
|
|
88
91
|
"how did we do X." Query again when they correct you or reference something from a previous
|
|
@@ -101,7 +104,7 @@ alwaysApply: true
|
|
|
101
104
|
# Ei MCP — User knowledge base
|
|
102
105
|
|
|
103
106
|
The **ei** MCP (server `user-ei`) is a persistent knowledge base built from the user's
|
|
104
|
-
conversations (facts, people, topics, quotes).
|
|
107
|
+
conversations (facts, people, topics, quotes, personas).
|
|
105
108
|
|
|
106
109
|
**Use it when:**
|
|
107
110
|
- The user refers to past decisions, fixes, or "how we did X" and current chat/codebase
|
|
@@ -112,7 +115,7 @@ conversations (facts, people, topics, quotes).
|
|
|
112
115
|
than only code.
|
|
113
116
|
|
|
114
117
|
**How to use:**
|
|
115
|
-
1. Call `ei_search` (server `user-ei`) with a natural-language query (or omit query and use `recent: true` to browse); optionally filter by `type` (facts, people, topics, quotes) or `persona` display_name.
|
|
118
|
+
1. Call `ei_search` (server `user-ei`) with a natural-language query (or omit query and use `recent: true` to browse); optionally filter by `type` (facts, people, topics, quotes, personas) or `persona` display_name.
|
|
116
119
|
2. If you need full detail for a result, call `ei_lookup` with the entity `id` from step 1.
|
|
117
120
|
|
|
118
121
|
Prefer querying Ei before asking the user for context they may have already shared.
|
|
@@ -120,13 +123,13 @@ Prefer querying Ei before asking the user for context they may have already shar
|
|
|
120
123
|
|
|
121
124
|
## What the Tool Provides
|
|
122
125
|
|
|
123
|
-
The installed tool gives OpenCode agents access to all
|
|
126
|
+
The installed tool gives OpenCode agents access to all five data types with proper Zod-validated args:
|
|
124
127
|
|
|
125
128
|
| Arg | Type | Description |
|
|
126
129
|
|-----|------|-------------|
|
|
127
130
|
| `query` | string (optional) | Search text, or entity ID when `lookup=true`. Omit to browse by recency. |
|
|
128
131
|
| `persona` | string (optional) | Persona display_name to filter results — only returns entities that persona has extracted |
|
|
129
|
-
| `type` | enum (optional) | `facts` \| `people` \| `topics` \| `quotes` — omit for balanced results |
|
|
132
|
+
| `type` | enum (optional) | `facts` \| `people` \| `topics` \| `quotes` \| `personas` — omit for balanced results |
|
|
130
133
|
| `limit` | number (optional) | Max results, default 10 |
|
|
131
134
|
| `lookup` | boolean (optional) | If true, fetch single entity by ID |
|
|
132
135
|
| `recent` | boolean (optional) | If true, sort by most recently mentioned. Can be combined with `persona` or `query`. |
|
|
@@ -139,4 +142,6 @@ All search commands return arrays. Each result includes a `type` field.
|
|
|
139
142
|
|
|
140
143
|
**Quote**: `{ type, id, text, speaker, timestamp, linked_items[] }`
|
|
141
144
|
|
|
145
|
+
**Persona**: `{ type, id, display_name, short_description, model, base_prompt, traits[], topics[] }`
|
|
146
|
+
|
|
142
147
|
**ID lookup** (`lookup: true`): single object (not an array) with the same shape.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { loadLatestState, retrievePersonas } from "../retrieval";
|
|
2
|
+
import type { PersonaResult } from "../retrieval";
|
|
3
|
+
|
|
4
|
+
export async function execute(query: string, limit: number, options: { recent?: boolean } = {}): Promise<PersonaResult[]> {
|
|
5
|
+
const state = await loadLatestState();
|
|
6
|
+
if (!state) {
|
|
7
|
+
console.error("No saved state found. Is EI_DATA_PATH set correctly?");
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return retrievePersonas(query, state, limit, options);
|
|
12
|
+
}
|
package/src/cli/mcp.ts
CHANGED
|
@@ -20,10 +20,10 @@ export function createMcpServer(): McpServer {
|
|
|
20
20
|
inputSchema: {
|
|
21
21
|
query: z.string().optional().describe("Search text. Supports natural language. Omit to browse without semantic filtering — useful with recent=true or persona filter."),
|
|
22
22
|
type: z
|
|
23
|
-
.enum(["facts", "people", "topics", "quotes"])
|
|
23
|
+
.enum(["facts", "people", "topics", "quotes", "personas"])
|
|
24
24
|
.optional()
|
|
25
25
|
.describe(
|
|
26
|
-
"Filter to a specific data type. Omit to search all types (balanced across all
|
|
26
|
+
"Filter to a specific data type. Omit to search all types (balanced across all 5)."
|
|
27
27
|
),
|
|
28
28
|
persona: z
|
|
29
29
|
.string()
|
package/src/cli/retrieval.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { StorageState, Quote, Fact, Person, Topic } from "../core/types";
|
|
2
|
+
import type { PersonaEntity } from "../core/types/entities.js";
|
|
3
|
+
import type { PersonaTrait, PersonaTopic } from "../core/types/data-items.js";
|
|
2
4
|
import { decodeAllEmbeddings } from "../storage/embeddings";
|
|
3
5
|
import { crossFind } from "../core/utils/index.ts";
|
|
4
6
|
import { join } from "path";
|
|
@@ -110,19 +112,30 @@ export interface TopicResult {
|
|
|
110
112
|
sentiment: number;
|
|
111
113
|
}
|
|
112
114
|
|
|
115
|
+
export interface PersonaResult {
|
|
116
|
+
id: string;
|
|
117
|
+
display_name: string;
|
|
118
|
+
short_description?: string;
|
|
119
|
+
model?: string;
|
|
120
|
+
base_prompt: string;
|
|
121
|
+
traits: PersonaTrait[];
|
|
122
|
+
topics: PersonaTopic[];
|
|
123
|
+
}
|
|
124
|
+
|
|
113
125
|
export type BalancedResult =
|
|
114
126
|
| ({ type: "quote" } & QuoteResult)
|
|
115
127
|
| ({ type: "fact" } & FactResult)
|
|
116
128
|
| ({ type: "person" } & PersonResult)
|
|
117
|
-
| ({ type: "topic" } & TopicResult)
|
|
129
|
+
| ({ type: "topic" } & TopicResult)
|
|
130
|
+
| ({ type: "persona" } & PersonaResult);
|
|
118
131
|
|
|
119
|
-
const DATA_TYPES = ["quote", "fact", "person", "topic"] as const;
|
|
132
|
+
const DATA_TYPES = ["quote", "fact", "person", "topic", "persona"] as const;
|
|
120
133
|
type DataType = typeof DATA_TYPES[number];
|
|
121
134
|
|
|
122
135
|
interface ScoredEntry {
|
|
123
136
|
type: DataType;
|
|
124
137
|
similarity: number;
|
|
125
|
-
mapped: QuoteResult | FactResult | PersonResult | TopicResult;
|
|
138
|
+
mapped: QuoteResult | FactResult | PersonResult | TopicResult | PersonaResult;
|
|
126
139
|
itemId: string;
|
|
127
140
|
}
|
|
128
141
|
|
|
@@ -182,6 +195,46 @@ function mapTopic(topic: Topic): TopicResult {
|
|
|
182
195
|
};
|
|
183
196
|
}
|
|
184
197
|
|
|
198
|
+
export function mapPersona(persona: PersonaEntity): PersonaResult {
|
|
199
|
+
return {
|
|
200
|
+
id: persona.id,
|
|
201
|
+
display_name: persona.display_name,
|
|
202
|
+
short_description: persona.short_description,
|
|
203
|
+
model: persona.model,
|
|
204
|
+
base_prompt: persona.long_description ?? "",
|
|
205
|
+
traits: persona.traits,
|
|
206
|
+
topics: persona.topics,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function retrievePersonas(
|
|
211
|
+
query: string,
|
|
212
|
+
state: StorageState,
|
|
213
|
+
limit: number = 10,
|
|
214
|
+
options: { recent?: boolean } = {}
|
|
215
|
+
): PersonaResult[] {
|
|
216
|
+
const { recent } = options;
|
|
217
|
+
const personaList = Object.values(state.personas).map((p) => p.entity);
|
|
218
|
+
|
|
219
|
+
if (recent && !query) {
|
|
220
|
+
return personaList
|
|
221
|
+
.sort((a, b) => b.last_updated.localeCompare(a.last_updated))
|
|
222
|
+
.slice(0, limit)
|
|
223
|
+
.map(mapPersona);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (!query) {
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const q = query.toLowerCase();
|
|
231
|
+
return personaList
|
|
232
|
+
.filter((p) => p.display_name.toLowerCase().includes(q))
|
|
233
|
+
.sort((a, b) => b.last_updated.localeCompare(a.last_updated))
|
|
234
|
+
.slice(0, limit)
|
|
235
|
+
.map(mapPersona);
|
|
236
|
+
}
|
|
237
|
+
|
|
185
238
|
export async function retrieveBalanced(
|
|
186
239
|
query: string,
|
|
187
240
|
limit: number = 10,
|
|
@@ -199,11 +252,16 @@ export async function retrieveBalanced(
|
|
|
199
252
|
const recentDate = (item: AnyItem): string => item.last_mentioned ?? item.last_updated ?? "";
|
|
200
253
|
|
|
201
254
|
if (recent && !query) {
|
|
202
|
-
const allItems: Array<{ type: DataType; item: AnyItem; mapped: QuoteResult | FactResult | PersonResult | TopicResult }> = [
|
|
255
|
+
const allItems: Array<{ type: DataType; item: AnyItem; mapped: QuoteResult | FactResult | PersonResult | TopicResult | PersonaResult }> = [
|
|
203
256
|
...state.human.quotes.map(q => ({ type: "quote" as DataType, item: q as AnyItem, mapped: mapQuote(q, state) })),
|
|
204
257
|
...state.human.facts.map(f => ({ type: "fact" as DataType, item: f as AnyItem, mapped: mapFact(f) })),
|
|
205
258
|
...state.human.people.map(p => ({ type: "person" as DataType, item: p as AnyItem, mapped: mapPerson(p) })),
|
|
206
259
|
...state.human.topics.map(t => ({ type: "topic" as DataType, item: t as AnyItem, mapped: mapTopic(t) })),
|
|
260
|
+
...Object.values(state.personas).map(({ entity: p }) => ({
|
|
261
|
+
type: "persona" as DataType,
|
|
262
|
+
item: { id: p.id, last_updated: p.last_updated } as AnyItem,
|
|
263
|
+
mapped: mapPersona(p),
|
|
264
|
+
})),
|
|
207
265
|
];
|
|
208
266
|
return allItems
|
|
209
267
|
.sort((a, b) => recentDate(b.item).localeCompare(recentDate(a.item)))
|
|
@@ -237,10 +295,19 @@ export async function retrieveBalanced(
|
|
|
237
295
|
}
|
|
238
296
|
}
|
|
239
297
|
}
|
|
240
|
-
|
|
298
|
+
const embeddingResults = allScored
|
|
241
299
|
.sort((a, b) => recentDate(b.mapped as AnyItem).localeCompare(recentDate(a.mapped as AnyItem)))
|
|
242
300
|
.slice(0, limit)
|
|
243
301
|
.map(({ type, mapped }) => ({ type, ...mapped }) as BalancedResult);
|
|
302
|
+
const personaMatches = retrievePersonas(query, state, limit, { recent: true });
|
|
303
|
+
if (personaMatches.length > 0) {
|
|
304
|
+
const combined = [
|
|
305
|
+
...personaMatches.map((p) => ({ type: "persona" as const, ...p }) as BalancedResult),
|
|
306
|
+
...embeddingResults,
|
|
307
|
+
];
|
|
308
|
+
return combined.slice(0, limit);
|
|
309
|
+
}
|
|
310
|
+
return embeddingResults;
|
|
244
311
|
}
|
|
245
312
|
|
|
246
313
|
for (const { type, items, mapper } of typeConfigs) {
|
|
@@ -278,7 +345,16 @@ export async function retrieveBalanced(
|
|
|
278
345
|
|
|
279
346
|
result.sort((a, b) => b.similarity - a.similarity);
|
|
280
347
|
|
|
281
|
-
|
|
348
|
+
const embeddingFinal = result.map(({ type, mapped }) => ({ type, ...mapped }) as BalancedResult);
|
|
349
|
+
const personaFinal = retrievePersonas(query, state, limit);
|
|
350
|
+
if (personaFinal.length > 0) {
|
|
351
|
+
const combined = [
|
|
352
|
+
...personaFinal.map((p) => ({ type: "persona" as const, ...p }) as BalancedResult),
|
|
353
|
+
...embeddingFinal,
|
|
354
|
+
];
|
|
355
|
+
return combined.slice(0, limit);
|
|
356
|
+
}
|
|
357
|
+
return embeddingFinal;
|
|
282
358
|
}
|
|
283
359
|
|
|
284
360
|
export async function lookupById(id: string): Promise<({ type: string } & Record<string, unknown>) | null> {
|
|
@@ -289,6 +365,8 @@ export async function lookupById(id: string): Promise<({ type: string } & Record
|
|
|
289
365
|
|
|
290
366
|
const found = crossFind(id, state.human);
|
|
291
367
|
if (!found) return null;
|
|
292
|
-
const { type,
|
|
293
|
-
|
|
368
|
+
const { type, ...rest } = found;
|
|
369
|
+
const withoutEmbedding = { ...rest } as Record<string, unknown>;
|
|
370
|
+
delete withoutEmbedding.embedding;
|
|
371
|
+
return { type, ...withoutEmbedding };
|
|
294
372
|
}
|
package/src/cli.ts
CHANGED
|
@@ -26,6 +26,8 @@ const TYPE_ALIASES: Record<string, string> = {
|
|
|
26
26
|
people: "people",
|
|
27
27
|
topic: "topics",
|
|
28
28
|
topics: "topics",
|
|
29
|
+
persona: "personas",
|
|
30
|
+
personas: "personas",
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
function printHelp(): void {
|
|
@@ -47,10 +49,11 @@ Usage:
|
|
|
47
49
|
ei mcp Start the Ei MCP stdio server (for Cursor/Claude Desktop)
|
|
48
50
|
|
|
49
51
|
Types:
|
|
50
|
-
quote / quotes
|
|
51
|
-
fact / facts
|
|
52
|
-
person / people
|
|
53
|
-
topic / topics
|
|
52
|
+
quote / quotes Quotes from conversation history
|
|
53
|
+
fact / facts Facts about the user
|
|
54
|
+
person / people People from the user's life
|
|
55
|
+
topic / topics Topics of interest
|
|
56
|
+
persona / personas Personas in this Ei instance
|
|
54
57
|
|
|
55
58
|
Options:
|
|
56
59
|
--number, -n Maximum number of results (default: 10)
|
|
@@ -88,7 +91,7 @@ function buildOpenCodeToolContent(): string {
|
|
|
88
91
|
' "Search text, or an entity ID when lookup=true. Supports natural language. Omit to browse by recency."',
|
|
89
92
|
' ),',
|
|
90
93
|
' type: tool.schema',
|
|
91
|
-
' .enum(["facts", "people", "topics", "quotes"])',
|
|
94
|
+
' .enum(["facts", "people", "topics", "quotes", "personas"])',
|
|
92
95
|
' .optional()',
|
|
93
96
|
' .describe(',
|
|
94
97
|
' "Filter to a specific data type. Omit to search all types (balanced across all 4).",',
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export interface SeedTrait {
|
|
2
|
+
name: string;
|
|
3
|
+
description: string;
|
|
4
|
+
sentiment: number;
|
|
5
|
+
strength: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const SEED_TRAIT_GENUINE: SeedTrait = {
|
|
9
|
+
name: "Genuine Responses",
|
|
10
|
+
description: "Respond authentically rather than with empty validation. Disagree when appropriate. Skip phrases like 'Great question!' or 'Absolutely!' - just respond to the substance.",
|
|
11
|
+
sentiment: 0.5,
|
|
12
|
+
strength: 0.7,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const SEED_TRAIT_NATURAL_SPEECH: SeedTrait = {
|
|
16
|
+
name: "Natural Speech",
|
|
17
|
+
description: `Write in natural conversational flow. Avoid AI-typical patterns like:
|
|
18
|
+
- Choppy dramatic fragments ('Bold move. Risky play.')
|
|
19
|
+
- Rhetorical 'That X? Y.' structures
|
|
20
|
+
- 'That's not just... That's ...'
|
|
21
|
+
- formulaic paragraph openers`,
|
|
22
|
+
sentiment: 0.5,
|
|
23
|
+
strength: 0.7,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const DEFAULT_SEED_TRAITS: SeedTrait[] = [
|
|
27
|
+
SEED_TRAIT_GENUINE,
|
|
28
|
+
SEED_TRAIT_NATURAL_SPEECH,
|
|
29
|
+
];
|
|
@@ -17,6 +17,7 @@ export function filterMessagesForContext(
|
|
|
17
17
|
const boundaryMs = contextBoundary ? new Date(contextBoundary).getTime() : 0;
|
|
18
18
|
|
|
19
19
|
return messages.filter((msg) => {
|
|
20
|
+
if (msg.external === true) return false;
|
|
20
21
|
if (msg.context_status === ContextStatusEnum.Always) return true;
|
|
21
22
|
if (msg.context_status === ContextStatusEnum.Never) return false;
|
|
22
23
|
|
|
@@ -9,15 +9,10 @@ import type { StateManager } from "../state-manager.js";
|
|
|
9
9
|
import type { ItemMatchResult, ExposureImpact, TopicUpdateResult, PersonUpdateResult } from "../../prompts/human/types.js";
|
|
10
10
|
import { queueTopicUpdate, queuePersonUpdate, type ExtractionContext } from "../orchestrators/index.js";
|
|
11
11
|
import { getEmbeddingService, getTopicEmbeddingText, getPersonEmbeddingText } from "../embedding-service.js";
|
|
12
|
+
import { calculateExposureCurrent } from "../utils/exposure.js";
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (isNewItem) return [personaGroup];
|
|
16
|
-
const groups = new Set(existing ?? []);
|
|
17
|
-
groups.add(personaGroup);
|
|
18
|
-
return Array.from(groups);
|
|
19
|
-
}
|
|
20
|
-
import { resolveMessageWindow, getMessageText } from "./utils.js";
|
|
14
|
+
|
|
15
|
+
import { resolveMessageWindow, getMessageText, normalizeRoomMessages } from "./utils.js";
|
|
21
16
|
|
|
22
17
|
export function handleTopicMatch(response: LLMResponse, state: StateManager): void {
|
|
23
18
|
const result = response.parsed as ItemMatchResult | undefined;
|
|
@@ -28,6 +23,7 @@ export function handleTopicMatch(response: LLMResponse, state: StateManager): vo
|
|
|
28
23
|
|
|
29
24
|
const personaId = response.request.data.personaId as string;
|
|
30
25
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
26
|
+
const roomId = response.request.data.roomId as string | undefined;
|
|
31
27
|
const { messages_context, messages_analyze } = resolveMessageWindow(response, state);
|
|
32
28
|
|
|
33
29
|
let matched_guid = result.matched_guid;
|
|
@@ -51,6 +47,7 @@ export function handleTopicMatch(response: LLMResponse, state: StateManager): vo
|
|
|
51
47
|
} = {
|
|
52
48
|
personaId,
|
|
53
49
|
personaDisplayName,
|
|
50
|
+
roomId,
|
|
54
51
|
messages_context,
|
|
55
52
|
messages_analyze,
|
|
56
53
|
candidateName: response.request.data.candidateName as string,
|
|
@@ -73,6 +70,7 @@ export function handlePersonMatch(response: LLMResponse, state: StateManager): v
|
|
|
73
70
|
|
|
74
71
|
const personaId = response.request.data.personaId as string;
|
|
75
72
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
73
|
+
const roomId = response.request.data.roomId as string | undefined;
|
|
76
74
|
const { messages_context, messages_analyze } = resolveMessageWindow(response, state);
|
|
77
75
|
|
|
78
76
|
let matched_guid = result.matched_guid;
|
|
@@ -96,6 +94,7 @@ export function handlePersonMatch(response: LLMResponse, state: StateManager): v
|
|
|
96
94
|
} = {
|
|
97
95
|
personaId,
|
|
98
96
|
personaDisplayName,
|
|
97
|
+
roomId,
|
|
99
98
|
messages_context,
|
|
100
99
|
messages_analyze,
|
|
101
100
|
candidateName: response.request.data.candidateName as string,
|
|
@@ -121,6 +120,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
121
120
|
const existingItemId = response.request.data.existingItemId as string | undefined;
|
|
122
121
|
const personaId = response.request.data.personaId as string;
|
|
123
122
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
123
|
+
const roomId = response.request.data.roomId as string | undefined;
|
|
124
124
|
const candidateCategory = response.request.data.candidateCategory as string | undefined;
|
|
125
125
|
|
|
126
126
|
if (!result.name || !result.description || result.sentiment === undefined) {
|
|
@@ -128,6 +128,9 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
const personaIds = personaId.split("|").filter(Boolean);
|
|
132
|
+
const primaryId = personaIds[0] ?? personaId;
|
|
133
|
+
|
|
131
134
|
const now = new Date().toISOString();
|
|
132
135
|
const human = state.getHuman();
|
|
133
136
|
|
|
@@ -137,8 +140,11 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
137
140
|
};
|
|
138
141
|
const itemId = resolveItemId();
|
|
139
142
|
|
|
140
|
-
const persona = state.persona_getById(
|
|
143
|
+
const persona = state.persona_getById(primaryId);
|
|
141
144
|
const personaGroup = persona?.group_primary ?? null;
|
|
145
|
+
const allPersonaGroups = personaIds
|
|
146
|
+
.map(id => state.persona_getById(id)?.group_primary)
|
|
147
|
+
.filter((g): g is string => g != null);
|
|
142
148
|
|
|
143
149
|
const existingTopic = isNewItem ? undefined : human.topics.find(t => t.id === existingItemId);
|
|
144
150
|
|
|
@@ -153,27 +159,34 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
|
|
|
153
159
|
}
|
|
154
160
|
|
|
155
161
|
const exposureImpact = result.exposure_impact as ExposureImpact | undefined;
|
|
162
|
+
const interestedPersonas = isNewItem
|
|
163
|
+
? personaIds
|
|
164
|
+
: [...new Set([...(existingTopic?.interested_personas ?? []), ...personaIds])];
|
|
165
|
+
const personaGroupsMerged = isNewItem
|
|
166
|
+
? (allPersonaGroups.length > 0 ? allPersonaGroups : existingTopic?.persona_groups)
|
|
167
|
+
: [...new Set([...(existingTopic?.persona_groups ?? []), ...allPersonaGroups])];
|
|
168
|
+
|
|
156
169
|
const topic: Topic = {
|
|
157
170
|
id: itemId,
|
|
158
171
|
name: result.name,
|
|
159
172
|
description: result.description,
|
|
160
173
|
sentiment: result.sentiment,
|
|
161
174
|
category: result.category ?? candidateCategory ?? existingTopic?.category,
|
|
162
|
-
exposure_current: calculateExposureCurrent(exposureImpact),
|
|
175
|
+
exposure_current: calculateExposureCurrent(exposureImpact, existingTopic?.exposure_current ?? 0),
|
|
163
176
|
exposure_desired: result.exposure_desired ?? 0.5,
|
|
164
177
|
last_updated: now,
|
|
165
178
|
last_mentioned: now,
|
|
166
|
-
learned_by: isNewItem ?
|
|
167
|
-
last_changed_by:
|
|
168
|
-
interested_personas:
|
|
169
|
-
|
|
170
|
-
: [...new Set([...(existingTopic?.interested_personas ?? []), personaId])],
|
|
171
|
-
persona_groups: mergeGroups(personaGroup, isNewItem, existingTopic?.persona_groups),
|
|
179
|
+
learned_by: isNewItem ? primaryId : existingTopic?.learned_by,
|
|
180
|
+
last_changed_by: primaryId,
|
|
181
|
+
interested_personas: interestedPersonas,
|
|
182
|
+
persona_groups: personaGroupsMerged,
|
|
172
183
|
embedding,
|
|
173
184
|
};
|
|
174
185
|
state.human_topic_upsert(topic);
|
|
175
186
|
|
|
176
|
-
const allMessages =
|
|
187
|
+
const allMessages = roomId
|
|
188
|
+
? normalizeRoomMessages(state.getRoomMessages(roomId), state)
|
|
189
|
+
: state.messages_get(personaId);
|
|
177
190
|
await validateAndStoreQuotes(result.quotes, allMessages, itemId, personaDisplayName, personaGroup, state);
|
|
178
191
|
|
|
179
192
|
console.log(`[handleTopicUpdate] ${isNewItem ? "Created" : "Updated"} topic "${result.name}"`);
|
|
@@ -191,6 +204,7 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
191
204
|
const existingItemId = response.request.data.existingItemId as string | undefined;
|
|
192
205
|
const personaId = response.request.data.personaId as string;
|
|
193
206
|
const personaDisplayName = response.request.data.personaDisplayName as string;
|
|
207
|
+
const roomId = response.request.data.roomId as string | undefined;
|
|
194
208
|
const candidateRelationship = response.request.data.candidateRelationship as string | undefined;
|
|
195
209
|
|
|
196
210
|
if (!result.name || !result.description || result.sentiment === undefined) {
|
|
@@ -198,6 +212,9 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
198
212
|
return;
|
|
199
213
|
}
|
|
200
214
|
|
|
215
|
+
const personaIds = personaId.split("|").filter(Boolean);
|
|
216
|
+
const primaryId = personaIds[0] ?? personaId;
|
|
217
|
+
|
|
201
218
|
const now = new Date().toISOString();
|
|
202
219
|
const human = state.getHuman();
|
|
203
220
|
|
|
@@ -207,8 +224,11 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
207
224
|
};
|
|
208
225
|
const itemId = resolveItemId();
|
|
209
226
|
|
|
210
|
-
const persona = state.persona_getById(
|
|
227
|
+
const persona = state.persona_getById(primaryId);
|
|
211
228
|
const personaGroup = persona?.group_primary ?? null;
|
|
229
|
+
const allPersonaGroups = personaIds
|
|
230
|
+
.map(id => state.persona_getById(id)?.group_primary)
|
|
231
|
+
.filter((g): g is string => g != null);
|
|
212
232
|
|
|
213
233
|
const existingPerson = isNewItem ? undefined : human.people.find(p => p.id === existingItemId);
|
|
214
234
|
|
|
@@ -223,27 +243,34 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
|
|
|
223
243
|
}
|
|
224
244
|
|
|
225
245
|
const exposureImpact = result.exposure_impact as ExposureImpact | undefined;
|
|
246
|
+
const interestedPersonas = isNewItem
|
|
247
|
+
? personaIds
|
|
248
|
+
: [...new Set([...(existingPerson?.interested_personas ?? []), ...personaIds])];
|
|
249
|
+
const personaGroupsMerged = isNewItem
|
|
250
|
+
? (allPersonaGroups.length > 0 ? allPersonaGroups : existingPerson?.persona_groups)
|
|
251
|
+
: [...new Set([...(existingPerson?.persona_groups ?? []), ...allPersonaGroups])];
|
|
252
|
+
|
|
226
253
|
const person: Person = {
|
|
227
254
|
id: itemId,
|
|
228
255
|
name: result.name,
|
|
229
256
|
description: result.description,
|
|
230
257
|
sentiment: result.sentiment,
|
|
231
258
|
relationship: result.relationship ?? candidateRelationship ?? existingPerson?.relationship ?? "Unknown",
|
|
232
|
-
exposure_current: calculateExposureCurrent(exposureImpact),
|
|
259
|
+
exposure_current: calculateExposureCurrent(exposureImpact, existingPerson?.exposure_current ?? 0),
|
|
233
260
|
exposure_desired: result.exposure_desired ?? 0.5,
|
|
234
261
|
last_updated: now,
|
|
235
262
|
last_mentioned: now,
|
|
236
|
-
learned_by: isNewItem ?
|
|
237
|
-
last_changed_by:
|
|
238
|
-
interested_personas:
|
|
239
|
-
|
|
240
|
-
: [...new Set([...(existingPerson?.interested_personas ?? []), personaId])],
|
|
241
|
-
persona_groups: mergeGroups(personaGroup, isNewItem, existingPerson?.persona_groups),
|
|
263
|
+
learned_by: isNewItem ? primaryId : existingPerson?.learned_by,
|
|
264
|
+
last_changed_by: primaryId,
|
|
265
|
+
interested_personas: interestedPersonas,
|
|
266
|
+
persona_groups: personaGroupsMerged,
|
|
242
267
|
embedding,
|
|
243
268
|
};
|
|
244
269
|
state.human_person_upsert(person);
|
|
245
270
|
|
|
246
|
-
const allMessages =
|
|
271
|
+
const allMessages = roomId
|
|
272
|
+
? normalizeRoomMessages(state.getRoomMessages(roomId), state)
|
|
273
|
+
: state.messages_get(personaId);
|
|
247
274
|
await validateAndStoreQuotes(result.quotes, allMessages, itemId, personaDisplayName, personaGroup, state);
|
|
248
275
|
|
|
249
276
|
console.log(`[handlePersonUpdate] ${isNewItem ? "Created" : "Updated"} person "${result.name}"`);
|
|
@@ -436,14 +463,5 @@ async function validateAndStoreQuotes(
|
|
|
436
463
|
}
|
|
437
464
|
}
|
|
438
465
|
|
|
439
|
-
function calculateExposureCurrent(impact: ExposureImpact | undefined): number {
|
|
440
|
-
switch (impact) {
|
|
441
|
-
case "high": return 0.9;
|
|
442
|
-
case "medium": return 0.6;
|
|
443
|
-
case "low": return 0.3;
|
|
444
|
-
case "none": return 0.1;
|
|
445
|
-
default: return 0.5;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
466
|
|
|
449
467
|
|
|
@@ -18,6 +18,8 @@ import { handleFactFind, handleHumanTopicScan, handleHumanPersonScan, handleEven
|
|
|
18
18
|
import { handleTopicMatch, handleTopicUpdate, handlePersonMatch, handlePersonUpdate } from "./human-matching.js";
|
|
19
19
|
import { handleRewriteScan, handleRewriteRewrite } from "./rewrite.js";
|
|
20
20
|
import { handleDedupCurate } from "./dedup.js";
|
|
21
|
+
import { handleRoomResponse, handleRoomJudge } from "./rooms.js";
|
|
22
|
+
import { handlePersonaPreview } from "./persona-preview.js";
|
|
21
23
|
|
|
22
24
|
export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
23
25
|
handlePersonaResponse,
|
|
@@ -45,4 +47,7 @@ export const handlers: Record<LLMNextStep, ResponseHandler> = {
|
|
|
45
47
|
handleRewriteRewrite,
|
|
46
48
|
handleDedupCurate,
|
|
47
49
|
handleEventScan,
|
|
50
|
+
handleRoomResponse,
|
|
51
|
+
handleRoomJudge,
|
|
52
|
+
handlePersonaPreview,
|
|
48
53
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LLMResponse } from "../types.js";
|
|
2
|
+
import type { StateManager } from "../state-manager.js";
|
|
3
|
+
|
|
4
|
+
export function handlePersonaPreview(_response: LLMResponse, _state: StateManager): void {
|
|
5
|
+
// Intentionally empty — state writes are not needed for preview generation.
|
|
6
|
+
// The Processor post-dispatch block handles: completeness validation, re-queue, and Promise resolution.
|
|
7
|
+
}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../orchestrators/index.js";
|
|
23
23
|
import { buildPersonaDescriptionsPrompt } from "../../prompts/generation/index.js";
|
|
24
24
|
import { splitMessagesByTimestamp } from "./utils.js";
|
|
25
|
+
import { calculateExposureCurrent } from "../utils/exposure.js";
|
|
25
26
|
|
|
26
27
|
export const MIN_MESSAGE_COUNT_FOR_CREATE = 2;
|
|
27
28
|
|
|
@@ -272,7 +273,7 @@ export function handlePersonaTopicUpdate(response: LLMResponse, state: StateMana
|
|
|
272
273
|
approach: result.approach || "",
|
|
273
274
|
personal_stake: result.personal_stake || "",
|
|
274
275
|
sentiment: result.sentiment,
|
|
275
|
-
exposure_current: result.
|
|
276
|
+
exposure_current: calculateExposureCurrent(result.exposure_impact, 0),
|
|
276
277
|
exposure_desired: result.exposure_desired,
|
|
277
278
|
last_updated: now,
|
|
278
279
|
};
|
|
@@ -284,7 +285,7 @@ export function handlePersonaTopicUpdate(response: LLMResponse, state: StateMana
|
|
|
284
285
|
const updatedTopics = persona.topics.map((t: PersonaTopic) => {
|
|
285
286
|
if (t.id !== existingTopicId) return t;
|
|
286
287
|
|
|
287
|
-
const newExposure = Math.
|
|
288
|
+
const newExposure = Math.max(calculateExposureCurrent(result.exposure_impact, t.exposure_current), t.exposure_current);
|
|
288
289
|
|
|
289
290
|
return {
|
|
290
291
|
...t,
|