ei-tui 1.0.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +3 -1
  2. package/package.json +2 -21
  3. package/src/cli/README.md +42 -14
  4. package/src/cli/mcp.ts +237 -0
  5. package/src/cli.ts +17 -51
  6. package/src/core/handlers/dedup.ts +4 -15
  7. package/src/core/handlers/document-segmentation.ts +2 -3
  8. package/src/core/handlers/heartbeat.ts +5 -10
  9. package/src/core/handlers/human-extraction.ts +6 -0
  10. package/src/core/handlers/human-matching.ts +53 -10
  11. package/src/core/handlers/index.ts +2 -0
  12. package/src/core/handlers/knowledge-synthesis.ts +50 -0
  13. package/src/core/handlers/persona-generation.ts +4 -8
  14. package/src/core/handlers/persona-response.ts +3 -4
  15. package/src/core/handlers/persona-topics.ts +2 -4
  16. package/src/core/handlers/rewrite.ts +26 -9
  17. package/src/core/handlers/rooms.ts +6 -12
  18. package/src/core/llm-client.ts +53 -7
  19. package/src/core/message-manager.ts +2 -4
  20. package/src/core/orchestrators/ceremony.ts +44 -13
  21. package/src/core/orchestrators/human-extraction.ts +38 -1
  22. package/src/core/orchestrators/index.ts +1 -0
  23. package/src/core/processor.ts +192 -41
  24. package/src/core/prompt-context-builder.ts +1 -0
  25. package/src/core/queue-manager.ts +10 -0
  26. package/src/core/queue-processor.ts +13 -4
  27. package/src/core/state-manager.ts +35 -0
  28. package/src/core/tools/builtin/fetch-memory.ts +92 -0
  29. package/src/core/tools/builtin/fetch-message.ts +123 -0
  30. package/src/core/tools/builtin/find-memory.ts +99 -0
  31. package/src/core/tools/index.ts +88 -5
  32. package/src/core/tools/types.ts +1 -1
  33. package/src/core/types/data-items.ts +1 -1
  34. package/src/core/types/entities.ts +7 -1
  35. package/src/core/types/enums.ts +1 -0
  36. package/src/core/types/integrations.ts +3 -1
  37. package/src/integrations/claude-code/importer.ts +6 -0
  38. package/src/integrations/cursor/importer.ts +6 -0
  39. package/src/integrations/document/unsource.ts +5 -3
  40. package/src/integrations/opencode/importer.ts +13 -1
  41. package/src/integrations/persona-history/importer.ts +12 -1
  42. package/src/prompts/ceremony/dedup.ts +3 -3
  43. package/src/prompts/ceremony/people-rewrite.ts +2 -2
  44. package/src/prompts/ceremony/topic-rewrite.ts +2 -2
  45. package/src/prompts/ceremony/types.ts +1 -1
  46. package/src/prompts/human/person-scan.ts +17 -0
  47. package/src/prompts/human/types.ts +4 -0
  48. package/src/prompts/index.ts +3 -0
  49. package/src/prompts/response/sections.ts +14 -7
  50. package/src/prompts/response/types.ts +1 -0
  51. package/src/prompts/synthesis/index.ts +101 -0
  52. package/src/prompts/synthesis/types.ts +26 -0
  53. package/tui/src/commands/generate.tsx +98 -0
  54. package/tui/src/commands/unsource.tsx +17 -10
  55. package/tui/src/components/GeneratedDocsOverlay.tsx +136 -0
  56. package/tui/src/components/PromptInput.tsx +2 -0
  57. package/tui/src/context/ei.tsx +49 -2
  58. package/tui/src/util/logger.ts +22 -2
  59. package/tui/src/util/provider-detection.ts +5 -2
  60. package/tui/src/util/yaml-provider.ts +2 -8
  61. package/src/core/tools/builtin/read-memory.ts +0 -70
@@ -13,7 +13,7 @@ import { createStore } from "solid-js/store";
13
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
- import { logger, clearLog, interceptConsole } from "../util/logger.js";
16
+ import { logger, rotateLog, interceptConsole } from "../util/logger.js";
17
17
  import { E2E_SKIP_LOCAL_DETECT, E2E_SKIP_CLOUD_DETECT } from "../util/e2e-flags.js";
