ei-tui 0.2.0 → 0.3.5

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.
Files changed (39) hide show
  1. package/package.json +2 -1
  2. package/src/cli/README.md +83 -2
  3. package/src/cli/commands/facts.ts +2 -2
  4. package/src/cli/commands/people.ts +2 -2
  5. package/src/cli/commands/quotes.ts +2 -2
  6. package/src/cli/commands/topics.ts +2 -2
  7. package/src/cli/mcp.ts +94 -0
  8. package/src/cli/retrieval.ts +64 -6
  9. package/src/cli.ts +61 -16
  10. package/src/core/handlers/dedup.ts +2 -8
  11. package/src/core/handlers/human-extraction.ts +1 -0
  12. package/src/core/handlers/human-matching.ts +2 -0
  13. package/src/core/handlers/rewrite.ts +3 -25
  14. package/src/core/human-data-manager.ts +35 -11
  15. package/src/core/orchestrators/ceremony.ts +0 -6
  16. package/src/core/orchestrators/dedup-phase.ts +2 -3
  17. package/src/core/orchestrators/human-extraction.ts +20 -6
  18. package/src/core/processor.ts +93 -4
  19. package/src/core/queue-manager.ts +1 -0
  20. package/src/core/state-manager.ts +9 -0
  21. package/src/core/tools/builtin/read-memory.ts +7 -11
  22. package/src/core/tools/builtin/web-fetch.ts +73 -0
  23. package/src/core/tools/index.ts +2 -0
  24. package/src/core/types/data-items.ts +1 -0
  25. package/src/core/types/entities.ts +1 -0
  26. package/src/core/types/integrations.ts +2 -0
  27. package/src/integrations/claude-code/importer.ts +6 -1
  28. package/src/integrations/claude-code/types.ts +1 -0
  29. package/src/integrations/cursor/importer.ts +282 -0
  30. package/src/integrations/cursor/index.ts +10 -0
  31. package/src/integrations/cursor/reader.ts +209 -0
  32. package/src/integrations/cursor/types.ts +140 -0
  33. package/src/integrations/opencode/importer.ts +7 -1
  34. package/src/prompts/ceremony/dedup.ts +0 -33
  35. package/src/prompts/ceremony/rewrite.ts +4 -20
  36. package/src/prompts/ceremony/types.ts +1 -1
  37. package/src/prompts/response/index.ts +17 -9
  38. package/tui/src/context/keyboard.tsx +4 -1
  39. package/tui/src/util/yaml-serializers.ts +28 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "0.2.0",
3
+ "version": "0.3.5",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
@@ -58,6 +58,7 @@
58
58
  },
59
59
  "type": "module",
