ei-tui 0.9.4 → 1.0.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.
- package/README.md +22 -3
- package/package.json +5 -1
- package/src/README.md +9 -25
- package/src/core/handlers/document-segmentation.ts +113 -0
- package/src/core/handlers/index.ts +2 -0
- package/src/core/handlers/rewrite.ts +13 -9
- package/src/core/heartbeat-manager.ts +2 -2
- package/src/core/llm-client.ts +11 -1
- package/src/core/message-manager.ts +20 -18
- package/src/core/orchestrators/ceremony.ts +83 -40
- package/src/core/orchestrators/human-extraction.ts +5 -1
- package/src/core/persona-manager.ts +4 -0
- package/src/core/processor.ts +90 -1
- package/src/core/queue-manager.ts +35 -0
- package/src/core/state/queue.ts +9 -1
- package/src/core/state-manager.ts +4 -0
- package/src/core/types/entities.ts +15 -0
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +2 -0
- package/src/core/types/llm.ts +9 -0
- package/src/integrations/document/chunker.ts +88 -0
- package/src/integrations/document/importer.ts +82 -0
- package/src/integrations/document/index.ts +2 -0
- package/src/integrations/document/invoice.ts +63 -0
- package/src/integrations/document/types.ts +16 -0
- package/src/integrations/document/unsource.ts +164 -0
- package/src/integrations/persona-history/importer.ts +197 -0
- package/src/integrations/persona-history/index.ts +3 -0
- package/src/integrations/persona-history/types.ts +7 -0
- package/src/prompts/ceremony/dedup.ts +7 -3
- package/src/prompts/ceremony/index.ts +2 -1
- package/src/prompts/ceremony/people-rewrite.ts +190 -0
- package/src/prompts/ceremony/{rewrite.ts → topic-rewrite.ts} +103 -78
- package/src/prompts/human/person-scan.ts +13 -4
- package/src/prompts/human/topic-scan.ts +16 -2
- package/src/prompts/human/topic-update.ts +36 -4
- package/src/prompts/human/types.ts +1 -0
- package/src/storage/indexed.ts +4 -0
- package/src/storage/interface.ts +1 -0
- package/src/storage/local.ts +4 -0
- package/src/templates/emmett.ts +49 -0
- package/tui/README.md +22 -0
- package/tui/src/app.tsx +9 -6
- package/tui/src/commands/delete.tsx +7 -1
- package/tui/src/commands/import.tsx +30 -0
- package/tui/src/commands/unsource.tsx +115 -0
- package/tui/src/components/PromptInput.tsx +4 -0
- package/tui/src/components/WelcomeOverlay.tsx +58 -32
- package/tui/src/context/ei.tsx +80 -60
- package/tui/src/index.tsx +14 -0
- package/tui/src/storage/file.ts +11 -5
- package/tui/src/util/e2e-flags.ts +4 -3
- package/tui/src/util/help-content.ts +20 -0
- package/tui/src/util/provider-detection.ts +251 -0
- package/tui/src/util/yaml-human.ts +7 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { Command } from "./registry";
|
|
2
|
+
import { ConfirmOverlay } from "../components/ConfirmOverlay";
|
|
3
|
+
import { PersonaListOverlay } from "../components/PersonaListOverlay";
|
|
4
|
+
|
|
5
|
+
export const unsourceCommand: Command = {
|
|
6
|
+
name: "unsource",
|
|
7
|
+
aliases: [],
|
|
8
|
+
description: "Remove knowledge extracted from a specific document source",
|
|
9
|
+
usage: "/unsource <sourceTag>",
|
|
10
|
+
|
|
11
|
+
async execute(args, ctx) {
|
|
12
|
+
if (args.length === 0) {
|
|
13
|
+
const human = await ctx.ei.getHuman();
|
|
14
|
+
const docs = human.settings?.document?.processed_documents ?? {};
|
|
15
|
+
const sources = Object.keys(docs);
|
|
16
|
+
|
|
17
|
+
if (sources.length === 0) {
|
|
18
|
+
ctx.showNotification("No imported documents found. Use /import first.", "warn");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const items = sources.map(f => ({
|
|
23
|
+
id: `import:document:${f}`,
|
|
24
|
+
display_name: `import:document:${f}`,
|
|
25
|
+
aliases: [] as string[],
|
|
26
|
+
is_paused: false,
|
|
27
|
+
is_archived: false,
|
|
28
|
+
unread_count: 0,
|
|
29
|
+
has_pending_update: false,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
ctx.showOverlay((hideOverlay) => (
|
|
33
|
+
<PersonaListOverlay
|
|
34
|
+
personas={items}
|
|
35
|
+
activePersonaId={null}
|
|
36
|
+
title="Select source to unsource"
|
|
37
|
+
onSelect={async (sourceTag) => {
|
|
38
|
+
hideOverlay();
|
|
39
|
+
await unsourceCommand.execute([sourceTag], ctx);
|
|
40
|
+
}}
|
|
41
|
+
onDismiss={hideOverlay}
|
|
42
|
+
/>
|
|
43
|
+
), ctx.renderer);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const rawArg = args.join(" ").trim();
|
|
48
|
+
|
|
49
|
+
let sourceTag = rawArg;
|
|
50
|
+
if (!rawArg.includes(":")) {
|
|
51
|
+
const human = await ctx.ei.getHuman();
|
|
52
|
+
const docs = human.settings?.document?.processed_documents ?? {};
|
|
53
|
+
const allSources = Object.keys(docs).map(f => `import:document:${f}`);
|
|
54
|
+
const matches = allSources.filter(s => s.endsWith(rawArg) || s.includes(rawArg));
|
|
55
|
+
if (matches.length === 1) {
|
|
56
|
+
sourceTag = matches[0];
|
|
57
|
+
} else if (matches.length > 1) {
|
|
58
|
+
ctx.showNotification(`Ambiguous: "${rawArg}" matches multiple sources. Use /unsource with no args to pick.`, "warn");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const preview = ctx.ei.getUnsourcePreview(sourceTag);
|
|
64
|
+
|
|
65
|
+
const totalDelete =
|
|
66
|
+
preview.toDelete.facts.length +
|
|
67
|
+
preview.toDelete.topics.length +
|
|
68
|
+
preview.toDelete.people.length;
|
|
69
|
+
const totalStrip =
|
|
70
|
+
preview.toStrip.facts.length +
|
|
71
|
+
preview.toStrip.topics.length +
|
|
72
|
+
preview.toStrip.people.length;
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
totalDelete === 0 &&
|
|
76
|
+
preview.toDelete.quotes.length === 0 &&
|
|
77
|
+
totalStrip === 0
|
|
78
|
+
) {
|
|
79
|
+
ctx.showNotification(`No knowledge found for source: ${sourceTag}`, "warn");
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const confirmed = await new Promise<boolean>((resolve) => {
|
|
84
|
+
const msg = [
|
|
85
|
+
`Unsource: ${sourceTag}`,
|
|
86
|
+
"",
|
|
87
|
+
`Delete: ${preview.toDelete.facts.length} facts, ${preview.toDelete.topics.length} topics, ${preview.toDelete.people.length} people, ${preview.toDelete.quotes.length} quotes`,
|
|
88
|
+
`Strip source: ${preview.toStrip.facts.length} facts, ${preview.toStrip.topics.length} topics, ${preview.toStrip.people.length} people`,
|
|
89
|
+
"",
|
|
90
|
+
"This cannot be undone. Proceed? [y/N]",
|
|
91
|
+
].join("\n");
|
|
92
|
+
|
|
93
|
+
ctx.showOverlay((hideOverlay, _hideForEditor) => (
|
|
94
|
+
<ConfirmOverlay
|
|
95
|
+
message={msg}
|
|
96
|
+
onConfirm={() => { hideOverlay(); resolve(true); }}
|
|
97
|
+
onCancel={() => { hideOverlay(); resolve(false); }}
|
|
98
|
+
/>
|
|
99
|
+
), ctx.renderer);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (!confirmed) {
|
|
103
|
+
ctx.showNotification("Cancelled", "info");
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const result = await ctx.ei.executeUnsource(preview);
|
|
108
|
+
const deletedTotal = result.deleted.facts + result.deleted.topics + result.deleted.people + result.deleted.quotes;
|
|
109
|
+
const strippedTotal = result.stripped.facts + result.stripped.topics + result.stripped.people;
|
|
110
|
+
ctx.showNotification(
|
|
111
|
+
`Unsourced ${sourceTag}: deleted ${deletedTotal} items, stripped source from ${strippedTotal} items`,
|
|
112
|
+
"info"
|
|
113
|
+
);
|
|
114
|
+
},
|
|
115
|
+
};
|
|
@@ -30,6 +30,8 @@ import { activateCommand } from "../commands/activate.js";
|
|
|
30
30
|
import { reflectCommand } from "../commands/reflect.js";
|
|
31
31
|
import { silenceCommand } from "../commands/silence.js";
|
|
32
32
|
import { captureCommand } from "../commands/capture.js";
|
|
33
|
+
import { importCommand } from "../commands/import.js";
|
|
34
|
+
import { unsourceCommand } from "../commands/unsource.js";
|
|
33
35
|
import { openCYPEditor } from "../util/cyp-editor.js";
|
|
34
36
|
import { useOverlay } from "../context/overlay";
|
|
35
37
|
import { CommandSuggest } from "./CommandSuggest";
|
|
@@ -86,6 +88,8 @@ export function PromptInput() {
|
|
|
86
88
|
registerCommand(activateCommand);
|
|
87
89
|
registerCommand(silenceCommand);
|
|
88
90
|
registerCommand(captureCommand);
|
|
91
|
+
registerCommand(importCommand);
|
|
92
|
+
registerCommand(unsourceCommand);
|
|
89
93
|
registerCommand(authCommand);
|
|
90
94
|
registerCommand(pauseCommand);
|
|
91
95
|
registerCommand(resumeCommand);
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { useKeyboard } from "@opentui/solid";
|
|
2
|
-
import { onMount, onCleanup } from "solid-js";
|
|
2
|
+
import { onMount, onCleanup, For } from "solid-js";
|
|
3
3
|
import { useKeyboardNav } from "../context/keyboard.js";
|
|
4
|
+
import type { ProviderDetectionStatus } from "../util/provider-detection.js";
|
|
4
5
|
|
|
5
6
|
interface WelcomeOverlayProps {
|
|
6
7
|
onDismiss: () => void;
|
|
8
|
+
detectedProviders: ProviderDetectionStatus[];
|
|
9
|
+
defaultModel?: string;
|
|
7
10
|
}
|
|
8
11
|
|
|
12
|
+
const COLUMNS = 3;
|
|
13
|
+
|
|
9
14
|
export function WelcomeOverlay(props: WelcomeOverlayProps) {
|
|
10
15
|
const { setOverlayActive } = useKeyboardNav();
|
|
11
16
|
onMount(() => setOverlayActive(true));
|
|
@@ -16,6 +21,17 @@ export function WelcomeOverlay(props: WelcomeOverlayProps) {
|
|
|
16
21
|
props.onDismiss();
|
|
17
22
|
});
|
|
18
23
|
|
|
24
|
+
const hasAny = () => props.detectedProviders.some((p) => p.detected);
|
|
25
|
+
|
|
26
|
+
const rows = () => {
|
|
27
|
+
const items = props.detectedProviders;
|
|
28
|
+
const out: ProviderDetectionStatus[][] = [];
|
|
29
|
+
for (let i = 0; i < items.length; i += COLUMNS) {
|
|
30
|
+
out.push(items.slice(i, i + COLUMNS));
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
};
|
|
34
|
+
|
|
19
35
|
return (
|
|
20
36
|
<box
|
|
21
37
|
position="absolute"
|
|
@@ -35,44 +51,54 @@ export function WelcomeOverlay(props: WelcomeOverlayProps) {
|
|
|
35
51
|
padding={2}
|
|
36
52
|
flexDirection="column"
|
|
37
53
|
>
|
|
38
|
-
<text fg="#eee8d5">
|
|
39
|
-
Welcome to Ei!
|
|
40
|
-
</text>
|
|
54
|
+
<text fg="#eee8d5">Welcome to Ei!</text>
|
|
41
55
|
<text> </text>
|
|
42
56
|
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
<text> </text>
|
|
57
|
+
<box visible={hasAny()} flexDirection="column">
|
|
58
|
+
<text fg="#93a1a1">Detected providers:</text>
|
|
59
|
+
<text> </text>
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
61
|
+
<For each={rows()}>
|
|
62
|
+
{(row) => (
|
|
63
|
+
<box flexDirection="row">
|
|
64
|
+
<For each={row}>
|
|
65
|
+
{(provider) => (
|
|
66
|
+
<box width={22} flexDirection="row">
|
|
67
|
+
<text fg="#93a1a1">{provider.name}:</text>
|
|
68
|
+
<text> </text>
|
|
69
|
+
<text fg={provider.detected ? "#859900" : "#dc322f"}>
|
|
70
|
+
{provider.detected ? "[✓]" : "[✗]"}
|
|
71
|
+
</text>
|
|
72
|
+
</box>
|
|
73
|
+
)}
|
|
74
|
+
</For>
|
|
75
|
+
</box>
|
|
76
|
+
)}
|
|
77
|
+
</For>
|
|
52
78
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
79
|
+
<text> </text>
|
|
80
|
+
<box visible={!!props.defaultModel} flexDirection="row">
|
|
81
|
+
<text fg="#657b83">Default model: </text>
|
|
82
|
+
<text fg="#eee8d5">{props.defaultModel ?? ""}</text>
|
|
83
|
+
</box>
|
|
84
|
+
<text> </text>
|
|
85
|
+
<text fg="#93a1a1">To chat with a smarter model, try: /provider</text>
|
|
86
|
+
<text fg="#93a1a1">To change your default, use: /settings</text>
|
|
87
|
+
<text fg="#93a1a1">See /help for... well, Help!</text>
|
|
88
|
+
</box>
|
|
63
89
|
|
|
64
|
-
<
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
90
|
+
<box visible={!hasAny()} flexDirection="column">
|
|
91
|
+
<text fg="#dc322f">No LLM provider detected.</text>
|
|
92
|
+
<text> </text>
|
|
93
|
+
<text fg="#93a1a1">Start LMStudio (port 1234) or Ollama (port 11434), or</text>
|
|
94
|
+
<text fg="#93a1a1">set one of: ANTHROPIC_API_KEY, OPENAI_API_KEY, GROQ_API_KEY,</text>
|
|
95
|
+
<text fg="#93a1a1">MISTRAL_API_KEY, GEMINI_API_KEY and restart.</text>
|
|
96
|
+
</box>
|
|
68
97
|
|
|
69
|
-
<text fg="#586e75">
|
|
70
|
-
Press any key to dismiss
|
|
71
|
-
</text>
|
|
72
98
|
<text> </text>
|
|
73
|
-
<text fg="#
|
|
74
|
-
|
|
75
|
-
</text>
|
|
99
|
+
<text fg="#586e75">Press any key to continue</text>
|
|
100
|
+
<text> </text>
|
|
101
|
+
<text fg="#2a2a3e">Ei - 永 (ei) - eternal</text>
|
|
76
102
|
</box>
|
|
77
103
|
</box>
|
|
78
104
|
);
|
package/tui/src/context/ei.tsx
CHANGED
|
@@ -14,7 +14,13 @@ import { Processor } from "../../../src/core/processor.js";
|
|
|
14
14
|
import { FileStorage } from "../storage/file.js";
|
|
15
15
|
import { remoteSync } from "../../../src/storage/remote.js";
|
|
16
16
|
import { logger, clearLog, interceptConsole } from "../util/logger.js";
|
|
17
|
-
import { E2E_SKIP_LOCAL_DETECT } from "../util/e2e-flags.js";
|
|
17
|
+
import { E2E_SKIP_LOCAL_DETECT, E2E_SKIP_CLOUD_DETECT } from "../util/e2e-flags.js";
|
|
18
|
+
import {
|
|
19
|
+
detectProviders,
|
|
20
|
+
buildProviderAccounts,
|
|
21
|
+
ALL_PROVIDER_NAMES,
|
|
22
|
+
} from "../util/provider-detection.js";
|
|
23
|
+
import type { ProviderDetectionStatus } from "../util/provider-detection.js";
|
|
18
24
|
import { ConflictOverlay } from "../components/ConflictOverlay.js";
|
|
19
25
|
import type {
|
|
20
26
|
Ei_Interface,
|
|
@@ -28,8 +34,6 @@ import type {
|
|
|
28
34
|
Topic,
|
|
29
35
|
Person,
|
|
30
36
|
Quote,
|
|
31
|
-
ProviderAccount,
|
|
32
|
-
ProviderType,
|
|
33
37
|
StateConflictData,
|
|
34
38
|
StateConflictResolution,
|
|
35
39
|
ContextStatus,
|
|
@@ -109,6 +113,8 @@ export interface EiContextValue {
|
|
|
109
113
|
}>;
|
|
110
114
|
showWelcomeOverlay: () => boolean;
|
|
111
115
|
dismissWelcomeOverlay: () => void;
|
|
116
|
+
detectedProviders: () => ProviderDetectionStatus[];
|
|
117
|
+
firstBootDefaultModel: () => string | undefined;
|
|
112
118
|
deleteMessages: (personaId: string, messageIds: string[]) => Promise<void>;
|
|
113
119
|
setMessageContextStatus: (personaId: string, messageId: string, status: ContextStatus) => Promise<void>;
|
|
114
120
|
deleteRoomMessages: (roomId: string, messageIds: string[]) => Promise<void>;
|
|
@@ -146,6 +152,9 @@ export interface EiContextValue {
|
|
|
146
152
|
humanRoomMessagePending: () => boolean;
|
|
147
153
|
getArchivedRooms: () => RoomSummary[];
|
|
148
154
|
generatePersonaPreview: (name: string, description: string, relationship?: string, personaId?: string) => Promise<import('../../../src/prompts/generation/types.js').PersonaGenerationResult>;
|
|
155
|
+
importDocument: (filePath: string) => Promise<import('../../../src/integrations/document/types.js').DocumentImportResult>;
|
|
156
|
+
getUnsourcePreview: (sourceTag: string) => import('../../../src/integrations/document/unsource.js').UnsourcePreview;
|
|
157
|
+
executeUnsource: (preview: import('../../../src/integrations/document/unsource.js').UnsourcePreview) => Promise<import('../../../src/integrations/document/unsource.js').UnsourceResult>;
|
|
149
158
|
}
|
|
150
159
|
const EiContext = createContext<EiContextValue>();
|
|
151
160
|
|
|
@@ -168,6 +177,8 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
168
177
|
const [contextBoundarySignal, setContextBoundarySignal] = createSignal<string | undefined>(undefined);
|
|
169
178
|
const [quotesVersion, setQuotesVersion] = createSignal(0);
|
|
170
179
|
const [showWelcomeOverlay, setShowWelcomeOverlay] = createSignal(false);
|
|
180
|
+
const [detectedProviders, setDetectedProviders] = createSignal<ProviderDetectionStatus[]>([]);
|
|
181
|
+
const [firstBootDefaultModel, setFirstBootDefaultModel] = createSignal<string | undefined>(undefined);
|
|
171
182
|
const [conflictData, setConflictData] = createSignal<StateConflictData | null>(null);
|
|
172
183
|
|
|
173
184
|
let processor: Processor | null = null;
|
|
@@ -175,6 +186,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
175
186
|
let readTimer: Timer | null = null;
|
|
176
187
|
let dwelledPersona: string | null = null;
|
|
177
188
|
let syncConfiguredFromEnv = false;
|
|
189
|
+
let eiDataPath = "";
|
|
178
190
|
|
|
179
191
|
const showNotification = (message: string, level: "error" | "warn" | "info") => {
|
|
180
192
|
if (notificationTimer) clearTimeout(notificationTimer);
|
|
@@ -320,6 +332,32 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
320
332
|
return processor.generatePersonaPreview(name, description, relationship, personaId);
|
|
321
333
|
};
|
|
322
334
|
|
|
335
|
+
const importDocument = async (filePath: string) => {
|
|
336
|
+
if (!processor) throw new Error("Processor not ready");
|
|
337
|
+
const { readFile } = await import("node:fs/promises");
|
|
338
|
+
const { basename } = await import("node:path");
|
|
339
|
+
const { homedir } = await import("node:os");
|
|
340
|
+
const expandedPath = filePath === "~" || filePath.startsWith("~/")
|
|
341
|
+
? homedir() + filePath.slice(1)
|
|
342
|
+
: filePath.replace(/^\$HOME(?=\/|$)/, homedir());
|
|
343
|
+
const content = await readFile(expandedPath, "utf-8");
|
|
344
|
+
const filename = basename(expandedPath);
|
|
345
|
+
return processor.importDocument(content, filename);
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const getUnsourcePreview = (sourceTag: string) => {
|
|
349
|
+
if (!processor) throw new Error("Processor not ready");
|
|
350
|
+
return processor.getUnsourcePreview(sourceTag);
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
const executeUnsource = async (preview: import('../../../src/integrations/document/unsource.js').UnsourcePreview) => {
|
|
354
|
+
if (!processor) throw new Error("Processor not ready");
|
|
355
|
+
const result = await processor.executeUnsource(preview);
|
|
356
|
+
const { writeUnsourceInvoice } = await import("../../../src/integrations/document/invoice.js");
|
|
357
|
+
await writeUnsourceInvoice(preview, result, eiDataPath);
|
|
358
|
+
return result;
|
|
359
|
+
};
|
|
360
|
+
|
|
323
361
|
const archivePersona = async (personaId: string) => {
|
|
324
362
|
if (!processor) return;
|
|
325
363
|
await processor.archivePersona(personaId);
|
|
@@ -748,64 +786,40 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
748
786
|
try {
|
|
749
787
|
const human = await processor!.getHuman();
|
|
750
788
|
const hasAccounts = human.settings?.accounts && human.settings.accounts.length > 0;
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
id: crypto.randomUUID(),
|
|
783
|
-
name: candidate.name,
|
|
784
|
-
type: "llm" as ProviderType,
|
|
785
|
-
url: candidate.url,
|
|
786
|
-
enabled: true,
|
|
787
|
-
created_at: new Date().toISOString(),
|
|
788
|
-
default_model: defaultModelId,
|
|
789
|
-
models: [{ id: defaultModelId, name: "default" }],
|
|
790
|
-
};
|
|
791
|
-
});
|
|
792
|
-
const firstDefaultModelId = accounts[0].default_model!;
|
|
793
|
-
const currentHuman = await processor!.getHuman();
|
|
794
|
-
await processor!.updateHuman({
|
|
795
|
-
settings: {
|
|
796
|
-
...currentHuman.settings,
|
|
797
|
-
accounts,
|
|
798
|
-
default_model: firstDefaultModelId,
|
|
799
|
-
},
|
|
800
|
-
});
|
|
801
|
-
const names = found.map((c) => c.name).join(" and ");
|
|
802
|
-
showNotification(`${names} detected and configured!`, "info");
|
|
803
|
-
logger.info(`Auto-configured: ${names}`);
|
|
804
|
-
} else {
|
|
805
|
-
logger.info("No local LLMs found, showing welcome overlay");
|
|
806
|
-
setShowWelcomeOverlay(true);
|
|
807
|
-
}
|
|
789
|
+
if (hasAccounts) return;
|
|
790
|
+
|
|
791
|
+
const { detected, statuses } = await detectProviders({
|
|
792
|
+
skipLocalDetect: E2E_SKIP_LOCAL_DETECT,
|
|
793
|
+
skipCloudDetect: E2E_SKIP_CLOUD_DETECT,
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
const allStatuses: ProviderDetectionStatus[] = ALL_PROVIDER_NAMES.map((name) => {
|
|
797
|
+
const found = statuses.find((s) => s.name === name);
|
|
798
|
+
return found ?? { name, detected: false };
|
|
799
|
+
});
|
|
800
|
+
setDetectedProviders(allStatuses);
|
|
801
|
+
|
|
802
|
+
if (detected.length > 0) {
|
|
803
|
+
const accounts = buildProviderAccounts(detected);
|
|
804
|
+
const topProvider = detected[0];
|
|
805
|
+
const defaultModel = `${topProvider.name}:${topProvider.selected.extractionModel}`;
|
|
806
|
+
setFirstBootDefaultModel(defaultModel);
|
|
807
|
+
const currentHuman = await processor!.getHuman();
|
|
808
|
+
await processor!.updateHuman({
|
|
809
|
+
settings: {
|
|
810
|
+
...currentHuman.settings,
|
|
811
|
+
accounts,
|
|
812
|
+
default_model: defaultModel,
|
|
813
|
+
},
|
|
814
|
+
});
|
|
815
|
+
const names = detected.map((d) => d.name).join(" and ");
|
|
816
|
+
showNotification(`${names} detected and configured!`, "info");
|
|
817
|
+
logger.info(`Auto-configured: ${names}`);
|
|
818
|
+
} else {
|
|
819
|
+
logger.info("No LLM providers found, showing welcome overlay");
|
|
808
820
|
}
|
|
821
|
+
|
|
822
|
+
setShowWelcomeOverlay(true);
|
|
809
823
|
} catch (err: any) {
|
|
810
824
|
logger.warn(`LLM detection failed: ${err?.message || err}`);
|
|
811
825
|
}
|
|
@@ -826,6 +840,7 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
826
840
|
logger.info("Ei TUI bootstrap starting");
|
|
827
841
|
try {
|
|
828
842
|
const storage = new FileStorage(Bun.env.EI_DATA_PATH);
|
|
843
|
+
eiDataPath = storage.getDataPath();
|
|
829
844
|
// Pre-configure remoteSync from env vars BEFORE processor.start()
|
|
830
845
|
// so the processor's sync decision tree can detect remote state
|
|
831
846
|
const syncUsername = Bun.env.EI_SYNC_USERNAME;
|
|
@@ -969,6 +984,8 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
969
984
|
searchHumanData,
|
|
970
985
|
showWelcomeOverlay,
|
|
971
986
|
dismissWelcomeOverlay: () => setShowWelcomeOverlay(false),
|
|
987
|
+
detectedProviders,
|
|
988
|
+
firstBootDefaultModel,
|
|
972
989
|
deleteMessages,
|
|
973
990
|
setMessageContextStatus,
|
|
974
991
|
deleteRoomMessages,
|
|
@@ -1006,6 +1023,9 @@ export const EiProvider: ParentComponent = (props) => {
|
|
|
1006
1023
|
humanRoomMessagePending,
|
|
1007
1024
|
getArchivedRooms,
|
|
1008
1025
|
generatePersonaPreview,
|
|
1026
|
+
importDocument,
|
|
1027
|
+
getUnsourcePreview,
|
|
1028
|
+
executeUnsource,
|
|
1009
1029
|
};
|
|
1010
1030
|
return (
|
|
1011
1031
|
<Switch>
|
package/tui/src/index.tsx
CHANGED
|
@@ -30,6 +30,20 @@ if (!lockResult.acquired) {
|
|
|
30
30
|
// Release lock when the app exits (keyboard context calls process.exit(0) on normal quit)
|
|
31
31
|
process.on("exit", () => { void lock.release(); });
|
|
32
32
|
|
|
33
|
+
// Validate state.json is parseable before handing off to the app.
|
|
34
|
+
// A corrupt file must never silently wipe all data — exit cleanly with recovery instructions.
|
|
35
|
+
try {
|
|
36
|
+
await storage.load();
|
|
37
|
+
} catch (e) {
|
|
38
|
+
await lock.release();
|
|
39
|
+
process.stderr.write(
|
|
40
|
+
`\nEi cannot start: state.json failed to load.\n\n` +
|
|
41
|
+
` ${e instanceof Error ? e.message : String(e)}\n\n` +
|
|
42
|
+
`Fix the file manually, restore from a backup, or delete it to start fresh (all data will be lost).\n\n`
|
|
43
|
+
);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
render(App, {
|
|
34
48
|
exitOnCtrlC: false,
|
|
35
49
|
targetFps: 30,
|
package/tui/src/storage/file.ts
CHANGED
|
@@ -59,15 +59,21 @@ export class FileStorage implements Storage {
|
|
|
59
59
|
async load(): Promise<StorageState | null> {
|
|
60
60
|
const filePath = join(this.dataPath, STATE_FILE);
|
|
61
61
|
const file = Bun.file(filePath);
|
|
62
|
-
|
|
62
|
+
|
|
63
63
|
if (await file.exists()) {
|
|
64
|
+
let text: string;
|
|
64
65
|
try {
|
|
65
|
-
|
|
66
|
-
|
|
66
|
+
text = await file.text();
|
|
67
|
+
} catch (e) {
|
|
68
|
+
throw new Error(`STORAGE_READ_FAILED: Could not read ${filePath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (text) {
|
|
72
|
+
try {
|
|
67
73
|
return decodeAllEmbeddings(JSON.parse(text) as StorageState);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
throw new Error(`STORAGE_PARSE_FAILED: ${filePath} exists but could not be parsed as JSON. Your data is intact — fix the file manually or restore from a backup in ${join(this.dataPath, "backups")}.\n Parse error: ${e instanceof Error ? e.message : String(e)}`);
|
|
68
76
|
}
|
|
69
|
-
} catch {
|
|
70
|
-
return null;
|
|
71
77
|
}
|
|
72
78
|
}
|
|
73
79
|
|
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Use prime-power bits so combinations are unambiguous:
|
|
5
5
|
* 1 — skip local LLM auto-detect (fetch to :1234/:11434)
|
|
6
|
-
* 2 — (
|
|
7
|
-
* 3 — flags 1 + 2 combined
|
|
6
|
+
* 2 — skip cloud provider auto-detect (env var → /models checks)
|
|
7
|
+
* 3 — flags 1 + 2 combined (skip all auto-detect)
|
|
8
8
|
*
|
|
9
9
|
* Production code should never set this. Tests pass it via env in test.use({ env: { EI_E2E_MODE: "1" } }).
|
|
10
10
|
*/
|
|
11
11
|
const E2E_MODE = parseInt(process.env.EI_E2E_MODE ?? "0", 10);
|
|
12
12
|
|
|
13
|
-
export const E2E_SKIP_LOCAL_DETECT
|
|
13
|
+
export const E2E_SKIP_LOCAL_DETECT = (E2E_MODE & 1) !== 0;
|
|
14
|
+
export const E2E_SKIP_CLOUD_DETECT = (E2E_MODE & 2) !== 0;
|
|
@@ -89,6 +89,26 @@ ROOM COMMANDS
|
|
|
89
89
|
/capture topic <name> Re-scan all messages for a specific topic.
|
|
90
90
|
|
|
91
91
|
EXTENDED COMMANDS
|
|
92
|
+
/reflect
|
|
93
|
+
Review a persona's pending reflection — a proposed identity update
|
|
94
|
+
generated by Ei after observing patterns in your conversations.
|
|
95
|
+
A badge appears on the persona pill when one is ready.
|
|
96
|
+
/reflect generate Write current + proposed YAML files to disk
|
|
97
|
+
/reflect update Read edited proposed.yaml back into Ei
|
|
98
|
+
/reflect apply Apply the proposed identity to the persona
|
|
99
|
+
/reflect dismiss Discard without changing anything
|
|
100
|
+
|
|
101
|
+
/import <path>
|
|
102
|
+
Import a document (txt, md, pdf, etc.) into Ei. Ei segments it,
|
|
103
|
+
extracts knowledge, and attributes it to the "Emmett" persona.
|
|
104
|
+
/import ~/notes/journal.md
|
|
105
|
+
/import /path/to/report.pdf
|
|
106
|
+
|
|
107
|
+
/unsource <source_tag>
|
|
108
|
+
Remove all facts, topics, etc. extracted from a previously imported
|
|
109
|
+
document. Use the source tag shown when the import completed.
|
|
110
|
+
/unsource my-journal-2024
|
|
111
|
+
|
|
92
112
|
/tools
|
|
93
113
|
Manage tool providers — enable or disable tools per persona.
|
|
94
114
|
|