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.
Files changed (55) hide show
  1. package/README.md +22 -3
  2. package/package.json +5 -1
  3. package/src/README.md +9 -25
  4. package/src/core/handlers/document-segmentation.ts +113 -0
  5. package/src/core/handlers/index.ts +2 -0
  6. package/src/core/handlers/rewrite.ts +13 -9
  7. package/src/core/heartbeat-manager.ts +2 -2
  8. package/src/core/llm-client.ts +11 -1
  9. package/src/core/message-manager.ts +20 -18
  10. package/src/core/orchestrators/ceremony.ts +83 -40
  11. package/src/core/orchestrators/human-extraction.ts +5 -1
  12. package/src/core/persona-manager.ts +4 -0
  13. package/src/core/processor.ts +90 -1
  14. package/src/core/queue-manager.ts +35 -0
  15. package/src/core/state/queue.ts +9 -1
  16. package/src/core/state-manager.ts +4 -0
  17. package/src/core/types/entities.ts +15 -0
  18. package/src/core/types/enums.ts +1 -0
  19. package/src/core/types/integrations.ts +2 -0
  20. package/src/core/types/llm.ts +9 -0
  21. package/src/integrations/document/chunker.ts +88 -0
  22. package/src/integrations/document/importer.ts +82 -0
  23. package/src/integrations/document/index.ts +2 -0
  24. package/src/integrations/document/invoice.ts +63 -0
  25. package/src/integrations/document/types.ts +16 -0
  26. package/src/integrations/document/unsource.ts +164 -0
  27. package/src/integrations/persona-history/importer.ts +197 -0
  28. package/src/integrations/persona-history/index.ts +3 -0
  29. package/src/integrations/persona-history/types.ts +7 -0
  30. package/src/prompts/ceremony/dedup.ts +7 -3
  31. package/src/prompts/ceremony/index.ts +2 -1
  32. package/src/prompts/ceremony/people-rewrite.ts +190 -0
  33. package/src/prompts/ceremony/{rewrite.ts → topic-rewrite.ts} +103 -78
  34. package/src/prompts/human/person-scan.ts +13 -4
  35. package/src/prompts/human/topic-scan.ts +16 -2
  36. package/src/prompts/human/topic-update.ts +36 -4
  37. package/src/prompts/human/types.ts +1 -0
  38. package/src/storage/indexed.ts +4 -0
  39. package/src/storage/interface.ts +1 -0
  40. package/src/storage/local.ts +4 -0
  41. package/src/templates/emmett.ts +49 -0
  42. package/tui/README.md +22 -0
  43. package/tui/src/app.tsx +9 -6
  44. package/tui/src/commands/delete.tsx +7 -1
  45. package/tui/src/commands/import.tsx +30 -0
  46. package/tui/src/commands/unsource.tsx +115 -0
  47. package/tui/src/components/PromptInput.tsx +4 -0
  48. package/tui/src/components/WelcomeOverlay.tsx +58 -32
  49. package/tui/src/context/ei.tsx +80 -60
  50. package/tui/src/index.tsx +14 -0
  51. package/tui/src/storage/file.ts +11 -5
  52. package/tui/src/util/e2e-flags.ts +4 -3
  53. package/tui/src/util/help-content.ts +20 -0
  54. package/tui/src/util/provider-detection.ts +251 -0
  55. package/tui/src/util/yaml-human.ts +7 -1