18
18
  import {
19
19
  detectProviders,
@@ -155,6 +155,10 @@ export interface EiContextValue {
155
155
  importDocument: (filePath: string) => Promise<import('../../../src/integrations/document/types.js').DocumentImportResult>;
156
156
  getUnsourcePreview: (sourceTag: string) => import('../../../src/integrations/document/unsource.js').UnsourcePreview;
157
157
  executeUnsource: (preview: import('../../../src/integrations/document/unsource.js').UnsourcePreview) => Promise<import('../../../src/integrations/document/unsource.js').UnsourceResult>;
158
+ generateDocument: (subject: string) => Promise<{ slug: string }>;
159
+ reRunDocument: (slug: string) => Promise<{ slug: string }>;
160
+ writeGeneratedDocument: (slug: string) => Promise<string | null>;
161
+ checkGenerationModel: () => { model: string; isRewriteModel: boolean };
158
162
  }
159
163
  const EiContext = createContext<EiContextValue>();
160
164
 
@@ -358,6 +362,34 @@ export const EiProvider: ParentComponent = (props) => {
358
362
  return result;
359
363
  };
360
364
 
365
+ const generateDocument = async (subject: string): Promise<{ slug: string }> => {
366
+ if (!processor) throw new Error("Processor not ready");
367
+ return processor.generateDocument(subject);
368
+ };
369
+
370
+ const reRunDocument = async (slug: string): Promise<{ slug: string }> => {
371
+ if (!processor) throw new Error("Processor not ready");
372
+ return processor.reRunDocument(slug);
373
+ };
374
+
375
+ const writeGeneratedDocument = async (slug: string): Promise<string | null> => {
376
+ if (!processor) throw new Error("Processor not ready");
377
+ const content = await processor.getGeneratedDocumentContent(slug);
378
+ if (!content) return null;
379
+ const { join } = await import("node:path");
380
+ const { mkdirSync, writeFileSync } = await import("node:fs");
381
+ const dir = join(eiDataPath, "docs");
382
+ mkdirSync(dir, { recursive: true });
383
+ const outPath = join(dir, `${slug}.md`);
384
+ writeFileSync(outPath, content);
385
+ return outPath;
386
+ };
387
+
388
+ const checkGenerationModel = (): { model: string; isRewriteModel: boolean } => {
389
+ if (!processor) throw new Error("Processor not ready");
390
+ return processor.checkGenerationModel();
391
+ };
392
+
361
393
  const archivePersona = async (personaId: string) => {
362
394
  if (!processor) return;
363
395
  await processor.archivePersona(personaId);
@@ -835,7 +867,7 @@ export const EiProvider: ParentComponent = (props) => {
835
867
  await finishBootstrap();
836
868
  };
837
869
  async function bootstrap() {
838
- clearLog();
870
+ rotateLog();
839
871
  interceptConsole();
840
872
  logger.info("Ei TUI bootstrap starting");
841
873
  try {
@@ -909,6 +941,17 @@ export const EiProvider: ParentComponent = (props) => {
909
941
  onRoomMessageProcessing: (roomId) => {
910
942
  if (roomId === store.activeRoomId) setStore("isRoomProcessing", true);
911
943
  },
944
+ onDocumentGenerated: async (slug) => {
945
+ const { join } = await import("node:path");
946
+ const { mkdirSync, writeFileSync } = await import("node:fs");
947
+ const content = await processor!.getGeneratedDocumentContent(slug);
948
+ if (content) {
949
+ const dir = join(eiDataPath, "docs");
950
+ mkdirSync(dir, { recursive: true });
951
+ writeFileSync(join(dir, `${slug}.md`), content);
952
+ showNotification(`Document ready: ${join(dir, `${slug}.md`)}`, "info");
953
+ }
954
+ },
912
955
  };
913
956
  processor = new Processor(eiInterface);
914
957
  logger.debug("Processor created, calling start()");
@@ -1026,6 +1069,10 @@ export const EiProvider: ParentComponent = (props) => {
1026
1069
  importDocument,
1027
1070
  getUnsourcePreview,
1028
1071
  executeUnsource,
1072
+ generateDocument,
1073
+ reRunDocument,
1074
+ writeGeneratedDocument,
1075
+ checkGenerationModel,
1029
1076
  };
1030
1077
  return (
1031
1078
  <Switch>
@@ -1,8 +1,10 @@
1
1
  /** File-based logger for TUI debugging. Usage: tail -f $EI_DATA_PATH/tui.log */
2
2
 
3
- import { appendFileSync, mkdirSync } from "node:fs";
3
+ import { appendFileSync, mkdirSync, existsSync, readdirSync, unlinkSync, renameSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
 
6
+ const MAX_ROLLED_LOGS = 10;
7
+
6
8
  function getDataPath(): string {
7
9
  if (Bun.env.EI_DATA_PATH) return Bun.env.EI_DATA_PATH;
8
10
  const xdgData = Bun.env.XDG_DATA_HOME || join(Bun.env.HOME || "~", ".local", "share");
@@ -59,16 +61,34 @@ export const logger = {
59
61
  error: (message: string, data?: unknown) => writeLogSync("error", message, data),
60
62
  };
61
63
 
62
- export function clearLog(): void {
64
+ export function rotateLog(): void {
63
65
  try {
64
66
  const logPath = getLogPath();
65
67
  const dataDir = logPath.substring(0, logPath.lastIndexOf("/"));
66
68
  mkdirSync(dataDir, { recursive: true });
69
+
70
+ if (existsSync(logPath)) {
71
+ const ts = new Date().toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
72
+ renameSync(logPath, join(dataDir, `tui-${ts}.log`));
73
+ }
74
+
75
+ const rolled = readdirSync(dataDir)
76
+ .filter(f => /^tui-\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}\.log$/.test(f))
77
+ .sort();
78
+ for (const old of rolled.slice(0, Math.max(0, rolled.length - MAX_ROLLED_LOGS))) {
79
+ unlinkSync(join(dataDir, old));
80
+ }
81
+
67
82
  const header = `--- TUI Started at ${new Date().toISOString()} ---\n`;
68
83
  Bun.write(logPath, header);
69
84
  } catch {}
70
85
  }
71
86
 
87
+ /** @deprecated Use rotateLog() instead */
88
+ export function clearLog(): void {
89
+ rotateLog();
90
+ }
91
+
72
92
  export function interceptConsole(): void {
73
93
  const originalLog = console.log.bind(console);
74
94
  const originalWarn = console.warn.bind(console);
@@ -158,7 +158,7 @@ export async function detectProviders(
158
158
  detected: ProviderDetectionResult[];
159
159
  statuses: ProviderDetectionStatus[];
160
160
  }> {
161
- const env = options.env ?? (process.env as Record<string, string | undefined>);
161
+ const env = options.env ?? (Bun.env as Record<string, string | undefined>);
162
162
  const detected: ProviderDetectionResult[] = [];
163
163
  const statuses: ProviderDetectionStatus[] = [];
164
164
 
@@ -236,12 +236,15 @@ export function buildProviderAccounts(
236
236
  if (d.selected.bonusModel) pushIfNew(d.selected.bonusModel);
237
237
  for (const id of d.modelIds) pushIfNew(id);
238
238
 
239
+ const cloudConfig = CLOUD_PROVIDERS.find((p) => p.name === d.name);
240
+ const apiKey = cloudConfig ? `$${cloudConfig.envVar}` : d.apiKey;
241
+
239
242
  return {
240
243
  id: crypto.randomUUID(),
241
244
  name: d.name,
242
245
  type: "llm" as ProviderType,
243
246
  url: d.url,
244
- api_key: d.apiKey,
247
+ api_key: apiKey,
245
248
  enabled: true,
246
249
  created_at: new Date().toISOString(),
247
250
  default_model: d.selected.chatModel,
@@ -35,12 +35,6 @@ export interface ProviderYAMLResult {
35
35
  _delete: boolean;
36
36
  }
37
37
 
38
- function resolveEnvVar(value: string | undefined): string | undefined {
39
- if (!value || !value.startsWith("$")) return value;
40
- const varName = value.slice(1);
41
- return process.env[varName] || value;
42
- }
43
-
44
38
  const PLACEHOLDER_PROVIDER_NAME = "My Provider";
45
39
  const PLACEHOLDER_PROVIDER_URL = "https://api.example.com/v1";
46
40
  const PLACEHOLDER_PROVIDER_API_KEY = "your-api-key-or-$ENVAR";
@@ -120,7 +114,7 @@ export function newProviderFromYAML(yamlContent: string): ProviderAccount {
120
114
  name: data.name,
121
115
  type: (data.type === "storage" ? "storage" : "llm") as ProviderType,
122
116
  url: data.url,
123
- api_key: resolveEnvVar(data.api_key),
117
+ api_key: data.api_key,
124
118
  default_model: data.default_model,
125
119
  token_limit: data.token_limit ?? undefined,
126
120
  extra_headers: data.extra_headers && Object.keys(data.extra_headers).length > 0 ? data.extra_headers : undefined,
@@ -227,7 +221,7 @@ export function providerFromYAML(yamlContent: string, original: ProviderAccount)
227
221
  name: data.name,
228
222
  type: (data.type === "storage" ? "storage" : "llm") as ProviderType,
229
223
  url: data.url,
230
- api_key: resolveEnvVar(data.api_key),
224
+ api_key: data.api_key,
231
225
  default_model: data.default_model,
232
226
  token_limit: data.token_limit ?? undefined,
233
227
  extra_headers: data.extra_headers && Object.keys(data.extra_headers).length > 0 ? data.extra_headers : undefined,
@@ -1,70 +0,0 @@
1
- import type { ToolExecutor } from "../types.js";
2
- import type { Fact, Topic, Person, Quote } from "../../types.js";
3
-
4
- interface PersonaSummary {
5
- id: string;
6
- display_name: string;
7
- }
8
-
9
- type SearchHumanData = (
10
- query: string,
11
- options?: { types?: Array<"fact" | "topic" | "person" | "quote">; limit?: number; recent?: boolean; persona_filter?: string }
12
- ) => Promise<{ facts: Fact[]; topics: Topic[]; people: Person[]; quotes: Quote[] }>;
13
-
14
- type GetPersonaList = () => Promise<PersonaSummary[]>;
15
-
16
- export function createReadMemoryExecutor(searchHumanData: SearchHumanData, getPersonaList?: GetPersonaList): ToolExecutor {
17
- return {
18
- name: "read_memory",
19
-
20
- async execute(args: Record<string, unknown>): Promise<string> {
21
- const query = typeof args.query === "string" ? args.query.trim() : "";
22
- const recent = args.recent === true;
23
- const personaArg = typeof args.persona === "string" ? args.persona.trim() : "";
24
- console.log(`[read_memory] called with query="${query}", types=${JSON.stringify(args.types ?? null)}, limit=${args.limit ?? 10}, recent=${recent}, persona="${personaArg}"`);
25
-
26
- if (!query && !recent) {
27
- console.warn("[read_memory] missing query argument");
28
- return JSON.stringify({ error: "Missing required argument: query (or use recent: true)" });
29
- }
30
-
31
- const types = Array.isArray(args.types)
32
- ? (args.types.filter(
33
- t => typeof t === "string" && ["fact", "topic", "person", "quote"].includes(t)
34
- ) as Array<"fact" | "topic" | "person" | "quote">)
35
- : undefined;
36
-
37
- const limit = typeof args.limit === "number" && args.limit > 0 ? Math.min(args.limit, 20) : 10;
38
-
39
- // Resolve persona display_name to ID
40
- let persona_filter: string | undefined;
41
- if (personaArg && getPersonaList) {
42
- const personas = await getPersonaList();
43
- const match = personas.find(p => p.display_name.toLowerCase() === personaArg.toLowerCase());
44
- if (match) {
45
- persona_filter = match.id;
46
- console.log(`[read_memory] resolved persona "${personaArg}" to ID "${persona_filter}"`);
47
- } else {
48
- console.warn(`[read_memory] persona "${personaArg}" not found, proceeding without filter`);
49
- }
50
- }
51
-
52
- const results = await searchHumanData(query, { types, limit, recent, persona_filter });
53
-
54
- const total = results.facts.length + results.topics.length + results.people.length + results.quotes.length;
55
- console.log(`[read_memory] query="${query}" => ${total} results (facts=${results.facts.length}, topics=${results.topics.length}, people=${results.people.length}, quotes=${results.quotes.length})`);
56
-
57
- const output: Record<string, unknown[]> = {};
58
- if (results.facts.length > 0) output.facts = results.facts.map(f => ({ name: f.name, description: f.description }));
59
- if (results.topics.length > 0) output.topics = results.topics.map(t => ({ name: t.name, description: t.description }));
60
- if (results.people.length > 0) output.people = results.people.map(p => ({ name: p.name, relationship: p.relationship, description: p.description, identifiers: p.identifiers ?? [] }));
61
- if (results.quotes.length > 0) output.quotes = results.quotes.map(q => ({ text: q.text, speaker: q.speaker }));
62
-
63
- if (Object.keys(output).length === 0) {
64
- return JSON.stringify({ result: "No relevant memories found for this query." });
65
- }
66
-
67
- return JSON.stringify(output);
68
- },
69
- };
70
- }