ei-tui 0.2.0 → 0.3.1
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 +2 -1
- package/src/cli/README.md +83 -2
- package/src/cli/commands/facts.ts +2 -2
- package/src/cli/commands/people.ts +2 -2
- package/src/cli/commands/quotes.ts +2 -2
- package/src/cli/commands/topics.ts +2 -2
- package/src/cli/mcp.ts +94 -0
- package/src/cli/retrieval.ts +64 -6
- package/src/cli.ts +61 -16
- package/src/core/handlers/dedup.ts +2 -8
- package/src/core/handlers/human-extraction.ts +1 -0
- package/src/core/handlers/human-matching.ts +2 -0
- package/src/core/handlers/rewrite.ts +3 -25
- package/src/core/human-data-manager.ts +35 -11
- package/src/core/orchestrators/ceremony.ts +0 -6
- package/src/core/orchestrators/dedup-phase.ts +2 -3
- package/src/core/processor.ts +69 -4
- package/src/core/tools/builtin/read-memory.ts +7 -11
- package/src/core/types/data-items.ts +1 -0
- package/src/core/types/entities.ts +1 -0
- package/src/integrations/claude-code/importer.ts +6 -1
- package/src/integrations/claude-code/types.ts +1 -0
- package/src/integrations/cursor/importer.ts +282 -0
- package/src/integrations/cursor/index.ts +10 -0
- package/src/integrations/cursor/reader.ts +209 -0
- package/src/integrations/cursor/types.ts +140 -0
- package/src/integrations/opencode/importer.ts +7 -1
- package/src/prompts/ceremony/dedup.ts +0 -33
- package/src/prompts/ceremony/rewrite.ts +4 -20
- package/src/prompts/ceremony/types.ts +1 -1
- 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.
|
|
3
|
+
"version": "0.3.1",
|
|
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 #
|
|
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
|
|
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
|
+
}
|
package/src/cli/retrieval.ts
CHANGED
|
@@ -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
|
|
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
|
|
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 --
|
|
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
|
|
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
|
|
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 || !['
|
|
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 === '
|
|
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),
|