ei-tui 0.1.3
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/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +63 -0
- package/src/README.md +96 -0
- package/src/cli/README.md +47 -0
- package/src/cli/commands/facts.ts +25 -0
- package/src/cli/commands/people.ts +25 -0
- package/src/cli/commands/quotes.ts +19 -0
- package/src/cli/commands/topics.ts +25 -0
- package/src/cli/commands/traits.ts +25 -0
- package/src/cli/retrieval.ts +269 -0
- package/src/cli.ts +176 -0
- package/src/core/AGENTS.md +104 -0
- package/src/core/embedding-service.ts +241 -0
- package/src/core/handlers/index.ts +1057 -0
- package/src/core/index.ts +4 -0
- package/src/core/llm-client.ts +265 -0
- package/src/core/model-context-windows.ts +49 -0
- package/src/core/orchestrators/ceremony.ts +500 -0
- package/src/core/orchestrators/extraction-chunker.ts +138 -0
- package/src/core/orchestrators/human-extraction.ts +457 -0
- package/src/core/orchestrators/index.ts +28 -0
- package/src/core/orchestrators/persona-generation.ts +76 -0
- package/src/core/orchestrators/persona-topics.ts +117 -0
- package/src/core/personas/index.ts +5 -0
- package/src/core/personas/opencode-agent.ts +81 -0
- package/src/core/processor.ts +1413 -0
- package/src/core/queue-processor.ts +197 -0
- package/src/core/state/checkpoints.ts +68 -0
- package/src/core/state/human.ts +176 -0
- package/src/core/state/index.ts +5 -0
- package/src/core/state/personas.ts +217 -0
- package/src/core/state/queue.ts +144 -0
- package/src/core/state-manager.ts +347 -0
- package/src/core/types.ts +421 -0
- package/src/core/utils/decay.ts +33 -0
- package/src/index.ts +1 -0
- package/src/integrations/opencode/importer.ts +896 -0
- package/src/integrations/opencode/index.ts +16 -0
- package/src/integrations/opencode/json-reader.ts +304 -0
- package/src/integrations/opencode/reader-factory.ts +35 -0
- package/src/integrations/opencode/sqlite-reader.ts +189 -0
- package/src/integrations/opencode/types.ts +244 -0
- package/src/prompts/AGENTS.md +62 -0
- package/src/prompts/ceremony/description-check.ts +47 -0
- package/src/prompts/ceremony/expire.ts +30 -0
- package/src/prompts/ceremony/explore.ts +60 -0
- package/src/prompts/ceremony/index.ts +11 -0
- package/src/prompts/ceremony/types.ts +42 -0
- package/src/prompts/generation/descriptions.ts +91 -0
- package/src/prompts/generation/index.ts +15 -0
- package/src/prompts/generation/persona.ts +155 -0
- package/src/prompts/generation/seeds.ts +31 -0
- package/src/prompts/generation/types.ts +47 -0
- package/src/prompts/heartbeat/check.ts +179 -0
- package/src/prompts/heartbeat/ei.ts +208 -0
- package/src/prompts/heartbeat/index.ts +15 -0
- package/src/prompts/heartbeat/types.ts +70 -0
- package/src/prompts/human/fact-scan.ts +152 -0
- package/src/prompts/human/index.ts +32 -0
- package/src/prompts/human/item-match.ts +74 -0
- package/src/prompts/human/item-update.ts +322 -0
- package/src/prompts/human/person-scan.ts +115 -0
- package/src/prompts/human/topic-scan.ts +135 -0
- package/src/prompts/human/trait-scan.ts +115 -0
- package/src/prompts/human/types.ts +127 -0
- package/src/prompts/index.ts +90 -0
- package/src/prompts/message-utils.ts +39 -0
- package/src/prompts/persona/index.ts +16 -0
- package/src/prompts/persona/topics-match.ts +69 -0
- package/src/prompts/persona/topics-scan.ts +98 -0
- package/src/prompts/persona/topics-update.ts +157 -0
- package/src/prompts/persona/traits.ts +117 -0
- package/src/prompts/persona/types.ts +74 -0
- package/src/prompts/response/index.ts +147 -0
- package/src/prompts/response/sections.ts +355 -0
- package/src/prompts/response/types.ts +38 -0
- package/src/prompts/validation/ei.ts +93 -0
- package/src/prompts/validation/index.ts +6 -0
- package/src/prompts/validation/types.ts +22 -0
- package/src/storage/crypto.ts +96 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/interface.ts +9 -0
- package/src/storage/local.ts +79 -0
- package/src/storage/merge.ts +69 -0
- package/src/storage/remote.ts +145 -0
- package/src/templates/welcome.ts +91 -0
- package/tui/README.md +62 -0
- package/tui/bunfig.toml +4 -0
- package/tui/src/app.tsx +55 -0
- package/tui/src/commands/archive.tsx +93 -0
- package/tui/src/commands/context.tsx +124 -0
- package/tui/src/commands/delete.tsx +71 -0
- package/tui/src/commands/details.tsx +41 -0
- package/tui/src/commands/editor.tsx +46 -0
- package/tui/src/commands/help.tsx +12 -0
- package/tui/src/commands/me.tsx +145 -0
- package/tui/src/commands/model.ts +47 -0
- package/tui/src/commands/new.ts +31 -0
- package/tui/src/commands/pause.ts +46 -0
- package/tui/src/commands/persona.tsx +58 -0
- package/tui/src/commands/provider.tsx +124 -0
- package/tui/src/commands/quit.ts +22 -0
- package/tui/src/commands/quotes.tsx +172 -0
- package/tui/src/commands/registry.test.ts +137 -0
- package/tui/src/commands/registry.ts +130 -0
- package/tui/src/commands/resume.ts +39 -0
- package/tui/src/commands/setsync.tsx +43 -0
- package/tui/src/commands/settings.tsx +83 -0
- package/tui/src/components/ConfirmOverlay.tsx +51 -0
- package/tui/src/components/ConflictOverlay.tsx +78 -0
- package/tui/src/components/HelpOverlay.tsx +69 -0
- package/tui/src/components/Layout.tsx +24 -0
- package/tui/src/components/MessageList.tsx +174 -0
- package/tui/src/components/PersonaListOverlay.tsx +186 -0
- package/tui/src/components/PromptInput.tsx +145 -0
- package/tui/src/components/ProviderListOverlay.tsx +208 -0
- package/tui/src/components/QuotesOverlay.tsx +157 -0
- package/tui/src/components/Sidebar.tsx +95 -0
- package/tui/src/components/StatusBar.tsx +77 -0
- package/tui/src/components/WelcomeOverlay.tsx +73 -0
- package/tui/src/context/ei.tsx +623 -0
- package/tui/src/context/keyboard.tsx +164 -0
- package/tui/src/context/overlay.tsx +53 -0
- package/tui/src/index.tsx +8 -0
- package/tui/src/storage/file.ts +185 -0
- package/tui/src/util/duration.ts +32 -0
- package/tui/src/util/editor.ts +188 -0
- package/tui/src/util/logger.ts +109 -0
- package/tui/src/util/persona-editor.tsx +181 -0
- package/tui/src/util/provider-editor.tsx +168 -0
- package/tui/src/util/syntax.ts +35 -0
- package/tui/src/util/yaml-serializers.ts +755 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/solid";
|
|
2
|
+
|
|
3
|
+
interface WelcomeOverlayProps {
|
|
4
|
+
onDismiss: () => void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function WelcomeOverlay(props: WelcomeOverlayProps) {
|
|
8
|
+
useKeyboard((event) => {
|
|
9
|
+
event.preventDefault();
|
|
10
|
+
props.onDismiss();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<box
|
|
15
|
+
position="absolute"
|
|
16
|
+
width="100%"
|
|
17
|
+
height="100%"
|
|
18
|
+
left={0}
|
|
19
|
+
top={0}
|
|
20
|
+
backgroundColor="#000000"
|
|
21
|
+
alignItems="center"
|
|
22
|
+
justifyContent="center"
|
|
23
|
+
>
|
|
24
|
+
<box
|
|
25
|
+
width={70}
|
|
26
|
+
backgroundColor="#1a1a2e"
|
|
27
|
+
borderStyle="single"
|
|
28
|
+
borderColor="#586e75"
|
|
29
|
+
padding={2}
|
|
30
|
+
flexDirection="column"
|
|
31
|
+
>
|
|
32
|
+
<text fg="#eee8d5">
|
|
33
|
+
Welcome to Ei!
|
|
34
|
+
</text>
|
|
35
|
+
<text> </text>
|
|
36
|
+
|
|
37
|
+
<text fg="#dc322f">
|
|
38
|
+
No LLM provider detected.
|
|
39
|
+
</text>
|
|
40
|
+
<text> </text>
|
|
41
|
+
|
|
42
|
+
<text fg="#93a1a1">
|
|
43
|
+
To get started, you need a local LLM running or a provider configured.
|
|
44
|
+
</text>
|
|
45
|
+
<text> </text>
|
|
46
|
+
|
|
47
|
+
<text fg="#93a1a1">
|
|
48
|
+
Options:
|
|
49
|
+
</text>
|
|
50
|
+
<text fg="#93a1a1">
|
|
51
|
+
1. Start a local LLM (LM Studio, Ollama) on port 1234
|
|
52
|
+
</text>
|
|
53
|
+
<text fg="#93a1a1">
|
|
54
|
+
2. Run /provider new to configure a cloud provider
|
|
55
|
+
</text>
|
|
56
|
+
<text> </text>
|
|
57
|
+
|
|
58
|
+
<text fg="#657b83">
|
|
59
|
+
Once configured, restart Ei or run /provider new to add your provider.
|
|
60
|
+
</text>
|
|
61
|
+
<text> </text>
|
|
62
|
+
|
|
63
|
+
<text fg="#586e75">
|
|
64
|
+
Press any key to dismiss
|
|
65
|
+
</text>
|
|
66
|
+
<text> </text>
|
|
67
|
+
<text fg="#2a2a3e">
|
|
68
|
+
Ei - 永 (ei) - eternal
|
|
69
|
+
</text>
|
|
70
|
+
</box>
|
|
71
|
+
</box>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createContext,
|
|
3
|
+
useContext,
|
|
4
|
+
onMount,
|
|
5
|
+
onCleanup,
|
|
6
|
+
Match,
|
|
7
|
+
Switch,
|
|
8
|
+
createSignal,
|
|
9
|
+
type ParentComponent,
|
|
10
|
+
} from "solid-js";
|
|
11
|
+
import { createStore } from "solid-js/store";
|
|
12
|
+
import { Processor } from "../../../src/core/processor.js";
|
|
13
|
+
import { FileStorage } from "../storage/file.js";
|
|
14
|
+
import { remoteSync } from "../../../src/storage/remote.js";
|
|
15
|
+
import { logger, clearLog, interceptConsole } from "../util/logger.js";
|
|
16
|
+
import { ConflictOverlay } from "../components/ConflictOverlay.js";
|
|
17
|
+
import type {
|
|
18
|
+
Ei_Interface,
|
|
19
|
+
PersonaSummary,
|
|
20
|
+
PersonaEntity,
|
|
21
|
+
Message,
|
|
22
|
+
QueueStatus,
|
|
23
|
+
HumanEntity,
|
|
24
|
+
HumanSettings,
|
|
25
|
+
Fact,
|
|
26
|
+
Trait,
|
|
27
|
+
Topic,
|
|
28
|
+
Person,
|
|
29
|
+
Quote,
|
|
30
|
+
ProviderAccount,
|
|
31
|
+
ProviderType,
|
|
32
|
+
StateConflictData,
|
|
33
|
+
StateConflictResolution,
|
|
34
|
+
ContextStatus,
|
|
35
|
+
} from "../../../src/core/types.js";
|
|
36
|
+
|
|
37
|
+
interface EiStore {
|
|
38
|
+
ready: boolean;
|
|
39
|
+
personas: PersonaSummary[];
|
|
40
|
+
activePersonaId: string | null;
|
|
41
|
+
activeContextBoundary: string | undefined;
|
|
42
|
+
messages: Message[];
|
|
43
|
+
queueStatus: QueueStatus;
|
|
44
|
+
notification: { message: string; level: "error" | "warn" | "info" } | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface EiContextValue {
|
|
48
|
+
personas: () => PersonaSummary[];
|
|
49
|
+
activePersonaId: () => string | null;
|
|
50
|
+
activeContextBoundary: () => string | undefined;
|
|
51
|
+
messages: () => Message[];
|
|
52
|
+
queueStatus: () => QueueStatus;
|
|
53
|
+
notification: () => { message: string; level: "error" | "warn" | "info" } | null;
|
|
54
|
+
selectPersona: (personaId: string) => void;
|
|
55
|
+
sendMessage: (content: string) => Promise<void>;
|
|
56
|
+
refreshPersonas: () => Promise<void>;
|
|
57
|
+
refreshMessages: () => Promise<void>;
|
|
58
|
+
abortCurrentOperation: () => Promise<void>;
|
|
59
|
+
resumeQueue: () => Promise<void>;
|
|
60
|
+
stopProcessor: () => Promise<void>;
|
|
61
|
+
saveAndExit: () => Promise<{ success: boolean; error?: string }>;
|
|
62
|
+
showNotification: (message: string, level: "error" | "warn" | "info") => void;
|
|
63
|
+
createPersona: (input: { name: string }) => Promise<string>;
|
|
64
|
+
archivePersona: (personaId: string) => Promise<void>;
|
|
65
|
+
unarchivePersona: (personaId: string) => Promise<void>;
|
|
66
|
+
deletePersona: (personaId: string) => Promise<void>;
|
|
67
|
+
setContextBoundary: (personaId: string, timestamp: string | null) => Promise<void>;
|
|
68
|
+
updatePersona: (personaId: string, updates: Partial<PersonaEntity>) => Promise<void>;
|
|
69
|
+
getPersona: (personaId: string) => Promise<PersonaEntity | null>;
|
|
70
|
+
resolvePersonaName: (nameOrAlias: string) => Promise<string | null>;
|
|
71
|
+
getHuman: () => Promise<HumanEntity>;
|
|
72
|
+
updateHuman: (updates: Partial<HumanEntity>) => Promise<void>;
|
|
73
|
+
updateSettings: (updates: Partial<HumanSettings>) => Promise<void>;
|
|
74
|
+
upsertFact: (fact: Fact) => Promise<void>;
|
|
75
|
+
upsertTrait: (trait: Trait) => Promise<void>;
|
|
76
|
+
upsertTopic: (topic: Topic) => Promise<void>;
|
|
77
|
+
upsertPerson: (person: Person) => Promise<void>;
|
|
78
|
+
removeDataItem: (type: "fact" | "trait" | "topic" | "person", id: string) => Promise<void>;
|
|
79
|
+
syncStatus: () => { configured: boolean; envBased: boolean };
|
|
80
|
+
triggerSync: () => Promise<{ success: boolean; error?: string }>;
|
|
81
|
+
getGroupList: () => Promise<string[]>;
|
|
82
|
+
getQuotes: (filter?: { message_id?: string; speaker?: string }) => Promise<Quote[]>;
|
|
83
|
+
getQuotesForMessage: (messageId: string) => Promise<Quote[]>;
|
|
84
|
+
updateQuote: (id: string, updates: Partial<Quote>) => Promise<void>;
|
|
85
|
+
removeQuote: (id: string) => Promise<void>;
|
|
86
|
+
quotesVersion: () => number;
|
|
87
|
+
searchHumanData: (
|
|
88
|
+
query: string,
|
|
89
|
+
options?: { types?: Array<"fact" | "trait" | "topic" | "person" | "quote">; limit?: number }
|
|
90
|
+
) => Promise<{
|
|
91
|
+
facts: Fact[];
|
|
92
|
+
traits: Trait[];
|
|
93
|
+
topics: Topic[];
|
|
94
|
+
people: Person[];
|
|
95
|
+
quotes: Quote[];
|
|
96
|
+
}>;
|
|
97
|
+
showWelcomeOverlay: () => boolean;
|
|
98
|
+
dismissWelcomeOverlay: () => void;
|
|
99
|
+
deleteMessages: (personaId: string, messageIds: string[]) => Promise<void>;
|
|
100
|
+
setMessageContextStatus: (personaId: string, messageId: string, status: ContextStatus) => Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const EiContext = createContext<EiContextValue>();
|
|
104
|
+
|
|
105
|
+
export const EiProvider: ParentComponent = (props) => {
|
|
106
|
+
const [store, setStore] = createStore<EiStore>({
|
|
107
|
+
ready: false,
|
|
108
|
+
personas: [],
|
|
109
|
+
activePersonaId: null,
|
|
110
|
+
activeContextBoundary: undefined,
|
|
111
|
+
messages: [],
|
|
112
|
+
queueStatus: { state: "idle", pending_count: 0 },
|
|
113
|
+
notification: null,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const [contextBoundarySignal, setContextBoundarySignal] = createSignal<string | undefined>(undefined);
|
|
117
|
+
const [quotesVersion, setQuotesVersion] = createSignal(0);
|
|
118
|
+
const [showWelcomeOverlay, setShowWelcomeOverlay] = createSignal(false);
|
|
119
|
+
const [conflictData, setConflictData] = createSignal<StateConflictData | null>(null);
|
|
120
|
+
|
|
121
|
+
let processor: Processor | null = null;
|
|
122
|
+
let notificationTimer: Timer | null = null;
|
|
123
|
+
let readTimer: Timer | null = null;
|
|
124
|
+
let dwelledPersona: string | null = null;
|
|
125
|
+
let syncConfiguredFromEnv = false;
|
|
126
|
+
|
|
127
|
+
const showNotification = (message: string, level: "error" | "warn" | "info") => {
|
|
128
|
+
if (notificationTimer) clearTimeout(notificationTimer);
|
|
129
|
+
setStore("notification", { message, level });
|
|
130
|
+
notificationTimer = setTimeout(() => {
|
|
131
|
+
setStore("notification", null);
|
|
132
|
+
notificationTimer = null;
|
|
133
|
+
}, 5000);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const refreshPersonas = async () => {
|
|
137
|
+
if (!processor) return;
|
|
138
|
+
const list = await processor.getPersonaList();
|
|
139
|
+
setStore("personas", list);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const refreshMessages = async () => {
|
|
143
|
+
if (!processor) return;
|
|
144
|
+
const currentId = store.activePersonaId;
|
|
145
|
+
if (!currentId) return;
|
|
146
|
+
const msgs = await processor.getMessages(currentId);
|
|
147
|
+
setStore("messages", [...msgs]);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const selectPersona = (personaId: string) => {
|
|
151
|
+
// Mark previous persona as read ONLY if we dwelled there 5+ seconds
|
|
152
|
+
const previousId = store.activePersonaId;
|
|
153
|
+
if (previousId && previousId === dwelledPersona && processor) {
|
|
154
|
+
void processor.markAllMessagesRead(previousId);
|
|
155
|
+
void refreshPersonas();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Cancel any pending timer and reset dwell tracking
|
|
159
|
+
if (readTimer) {
|
|
160
|
+
clearTimeout(readTimer);
|
|
161
|
+
readTimer = null;
|
|
162
|
+
}
|
|
163
|
+
dwelledPersona = null;
|
|
164
|
+
|
|
165
|
+
// Set new persona
|
|
166
|
+
setStore("activePersonaId", personaId);
|
|
167
|
+
setStore("messages", []);
|
|
168
|
+
const persona = store.personas.find(p => p.id === personaId);
|
|
169
|
+
setStore("activeContextBoundary", persona?.context_boundary);
|
|
170
|
+
setContextBoundarySignal(persona?.context_boundary);
|
|
171
|
+
if (processor) {
|
|
172
|
+
processor.getMessages(personaId).then((msgs) => {
|
|
173
|
+
setStore("messages", [...msgs]);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Start 5-second dwell timer
|
|
178
|
+
readTimer = setTimeout(async () => {
|
|
179
|
+
if (store.activePersonaId === personaId && processor) {
|
|
180
|
+
dwelledPersona = personaId; // Mark that we've dwelled
|
|
181
|
+
await processor.markAllMessagesRead(personaId);
|
|
182
|
+
await refreshPersonas();
|
|
183
|
+
}
|
|
184
|
+
readTimer = null;
|
|
185
|
+
}, 5000);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const sendMessage = async (content: string) => {
|
|
189
|
+
const currentId = store.activePersonaId;
|
|
190
|
+
if (!currentId || !processor) return;
|
|
191
|
+
|
|
192
|
+
// Mark all read immediately - user is engaged
|
|
193
|
+
await processor.markAllMessagesRead(currentId);
|
|
194
|
+
dwelledPersona = currentId;
|
|
195
|
+
|
|
196
|
+
await processor.sendMessage(currentId, content);
|
|
197
|
+
await refreshPersonas();
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const abortCurrentOperation = async () => {
|
|
201
|
+
if (!processor) return;
|
|
202
|
+
logger.info("Aborting current LLM operation");
|
|
203
|
+
await processor.abortCurrentOperation();
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const resumeQueue = async () => {
|
|
207
|
+
if (!processor) return;
|
|
208
|
+
logger.info("Resuming queue");
|
|
209
|
+
await processor.resumeQueue();
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const stopProcessor = async () => {
|
|
213
|
+
if (processor) {
|
|
214
|
+
await processor.stop();
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const createPersona = async (input: { name: string }): Promise<string> => {
|
|
219
|
+
if (!processor) return "";
|
|
220
|
+
return await processor.createPersona(input);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const archivePersona = async (personaId: string) => {
|
|
224
|
+
if (!processor) return;
|
|
225
|
+
await processor.archivePersona(personaId);
|
|
226
|
+
await refreshPersonas();
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const unarchivePersona = async (personaId: string) => {
|
|
230
|
+
if (!processor) return;
|
|
231
|
+
await processor.unarchivePersona(personaId);
|
|
232
|
+
await refreshPersonas();
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const deletePersona = async (personaId: string) => {
|
|
236
|
+
if (!processor) return;
|
|
237
|
+
await processor.deletePersona(personaId, false);
|
|
238
|
+
await refreshPersonas();
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const setContextBoundary = async (personaId: string, timestamp: string | null) => {
|
|
242
|
+
if (!processor) return;
|
|
243
|
+
// Set signal BEFORE processor call - processor fires callback synchronously
|
|
244
|
+
// which triggers refreshMessages() that needs the NEW boundary value
|
|
245
|
+
const newValue = timestamp ?? undefined;
|
|
246
|
+
logger.debug(`setContextBoundary: ${personaId}, timestamp=${timestamp}, newValue=${newValue}`);
|
|
247
|
+
if (personaId === store.activePersonaId) {
|
|
248
|
+
logger.debug(`setContextBoundary: updating signal to ${newValue}`);
|
|
249
|
+
setContextBoundarySignal(newValue);
|
|
250
|
+
}
|
|
251
|
+
await processor.setContextBoundary(personaId, timestamp);
|
|
252
|
+
await refreshPersonas();
|
|
253
|
+
if (personaId === store.activePersonaId) {
|
|
254
|
+
await refreshMessages();
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const updatePersona = async (personaId: string, updates: Partial<PersonaEntity>) => {
|
|
259
|
+
if (!processor) return;
|
|
260
|
+
await processor.updatePersona(personaId, updates);
|
|
261
|
+
await refreshPersonas();
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const getPersona = async (personaId: string) => {
|
|
265
|
+
if (!processor) return null;
|
|
266
|
+
return processor.getPersona(personaId);
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const resolvePersonaName = async (nameOrAlias: string) => {
|
|
270
|
+
if (!processor) return null;
|
|
271
|
+
return processor.resolvePersonaName(nameOrAlias);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const getHuman = async () => {
|
|
275
|
+
if (!processor) throw new Error("Processor not initialized");
|
|
276
|
+
return processor.getHuman();
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const updateHuman = async (updates: Partial<HumanEntity>) => {
|
|
280
|
+
if (!processor) return;
|
|
281
|
+
await processor.updateHuman(updates);
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const upsertFact = async (fact: Fact) => {
|
|
285
|
+
if (!processor) return;
|
|
286
|
+
await processor.upsertFact(fact);
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const upsertTrait = async (trait: Trait) => {
|
|
290
|
+
if (!processor) return;
|
|
291
|
+
await processor.upsertTrait(trait);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
const upsertTopic = async (topic: Topic) => {
|
|
295
|
+
if (!processor) return;
|
|
296
|
+
await processor.upsertTopic(topic);
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const upsertPerson = async (person: Person) => {
|
|
300
|
+
if (!processor) return;
|
|
301
|
+
await processor.upsertPerson(person);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const removeDataItem = async (type: "fact" | "trait" | "topic" | "person", id: string) => {
|
|
305
|
+
if (!processor) return;
|
|
306
|
+
await processor.removeDataItem(type, id);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const saveAndExit = async (): Promise<{ success: boolean; error?: string }> => {
|
|
310
|
+
if (!processor) return { success: false, error: "Processor not initialized" };
|
|
311
|
+
return processor.saveAndExit();
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
const updateSettings = async (updates: Partial<HumanSettings>): Promise<void> => {
|
|
315
|
+
if (!processor) return;
|
|
316
|
+
const human = await processor.getHuman();
|
|
317
|
+
const newSettings = { ...human.settings, ...updates };
|
|
318
|
+
await processor.updateHuman({ settings: newSettings });
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const syncStatus = (): { configured: boolean; envBased: boolean } => {
|
|
322
|
+
return {
|
|
323
|
+
configured: remoteSync.isConfigured(),
|
|
324
|
+
envBased: syncConfiguredFromEnv,
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const triggerSync = async (): Promise<{ success: boolean; error?: string }> => {
|
|
329
|
+
if (!processor) return { success: false, error: "Processor not initialized" };
|
|
330
|
+
if (!remoteSync.isConfigured()) {
|
|
331
|
+
return { success: false, error: "Sync not configured" };
|
|
332
|
+
}
|
|
333
|
+
const human = await processor.getHuman();
|
|
334
|
+
const hasSyncCreds = !!human.settings?.sync?.username && !!human.settings?.sync?.passphrase;
|
|
335
|
+
if (!hasSyncCreds) {
|
|
336
|
+
return { success: false, error: "No sync credentials in settings" };
|
|
337
|
+
}
|
|
338
|
+
const state = await processor.getStorageState();
|
|
339
|
+
return remoteSync.sync(state);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const getGroupList = async (): Promise<string[]> => {
|
|
343
|
+
if (!processor) return [];
|
|
344
|
+
return processor.getGroupList();
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
const getQuotes = async (filter?: { message_id?: string; speaker?: string }): Promise<Quote[]> => {
|
|
348
|
+
if (!processor) return [];
|
|
349
|
+
const all = await processor.getQuotes(filter?.message_id ? { message_id: filter.message_id } : undefined);
|
|
350
|
+
if (filter?.speaker) {
|
|
351
|
+
return all.filter(q => q.speaker.toLowerCase() === filter.speaker!.toLowerCase());
|
|
352
|
+
}
|
|
353
|
+
return all;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const getQuotesForMessage = async (messageId: string): Promise<Quote[]> => {
|
|
357
|
+
if (!processor) return [];
|
|
358
|
+
return processor.getQuotesForMessage(messageId);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const updateQuote = async (id: string, updates: Partial<Quote>): Promise<void> => {
|
|
362
|
+
if (!processor) return;
|
|
363
|
+
await processor.updateQuote(id, updates);
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const removeQuote = async (id: string): Promise<void> => {
|
|
367
|
+
if (!processor) return;
|
|
368
|
+
await processor.removeQuote(id);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const deleteMessages = async (personaId: string, messageIds: string[]): Promise<void> => {
|
|
372
|
+
if (!processor) return;
|
|
373
|
+
await processor.deleteMessages(personaId, messageIds);
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const setMessageContextStatus = async (personaId: string, messageId: string, status: ContextStatus): Promise<void> => {
|
|
377
|
+
if (!processor) return;
|
|
378
|
+
await processor.setMessageContextStatus(personaId, messageId, status);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
const searchHumanData = async (
|
|
382
|
+
query: string,
|
|
383
|
+
options?: { types?: Array<"fact" | "trait" | "topic" | "person" | "quote">; limit?: number }
|
|
384
|
+
) => {
|
|
385
|
+
if (!processor) return { facts: [], traits: [], topics: [], people: [], quotes: [] };
|
|
386
|
+
return processor.searchHumanData(query, options);
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
// Post-start initialization: refresh UI state, select first persona, detect LLM
|
|
390
|
+
async function finishBootstrap() {
|
|
391
|
+
if (!processor) return;
|
|
392
|
+
|
|
393
|
+
// If env vars provided sync creds, ensure they're written to settings
|
|
394
|
+
// (needed for first-ever-use where bootstrapFirstRun was called)
|
|
395
|
+
const syncUsername = Bun.env.EI_SYNC_USERNAME;
|
|
396
|
+
const syncPassphrase = Bun.env.EI_SYNC_PASSPHRASE;
|
|
397
|
+
if (syncUsername && syncPassphrase) {
|
|
398
|
+
const human = await processor.getHuman();
|
|
399
|
+
if (!human.settings?.sync?.username || !human.settings?.sync?.passphrase) {
|
|
400
|
+
await processor.updateHuman({
|
|
401
|
+
settings: { ...human.settings, sync: { username: syncUsername, passphrase: syncPassphrase } }
|
|
402
|
+
});
|
|
403
|
+
logger.debug("Sync credentials written to settings");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
await refreshPersonas();
|
|
407
|
+
logger.debug(`refreshPersonas done, count: ${store.personas.length}`);
|
|
408
|
+
const status = await processor.getQueueStatus();
|
|
409
|
+
logger.debug("Initial getQueueStatus:", status);
|
|
410
|
+
setStore("queueStatus", status);
|
|
411
|
+
logger.debug("Initial queueStatus set in store:", store.queueStatus);
|
|
412
|
+
const list = store.personas;
|
|
413
|
+
if (list.length > 0 && !store.activePersonaId && list[0].id) {
|
|
414
|
+
selectPersona(list[0].id);
|
|
415
|
+
}
|
|
416
|
+
// LLM detection: run async after processor starts, don't block ready state
|
|
417
|
+
void (async () => {
|
|
418
|
+
try {
|
|
419
|
+
const human = await processor!.getHuman();
|
|
420
|
+
const hasAccounts = human.settings?.accounts && human.settings.accounts.length > 0;
|
|
421
|
+
if (!hasAccounts) {
|
|
422
|
+
logger.info("No LLM accounts configured, checking for local LLM...");
|
|
423
|
+
try {
|
|
424
|
+
const response = await fetch("http://127.0.0.1:1234/v1/models", {
|
|
425
|
+
method: "GET",
|
|
426
|
+
signal: AbortSignal.timeout(3000),
|
|
427
|
+
});
|
|
428
|
+
if (response.ok) {
|
|
429
|
+
logger.info("Local LLM detected, auto-configuring...");
|
|
430
|
+
const localAccount: ProviderAccount = {
|
|
431
|
+
id: crypto.randomUUID(),
|
|
432
|
+
name: "Local LLM",
|
|
433
|
+
type: "llm" as ProviderType,
|
|
434
|
+
url: "http://127.0.0.1:1234/v1",
|
|
435
|
+
enabled: true,
|
|
436
|
+
created_at: new Date().toISOString(),
|
|
437
|
+
};
|
|
438
|
+
const currentHuman = await processor!.getHuman();
|
|
439
|
+
await processor!.updateHuman({
|
|
440
|
+
settings: {
|
|
441
|
+
...currentHuman.settings,
|
|
442
|
+
accounts: [localAccount],
|
|
443
|
+
default_model: "Local LLM",
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
showNotification("Local LLM detected and configured!", "info");
|
|
447
|
+
logger.info("Local LLM auto-configured successfully");
|
|
448
|
+
} else {
|
|
449
|
+
logger.info("Local LLM check failed, showing welcome overlay");
|
|
450
|
+
setShowWelcomeOverlay(true);
|
|
451
|
+
}
|
|
452
|
+
} catch {
|
|
453
|
+
logger.info("No local LLM found, showing welcome overlay");
|
|
454
|
+
setShowWelcomeOverlay(true);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
} catch (err: any) {
|
|
458
|
+
logger.warn(`LLM detection failed: ${err?.message || err}`);
|
|
459
|
+
}
|
|
460
|
+
})();
|
|
461
|
+
setStore("ready", true);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const resolveStateConflict = async (resolution: StateConflictResolution): Promise<void> => {
|
|
465
|
+
if (!processor) return;
|
|
466
|
+
logger.info(`Resolving state conflict: ${resolution}`);
|
|
467
|
+
await processor.resolveStateConflict(resolution);
|
|
468
|
+
setConflictData(null);
|
|
469
|
+
await finishBootstrap();
|
|
470
|
+
};
|
|
471
|
+
async function bootstrap() {
|
|
472
|
+
clearLog();
|
|
473
|
+
interceptConsole();
|
|
474
|
+
logger.info("Ei TUI bootstrap starting");
|
|
475
|
+
try {
|
|
476
|
+
const storage = new FileStorage(Bun.env.EI_DATA_PATH);
|
|
477
|
+
// Pre-configure remoteSync from env vars BEFORE processor.start()
|
|
478
|
+
// so the processor's sync decision tree can detect remote state
|
|
479
|
+
const syncUsername = Bun.env.EI_SYNC_USERNAME;
|
|
480
|
+
const syncPassphrase = Bun.env.EI_SYNC_PASSPHRASE;
|
|
481
|
+
if (syncUsername && syncPassphrase) {
|
|
482
|
+
logger.info("Sync credentials found in env, pre-configuring remoteSync");
|
|
483
|
+
await remoteSync.configure({ username: syncUsername, passphrase: syncPassphrase });
|
|
484
|
+
syncConfiguredFromEnv = true;
|
|
485
|
+
}
|
|
486
|
+
const eiInterface: Ei_Interface = {
|
|
487
|
+
onPersonaAdded: () => void refreshPersonas(),
|
|
488
|
+
onPersonaRemoved: () => void refreshPersonas(),
|
|
489
|
+
onPersonaUpdated: () => void refreshPersonas(),
|
|
490
|
+
onMessageAdded: (personaId) => {
|
|
491
|
+
void refreshPersonas();
|
|
492
|
+
if (personaId === store.activePersonaId) {
|
|
493
|
+
void refreshMessages();
|
|
494
|
+
}
|
|
495
|
+
},
|
|
496
|
+
onQueueStateChanged: (state) => {
|
|
497
|
+
logger.debug(`onQueueStateChanged called with state: ${state}`);
|
|
498
|
+
if (processor) {
|
|
499
|
+
processor.getQueueStatus().then((status) => {
|
|
500
|
+
setStore("queueStatus", { state: status.state, pending_count: status.pending_count });
|
|
501
|
+
logger.debug(`store.queueStatus after setStore:`, store.queueStatus);
|
|
502
|
+
});
|
|
503
|
+
} else {
|
|
504
|
+
setStore("queueStatus", { state, pending_count: 0 });
|
|
505
|
+
}
|
|
506
|
+
},
|
|
507
|
+
onContextBoundaryChanged: (personaId) => {
|
|
508
|
+
logger.debug(`onContextBoundaryChanged: ${personaId}`);
|
|
509
|
+
void refreshPersonas();
|
|
510
|
+
},
|
|
511
|
+
onQuoteAdded: () => setQuotesVersion(v => v + 1),
|
|
512
|
+
onQuoteUpdated: () => setQuotesVersion(v => v + 1),
|
|
513
|
+
onQuoteRemoved: () => setQuotesVersion(v => v + 1),
|
|
514
|
+
onError: (error) => {
|
|
515
|
+
logger.error(`${error.code}: ${error.message}`);
|
|
516
|
+
showNotification(`${error.code}: ${error.message}`, "error");
|
|
517
|
+
},
|
|
518
|
+
onStateConflict: (data) => {
|
|
519
|
+
logger.info("State conflict detected, waiting for user resolution");
|
|
520
|
+
setConflictData(data);
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
processor = new Processor(eiInterface);
|
|
524
|
+
logger.debug("Processor created, calling start()");
|
|
525
|
+
await processor.start(storage);
|
|
526
|
+
logger.debug("Processor started");
|
|
527
|
+
// If start() detected a conflict, it returned without starting the loop.
|
|
528
|
+
// Don't set ready — wait for resolveStateConflict() to be called.
|
|
529
|
+
if (conflictData()) {
|
|
530
|
+
logger.info("Conflict pending, waiting for user resolution before finishing bootstrap");
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
await finishBootstrap();
|
|
535
|
+
} catch (err: any) {
|
|
536
|
+
logger.error(`bootstrap() failed: ${err?.message || err}`);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
onMount(() => {
|
|
541
|
+
void bootstrap();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
onCleanup(() => {
|
|
545
|
+
if (readTimer) clearTimeout(readTimer);
|
|
546
|
+
processor?.stop();
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
const value: EiContextValue = {
|
|
550
|
+
personas: () => store.personas,
|
|
551
|
+
activePersonaId: () => store.activePersonaId,
|
|
552
|
+
activeContextBoundary: contextBoundarySignal,
|
|
553
|
+
messages: () => store.messages,
|
|
554
|
+
queueStatus: () => store.queueStatus,
|
|
555
|
+
notification: () => store.notification,
|
|
556
|
+
selectPersona,
|
|
557
|
+
sendMessage,
|
|
558
|
+
refreshPersonas,
|
|
559
|
+
refreshMessages,
|
|
560
|
+
abortCurrentOperation,
|
|
561
|
+
resumeQueue,
|
|
562
|
+
stopProcessor,
|
|
563
|
+
saveAndExit,
|
|
564
|
+
showNotification,
|
|
565
|
+
createPersona,
|
|
566
|
+
archivePersona,
|
|
567
|
+
unarchivePersona,
|
|
568
|
+
deletePersona,
|
|
569
|
+
setContextBoundary,
|
|
570
|
+
updatePersona,
|
|
571
|
+
getPersona,
|
|
572
|
+
resolvePersonaName,
|
|
573
|
+
getHuman,
|
|
574
|
+
updateHuman,
|
|
575
|
+
updateSettings,
|
|
576
|
+
upsertFact,
|
|
577
|
+
upsertTrait,
|
|
578
|
+
upsertTopic,
|
|
579
|
+
upsertPerson,
|
|
580
|
+
removeDataItem,
|
|
581
|
+
syncStatus,
|
|
582
|
+
triggerSync,
|
|
583
|
+
getGroupList,
|
|
584
|
+
getQuotes,
|
|
585
|
+
getQuotesForMessage,
|
|
586
|
+
updateQuote,
|
|
587
|
+
removeQuote,
|
|
588
|
+
quotesVersion,
|
|
589
|
+
searchHumanData,
|
|
590
|
+
showWelcomeOverlay,
|
|
591
|
+
dismissWelcomeOverlay: () => setShowWelcomeOverlay(false),
|
|
592
|
+
deleteMessages,
|
|
593
|
+
setMessageContextStatus,
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
return (
|
|
597
|
+
<Switch>
|
|
598
|
+
<Match when={conflictData()}>
|
|
599
|
+
<ConflictOverlay
|
|
600
|
+
localTimestamp={conflictData()!.localTimestamp}
|
|
601
|
+
remoteTimestamp={conflictData()!.remoteTimestamp}
|
|
602
|
+
onResolve={(resolution) => void resolveStateConflict(resolution)}
|
|
603
|
+
/>
|
|
604
|
+
</Match>
|
|
605
|
+
<Match when={store.ready}>
|
|
606
|
+
<EiContext.Provider value={value}>{props.children}</EiContext.Provider>
|
|
607
|
+
</Match>
|
|
608
|
+
<Match when={!store.ready}>
|
|
609
|
+
<box width="100%" height="100%" justifyContent="center" alignItems="center">
|
|
610
|
+
<text>Loading Ei...</text>
|
|
611
|
+
</box>
|
|
612
|
+
</Match>
|
|
613
|
+
</Switch>
|
|
614
|
+
);
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
export const useEi = () => {
|
|
618
|
+
const ctx = useContext(EiContext);
|
|
619
|
+
if (!ctx) {
|
|
620
|
+
throw new Error("useEi must be used within EiProvider");
|
|
621
|
+
}
|
|
622
|
+
return ctx;
|
|
623
|
+
};
|