ei-tui 1.1.0 → 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.
- package/package.json +2 -23
- package/src/core/handlers/dedup.ts +4 -15
- package/src/core/handlers/document-segmentation.ts +2 -3
- package/src/core/handlers/heartbeat.ts +5 -10
- package/src/core/handlers/human-matching.ts +8 -0
- package/src/core/handlers/index.ts +2 -0
- package/src/core/handlers/knowledge-synthesis.ts +50 -0
- package/src/core/handlers/persona-generation.ts +4 -8
- package/src/core/handlers/persona-response.ts +3 -4
- package/src/core/handlers/persona-topics.ts +2 -4
- package/src/core/handlers/rewrite.ts +26 -9
- package/src/core/handlers/rooms.ts +6 -12
- package/src/core/llm-client.ts +13 -3
- package/src/core/message-manager.ts +2 -4
- package/src/core/orchestrators/ceremony.ts +44 -13
- package/src/core/orchestrators/human-extraction.ts +10 -1
- package/src/core/processor.ts +155 -0
- package/src/core/queue-manager.ts +10 -0
- package/src/core/state-manager.ts +35 -0
- package/src/core/tools/builtin/fetch-memory.ts +6 -6
- package/src/core/tools/index.ts +1 -1
- package/src/core/tools/types.ts +1 -1
- package/src/core/types/data-items.ts +1 -1
- package/src/core/types/entities.ts +7 -1
- package/src/core/types/enums.ts +1 -0
- package/src/core/types/integrations.ts +3 -1
- package/src/integrations/claude-code/importer.ts +6 -0
- package/src/integrations/cursor/importer.ts +6 -0
- package/src/integrations/document/unsource.ts +5 -3
- package/src/integrations/opencode/importer.ts +13 -1
- package/src/integrations/persona-history/importer.ts +9 -0
- package/src/prompts/ceremony/people-rewrite.ts +2 -2
- package/src/prompts/ceremony/topic-rewrite.ts +2 -2
- package/src/prompts/index.ts +3 -0
- package/src/prompts/synthesis/index.ts +101 -0
- package/src/prompts/synthesis/types.ts +26 -0
- package/tui/src/commands/generate.tsx +98 -0
- package/tui/src/commands/unsource.tsx +17 -10
- package/tui/src/components/GeneratedDocsOverlay.tsx +136 -0
- package/tui/src/components/PromptInput.tsx +2 -0
- package/tui/src/context/ei.tsx +49 -2
- package/tui/src/util/logger.ts +22 -2
- package/tui/src/util/provider-detection.ts +5 -2
- package/tui/src/util/yaml-provider.ts +2 -8
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { SynthesisPromptData } from "./types.js";
|
|
2
|
+
import type { PromptOutput } from "../response/types.js";
|
|
3
|
+
|
|
4
|
+
export type { SynthesisPromptData, EnrichedTopic, EnrichedPerson } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export function buildSynthesisPrompt(data: SynthesisPromptData): PromptOutput {
|
|
7
|
+
const hasEntityMap = data.loadedEntityNames !== undefined;
|
|
8
|
+
|
|
9
|
+
const system = `You are synthesizing a knowledge document from a personal knowledge base called Ei.
|
|
10
|
+
|
|
11
|
+
Your goal is to produce a well-structured markdown document that a human could share with a teammate, hand to their future self, or use as a reference. Write as if you are distilling what someone actually knows — not restating a list of facts, but synthesizing relationships, context, and meaning.
|
|
12
|
+
|
|
13
|
+
## What you have been given
|
|
14
|
+
|
|
15
|
+
Everything below is complete as provided — do not use tools to re-fetch records already present here. Only use tools to fill genuine gaps not covered by the data below.
|
|
16
|
+
|
|
17
|
+
- **Facts**: Ground-truth statements.
|
|
18
|
+
- **Topics**: Areas of interest, work, or concern with descriptions.
|
|
19
|
+
- **People**: Individuals with relationship context.
|
|
20
|
+
- **Quotes**: Verbatim things said, with a \`message_id\`. Use \`fetch_message\` with the \`message_id\` if you want the surrounding conversation for additional context.${hasEntityMap ? `
|
|
21
|
+
- **Quote links**: Each quote lists the entities it was extracted from. Entities marked \`(not loaded)\` were referenced by that quote but are not present in this payload — use \`fetch_memory\` with the entity ID to retrieve them if the gap is relevant to your synthesis.` : ""}
|
|
22
|
+
|
|
23
|
+
## Output
|
|
24
|
+
|
|
25
|
+
Write clean, structured markdown. Use headings. Synthesize — do not just restate the bullets. Where the data tells a story or shows a pattern, say so. Where something is uncertain or a work-in-progress, reflect that. Aim for the document a thoughtful person would write after reviewing all of this, not a formatted dump.`;
|
|
26
|
+
|
|
27
|
+
const lines: string[] = [`# ${data.subject}`, ""];
|
|
28
|
+
|
|
29
|
+
const formatQuoteLinks = (dataItemIds: string[]): string | null => {
|
|
30
|
+
if (!hasEntityMap || dataItemIds.length === 0) return null;
|
|
31
|
+
const labels = dataItemIds.map(id => {
|
|
32
|
+
const name = data.loadedEntityNames!.get(id);
|
|
33
|
+
return name ? `[id:${id}] ${name}` : `[id:${id}] (not loaded)`;
|
|
34
|
+
});
|
|
35
|
+
return ` _Links: ${labels.join(", ")}_`;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
if (data.facts.length > 0) {
|
|
39
|
+
lines.push("## Facts");
|
|
40
|
+
for (const fact of data.facts) {
|
|
41
|
+
lines.push(`- [id:${fact.id}] **${fact.name}**: ${fact.description}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push("");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (data.topics.length > 0) {
|
|
47
|
+
lines.push("## Topics");
|
|
48
|
+
for (const { topic, quotes } of data.topics) {
|
|
49
|
+
const categoryTag = topic.category ? ` _(${topic.category})_` : "";
|
|
50
|
+
lines.push(`### [id:${topic.id}] ${topic.name}${categoryTag}`);
|
|
51
|
+
lines.push(topic.description);
|
|
52
|
+
if (quotes.length > 0) {
|
|
53
|
+
lines.push("");
|
|
54
|
+
lines.push("**Related quotes:**");
|
|
55
|
+
for (const q of quotes) {
|
|
56
|
+
const attribution = q.channel ? `${q.speaker} in ${q.channel}` : q.speaker;
|
|
57
|
+
lines.push(`- [message_id:${q.message_id ?? "none"}] "${q.text}" — ${attribution}`);
|
|
58
|
+
const linkLine = formatQuoteLinks(q.data_item_ids);
|
|
59
|
+
if (linkLine) lines.push(linkLine);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
lines.push("");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (data.people.length > 0) {
|
|
67
|
+
lines.push("## People");
|
|
68
|
+
for (const { person, quotes } of data.people) {
|
|
69
|
+
lines.push(`### [id:${person.id}] ${person.name}`);
|
|
70
|
+
lines.push(`_${person.relationship}_`);
|
|
71
|
+
lines.push("");
|
|
72
|
+
lines.push(person.description);
|
|
73
|
+
if (quotes.length > 0) {
|
|
74
|
+
lines.push("");
|
|
75
|
+
lines.push("**Related quotes:**");
|
|
76
|
+
for (const q of quotes) {
|
|
77
|
+
const attribution = q.channel ? `${q.speaker} in ${q.channel}` : q.speaker;
|
|
78
|
+
lines.push(`- [message_id:${q.message_id ?? "none"}] "${q.text}" — ${attribution}`);
|
|
79
|
+
const linkLine = formatQuoteLinks(q.data_item_ids);
|
|
80
|
+
if (linkLine) lines.push(linkLine);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
lines.push("");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (data.standaloneQuotes.length > 0) {
|
|
88
|
+
lines.push("## Additional Quotes");
|
|
89
|
+
for (const q of data.standaloneQuotes) {
|
|
90
|
+
const attribution = q.channel ? `${q.speaker} in ${q.channel}` : q.speaker;
|
|
91
|
+
lines.push(`- [message_id:${q.message_id ?? "none"}] "${q.text}" — ${attribution}`);
|
|
92
|
+
const linkLine = formatQuoteLinks(q.data_item_ids);
|
|
93
|
+
if (linkLine) lines.push(linkLine);
|
|
94
|
+
}
|
|
95
|
+
lines.push("");
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const user = lines.join("\n");
|
|
99
|
+
|
|
100
|
+
return { system, user };
|
|
101
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Fact, Topic, Person, Quote } from "../../core/types.js";
|
|
2
|
+
|
|
3
|
+
export interface EnrichedTopic {
|
|
4
|
+
topic: Topic;
|
|
5
|
+
quotes: Quote[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface EnrichedPerson {
|
|
9
|
+
person: Person;
|
|
10
|
+
quotes: Quote[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SynthesisPromptData {
|
|
14
|
+
subject: string;
|
|
15
|
+
facts: Fact[];
|
|
16
|
+
topics: EnrichedTopic[];
|
|
17
|
+
people: EnrichedPerson[];
|
|
18
|
+
standaloneQuotes: Quote[];
|
|
19
|
+
/**
|
|
20
|
+
* Map of entity ID → display name for all entities included in this payload.
|
|
21
|
+
* Used to annotate quote links: IDs present in a quote's data_item_ids but
|
|
22
|
+
* absent from this map are rendered as "(not loaded)" — a signal to the LLM
|
|
23
|
+
* that a related record exists and can be fetched via fetch_memory.
|
|
24
|
+
*/
|
|
25
|
+
loadedEntityNames?: Map<string, string>;
|
|
26
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Command } from "./registry";
|
|
2
|
+
import { ConfirmOverlay } from "../components/ConfirmOverlay";
|
|
3
|
+
import { GeneratedDocsOverlay } from "../components/GeneratedDocsOverlay";
|
|
4
|
+
|
|
5
|
+
async function doGenerate(subject: string, ctx: Parameters<Command["execute"]>[1]): Promise<void> {
|
|
6
|
+
const { model, isRewriteModel } = ctx.ei.checkGenerationModel();
|
|
7
|
+
|
|
8
|
+
if (!isRewriteModel) {
|
|
9
|
+
const confirmed = await new Promise<boolean>((resolve) => {
|
|
10
|
+
const msg = [
|
|
11
|
+
`Generating with your default model (${model}).`,
|
|
12
|
+
"A high-capability model (Opus-class) is recommended.",
|
|
13
|
+
"Set one via /settings → rewrite_model, or continue anyway?",
|
|
14
|
+
].join("\n");
|
|
15
|
+
|
|
16
|
+
ctx.showOverlay((hideOverlay) => (
|
|
17
|
+
<ConfirmOverlay
|
|
18
|
+
message={msg}
|
|
19
|
+
onConfirm={() => { hideOverlay(); resolve(true); }}
|
|
20
|
+
onCancel={() => { hideOverlay(); resolve(false); }}
|
|
21
|
+
/>
|
|
22
|
+
), ctx.renderer);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!confirmed) {
|
|
26
|
+
ctx.showNotification("Cancelled", "info");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
ctx.showNotification(`Generating knowledge document about: ${subject.slice(0, 60)}`, "info");
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await ctx.ei.generateDocument(subject);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
37
|
+
ctx.showNotification(`Generation failed: ${message}`, "error");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function writeDoc(slug: string, ctx: Parameters<Command["execute"]>[1]): Promise<void> {
|
|
42
|
+
const outPath = await ctx.ei.writeGeneratedDocument(slug);
|
|
43
|
+
if (!outPath) {
|
|
44
|
+
ctx.showNotification(`No content found for document "${slug}"`, "error");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
ctx.showNotification(`Written to ${outPath}`, "info");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const generateCommand: Command = {
|
|
51
|
+
name: "generate",
|
|
52
|
+
aliases: [],
|
|
53
|
+
description: "Generate a knowledge document | /generate <subject> to create, /generate to manage",
|
|
54
|
+
usage: "/generate [subject description]",
|
|
55
|
+
|
|
56
|
+
async execute(args, ctx) {
|
|
57
|
+
if (args.length === 0) {
|
|
58
|
+
const human = await ctx.ei.getHuman();
|
|
59
|
+
const docs = human.settings?.document?.processed_documents ?? {};
|
|
60
|
+
const generated = Object.entries(docs)
|
|
61
|
+
.filter(([, r]) => r.type === "generated" && r.subject)
|
|
62
|
+
.sort(([, a], [, b]) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
|
63
|
+
.map(([slug, r]) => ({ slug, subject: r.subject!, created_at: r.created_at }));
|
|
64
|
+
|
|
65
|
+
if (generated.length === 0) {
|
|
66
|
+
ctx.showNotification(
|
|
67
|
+
"No generated documents yet. Use /generate <subject description> to create one.",
|
|
68
|
+
"info"
|
|
69
|
+
);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ctx.showOverlay((hideOverlay) => (
|
|
74
|
+
<GeneratedDocsOverlay
|
|
75
|
+
docs={generated}
|
|
76
|
+
onWrite={async (doc) => {
|
|
77
|
+
hideOverlay();
|
|
78
|
+
await writeDoc(doc.slug, ctx);
|
|
79
|
+
}}
|
|
80
|
+
onReRun={async (doc) => {
|
|
81
|
+
hideOverlay();
|
|
82
|
+
ctx.showNotification(`Re-running generation for: ${doc.subject.slice(0, 60)}`, "info");
|
|
83
|
+
try {
|
|
84
|
+
await ctx.ei.reRunDocument(doc.slug);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
87
|
+
ctx.showNotification(`Re-run failed: ${message}`, "error");
|
|
88
|
+
}
|
|
89
|
+
}}
|
|
90
|
+
onDismiss={hideOverlay}
|
|
91
|
+
/>
|
|
92
|
+
), ctx.renderer);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
await doGenerate(args.join(" "), ctx);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
@@ -19,15 +19,19 @@ export const unsourceCommand: Command = {
|
|
|
19
19
|
return;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const items = sources.map(f =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
22
|
+
const items = sources.map(f => {
|
|
23
|
+
const prefix = docs[f]?.type === "generated" ? "generate:document:" : "import:document:";
|
|
24
|
+
const tag = `${prefix}${f}`;
|
|
25
|
+
return {
|
|
26
|
+
id: tag,
|
|
27
|
+
display_name: tag,
|
|
28
|
+
aliases: [] as string[],
|
|
29
|
+
is_paused: false,
|
|
30
|
+
is_archived: false,
|
|
31
|
+
unread_count: 0,
|
|
32
|
+
has_pending_update: false,
|
|
33
|
+
};
|
|
34
|
+
});
|
|
31
35
|
|
|
32
36
|
ctx.showOverlay((hideOverlay) => (
|
|
33
37
|
<PersonaListOverlay
|
|
@@ -50,7 +54,10 @@ export const unsourceCommand: Command = {
|
|
|
50
54
|
if (!rawArg.includes(":")) {
|
|
51
55
|
const human = await ctx.ei.getHuman();
|
|
52
56
|
const docs = human.settings?.document?.processed_documents ?? {};
|
|
53
|
-
const allSources = Object.keys(docs).map(f =>
|
|
57
|
+
const allSources = Object.keys(docs).map(f => {
|
|
58
|
+
const prefix = docs[f]?.type === "generated" ? "generate:document:" : "import:document:";
|
|
59
|
+
return `${prefix}${f}`;
|
|
60
|
+
});
|
|
54
61
|
const matches = allSources.filter(s => s.endsWith(rawArg) || s.includes(rawArg));
|
|
55
62
|
if (matches.length === 1) {
|
|
56
63
|
sourceTag = matches[0];
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { useKeyboard } from "@opentui/solid";
|
|
2
|
+
import { For, createSignal, createMemo, onMount, onCleanup } from "solid-js";
|
|
3
|
+
import type { KeyEvent } from "@opentui/core";
|
|
4
|
+
import { useKeyboardNav } from "../context/keyboard.js";
|
|
5
|
+
|
|
6
|
+
export interface GeneratedDocItem {
|
|
7
|
+
slug: string;
|
|
8
|
+
subject: string;
|
|
9
|
+
created_at: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface GeneratedDocsOverlayProps {
|
|
13
|
+
docs: GeneratedDocItem[];
|
|
14
|
+
onWrite: (doc: GeneratedDocItem) => void;
|
|
15
|
+
onReRun: (doc: GeneratedDocItem) => void;
|
|
16
|
+
onDismiss: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function GeneratedDocsOverlay(props: GeneratedDocsOverlayProps) {
|
|
20
|
+
const { setOverlayActive } = useKeyboardNav();
|
|
21
|
+
onMount(() => setOverlayActive(true));
|
|
22
|
+
onCleanup(() => setOverlayActive(false));
|
|
23
|
+
|
|
24
|
+
const [selectedIndex, setSelectedIndex] = createSignal(0);
|
|
25
|
+
|
|
26
|
+
const clampedIndex = createMemo(() =>
|
|
27
|
+
Math.min(selectedIndex(), Math.max(0, props.docs.length - 1))
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
useKeyboard((event: KeyEvent) => {
|
|
31
|
+
const key = event.name;
|
|
32
|
+
const listLength = props.docs.length;
|
|
33
|
+
|
|
34
|
+
if (key === "j" || key === "down") {
|
|
35
|
+
event.preventDefault();
|
|
36
|
+
setSelectedIndex((prev) => Math.min(prev + 1, listLength - 1));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (key === "k" || key === "up") {
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
setSelectedIndex((prev) => Math.max(prev - 1, 0));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (key === "return" || key === "w") {
|
|
47
|
+
event.preventDefault();
|
|
48
|
+
if (listLength > 0) props.onWrite(props.docs[clampedIndex()]);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (key === "r") {
|
|
53
|
+
event.preventDefault();
|
|
54
|
+
if (listLength > 0) props.onReRun(props.docs[clampedIndex()]);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (key === "escape") {
|
|
59
|
+
event.preventDefault();
|
|
60
|
+
props.onDismiss();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const formatDate = (iso: string) => {
|
|
66
|
+
try {
|
|
67
|
+
return new Date(iso).toLocaleDateString(undefined, {
|
|
68
|
+
year: "numeric",
|
|
69
|
+
month: "short",
|
|
70
|
+
day: "numeric",
|
|
71
|
+
});
|
|
72
|
+
} catch {
|
|
73
|
+
return iso.slice(0, 10);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const truncate = (s: string, max: number) =>
|
|
78
|
+
s.length > max ? s.slice(0, max - 3) + "..." : s;
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<box
|
|
82
|
+
position="absolute"
|
|
83
|
+
width="100%"
|
|
84
|
+
height="100%"
|
|
85
|
+
left={0}
|
|
86
|
+
top={0}
|
|
87
|
+
backgroundColor="#000000"
|
|
88
|
+
alignItems="center"
|
|
89
|
+
justifyContent="center"
|
|
90
|
+
>
|
|
91
|
+
<box
|
|
92
|
+
width={72}
|
|
93
|
+
height="80%"
|
|
94
|
+
backgroundColor="#1a1a2e"
|
|
95
|
+
borderStyle="single"
|
|
96
|
+
borderColor="#586e75"
|
|
97
|
+
padding={2}
|
|
98
|
+
flexDirection="column"
|
|
99
|
+
>
|
|
100
|
+
<text fg="#eee8d5" marginBottom={1}>
|
|
101
|
+
Generated Documents
|
|
102
|
+
</text>
|
|
103
|
+
|
|
104
|
+
<scrollbox height="100%">
|
|
105
|
+
<For each={props.docs}>
|
|
106
|
+
{(doc, index) => {
|
|
107
|
+
const isSelected = () => clampedIndex() === index();
|
|
108
|
+
const label = () =>
|
|
109
|
+
` ${truncate(doc.subject, 48)} ${formatDate(doc.created_at)}`;
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<box
|
|
113
|
+
backgroundColor={isSelected() ? "#2d3748" : "transparent"}
|
|
114
|
+
paddingLeft={1}
|
|
115
|
+
paddingRight={1}
|
|
116
|
+
>
|
|
117
|
+
<text fg={isSelected() ? "#eee8d5" : "#839496"}>
|
|
118
|
+
{label()}
|
|
119
|
+
</text>
|
|
120
|
+
</box>
|
|
121
|
+
);
|
|
122
|
+
}}
|
|
123
|
+
</For>
|
|
124
|
+
</scrollbox>
|
|
125
|
+
|
|
126
|
+
<text> </text>
|
|
127
|
+
<text fg="#586e75">
|
|
128
|
+
j/k: navigate | Enter/w: write file | r: re-run | Esc: cancel
|
|
129
|
+
</text>
|
|
130
|
+
<text fg="#dc322f">
|
|
131
|
+
⚠ Re-running replaces the existing file — write first to keep it
|
|
132
|
+
</text>
|
|
133
|
+
</box>
|
|
134
|
+
</box>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
@@ -32,6 +32,7 @@ import { silenceCommand } from "../commands/silence.js";
|
|
|
32
32
|
import { captureCommand } from "../commands/capture.js";
|
|
33
33
|
import { importCommand } from "../commands/import.js";
|
|
34
34
|
import { unsourceCommand } from "../commands/unsource.js";
|
|
35
|
+
import { generateCommand } from "../commands/generate.js";
|
|
35
36
|
import { openCYPEditor } from "../util/cyp-editor.js";
|
|
36
37
|
import { useOverlay } from "../context/overlay";
|
|
37
38
|
import { CommandSuggest } from "./CommandSuggest";
|
|
@@ -90,6 +91,7 @@ export function PromptInput() {
|
|
|
90
91
|
registerCommand(captureCommand);
|
|
91
92
|
registerCommand(importCommand);
|
|
92
93
|
registerCommand(unsourceCommand);
|
|
94
|
+
registerCommand(generateCommand);
|
|
93
95
|
registerCommand(authCommand);
|
|
94
96
|
registerCommand(pauseCommand);
|
|
95
97
|
registerCommand(resumeCommand);
|
package/tui/src/context/ei.tsx
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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>
|
package/tui/src/util/logger.ts
CHANGED
|
@@ -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
|
|
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 ?? (
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|