elenalyse 1.0.1 → 1.0.2

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/dist/chat.js CHANGED
@@ -72,19 +72,54 @@ export async function executeToolDirectly(toolName, rawArgs, toolsMap) {
72
72
  }
73
73
  }
74
74
  import { startBridgeServer, stopBridgeServer, broadcastSpeakEvent, broadcastPersonaEvent, hasConnectedClients, } from "./bridge.js";
75
- function extractVoiceSegments(responseText, debug = false) {
75
+ export function extractVoiceSegments(responseText, debug = false) {
76
76
  if (!responseText)
77
77
  return { displayText: "", voiceLines: [] };
78
78
  const voiceLines = [];
79
- // Match <voice> or <voice emotion="sad">
80
- const displayText = responseText.replace(/<voice(?:\s+emotion="([^"]+)")?>([\s\S]*?)<\/voice>/g, (_, emotion, content) => {
81
- voiceLines.push({ text: content.trim(), emotion });
82
- return debug ? chalk.magenta(`\n[VOICE${emotion ? ` (${emotion})` : ''}: ${content.trim()}]\n`) : "";
79
+ // Match <voice ...>content</voice> (case-insensitive, flexible attributes)
80
+ const voiceBlockRegex = /<voice(?:\s+[^>]*?)?>([\s\S]*?)<\/voice>/gi;
81
+ let displayText = responseText.replace(voiceBlockRegex, (match, content) => {
82
+ const emotionMatch = match.match(/emotion=["']?([^"'\s>]+)["']?/i);
83
+ const emotion = emotionMatch ? emotionMatch[1] : undefined;
84
+ const cleanContent = content.trim();
85
+ if (cleanContent) {
86
+ voiceLines.push({ text: cleanContent, emotion });
87
+ }
88
+ if (debug && cleanContent) {
89
+ return chalk.magenta(`\n[VOICE${emotion ? ` (${emotion})` : ""}: ${cleanContent}]\n`);
90
+ }
91
+ return "";
92
+ });
93
+ // Handle unclosed <voice ...> at end of string
94
+ const unclosedVoiceRegex = /<voice(?:\s+[^>]*?)?>([\s\S]*)$/gi;
95
+ displayText = displayText.replace(unclosedVoiceRegex, (match, content) => {
96
+ const emotionMatch = match.match(/emotion=["']?([^"'\s>]+)["']?/i);
97
+ const emotion = emotionMatch ? emotionMatch[1] : undefined;
98
+ const cleanContent = content.trim();
99
+ if (cleanContent) {
100
+ voiceLines.push({ text: cleanContent, emotion });
101
+ }
102
+ if (debug && cleanContent) {
103
+ return chalk.magenta(`\n[VOICE${emotion ? ` (${emotion})` : ""}: ${cleanContent}]\n`);
104
+ }
105
+ return "";
83
106
  });
84
- return { displayText: displayText.trim(), voiceLines };
107
+ // Clean up any remaining stray tags like </voice> or self-closing <voice .../>
108
+ displayText = displayText
109
+ .replace(/<\/?voice(?:\s+[^>]*?)?\/?>/gi, "")
110
+ .replace(/\n{3,}/g, "\n\n")
111
+ .trim();
112
+ return { displayText, voiceLines };
85
113
  }
86
- function stripVoiceTags(text) {
87
- return text.replace(/<voice(?:\s+emotion="([^"]+)")?>[\s\S]*?<\/voice>/g, "").trim();
114
+ export function stripVoiceTags(text) {
115
+ if (!text)
116
+ return "";
117
+ return text
118
+ .replace(/<voice(?:\s+[^>]*?)?>[\s\S]*?<\/voice>/gi, "")
119
+ .replace(/<voice(?:\s+[^>]*?)?>[\s\S]*$/gi, "")
120
+ .replace(/<\/?voice(?:\s+[^>]*?)?\/?>/gi, "")
121
+ .replace(/\n{3,}/g, "\n\n")
122
+ .trim();
88
123
  }
89
124
  function sanitizeForMemory(messages) {
90
125
  return messages.map((m) => {
@@ -295,15 +330,13 @@ export async function handleChat(message, voiceEnabled, debug = false) {
295
330
  }
296
331
  spinner.stop();
297
332
  }
298
- console.log(colorFn(`Elenalyse (${activePersona.toUpperCase()}):\n`) + renderMarkdown(finalText, activePersona));
333
+ const { displayText, voiceLines } = extractVoiceSegments(finalText, debug);
334
+ console.log(colorFn(`Elenalyse (${activePersona.toUpperCase()}):\n`) + renderMarkdown(displayText, activePersona));
299
335
  console.log(chalk.gray(`\n[Token Usage: ${totalPromptTokens} input / ${totalCompletionTokens} output]`));
300
- if (voiceEnabled) {
336
+ if (voiceEnabled && voiceLines.length > 0) {
301
337
  await startBridgeServer();
302
- let { displayText, voiceLines } = extractVoiceSegments(finalText, debug);
303
- if (voiceLines.length > 0) {
304
- const speakerId = PERSONA_SPEAKER_MAP[activePersona] || 2;
305
- await playVoiceLines(voiceLines, speakerId);
306
- }
338
+ const speakerId = PERSONA_SPEAKER_MAP[activePersona] || 2;
339
+ await playVoiceLines(voiceLines, speakerId);
307
340
  }
308
341
  break; // Successfully finished
309
342
  }
@@ -1016,12 +1049,12 @@ export async function startInteractiveChat(voiceEnabled = false, debug = false,
1016
1049
  content: stripVoiceTags(finalText),
1017
1050
  });
1018
1051
  }
1019
- if (finalText) {
1020
- console.log(colorFn(`Elenalyse (${activePersona.toUpperCase()}):\n`) + renderMarkdown(finalText, activePersona));
1052
+ const { displayText, voiceLines } = extractVoiceSegments(finalText, debug);
1053
+ if (displayText) {
1054
+ console.log(colorFn(`Elenalyse (${activePersona.toUpperCase()}):\n`) + renderMarkdown(displayText, activePersona));
1021
1055
  }
1022
1056
  // Save to memory
1023
1057
  memory[activePersona] = sanitizeForMemory(currentMessages);
1024
- let { displayText, voiceLines } = extractVoiceSegments(finalText, debug);
1025
1058
  console.log(chalk.gray(`[Token Usage: ${totalPromptTokens} input / ${totalCompletionTokens} output]`));
1026
1059
  if (debug) {
1027
1060
  console.log(chalk.gray(`[DEBUG] Memory (${memory[activePersona].length} messages):`));
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ const program = new Command();
8
8
  program
9
9
  .name("elenalyse")
10
10
  .description("Interactive CLI personal developer assistant")
11
- .version("1.0.1");
11
+ .version("1.0.2");
12
12
  program
13
13
  .command("start")
14
14
  .description("Start the interactive Elenalyse REPL chat")
package/dist/ui/banner.js CHANGED
@@ -27,7 +27,7 @@ export function renderWelcomeBanner(options) {
27
27
  const spaceCount = Math.max(0, width - 4 - rawLen);
28
28
  return text + " ".repeat(spaceCount);
29
29
  };
30
- const titleRaw = "ELENALYSE v1.0.1";
30
+ const titleRaw = "ELENALYSE v1.0.2";
31
31
  const titlePadSpaces = Math.max(0, Math.floor((width - 2 - titleRaw.length) / 2));
32
32
  const centeredTitle = " ".repeat(titlePadSpaces) +
33
33
  titleRaw +
@@ -140,10 +140,16 @@ export function renderCodeBlock(code, lang) {
140
140
  export function renderMarkdown(text, persona) {
141
141
  if (!text)
142
142
  return "";
143
+ // Defensively strip any lingering voice tags before rendering
144
+ const cleanedText = text
145
+ .replace(/<voice(?:\s+[^>]*?)?>[\s\S]*?<\/voice>/gi, "")
146
+ .replace(/<voice(?:\s+[^>]*?)?>[\s\S]*$/gi, "")
147
+ .replace(/<\/?voice(?:\s+[^>]*?)?\/?>/gi, "")
148
+ .replace(/\n{3,}/g, "\n\n");
143
149
  // Step 1: Extract fenced code blocks to prevent markdown parsing inside them
144
150
  const codeBlocks = [];
145
151
  let blockIndex = 0;
146
- let processed = text.replace(/```([a-zA-Z0-9_-]*)\r?\n([\s\S]*?)```/g, (_match, lang, code) => {
152
+ let processed = cleanedText.replace(/```([a-zA-Z0-9_-]*)\r?\n([\s\S]*?)```/g, (_match, lang, code) => {
147
153
  const ph = `\u0002CODEBLOCK_${blockIndex++}\u0002`;
148
154
  codeBlocks.push({
149
155
  placeholder: ph,
package/dist/ui/prompt.js CHANGED
@@ -52,6 +52,7 @@ export async function interactivePrompt(promptPrefix = chalk.green("You ❯ "),
52
52
  let cursor = 0;
53
53
  let selectedIndex = 0;
54
54
  let dropdownActive = false;
55
+ let userNavigated = false;
55
56
  let dropdownTitle = "Available Commands";
56
57
  let filtered = [];
57
58
  let previousDropdownHeight = 0;
@@ -62,25 +63,38 @@ export async function interactivePrompt(promptPrefix = chalk.green("You ❯ "),
62
63
  stdin.resume();
63
64
  const clearDropdown = () => {
64
65
  if (previousDropdownHeight > 0) {
65
- stdout.write("\x1b[s\x1b[1B\x1b[J\x1b[u");
66
+ stdout.write("\r\x1b[J" + promptPrefix + buffer);
67
+ const colOffset = promptLen + cursor;
68
+ if (colOffset > 0) {
69
+ stdout.write(`\r\x1b[${colOffset}C`);
70
+ }
71
+ else {
72
+ stdout.write("\r");
73
+ }
66
74
  previousDropdownHeight = 0;
67
75
  }
68
76
  };
69
77
  const render = () => {
70
- // Redraw prompt line
71
- stdout.write("\r\x1b[2K" + promptPrefix + buffer);
78
+ // 1. Clear everything from the prompt line downwards and redraw prompt line
79
+ stdout.write("\r\x1b[J" + promptPrefix + buffer);
80
+ // 2. If dropdown is active and there are items, draw them below and return cursor
72
81
  if (dropdownActive && filtered.length > 0) {
73
82
  const dropdownLines = renderDropdownLines(filtered, selectedIndex, dropdownTitle);
74
83
  previousDropdownHeight = dropdownLines.length;
75
- stdout.write("\x1b[s");
76
- stdout.write("\n" + dropdownLines.join("\n") + "\x1b[J");
77
- stdout.write("\x1b[u");
78
- stdout.write(`\r\x1b[${promptLen + cursor}C`);
84
+ stdout.write("\n" + dropdownLines.join("\n"));
85
+ // Move cursor back up to the prompt line
86
+ stdout.write(`\x1b[${dropdownLines.length}A`);
79
87
  }
80
88
  else {
81
- stdout.write("\x1b[s\x1b[1B\x1b[J\x1b[u");
82
89
  previousDropdownHeight = 0;
83
- stdout.write(`\r\x1b[${promptLen + cursor}C`);
90
+ }
91
+ // 3. Position cursor at the exact typing column on the prompt line
92
+ const colOffset = promptLen + cursor;
93
+ if (colOffset > 0) {
94
+ stdout.write(`\r\x1b[${colOffset}C`);
95
+ }
96
+ else {
97
+ stdout.write("\r");
84
98
  }
85
99
  };
86
100
  const updateDropdown = () => {
@@ -88,7 +102,11 @@ export async function interactivePrompt(promptPrefix = chalk.green("You ❯ "),
88
102
  const res = getContextualSuggestions(buffer);
89
103
  dropdownTitle = res.title;
90
104
  filtered = res.items;
91
- dropdownActive = filtered.length > 0;
105
+ const trimmed = buffer.trim();
106
+ // Auto-dismiss if input already matches a single suggestion exactly
107
+ const isExactMatch = filtered.length === 1 &&
108
+ (trimmed === filtered[0].value || trimmed.endsWith(" " + filtered[0].value));
109
+ dropdownActive = filtered.length > 0 && !isExactMatch;
92
110
  if (selectedIndex >= filtered.length) {
93
111
  selectedIndex = 0;
94
112
  }
@@ -101,16 +119,16 @@ export async function interactivePrompt(promptPrefix = chalk.green("You ❯ "),
101
119
  };
102
120
  render();
103
121
  const cleanup = () => {
104
- clearDropdown();
122
+ stdout.write("\r\x1b[J" + promptPrefix + buffer + "\n");
105
123
  stdin.removeListener("keypress", onKeypress);
106
124
  stdin.setRawMode(false);
107
- stdout.write("\n");
108
125
  };
109
126
  const onKeypress = (str, key) => {
110
127
  if (!key) {
111
128
  if (str && str.length === 1) {
112
129
  buffer = buffer.slice(0, cursor) + str + buffer.slice(cursor);
113
130
  cursor++;
131
+ userNavigated = false;
114
132
  updateDropdown();
115
133
  render();
116
134
  }
@@ -130,12 +148,14 @@ export async function interactivePrompt(promptPrefix = chalk.green("You ❯ "),
130
148
  // Escape
131
149
  if (key.name === "escape") {
132
150
  dropdownActive = false;
151
+ userNavigated = false;
133
152
  render();
134
153
  return;
135
154
  }
136
155
  // Up arrow
137
156
  if (key.name === "up") {
138
157
  if (dropdownActive && filtered.length > 0) {
158
+ userNavigated = true;
139
159
  selectedIndex = getNextIndex(selectedIndex, filtered.length, "up");
140
160
  render();
141
161
  return;
@@ -144,6 +164,7 @@ export async function interactivePrompt(promptPrefix = chalk.green("You ❯ "),
144
164
  // Down arrow
145
165
  if (key.name === "down") {
146
166
  if (dropdownActive && filtered.length > 0) {
167
+ userNavigated = true;
147
168
  selectedIndex = getNextIndex(selectedIndex, filtered.length, "down");
148
169
  render();
149
170
  return;
@@ -155,6 +176,7 @@ export async function interactivePrompt(promptPrefix = chalk.green("You ❯ "),
155
176
  const chosen = filtered[selectedIndex];
156
177
  buffer = completeSlashCommand(buffer, chosen);
157
178
  cursor = buffer.length;
179
+ userNavigated = false;
158
180
  updateDropdown();
159
181
  render();
160
182
  return;
@@ -162,12 +184,15 @@ export async function interactivePrompt(promptPrefix = chalk.green("You ❯ "),
162
184
  }
163
185
  // Enter key
164
186
  if (key.name === "return" || key.name === "enter") {
187
+ // If dropdown is active and user actively navigated to an option with arrow keys
165
188
  if (dropdownActive &&
189
+ userNavigated &&
166
190
  filtered.length > 0 &&
167
191
  !filtered.some((c) => buffer.trim().endsWith(c.value))) {
168
192
  const chosen = filtered[selectedIndex];
169
193
  buffer = completeSlashCommand(buffer, chosen);
170
194
  cursor = buffer.length;
195
+ userNavigated = false;
171
196
  updateDropdown();
172
197
  render();
173
198
  return;
@@ -181,6 +206,7 @@ export async function interactivePrompt(promptPrefix = chalk.green("You ❯ "),
181
206
  if (cursor > 0) {
182
207
  buffer = buffer.slice(0, cursor - 1) + buffer.slice(cursor);
183
208
  cursor--;
209
+ userNavigated = false;
184
210
  updateDropdown();
185
211
  render();
186
212
  }
@@ -190,6 +216,7 @@ export async function interactivePrompt(promptPrefix = chalk.green("You ❯ "),
190
216
  if (key.name === "delete") {
191
217
  if (cursor < buffer.length) {
192
218
  buffer = buffer.slice(0, cursor) + buffer.slice(cursor + 1);
219
+ userNavigated = false;
193
220
  updateDropdown();
194
221
  render();
195
222
  }
@@ -215,6 +242,7 @@ export async function interactivePrompt(promptPrefix = chalk.green("You ❯ "),
215
242
  if (str && !key.ctrl && !key.meta) {
216
243
  buffer = buffer.slice(0, cursor) + str + buffer.slice(cursor);
217
244
  cursor += str.length;
245
+ userNavigated = false;
218
246
  updateDropdown();
219
247
  render();
220
248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elenalyse",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Interactive CLI personal developer assistant",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",