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.
Files changed (88) hide show
  1. package/README.md +42 -0
  2. package/package.json +2 -1
  3. package/src/README.md +4 -11
  4. package/src/cli/README.md +87 -7
  5. package/src/cli/commands/facts.ts +2 -2
  6. package/src/cli/commands/people.ts +2 -2
  7. package/src/cli/commands/quotes.ts +2 -2
  8. package/src/cli/commands/topics.ts +2 -2
  9. package/src/cli/mcp.ts +94 -0
  10. package/src/cli/retrieval.ts +67 -31
  11. package/src/cli.ts +64 -23
  12. package/src/core/AGENTS.md +1 -1
  13. package/src/core/constants/built-in-facts.ts +49 -0
  14. package/src/core/constants/index.ts +1 -0
  15. package/src/core/context-utils.ts +0 -1
  16. package/src/core/embedding-service.ts +8 -0
  17. package/src/core/handlers/dedup.ts +11 -23
  18. package/src/core/handlers/heartbeat.ts +2 -3
  19. package/src/core/handlers/human-extraction.ts +96 -30
  20. package/src/core/handlers/human-matching.ts +328 -248
  21. package/src/core/handlers/index.ts +8 -6
  22. package/src/core/handlers/persona-generation.ts +8 -8
  23. package/src/core/handlers/rewrite.ts +4 -51
  24. package/src/core/handlers/utils.ts +23 -1
  25. package/src/core/heartbeat-manager.ts +2 -4
  26. package/src/core/human-data-manager.ts +38 -36
  27. package/src/core/message-manager.ts +10 -10
  28. package/src/core/orchestrators/ceremony.ts +49 -44
  29. package/src/core/orchestrators/dedup-phase.ts +2 -4
  30. package/src/core/orchestrators/human-extraction.ts +351 -207
  31. package/src/core/orchestrators/index.ts +6 -4
  32. package/src/core/orchestrators/persona-generation.ts +3 -3
  33. package/src/core/processor.ts +167 -20
  34. package/src/core/prompt-context-builder.ts +4 -6
  35. package/src/core/state/human.ts +1 -26
  36. package/src/core/state/personas.ts +2 -2
  37. package/src/core/state-manager.ts +107 -14
  38. package/src/core/tools/builtin/read-memory.ts +13 -18
  39. package/src/core/types/data-items.ts +3 -4
  40. package/src/core/types/entities.ts +7 -4
  41. package/src/core/types/enums.ts +6 -9
  42. package/src/core/types/llm.ts +2 -2
  43. package/src/core/utils/crossFind.ts +2 -5
  44. package/src/core/utils/event-windows.ts +31 -0
  45. package/src/integrations/claude-code/importer.ts +14 -5
  46. package/src/integrations/claude-code/types.ts +3 -0
  47. package/src/integrations/cursor/importer.ts +282 -0
  48. package/src/integrations/cursor/index.ts +10 -0
  49. package/src/integrations/cursor/reader.ts +209 -0
  50. package/src/integrations/cursor/types.ts +140 -0
  51. package/src/integrations/opencode/importer.ts +14 -4
  52. package/src/prompts/AGENTS.md +73 -1
  53. package/src/prompts/ceremony/dedup.ts +0 -33
  54. package/src/prompts/ceremony/rewrite.ts +6 -41
  55. package/src/prompts/ceremony/types.ts +4 -4
  56. package/src/prompts/generation/descriptions.ts +2 -2
  57. package/src/prompts/generation/types.ts +2 -2
  58. package/src/prompts/heartbeat/types.ts +2 -2
  59. package/src/prompts/human/event-scan.ts +122 -0
  60. package/src/prompts/human/fact-find.ts +106 -0
  61. package/src/prompts/human/fact-scan.ts +0 -2
  62. package/src/prompts/human/index.ts +17 -10
  63. package/src/prompts/human/person-match.ts +65 -0
  64. package/src/prompts/human/person-scan.ts +52 -59
  65. package/src/prompts/human/person-update.ts +241 -0
  66. package/src/prompts/human/topic-match.ts +65 -0
  67. package/src/prompts/human/topic-scan.ts +51 -71
  68. package/src/prompts/human/topic-update.ts +295 -0
  69. package/src/prompts/human/types.ts +63 -40
  70. package/src/prompts/index.ts +4 -8
  71. package/src/prompts/persona/topics-update.ts +2 -2
  72. package/src/prompts/persona/traits.ts +2 -2
  73. package/src/prompts/persona/types.ts +3 -3
  74. package/src/prompts/response/index.ts +1 -1
  75. package/src/prompts/response/sections.ts +9 -12
  76. package/src/prompts/response/types.ts +2 -3
  77. package/src/storage/embeddings.ts +1 -1
  78. package/src/storage/index.ts +1 -0
  79. package/src/storage/indexed.ts +174 -0
  80. package/src/storage/merge.ts +67 -2
  81. package/tui/src/commands/me.tsx +5 -14
  82. package/tui/src/commands/settings.tsx +15 -0
  83. package/tui/src/context/ei.tsx +5 -14
  84. package/tui/src/util/yaml-serializers.ts +76 -33
  85. package/src/cli/commands/traits.ts +0 -25
  86. package/src/prompts/human/item-match.ts +0 -74
  87. package/src/prompts/human/item-update.ts +0 -364
  88. package/src/prompts/human/trait-scan.ts +0 -115
