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,269 @@
|
|
|
1
|
+
import type { StorageState, Quote, Fact, Trait, Person, Topic } from "../core/types";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { readFile } from "fs/promises";
|
|
4
|
+
import { getEmbeddingService, findTopK } from "../core/embedding-service";
|
|
5
|
+
|
|
6
|
+
const STATE_FILE = "state.json";
|
|
7
|
+
const BACKUP_FILE = "state.backup.json";
|
|
8
|
+
const EMBEDDING_MIN_SIMILARITY = 0.3;
|
|
9
|
+
|
|
10
|
+
export function getDataPath(): string {
|
|
11
|
+
if (process.env.EI_DATA_PATH) {
|
|
12
|
+
return process.env.EI_DATA_PATH;
|
|
13
|
+
}
|
|
14
|
+
const xdgData = process.env.XDG_DATA_HOME || join(process.env.HOME || "~", ".local", "share");
|
|
15
|
+
return join(xdgData, "ei");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function loadLatestState(): Promise<StorageState | null> {
|
|
19
|
+
const dataPath = getDataPath();
|
|
20
|
+
for (const file of [STATE_FILE, BACKUP_FILE]) {
|
|
21
|
+
try {
|
|
22
|
+
const text = await readFile(join(dataPath, file), "utf-8");
|
|
23
|
+
if (text) return JSON.parse(text) as StorageState;
|
|
24
|
+
} catch {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function retrieve<T extends { id: string; embedding?: number[] }>(
|
|
32
|
+
items: T[],
|
|
33
|
+
query: string,
|
|
34
|
+
limit: number = 10
|
|
35
|
+
): Promise<T[]> {
|
|
36
|
+
if (items.length === 0 || !query) {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const embeddingService = getEmbeddingService();
|
|
41
|
+
const queryVector = await embeddingService.embed(query);
|
|
42
|
+
|
|
43
|
+
const results = findTopK(queryVector, items, limit);
|
|
44
|
+
|
|
45
|
+
return results
|
|
46
|
+
.filter(({ similarity }) => similarity >= EMBEDDING_MIN_SIMILARITY)
|
|
47
|
+
.map(({ item }) => item);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface LinkedItem {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
type: string;
|
|
54
|
+
}
|
|
55
|
+
export interface QuoteResult {
|
|
56
|
+
id: string;
|
|
57
|
+
text: string;
|
|
58
|
+
speaker: string;
|
|
59
|
+
timestamp: string;
|
|
60
|
+
linked_items: LinkedItem[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface FactResult {
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
description: string;
|
|
67
|
+
sentiment: number;
|
|
68
|
+
validated: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface TraitResult {
|
|
72
|
+
id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
description: string;
|
|
75
|
+
strength: number;
|
|
76
|
+
sentiment: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface PersonResult {
|
|
80
|
+
id: string;
|
|
81
|
+
name: string;
|
|
82
|
+
description: string;
|
|
83
|
+
relationship: string;
|
|
84
|
+
sentiment: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface TopicResult {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
description: string;
|
|
91
|
+
category?: string;
|
|
92
|
+
sentiment: number;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type BalancedResult =
|
|
96
|
+
| ({ type: "quote" } & QuoteResult)
|
|
97
|
+
| ({ type: "fact" } & FactResult)
|
|
98
|
+
| ({ type: "trait" } & TraitResult)
|
|
99
|
+
| ({ type: "person" } & PersonResult)
|
|
100
|
+
| ({ type: "topic" } & TopicResult);
|
|
101
|
+
|
|
102
|
+
const DATA_TYPES = ["quote", "fact", "trait", "person", "topic"] as const;
|
|
103
|
+
type DataType = typeof DATA_TYPES[number];
|
|
104
|
+
|
|
105
|
+
interface ScoredEntry {
|
|
106
|
+
type: DataType;
|
|
107
|
+
similarity: number;
|
|
108
|
+
mapped: QuoteResult | FactResult | TraitResult | PersonResult | TopicResult;
|
|
109
|
+
itemId: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function resolveLinkedItems(dataItemIds: string[], state: StorageState): LinkedItem[] {
|
|
113
|
+
const items: LinkedItem[] = [];
|
|
114
|
+
const collections: Array<{ type: string; source: Array<{ id: string; name: string }> }> = [
|
|
115
|
+
{ type: "topic", source: state.human.topics },
|
|
116
|
+
{ type: "person", source: state.human.people },
|
|
117
|
+
{ type: "fact", source: state.human.facts },
|
|
118
|
+
{ type: "trait", source: state.human.traits },
|
|
119
|
+
];
|
|
120
|
+
for (const { type, source } of collections) {
|
|
121
|
+
for (const entity of source) {
|
|
122
|
+
if (dataItemIds.includes(entity.id)) {
|
|
123
|
+
items.push({ id: entity.id, name: entity.name, type });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return items;
|
|
128
|
+
}
|
|
129
|
+
export function mapQuote(quote: Quote, state: StorageState): QuoteResult {
|
|
130
|
+
return {
|
|
131
|
+
id: quote.id,
|
|
132
|
+
text: quote.text,
|
|
133
|
+
speaker: quote.speaker,
|
|
134
|
+
timestamp: quote.timestamp,
|
|
135
|
+
linked_items: resolveLinkedItems(quote.data_item_ids, state),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function mapFact(fact: Fact): FactResult {
|
|
140
|
+
return {
|
|
141
|
+
id: fact.id,
|
|
142
|
+
name: fact.name,
|
|
143
|
+
description: fact.description,
|
|
144
|
+
sentiment: fact.sentiment,
|
|
145
|
+
validated: fact.validated,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function mapTrait(trait: Trait): TraitResult {
|
|
150
|
+
return {
|
|
151
|
+
id: trait.id,
|
|
152
|
+
name: trait.name,
|
|
153
|
+
description: trait.description,
|
|
154
|
+
strength: trait.strength ?? 0.5,
|
|
155
|
+
sentiment: trait.sentiment,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function mapPerson(person: Person): PersonResult {
|
|
160
|
+
return {
|
|
161
|
+
id: person.id,
|
|
162
|
+
name: person.name,
|
|
163
|
+
description: person.description,
|
|
164
|
+
relationship: person.relationship,
|
|
165
|
+
sentiment: person.sentiment,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function mapTopic(topic: Topic): TopicResult {
|
|
170
|
+
return {
|
|
171
|
+
id: topic.id,
|
|
172
|
+
name: topic.name,
|
|
173
|
+
description: topic.description,
|
|
174
|
+
category: topic.category,
|
|
175
|
+
sentiment: topic.sentiment,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export async function retrieveBalanced(
|
|
180
|
+
query: string,
|
|
181
|
+
limit: number = 10
|
|
182
|
+
): Promise<BalancedResult[]> {
|
|
183
|
+
const state = await loadLatestState();
|
|
184
|
+
if (!state) {
|
|
185
|
+
console.error("No saved state found. Is EI_DATA_PATH set correctly?");
|
|
186
|
+
return [];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const embeddingService = getEmbeddingService();
|
|
190
|
+
const queryVector = await embeddingService.embed(query);
|
|
191
|
+
|
|
192
|
+
const allScored: ScoredEntry[] = [];
|
|
193
|
+
|
|
194
|
+
const typeConfigs: Array<{
|
|
195
|
+
type: DataType;
|
|
196
|
+
items: Array<{ id: string; embedding?: number[] }>;
|
|
197
|
+
mapper: (item: any) => any;
|
|
198
|
+
}> = [
|
|
199
|
+
{ type: "quote", items: state.human.quotes, mapper: (q: Quote) => mapQuote(q, state) },
|
|
200
|
+
{ type: "fact", items: state.human.facts, mapper: mapFact },
|
|
201
|
+
{ type: "trait", items: state.human.traits, mapper: mapTrait },
|
|
202
|
+
{ type: "person", items: state.human.people, mapper: mapPerson },
|
|
203
|
+
{ type: "topic", items: state.human.topics, mapper: mapTopic },
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
for (const { type, items, mapper } of typeConfigs) {
|
|
207
|
+
const scored = findTopK(queryVector, items, items.length);
|
|
208
|
+
for (const { item, similarity } of scored) {
|
|
209
|
+
if (similarity >= EMBEDDING_MIN_SIMILARITY) {
|
|
210
|
+
allScored.push({ type, similarity, mapped: mapper(item), itemId: item.id });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const result: ScoredEntry[] = [];
|
|
216
|
+
const used = new Set<string>();
|
|
217
|
+
|
|
218
|
+
// Floor: at least 1 result per type (if available and meets threshold)
|
|
219
|
+
for (const type of DATA_TYPES) {
|
|
220
|
+
if (result.length >= limit) break;
|
|
221
|
+
const best = allScored
|
|
222
|
+
.filter(r => r.type === type && !used.has(r.itemId))
|
|
223
|
+
.sort((a, b) => b.similarity - a.similarity)[0];
|
|
224
|
+
if (best) {
|
|
225
|
+
result.push(best);
|
|
226
|
+
used.add(best.itemId);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Fill remaining slots with highest-similarity results across all types
|
|
231
|
+
const remaining = allScored
|
|
232
|
+
.filter(r => !used.has(r.itemId))
|
|
233
|
+
.sort((a, b) => b.similarity - a.similarity);
|
|
234
|
+
|
|
235
|
+
for (const entry of remaining) {
|
|
236
|
+
if (result.length >= limit) break;
|
|
237
|
+
result.push(entry);
|
|
238
|
+
used.add(entry.itemId);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
result.sort((a, b) => b.similarity - a.similarity);
|
|
242
|
+
|
|
243
|
+
return result.map(({ type, mapped }) => ({ type, ...mapped }) as BalancedResult);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function lookupById(id: string): Promise<({ type: string } & Record<string, unknown>) | null> {
|
|
247
|
+
const state = await loadLatestState();
|
|
248
|
+
if (!state) {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const collections: Array<{ type: string; source: Array<{ id: string; [k: string]: unknown }> }> = [
|
|
253
|
+
{ type: "fact", source: state.human.facts },
|
|
254
|
+
{ type: "trait", source: state.human.traits },
|
|
255
|
+
{ type: "person", source: state.human.people },
|
|
256
|
+
{ type: "topic", source: state.human.topics },
|
|
257
|
+
{ type: "quote", source: state.human.quotes },
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
for (const { type, source } of collections) {
|
|
261
|
+
const entity = source.find(item => item.id === id);
|
|
262
|
+
if (entity) {
|
|
263
|
+
const { embedding, ...rest } = entity;
|
|
264
|
+
return { type, ...rest };
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return null;
|
|
269
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* EI CLI - Memory retrieval interface for OpenCode integration
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* ei "search text" Search all data types
|
|
7
|
+
* ei -n 5 "search text" Limit results
|
|
8
|
+
* ei quote "search text" Search specific type
|
|
9
|
+
* ei quote -n 5 "search text" Type-specific with limit
|
|
10
|
+
* ei --id <id> Look up entity by ID
|
|
11
|
+
* echo <id> | ei --id Look up entity by ID from stdin
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { parseArgs } from "util";
|
|
15
|
+
import { retrieveBalanced, lookupById } from "./cli/retrieval";
|
|
16
|
+
|
|
17
|
+
const TYPE_ALIASES: Record<string, string> = {
|
|
18
|
+
quote: "quotes",
|
|
19
|
+
quotes: "quotes",
|
|
20
|
+
fact: "facts",
|
|
21
|
+
facts: "facts",
|
|
22
|
+
trait: "traits",
|
|
23
|
+
traits: "traits",
|
|
24
|
+
person: "people",
|
|
25
|
+
people: "people",
|
|
26
|
+
topic: "topics",
|
|
27
|
+
topics: "topics",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function printHelp(): void {
|
|
31
|
+
console.log(`
|
|
32
|
+
Ei
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
ei Launch the TUI chat interface
|
|
36
|
+
ei "search text" Search all data types (top 10)
|
|
37
|
+
ei -n 5 "search text" Limit results
|
|
38
|
+
ei <type> "search text" Search a specific data type
|
|
39
|
+
ei <type> -n 5 "search text" Type-specific with limit
|
|
40
|
+
ei --id <id> Look up a specific entity by ID
|
|
41
|
+
echo <id> | ei --id Look up entity by ID from stdin
|
|
42
|
+
|
|
43
|
+
Types:
|
|
44
|
+
quote / quotes Quotes from conversation history
|
|
45
|
+
fact / facts Facts about the user
|
|
46
|
+
trait / traits Personality traits
|
|
47
|
+
person / people People from the user's life
|
|
48
|
+
topic / topics Topics of interest
|
|
49
|
+
|
|
50
|
+
Options:
|
|
51
|
+
--number, -n Maximum number of results (default: 10)
|
|
52
|
+
--id Look up entity by ID (accepts value or stdin)
|
|
53
|
+
--help, -h Show this help message
|
|
54
|
+
|
|
55
|
+
Examples:
|
|
56
|
+
ei "debugging" # Search everything
|
|
57
|
+
ei -n 5 "API design" # Top 5 across all types
|
|
58
|
+
ei quote "you guessed it" # Search quotes only
|
|
59
|
+
ei trait -n 3 "problem solving" # Top 3 matching traits
|
|
60
|
+
ei --id abc-123 # Look up entity by ID
|
|
61
|
+
ei "memory leak" | jq .[0].id | ei --id # Pipe ID from search
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main(): Promise<void> {
|
|
66
|
+
const args = process.argv.slice(2);
|
|
67
|
+
|
|
68
|
+
if (args.length === 0) {
|
|
69
|
+
const tuiDir = new URL("../tui", import.meta.url).pathname;
|
|
70
|
+
const tuiEntry = new URL("../tui/src/index.tsx", import.meta.url).pathname;
|
|
71
|
+
const proc = Bun.spawn(["bun", "--conditions=browser", "run", tuiEntry], {
|
|
72
|
+
stdio: ["inherit", "inherit", "inherit"],
|
|
73
|
+
env: { ...process.env },
|
|
74
|
+
cwd: tuiDir,
|
|
75
|
+
});
|
|
76
|
+
await proc.exited;
|
|
77
|
+
process.exit(proc.exitCode ?? 0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (args[0] === "--help" || args[0] === "-h") {
|
|
81
|
+
printHelp();
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
// Handle --id flag: look up entity by ID
|
|
87
|
+
const idFlagIndex = args.indexOf("--id");
|
|
88
|
+
if (idFlagIndex !== -1) {
|
|
89
|
+
let id = args[idFlagIndex + 1]?.trim();
|
|
90
|
+
|
|
91
|
+
// If no value after --id, try reading from stdin
|
|
92
|
+
if (!id && !process.stdin.isTTY) {
|
|
93
|
+
const chunks: Buffer[] = [];
|
|
94
|
+
for await (const chunk of process.stdin) {
|
|
95
|
+
chunks.push(chunk as Buffer);
|
|
96
|
+
}
|
|
97
|
+
id = Buffer.concat(chunks).toString("utf-8").trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!id) {
|
|
101
|
+
console.error("--id requires a value. Usage: ei --id <id> or echo <id> | ei --id");
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Strip surrounding quotes (from jq output or shell quoting)
|
|
106
|
+
id = id.replace(/^["']|["']$/g, "");
|
|
107
|
+
|
|
108
|
+
const entity = await lookupById(id);
|
|
109
|
+
if (!entity) {
|
|
110
|
+
console.error(`No entity found with ID: ${id}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
console.log(JSON.stringify(entity, null, 2));
|
|
114
|
+
process.exit(0);
|
|
115
|
+
}
|
|
116
|
+
let targetType: string | null = null;
|
|
117
|
+
let parseableArgs = args;
|
|
118
|
+
|
|
119
|
+
if (TYPE_ALIASES[args[0]]) {
|
|
120
|
+
targetType = TYPE_ALIASES[args[0]];
|
|
121
|
+
parseableArgs = args.slice(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = parseArgs({
|
|
127
|
+
args: parseableArgs,
|
|
128
|
+
options: {
|
|
129
|
+
number: { type: "string", short: "n" },
|
|
130
|
+
help: { type: "boolean", short: "h" },
|
|
131
|
+
},
|
|
132
|
+
allowPositionals: true,
|
|
133
|
+
strict: true,
|
|
134
|
+
});
|
|
135
|
+
} catch (e) {
|
|
136
|
+
console.error(`Error parsing arguments: ${(e as Error).message}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (parsed.values.help) {
|
|
141
|
+
printHelp();
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const query = parsed.positionals.join(" ").trim();
|
|
146
|
+
const limit = parsed.values.number ? parseInt(parsed.values.number, 10) : 10;
|
|
147
|
+
|
|
148
|
+
if (!query) {
|
|
149
|
+
if (targetType) {
|
|
150
|
+
console.error(`Search text required. Usage: ei ${targetType} "search text"`);
|
|
151
|
+
} else {
|
|
152
|
+
console.error(`Search text required. Usage: ei "search text"`);
|
|
153
|
+
}
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (isNaN(limit) || limit < 1) {
|
|
158
|
+
console.error("--number must be a positive integer");
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let result;
|
|
163
|
+
if (targetType) {
|
|
164
|
+
const module = await import(`./cli/commands/${targetType}.js`);
|
|
165
|
+
result = await module.execute(query, limit);
|
|
166
|
+
} else {
|
|
167
|
+
result = await retrieveBalanced(query, limit);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log(JSON.stringify(result, null, 2));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
main().catch((e) => {
|
|
174
|
+
console.error(`Fatal error: ${e.message}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Core Module
|
|
2
|
+
|
|
3
|
+
The brain of Ei. Handles state, queue, LLM communication, and orchestration.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
core/
|
|
9
|
+
├── processor.ts # Main orchestrator (1100+ lines)
|
|
10
|
+
├── state-manager.ts # In-memory state + persistence
|
|
11
|
+
├── queue-processor.ts # LLM request queue with priorities
|
|
12
|
+
├── llm-client.ts # Multi-provider LLM abstraction
|
|
13
|
+
├── types.ts # All core types (source: CONTRACTS.md)
|
|
14
|
+
├── handlers/ # LLM response handlers
|
|
15
|
+
├── orchestrators/ # Multi-step workflows
|
|
16
|
+
├── personas/ # Persona loading logic
|
|
17
|
+
└── state/ # State slices (human, persona, messages)
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Key Files
|
|
21
|
+
|
|
22
|
+
### processor.ts (The Hub)
|
|
23
|
+
|
|
24
|
+
Everything flows through Processor:
|
|
25
|
+
- **Main loop**: 100ms tick checking queue, auto-save, heartbeat
|
|
26
|
+
- **Message flow**: User input → queue response request → handle result → update state
|
|
27
|
+
- **Background work**: Extraction, ceremony, heartbeat (all async, queued)
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
// Entry points
|
|
31
|
+
processor.start() // Begin main loop
|
|
32
|
+
processor.sendMessage(persona, text) // User sends message
|
|
33
|
+
processor.stop() // Graceful shutdown
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### state-manager.ts
|
|
37
|
+
|
|
38
|
+
In-memory state with dirty tracking:
|
|
39
|
+
- `loadState()` / `saveState()` for persistence
|
|
40
|
+
- Slices: human, personas, messages, config
|
|
41
|
+
- Auto-save every 60s when dirty
|
|
42
|
+
|
|
43
|
+
### queue-processor.ts
|
|
44
|
+
|
|
45
|
+
Priority queue for LLM requests:
|
|
46
|
+
- High: User-facing responses
|
|
47
|
+
- Normal: Extraction, analysis
|
|
48
|
+
- Low: Background maintenance
|
|
49
|
+
|
|
50
|
+
**Async model**: Handlers queue work, don't await results inline.
|
|
51
|
+
|
|
52
|
+
### handlers/index.ts (1000+ lines)
|
|
53
|
+
|
|
54
|
+
All `LLMNextStep` handlers in one file. Each handler:
|
|
55
|
+
1. Parses LLM response (JSON or text)
|
|
56
|
+
2. Updates state via StateManager
|
|
57
|
+
3. May queue follow-up requests
|
|
58
|
+
|
|
59
|
+
### orchestrators/
|
|
60
|
+
|
|
61
|
+
Multi-step workflows:
|
|
62
|
+
- `persona-generation.ts`: Create new persona (multi-LLM-call process)
|
|
63
|
+
- `extraction.ts`: Scan messages for facts/topics/people
|
|
64
|
+
- `ceremony.ts`: Periodic exposure decay + persona enrichment
|
|
65
|
+
|
|
66
|
+
## Patterns
|
|
67
|
+
|
|
68
|
+
### Time-Based Triggers
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// ✅ CORRECT: Update timestamp BEFORE async work
|
|
72
|
+
if (timeSinceLastX >= delay) {
|
|
73
|
+
lastX = Date.now(); // Prevent duplicate queueing
|
|
74
|
+
await doAsyncWork();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ❌ WRONG: Other loop iterations queue duplicates
|
|
78
|
+
if (timeSinceLastX >= delay) {
|
|
79
|
+
await doAsyncWork();
|
|
80
|
+
lastX = Date.now();
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Adding New Handlers
|
|
85
|
+
|
|
86
|
+
1. Add enum to `LLMNextStep` in types.ts
|
|
87
|
+
2. Add handler function in handlers/index.ts
|
|
88
|
+
3. Register in `handlers` map at bottom of file
|
|
89
|
+
4. Queue from Processor or orchestrator
|
|
90
|
+
|
|
91
|
+
### State Updates
|
|
92
|
+
|
|
93
|
+
Always use StateManager methods, never mutate directly:
|
|
94
|
+
```typescript
|
|
95
|
+
// ✅ Correct
|
|
96
|
+
stateManager.updateHuman(h => ({ ...h, last_interaction: now }))
|
|
97
|
+
|
|
98
|
+
// ❌ Wrong - bypasses dirty tracking
|
|
99
|
+
state.human.last_interaction = now
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Testing
|
|
103
|
+
|
|
104
|
+
Unit tests in `tests/unit/core/`. Mock LLM responses for deterministic tests.
|