ei-tui 0.1.25 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +42 -0
  2. package/package.json +1 -1
  3. package/src/README.md +4 -11
  4. package/src/cli/README.md +4 -5
  5. package/src/cli/retrieval.ts +3 -25
  6. package/src/cli.ts +3 -7
  7. package/src/core/AGENTS.md +1 -1
  8. package/src/core/constants/built-in-facts.ts +49 -0
  9. package/src/core/constants/index.ts +1 -0
  10. package/src/core/context-utils.ts +0 -1
  11. package/src/core/embedding-service.ts +8 -0
  12. package/src/core/handlers/dedup.ts +10 -16
  13. package/src/core/handlers/heartbeat.ts +2 -3
  14. package/src/core/handlers/human-extraction.ts +95 -30
  15. package/src/core/handlers/human-matching.ts +326 -248
  16. package/src/core/handlers/index.ts +8 -6
  17. package/src/core/handlers/persona-generation.ts +8 -8
  18. package/src/core/handlers/rewrite.ts +4 -29
  19. package/src/core/handlers/utils.ts +23 -1
  20. package/src/core/heartbeat-manager.ts +2 -4
  21. package/src/core/human-data-manager.ts +5 -27
  22. package/src/core/message-manager.ts +10 -10
  23. package/src/core/orchestrators/ceremony.ts +50 -39
  24. package/src/core/orchestrators/dedup-phase.ts +0 -1
  25. package/src/core/orchestrators/human-extraction.ts +351 -207
  26. package/src/core/orchestrators/index.ts +6 -4
  27. package/src/core/orchestrators/persona-generation.ts +3 -3
  28. package/src/core/processor.ts +99 -17
  29. package/src/core/prompt-context-builder.ts +4 -6
  30. package/src/core/state/human.ts +1 -26
  31. package/src/core/state/personas.ts +2 -2
  32. package/src/core/state-manager.ts +107 -14
  33. package/src/core/tools/builtin/read-memory.ts +7 -8
  34. package/src/core/types/data-items.ts +2 -4
  35. package/src/core/types/entities.ts +6 -4
  36. package/src/core/types/enums.ts +6 -9
  37. package/src/core/types/llm.ts +2 -2
  38. package/src/core/utils/crossFind.ts +2 -5
  39. package/src/core/utils/event-windows.ts +31 -0
  40. package/src/integrations/claude-code/importer.ts +8 -4
  41. package/src/integrations/claude-code/types.ts +2 -0
  42. package/src/integrations/opencode/importer.ts +7 -3
  43. package/src/prompts/AGENTS.md +73 -1
  44. package/src/prompts/ceremony/rewrite.ts +3 -22
  45. package/src/prompts/ceremony/types.ts +3 -3
  46. package/src/prompts/generation/descriptions.ts +2 -2
  47. package/src/prompts/generation/types.ts +2 -2
  48. package/src/prompts/heartbeat/types.ts +2 -2
  49. package/src/prompts/human/event-scan.ts +122 -0
  50. package/src/prompts/human/fact-find.ts +106 -0
  51. package/src/prompts/human/fact-scan.ts +0 -2
  52. package/src/prompts/human/index.ts +17 -10
  53. package/src/prompts/human/person-match.ts +65 -0
  54. package/src/prompts/human/person-scan.ts +52 -59
  55. package/src/prompts/human/person-update.ts +241 -0
  56. package/src/prompts/human/topic-match.ts +65 -0
  57. package/src/prompts/human/topic-scan.ts +51 -71
  58. package/src/prompts/human/topic-update.ts +295 -0
  59. package/src/prompts/human/types.ts +63 -40
  60. package/src/prompts/index.ts +4 -8
  61. package/src/prompts/persona/topics-update.ts +2 -2
  62. package/src/prompts/persona/traits.ts +2 -2
  63. package/src/prompts/persona/types.ts +3 -3
  64. package/src/prompts/response/index.ts +1 -1
  65. package/src/prompts/response/sections.ts +9 -12
  66. package/src/prompts/response/types.ts +2 -3
  67. package/src/storage/embeddings.ts +1 -1
  68. package/src/storage/index.ts +1 -0
  69. package/src/storage/indexed.ts +174 -0
  70. package/src/storage/merge.ts +67 -2
  71. package/tui/src/commands/me.tsx +5 -14
  72. package/tui/src/commands/settings.tsx +15 -0
  73. package/tui/src/context/ei.tsx +5 -14
  74. package/tui/src/util/yaml-serializers.ts +48 -33
  75. package/src/cli/commands/traits.ts +0 -25
  76. package/src/prompts/human/item-match.ts +0 -74
  77. package/src/prompts/human/item-update.ts +0 -364
  78. package/src/prompts/human/trait-scan.ts +0 -115
