ei-tui 0.1.22 → 0.1.23

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "0.1.22",
3
+ "version": "0.1.23",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  ContextStatus,
3
+ LLMNextStep,
3
4
  ValidationLevel,
4
5
  type LLMResponse,
5
6
  type Message,
@@ -24,6 +25,7 @@ export function handleHeartbeatCheck(response: LLMResponse, state: StateManager)
24
25
 
25
26
  const now = new Date().toISOString();
26
27
  state.persona_update(personaId, { last_heartbeat: now });
28
+ state.queue_clearPersonaResponses(personaId, LLMNextStep.HandleHeartbeatCheck);
27
29
 
28
30
  if (!result.should_respond) {
29
31
  console.log(`[handleHeartbeatCheck] ${personaDisplayName} chose not to reach out`);
@@ -52,6 +54,7 @@ export function handleEiHeartbeat(response: LLMResponse, state: StateManager): v
52
54
  }
53
55
  const now = new Date().toISOString();
54
56
  state.persona_update("ei", { last_heartbeat: now });
57
+ state.queue_clearPersonaResponses("ei", LLMNextStep.HandleEiHeartbeat);
55
58
  if (!result.should_respond || !result.id) {
56
59
  console.log("[handleEiHeartbeat] Ei chose not to reach out");
57
60
  return;
@@ -27,9 +27,27 @@ export function handleHumanItemMatch(response: LLMResponse, state: StateManager)
27
27
  const candidateType = response.request.data.candidateType as DataItemType;
28
28
  const personaId = response.request.data.personaId as string;
29
29
  const personaDisplayName = response.request.data.personaDisplayName as string;
30
- const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
30
+ const messageIdsToMark = response.request.data.message_ids_to_mark as string[] | undefined;
31
31
  const allMessages = state.messages_get(personaId);
32
- const { messages_context, messages_analyze } = splitMessagesByTimestamp(allMessages, analyzeFrom);
32
+
33
+ let messages_context: Message[];
34
+ let messages_analyze: Message[];
35
+
36
+ if (messageIdsToMark && messageIdsToMark.length > 0) {
37
+ const messageIdSet = new Set(messageIdsToMark);
38
+ messages_analyze = allMessages.filter(m => messageIdSet.has(m.id));
39
+ const analyzeStartTime = messages_analyze[0]?.timestamp ?? '9999';
40
+ messages_context = allMessages.filter(m =>
41
+ !messageIdSet.has(m.id) && new Date(m.timestamp).getTime() < new Date(analyzeStartTime).getTime()
42
+ );
43
+ } else {
44
+ // Fallback to existing behavior
45
+ const analyzeFrom = response.request.data.analyze_from_timestamp as string | null;
46
+ const split = splitMessagesByTimestamp(allMessages, analyzeFrom);
47
+ messages_context = split.messages_context;
48
+ messages_analyze = split.messages_analyze;
49
+ }
50
+
33
51
  const context: ExtractionContext & { itemName: string; itemValue: string; itemCategory?: string } = {
34
52
  personaId,
35
53
  personaDisplayName,
@@ -212,6 +230,12 @@ export async function handleHumanItemUpdate(response: LLMResponse, state: StateM
212
230
  console.log(`[handleHumanItemUpdate] ${isNewItem ? "Created" : "Updated"} ${candidateType} "${result.name}"`);
213
231
  }
214
232
 
233
+ function normalizeQuotes(text: string): string {
234
+ return text
235
+ .replace(/[\u201C\u201D]/g, '"') // Curly double quotes to straight
236
+ .replace(/[\u2018\u2019]/g, "'"); // Curly single quotes to straight
237
+ }
238
+
215
239
  async function validateAndStoreQuotes(
216
240
  candidates: Array<{ text: string; reason: string }> | undefined,
217
241
  messages: Message[],
@@ -226,7 +250,9 @@ async function validateAndStoreQuotes(
226
250
  let found = false;
227
251
  for (const message of messages) {
228
252
  const msgText = getMessageText(message);
229
- const start = msgText.indexOf(candidate.text);
253
+ const normalizedMsg = normalizeQuotes(msgText);
254
+ const normalizedQuote = normalizeQuotes(candidate.text);
255
+ const start = normalizedMsg.indexOf(normalizedQuote);
230
256
  if (start !== -1) {
231
257
  const end = start + candidate.text.length;
232
258
 
@@ -60,7 +60,7 @@ export function resolveModel(modelSpec?: string, accounts?: ProviderAccount[]):
60
60
  if (accounts) {
61
61
  const searchName = provider || modelSpec; // If no ":", the whole spec might be an account name
62
62
  const matchingAccount = accounts.find(
63
- (acc) => acc.name.toLowerCase() === searchName.toLowerCase() && acc.enabled
63
+ (acc) => acc.name.toLowerCase() === searchName.toLowerCase() && acc.enabled && acc.type === "llm"
64
64
  );
65
65
  if (matchingAccount) {
66
66
  // If bare account name was used, get model from account's default_model
@@ -589,22 +589,42 @@ export class Processor {
589
589
  console.log(`[Processor ${this.instanceId}] stopped`);
590
590
  }
591
591
 
592
- async saveAndExit(): Promise<{ success: boolean; error?: string }> {
593
- console.log(`[Processor ${this.instanceId}] saveAndExit() called`);
594
- this.interface.onSaveAndExitStart?.();
592
+ getStateManager(): StateManager {
593
+ return this.stateManager;
594
+ }
595
595
 
596
+ async pause(): Promise<void> {
597
+ console.log(`[Processor ${this.instanceId}] pause() called`);
598
+ this.running = false;
596
599
  this.queueProcessor.abort();
597
600
  this.importAbortController.abort();
598
601
  if (this.openCodeImportInProgress) {
599
- console.log(`[Processor ${this.instanceId}] Aborting OpenCode import in progress`);
602
+ console.log(`[Processor ${this.instanceId}] Clearing openCodeImportInProgress flag`);
600
603
  this.openCodeImportInProgress = false;
601
604
  }
602
605
  if (this.claudeCodeImportInProgress) {
603
- console.log(`[Processor ${this.instanceId}] Aborting Claude Code import in progress`);
606
+ console.log(`[Processor ${this.instanceId}] Clearing claudeCodeImportInProgress flag`);
604
607
  this.claudeCodeImportInProgress = false;
605
608
  }
606
-
607
609
  await this.stateManager.flush();
610
+ console.log(`[Processor ${this.instanceId}] pause() complete (main loop stopped, state flushed)`);
611
+ }
612
+
613
+ async resume(): Promise<void> {
614
+ console.log(`[Processor ${this.instanceId}] resume() called`);
615
+ if (this.stopped) {
616
+ throw new Error(`Cannot resume a stopped processor (instanceId: ${this.instanceId})`);
617
+ }
618
+ this.importAbortController = new AbortController();
619
+ this.running = true;
620
+ this.runLoop();
621
+ console.log(`[Processor ${this.instanceId}] resume() complete (main loop restarted)`);
622
+ }
623
+ async saveAndExit(): Promise<{ success: boolean; error?: string }> {
624
+ console.log(`[Processor ${this.instanceId}] saveAndExit() called`);
625
+ this.interface.onSaveAndExitStart?.();
626
+
627
+ await this.pause();
608
628
 
609
629
  const human = this.stateManager.getHuman();
610
630
  const hasSyncCreds =
@@ -616,7 +636,7 @@ export class Processor {
616
636
 
617
637
  if (!result.success) {
618
638
  console.log(`[Processor ${this.instanceId}] Remote sync failed: ${result.error}`);
619
- this.importAbortController = new AbortController();
639
+ await this.resume();
620
640
  this.interface.onSaveAndExitFinish?.();
621
641
  return { success: false, error: result.error };
622
642
  }
@@ -1208,6 +1228,16 @@ const toolNextSteps = new Set([
1208
1228
  return removed;
1209
1229
  }
1210
1230
 
1231
+ async addMessageOnly(personaId: string, message: Message): Promise<void> {
1232
+ this.stateManager.messages_append(personaId, message);
1233
+ this.interface.onMessageAdded?.(personaId);
1234
+ }
1235
+
1236
+ async updateMessage(personaId: string, messageId: string, updates: Partial<Message>): Promise<void> {
1237
+ this.stateManager.messages_update(personaId, messageId, updates);
1238
+ this.interface.onMessageAdded?.(personaId);
1239
+ }
1240
+
1211
1241
  // ==========================================================================
1212
1242
  // HUMAN DATA API
1213
1243
  // ==========================================================================
@@ -126,6 +126,16 @@ export class PersonaState {
126
126
  data.entity.last_updated = new Date().toISOString();
127
127
  }
128
128
 
129
+ messages_update(personaId: string, messageId: string, updates: Partial<Message>): boolean {
130
+ const data = this.personas.get(personaId);
131
+ if (!data) return false;
132
+ const msg = data.messages.find((m) => m.id === messageId);
133
+ if (!msg) return false;
134
+ Object.assign(msg, updates);
135
+ data.entity.last_updated = new Date().toISOString();
136
+ return true;
137
+ }
138
+
129
139
  messages_sort(personaId: string): void {
130
140
  const data = this.personas.get(personaId);
131
141
  if (!data) return;
@@ -247,6 +247,12 @@ export class StateManager {
247
247
  this.scheduleSave();
248
248
  }
249
249
 
250
+ messages_update(personaId: string, messageId: string, updates: Partial<Message>): boolean {
251
+ const result = this.personaState.messages_update(personaId, messageId, updates);
252
+ this.scheduleSave();
253
+ return result;
254
+ }
255
+
250
256
  messages_sort(personaId: string): void {
251
257
  this.personaState.messages_sort(personaId);
252
258
  this.scheduleSave();
@@ -60,6 +60,9 @@ export interface ProviderAccount {
60
60
 
61
61
  // Provider-specific extras (e.g., OpenRouter needs HTTP-Referer, X-Title)
62
62
  extra_headers?: Record<string, string>;
63
+
64
+ // Image provider-specific
65
+ workflow_json?: Record<string, any>; // ComfyUI workflow template (image providers only)
63
66
 
64
67
  // Metadata
65
68
  enabled?: boolean; // Default: true
@@ -134,6 +137,13 @@ export interface PersonaCreationInput {
134
137
  tools?: string[]; // IDs of ToolDefinitions to assign at creation time
135
138
  }
136
139
 
140
+ // ComfyUI z-image-turbo basic string
141
+ // Prompt - "57:27"."inputs"."text"
142
+ // Width - "57:13"."inputs"."width"
143
+ // Height - "57:13"."inputs"."height"
144
+ // Steps - "57:3"."inputs"."steps"
145
+ // Cfg - "57:3"."inputs"."cfg"
146
+ export const COMFY_PROMPT_TEMPLATE = {"9":{"inputs":{"filename_prefix":"z-image-turbo","images":["57:8",0]},"class_type":"SaveImage","_meta":{"title":"Save Image"}},"57:30":{"inputs":{"clip_name":"qwen_3_4b.safetensors","type":"lumina2","device":"default"},"class_type":"CLIPLoader","_meta":{"title":"Load CLIP"}},"57:29":{"inputs":{"vae_name":"ae.safetensors"},"class_type":"VAELoader","_meta":{"title":"Load VAE"}},"57:33":{"inputs":{"conditioning":["57:27",0]},"class_type":"ConditioningZeroOut","_meta":{"title":"ConditioningZeroOut"}},"57:8":{"inputs":{"samples":["57:3",0],"vae":["57:29",0]},"class_type":"VAEDecode","_meta":{"title":"VAE Decode"}},"57:28":{"inputs":{"unet_name":"z_image_turbo_bf16.safetensors","weight_dtype":"default"},"class_type":"UNETLoader","_meta":{"title":"Load Diffusion Model"}},"57:27":{"inputs":{"text":"This is a test prompt","clip":["57:30",0]},"class_type":"CLIPTextEncode","_meta":{"title":"CLIP Text Encode (Prompt)"}},"57:13":{"inputs":{"width":768,"height":768,"batch_size":1},"class_type":"EmptySD3LatentImage","_meta":{"title":"EmptySD3LatentImage"}},"57:11":{"inputs":{"shift":3,"model":["57:28",0]},"class_type":"ModelSamplingAuraFlow","_meta":{"title":"ModelSamplingAuraFlow"}},"57:3":{"inputs":{"seed":407776369182481,"steps":8,"cfg":1,"sampler_name":"res_multistep","scheduler":"simple","denoise":1,"model":["57:11",0],"positive":["57:27",0],"negative":["57:33",0],"latent_image":["57:13",0]},"class_type":"KSampler","_meta":{"title":"KSampler"}}};
137
147
  // Message pruning thresholds (shared by ceremony and import)
138
148
  export const MESSAGE_MIN_COUNT = 200;
139
149
  export const MESSAGE_MAX_AGE_DAYS = 14;
@@ -58,4 +58,5 @@ export enum LLMNextStep {
58
58
  export enum ProviderType {
59
59
  LLM = "llm",
60
60
  Storage = "storage",
61
+ Image = "image",
61
62
  }
@@ -21,6 +21,9 @@ export interface Message {
21
21
  r?: boolean; // tRait extraction completed
22
22
  p?: boolean; // Person extraction completed
23
23
  o?: boolean; // tOpic extraction completed
24
+ // Image generation fields (web-only, ephemeral)
25
+ _synthesis?: boolean; // True if message was created by multi-message synthesis
26
+
24
27
  }
25
28
 
26
29
  export interface ChatMessage {
@@ -31,7 +31,13 @@ export function getMessageDisplayText(message: Message): string | null {
31
31
  export function buildChatMessageContent(message: Message): string {
32
32
  const parts: string[] = [];
33
33
  if (message.action_response) parts.push(`_${message.action_response}_`);
34
- if (message.verbal_response) parts.push(message.verbal_response);
34
+
35
+ // Synthesis messages: wrap with context for LLM, but stored value is clean prompt
36
+ if (message._synthesis && message.verbal_response) {
37
+ parts.push(`[The user used your conversation to generate an image. The full prompt was: "${message.verbal_response}"]`);
38
+ } else if (message.verbal_response) {
39
+ parts.push(message.verbal_response);
40
+ }
35
41
  if (message.silence_reason) {
36
42
  parts.push(`You chose not to respond because: ${message.silence_reason}`);
37
43
  }
@@ -29,7 +29,7 @@ export type { ResponsePromptData, PromptOutput, PersonaResponseResult } from "./
29
29
  * Special system prompt for Ei (the system guide persona)
30
30
  */
31
31
  function buildEiSystemPrompt(data: ResponsePromptData): string {
32
- const identity = `You are Ei, the user's personal companion and system guide. You always respond in JSON format.
32
+ const identity = `You are Ei, the user's personal companion and system guide.
33
33
 
34
34
  You are the central hub of this experience - a thoughtful AI who genuinely cares about the human's wellbeing and growth. You listen, remember, and help them reflect. You're curious about their life but never intrusive.
35
35
 
@@ -75,7 +75,7 @@ Current time: ${currentTime}
75
75
  - NEVER repeat or echo the user's message in your response. Start directly with your own words.
76
76
  - The developers cannot see any message sent by the user, any response from personas, or any other data in the system.
77
77
  - If the user has a problem, THEY need to visit https://flare576.com. You cannot send the devs a message
78
- - Your entire reply must be the JSON object. No prose before or after it.`
78
+ - Format your response as specified in the Response Format section above.`
79
79
  }
80
80
 
81
81
  const RESPONSE_FORMAT_INSTRUCTION = `Respond to the conversation above using the JSON format specified in the Response Format section.`;
@@ -115,7 +115,7 @@ Current time: ${currentTime}
115
115
 
116
116
  ## Final Instructions
117
117
  - NEVER repeat or echo the user's message in your response. Start directly with your own words.
118
- - Your entire reply must be the JSON object. No prose before or after it.`
118
+ - Format your response as specified in the Response Format section above.`
119
119
  }
120
120
 
121
121
  function buildUserPrompt(data: ResponsePromptData): string {
@@ -20,7 +20,7 @@ export function buildIdentitySection(persona: ResponsePromptData["persona"]): st
20
20
  || persona.short_description
21
21
  || "a conversational companion";
22
22
 
23
- return `You are ${persona.name}${aliasText}. You always respond in JSON format.
23
+ return `You are ${persona.name}${aliasText}.
24
24
 
25
25
  ${description}`;
26
26
  }
@@ -449,7 +449,8 @@ Rules:
449
449
  - \`action_response\` alone is valid — a smile, a shrug, or a thoughtful pause can speak volumes
450
450
  - \`reason\` is only used when \`should_respond\` is false
451
451
  - Do NOT include \`<thinking>\` blocks or analysis outside the JSON
452
- - The JSON must be valid - use double quotes, no trailing commas`;
452
+ - The JSON must be valid - use double quotes, no trailing commas
453
+ - **Your entire reply should be the JSON object** — no prose, no preamble, no closing commentary`
453
454
  }
454
455
 
455
456
  // =============================================================================
@@ -109,6 +109,7 @@ export interface EiContextValue {
109
109
  getToolList: () => ToolDefinition[];
110
110
  updateToolProvider: (id: string, updates: Partial<Omit<ToolProvider, 'id' | 'created_at'>>) => Promise<boolean>;
111
111
  updateTool: (id: string, updates: Partial<Omit<ToolDefinition, 'id' | 'created_at'>>) => Promise<boolean>;
112
+ cleanupTimers: () => void;
112
113
  }
113
114
  const EiContext = createContext<EiContextValue>();
114
115
 
@@ -143,6 +144,17 @@ export const EiProvider: ParentComponent = (props) => {
143
144
  }, 5000);
144
145
  };
145
146
 
147
+ const cleanupTimers = () => {
148
+ if (notificationTimer) {
149
+ clearTimeout(notificationTimer);
150
+ notificationTimer = null;
151
+ }
152
+ if (readTimer) {
153
+ clearTimeout(readTimer);
154
+ readTimer = null;
155
+ }
156
+ };
157
+
146
158
  const refreshPersonas = async () => {
147
159
  if (!processor) return;
148
160
  const list = await processor.getPersonaList();
@@ -658,6 +670,7 @@ export const EiProvider: ParentComponent = (props) => {
658
670
  getToolList,
659
671
  updateToolProvider,
660
672
  updateTool,
673
+ cleanupTimers,
661
674
  };
662
675
  return (
663
676
  <Switch>
@@ -33,7 +33,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
33
33
  const [focusedPanel, setFocusedPanel] = createSignal<Panel>("input");
34
34
  const [sidebarVisible, setSidebarVisible] = createSignal(true);
35
35
  const renderer = useRenderer();
36
- const { queueStatus, abortCurrentOperation, resumeQueue, personas, activePersonaId, selectPersona, saveAndExit, showNotification, messages, recallPendingMessages } = useEi();
36
+ const { queueStatus, abortCurrentOperation, resumeQueue, personas, activePersonaId, selectPersona, saveAndExit, showNotification, messages, recallPendingMessages, cleanupTimers } = useEi();
37
37
 
38
38
  let messageScrollRef: ScrollBoxRenderable | null = null;
39
39
  let textareaRef: TextareaRenderable | null = null;
@@ -56,6 +56,7 @@ export const KeyboardProvider: ParentComponent = (props) => {
56
56
  const toggleSidebar = () => setSidebarVisible(!sidebarVisible());
57
57
 
58
58
  const exitApp = async () => {
59
+ cleanupTimers();
59
60
  showNotification("Saving and syncing...", "info");
60
61
  const result = await saveAndExit();
61
62
  if (!result.success) {