@@ -7,7 +7,7 @@ import type {
7
7
  OpenCodeSettings,
8
8
  BackupConfig,
9
9
  Fact,
10
- Trait,
10
+ PersonaTrait,
11
11
  Topic,
12
12
  Person,
13
13
  PersonaTopic,
@@ -23,15 +23,13 @@ import type {
23
23
  } from "../../../src/core/types.js";
24
24
  import { ContextStatus } from "../../../src/core/types.js";
25
25
  import type { ClaudeCodeSettings } from "../../../src/integrations/claude-code/types.js";
26
+ import type { CursorSettings } from "../../../src/integrations/cursor/types.js";
27
+ import { BUILT_IN_FACT_NAMES } from "../../../src/core/constants/built-in-facts.js";
26
28
 
27
29
  // =============================================================================
28
30
  // TYPES FOR YAML EDITING
29
31
  // =============================================================================
30
32
 
31
- interface EditableTrait extends Trait {
32
- _delete?: boolean;
33
- }
34
-
35
33
  interface EditableTopic extends Topic {
36
34
  _delete?: boolean;
37
35
  }
@@ -64,7 +62,6 @@ interface EditablePersonaData {
64
62
 
65
63
  interface EditableHumanData {
66
64
  facts: EditableFact[];
67
- traits: EditableTrait[];
68
65
  topics: EditableTopic[];
69
66
  people: EditablePerson[];
70
67
  }
@@ -190,7 +187,7 @@ export function newPersonaFromYAML(yamlContent: string, allTools?: ToolDefinitio
190
187
  t.name === PLACEHOLDER_TRAIT.name &&
191
188
  t.description === PLACEHOLDER_TRAIT.description;
192
189
 
193
- const traits: Trait[] = [];
190
+ const traits: PersonaTrait[] = [];
194
191
  for (const t of data.traits ?? []) {
195
192
  if (isTraitPlaceholder(t)) {
196
193
  continue;
@@ -313,7 +310,7 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity, al
313
310
  t.name === PLACEHOLDER_TRAIT.name &&
314
311
  t.description === PLACEHOLDER_TRAIT.description;
315
312
 
316
- const traits: Trait[] = [];
313
+ const traits: PersonaTrait[] = [];
317
314
  for (const t of data.traits ?? []) {
318
315
  if (isTraitPlaceholder(t)) {
319
316
  continue;
@@ -406,7 +403,6 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity, al
406
403
  export function humanToYAML(human: HumanEntity, personaLookup?: Map<string, string>): string {
407
404
  const data: EditableHumanData = {
408
405
  facts: human.facts.map(f => ({ ...f, _delete: false })),
409
- traits: human.traits.map(t => ({ ...t, _delete: false })),
410
406
  topics: human.topics.map(t => ({ ...t, _delete: false })),
411
407
  people: human.people.map(p => ({ ...p, _delete: false })),
412
408
  };
@@ -414,27 +410,28 @@ export function humanToYAML(human: HumanEntity, personaLookup?: Map<string, stri
414
410
  return YAML.stringify(data, {
415
411
  lineWidth: 0,
416
412
  })
417
- .replace(/^(\s+validated:\s+\S+)$/mg, '$1 # none | ei | human')
418
413
  .replace(/^(\s+)(learned_by: )(.+)$/mg, (_, indent, key, val) => {
419
414
  const trimmed = val.trim();
420
415
  const displayName = personaLookup?.get(trimmed) ?? trimmed;
421
416
  return `${indent}# [read-only] ${key}${displayName}`;
422
417
  })
423
- .replace(/^(\s+)(last_changed_by: .+)$/mg, '$1# [read-only] $2');
418
+ .replace(/^(\s+)(last_changed_by: )(.+)$/mg, (_, indent, key, val) => {
419
+ const trimmed = val.trim();
420
+ const displayName = personaLookup?.get(trimmed) ?? trimmed;
421
+ return `${indent}# [read-only] ${key}${displayName}`;
422
+ });
424
423
  }
425
424
 
426
425
  export interface HumanYAMLResult {
427
426
  facts: Fact[];
428
- traits: Trait[];
429
427
  topics: Topic[];
430
428
  people: Person[];
431
429
  deletedFactIds: string[];
432
- deletedTraitIds: string[];
433
430
  deletedTopicIds: string[];
434
431
  deletedPersonIds: string[];
435
432
  }
436
433
 
437
- export function humanFromYAML(yamlContent: string): HumanYAMLResult {
434
+ export function humanFromYAML(yamlContent: string, original?: HumanEntity): HumanYAMLResult {
438
435
  // Strip read-only comment lines before parsing so users can't accidentally corrupt them
439
436
  const stripped = yamlContent
440
437
  .split('\n')
@@ -443,36 +440,32 @@ export function humanFromYAML(yamlContent: string): HumanYAMLResult {
443
440
  const data = YAML.parse(stripped) as EditableHumanData;
444
441
 
445
442
  const deletedFactIds: string[] = [];
446
- const deletedTraitIds: string[] = [];
447
443
  const deletedTopicIds: string[] = [];
448
444
  const deletedPersonIds: string[] = [];
449
445
 
450
446
  const facts: Fact[] = [];
451
447
  for (const f of data.facts ?? []) {
452
- if (f._delete) {
448
+ if (f._delete && !BUILT_IN_FACT_NAMES.has(f.name)) {
453
449
  deletedFactIds.push(f.id);
454
450
  } else {
455
- const { _delete, ...fact } = f;
451
+ const { _delete, ...parsed } = f;
452
+ const originalFact = original?.facts.find(of => of.id === parsed.id);
453
+ const fact = originalFact ? { ...originalFact, ...parsed } : parsed;
454
+ if (fact.description && !fact.validated_date) {
455
+ fact.validated_date = new Date().toISOString();
456
+ }
456
457
  facts.push(fact);
457
458
  }
458
459
  }
459
460
 
460
- const traits: Trait[] = [];
461
- for (const t of data.traits ?? []) {
462
- if (t._delete) {
463
- deletedTraitIds.push(t.id);
464
- } else {
465
- const { _delete, ...trait } = t;
466
- traits.push(trait);
467
- }
468
- }
469
-
470
461
  const topics: Topic[] = [];
471
462
  for (const t of data.topics ?? []) {
472
463
  if (t._delete) {
473
464
  deletedTopicIds.push(t.id);
474
465
  } else {
475
- const { _delete, ...topic } = t;
466
+ const { _delete, ...parsed } = t;
467
+ const originalTopic = original?.topics.find(ot => ot.id === parsed.id);
468
+ const topic = originalTopic ? { ...originalTopic, ...parsed } : parsed;
476
469
  topics.push(topic);
477
470
  }
478
471
  }
@@ -482,18 +475,18 @@ export function humanFromYAML(yamlContent: string): HumanYAMLResult {
482
475
  if (p._delete) {
483
476
  deletedPersonIds.push(p.id);
484
477
  } else {
485
- const { _delete, ...person } = p;
478
+ const { _delete, ...parsed } = p;
479
+ const originalPerson = original?.people.find(op => op.id === parsed.id);
480
+ const person = originalPerson ? { ...originalPerson, ...parsed } : parsed;
486
481
  people.push(person);
487
482
  }
488
483
  }
489
484
 
490
485
  return {
491
486
  facts,
492
- traits,
493
487
  topics,
494
488
  people,
495
489
  deletedFactIds,
496
- deletedTraitIds,
497
490
  deletedTopicIds,
498
491
  deletedPersonIds,
499
492
  };
@@ -520,11 +513,22 @@ interface EditableSettingsData {
520
513
  polling_interval_ms?: number | null;
521
514
  last_sync?: string | null;
522
515
  extraction_point?: string | null;
516
+ extraction_model?: string | null;
517
+ extraction_token_limit?: string | number | null;
523
518
  };
524
519
  claudeCode?: {
525
520
  integration?: boolean | null;
526
521
  polling_interval_ms?: number | null;
527
522
  last_sync?: string | null;
523
+ extraction_point?: string | null;
524
+ extraction_model?: string | null;
525
+ extraction_token_limit?: string | number | null;
526
+ };
527
+ cursor?: {
528
+ integration?: boolean | null;
529
+ polling_interval_ms?: number | null;
530
+ last_sync?: string | null;
531
+ extraction_point?: string | null;
528
532
  };
529
533
  backup?: {
530
534
  enabled?: boolean | null;
@@ -550,13 +554,24 @@ export function settingsToYAML(settings: HumanSettings | undefined): string {
550
554
  opencode: {
551
555
  integration: settings?.opencode?.integration ?? false,
552
556
  polling_interval_ms: settings?.opencode?.polling_interval_ms ?? 1800000,
557
+ extraction_model: settings?.opencode?.extraction_model ?? 'default',
558
+ extraction_token_limit: settings?.opencode?.extraction_token_limit ?? 'default',
553
559
  last_sync: settings?.opencode?.last_sync ?? null,
554
560
  extraction_point: settings?.opencode?.extraction_point ?? null,
555
561
  },
556
562
  claudeCode: {
557
563
  integration: settings?.claudeCode?.integration ?? false,
558
564
  polling_interval_ms: settings?.claudeCode?.polling_interval_ms ?? 1800000,
565
+ extraction_model: settings?.claudeCode?.extraction_model ?? 'default',
566
+ extraction_token_limit: settings?.claudeCode?.extraction_token_limit ?? 'default',
559
567
  last_sync: settings?.claudeCode?.last_sync ?? null,
568
+ extraction_point: settings?.claudeCode?.extraction_point ?? null,
569
+ },
570
+ cursor: {
571
+ integration: settings?.cursor?.integration ?? false,
572
+ polling_interval_ms: settings?.cursor?.polling_interval_ms ?? 1800000,
573
+ last_sync: settings?.cursor?.last_sync ?? null,
574
+ extraction_point: settings?.cursor?.extraction_point ?? null,
560
575
  },
561
576
  backup: {
562
577
  enabled: settings?.backup?.enabled ?? false,
@@ -569,7 +584,9 @@ export function settingsToYAML(settings: HumanSettings | undefined): string {
569
584
  lineWidth: 0,
570
585
  })
571
586
  .replace(/^(\s+)(last_sync: .+)$/mg, '$1# [read-only] $2')
572
- .replace(/^(\s+)(extraction_point: .+)$/mg, '$1# [read-only] $2');
587
+ .replace(/^(\s+)(extraction_point: .+)$/mg, '$1# [read-only] $2')
588
+ .replace(/^(\s+)(extraction_model: .+)$/mg, '$1$2 # e.g. Anthropic:claude-haiku-4-5')
589
+ .replace(/^(\s+)(extraction_token_limit: .+)$/mg, '$1$2 # e.g. 100000 for Haiku');
573
590
  }
574
591
  export function settingsFromYAML(yamlContent: string, original: HumanSettings | undefined): HumanSettings {
575
592
  const data = YAML.parse(yamlContent) as EditableSettingsData;
@@ -596,6 +613,12 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
596
613
  last_sync: original?.opencode?.last_sync,
597
614
  extraction_point: original?.opencode?.extraction_point,
598
615
  processed_sessions: original?.opencode?.processed_sessions,
616
+ extraction_model: (data.opencode.extraction_model != null && data.opencode.extraction_model !== 'default')
617
+ ? data.opencode.extraction_model
618
+ : undefined,
619
+ extraction_token_limit: (data.opencode.extraction_token_limit != null && data.opencode.extraction_token_limit !== 'default')
620
+ ? Number(data.opencode.extraction_token_limit)
621
+ : undefined,
599
622
  };
600
623
  }
601
624
 
@@ -605,7 +628,25 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
605
628
  integration: nullToUndefined(data.claudeCode.integration),
606
629
  polling_interval_ms: nullToUndefined(data.claudeCode.polling_interval_ms),
607
630
  last_sync: original?.claudeCode?.last_sync,
631
+ extraction_point: original?.claudeCode?.extraction_point,
608
632
  processed_sessions: original?.claudeCode?.processed_sessions,
633
+ extraction_model: (data.claudeCode.extraction_model != null && data.claudeCode.extraction_model !== 'default')
634
+ ? data.claudeCode.extraction_model
635
+ : undefined,
636
+ extraction_token_limit: (data.claudeCode.extraction_token_limit != null && data.claudeCode.extraction_token_limit !== 'default')
637
+ ? Number(data.claudeCode.extraction_token_limit)
638
+ : undefined,
639
+ };
640
+ }
641
+
642
+ let cursor: CursorSettings | undefined;
643
+ if (data.cursor) {
644
+ cursor = {
645
+ integration: nullToUndefined(data.cursor.integration),
646
+ polling_interval_ms: nullToUndefined(data.cursor.polling_interval_ms),
647
+ last_sync: original?.cursor?.last_sync,
648
+ extraction_point: original?.cursor?.extraction_point,
649
+ processed_sessions: original?.cursor?.processed_sessions,
609
650
  };
610
651
  }
611
652
 
@@ -629,6 +670,7 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
629
670
  ceremony,
630
671
  opencode,
631
672
  claudeCode,
673
+ cursor,
632
674
  backup,
633
675
  };
634
676
  }
@@ -896,7 +938,8 @@ export function contextFromYAML(yamlContent: string): ContextYAMLResult {
896
938
  if (m._delete) {
897
939
  deletedMessageIds.push(m.id);
898
940
  } else {
899
- messages.push({ id: m.id, context_status: m.context_status });
941
+ const normalized = (m.context_status ?? 'default').toString().toLowerCase() as ContextStatus;
942
+ messages.push({ id: m.id, context_status: normalized });
900
943
  }
901
944
  }
902
945
 
@@ -1,25 +0,0 @@
1
- import { loadLatestState, retrieve } from "../retrieval";
2
- import type { TraitResult } from "../retrieval";
3
-
4
- export async function execute(query: string, limit: number): Promise<TraitResult[]> {
5
- const state = await loadLatestState();
6
- if (!state) {
7
- console.error("No saved state found. Is EI_DATA_PATH set correctly?");
8
- return [];
9
- }
10
-
11
- const traits = state.human.traits;
12
- if (traits.length === 0) {
13
- return [];
14
- }
15
-
16
- const results = await retrieve(traits, query, limit);
17
-
18
- return results.map(trait => ({
19
- id: trait.id,
20
- name: trait.name,
21
- description: trait.description,
22
- strength: trait.strength ?? 0.5,
23
- sentiment: trait.sentiment,
24
- }));
25
- }
@@ -1,74 +0,0 @@
1
- import type { ItemMatchPromptData, PromptOutput } from "./types.js";
2
-
3
- export function buildHumanItemMatchPrompt(data: ItemMatchPromptData): PromptOutput {
4
- if (!data.candidate_type || !data.candidate_name) {
5
- throw new Error("buildHumanItemMatchPrompt: candidate_type and candidate_name are required");
6
- }
7
-
8
- const typeLabel = data.candidate_type.toUpperCase();
9
-
10
- const system = `# Task
11
-
12
- You are checking if a new ${typeLabel} already exists in our database.
13
-
14
- We track four types of data about the human user:
15
- - **FACT**: Biographical data (name, birthday, job, etc.)
16
- - **TRAIT**: Personality patterns (introverted, analytical, etc.)
17
- - **TOPIC**: Interests, goals, concerns, stories
18
- - **PERSON**: People in their life (family, friends, coworkers)
19
-
20
- Your job is to find if this candidate matches ANY existing entry — even if it's stored as a different type.
21
-
22
- ## Why Cross-Type Matching?
23
-
24
- Sometimes the same concept gets detected as different types:
25
- - "Juliet" might be detected as a TOPIC but should be a PERSON
26
- - "Birthday" might be detected as a TOPIC but is actually a FACT
27
- - "Always planning ahead" might be a TOPIC but is really a TRAIT
28
-
29
- If you find a match in a DIFFERENT type, still return it! The system will handle the type mismatch.
30
-
31
- ## Matching Rules
32
-
33
- 1. **Exact match**: Same name/concept → return its ID
34
- 2. **Similar match**: Clearly the same thing with different wording → return its ID
35
- 3. **Cross-type match**: Same concept stored as different type → return its ID
36
- 4. **No match**: Genuinely new information → return "new"
37
-
38
- # Existing Data
39
-
40
- The following entries are already in our database. Same-type entries have full descriptions; cross-type entries are truncated for brevity.
41
-
42
- \`\`\`json
43
- ${JSON.stringify(data.all_items, null, 2)}
44
- \`\`\`
45
-
46
- # Response Format
47
-
48
- Return ONLY the ID of the matching entry, or "new" if no match exists.
49
-
50
- \`\`\`json
51
- {
52
- "matched_guid": "uuid-of-matching-entry" | "new"
53
- }
54
- \`\`\`
55
-
56
- **Return JSON only.**`;
57
-
58
- const user = `# Candidate to Match
59
-
60
- Type: ${typeLabel}
61
- Name: ${data.candidate_name}
62
- Value: ${data.candidate_value}
63
-
64
- Find the best match in existing data, or return "new" if this is genuinely new.
65
-
66
- **Return JSON:**
67
- \`\`\`json
68
- {
69
- "matched_guid": "..." | "new"
70
- }
71
- \`\`\``;
72
-
73
- return { system, user };
74
- }