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 +51 -18
- package/dist/index.js +1 -1
- package/dist/ui/banner.js +1 -1
- package/dist/ui/markdown.js +7 -1
- package/dist/ui/prompt.js +40 -12
- package/package.json +1 -1
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>
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
303
|
-
|
|
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
|
-
|
|
1020
|
-
|
|
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
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.
|
|
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 +
|
package/dist/ui/markdown.js
CHANGED
|
@@ -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 =
|
|
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("\
|
|
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
|
-
//
|
|
71
|
-
stdout.write("\r\x1b[
|
|
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("\
|
|
76
|
-
|
|
77
|
-
stdout.write(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|