ei-tui 0.1.25 → 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/README.md +42 -0
- package/package.json +2 -1
- package/src/README.md +4 -11
- package/src/cli/README.md +87 -7
- 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 +67 -31
- package/src/cli.ts +64 -23
- package/src/core/AGENTS.md +1 -1
- package/src/core/constants/built-in-facts.ts +49 -0
- package/src/core/constants/index.ts +1 -0
- package/src/core/context-utils.ts +0 -1
- package/src/core/embedding-service.ts +8 -0
- package/src/core/handlers/dedup.ts +11 -23
- package/src/core/handlers/heartbeat.ts +2 -3
- package/src/core/handlers/human-extraction.ts +96 -30
- package/src/core/handlers/human-matching.ts +328 -248
- package/src/core/handlers/index.ts +8 -6
- package/src/core/handlers/persona-generation.ts +8 -8
- package/src/core/handlers/rewrite.ts +4 -51
- package/src/core/handlers/utils.ts +23 -1
- package/src/core/heartbeat-manager.ts +2 -4
- package/src/core/human-data-manager.ts +38 -36
- package/src/core/message-manager.ts +10 -10
- package/src/core/orchestrators/ceremony.ts +49 -44
- package/src/core/orchestrators/dedup-phase.ts +2 -4
- package/src/core/orchestrators/human-extraction.ts +351 -207
- package/src/core/orchestrators/index.ts +6 -4
- package/src/core/orchestrators/persona-generation.ts +3 -3
- package/src/core/processor.ts +167 -20
- package/src/core/prompt-context-builder.ts +4 -6
- package/src/core/state/human.ts +1 -26
- package/src/core/state/personas.ts +2 -2
- package/src/core/state-manager.ts +107 -14
- package/src/core/tools/builtin/read-memory.ts +13 -18
- package/src/core/types/data-items.ts +3 -4
- package/src/core/types/entities.ts +7 -4
- package/src/core/types/enums.ts +6 -9
- package/src/core/types/llm.ts +2 -2
- package/src/core/utils/crossFind.ts +2 -5
- package/src/core/utils/event-windows.ts +31 -0
- package/src/integrations/claude-code/importer.ts +14 -5
- package/src/integrations/claude-code/types.ts +3 -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 +14 -4
- package/src/prompts/AGENTS.md +73 -1
- package/src/prompts/ceremony/dedup.ts +0 -33
- package/src/prompts/ceremony/rewrite.ts +6 -41
- package/src/prompts/ceremony/types.ts +4 -4
- package/src/prompts/generation/descriptions.ts +2 -2
- package/src/prompts/generation/types.ts +2 -2
- package/src/prompts/heartbeat/types.ts +2 -2
- package/src/prompts/human/event-scan.ts +122 -0
- package/src/prompts/human/fact-find.ts +106 -0
- package/src/prompts/human/fact-scan.ts +0 -2
- package/src/prompts/human/index.ts +17 -10
- package/src/prompts/human/person-match.ts +65 -0
- package/src/prompts/human/person-scan.ts +52 -59
- package/src/prompts/human/person-update.ts +241 -0
- package/src/prompts/human/topic-match.ts +65 -0
- package/src/prompts/human/topic-scan.ts +51 -71
- package/src/prompts/human/topic-update.ts +295 -0
- package/src/prompts/human/types.ts +63 -40
- package/src/prompts/index.ts +4 -8
- package/src/prompts/persona/topics-update.ts +2 -2
- package/src/prompts/persona/traits.ts +2 -2
- package/src/prompts/persona/types.ts +3 -3
- package/src/prompts/response/index.ts +1 -1
- package/src/prompts/response/sections.ts +9 -12
- package/src/prompts/response/types.ts +2 -3
- package/src/storage/embeddings.ts +1 -1
- package/src/storage/index.ts +1 -0
- package/src/storage/indexed.ts +174 -0
- package/src/storage/merge.ts +67 -2
- package/tui/src/commands/me.tsx +5 -14
- package/tui/src/commands/settings.tsx +15 -0
- package/tui/src/context/ei.tsx +5 -14
- package/tui/src/util/yaml-serializers.ts +76 -33
- package/src/cli/commands/traits.ts +0 -25
- package/src/prompts/human/item-match.ts +0 -74
- package/src/prompts/human/item-update.ts +0 -364
- package/src/prompts/human/trait-scan.ts +0 -115
package/src/cli.ts
CHANGED
|
@@ -20,8 +20,6 @@ const TYPE_ALIASES: Record<string, string> = {
|
|
|
20
20
|
quotes: "quotes",
|
|
21
21
|
fact: "facts",
|
|
22
22
|
facts: "facts",
|
|
23
|
-
trait: "traits",
|
|
24
|
-
traits: "traits",
|
|
25
23
|
person: "people",
|
|
26
24
|
people: "people",
|
|
27
25
|
topic: "topics",
|
|
@@ -38,28 +36,33 @@ Usage:
|
|
|
38
36
|
ei -n 5 "search text" Limit results
|
|
39
37
|
ei <type> "search text" Search a specific data type
|
|
40
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
|
|
41
42
|
ei --id <id> Look up a specific entity by ID
|
|
42
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)
|
|
43
45
|
|
|
44
46
|
Types:
|
|
45
47
|
quote / quotes Quotes from conversation history
|
|
46
48
|
fact / facts Facts about the user
|
|
47
|
-
trait / traits Personality traits
|
|
48
49
|
person / people People from the user's life
|
|
49
50
|
topic / topics Topics of interest
|
|
50
51
|
|
|
51
52
|
Options:
|
|
52
53
|
--number, -n Maximum number of results (default: 10)
|
|
54
|
+
--recent, -r Sort by last_mentioned date (most recent first)
|
|
53
55
|
--id Look up entity by ID (accepts value or stdin)
|
|
54
|
-
--install
|
|
56
|
+
--install Register Ei with OpenCode, Claude Code, and Cursor
|
|
55
57
|
--help, -h Show this help message
|
|
56
58
|
|
|
57
59
|
Examples:
|
|
58
60
|
ei "debugging" # Search everything
|
|
59
61
|
ei -n 5 "API design" # Top 5 across all types
|
|
60
62
|
ei quote "you guessed it" # Search quotes only
|
|
61
|
-
ei
|
|
62
|
-
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
|
|
63
66
|
ei "memory leak" | jq .[0].id | ei --id # Pipe ID from search
|
|
64
67
|
`);
|
|
65
68
|
}
|
|
@@ -71,7 +74,7 @@ function buildOpenCodeToolContent(): string {
|
|
|
71
74
|
'export default tool({',
|
|
72
75
|
' description: [',
|
|
73
76
|
' "Search the user\'s Ei knowledge base \u2014 a persistent memory store built from conversations.",',
|
|
74
|
-
' "Returns facts,
|
|
77
|
+
' "Returns facts, people, topics of interest, and quotes.",',
|
|
75
78
|
' "Use this to recall anything about the user: preferences, relationships, or past discussions.",',
|
|
76
79
|
' "Results include entity IDs that can be passed back with lookup=true to get full detail.",',
|
|
77
80
|
' ].join(" "),',
|
|
@@ -80,10 +83,10 @@ function buildOpenCodeToolContent(): string {
|
|
|
80
83
|
' "Search text, or an entity ID when lookup=true. Supports natural language."',
|
|
81
84
|
' ),',
|
|
82
85
|
' type: tool.schema',
|
|
83
|
-
' .enum(["facts", "
|
|
86
|
+
' .enum(["facts", "people", "topics", "quotes"])',
|
|
84
87
|
' .optional()',
|
|
85
88
|
' .describe(',
|
|
86
|
-
' "Filter to a specific data type. Omit to search all types (balanced across all
|
|
89
|
+
' "Filter to a specific data type. Omit to search all types (balanced across all 4).",',
|
|
87
90
|
' ),',
|
|
88
91
|
' limit: tool.schema',
|
|
89
92
|
' .number()',
|
|
@@ -126,7 +129,7 @@ async function installOpenCodeTool(): Promise<void> {
|
|
|
126
129
|
console.log(` Restart OpenCode to activate.`);
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
async function
|
|
132
|
+
async function installClaudeCode(): Promise<void> {
|
|
130
133
|
const home = process.env.HOME || "~";
|
|
131
134
|
const claudeJsonPath = join(home, ".claude.json");
|
|
132
135
|
|
|
@@ -136,7 +139,7 @@ async function installClaudeCodeMcp(): Promise<void> {
|
|
|
136
139
|
const which = Bun.spawnSync(["which", "claude"], { stdout: "pipe", stderr: "pipe" });
|
|
137
140
|
if (which.exitCode === 0) {
|
|
138
141
|
const result = Bun.spawnSync(
|
|
139
|
-
["claude", "mcp", "add", "--scope", "user", "--transport", "stdio", "ei", "--", "ei"],
|
|
142
|
+
["claude", "mcp", "add", "--scope", "user", "--transport", "stdio", "ei", "--", "ei", "mcp"],
|
|
140
143
|
{ stdout: "pipe", stderr: "pipe" }
|
|
141
144
|
);
|
|
142
145
|
if (result.exitCode === 0) {
|
|
@@ -159,17 +162,11 @@ async function installClaudeCodeMcp(): Promise<void> {
|
|
|
159
162
|
// File doesn't exist or isn't valid JSON — start fresh
|
|
160
163
|
}
|
|
161
164
|
|
|
162
|
-
// Resolve the ei binary: if running as compiled binary, argv[1] is our path;
|
|
163
|
-
// if running as 'bun src/cli.ts', fall back to 'ei' (assumed on PATH after npm install -g)
|
|
164
|
-
const isBunScript = process.argv[1]?.endsWith("/cli.ts") || process.argv[1]?.endsWith("/cli.js");
|
|
165
|
-
const command = isBunScript ? "ei" : (process.argv[1] ?? "ei");
|
|
166
|
-
|
|
167
165
|
const mcpServers = (config.mcpServers ?? {}) as Record<string, unknown>;
|
|
168
166
|
mcpServers["ei"] = {
|
|
169
167
|
type: "stdio",
|
|
170
|
-
command,
|
|
171
|
-
args: [],
|
|
172
|
-
env: {},
|
|
168
|
+
command: "ei",
|
|
169
|
+
args: ["mcp"],
|
|
173
170
|
};
|
|
174
171
|
config.mcpServers = mcpServers;
|
|
175
172
|
|
|
@@ -183,6 +180,41 @@ async function installClaudeCodeMcp(): Promise<void> {
|
|
|
183
180
|
console.log(` Restart Claude Code to activate.`);
|
|
184
181
|
}
|
|
185
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
|
+
|
|
186
218
|
async function main(): Promise<void> {
|
|
187
219
|
const args = process.argv.slice(2);
|
|
188
220
|
|
|
@@ -205,10 +237,15 @@ async function main(): Promise<void> {
|
|
|
205
237
|
|
|
206
238
|
if (args[0] === "--install") {
|
|
207
239
|
await installOpenCodeTool();
|
|
208
|
-
await
|
|
240
|
+
await installMcpClients();
|
|
209
241
|
process.exit(0);
|
|
210
242
|
}
|
|
211
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
|
+
}
|
|
212
249
|
|
|
213
250
|
// Handle --id flag: look up entity by ID
|
|
214
251
|
const idFlagIndex = args.indexOf("--id");
|
|
@@ -254,6 +291,7 @@ async function main(): Promise<void> {
|
|
|
254
291
|
args: parseableArgs,
|
|
255
292
|
options: {
|
|
256
293
|
number: { type: "string", short: "n" },
|
|
294
|
+
recent: { type: "boolean", short: "r" },
|
|
257
295
|
help: { type: "boolean", short: "h" },
|
|
258
296
|
},
|
|
259
297
|
allowPositionals: true,
|
|
@@ -271,8 +309,9 @@ async function main(): Promise<void> {
|
|
|
271
309
|
|
|
272
310
|
const query = parsed.positionals.join(" ").trim();
|
|
273
311
|
const limit = parsed.values.number ? parseInt(parsed.values.number, 10) : 10;
|
|
312
|
+
const recent = parsed.values.recent === true;
|
|
274
313
|
|
|
275
|
-
if (!query) {
|
|
314
|
+
if (!query && !recent) {
|
|
276
315
|
if (targetType) {
|
|
277
316
|
console.error(`Search text required. Usage: ei ${targetType} "search text"`);
|
|
278
317
|
} else {
|
|
@@ -286,12 +325,14 @@ async function main(): Promise<void> {
|
|
|
286
325
|
process.exit(1);
|
|
287
326
|
}
|
|
288
327
|
|
|
328
|
+
const options = { recent };
|
|
329
|
+
|
|
289
330
|
let result;
|
|
290
331
|
if (targetType) {
|
|
291
332
|
const module = await import(`./cli/commands/${targetType}.js`);
|
|
292
|
-
result = await module.execute(query, limit);
|
|
333
|
+
result = await module.execute(query, limit, options);
|
|
293
334
|
} else {
|
|
294
|
-
result = await retrieveBalanced(query, limit);
|
|
335
|
+
result = await retrieveBalanced(query, limit, options);
|
|
295
336
|
}
|
|
296
337
|
|
|
297
338
|
console.log(JSON.stringify(result, null, 2));
|
package/src/core/AGENTS.md
CHANGED
|
@@ -10,7 +10,7 @@ core/
|
|
|
10
10
|
├── state-manager.ts # In-memory state + persistence
|
|
11
11
|
├── queue-processor.ts # LLM request queue with priorities
|
|
12
12
|
├── llm-client.ts # Multi-provider LLM abstraction
|
|
13
|
-
├── types.ts # All core types (source
|
|
13
|
+
├── types.ts # All core types (canonical source — CONTRACTS.md defers to these)
|
|
14
14
|
├── handlers/ # LLM response handlers
|
|
15
15
|
├── orchestrators/ # Multi-step workflows
|
|
16
16
|
├── personas/ # Persona loading logic
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in biographical fact categories that Ei tracks.
|
|
3
|
+
*
|
|
4
|
+
* BUILT_IN_FACTS: Array of fact objects (name field only) for iteration/display.
|
|
5
|
+
* BUILT_IN_FACT_NAMES: Set<string> for O(1) lookup (is this fact built-in?).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export const BUILT_IN_FACTS: { name: string }[] = [
|
|
9
|
+
// Core Identity
|
|
10
|
+
{ name: "Full Name" },
|
|
11
|
+
{ name: "Nickname/Preferred Name" },
|
|
12
|
+
{ name: "Birthday" },
|
|
13
|
+
{ name: "Birthplace" },
|
|
14
|
+
{ name: "Hometown" },
|
|
15
|
+
{ name: "Current Location" },
|
|
16
|
+
|
|
17
|
+
// Professional
|
|
18
|
+
{ name: "Current Job Title" },
|
|
19
|
+
{ name: "Current Employer" },
|
|
20
|
+
{ name: "Industry/Field" },
|
|
21
|
+
{ name: "Years of Experience" },
|
|
22
|
+
|
|
23
|
+
// Personal
|
|
24
|
+
{ name: "Marital Status" },
|
|
25
|
+
{ name: "Spouse Name" },
|
|
26
|
+
{ name: "Spouse Birthday" },
|
|
27
|
+
{ name: "Date of Marriage" },
|
|
28
|
+
{ name: "Children" },
|
|
29
|
+
{ name: "Parents" },
|
|
30
|
+
{ name: "Gender" },
|
|
31
|
+
{ name: "Pronouns" },
|
|
32
|
+
{ name: "Eye Color" },
|
|
33
|
+
{ name: "Hair Color" },
|
|
34
|
+
{ name: "Height" },
|
|
35
|
+
{ name: "Weight" },
|
|
36
|
+
|
|
37
|
+
// Background
|
|
38
|
+
{ name: "Nationality/Citizenship" },
|
|
39
|
+
{ name: "Languages Spoken" },
|
|
40
|
+
{ name: "Education Level" },
|
|
41
|
+
{ name: "School/University" },
|
|
42
|
+
{ name: "Field of Study" },
|
|
43
|
+
{ name: "Military Service" },
|
|
44
|
+
{ name: "Religious Affiliation" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
export const BUILT_IN_FACT_NAMES: Set<string> = new Set(
|
|
48
|
+
BUILT_IN_FACTS.map((f) => f.name)
|
|
49
|
+
);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./built-in-facts";
|
|
@@ -49,7 +49,6 @@ export function stripHumanEmbeddings(human: HumanEntity): HumanEntity {
|
|
|
49
49
|
return {
|
|
50
50
|
...human,
|
|
51
51
|
facts: (human.facts ?? []).map(stripDataItemEmbedding),
|
|
52
|
-
traits: (human.traits ?? []).map(stripDataItemEmbedding),
|
|
53
52
|
topics: (human.topics ?? []).map(stripDataItemEmbedding),
|
|
54
53
|
people: (human.people ?? []).map(stripDataItemEmbedding),
|
|
55
54
|
quotes: (human.quotes ?? []).map(stripQuoteEmbedding),
|
|
@@ -54,6 +54,14 @@ export function getItemEmbeddingText(item: { name: string; description?: string
|
|
|
54
54
|
return item.name;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
export function getTopicEmbeddingText(topic: { name: string; category?: string; description?: string }): string {
|
|
58
|
+
return [topic.name, topic.category, topic.description].filter(Boolean).join(' - ');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getPersonEmbeddingText(person: { name: string; relationship?: string; description?: string }): string {
|
|
62
|
+
return [person.name, person.relationship, person.description].filter(Boolean).join(' - ');
|
|
63
|
+
}
|
|
64
|
+
|
|
57
65
|
export function needsEmbeddingUpdate(
|
|
58
66
|
existing: { name: string; description?: string } | undefined,
|
|
59
67
|
incoming: { name: string; description?: string }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { StateManager } from "../state-manager.js";
|
|
2
2
|
import { LLMResponse } from "../types.js";
|
|
3
3
|
import type { DedupResult } from "../../prompts/ceremony/types.js";
|
|
4
|
-
import type { DataItemType, Fact,
|
|
4
|
+
import type { DataItemType, Fact, Topic, Person, Quote } from "../types/data-items.js";
|
|
5
5
|
import { getEmbeddingService } from "../embedding-service.js";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -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
|
}
|
|
@@ -50,9 +50,8 @@ export async function handleDedupCurate(
|
|
|
50
50
|
console.log(`[Dedup] Processing cluster: ${decisions.update.length} updates, ${decisions.remove.length} removals, ${decisions.add.length} additions`);
|
|
51
51
|
|
|
52
52
|
// Map entity_type to pluralized state property name
|
|
53
|
-
const pluralMap: Record<
|
|
53
|
+
const pluralMap: Record<string, 'facts' | 'topics' | 'people'> = {
|
|
54
54
|
fact: 'facts',
|
|
55
|
-
trait: 'traits',
|
|
56
55
|
topic: 'topics',
|
|
57
56
|
person: 'people'
|
|
58
57
|
};
|
|
@@ -65,15 +64,14 @@ export async function handleDedupCurate(
|
|
|
65
64
|
entity_ids,
|
|
66
65
|
stateKeys: Object.keys(state),
|
|
67
66
|
factsExists: !!state.facts,
|
|
68
|
-
traitsExists: !!state.traits,
|
|
69
67
|
topicsExists: !!state.topics,
|
|
70
68
|
peopleExists: !!state.people
|
|
71
69
|
});
|
|
72
70
|
return;
|
|
73
71
|
}
|
|
74
72
|
const entities = entity_ids
|
|
75
|
-
.map((id: string) => entityList.find((e: Fact |
|
|
76
|
-
.filter((e: Fact |
|
|
73
|
+
.map((id: string) => entityList.find((e: Fact | Topic | Person) => e.id === id))
|
|
74
|
+
.filter((e: Fact | Topic | Person | undefined): e is (Fact | Topic | Person) => e !== undefined);
|
|
77
75
|
|
|
78
76
|
if (entities.length === 0) {
|
|
79
77
|
console.warn(`[Dedup] No entities found for cluster (already merged?)`);
|
|
@@ -109,7 +107,7 @@ export async function handleDedupCurate(
|
|
|
109
107
|
// =========================================================================
|
|
110
108
|
|
|
111
109
|
for (const update of decisions.update) {
|
|
112
|
-
const entity = entityList.find((e: Fact |
|
|
110
|
+
const entity = entityList.find((e: Fact | Topic | Person) => e.id === update.id);
|
|
113
111
|
|
|
114
112
|
if (!entity) {
|
|
115
113
|
console.warn(`[Dedup] Entity ${update.id} not found (already merged?)`);
|
|
@@ -148,8 +146,6 @@ export async function handleDedupCurate(
|
|
|
148
146
|
// Type-safe cast based on entity_type
|
|
149
147
|
if (entity_type === 'fact') {
|
|
150
148
|
stateManager.human_fact_upsert(updatedEntity as Fact);
|
|
151
|
-
} else if (entity_type === 'trait') {
|
|
152
|
-
stateManager.human_trait_upsert(updatedEntity as Trait);
|
|
153
149
|
} else if (entity_type === 'topic') {
|
|
154
150
|
stateManager.human_topic_upsert(updatedEntity as Topic);
|
|
155
151
|
} else if (entity_type === 'person') {
|
|
@@ -163,7 +159,7 @@ export async function handleDedupCurate(
|
|
|
163
159
|
// =========================================================================
|
|
164
160
|
|
|
165
161
|
for (const removal of decisions.remove) {
|
|
166
|
-
const entity = entityList.find((e: Fact |
|
|
162
|
+
const entity = entityList.find((e: Fact | Topic | Person) => e.id === removal.to_be_removed);
|
|
167
163
|
|
|
168
164
|
if (!entity) {
|
|
169
165
|
console.warn(`[Dedup] Entity ${removal.to_be_removed} already deleted`);
|
|
@@ -172,7 +168,7 @@ export async function handleDedupCurate(
|
|
|
172
168
|
|
|
173
169
|
// Remove via StateManager (also cleans up quote references)
|
|
174
170
|
const removeMethod = `human_${entity_type}_remove` as
|
|
175
|
-
'human_fact_remove' | '
|
|
171
|
+
'human_fact_remove' | 'human_topic_remove' | 'human_person_remove';
|
|
176
172
|
|
|
177
173
|
const removed = stateManager[removeMethod](removal.to_be_removed);
|
|
178
174
|
if (removed) {
|
|
@@ -206,14 +202,10 @@ export async function handleDedupCurate(
|
|
|
206
202
|
description: addition.description,
|
|
207
203
|
sentiment: addition.sentiment ?? 0.0,
|
|
208
204
|
last_updated: new Date().toISOString(),
|
|
205
|
+
learned_by: "ei",
|
|
206
|
+
last_changed_by: "ei",
|
|
209
207
|
embedding,
|
|
210
208
|
// Type-specific fields with defaults
|
|
211
|
-
...(entity_type === 'trait' && { strength: addition.strength ?? 0.5 }),
|
|
212
|
-
...(entity_type === 'fact' && {
|
|
213
|
-
confidence: addition.confidence ?? 0.5,
|
|
214
|
-
validated: 'unknown' as import("../types/enums.js").ValidationLevel,
|
|
215
|
-
validated_date: ''
|
|
216
|
-
}),
|
|
217
209
|
...((entity_type === 'topic' || entity_type === 'person') && {
|
|
218
210
|
exposure_current: addition.exposure_current ?? 0.0,
|
|
219
211
|
exposure_desired: addition.exposure_desired ?? 0.5,
|
|
@@ -224,11 +216,7 @@ export async function handleDedupCurate(
|
|
|
224
216
|
};
|
|
225
217
|
|
|
226
218
|
// Type-safe cast based on entity_type
|
|
227
|
-
if (entity_type === '
|
|
228
|
-
stateManager.human_fact_upsert(newEntity as Fact);
|
|
229
|
-
} else if (entity_type === 'trait') {
|
|
230
|
-
stateManager.human_trait_upsert(newEntity as Trait);
|
|
231
|
-
} else if (entity_type === 'topic') {
|
|
219
|
+
if (entity_type === 'topic') {
|
|
232
220
|
stateManager.human_topic_upsert(newEntity as Topic);
|
|
233
221
|
} else if (entity_type === 'person') {
|
|
234
222
|
stateManager.human_person_upsert(newEntity as Person);
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ContextStatus,
|
|
3
3
|
LLMNextStep,
|
|
4
|
-
ValidationLevel,
|
|
5
4
|
type LLMResponse,
|
|
6
5
|
type Message,
|
|
7
6
|
} from "../types.js";
|
|
@@ -77,13 +76,13 @@ export function handleEiHeartbeat(response: LLMResponse, state: StateManager): v
|
|
|
77
76
|
timestamp: now,
|
|
78
77
|
read: false,
|
|
79
78
|
context_status: ContextStatus.Default,
|
|
80
|
-
f: true,
|
|
79
|
+
f: true, t: true, p: true,
|
|
81
80
|
});
|
|
82
81
|
|
|
83
82
|
if (found.type === "fact") {
|
|
84
83
|
const factsNav = isTUI ? "using /me facts" : "using \u2630 \u2192 My Data";
|
|
85
84
|
sendMessage(`Another persona updated a fact called "${found.name}" to "${found.description}". If that's right, you can lock it from further changes by ${factsNav}.`);
|
|
86
|
-
state.human_fact_upsert({ ...found,
|
|
85
|
+
state.human_fact_upsert({ ...found, validated_date: now });
|
|
87
86
|
console.log(`[handleEiHeartbeat] Notified about fact "${found.name}"`);
|
|
88
87
|
return;
|
|
89
88
|
}
|
|
@@ -1,57 +1,92 @@
|
|
|
1
|
-
import type { LLMResponse } from "../types.js";
|
|
1
|
+
import type { LLMResponse, Fact } from "../types.js";
|
|
2
2
|
import type { StateManager } from "../state-manager.js";
|
|
3
3
|
import type {
|
|
4
|
-
|
|
5
|
-
TraitScanResult,
|
|
4
|
+
FactFindResult,
|
|
6
5
|
TopicScanResult,
|
|
7
6
|
PersonScanResult,
|
|
7
|
+
TopicScanCandidate,
|
|
8
8
|
} from "../../prompts/human/types.js";
|
|
9
|
-
import {
|
|
9
|
+
import { queueTopicMatch, queuePersonMatch, type ExtractionContext } from "../orchestrators/index.js";
|
|
10
10
|
import { markMessagesExtracted } from "./utils.js";
|
|
11
|
+
import { BUILT_IN_FACT_NAMES } from "../constants/built-in-facts.js";
|
|
12
|
+
import { getEmbeddingService, getItemEmbeddingText } from "../embedding-service.js";
|
|
11
13
|
|
|
12
|
-
export async function
|
|
13
|
-
const result = response.parsed as
|
|
14
|
+
export async function handleFactFind(response: LLMResponse, state: StateManager): Promise<void> {
|
|
15
|
+
const result = response.parsed as FactFindResult | undefined;
|
|
14
16
|
|
|
15
17
|
// Mark messages as scanned regardless of whether facts were found
|
|
16
18
|
markMessagesExtracted(response, state, "f");
|
|
17
19
|
|
|
18
20
|
if (!result?.facts || !Array.isArray(result.facts)) {
|
|
19
|
-
console.log("[
|
|
21
|
+
console.log("[handleFactFind] No facts detected or invalid result");
|
|
20
22
|
return;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
const context = response.request.data as unknown as ExtractionContext;
|
|
24
26
|
if (!context?.personaId) return;
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
console.log(`[handleHumanFactScan] Queued ${result.facts.length} fact(s) for matching`);
|
|
30
|
-
}
|
|
28
|
+
const human = state.getHuman();
|
|
29
|
+
const now = new Date().toISOString();
|
|
30
|
+
let upsertCount = 0;
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
console.log("[handleHumanTraitScan] No traits detected or invalid result");
|
|
39
|
-
return;
|
|
40
|
-
}
|
|
32
|
+
for (const factResult of result.facts) {
|
|
33
|
+
// Only upsert facts that match a built-in name
|
|
34
|
+
if (!BUILT_IN_FACT_NAMES.has(factResult.name)) {
|
|
35
|
+
console.log(`[handleFactFind] Skipping non-built-in fact: "${factResult.name}"`);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
41
38
|
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
// Find the existing fact in state
|
|
40
|
+
const existingFact = human.facts.find(f => f.name === factResult.name);
|
|
41
|
+
if (!existingFact) {
|
|
42
|
+
console.log(`[handleFactFind] Skipping unknown fact: "${factResult.name}"`);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
// Skip facts that already have descriptions (only fill empty ones)
|
|
47
|
+
if (existingFact.description && existingFact.description !== "") {
|
|
48
|
+
console.log(`[handleFactFind] Skipping fact with existing description: "${factResult.name}"`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Skip if the LLM returned a null/empty value — don't store null descriptions
|
|
53
|
+
if (!factResult.value) {
|
|
54
|
+
console.log(`[handleFactFind] Skipping fact with null/empty value: "${factResult.name}"`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Compute embedding for the updated fact
|
|
59
|
+
let embedding: number[] | undefined;
|
|
60
|
+
try {
|
|
61
|
+
const embeddingService = getEmbeddingService();
|
|
62
|
+
const text = getItemEmbeddingText({ name: factResult.name, description: factResult.value });
|
|
63
|
+
embedding = await embeddingService.embed(text);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
console.warn(`[handleFactFind] Failed to compute embedding for fact "${factResult.name}":`, err);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const updatedFact: Fact = {
|
|
69
|
+
...existingFact,
|
|
70
|
+
description: factResult.value,
|
|
71
|
+
last_updated: now,
|
|
72
|
+
last_mentioned: now,
|
|
73
|
+
learned_by: existingFact.learned_by ?? context.personaId,
|
|
74
|
+
last_changed_by: context.personaId,
|
|
75
|
+
embedding,
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
state.human_fact_upsert(updatedFact);
|
|
79
|
+
upsertCount++;
|
|
47
80
|
}
|
|
48
|
-
|
|
81
|
+
|
|
82
|
+
console.log(`[handleFactFind] Upserted ${upsertCount} fact(s)`);
|
|
49
83
|
}
|
|
50
84
|
|
|
85
|
+
|
|
51
86
|
export async function handleHumanTopicScan(response: LLMResponse, state: StateManager): Promise<void> {
|
|
52
87
|
const result = response.parsed as TopicScanResult | undefined;
|
|
53
88
|
|
|
54
|
-
markMessagesExtracted(response, state, "
|
|
89
|
+
markMessagesExtracted(response, state, "t");
|
|
55
90
|
|
|
56
91
|
if (!result?.topics || !Array.isArray(result.topics)) {
|
|
57
92
|
console.log("[handleHumanTopicScan] No topics detected or invalid result");
|
|
@@ -61,8 +96,9 @@ export async function handleHumanTopicScan(response: LLMResponse, state: StateMa
|
|
|
61
96
|
const context = response.request.data as unknown as ExtractionContext;
|
|
62
97
|
if (!context?.personaId) return;
|
|
63
98
|
|
|
99
|
+
const extractionModel = (response.request.data as Record<string, unknown>).extraction_model as string | undefined;
|
|
64
100
|
for (const candidate of result.topics) {
|
|
65
|
-
await
|
|
101
|
+
await queueTopicMatch(candidate, context, state, extractionModel);
|
|
66
102
|
}
|
|
67
103
|
console.log(`[handleHumanTopicScan] Queued ${result.topics.length} topic(s) for matching`);
|
|
68
104
|
}
|
|
@@ -70,7 +106,7 @@ export async function handleHumanTopicScan(response: LLMResponse, state: StateMa
|
|
|
70
106
|
export async function handleHumanPersonScan(response: LLMResponse, state: StateManager): Promise<void> {
|
|
71
107
|
const result = response.parsed as PersonScanResult | undefined;
|
|
72
108
|
|
|
73
|
-
markMessagesExtracted(response, state, "
|
|
109
|
+
markMessagesExtracted(response, state, "p");
|
|
74
110
|
|
|
75
111
|
if (!result?.people || !Array.isArray(result.people)) {
|
|
76
112
|
console.log("[handleHumanPersonScan] No people detected or invalid result");
|
|
@@ -80,8 +116,38 @@ export async function handleHumanPersonScan(response: LLMResponse, state: StateM
|
|
|
80
116
|
const context = response.request.data as unknown as ExtractionContext;
|
|
81
117
|
if (!context?.personaId) return;
|
|
82
118
|
|
|
119
|
+
const extractionModel = (response.request.data as Record<string, unknown>).extraction_model as string | undefined;
|
|
83
120
|
for (const candidate of result.people) {
|
|
84
|
-
await
|
|
121
|
+
await queuePersonMatch(candidate, context, state, extractionModel);
|
|
85
122
|
}
|
|
86
123
|
console.log(`[handleHumanPersonScan] Queued ${result.people.length} person(s) for matching`);
|
|
87
124
|
}
|
|
125
|
+
|
|
126
|
+
export async function handleEventScan(response: LLMResponse, state: StateManager): Promise<void> {
|
|
127
|
+
markMessagesExtracted(response, state, "e");
|
|
128
|
+
|
|
129
|
+
const result = response.parsed as { events?: Array<{ name: string; description: string; reason: string }> } | undefined;
|
|
130
|
+
|
|
131
|
+
if (!result?.events || !Array.isArray(result.events) || result.events.length === 0) {
|
|
132
|
+
console.log("[handleEventScan] No epic events detected");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const context = response.request.data as unknown as ExtractionContext;
|
|
137
|
+
if (!context?.personaId) return;
|
|
138
|
+
|
|
139
|
+
const extractionModel = (response.request.data as Record<string, unknown>).extraction_model as string | undefined;
|
|
140
|
+
|
|
141
|
+
for (const event of result.events) {
|
|
142
|
+
const candidate: TopicScanCandidate = {
|
|
143
|
+
name: event.name,
|
|
144
|
+
description: event.description,
|
|
145
|
+
category: "Event",
|
|
146
|
+
reason: event.reason,
|
|
147
|
+
};
|
|
148
|
+
await queueTopicMatch(candidate, context, state, extractionModel);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log(`[handleEventScan] Queued ${result.events.length} event(s) for matching`);
|
|
152
|
+
}
|
|
153
|
+
|