ei-tui 1.4.0 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli/README.md CHANGED
@@ -37,7 +37,7 @@ ei --id "claudecode:my-machine:session-uuid:message-uuid"
37
37
  ei --id "cursor:my-machine:composer-uuid:bubble-uuid"
38
38
  ```
39
39
 
40
- Quotes surfaced by `ei_search` or `ei_find_memory` include a `message_id` field in this format — pipe it to `ei --id` to read the original conversation.
40
+ Quotes surfaced by `ei_search` include a `message_id` field in this format — pipe it to `ei --id` to read the original conversation.
41
41
 
42
42
  # OpenCode Integration
43
43
 
@@ -47,12 +47,15 @@ Quotes surfaced by `ei_search` or `ei_find_memory` include a `message_id` field
47
47
  ei --install
48
48
  ```
49
49
 
50
- This registers Ei with Claude Code and Cursor via MCP:
50
+ This registers Ei with Claude Code, Cursor, and OpenCode MCP server config **and** context injection hooks so agents get Ei memory automatically without needing to call a tool:
51
51
 
52
- - **Claude Code**: writes `~/.claude.json` with an MCP server entry
53
- - **Cursor**: writes `~/.cursor/mcp.json` with an MCP server entry
52
+ | Tool | MCP | Context Hook |
53
+ |------|-----|-------------|
54
+ | **Claude Code** | `~/.claude.json` | `~/.claude/settings.json` (`UserPromptSubmit`) + `~/.claude/hooks/ei-inject.ts` |
55
+ | **Cursor** | `~/.cursor/mcp.json` | `~/.cursor/hooks.json` (`beforeSubmitPrompt`) + `~/.cursor/hooks/ei-inject.sh` |
56
+ | **OpenCode** | manual (see below) | Detected automatically via Oh My OpenCode compatibility layer (reads `~/.claude/settings.json`) |
54
57
 
55
- **OpenCode**: add the MCP server manually to `~/.config/opencode/opencode.jsonc`:
58
+ **OpenCode MCP**: add manually to `~/.config/opencode/opencode.jsonc`:
56
59
 
57
60
  ```json
58
61
  {
@@ -81,28 +84,20 @@ ei mcp
81
84
 
82
85
  ## Activating Ei in Your Agent
83
86
 
84
- `ei --install` handles the technical wiring. This step tells your agent *when* and *how* to reach for it.
87
+ `ei --install` handles both the technical wiring **and** context injection. After running it, your agent will automatically receive recent Ei memory before every message — no tool calls required.
85
88
 
86
- Without this, your agent has Ei available but may never call it. Add a snippet to your tool's config and it'll start querying Ei at the start of each session — and whenever you reference past context.
89
+ The snippets below are optional manual overrides if you want to customize the behavior or add targeted mid-session queries.
87
90
 
88
91
  ### OpenCode
89
92
 
90
- Add to `~/.config/opencode/AGENTS.md` (applies to all projects):
93
+ If you're using [Oh My OpenCode](https://github.com/code-yeongyu/oh-my-opencode), the `UserPromptSubmit` hook installed by `ei --install` is picked up automatically via its Claude Code compatibility layer — no additional config needed.
94
+
95
+ If you're running vanilla OpenCode without Oh My OpenCode, add to `~/.config/opencode/AGENTS.md`:
91
96
 
92
97
  ```markdown
93
- At session start, query Ei for user context:
94
-
95
- \```bash
96
- ei "What are the user's current preferences, active projects, and workflow?"
97
- \```
98
-
99
- Ei is a persistent knowledge base built from the user's conversations — facts, preferences,
100
- people, topics, personas. Use it when the user references past work, mentions how they like things done,
101
- asks "how did we do X," or needs to look up a person by any name, handle, or account (GitHub username,
102
- Discord handle, email, nickname, etc.) — people results include an `identifiers` array covering all
103
- known accounts and aliases for that person. Use `ei --persona "Beta" "walruses"` to scope results to
104
- what a specific persona has learned. Use `ei personas "name"` to find personas by name. Query again
105
- mid-session when they correct you or reference something from a previous session.
98
+ Use the **ei** MCP to pull user context when the user references past work, mentions people
99
+ or preferences, or asks "how did we do X." Call `ei_search` with a natural-language query.
100
+ Use `ei --persona "Beta" "topic"` to scope results to what a specific persona has learned.
106
101
  ```