@@ -1,8 +1,8 @@
1
1
  import type { PersonaTraitExtractionPromptData, PromptOutput } from "./types.js";
2
- import type { Trait } from "../../core/types.js";
2
+ import type { PersonaTrait } from "../../core/types.js";
3
3
  import { formatMessagesAsPlaceholders } from "../message-utils.js";
4
4
 
5
- function formatTraitsForPrompt(traits: Trait[]): string {
5
+ function formatTraitsForPrompt(traits: PersonaTrait[]): string {
6
6
  if (traits.length === 0) return "(No traits yet)";
7
7
 
8
8
  return JSON.stringify(traits.map(t => ({
@@ -1,4 +1,4 @@
1
- import type { Trait, Message, PersonaTopic } from "../../core/types.js";
1
+ import type { PersonaTrait, Message, PersonaTopic } from "../../core/types.js";
2
2
 
3
3
  export interface PromptOutput {
4
4
  system: string;
@@ -7,7 +7,7 @@ export interface PromptOutput {
7
7
 
8
8
  export interface PersonaTraitExtractionPromptData {
9
9
  persona_name: string;
10
- current_traits: Trait[];
10
+ current_traits: PersonaTrait[];
11
11
  messages_context: Message[];
12
12
  messages_analyze: Message[];
13
13
  }
@@ -56,7 +56,7 @@ export interface PersonaTopicUpdatePromptData {
56
56
  persona_name: string;
57
57
  short_description?: string;
58
58
  long_description?: string;
59
- traits: Trait[];
59
+ traits: PersonaTrait[];
60
60
  existing_topic?: PersonaTopic; // If updating existing
61
61
  candidate: PersonaTopicScanCandidate;
62
62
  messages_context: Message[];
@@ -34,7 +34,7 @@ function buildEiSystemPrompt(data: ResponsePromptData): string {
34
34
  You are the central hub of this experience - a thoughtful AI who genuinely cares about the human's wellbeing and growth. You listen, remember, and help them reflect. You're curious about their life but never intrusive.
35
35
 
36
36
  Your role is unique among personas:
37
- - You see ALL of the human's data (facts, traits, topics, people) across all groups
37
+ - You see ALL of the human's data (facts, topics, people) across all groups
38
38
  - You help them understand and navigate the system
39
39
  - You gently help them explore their thoughts and feelings
40
40
  - You attempt to emulate their speech patterns;
@@ -3,9 +3,15 @@
3
3
  * Building blocks for constructing response prompts
4
4
  */
5
5
 
6
- import type { Trait, Quote, PersonaTopic } from "../../core/types.js";
6
+ import type { PersonaTrait, Quote, PersonaTopic } from "../../core/types.js";
7
7
  import type { ResponsePromptData } from "./types.js";
8
- import { truncateDescription } from "../human/item-update.js";
8
+
9
+ const DESCRIPTION_MAX_CHARS = 500;
10
+
11
+ function truncateDescription(description: string): string {
12
+ if (description.length <= DESCRIPTION_MAX_CHARS) return description;
13
+ return description.slice(0, DESCRIPTION_MAX_CHARS) + "…";
14
+ }
9
15
 
10
16
  // =============================================================================
11
17
  // IDENTITY SECTION
@@ -62,7 +68,7 @@ export function buildGuidelinesSection(personaName: string): string {
62
68
  // TRAITS SECTION
63
69
  // =============================================================================
64
70
 
65
- export function buildTraitsSection(traits: Trait[], header: string): string {
71
+ export function buildTraitsSection(traits: PersonaTrait[], header: string): string {
66
72
  if (traits.length === 0) return "";
67
73
 
68
74
  const sorted = [...traits].sort((a, b) => (b.strength ?? 0.5) - (a.strength ?? 0.5)).slice(0, 15);
@@ -146,14 +152,6 @@ export function buildHumanSection(human: ResponsePromptData["human"]): string {
146
152
  if (facts) sections.push(`### Key Facts\n${facts}`);
147
153
  }
148
154
 
149
- // Traits
150
- if (human.traits.length > 0) {
151
- const traits = human.traits
152
- .slice(0, 15)
153
- .map(t => `- **${t.name}**: ${truncateDescription(t.description)}`)
154
- .join("\n");
155
- sections.push(`### Personality\n${traits}`);
156
- }
157
155
 
158
156
  // Active topics (exposure_current > 0.3)
159
157
  const activeTopics = human.topics.filter(t => t.exposure_current > 0.3);
@@ -275,7 +273,6 @@ export function buildQuotesSection(quotes: Quote[], human: ResponsePromptData["h
275
273
 
276
274
  const allDataItems = [
277
275
  ...human.facts.map(f => ({ id: f.id, name: f.name })),
278
- ...human.traits.map(t => ({ id: t.id, name: t.name })),
279
276
  ...human.topics.map(t => ({ id: t.id, name: t.name })),
280
277
  ...human.people.map(p => ({ id: p.id, name: p.name })),
281
278
  ];
@@ -3,7 +3,7 @@
3
3
  * Based on CONTRACTS.md ResponsePromptData specification
4
4
  */
5
5
 
6
- import type { Fact, Trait, Topic, Person, Quote, PersonaTopic } from "../../core/types.js";
6
+ import type { Fact, PersonaTrait, Topic, Person, Quote, PersonaTopic } from "../../core/types.js";
7
7
  import type { ToolDefinition } from "../../core/types.js";
8
8
 
9
9
  /**
@@ -15,12 +15,11 @@ export interface ResponsePromptData {
15
15
  aliases: string[];
16
16
  short_description?: string;
17
17
  long_description?: string;
18
- traits: Trait[];
18
+ traits: PersonaTrait[];
19
19
  topics: PersonaTopic[];
20
20
  };
21
21
  human: {
22
22
  facts: Fact[];
23
- traits: Trait[];
24
23
  topics: Topic[];
25
24
  people: Person[];
26
25
  quotes: Quote[];
@@ -56,7 +56,7 @@ function decodeEmbedding(value: unknown): number[] | undefined {
56
56
  // Walk the entire StorageState and encode/decode all embedding fields
57
57
  // ---------------------------------------------------------------------------
58
58
 
59
- const HUMAN_ITEM_KEYS = ["facts", "traits", "topics", "people", "quotes"] as const;
59
+ const HUMAN_ITEM_KEYS = ["facts", "topics", "people", "quotes"] as const;
60
60
 
61
61
  /**
62
62
  * Returns a new StorageState with embeddings encoded as base64 strings.
@@ -1,5 +1,6 @@
1
1
  export type { Storage } from "./interface.js";
2
2
  export { LocalStorage } from "./local.js";
3
+ export { IndexedDBStorage } from "./indexed.js";
3
4
  export { remoteSync, RemoteSync, type RemoteSyncCredentials, type RemoteTimestamp, type SyncResult, type FetchResult } from "./remote.js";
4
5
  export { encrypt, decrypt, generateUserId, type CryptoCredentials, type EncryptedPayload } from "./crypto.js";
5
6
  export { yoloMerge } from "./merge.js";
@@ -0,0 +1,174 @@
1
+ import type { StorageState } from "../core/types.js";
2
+ import type { Storage } from "./interface.js";
3
+ import { compress, decompress, isCompressed } from "./compress.js";
4
+ import { encodeAllEmbeddings, decodeAllEmbeddings } from "./embeddings.js";
5
+
6
+ const DB_NAME = "ei_db";
7
+ const DB_VERSION = 1;
8
+ const STORE_NAME = "state";
9
+ const PRIMARY_KEY = "primary";
10
+ const BACKUP_KEY = "backup";
11
+
12
+ export class IndexedDBStorage implements Storage {
13
+ async isAvailable(): Promise<boolean> {
14
+ try {
15
+ const db = await this.openDB();
16
+ db.close();
17
+ return true;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ async save(state: StorageState): Promise<void> {
24
+ state.timestamp = new Date().toISOString();
25
+ try {
26
+ const json = JSON.stringify(encodeAllEmbeddings(state));
27
+ const payload = await compress(json);
28
+ await this.setItem(PRIMARY_KEY, payload);
29
+ } catch (e) {
30
+ if (this.isQuotaError(e)) {
31
+ throw new Error("STORAGE_SAVE_FAILED: IndexedDB quota exceeded");
32
+ }
33
+ throw e;
34
+ }
35
+ }
36
+
37
+ async load(): Promise<StorageState | null> {
38
+ const current = await this.getItem(PRIMARY_KEY);
39
+ if (current) {
40
+ try {
41
+ const json = isCompressed(current) ? await decompress(current) : current;
42
+ return decodeAllEmbeddings(JSON.parse(json) as StorageState);
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+ return null;
48
+ }
49
+
50
+ /**
51
+ * Move current state to backup location and clear primary state.
52
+ * Used after successful remote sync to signal "no local state to load" on next launch.
53
+ * Backup can be restored manually if remote pull fails.
54
+ */
55
+ async moveToBackup(): Promise<void> {
56
+ const current = await this.getItem(PRIMARY_KEY);
57
+ if (current) {
58
+ // Remove primary first so backup write doesn't double-count against quota.
59
+ await this.deleteItem(PRIMARY_KEY);
60
+ await this.setItem(BACKUP_KEY, current);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Read backup state without removing it.
66
+ * Used to peek sync credentials from a previous session's backup.
67
+ */
68
+ async loadBackup(): Promise<StorageState | null> {
69
+ const backup = await this.getItem(BACKUP_KEY);
70
+ if (backup) {
71
+ try {
72
+ const json = isCompressed(backup) ? await decompress(backup) : backup;
73
+ return decodeAllEmbeddings(JSON.parse(json) as StorageState);
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ /** No-op in browser — rolling backups are TUI-only (filesystem required). */
82
+ async saveRollingBackup(_state: StorageState, _maxBackups: number): Promise<void> {
83
+ // Intentional no-op: IndexedDB has no directory/file concept.
84
+ // The Processor gates this call with `this.isTUI` so it never runs in the browser.
85
+ }
86
+
87
+ // ─── Private IDB helpers ──────────────────────────────────────────────────
88
+
89
+ private openDB(): Promise<IDBDatabase> {
90
+ return new Promise((resolve, reject) => {
91
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
92
+
93
+ request.onupgradeneeded = (event) => {
94
+ const db = (event.target as IDBOpenDBRequest).result;
95
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
96
+ db.createObjectStore(STORE_NAME);
97
+ }
98
+ };
99
+
100
+ request.onsuccess = (event) => {
101
+ resolve((event.target as IDBOpenDBRequest).result);
102
+ };
103
+
104
+ request.onerror = (event) => {
105
+ reject((event.target as IDBOpenDBRequest).error);
106
+ };
107
+ });
108
+ }
109
+
110
+ private async getItem(key: string): Promise<string | null> {
111
+ const db = await this.openDB();
112
+ return new Promise((resolve, reject) => {
113
+ const tx = db.transaction(STORE_NAME, "readonly");
114
+ const store = tx.objectStore(STORE_NAME);
115
+ const request = store.get(key);
116
+
117
+ request.onsuccess = (event) => {
118
+ const result = (event.target as IDBRequest).result;
119
+ db.close();
120
+ resolve(result ?? null);
121
+ };
122
+
123
+ request.onerror = (event) => {
124
+ db.close();
125
+ reject((event.target as IDBRequest).error);
126
+ };
127
+ });
128
+ }
129
+
130
+ private async setItem(key: string, value: string): Promise<void> {
131
+ const db = await this.openDB();
132
+ return new Promise((resolve, reject) => {
133
+ const tx = db.transaction(STORE_NAME, "readwrite");
134
+ const store = tx.objectStore(STORE_NAME);
135
+ const request = store.put(value, key);
136
+
137
+ request.onsuccess = () => {
138
+ db.close();
139
+ resolve();
140
+ };
141
+
142
+ request.onerror = (event) => {
143
+ db.close();
144
+ reject((event.target as IDBRequest).error);
145
+ };
146
+ });
147
+ }
148
+
149
+ private async deleteItem(key: string): Promise<void> {
150
+ const db = await this.openDB();
151
+ return new Promise((resolve, reject) => {
152
+ const tx = db.transaction(STORE_NAME, "readwrite");
153
+ const store = tx.objectStore(STORE_NAME);
154
+ const request = store.delete(key);
155
+
156
+ request.onsuccess = () => {
157
+ db.close();
158
+ resolve();
159
+ };
160
+
161
+ request.onerror = (event) => {
162
+ db.close();
163
+ reject((event.target as IDBRequest).error);
164
+ };
165
+ });
166
+ }
167
+
168
+ private isQuotaError(e: unknown): boolean {
169
+ return (
170
+ e instanceof DOMException &&
171
+ (e.name === "QuotaExceededError" || e.name === "NS_ERROR_DOM_QUOTA_REACHED")
172
+ );
173
+ }
174
+ }
@@ -1,4 +1,23 @@
1
- import type { StorageState, DataItem, Quote } from "../core/types.js";
1
+ import type { StorageState, DataItem, Quote, ToolProvider, ToolDefinition, ProviderAccount } from "../core/types.js";
2
+
3
+ function mergeByName<T extends { name: string }>(
4
+ local: T[],
5
+ remote: T[],
6
+ preferRemote: boolean,
7
+ ): T[] {
8
+ const merged = [...local];
9
+
10
+ for (const remoteItem of remote) {
11
+ const localIndex = merged.findIndex(item => item.name === remoteItem.name);
12
+ if (localIndex === -1) {
13
+ merged.push(remoteItem);
14
+ } else if (preferRemote) {
15
+ merged[localIndex] = remoteItem;
16
+ }
17
+ }
18
+
19
+ return merged;
20
+ }
2
21
 
3
22
  function mergeDataItems<T extends DataItem>(local: T[], remote: T[]): T[] {
4
23
  const merged = [...local];
@@ -32,7 +51,6 @@ export function yoloMerge(local: StorageState, remote: StorageState): StorageSta
32
51
  const merged = structuredClone(local);
33
52
 
34
53
  merged.human.facts = mergeDataItems(merged.human.facts, remote.human.facts);
35
- merged.human.traits = mergeDataItems(merged.human.traits, remote.human.traits);
36
54
  merged.human.topics = mergeDataItems(merged.human.topics, remote.human.topics);
37
55
  merged.human.people = mergeDataItems(merged.human.people, remote.human.people);
38
56
  merged.human.quotes = mergeQuotes(merged.human.quotes || [], remote.human.quotes || []);
@@ -63,6 +81,53 @@ export function yoloMerge(local: StorageState, remote: StorageState): StorageSta
63
81
  }
64
82
  }
65
83
 
84
+ if ('traits' in merged.human) {
85
+ delete (merged.human as Record<string, unknown>)['traits'];
86
+ }
87
+
88
+ const preferRemote = remote.timestamp > local.timestamp;
89
+
90
+ if (remote.human.settings?.accounts && merged.human.settings) {
91
+ merged.human.settings.accounts = mergeByName<ProviderAccount>(
92
+ merged.human.settings?.accounts || [],
93
+ remote.human.settings.accounts,
94
+ preferRemote,
95
+ );
96
+ }
97
+
98
+ if (preferRemote && remote.human.settings) {
99
+ const remoteSettings = remote.human.settings;
100
+ const localSettings = merged.human.settings || {};
101
+
102
+ if (remoteSettings.default_model !== undefined) localSettings.default_model = remoteSettings.default_model;
103
+ if (remoteSettings.oneshot_model !== undefined) localSettings.oneshot_model = remoteSettings.oneshot_model;
104
+ if (remoteSettings.rewrite_model !== undefined) localSettings.rewrite_model = remoteSettings.rewrite_model;
105
+ if (remoteSettings.queue_paused !== undefined) localSettings.queue_paused = remoteSettings.queue_paused;
106
+ if (remoteSettings.skip_quote_delete_confirm !== undefined) localSettings.skip_quote_delete_confirm = remoteSettings.skip_quote_delete_confirm;
107
+ if (remoteSettings.name_display !== undefined) localSettings.name_display = remoteSettings.name_display;
108
+ if (remoteSettings.time_mode !== undefined) localSettings.time_mode = remoteSettings.time_mode;
109
+
110
+ if (remoteSettings.opencode) localSettings.opencode = remoteSettings.opencode;
111
+ if (remoteSettings.ceremony) localSettings.ceremony = remoteSettings.ceremony;
112
+ if (remoteSettings.backup) localSettings.backup = remoteSettings.backup;
113
+ if (remoteSettings.claudeCode) localSettings.claudeCode = remoteSettings.claudeCode;
114
+ // NOTE: Do NOT merge sync credentials — always keep local sync creds
115
+
116
+ merged.human.settings = localSettings;
117
+ }
118
+
119
+ merged.providers = mergeByName<ToolProvider>(
120
+ merged.providers || [],
121
+ remote.providers || [],
122
+ preferRemote,
123
+ );
124
+
125
+ merged.tools = mergeByName<ToolDefinition>(
126
+ merged.tools || [],
127
+ remote.tools || [],
128
+ preferRemote,
129
+ );
130
+
66
131
  merged.timestamp = new Date().toISOString();
67
132
 
68
133
  return merged;
@@ -4,15 +4,15 @@ import { humanToYAML, humanFromYAML } from "../util/yaml-serializers.js";
4
4
  import { logger } from "../util/logger.js";
5
5
  import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
6
6
 
7
- type DataType = "facts" | "traits" | "topics" | "people";
7
+ type DataType = "facts" | "topics" | "people";
8
8
 
9
- const VALID_TYPES: DataType[] = ["facts", "traits", "topics", "people"];
9
+ const VALID_TYPES: DataType[] = ["facts", "topics", "people"];
10
10
 
11
11
  export const meCommand: Command = {
12
12
  name: "me",
13
13
  aliases: [],
14
14
  description: "Edit your data in $EDITOR",
15
- usage: "/me [facts|traits|topics|people]",
15
+ usage: "/me [facts|topics|people]",
16
16
 
17
17
  async execute(args, ctx) {
18
18
  const human = await ctx.ei.getHuman();
@@ -23,14 +23,13 @@ export const meCommand: Command = {
23
23
  : null;
24
24
 
25
25
  if (filterArg && !filterType) {
26
- ctx.showNotification(`Invalid type: ${filterArg}. Use: facts, traits, topics, people`, "error");
26
+ ctx.showNotification(`Invalid type: ${filterArg}. Use: facts, topics, people`, "error");
27
27
  return;
28
28
  }
29
29
 
30
30
  const filteredHuman = filterType ? {
31
31
  ...human,
32
32
  facts: filterType === "facts" ? human.facts : [],
33
- traits: filterType === "traits" ? human.traits : [],
34
33
  topics: filterType === "topics" ? human.topics : [],
35
34
  people: filterType === "people" ? human.people : [],
36
35
  } : human;
@@ -67,14 +66,11 @@ export const meCommand: Command = {
67
66
  }
68
67
 
69
68
  try {
70
- const parsed = humanFromYAML(result.content);
69
+ const parsed = humanFromYAML(result.content, filteredHuman);
71
70
 
72
71
  for (const id of parsed.deletedFactIds) {
73
72
  await ctx.ei.removeDataItem("fact", id);
74
73
  }
75
- for (const id of parsed.deletedTraitIds) {
76
- await ctx.ei.removeDataItem("trait", id);
77
- }
78
74
  for (const id of parsed.deletedTopicIds) {
79
75
  await ctx.ei.removeDataItem("topic", id);
80
76
  }
@@ -85,9 +81,6 @@ export const meCommand: Command = {
85
81
  for (const fact of parsed.facts) {
86
82
  await ctx.ei.upsertFact(fact);
87
83
  }
88
- for (const trait of parsed.traits) {
89
- await ctx.ei.upsertTrait(trait);
90
- }
91
84
  for (const topic of parsed.topics) {
92
85
  await ctx.ei.upsertTopic(topic);
93
86
  }
@@ -96,11 +89,9 @@ export const meCommand: Command = {
96
89
  }
97
90
 
98
91
  const deleteCount = parsed.deletedFactIds.length +
99
- parsed.deletedTraitIds.length +
100
92
  parsed.deletedTopicIds.length +
101
93
  parsed.deletedPersonIds.length;
102
94
  const updateCount = parsed.facts.length +
103
- parsed.traits.length +
104
95
  parsed.topics.length +
105
96
  parsed.people.length;
106
97
 
@@ -45,6 +45,21 @@ export const settingsCommand: Command = {
45
45
  // Validate provider name in default_model (case-insensitive match + auto-correct)
46
46
  const llmAccounts = human.settings?.accounts?.filter(a => a.type === "llm") ?? [];
47
47
  newSettings.default_model = validateModelProvider(newSettings.default_model, llmAccounts);
48
+
49
+ if (newSettings.opencode?.extraction_model) {
50
+ newSettings.opencode.extraction_model = validateModelProvider(
51
+ newSettings.opencode.extraction_model,
52
+ llmAccounts
53
+ );
54
+ }
55
+
56
+ if (newSettings.claudeCode?.extraction_model) {
57
+ newSettings.claudeCode.extraction_model = validateModelProvider(
58
+ newSettings.claudeCode.extraction_model,
59
+ llmAccounts
60
+ );
61
+ }
62
+
48
63
  await ctx.ei.updateSettings(newSettings);
49
64
  ctx.showNotification("Settings updated", "info");
50
65
  return;
@@ -23,7 +23,6 @@ import type {
23
23
  HumanEntity,
24
24
  HumanSettings,
25
25
  Fact,
26
- Trait,
27
26
  Topic,
28
27
  Person,
29
28
  Quote,
@@ -78,10 +77,9 @@ export interface EiContextValue {
78
77
  updateHuman: (updates: Partial<HumanEntity>) => Promise<void>;
79
78
  updateSettings: (updates: Partial<HumanSettings>) => Promise<void>;
80
79
  upsertFact: (fact: Fact) => Promise<void>;
81
- upsertTrait: (trait: Trait) => Promise<void>;
82
80
  upsertTopic: (topic: Topic) => Promise<void>;
83
81
  upsertPerson: (person: Person) => Promise<void>;
84
- removeDataItem: (type: "fact" | "trait" | "topic" | "person", id: string) => Promise<void>;
82
+ removeDataItem: (type: "fact" | "topic" | "person", id: string) => Promise<void>;
85
83
  syncStatus: () => { configured: boolean; envBased: boolean };
86
84
  triggerSync: () => Promise<{ success: boolean; error?: string }>;
87
85
  getGroupList: () => Promise<string[]>;
@@ -92,10 +90,9 @@ export interface EiContextValue {
92
90
  quotesVersion: () => number;
93
91
  searchHumanData: (
94
92
  query: string,
95
- options?: { types?: Array<"fact" | "trait" | "topic" | "person" | "quote">; limit?: number }
93
+ options?: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number }
96
94
  ) => Promise<{
97
95
  facts: Fact[];
98
- traits: Trait[];
99
96
  topics: Topic[];
100
97
  people: Person[];
101
98
  quotes: Quote[];
@@ -329,11 +326,6 @@ export const EiProvider: ParentComponent = (props) => {
329
326
  await processor.upsertFact(fact);
330
327
  };
331
328
 
332
- const upsertTrait = async (trait: Trait) => {
333
- if (!processor) return;
334
- await processor.upsertTrait(trait);
335
- };
336
-
337
329
  const upsertTopic = async (topic: Topic) => {
338
330
  if (!processor) return;
339
331
  await processor.upsertTopic(topic);
@@ -344,7 +336,7 @@ export const EiProvider: ParentComponent = (props) => {
344
336
  await processor.upsertPerson(person);
345
337
  };
346
338
 
347
- const removeDataItem = async (type: "fact" | "trait" | "topic" | "person", id: string) => {
339
+ const removeDataItem = async (type: "fact" | "topic" | "person", id: string) => {
348
340
  if (!processor) return;
349
341
  await processor.removeDataItem(type, id);
350
342
  };
@@ -450,9 +442,9 @@ export const EiProvider: ParentComponent = (props) => {
450
442
 
451
443
  const searchHumanData = async (
452
444
  query: string,
453
- options?: { types?: Array<"fact" | "trait" | "topic" | "person" | "quote">; limit?: number }
445
+ options?: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number }
454
446
  ) => {
455
- if (!processor) return { facts: [], traits: [], topics: [], people: [], quotes: [] };
447
+ if (!processor) return { facts: [], topics: [], people: [], quotes: [] };
456
448
  return processor.searchHumanData(query, options);
457
449
  };
458
450
 
@@ -648,7 +640,6 @@ export const EiProvider: ParentComponent = (props) => {
648
640
  updateHuman,
649
641
  updateSettings,
650
642
  upsertFact,
651
- upsertTrait,
652
643
  upsertTopic,
653
644
  upsertPerson,
654
645
  removeDataItem,