@@ -0,0 +1,251 @@
1
+ import type { ProviderType } from "../../../src/core/types.js";
2
+ import type { ProviderAccount, ModelConfig } from "../../../src/core/types.js";
3
+
4
+ export interface LocalProviderConfig {
5
+ name: string;
6
+ url: string;
7
+ priority: number;
8
+ }
9
+
10
+ export interface CloudProviderConfig {
11
+ name: string;
12
+ envVar: string;
13
+ url: string;
14
+ priority: number;
15
+ }
16
+
17
+ export interface SelectedModels {
18
+ extractionModel: string;
19
+ chatModel: string;
20
+ bonusModel?: string;
21
+ }
22
+
23
+ export interface ProviderDetectionResult {
24
+ name: string;
25
+ url: string;
26
+ apiKey?: string;
27
+ modelIds: string[];
28
+ selected: SelectedModels;
29
+ status: "detected" | "failed";
30
+ }
31
+
32
+ export interface ProviderDetectionStatus {
33
+ name: string;
34
+ detected: boolean;
35
+ }
36
+
37
+ export interface DetectProvidersOptions {
38
+ skipLocalDetect?: boolean;
39
+ skipCloudDetect?: boolean;
40
+ env?: Record<string, string | undefined>;
41
+ }
42
+
43
+ export const LOCAL_PROVIDERS: ReadonlyArray<LocalProviderConfig> = [
44
+ { name: "LMStudio", url: "http://127.0.0.1:1234/v1", priority: 1 },
45
+ { name: "Ollama", url: "http://127.0.0.1:11434/v1", priority: 2 },
46
+ ];
47
+
48
+ export const CLOUD_PROVIDERS: ReadonlyArray<CloudProviderConfig> = [
49
+ { name: "Anthropic", envVar: "ANTHROPIC_API_KEY", url: "https://api.anthropic.com/v1", priority: 3 },
50
+ { name: "OpenAI", envVar: "OPENAI_API_KEY", url: "https://api.openai.com/v1", priority: 4 },
51
+ { name: "Groq", envVar: "GROQ_API_KEY", url: "https://api.groq.com/openai/v1", priority: 5 },
52
+ { name: "Mistral", envVar: "MISTRAL_API_KEY", url: "https://api.mistral.ai/v1", priority: 6 },
53
+ { name: "Gemini", envVar: "GEMINI_API_KEY", url: "https://generativelanguage.googleapis.com/v1beta/openai", priority: 7 },
54
+ ];
55
+
56
+ export const ALL_PROVIDER_NAMES: ReadonlyArray<string> = [
57
+ ...LOCAL_PROVIDERS.map((p) => p.name),
58
+ ...CLOUD_PROVIDERS.map((p) => p.name),
59
+ ];
60
+
61
+ function latestMatch(modelIds: string[], pattern: string): string | undefined {
62
+ const matches = modelIds.filter((id) => id.toLowerCase().includes(pattern));
63
+ if (matches.length === 0) return undefined;
64
+ return [...matches].sort().reverse()[0];
65
+ }
66
+
67
+ export function selectModelsForProvider(
68
+ providerName: string,
69
+ modelIds: string[]
70
+ ): SelectedModels {
71
+ const name = providerName.toLowerCase();
72
+
73
+ if (name === "groq") {
74
+ return {
75
+ extractionModel: "llama-3.1-8b-instant",
76
+ chatModel: "llama-3.3-70b-versatile",
77
+ };
78
+ }
79
+
80
+ if (modelIds.length === 0) {
81
+ return { extractionModel: "default", chatModel: "default" };
82
+ }
83
+
84
+ if (name === "anthropic") {
85
+ return {
86
+ extractionModel: latestMatch(modelIds, "haiku") ?? modelIds[0],
87
+ chatModel: latestMatch(modelIds, "sonnet") ?? modelIds[0],
88
+ bonusModel: latestMatch(modelIds, "opus"),
89
+ };
90
+ }
91
+
92
+ if (name === "openai") {
93
+ const gpt4oNonMini = modelIds.filter(
94
+ (id) => id.toLowerCase().includes("gpt-4o") && !id.toLowerCase().includes("mini")
95
+ );
96
+ return {
97
+ extractionModel: latestMatch(modelIds, "mini") ?? modelIds[0],
98
+ chatModel: gpt4oNonMini.length > 0
99
+ ? [...gpt4oNonMini].sort().reverse()[0]
100
+ : modelIds[0],
101
+ };
102
+ }
103
+
104
+ if (name === "mistral") {
105
+ return {
106
+ extractionModel: latestMatch(modelIds, "small") ?? modelIds[0],
107
+ chatModel: latestMatch(modelIds, "large") ?? modelIds[0],
108
+ };
109
+ }
110
+
111
+ if (name === "gemini") {
112
+ return {
113
+ extractionModel: latestMatch(modelIds, "flash") ?? modelIds[0],
114
+ chatModel: latestMatch(modelIds, "pro") ?? modelIds[0],
115
+ };
116
+ }
117
+
118
+ return { extractionModel: modelIds[0], chatModel: modelIds[0] };
119
+ }
120
+
121
+ type FetchFn = (url: string, init?: RequestInit) => Promise<Response>;
122
+
123
+ function buildAuthHeaders(url: string, apiKey: string | undefined): Record<string, string> {
124
+ if (!apiKey) return {};
125
+ if (url.includes("api.anthropic.com")) {
126
+ return {
127
+ "x-api-key": apiKey,
128
+ "anthropic-version": "2023-06-01",
129
+ };
130
+ }
131
+ return { "Authorization": `Bearer ${apiKey}` };
132
+ }
133
+
134
+ async function probeModels(
135
+ url: string,
136
+ apiKey: string | undefined,
137
+ fetchFn: FetchFn
138
+ ): Promise<string[] | null> {
139
+ try {
140
+ const headers = buildAuthHeaders(url, apiKey);
141
+ const response = await fetchFn(`${url}/models`, {
142
+ method: "GET",
143
+ headers,
144
+ signal: AbortSignal.timeout(3000),
145
+ });
146
+ if (!response.ok) return null;
147
+ const json = await response.json() as { data?: Array<{ id: string }> };
148
+ return (json.data ?? []).map((m) => m.id).filter(Boolean);
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ export async function detectProviders(
155
+ options: DetectProvidersOptions = {},
156
+ fetchFn: FetchFn = fetch
157
+ ): Promise<{
158
+ detected: ProviderDetectionResult[];
159
+ statuses: ProviderDetectionStatus[];
160
+ }> {
161
+ const env = options.env ?? (process.env as Record<string, string | undefined>);
162
+ const detected: ProviderDetectionResult[] = [];
163
+ const statuses: ProviderDetectionStatus[] = [];
164
+
165
+ const localResults = await Promise.all(
166
+ LOCAL_PROVIDERS.map(async (provider) => {
167
+ if (options.skipLocalDetect) return { provider, modelIds: null };
168
+ const modelIds = await probeModels(provider.url, undefined, fetchFn);
169
+ return { provider, modelIds };
170
+ })
171
+ );
172
+
173
+ for (const { provider, modelIds } of localResults) {
174
+ const ok = modelIds !== null;
175
+ statuses.push({ name: provider.name, detected: ok });
176
+ if (ok) {
177
+ detected.push({
178
+ name: provider.name,
179
+ url: provider.url,
180
+ modelIds: modelIds!,
181
+ selected: selectModelsForProvider(provider.name, modelIds!),
182
+ status: "detected",
183
+ });
184
+ }
185
+ }
186
+
187
+ const cloudResults = await Promise.all(
188
+ CLOUD_PROVIDERS.map(async (provider) => {
189
+ const apiKey = env[provider.envVar];
190
+ if (!apiKey) return { provider, apiKey: undefined, modelIds: null };
191
+ if (options.skipCloudDetect) return { provider, apiKey, modelIds: null };
192
+ const modelIds = await probeModels(provider.url, apiKey, fetchFn);
193
+ return { provider, apiKey, modelIds };
194
+ })
195
+ );
196
+
197
+ for (const { provider, apiKey, modelIds } of cloudResults) {
198
+ const ok = modelIds !== null;
199
+ statuses.push({ name: provider.name, detected: ok });
200
+ if (ok) {
201
+ detected.push({
202
+ name: provider.name,
203
+ url: provider.url,
204
+ apiKey,
205
+ modelIds: modelIds!,
206
+ selected: selectModelsForProvider(provider.name, modelIds!),
207
+ status: "detected",
208
+ });
209
+ }
210
+ }
211
+
212
+ return { detected, statuses };
213
+ }
214
+
215
+ export function buildProviderAccounts(
216
+ detected: ProviderDetectionResult[]
217
+ ): ProviderAccount[] {
218
+ return detected.map((d) => {
219
+ const makeModel = (modelName: string): ModelConfig => ({
220
+ id: crypto.randomUUID(),
221
+ name: modelName,
222
+ });
223
+
224
+ const seenNames = new Set<string>();
225
+ const models: ModelConfig[] = [];
226
+
227
+ const pushIfNew = (name: string) => {
228
+ if (!seenNames.has(name)) {
229
+ seenNames.add(name);
230
+ models.push(makeModel(name));
231
+ }
232
+ };
233
+
234
+ pushIfNew(d.selected.chatModel);
235
+ pushIfNew(d.selected.extractionModel);
236
+ if (d.selected.bonusModel) pushIfNew(d.selected.bonusModel);
237
+ for (const id of d.modelIds) pushIfNew(id);
238
+
239
+ return {
240
+ id: crypto.randomUUID(),
241
+ name: d.name,
242
+ type: "llm" as ProviderType,
243
+ url: d.url,
244
+ api_key: d.apiKey,
245
+ enabled: true,
246
+ created_at: new Date().toISOString(),
247
+ default_model: d.selected.chatModel,
248
+ models,
249
+ };
250
+ });
251
+ }
@@ -191,6 +191,12 @@ export function humanToYAML(
191
191
  })
192
192
  .replace(/^(\s+)(identifiers:)/mg, (_, indent, _key) => {
193
193
  return `${indent}${personComment}\n${indent}identifiers:`;
194
+ })
195
+ .replace(/^( +)(sources:\n(?:\1 - .+\n)*)/mg, (match, indent, block) => {
196
+ return block
197
+ .split('\n')
198
+ .map(line => line.trim() ? `${indent}# [read-only] ${line}` : line)
199
+ .join('\n');
194
200
  });
195
201
 
196
202
  const serializeSection = (key: "facts" | "topics" | "people", items: unknown[] | undefined): string => {
@@ -199,7 +205,7 @@ export function humanToYAML(
199
205
  return `${key}:\n${stub}`;
200
206
  }
201
207
  const ordered = (items as object[]).map(canonicalFieldOrder);
202
- const itemsYaml = YAML.stringify(ordered, { lineWidth: 0 })
208
+ const itemsYaml = YAML.stringify(ordered, { lineWidth: 0, aliasDuplicateObjects: false })
203
209
  .split('\n')
204
210
  .map(line => ` ${line}`)
205
211
  .join('\n')