107
102
 
108
103
  ### Claude Code
@@ -149,9 +144,8 @@ conversations (facts, people, topics, quotes, personas).
149
144
 
150
145
  **How to use:**
151
146
  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.
152
- 2. If you need full detail for a human entity (fact, topic, person, quote), call `ei_fetch_memory` with the entity `id`.
153
- 3. If you need full detail for a result including personas, call `ei_lookup` with the entity `id` from step 1.
154
- 4. To fetch a specific message with surrounding context, call `ei_fetch_message` with the message `id` and optional `before`/`after` counts.
147
+ 2. If you need the full record for any result, call `ei_lookup` with the entity `id` from step 1 — works for all types including personas.
148
+ 3. If a quote result has a `message_id`, call `ei_fetch_message` with that ID and optional `before`/`after` counts to read the original conversation with context.
155
149
 
156
150
  Prefer querying Ei before asking the user for context they may have already shared.
157
151
  ```
@@ -162,20 +156,18 @@ The MCP server exposes these tools to Claude Code, Cursor, and OpenCode:
162
156
 
163
157
  | Tool | Description |
164
158
  |------|-------------|
165
- | `ei_search` | Balanced search across all five data types (facts, topics, people, quotes, personas). Supports `type`, `persona`, `source`, `recent`, `limit` filters. |
166
- | `ei_lookup` | Full-record lookup for any entity by ID (facts, topics, people, quotes, personas). |
167
- | `ei_find_memory` | Grouped human-data search facts, topics, people, quotes. Returns results grouped by type. Mirrors the persona `find_memory` tool interface. |
168
- | `ei_fetch_memory` | Full-record lookup for a human entity (Fact, Topic, Person, or Quote) by ID. Returns the complete record including all fields. |
169
- | `ei_fetch_message` | Retrieve a specific message by fully-qualified ID with optional `before`/`after` context window. Routes to the correct source: `ei:uuid` searches Ei state, `opencode:machine:session:id` queries the OpenCode SQLite DB, `claudecode:...` scans Claude Code JSONL files, `cursor:...` reads the Cursor global DB. Returns message content, surrounding context, and session metadata. |
159
+ | `ei_search` | Search across all five data types (facts, topics, people, quotes, personas). Supports `type`, `persona`, `source`, `recent`, `limit` filters. Start here. |
160
+ | `ei_lookup` | Full-record lookup for any entity by ID facts, topics, people, quotes, or personas. Use when you need complete details beyond the search summary. |
161
+ | `ei_fetch_message` | Retrieve a specific message by fully-qualified ID with optional `before`/`after` context window. Use when a quote result has a `message_id` and you want the original conversation. Routes to the correct source automatically. |
170
162
 
171
- ### `ei_search` / `ei_find_memory` arguments
163
+ ### `ei_search` arguments
172
164
 
173
165
  | Arg | Type | Description |
174
166
  |-----|------|-------------|
175
167
  | `query` | string (optional) | Search text. Omit to browse by recency. |
168
+ | `type` | enum (optional) | `facts` \| `people` \| `topics` \| `quotes` \| `personas` — omit for balanced results across all types |
176
169
  | `persona` | string (optional) | Persona display_name to scope results to what that persona has learned |
177
- | `type` | enum (optional, `ei_search` only) | `facts` \| `people` \| `topics` \| `quotes` \| `personas` — omit for balanced results |
178
- | `types` | array (optional, `ei_find_memory` only) | `["facts", "topics", "people", "quotes"]` — omit for all human types |
170
+ | `source` | string (optional) | Prefix match against source identifiers (e.g. `opencode`, `cursor:my-machine`) |
179
171
  | `limit` | number (optional) | Max results, default 10 |
180
172
  | `recent` | boolean (optional) | Sort by most recently mentioned instead of relevance |
181
173
 
@@ -187,7 +179,7 @@ All search commands return arrays. Each result includes a `type` field.
187
179
 
188
180
  **Person**: `{ type, id, name, description, relationship, sentiment, identifiers[] }` — `identifiers` contains all known accounts and aliases (e.g. `{ type: "GitHub", value: "flare576" }`)
189
181
 
190
- **Quote**: `{ type, id, text, speaker, timestamp, linked_items[] }`
182
+ **Quote**: `{ type, text, speaker, message_id, timestamp, linked_items[] }` — note: `id` is intentionally omitted; use `message_id` with `ei_fetch_message` to retrieve the original conversation
191
183
 
192
184
  **Persona**: `{ type, id, display_name, short_description, model, base_prompt, traits[], topics[] }`
193
185
 
package/src/cli/mcp.ts CHANGED
@@ -18,7 +18,7 @@ export function createMcpServer(): McpServer {
18
18
  "ei_search",
19
19
  {
20
20
  description:
21
- "Search the user's Ei knowledge base — a persistent memory store built from conversations. Returns facts, people, topics of interest, quotes, and personas. People results include an identifiers array (e.g. GitHub username, Discord handle, email, nickname) — query by any name or handle to find what Ei knows about that person. Persona results include traits and topics that define the persona's identity and working style — use type='personas' with the persona's name OR a natural-language description of what they do to load a persona's character sheet. Results include entity IDs that can be passed back to ei_lookup for full detail. Omit query to browse by recency (use with recent=true or persona filter).",
21
+ "Search the user's Ei knowledge base — a persistent memory store built from conversations. Use at session start to load user context, and mid-session whenever the user references past work, preferences, or people. Returns facts, people, topics of interest, quotes, and personas. TYPE GUIDANCE: 'facts' are ONLY user demographics (name, age, job title, location, family structure, physical traits). For interests, opinions, hobbies, or anything the human cares about, use 'topics'. For named individuals, use 'people'. For verbatim things said, use 'quotes'. For AI agent identities with traits and working style, use 'personas'. People results include an identifiers array (e.g. GitHub username, Discord handle, email, nickname) — query by any name or handle to find what Ei knows about that person. Persona results include traits and topics — use type='personas' with the persona's name OR a natural-language description of their role to load a persona's character sheet. Results include entity IDs that can be passed to ei_lookup for full detail. Omit query with recent=true to browse the most recently discussed items.",
22
22
  inputSchema: {
23
23
  query: z.string().optional().describe("Search text. Supports natural language. Omit to browse without semantic filtering — useful with recent=true or persona filter."),
24
24
  type: z
@@ -99,7 +99,7 @@ export function createMcpServer(): McpServer {
99
99
  "ei_lookup",
100
100
  {
101
101
  description:
102
- "Look up a specific entity in the Ei knowledge base by ID. Returns the full entity record. Use IDs from ei_search results.",
102
+ "Retrieve the full record for any Ei entity by ID facts, topics, people, quotes, or personas. Use when ei_search returns an item and you need its complete details (all fields, traits, topics, identifiers, etc.). Pass the entity id from ei_search results.",
103
103
  inputSchema: {
104
104
  id: z.string().describe("The entity ID to look up."),
105
105
  source: z
@@ -130,131 +130,11 @@ export function createMcpServer(): McpServer {
130
130
  }
131
131
  );
132
132
 
133
- server.registerTool(
134
- "ei_find_memory",
135
- {
136
- description:
137
- "Search Ei's persistent knowledge base — facts, topics, people, and quotes learned across ALL conversations over time. Use when you need context about the user, their life, relationships, or interests that may not be visible in the current exchange. Returns results grouped by type. Use `recent: true` to retrieve what's been discussed recently. TYPE GUIDANCE: 'facts' are ONLY user demographics — name, age, job title, location, family structure, physical traits. For interests, opinions, hobbies, or anything the human cares about, use 'topics'. For named individuals, use 'people'. For verbatim things said, use 'quotes'.",
138
- inputSchema: {
139
- query: z
140
- .string()
141
- .optional()
142
- .describe(
143
- "What to search for — a person, topic, fact, or anything Ei has learned about the user. Omit with recent: true to browse recent items."
144
- ),
145
- types: z
146
- .array(z.enum(["facts", "topics", "people", "quotes"]))
147
- .optional()
148
- .describe("Limit search to specific memory types (default: all types). Use 'facts' ONLY for user demographics (name, age, job, location, family). Use 'topics' for interests, opinions, and anything the human cares about."),
149
- limit: z
150
- .number()
151
- .optional()
152
- .default(10)
153
- .describe("Max results per type to return (default: 10, max: 20)"),
154
- recent: z
155
- .boolean()
156
- .optional()
157
- .describe(
158
- "If true, return recently-mentioned results sorted by last_mentioned date instead of relevance. Combine with a query to filter recent results by topic."
159
- ),
160
- persona: z
161
- .string()
162
- .optional()
163
- .describe(
164
- "Filter results to what a specific persona has learned. Use the persona display name."
165
- ),
166
- },
167
- },
168
- async ({ query: rawQuery, types, limit, recent, persona }) => {
169
- const query = rawQuery ?? "";
170
- const effectiveLimit = Math.min(limit ?? 10, 20);
171
- const options = { recent: recent ?? false };
172
-
173
- const humanTypes = ["facts", "topics", "people", "quotes"] as const;
174
- type HumanType = (typeof humanTypes)[number];
175
- const requestedTypes: HumanType[] =
176
- types && types.length > 0
177
- ? (types as HumanType[])
178
- : [...humanTypes];
179
-
180
- let state: StorageState | null = null;
181
- let personaId: string | undefined;
182
- if (persona) {
183
- state = await loadLatestState();
184
- if (state) {
185
- personaId = resolvePersonaId(state, persona) ?? undefined;
186
- if (!personaId) {
187
- return {
188
- content: [{ type: "text" as const, text: `Persona "${persona}" not found.` }],
189
- };
190
- }
191
- }
192
- }
193
-
194
- const grouped: Record<string, unknown[]> = {};
195
- for (const t of requestedTypes) {
196
- const module = await import(`./commands/${t}.js`);
197
- let results = await (
198
- module.execute as (
199
- q: string,
200
- l: number,
201
- o: { recent: boolean }
202
- ) => Promise<{ id: string }[]>
203
- )(query, effectiveLimit, options);
204
- if (personaId && state) {
205
- results = filterTypeSpecificByPersona(results, state, personaId, t);
206
- }
207
- if (results.length > 0) {
208
- grouped[t] = results;
209
- }
210
- }
211
-
212
- if (Object.keys(grouped).length === 0) {
213
- return {
214
- content: [
215
- {
216
- type: "text" as const,
217
- text: JSON.stringify({ result: "No relevant memories found for this query." }),
218
- },
219
- ],
220
- };
221
- }
222
-
223
- return {
224
- content: [{ type: "text" as const, text: JSON.stringify(grouped, null, 2) }],
225
- };
226
- }
227
- );
228
-
229
- server.registerTool(
230
- "ei_fetch_memory",
231
- {
232
- description:
233
- "Retrieve the full record for a specific memory by its ID. Use when ei_find_memory or ei_search returns an item and you need its complete details. Returns the full Fact, Topic, Person, or Quote record.",
234
- inputSchema: {
235
- id: z.string().describe("The ID of the memory record to retrieve"),
236
- },
237
- },
238
- async ({ id }) => {
239
- const result = await lookupById(id);
240
-
241
- if (result === null || result.type === "persona") {
242
- return {
243
- content: [{ type: "text" as const, text: `No memory record found with ID: ${id}` }],
244
- };
245
- }
246
-
247
- return {
248
- content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
249
- };
250
- }
251
- );
252
-
253
133
  server.registerTool(
254
134
  "ei_fetch_message",
255
135
  {
256
136
  description:
257
- "Retrieve a specific message by its ID, with optional surrounding context. Use when ei_find_memory returns a quote with a message_id and you want to read the original conversation. The 'before' and 'after' parameters return that many additional messages for context (default 0).",
137
+ "Retrieve a specific message by its fully-qualified ID, with optional surrounding conversation context. Use when ei_search returns a quote with a message_id and you want to read the original exchange. The 'before' and 'after' parameters expand the context window in either direction (default 0). Accepts IDs from any integrated source: 'ei:uuid' searches Ei state, 'opencode:machine:session:id' queries OpenCode SQLite, 'claudecode:...' scans Claude Code JSONL files, 'cursor:...' reads the Cursor DB.",
258
138
  inputSchema: {
259
139
  id: z.string().describe("The ID of the message to retrieve"),
260
140
  before: z
@@ -84,7 +84,6 @@ export interface LinkedItem {
84
84
  type: string;
85
85
  }
86
86
  export interface QuoteResult {
87
- id: string;
88
87
  text: string;
89
88
  speaker: string;
90
89
  timestamp: string;
@@ -163,7 +162,6 @@ export function resolveLinkedItems(dataItemIds: string[], state: StorageState):
163
162
  }
164
163
  export function mapQuote(quote: Quote, state: StorageState): QuoteResult {
165
164
  return {
166
- id: quote.id,
167
165
  text: quote.text,
168
166
  speaker: quote.speaker,
169
167
  timestamp: quote.timestamp,
@@ -287,16 +285,11 @@ export async function retrieveBalanced(
287
285
  const recentDate = (item: AnyItem): string => item.last_mentioned ?? item.last_updated ?? "";
288
286
 
289
287
  if (recent && !query) {
290
- const allItems: Array<{ type: DataType; item: AnyItem; mapped: QuoteResult | FactResult | PersonResult | TopicResult | PersonaResult }> = [
288
+ const allItems: Array<{ type: DataType; item: AnyItem; mapped: QuoteResult | FactResult | PersonResult | TopicResult }> = [
291
289
  ...state.human.quotes.map(q => ({ type: "quote" as DataType, item: q as AnyItem, mapped: mapQuote(q, state) })),
292
290
  ...state.human.facts.map(f => ({ type: "fact" as DataType, item: f as AnyItem, mapped: mapFact(f) })),
293
291
  ...state.human.people.map(p => ({ type: "person" as DataType, item: p as AnyItem, mapped: mapPerson(p) })),
294
292
  ...state.human.topics.map(t => ({ type: "topic" as DataType, item: t as AnyItem, mapped: mapTopic(t) })),
295
- ...Object.values(state.personas).map(({ entity: p }) => ({
296
- type: "persona" as DataType,
297
- item: { id: p.id, last_updated: p.last_updated } as AnyItem,
298
- mapped: mapPersona(p),
299
- })),
300
293
  ];
301
294
  return allItems
302
295
  .sort((a, b) => recentDate(b.item).localeCompare(recentDate(a.item)))
@@ -330,22 +323,10 @@ export async function retrieveBalanced(
330
323
  }
331
324
  }
332
325
  }
333
- const embeddingResults = allScored
326
+ return allScored
334
327
  .sort((a, b) => recentDate(b.mapped as AnyItem).localeCompare(recentDate(a.mapped as AnyItem)))
335
328
  .slice(0, limit)
336
329
  .map(({ type, mapped }) => ({ type, ...mapped }) as BalancedResult);
337
- const personaMatches = [
338
- ...retrievePersonas(query, state, limit, { recent: true }),
339
- ...await retrievePersonasSemantic(queryVector, state, limit),
340
- ].filter((p, i, arr) => arr.findIndex(x => x.id === p.id) === i);
341
- if (personaMatches.length > 0) {
342
- const combined = [
343
- ...personaMatches.map((p) => ({ type: "persona" as const, ...p }) as BalancedResult),
344
- ...embeddingResults,
345
- ];
346
- return combined.slice(0, limit);
347
- }
348
- return embeddingResults;
349
330
  }
350
331
 
351
332
  for (const { type, items, mapper } of typeConfigs) {
@@ -383,19 +364,7 @@ export async function retrieveBalanced(
383
364
 
384
365
  result.sort((a, b) => b.similarity - a.similarity);
385
366
 
386
- const embeddingFinal = result.map(({ type, mapped }) => ({ type, ...mapped }) as BalancedResult);
387
- const personaFinal = [
388
- ...retrievePersonas(query, state, limit),
389
- ...await retrievePersonasSemantic(queryVector, state, limit),
390
- ].filter((p, i, arr) => arr.findIndex(x => x.id === p.id) === i);
391
- if (personaFinal.length > 0) {
392
- const combined = [
393
- ...personaFinal.map((p) => ({ type: "persona" as const, ...p }) as BalancedResult),
394
- ...embeddingFinal,
395
- ];
396
- return combined.slice(0, limit);
397
- }
398
- return embeddingFinal;
367
+ return result.map(({ type, mapped }) => ({ type, ...mapped }) as BalancedResult);
399
368
  }
400
369
 
401
370
  const OPENCODE_MESSAGE_ID = /^msg_[a-zA-Z0-9]+$/;