60
60
  "dependencies": {
61
+ "@modelcontextprotocol/sdk": "^1.27.1",
61
62
  "@opentui/core": "^0.1.79",
62
63
  "@opentui/solid": "^0.1.79",
63
64
  "fastembed": "^2.1.0",
package/src/cli/README.md CHANGED
@@ -11,7 +11,8 @@ ei topics -n 5 "query string" # Return up to 5 topics
11
11
  ei quotes -n 5 "query string" # Return up to 5 quotes
12
12
  ei --id <id> # Look up a specific entity by ID
13
13
  echo <id> | ei --id # Look up entity by ID from stdin
14
- ei --install # Install the Ei tool for OpenCode
14
+ ei --install # Register Ei with OpenCode, Claude Code, and Cursor
15
+ ei mcp # Start the Ei MCP stdio server (for Cursor/Claude Desktop)
15
16
  ```
16
17
 
17
18
  Type aliases: `fact`, `person`, `topic`, `quote` all work (singular or plural).
@@ -32,7 +33,87 @@ ei "memory leak" | jq '.[0].id' | ei --id
32
33
  ei --install
33
34
  ```
34
35
 
35
- This writes `~/.config/opencode/tools/ei.ts` with a complete tool definition. Restart OpenCode to activate.
36
+ This registers Ei with every supported agent environment it detects:
37
+
38
+ - **OpenCode**: writes `~/.config/opencode/tools/ei.ts`
39
+ - **Claude Code**: runs `claude mcp add` (or writes `~/.claude.json` as fallback)
40
+ - **Cursor**: writes `~/.cursor/mcp.json`
41
+
42
+ Restart your agent tool after running to activate.
43
+
44
+ ### MCP Server
45
+
46
+ Claude Code and Cursor call `ei mcp` to start the MCP stdio server. You can run it directly to test:
47
+
48
+ ```sh
49
+ ei mcp
50
+ ```
51
+
52
+ ## Activating Ei in Your Agent
53
+
54
+ `ei --install` handles the technical wiring. This step tells your agent *when* and *how* to reach for it.
55
+
56
+ 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.
57
+
58
+ ### OpenCode
59
+
60
+ Add to `~/.config/opencode/AGENTS.md` (applies to all projects):
61
+
62
+ ```markdown
63
+ At session start, query Ei for user context:
64
+
65
+ \```bash
66
+ ei "What are the user's current preferences, active projects, and workflow?"
67
+ \```
68
+
69
+ Ei is a persistent knowledge base built from the user's conversations — facts, preferences,
70
+ people, topics. Use it when the user references past work, mentions how they like things done,
71
+ or asks "how did we do X." Query again mid-session when they correct you or reference something
72
+ from a previous session.
73
+ ```
74
+
75
+ ### Claude Code
76
+
77
+ Add to `~/.claude/CLAUDE.md` (user-level) or `CLAUDE.md` at project root:
78
+
79
+ ```markdown
80
+ At session start, use the **ei** MCP to pull user context: call `ei_search` with a
81
+ natural-language query about the user's preferences, active projects, and workflow.
82
+
83
+ Use Ei when the user references past decisions, mentions people or preferences, or asks
84
+ "how did we do X." Query again when they correct you or reference something from a previous
85
+ session.
86
+ ```
87
+
88
+ ### Cursor
89
+
90
+ Create `.cursor/rules/ei-mcp.mdc` in your project (or `~/.cursor/rules/` for user-level):
91
+
92
+ ```markdown
93
+ ---
94
+ description: When to use the Ei MCP for user memory and context
95
+ alwaysApply: true
96
+ ---
97
+ # Ei MCP — User knowledge base
98
+
99
+ The **ei** MCP (server `user-ei`) is a persistent knowledge base built from the user's
100
+ conversations (facts, people, topics, quotes).
101
+
102
+ **Use it when:**
103
+ - The user refers to past decisions, fixes, or "how we did X" and current chat/codebase
104
+ doesn't have that context.
105
+ - You need the user's preferences, contacts, or project conventions (e.g. who to ask for
106
+ access, how something was fixed).
107
+ - The question is about the user personally (people, workflow, prior discussions) rather
108
+ than only code.
109
+
110
+ **How to use:**
111
+ 1. Call `ei_search` (server `user-ei`) with a natural-language query; optionally filter by
112
+ `type`: facts, people, topics, quotes.
113
+ 2. If you need full detail for a result, call `ei_lookup` with the entity `id` from step 1.
114
+
115
+ Prefer querying Ei before asking the user for context they may have already shared.
116
+ ```
36
117
 
37
118
  ## What the Tool Provides
38
119
 
@@ -1,7 +1,7 @@
1
1
  import { loadLatestState, retrieve } from "../retrieval";
2
2
  import type { FactResult } from "../retrieval";
3
3
 
4
- export async function execute(query: string, limit: number): Promise<FactResult[]> {
4
+ export async function execute(query: string, limit: number, options: { recent?: boolean } = {}): Promise<FactResult[]> {
5
5
  const state = await loadLatestState();
6
6
  if (!state) {
7
7
  console.error("No saved state found. Is EI_DATA_PATH set correctly?");
@@ -13,7 +13,7 @@ export async function execute(query: string, limit: number): Promise<FactResult[
13
13
  return [];
14
14
  }
15
15
 
16
- const results = await retrieve(facts, query, limit);
16
+ const results = await retrieve(facts, query, limit, options);
17
17
 
18
18
  return results.map(fact => ({
19
19
  id: fact.id,
@@ -1,7 +1,7 @@
1
1
  import { loadLatestState, retrieve } from "../retrieval";
2
2
  import type { PersonResult } from "../retrieval";
3
3
 
4
- export async function execute(query: string, limit: number): Promise<PersonResult[]> {
4
+ export async function execute(query: string, limit: number, options: { recent?: boolean } = {}): Promise<PersonResult[]> {
5
5
  const state = await loadLatestState();
6
6
  if (!state) {
7
7
  console.error("No saved state found. Is EI_DATA_PATH set correctly?");
@@ -13,7 +13,7 @@ export async function execute(query: string, limit: number): Promise<PersonResul
13
13
  return [];
14
14
  }
15
15
 
16
- const results = await retrieve(people, query, limit);
16
+ const results = await retrieve(people, query, limit, options);
17
17
 
18
18
  return results.map(person => ({
19
19
  id: person.id,
@@ -1,7 +1,7 @@
1
1
  import { loadLatestState, retrieve, mapQuote } from "../retrieval";
2
2
  import type { QuoteResult } from "../retrieval";
3
3
 
4
- export async function execute(query: string, limit: number): Promise<QuoteResult[]> {
4
+ export async function execute(query: string, limit: number, options: { recent?: boolean } = {}): Promise<QuoteResult[]> {
5
5
  const state = await loadLatestState();
6
6
  if (!state) {
7
7
  console.error("No saved state found. Is EI_DATA_PATH set correctly?");
@@ -13,7 +13,7 @@ export async function execute(query: string, limit: number): Promise<QuoteResult
13
13
  return [];
14
14
  }
15
15
 
16
- const results = await retrieve(quotes, query, limit);
16
+ const results = await retrieve(quotes, query, limit, options);
17
17
 
18
18
  return results.map(quote => mapQuote(quote, state));
19
19
  }
@@ -1,7 +1,7 @@
1
1
  import { loadLatestState, retrieve } from "../retrieval";
2
2
  import type { TopicResult } from "../retrieval";
3
3
 
4
- export async function execute(query: string, limit: number): Promise<TopicResult[]> {
4
+ export async function execute(query: string, limit: number, options: { recent?: boolean } = {}): Promise<TopicResult[]> {
5
5
  const state = await loadLatestState();
6
6
  if (!state) {
7
7
  console.error("No saved state found. Is EI_DATA_PATH set correctly?");
@@ -13,7 +13,7 @@ export async function execute(query: string, limit: number): Promise<TopicResult
13
13
  return [];
14
14
  }
15
15
 
16
- const results = await retrieve(topics, query, limit);
16
+ const results = await retrieve(topics, query, limit, options);
17
17
 
18
18
  return results.map(topic => ({
19
19
  id: topic.id,
package/src/cli/mcp.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { z } from "zod";
4
+ import { retrieveBalanced, lookupById } from "./retrieval.js";
5
+
6
+ // Exported so tests can inject their own transport
7
+ export function createMcpServer(): McpServer {
8
+ const server = new McpServer({
9
+ name: "ei",
10
+ version: "1.0.0",
11
+ });
12
+
13
+ server.registerTool(
14
+ "ei_search",
15
+ {
16
+ description:
17
+ "Search the user's Ei knowledge base — a persistent memory store built from conversations. Returns facts, people, topics of interest, and quotes. Results include entity IDs that can be passed back to ei_lookup for full detail.",
18
+ inputSchema: {
19
+ query: z.string().describe("Search text. Supports natural language."),
20
+ type: z
21
+ .enum(["facts", "people", "topics", "quotes"])
22
+ .optional()
23
+ .describe(
24
+ "Filter to a specific data type. Omit to search all types (balanced across all 4)."
25
+ ),
26
+ limit: z
27
+ .number()
28
+ .optional()
29
+ .default(10)
30
+ .describe("Maximum number of results to return."),
31
+ recent: z
32
+ .boolean()
33
+ .optional()
34
+ .describe("If true, sort by most recently mentioned."),
35
+ },
36
+ },
37
+ async ({ query, type, limit, recent }) => {
38
+ const options = { recent: recent ?? false };
39
+ const effectiveLimit = limit ?? 10;
40
+
41
+ let result: unknown;
42
+ if (type) {
43
+ const module = await import(`./commands/${type}.js`);
44
+ result = await (module.execute as (q: string, l: number, o: { recent: boolean }) => Promise<unknown>)(query, effectiveLimit, options);
45
+ } else {
46
+ result = await retrieveBalanced(query, effectiveLimit, options);
47
+ }
48
+
49
+ return {
50
+ content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }],
51
+ };
52
+ }
53
+ );
54
+
55
+ server.registerTool(
56
+ "ei_lookup",
57
+ {
58
+ description:
59
+ "Look up a specific entity in the Ei knowledge base by ID. Returns the full entity record. Use IDs from ei_search results.",
60
+ inputSchema: {
61
+ id: z.string().describe("The entity ID to look up."),
62
+ },
63
+ },
64
+ async ({ id }) => {
65
+ const result = await lookupById(id);
66
+ const text =
67
+ result === null
68
+ ? `No entity found with ID: ${id}`
69
+ : JSON.stringify(result, null, 2);
70
+
71
+ return {
72
+ content: [{ type: "text" as const, text }],
73
+ };
74
+ }
75
+ );
76
+
77
+ return server;
78
+ }
79
+
80
+ export async function handleMcpCommand(_args: string[]): Promise<void> {
81
+ const server = createMcpServer();
82
+ const transport = new StdioServerTransport();
83
+ await server.connect(transport);
84
+
85
+ process.stderr.write("Ei MCP server running on stdio\n");
86
+
87
+ // Block until the client disconnects (stdin closes), otherwise
88
+ // the caller's process.exit(0) fires immediately and kills the server
89
+ // before it can process any messages.
90
+ await new Promise<void>((resolve) => {
91
+ process.stdin.once("end", resolve);
92
+ process.stdin.once("close", resolve);
93
+ });
94
+ }
@@ -30,18 +30,43 @@ export async function loadLatestState(): Promise<StorageState | null> {
30
30
  return null;
31
31
  }
32
32
 
33
- export async function retrieve<T extends { id: string; embedding?: number[] }>(
33
+ export async function retrieve<T extends { id: string; embedding?: number[]; last_updated?: string; last_mentioned?: string }>(
34
34
  items: T[],
35
35
  query: string,
36
- limit: number = 10
36
+ limit: number = 10,
37
+ options: { recent?: boolean } = {}
37
38
  ): Promise<T[]> {
38
- if (items.length === 0 || !query) {
39
+ if (items.length === 0) {
40
+ return [];
41
+ }
42
+
43
+ const { recent } = options;
44
+
45
+ const sortByRecent = (a: T, b: T): number => {
46
+ const aDate = a.last_mentioned ?? (a as Record<string, unknown>).last_updated as string ?? "";
47
+ const bDate = b.last_mentioned ?? (b as Record<string, unknown>).last_updated as string ?? "";
48
+ return bDate.localeCompare(aDate);
49
+ };
50
+
51
+ if (recent && !query) {
52
+ return [...items].sort(sortByRecent).slice(0, limit);
53
+ }
54
+
55
+ if (!query) {
39
56
  return [];
40
57
  }
41
58
 
42
59
  const embeddingService = getEmbeddingService();
43
60
  const queryVector = await embeddingService.embed(query);
44
61
 
62
+ if (recent) {
63
+ const topK = Math.max(limit * 5, 50);
64
+ const results = findTopK(queryVector, items, topK)
65
+ .filter(({ similarity }) => similarity >= EMBEDDING_MIN_SIMILARITY)
66
+ .map(({ item }) => item);
67
+ return results.sort(sortByRecent).slice(0, limit);
68
+ }
69
+
45
70
  const results = findTopK(queryVector, items, limit);
46
71
 
47
72
  return results
@@ -159,7 +184,8 @@ function mapTopic(topic: Topic): TopicResult {
159
184
 
160
185
  export async function retrieveBalanced(
161
186
  query: string,
162
- limit: number = 10
187
+ limit: number = 10,
188
+ options: { recent?: boolean } = {}
163
189
  ): Promise<BalancedResult[]> {
164
190
  const state = await loadLatestState();
165
191
  if (!state) {
@@ -167,6 +193,24 @@ export async function retrieveBalanced(
167
193
  return [];
168
194
  }
169
195
 
196
+ const { recent } = options;
197
+
198
+ type AnyItem = { id: string; embedding?: number[]; last_updated?: string; last_mentioned?: string };
199
+ const recentDate = (item: AnyItem): string => item.last_mentioned ?? item.last_updated ?? "";
200
+
201
+ if (recent && !query) {
202
+ const allItems: Array<{ type: DataType; item: AnyItem; mapped: QuoteResult | FactResult | PersonResult | TopicResult }> = [
203
+ ...state.human.quotes.map(q => ({ type: "quote" as DataType, item: q as AnyItem, mapped: mapQuote(q, state) })),
204
+ ...state.human.facts.map(f => ({ type: "fact" as DataType, item: f as AnyItem, mapped: mapFact(f) })),
205
+ ...state.human.people.map(p => ({ type: "person" as DataType, item: p as AnyItem, mapped: mapPerson(p) })),
206
+ ...state.human.topics.map(t => ({ type: "topic" as DataType, item: t as AnyItem, mapped: mapTopic(t) })),
207
+ ];
208
+ return allItems
209
+ .sort((a, b) => recentDate(b.item).localeCompare(recentDate(a.item)))
210
+ .slice(0, limit)
211
+ .map(({ type, mapped }) => ({ type, ...mapped }) as BalancedResult);
212
+ }
213
+
170
214
  const embeddingService = getEmbeddingService();
171
215
  const queryVector = await embeddingService.embed(query);
172
216
 
@@ -183,6 +227,22 @@ export async function retrieveBalanced(
183
227
  { type: "topic", items: state.human.topics, mapper: mapTopic },
184
228
  ];
185
229
 
230
+ if (recent) {
231
+ for (const { type, items, mapper } of typeConfigs) {
232
+ const topK = Math.max(limit * 5, 50);
233
+ const scored = findTopK(queryVector, items, topK);
234
+ for (const { item, similarity } of scored) {
235
+ if (similarity >= EMBEDDING_MIN_SIMILARITY) {
236
+ allScored.push({ type, similarity, mapped: mapper(item), itemId: item.id });
237
+ }
238
+ }
239
+ }
240
+ return allScored
241
+ .sort((a, b) => recentDate(b.mapped as AnyItem).localeCompare(recentDate(a.mapped as AnyItem)))
242
+ .slice(0, limit)
243
+ .map(({ type, mapped }) => ({ type, ...mapped }) as BalancedResult);
244
+ }
245
+
186
246
  for (const { type, items, mapper } of typeConfigs) {
187
247
  const scored = findTopK(queryVector, items, items.length);
188
248
  for (const { item, similarity } of scored) {
@@ -195,7 +255,6 @@ export async function retrieveBalanced(
195
255
  const result: ScoredEntry[] = [];
196
256
  const used = new Set<string>();
197
257
 
198
- // Floor: at least 1 result per type (if available and meets threshold)
199
258
  for (const type of DATA_TYPES) {
200
259
  if (result.length >= limit) break;
201
260
  const best = allScored
@@ -207,7 +266,6 @@ export async function retrieveBalanced(
207
266
  }
208
267
  }
209
268
 
210
- // Fill remaining slots with highest-similarity results across all types
211
269
  const remaining = allScored
212
270
  .filter(r => !used.has(r.itemId))
213
271
  .sort((a, b) => b.similarity - a.similarity);
package/src/cli.ts CHANGED
@@ -36,8 +36,12 @@ Usage:
36
36
  ei -n 5 "search text" Limit results
37
37
  ei <type> "search text" Search a specific data type
38
38
  ei <type> -n 5 "search text" Type-specific with limit
39
+ ei --recent Return most recently mentioned items
40
+ ei --recent "query" Filter recent items by query
41
+ ei <type> --recent "query" Type-specific recent search
39
42
  ei --id <id> Look up a specific entity by ID
40
43
  echo <id> | ei --id Look up entity by ID from stdin
44
+ ei mcp Start the Ei MCP stdio server (for Cursor/Claude Desktop)
41
45
 
42
46
  Types:
43
47
  quote / quotes Quotes from conversation history
@@ -47,15 +51,18 @@ Types:
47
51
 
48
52
  Options:
49
53
  --number, -n Maximum number of results (default: 10)
54
+ --recent, -r Sort by last_mentioned date (most recent first)
50
55
  --id Look up entity by ID (accepts value or stdin)
51
- --install Write the Ei tool file to ~/.config/opencode/tools/
56
+ --install Register Ei with OpenCode, Claude Code, and Cursor
52
57
  --help, -h Show this help message
53
58
 
54
59
  Examples:
55
60
  ei "debugging" # Search everything
56
61
  ei -n 5 "API design" # Top 5 across all types
57
62
  ei quote "you guessed it" # Search quotes only
58
- ei --id abc-123 # Look up entity by ID
63
+ ei --recent # Most recently mentioned items
64
+ ei topics --recent "work" # Recent work-related topics
65
+ ei --id abc-123 # Look up entity by ID
59
66
  ei "memory leak" | jq .[0].id | ei --id # Pipe ID from search
60
67
  `);
61
68
  }
@@ -122,7 +129,7 @@ async function installOpenCodeTool(): Promise<void> {
122
129
  console.log(` Restart OpenCode to activate.`);
123
130
  }
124
131
 
125
- async function installClaudeCodeMcp(): Promise<void> {
132
+ async function installClaudeCode(): Promise<void> {
126
133
  const home = process.env.HOME || "~";
127
134
  const claudeJsonPath = join(home, ".claude.json");
128
135
 
@@ -132,7 +139,7 @@ async function installClaudeCodeMcp(): Promise<void> {
132
139
  const which = Bun.spawnSync(["which", "claude"], { stdout: "pipe", stderr: "pipe" });
133
140
  if (which.exitCode === 0) {
134
141
  const result = Bun.spawnSync(
135
- ["claude", "mcp", "add", "--scope", "user", "--transport", "stdio", "ei", "--", "ei"],
142
+ ["claude", "mcp", "add", "--scope", "user", "--transport", "stdio", "ei", "--", "ei", "mcp"],
136
143
  { stdout: "pipe", stderr: "pipe" }
137
144
  );
138
145
  if (result.exitCode === 0) {
@@ -155,17 +162,11 @@ async function installClaudeCodeMcp(): Promise<void> {
155
162
  // File doesn't exist or isn't valid JSON — start fresh
156
163
  }
157
164
 
158
- // Resolve the ei binary: if running as compiled binary, argv[1] is our path;
159
- // if running as 'bun src/cli.ts', fall back to 'ei' (assumed on PATH after npm install -g)
160
- const isBunScript = process.argv[1]?.endsWith("/cli.ts") || process.argv[1]?.endsWith("/cli.js");
161
- const command = isBunScript ? "ei" : (process.argv[1] ?? "ei");
162
-
163
165
  const mcpServers = (config.mcpServers ?? {}) as Record<string, unknown>;
164
166
  mcpServers["ei"] = {
165
167
  type: "stdio",
166
- command,
167
- args: [],
168
- env: {},
168
+ command: "ei",
169
+ args: ["mcp"],
169
170
  };
170
171
  config.mcpServers = mcpServers;
171
172
 
@@ -179,6 +180,41 @@ async function installClaudeCodeMcp(): Promise<void> {
179
180
  console.log(` Restart Claude Code to activate.`);
180
181
  }
181
182
 
183
+ async function installCursor(): Promise<void> {
184
+ const home = process.env.HOME || "~";
185
+ const cursorJsonPath = join(home, ".cursor", "mcp.json");
186
+
187
+ let config: Record<string, unknown> = {};
188
+ try {
189
+ const text = await Bun.file(cursorJsonPath).text();
190
+ config = JSON.parse(text) as Record<string, unknown>;
191
+ } catch {
192
+ // File doesn't exist or isn't valid JSON — start fresh
193
+ }
194
+
195
+ const mcpServers = (config.mcpServers ?? {}) as Record<string, unknown>;
196
+ mcpServers["ei"] = {
197
+ type: "stdio",
198
+ command: "ei",
199
+ args: ["mcp"],
200
+ };
201
+ config.mcpServers = mcpServers;
202
+
203
+ await Bun.$`mkdir -p ${join(home, ".cursor")}`;
204
+ const tmpPath = `${cursorJsonPath}.ei-install.tmp`;
205
+ await Bun.write(tmpPath, JSON.stringify(config, null, 2) + "\n");
206
+ const { rename } = await import(/* @vite-ignore */ "fs/promises");
207
+ await rename(tmpPath, cursorJsonPath);
208
+
209
+ console.log(`✓ Installed Ei MCP server to ${cursorJsonPath}`);
210
+ console.log(` Restart Cursor to activate.`);
211
+ }
212
+
213
+ async function installMcpClients(): Promise<void> {
214
+ await installClaudeCode();
215
+ await installCursor();
216
+ }
217
+
182
218
  async function main(): Promise<void> {
183
219
  const args = process.argv.slice(2);
184
220
 
@@ -201,10 +237,15 @@ async function main(): Promise<void> {
201
237
 
202
238
  if (args[0] === "--install") {
203
239
  await installOpenCodeTool();
204
- await installClaudeCodeMcp();
240
+ await installMcpClients();
205
241
  process.exit(0);
206
242
  }
207
243
 
244
+ if (args[0] === "mcp") {
245
+ const { handleMcpCommand } = await import("./cli/mcp.js");
246
+ await handleMcpCommand(args.slice(1));
247
+ process.exit(0);
248
+ }
208
249
 
209
250
  // Handle --id flag: look up entity by ID
210
251
  const idFlagIndex = args.indexOf("--id");
@@ -250,6 +291,7 @@ async function main(): Promise<void> {
250
291
  args: parseableArgs,
251
292
  options: {
252
293
  number: { type: "string", short: "n" },
294
+ recent: { type: "boolean", short: "r" },
253
295
  help: { type: "boolean", short: "h" },
254
296
  },
255
297
  allowPositionals: true,
@@ -267,8 +309,9 @@ async function main(): Promise<void> {
267
309
 
268
310
  const query = parsed.positionals.join(" ").trim();
269
311
  const limit = parsed.values.number ? parseInt(parsed.values.number, 10) : 10;
312
+ const recent = parsed.values.recent === true;
270
313
 
271
- if (!query) {
314
+ if (!query && !recent) {
272
315
  if (targetType) {
273
316
  console.error(`Search text required. Usage: ei ${targetType} "search text"`);
274
317
  } else {
@@ -282,12 +325,14 @@ async function main(): Promise<void> {
282
325
  process.exit(1);
283
326
  }
284
327
 
328
+ const options = { recent };
329
+
285
330
  let result;
286
331
  if (targetType) {
287
332
  const module = await import(`./cli/commands/${targetType}.js`);
288
- result = await module.execute(query, limit);
333
+ result = await module.execute(query, limit, options);
289
334
  } else {
290
- result = await retrieveBalanced(query, limit);
335
+ result = await retrieveBalanced(query, limit, options);
291
336
  }
292
337
 
293
338
  console.log(JSON.stringify(result, null, 2));
@@ -24,7 +24,7 @@ export async function handleDedupCurate(
24
24
  const state = stateManager.getHuman();
25
25
 
26
26
  // Validate entity_type
27
- if (!entity_type || !['fact', 'topic', 'person'].includes(entity_type)) {
27
+ if (!entity_type || !['topic', 'person'].includes(entity_type)) {
28
28
  console.error(`[Dedup] Invalid entity_type: "${entity_type}" (from request data)`, response.request.data);
29
29
  return;
30
30
  }
@@ -206,10 +206,6 @@ export async function handleDedupCurate(
206
206
  last_changed_by: "ei",
207
207
  embedding,
208
208
  // Type-specific fields with defaults
209
- ...(entity_type === 'fact' && {
210
- confidence: addition.confidence ?? 0.5,
211
- validated_date: ''
212
- }),
213
209
  ...((entity_type === 'topic' || entity_type === 'person') && {
214
210
  exposure_current: addition.exposure_current ?? 0.0,
215
211
  exposure_desired: addition.exposure_desired ?? 0.5,
@@ -220,9 +216,7 @@ export async function handleDedupCurate(
220
216
  };
221
217
 
222
218
  // Type-safe cast based on entity_type
223
- if (entity_type === 'fact') {
224
- stateManager.human_fact_upsert(newEntity as Fact);
225
- } else if (entity_type === 'topic') {
219
+ if (entity_type === 'topic') {
226
220
  stateManager.human_topic_upsert(newEntity as Topic);
227
221
  } else if (entity_type === 'person') {
228
222
  stateManager.human_person_upsert(newEntity as Person);
@@ -69,6 +69,7 @@ export async function handleFactFind(response: LLMResponse, state: StateManager)
69
69
  ...existingFact,
70
70
  description: factResult.value,
71
71
  last_updated: now,
72
+ last_mentioned: now,
72
73
  learned_by: existingFact.learned_by ?? context.personaId,
73
74
  last_changed_by: context.personaId,
74
75
  embedding,
@@ -162,6 +162,7 @@ export async function handleTopicUpdate(response: LLMResponse, state: StateManag
162
162
  exposure_current: calculateExposureCurrent(exposureImpact),
163
163
  exposure_desired: result.exposure_desired ?? 0.5,
164
164
  last_updated: now,
165
+ last_mentioned: now,
165
166
  learned_by: isNewItem ? personaId : existingTopic?.learned_by,
166
167
  last_changed_by: personaId,
167
168
  persona_groups: mergeGroups(personaGroup, isNewItem, existingTopic?.persona_groups),
@@ -228,6 +229,7 @@ export async function handlePersonUpdate(response: LLMResponse, state: StateMana
228
229
  exposure_current: calculateExposureCurrent(exposureImpact),
229
230
  exposure_desired: result.exposure_desired ?? 0.5,
230
231
  last_updated: now,
232
+ last_mentioned: now,
231
233
  learned_by: isNewItem ? personaId : existingPerson?.learned_by,
232
234
  last_changed_by: personaId,
233
235
  persona_groups: mergeGroups(personaGroup, isNewItem, existingPerson?.persona_groups),