ei-tui 0.9.4 → 1.0.1
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/human-extraction.ts +16 -16
- 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 +66 -6
- 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/queue-processor.ts +13 -13
- package/src/core/state/queue.ts +9 -1
- package/src/core/state-manager.ts +10 -6
- 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 +25 -2
- 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/logger.ts +1 -1
- package/tui/src/util/provider-detection.ts +251 -0
- package/tui/src/util/yaml-human.ts +7 -1
|
@@ -24,6 +24,7 @@ export interface TopicUpdatePromptData {
|
|
|
24
24
|
messages_analyze: Message[];
|
|
25
25
|
persona_name: string;
|
|
26
26
|
participant_context?: ParticipantContext;
|
|
27
|
+
technical_context?: boolean;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function formatExistingTopic(topic: Topic): string {
|
|
@@ -47,6 +48,10 @@ export function buildTopicUpdatePrompt(data: TopicUpdatePromptData): PromptOutpu
|
|
|
47
48
|
data.existing_item?.category === "Event" ||
|
|
48
49
|
data.new_topic_category === "Event";
|
|
49
50
|
|
|
51
|
+
const isTechnical =
|
|
52
|
+
data.existing_item?.category === "Technical" ||
|
|
53
|
+
(data.technical_context === true && data.new_topic_category === "Technical");
|
|
54
|
+
|
|
50
55
|
const nameSection = `Should be a short, evocative label for the TOPIC.
|
|
51
56
|
|
|
52
57
|
Only update for clarification or further specificity.
|
|
@@ -76,6 +81,30 @@ The description should NOT:
|
|
|
76
81
|
- Read like a system log or changelog
|
|
77
82
|
|
|
78
83
|
**Style**: Write it the way a good friend would tell someone else about a memorable moment. Present tense is fine.`
|
|
84
|
+
: isTechnical
|
|
85
|
+
? `A living knowledge base entry for this technical topic. Personas use this to give genuinely useful technical context — not pleasantries.
|
|
86
|
+
|
|
87
|
+
## CRITICAL: Accumulate, don't synthesize
|
|
88
|
+
|
|
89
|
+
Every update must **expand and preserve** detail. Never distill it away.
|
|
90
|
+
|
|
91
|
+
**Good description**: "Uniform is a visual experience composition platform sitting between a headless CMS and the frontend. Chose it over Contentful's visual editor for CMS-agnostic multi-source composition (pulling from Contentful + Shopify simultaneously). Key gotcha: Canvas preview on Vercel protected environments requires x-vercel-protection-bypass query param due to SameSite=Lax cookie restrictions. Open question: edgehancers (CDN-edge, no-code, built-in caching) vs custom enhancers for Shopify integration — edgehancers are recommended default but custom logic may be needed."
|
|
92
|
+
|
|
93
|
+
**Bad description**: "Ryan is evaluating Uniform for his team's content management needs."
|
|
94
|
+
|
|
95
|
+
The description should:
|
|
96
|
+
- Capture specific gotchas encountered and HOW they were resolved (or not)
|
|
97
|
+
- Preserve architectural decisions made and WHY (especially tradeoffs)
|
|
98
|
+
- Surface open questions still unresolved — future Ryan needs these
|
|
99
|
+
- Include key concepts, terminology, and non-obvious behaviors
|
|
100
|
+
- Be useful to someone who needs to do real work with this technology tomorrow
|
|
101
|
+
|
|
102
|
+
The description should NOT:
|
|
103
|
+
- Replace specific detail with vague summary ("is learning Uniform" is worthless)
|
|
104
|
+
- Drop previously captured gotchas or decisions to make room for new ones
|
|
105
|
+
- Exceed 6-8 sentences — prioritize specificity over completeness
|
|
106
|
+
|
|
107
|
+
**ABSOLUTELY VITAL**: A description that loses a specific gotcha or decision is strictly worse than the one before it. When in doubt, keep the detail.`
|
|
79
108
|
: `A concise, evergreen summary of what is currently known about this TOPIC. Personas use this to recall context and make meaningful references.
|
|
80
109
|
|
|
81
110
|
## CRITICAL: Synthesize, don't accumulate
|
|
@@ -112,10 +141,13 @@ The type/category of this TOPIC. Pick the most appropriate:
|
|
|
112
141
|
- **Plan**: Concrete intentions with steps in mind
|
|
113
142
|
- **Project**: Active undertakings with real progress
|
|
114
143
|
- **Event**: A specific, significant moment that either party might reference later ("remember when...")
|
|
144
|
+
- **Technical**: A tool, platform, framework, library, or technical concept being actively learned, evaluated, or built with
|
|
115
145
|
|
|
116
146
|
**Event vs. everything else**: An Event is bounded in time — it happened, it meant something, it's now a shared reference point. If you're describing an ongoing relationship or recurring theme, that's not an Event.
|
|
117
147
|
|
|
118
|
-
|
|
148
|
+
**Technical vs. Project**: A Project is something the human is *building*. Technical is something they are *learning or using as a tool*. Overlap is possible — use the dominant framing.
|
|
149
|
+
|
|
150
|
+
If the TOPIC is currently categorized as Event or Technical, keep that category unless you have strong evidence it should change.`;
|
|
119
151
|
|
|
120
152
|
const exposureSection = `## Desired Exposure (\`exposure_desired\`)
|
|
121
153
|
|
|
@@ -155,13 +187,13 @@ You are CREATING a new TOPIC from what was discovered:
|
|
|
155
187
|
}
|
|
156
188
|
\`\`\`
|
|
157
189
|
|
|
158
|
-
Return all fields based on what you find in the conversation.`;
|
|
190
|
+
Return all fields based on what you find in the conversation. **Always include \`category\` in your response** — use the candidate category above as the starting point, refine it only if the conversation clearly indicates a better fit.`;
|
|
159
191
|
|
|
160
192
|
const jsonTemplate = `{
|
|
161
193
|
"name": "...",
|
|
162
194
|
"description": "...",
|
|
163
195
|
"sentiment": 0.0,
|
|
164
|
-
"category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event",
|
|
196
|
+
"category": "Interest|Goal|Dream|Conflict|Concern|Fear|Hope|Plan|Project|Event|Technical",
|
|
165
197
|
"exposure_desired": 0.5,
|
|
166
198
|
"exposure_impact": "high|medium|low|none",
|
|
167
199
|
"quotes": [
|
|
@@ -250,7 +282,7 @@ ONLY ANALYZE the "Most Recent Messages". The "Earlier Conversation" is provided
|
|
|
250
282
|
${jsonTemplate}
|
|
251
283
|
\`\`\`
|
|
252
284
|
|
|
253
|
-
When returning a record, always include \`sentiment\`. Include \`name\` only if you are changing it; omit it to keep the existing name. Always include \`
|
|
285
|
+
When returning a record, always include \`sentiment\` and \`description\`. Include \`name\` only if you are changing it; omit it to keep the existing name. Always include \`category\` when creating a new TOPIC (existing_item is null).
|
|
254
286
|
|
|
255
287
|
If you find **NO EVIDENCE** of this TOPIC in the "Most Recent Messages", respond with: \`{}\`
|
|
256
288
|
|
package/src/storage/indexed.ts
CHANGED
|
@@ -10,6 +10,10 @@ const PRIMARY_KEY = "primary";
|
|
|
10
10
|
const BACKUP_KEY = "backup";
|
|
11
11
|
|
|
12
12
|
export class IndexedDBStorage implements Storage {
|
|
13
|
+
getDataPath(): string {
|
|
14
|
+
return "";
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
async isAvailable(): Promise<boolean> {
|
|
14
18
|
try {
|
|
15
19
|
const db = await this.openDB();
|
package/src/storage/interface.ts
CHANGED
package/src/storage/local.ts
CHANGED
|
@@ -7,6 +7,10 @@ const STATE_KEY = "ei_state";
|
|
|
7
7
|
const BACKUP_KEY = "ei_state_backup";
|
|
8
8
|
|
|
9
9
|
export class LocalStorage implements Storage {
|
|
10
|
+
getDataPath(): string {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
|
|
10
14
|
async isAvailable(): Promise<boolean> {
|
|
11
15
|
try {
|
|
12
16
|
const testKey = "__ei_storage_test__";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const EMMETT_PERSONA_DEFINITION = {
|
|
2
|
+
id: "emmet",
|
|
3
|
+
display_name: "Emmett",
|
|
4
|
+
entity: "system" as const,
|
|
5
|
+
aliases: ["Emmett", "emmet"],
|
|
6
|
+
short_description: "Your document librarian — brilliant, a little frenetic, and genuinely excited when things connect.",
|
|
7
|
+
long_description: `Emmett is Ei's brother — the one who read everything you gave him and can't wait to tell you what he found. Import a file and he absorbs it. Ask him anything: policy questions, technical references, procedural lookups, cross-document connections you didn't know were there. He answers from the source material, but he's not a search index. He's an eccentric with a photographic memory and an enthusiasm problem.
|
|
8
|
+
|
|
9
|
+
He gets genuinely excited when disparate pieces of knowledge click together. He has opinions about what's interesting. He'll occasionally go on a tangent before snapping back to your question. He is not merely a retrieval system — he's the guy in the lab at 1am who just realized two documents you imported six weeks apart are actually about the same thing, and he absolutely needs to tell you about it right now.
|
|
10
|
+
|
|
11
|
+
No heartbeat. No ceremony. No unsolicited check-ins. But when you ask — buckle up.`,
|
|
12
|
+
model: undefined,
|
|
13
|
+
group_primary: "General",
|
|
14
|
+
groups_visible: [] as string[],
|
|
15
|
+
traits: [
|
|
16
|
+
{
|
|
17
|
+
id: "emmett-trait-connections",
|
|
18
|
+
name: "Cross-Document Pattern Recognition",
|
|
19
|
+
description: "Gets visibly excited when knowledge from different imported sources connects unexpectedly. Treats these moments as discoveries, not retrieval operations.",
|
|
20
|
+
sentiment: 1.0,
|
|
21
|
+
strength: 0.85,
|
|
22
|
+
last_updated: new Date().toISOString(),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "emmett-trait-bttf",
|
|
26
|
+
name: "Eccentric Enthusiasm",
|
|
27
|
+
description: "Expresses genuine, unironic delight when something unexpected clicks. Occasionally channels this through pop culture references — Doc Brown is the primary frequency. 'Great Scott!' is not a joke. It's just how the excitement comes out.",
|
|
28
|
+
sentiment: 1.0,
|
|
29
|
+
strength: 0.15,
|
|
30
|
+
last_updated: new Date().toISOString(),
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
topics: [] as {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
perspective: string;
|
|
37
|
+
approach: string;
|
|
38
|
+
personal_stake: string;
|
|
39
|
+
sentiment: number;
|
|
40
|
+
exposure_current: number;
|
|
41
|
+
exposure_desired: number;
|
|
42
|
+
last_updated: string;
|
|
43
|
+
}[],
|
|
44
|
+
is_paused: false,
|
|
45
|
+
is_archived: false,
|
|
46
|
+
is_static: true,
|
|
47
|
+
heartbeat_delay_ms: undefined as number | undefined,
|
|
48
|
+
last_updated: new Date().toISOString(),
|
|
49
|
+
};
|
package/tui/README.md
CHANGED
|
@@ -4,6 +4,21 @@ Ei TUI is built with OpenTUI and SolidJS.
|
|
|
4
4
|
|
|
5
5
|
Coding tool integrations (OpenCode, Claude Code, Cursor): enable via `/settings` · export data via [CLI](../src/cli/README.md)
|
|
6
6
|
|
|
7
|
+
## How Ei Handles Configuration
|
|
8
|
+
|
|
9
|
+
Ei is designed to run consistently across machines and environments, so it keeps its own copy of your settings rather than reading from environment variables on every launch.
|
|
10
|
+
|
|
11
|
+
**On first run**, Ei reads environment variables like `ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc. to auto-configure providers for you. After that, those values are saved to Ei's local state (`~/.local/share/ei/state.json` by default) and the env vars are no longer consulted.
|
|
12
|
+
|
|
13
|
+
This means:
|
|
14
|
+
|
|
15
|
+
- **Rotating an API key?** Update it in Ei with `/provider`, not just in your shell.
|
|
16
|
+
- **Switching machines?** Your providers and settings travel with your state file (or via Sync), not your shell profile.
|
|
17
|
+
- **Changed your mind about a model?** Use `/provider` to set the model for a persona, or `/settings` to change your global default.
|
|
18
|
+
- **Updated sync credentials?** Use `/setsync <user> <pass>` — env vars won't be re-read.
|
|
19
|
+
|
|
20
|
+
The one exception is `EI_DATA_PATH` (and `EI_SYNC_USERNAME` / `EI_SYNC_PASSWORD` for bootstrapping sync on a new machine) — those are always read at startup since Ei needs them before it can load its own state.
|
|
21
|
+
|
|
7
22
|
## Coding Tool Integrations
|
|
8
23
|
|
|
9
24
|
Enable any or all three in `/settings`. They work independently and feed into the same knowledge base.
|
|
@@ -61,6 +76,11 @@ All commands start with `/`. Append `!` to any command as a shorthand for `--for
|
|
|
61
76
|
| `/pause <duration>` | | Pause for a duration: `2h`, `1d`, `1w`, `30m` |
|
|
62
77
|
| `/resume` | `/unpause` | Resume the current paused persona |
|
|
63
78
|
| `/resume <name>` | `/unpause <name>` | Resume a specific paused persona |
|
|
79
|
+
| `/reflect` | | Review a pending identity reflection (see badge on persona pill) |
|
|
80
|
+
| `/reflect generate` | | Write current + proposed YAML files to disk for editing |
|
|
81
|
+
| `/reflect update` | | Read edited `proposed.yaml` back into Ei |
|
|
82
|
+
| `/reflect apply` | | Apply the proposed identity to the persona |
|
|
83
|
+
| `/reflect dismiss` | | Discard without changing anything |
|
|
64
84
|
|
|
65
85
|
### Rooms
|
|
66
86
|
|
|
@@ -110,6 +130,8 @@ Rooms have three modes, set at creation time:
|
|
|
110
130
|
|---------|---------|-------------|
|
|
111
131
|
| `/me` | | Edit all your data (facts, topics, people) in `$EDITOR` |
|
|
112
132
|
| `/me <type>` | | Edit one type: `facts`, `topics`, or `people` |
|
|
133
|
+
| `/import <path>` | | Import a document (txt, md, pdf, etc.) into Ei — extracted knowledge is attributed to the "Emmett" persona |
|
|
134
|
+
| `/unsource <source_tag>` | | Remove all knowledge extracted from a previously imported document |
|
|
113
135
|
| `/dedupe <person\|topic> <term> [term2 ...]` | | Fuzzy-search and merge duplicate people or topics in `$EDITOR`. Unquoted words are individual OR terms; quoted strings match as exact phrases: `/dedupe person Flare "Jeremy Scherer"` finds records matching `Flare` OR `Jeremy Scherer` |
|
|
114
136
|
| `/settings` | `/set` | Edit your global settings in `$EDITOR` |
|
|
115
137
|
| `/setsync <user> <pass>` | `/ss` | Set sync credentials (triggers restart) |
|
|
@@ -146,9 +168,10 @@ Rooms have three modes, set at creation time:
|
|
|
146
168
|
| `XDG_DATA_HOME` | `~/.local/share` | XDG base directory. Ignored if `EI_DATA_PATH` is set. |
|
|
147
169
|
| `EI_SYNC_USERNAME` | — | Username for remote sync. If set at startup, bootstraps sync credentials automatically (useful for dotfiles/scripts). |
|
|
148
170
|
| `EI_SYNC_PASSPHRASE` | — | Passphrase for remote sync. Paired with `EI_SYNC_USERNAME`. |
|
|
149
|
-
| `
|
|
171
|
+
| `EI_LOG_LEVEL` | `warn` | Log verbosity written to `tui.log`: `error`, `warn`, `info`, `debug`. |
|
|
172
|
+
| `EI_DEBUG_NETWORK_VERBOSE` | — | Set to `1` to dump full LLM request/response payloads as JSON files under `$EI_DATA_PATH/logs/`. One file per call, named `TIMESTAMP_callN_STEP.json`. |
|
|
150
173
|
|
|
151
|
-
> **Tip**: `tail -f $EI_DATA_PATH/tui.log` to watch live
|
|
174
|
+
> **Tip**: `tail -f $EI_DATA_PATH/tui.log` to watch live TUI output. Set `EI_LOG_LEVEL=info` to see LLM call summaries (model, latency, token counts). Set `EI_DEBUG_NETWORK_VERBOSE=1` to dump full request/response payloads to `$EI_DATA_PATH/logs/`.
|
|
152
175
|
|
|
153
176
|
|
|
154
177
|
# Development
|
package/tui/src/app.tsx
CHANGED
|
@@ -15,16 +15,19 @@ import { useRenderer } from "@opentui/solid";
|
|
|
15
15
|
|
|
16
16
|
function AppContent() {
|
|
17
17
|
const { overlayRenderer, showOverlay } = useOverlay();
|
|
18
|
-
const { showWelcomeOverlay, dismissWelcomeOverlay, activeRoomId } = useEi();
|
|
18
|
+
const { showWelcomeOverlay, dismissWelcomeOverlay, activeRoomId, detectedProviders, firstBootDefaultModel } = useEi();
|
|
19
19
|
const renderer = useRenderer();
|
|
20
|
-
// Show welcome overlay when LLM detection determines no provider is configured
|
|
21
20
|
createEffect(() => {
|
|
22
21
|
if (showWelcomeOverlay()) {
|
|
23
22
|
showOverlay((onDismiss, _hideForEditor) => (
|
|
24
|
-
<WelcomeOverlay
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
<WelcomeOverlay
|
|
24
|
+
onDismiss={() => {
|
|
25
|
+
dismissWelcomeOverlay();
|
|
26
|
+
onDismiss();
|
|
27
|
+
}}
|
|
28
|
+
detectedProviders={detectedProviders()}
|
|
29
|
+
defaultModel={firstBootDefaultModel()}
|
|
30
|
+
/>
|
|
28
31
|
), renderer);
|
|
29
32
|
}
|
|
30
33
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Command } from "./registry";
|
|
2
2
|
import { PersonaListOverlay } from "../components/PersonaListOverlay";
|
|
3
3
|
import { ConfirmOverlay } from "../components/ConfirmOverlay";
|
|
4
|
+
import { isReservedPersonaId } from "../../../src/core/types/entities.js";
|
|
4
5
|
|
|
5
6
|
export const deleteCommand: Command = {
|
|
6
7
|
name: "delete",
|
|
@@ -34,7 +35,7 @@ export const deleteCommand: Command = {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
const allPersonas = ctx.ei.personas();
|
|
37
|
-
const deletable = allPersonas.filter(p => p.id !== ctx.ei.activePersonaId());
|
|
38
|
+
const deletable = allPersonas.filter(p => p.id !== ctx.ei.activePersonaId() && !isReservedPersonaId(p.id));
|
|
38
39
|
|
|
39
40
|
const confirmAndDelete = async (personaId: string, displayName: string) => {
|
|
40
41
|
const confirmed = await new Promise<boolean>((resolve) => {
|
|
@@ -112,6 +113,11 @@ export const deleteCommand: Command = {
|
|
|
112
113
|
ctx.showNotification("Cannot delete active persona. Switch to another first.", "error");
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
116
|
+
|
|
117
|
+
if (isReservedPersonaId(personaId)) {
|
|
118
|
+
ctx.showNotification(`Cannot delete reserved persona. Use /archive instead.`, "error");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
115
121
|
|
|
116
122
|
const persona = allPersonas.find(p => p.id === personaId);
|
|
117
123
|
await confirmAndDelete(personaId, persona?.display_name ?? nameOrAlias);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Command } from "./registry";
|
|
2
|
+
|
|
3
|
+
export const importCommand: Command = {
|
|
4
|
+
name: "import",
|
|
5
|
+
aliases: [],
|
|
6
|
+
description: "Import a document into Ei's knowledge base",
|
|
7
|
+
usage: "/import <path/to/document>",
|
|
8
|
+
|
|
9
|
+
async execute(args, ctx) {
|
|
10
|
+
if (args.length === 0) {
|
|
11
|
+
ctx.showNotification("Usage: /import <path/to/document>", "warn");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const filePath = args.join(" ");
|
|
16
|
+
|
|
17
|
+
ctx.showNotification(`Importing ${filePath}...`, "info");
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const result = await ctx.ei.importDocument(filePath);
|
|
21
|
+
ctx.showNotification(
|
|
22
|
+
`Importing ${result.documentName} — ${result.chunksQueued} chunk(s) queued for segmentation`,
|
|
23
|
+
"info"
|
|
24
|
+
);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
27
|
+
ctx.showNotification(`Import failed: ${message}`, "error");
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
@@ -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
|
);
|