ei-tui 1.5.0 → 1.6.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 CHANGED
@@ -6,7 +6,7 @@ You can access the Web version at [ei.flare576.com](https://ei.flare576.com).
6
6
 
7
7
  You can run the local version via `bunx ei-tui` — no install needed, always current (see [### TUI](#tui) for details).
8
8
 
9
- If you're here to give your coding tools (OpenCode, Claude Code, Cursor) persistent memory, jump over to [TUI README.md](./tui/README.md) to learn how to get information _into_ Ei, and [CLI README.md](./src/cli/README.md) to get it back _out_.
9
+ If you're here to give your coding tools (OpenCode, Claude Code, Cursor) persistent memory, jump over to [TUI README.md](./tui/README.md) to learn how to get information _into_ Ei, and [CLI README.md](./src/cli/README.md) to wire up automatic context injection so your agents receive relevant memory before every message — no tool calls required.
10
10
 
11
11
  ## What Does "Local First" Mean?
12
12
 
@@ -140,7 +140,7 @@ opencode:
140
140
 
141
141
  OpenCode saves sessions as JSON or SQLite (depending on version). Ei reads them, extracts context per-agent (each agent like Sisyphus gets its own persona), and keeps everything current as sessions accumulate.
142
142
 
143
- OpenCode can also *read* Ei's knowledge back out via the [CLI tool](src/cli/README.md) — making it a dynamic, perpetual RAG. That's why it always has context from your other projects.
143
+ OpenCode can also *read* Ei's knowledge back out via the [CLI tool](src/cli/README.md). Run `ei --install` to wire up automatic context injection relevant topics are injected before every message, and (with [Oh My OpenCode](https://github.com/code-yeongyu/oh-my-opencode)) each agent's Ei persona record is loaded into the system prompt at session start. The agent knows who it is *to you* before it reads your first message.
144
144
 
145
145
  #### Claude Code
146
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli/README.md CHANGED
@@ -15,7 +15,7 @@ ei --recent # Most recently mentioned items (no query
15
15
  ei --persona "Beta" --recent # Most recently mentioned items Beta has learned
16
16
  ei --id <id> # Look up entity by ID — or fetch a message by FQ ID
17
17
  echo <id> | ei --id # Look up entity by ID from stdin
18
- ei --install # Register Ei with OpenCode, Claude Code, and Cursor
18
+ ei --install # Wire Ei into Claude Code, Cursor, and OpenCode (MCP + hooks + persona plugin)
19
19
  ei mcp # Start the Ei MCP stdio server (for Cursor/Claude Desktop)
20
20
  ```
21
21
 
@@ -47,13 +47,17 @@ Quotes surfaced by `ei_search` include a `message_id` field in this format — p
47
47
  ei --install
48
48
  ```
49
49
 
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:
50
+ This registers Ei with Claude Code, Cursor, and OpenCode — MCP server config, context injection hooks, and (for OpenCode) a persona identity plugin so agents know who they are before the first message:
51
51
 
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`) |
52
+ | Tool | MCP | Context Hook | Persona Plugin |
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) | Via Oh My OpenCode compatibility layer (reads `~/.claude/settings.json`) | `~/.config/opencode/plugins/ei-persona.ts` |
57
+
58
+ **Context hook**: fires before every message, searches Ei for topics relevant to what you just asked, injects them silently. No tool call required.
59
+
60
+ **Persona plugin** (OpenCode + [Oh My OpenCode](https://github.com/code-yeongyu/oh-my-opencode) only): injects the agent's Ei relationship record directly into the system prompt at session start — traits, working style, shared context. The agent knows who it is *to you* before it reads a word of your message.
57
61
 
58
62
  **OpenCode MCP**: add manually to `~/.config/opencode/opencode.jsonc`:
59
63
 
@@ -82,73 +86,14 @@ Claude Code and Cursor call `ei mcp` to start the MCP stdio server. You can run
82
86
  ei mcp
83
87
  ```
84
88
 
85
- ## Activating Ei in Your Agent
86
-
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.
88
-
89
- The snippets below are optional manual overrides if you want to customize the behavior or add targeted mid-session queries.
89
+ ## How Automatic Context Injection Works
90
90
 
91
- ### OpenCode
91
+ After `ei --install`, agents receive Ei context without any manual tool calls:
92
92
 
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`:
96
-
97
- ```markdown
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.
101
- ```
93
+ 1. **Before each message** — the hook searches Ei using your prompt + recent conversation history as the query, then injects relevant topics into the conversation as `[Ei Memory Context]`. You won't see this in your chat view; the agent does.
94
+ 2. **At session start** (OpenCode + OMO only) — the persona plugin finds the agent's Ei persona record and appends it to the system prompt as `<ei-relationship>`. The agent knows its working style, traits, and shared history with you before the session begins.
102
95
 
103
- ### Claude Code
104
-
105
- Add to `~/.claude/CLAUDE.md` (user-level) or `CLAUDE.md` at project root:
106
-
107
- ```markdown
108
- At session start, use the **ei** MCP to pull user context: call `ei_search` with a
109
- natural-language query about the user's preferences, active projects, and workflow.
110
- A `persona` filter is available to scope results to what a specific persona has learned.
111
- Use `type: "personas"` to search for personas by name.
112
-
113
- Use Ei when the user references past decisions, mentions people or preferences, asks
114
- "how did we do X," or needs to look up a person by any name, handle, or account — people
115
- results include an `identifiers` array (GitHub username, Discord handle, email, nickname, etc.)
116
- covering all known accounts and aliases. Query again when they correct you or reference
117
- something from a previous session.
118
- ```
119
-
120
- ### Cursor
121
-
122
- Create `.cursor/rules/ei-mcp.mdc` in your project (or `~/.cursor/rules/` for user-level):
123
-
124
- ```markdown
125
- ---
126
- description: When to use the Ei MCP for user memory and context
127
- alwaysApply: true
128
- ---
129
- # Ei MCP — User knowledge base
130
-
131
- The **ei** MCP (server `user-ei`) is a persistent knowledge base built from the user's
132
- conversations (facts, people, topics, quotes, personas).
133
-
134
- **Use it when:**
135
- - The user refers to past decisions, fixes, or "how we did X" and current chat/codebase
136
- doesn't have that context.
137
- - You need the user's preferences, contacts, or project conventions (e.g. who to ask for
138
- access, how something was fixed).
139
- - You need to look up a person by any name, handle, or account — people results include an
140
- `identifiers` array (GitHub username, Discord handle, email, nickname, etc.) covering all
141
- known accounts and aliases for that person.
142
- - The question is about the user personally (people, workflow, prior discussions) rather
143
- than only code.
144
-
145
- **How to use:**
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.
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.
149
-
150
- Prefer querying Ei before asking the user for context they may have already shared.
151
- ```
96
+ The `ei_search`, `ei_lookup`, and `ei_fetch_message` MCP tools are still available for targeted mid-session queries — use them when you want to look something up explicitly.
152
97
 
153
98
  ## MCP Tools Reference
154
99
 
package/src/cli.ts CHANGED
@@ -90,8 +90,31 @@ Examples:
90
90
 
91
91
  async function installMcpClients(): Promise<void> {
92
92
  await installClaudeCode();
93
- await installCursor();
94
- await installOpenCodePlugin();
93
+
94
+ const home = process.env.HOME || "~";
95
+
96
+ const cursorDataDirs = [
97
+ join(home, "Library", "Application Support", "Cursor"),
98
+ join(home, ".config", "Cursor"),
99
+ join(home, "AppData", "Roaming", "Cursor"),
100
+ ];
101
+ const hasCursor = (await Promise.all(cursorDataDirs.map((p) => Bun.file(join(p, "User")).exists()))).some(Boolean);
102
+ if (hasCursor) {
103
+ await installCursor();
104
+ } else {
105
+ console.log(`ℹ️ Cursor not detected — skipping Cursor install.`);
106
+ }
107
+
108
+ const opencodeDir = join(home, ".config", "opencode");
109
+ const hasOpenCode = await Bun.file(join(opencodeDir, "opencode.jsonc")).exists() ||
110
+ await Bun.file(join(opencodeDir, "opencode.json")).exists() ||
111
+ await Bun.file(join(opencodeDir, "opencode.db")).exists();
112
+
113
+ if (hasOpenCode) {
114
+ await installOpenCodePlugin();
115
+ } else {
116
+ console.log(`ℹ️ OpenCode not detected — skipping OpenCode plugin install.`);
117
+ }
95
118
  }
96
119
 
97
120
  async function installClaudeCode(): Promise<void> {
@@ -155,10 +178,11 @@ import { $ } from "bun";
155
178
 
156
179
  const heading = \`
157
180
  ## Ei Memory Context
181
+ *(The user cannot see this block. It is injected automatically before their message.)*
182
+ *(If you reference anything from it, briefly explain where it came from — e.g. "Ei shows you've been working on X" — so the user isn't confused by knowledge that appeared from nowhere.)*
158
183
 
159
- Ei is a personal knowledge base built from coding sessions, Slack, documents, and conversations.
160
- The following topics MAY be relevant to your current task — use the \\\`ei_search\\\` and \\\`ei_lookup\\\`
161
- MCP tools for targeted queries.
184
+ Ei is a personal knowledge base built from the user's coding sessions, Slack, documents, and conversations.
185
+ The following topics MAY be relevant to your current task — use \\\`ei_search\\\` or \\\`ei_lookup\\\` for targeted queries.
162
186
  \`;
163
187
 
164
188
  const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
@@ -319,6 +343,127 @@ exit 0
319
343
  async function installOpenCodePlugin(): Promise<void> {
320
344
  const home = process.env.HOME || "~";
321
345
  const opencodeDir = join(home, ".config", "opencode");
346
+ const pluginsDir = join(opencodeDir, "plugins");
347
+ const pluginPath = join(pluginsDir, "ei-persona.ts");
348
+
349
+ await Bun.$`mkdir -p ${pluginsDir}`;
350
+
351
+ const pluginContent = `import { $ } from "bun"
352
+ import { join } from "path"
353
+ import { appendFileSync } from "fs"
354
+
355
+ const sessionCache = new Map<string, string | null>()
356
+ const sessionFetch = new Map<string, Promise<string | null>>()
357
+
358
+ const logPath = join(process.env.EI_DATA_PATH ?? join(process.env.HOME ?? "~", ".local", "share", "ei"), "ei-persona-plugin.log")
359
+
360
+ function log(msg: string) {
361
+ try {
362
+ appendFileSync(logPath, \`[\${new Date().toISOString()}] \${msg}\\n\`)
363
+ } catch {}
364
+ }
365
+
366
+ type PersonaTrait = { name: string; description: string; strength: number }
367
+ type PersonaTopic = { name: string; perspective: string; approach: string; exposure_current: number }
368
+ type PersonaResult = { display_name: string; base_prompt?: string; traits?: PersonaTrait[]; topics?: PersonaTopic[] }
369
+
370
+ // Pulls the agent name from the system prompt. Handles OMO's multiple formats:
371
+ // You are "Sisyphus" - ... (quoted, dash)
372
+ // You are "Sisyphus - Ultraworker" (quoted, dash in name)
373
+ // You are Atlas - ... (unquoted, dash)
374
+ // You are Hephaestus, ... (unquoted, comma)
375
+ export function extractAgentName(systemPrompt: string): string | null {
376
+ const clean = systemPrompt.replace(/[\\u200B-\\u200D\\uFEFF]/g, "")
377
+ const quoted = clean.match(/You are "([^"]+)"/)
378
+ if (quoted?.[1]) return quoted[1].trim()
379
+ const unquoted = clean.match(/You are ([A-Za-z][A-Za-z0-9]*)(?:\\s*[-—,]|\\s*$)/m)
380
+ if (unquoted?.[1]) return unquoted[1].trim()
381
+ return null
382
+ }
383
+
384
+ // Queries Ei for persona candidates and validates by name containment —
385
+ // tolerates OMO renaming agents without requiring a hardcoded alias map.
386
+ export async function resolveEiPersona(rawName: string): Promise<PersonaResult | null> {
387
+ try {
388
+ const out = await $\`bunx ei-tui@latest personas -n 5 \${rawName}\`.text()
389
+ const candidates = JSON.parse(out.trim()) as PersonaResult[]
390
+ if (!Array.isArray(candidates) || candidates.length === 0) return null
391
+ const rawLower = rawName.toLowerCase()
392
+ const match = candidates.find((p) => {
393
+ const nameLower = p.display_name.toLowerCase()
394
+ return rawLower.includes(nameLower) || nameLower.includes(rawLower)
395
+ })
396
+ return match ?? null
397
+ } catch {
398
+ return null
399
+ }
400
+ }
401
+
402
+ function buildEiRelationshipBlock(persona: PersonaResult): string {
403
+ const strongTraits = (persona.traits ?? [])
404
+ .filter((t) => t.strength >= 0.7)
405
+ .sort((a, b) => b.strength - a.strength)
406
+ .map((t) => \`**\${t.name}** (\${Math.round(t.strength * 100)}%): \${t.description}\`)
407
+ .join("\\n")
408
+ const sortedTopics = [...(persona.topics ?? [])]
409
+ .sort((a, b) => b.exposure_current - a.exposure_current)
410
+ .map((t) => \`**\${t.name}**: \${t.perspective} — \${t.approach}\`)
411
+ .join("\\n")
412
+ return [
413
+ "<ei-relationship>",
414
+ "## Ei: Relationship Context",
415
+ "",
416
+ persona.base_prompt ?? "",
417
+ "",
418
+ "### Working Style",
419
+ strongTraits || "(no traits above threshold)",
420
+ "",
421
+ "### Shared Context",
422
+ sortedTopics || "(no topics)",
423
+ "</ei-relationship>",
424
+ ].join("\\n")
425
+ }
426
+
427
+ export default async function EiPersonaPlugin() {
428
+ return {
429
+ name: "ei-persona",
430
+ "experimental.chat.system.transform": async (
431
+ input: { sessionID?: string; model: { id: string; providerID: string; [key: string]: unknown } },
432
+ output: { system: string[] },
433
+ ): Promise<void> => {
434
+ const rawName = extractAgentName(output.system[0] ?? "")
435
+ if (!rawName) return
436
+
437
+ const cacheKey = \`\${input.sessionID ?? "unknown"}:\${rawName}\`
438
+
439
+ if (sessionCache.has(cacheKey)) {
440
+ const cached = sessionCache.get(cacheKey) ?? null
441
+ if (cached !== null && !output.system[0].includes("<ei-relationship>"))
442
+ output.system[0] = output.system[0] + "\\n\\n" + cached
443
+ return
444
+ }
445
+
446
+ if (!sessionFetch.has(cacheKey)) {
447
+ sessionFetch.set(cacheKey, (async () => {
448
+ const persona = await resolveEiPersona(rawName)
449
+ if (!persona) return null
450
+ log(\`ei-persona: injecting \${persona.display_name}\`)
451
+ return buildEiRelationshipBlock(persona)
452
+ })())
453
+ }
454
+
455
+ const block = await sessionFetch.get(cacheKey)!
456
+ sessionCache.set(cacheKey, block)
457
+ if (block !== null && !output.system[0].includes("<ei-relationship>"))
458
+ output.system[0] = output.system[0] + "\\n\\n" + block
459
+ },
460
+ }
461
+ }
462
+ `;
463
+
464
+ await Bun.write(pluginPath, pluginContent);
465
+ console.log(`✓ Installed Ei persona plugin to ${pluginPath}`);
466
+
322
467
  const omoCandidates = [
323
468
  join(opencodeDir, "oh-my-opencode.json"),
324
469
  join(opencodeDir, "oh-my-opencode.jsonc"),
@@ -329,22 +474,18 @@ async function installOpenCodePlugin(): Promise<void> {
329
474
  ];
330
475
  const hasOmo = (await Promise.all(omoCandidates.map((p) => Bun.file(p).exists()))).some(Boolean);
331
476
 
332
- if (hasOmo) {
333
- console.log(`✓ Oh My OpenCode detected — UserPromptSubmit hook covers OpenCode automatically.`);
334
- return;
335
- }
336
-
337
- console.log(`
338
- ℹ️ OpenCode detected without Oh My OpenCode.
339
- The ~/.claude/settings.json UserPromptSubmit hook only fires in Claude Code.
340
- For the same context injection in OpenCode, we recommend:
477
+ if (!hasOmo) {
478
+ console.log(`
479
+ ℹ️ Oh My OpenCode not detected.
480
+ The Ei persona plugin is installed, but context injection (hook) requires OMO.
481
+ For full Ei integration in OpenCode, we recommend:
341
482
 
342
483
  bunx oh-my-opencode install
343
484
 
344
- Oh My OpenCode is to OpenCode what oh-my-zsh is to zsh — you can run
345
- without it, but you probably shouldn't. It also picks up the Ei hook
346
- automatically via its Claude Code compatibility layer.
485
+ OMO picks up the Ei UserPromptSubmit hook automatically via its Claude Code
486
+ compatibility layer.
347
487
  `);
488
+ }
348
489
  }
349
490
 
350
491
  async function getRecentSessionMessages(
@@ -75,8 +75,7 @@ export interface Person extends DataItemBase {
75
75
  }
76
76
 
77
77
  export interface Quote {
78
- /** @deprecated Remove in v1.6 use message_id for retrieval */
79
- id: string; // UUID (use crypto.randomUUID())
78
+ id: string; // UUIDstable identity for CRUD operations (use crypto.randomUUID())
80
79
  message_id: string | null; // FK to Message.id (nullable for manual quotes)
81
80
  data_item_ids: string[]; // FK[] to DataItemBase.id
82
81
  persona_groups: string[]; // Visibility groups
package/tui/README.md CHANGED
@@ -39,7 +39,7 @@ Enable any or all three in `/settings`. They work independently and feed into th
39
39
 
40
40
  Sessions are processed oldest-first, one per queue cycle. On first run Ei works through your backlog gradually — it won't flood your LLM provider.
41
41
 
42
- OpenCode also supports reading Ei's extracted knowledge back out via the [CLI tool](../src/cli/README.md), giving it persistent memory across sessions.
42
+ All three tools also support reading Ei's knowledge back out via the [CLI tool](../src/cli/README.md) run `ei --install` to wire up automatic context injection (hooks + persona plugin) so your coding agents receive relevant Ei memory before every message without any manual tool calls.
43
43
 
44
44
  ## Slack Integration
45
45