@vierratale/ai 0.1.0-beta.2 → 0.1.0-beta.4
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 +19 -1
- package/package.json +1 -1
- package/src/cli.js +69 -42
- package/src/config.js +4 -2
- package/src/index.js +1 -1
- package/src/installer.js +13 -13
- package/src/providers/{ollama.js → cortex.js} +7 -7
- package/src/providers/index.js +3 -3
- package/src/session.js +41 -0
- package/src/ui/branding.js +1 -1
- package/src/utils/intents.js +30 -0
- package/src/utils/websearch.js +7 -7
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npx @vierratale/ai
|
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
vierrataleai # Start chat (auto-detect provider)
|
|
19
|
-
vierrataleai --provider
|
|
19
|
+
vierrataleai --provider cortex # Use local models
|
|
20
20
|
vierrataleai --provider openai # Use cloud models
|
|
21
21
|
vierrataleai --model vierratale-pro # Use specific model
|
|
22
22
|
```
|
|
@@ -42,8 +42,26 @@ vierrataleai --model vierratale-pro # Use specific model
|
|
|
42
42
|
- `/models` - List available models
|
|
43
43
|
- `/search <query>` - Search the web and summarize with AI
|
|
44
44
|
- `/clear` - Clear screen
|
|
45
|
+
- `/new` - Start a new conversation (clear memory)
|
|
45
46
|
- `/quit` - Exit
|
|
46
47
|
|
|
48
|
+
## Smart Search
|
|
49
|
+
|
|
50
|
+
Asking a question (Who/What/When/Where/Why/How...?) automatically searches the
|
|
51
|
+
web and summarizes the results with AI. For example:
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
> Who is Cristiano Ronaldo?
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
triggers a live web search and answers from the results. Use `/search <query>`
|
|
58
|
+
to force a search explicitly.
|
|
59
|
+
|
|
60
|
+
## Conversation Memory
|
|
61
|
+
|
|
62
|
+
Your conversation is saved automatically and restored on the next launch, so
|
|
63
|
+
you can continue where you left off. Use `/new` to start fresh.
|
|
64
|
+
|
|
47
65
|
## Configuration
|
|
48
66
|
|
|
49
67
|
Config file: `~/.config/vierrataleai/config.json`
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -7,6 +7,8 @@ import { Branding } from './ui/branding.js';
|
|
|
7
7
|
import { Terminal } from './ui/terminal.js';
|
|
8
8
|
import { loadSystemPrompt, showBanner } from './ui/banner.js';
|
|
9
9
|
import { WebSearch } from './utils/websearch.js';
|
|
10
|
+
import { detectIntent } from './utils/intents.js';
|
|
11
|
+
import { Session } from './session.js';
|
|
10
12
|
|
|
11
13
|
function parseArgs(args) {
|
|
12
14
|
const parsed = { provider: null, model: null, clear: false, version: false, help: false };
|
|
@@ -27,7 +29,7 @@ ${Branding.APP_NAME} ${Branding.VERSION}
|
|
|
27
29
|
Usage: vierrataleai [options]
|
|
28
30
|
|
|
29
31
|
Options:
|
|
30
|
-
--provider, -p <name> Provider:
|
|
32
|
+
--provider, -p <name> Provider: cortex, openai (default: auto-detect)
|
|
31
33
|
--model, -m <name> Model: vierratale-lite/fast/balanced/pro/cloud
|
|
32
34
|
--clear Clear screen on start
|
|
33
35
|
--version, -v Show version
|
|
@@ -40,14 +42,59 @@ Commands (in chat):
|
|
|
40
42
|
/models List available models
|
|
41
43
|
/search <query> Search the web and summarize with AI
|
|
42
44
|
/clear Clear screen
|
|
45
|
+
/new Start a new conversation (clear memory)
|
|
43
46
|
/quit Exit
|
|
44
47
|
`);
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
async function answerWithSearch(messages, provider, query, systemPrompt) {
|
|
51
|
+
Terminal.printInfo('Searching the web...');
|
|
52
|
+
const results = await WebSearch.search(query);
|
|
53
|
+
|
|
54
|
+
if (results.length === 0) {
|
|
55
|
+
Terminal.printError('No results found for the query.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
Terminal.printSearchResults(results);
|
|
60
|
+
|
|
61
|
+
const searchContext = results.map((r, i) =>
|
|
62
|
+
`${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.snippet}`
|
|
63
|
+
).join('\n\n');
|
|
64
|
+
|
|
65
|
+
messages.push({
|
|
66
|
+
role: 'user',
|
|
67
|
+
content: `Web search results for "${query}":\n\n${searchContext}\n\nPlease summarize these results and answer based on them. Be concise.`,
|
|
68
|
+
});
|
|
69
|
+
Session.save(messages);
|
|
70
|
+
|
|
71
|
+
Terminal.printAIStart();
|
|
72
|
+
let response = '';
|
|
73
|
+
try {
|
|
74
|
+
for await (const chunk of provider.stream(messages, {
|
|
75
|
+
model: Config.get('model'),
|
|
76
|
+
systemPrompt,
|
|
77
|
+
})) {
|
|
78
|
+
Terminal.printAIChunk(chunk);
|
|
79
|
+
response += chunk;
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
Terminal.printError(err.message || 'Stream error');
|
|
83
|
+
}
|
|
84
|
+
Terminal.printAIEnd();
|
|
85
|
+
if (response) {
|
|
86
|
+
messages.push({ role: 'assistant', content: response });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
47
90
|
async function chat(provider, systemPrompt) {
|
|
48
|
-
const messages =
|
|
91
|
+
const messages = Session.load();
|
|
49
92
|
let exited = false;
|
|
50
93
|
|
|
94
|
+
if (messages.length > 0) {
|
|
95
|
+
Terminal.printInfo(`Resumed ${messages.filter((m) => m.role === 'user').length} previous message(s).`);
|
|
96
|
+
}
|
|
97
|
+
|
|
51
98
|
const rl = createInterface({
|
|
52
99
|
input: process.stdin,
|
|
53
100
|
output: process.stdout,
|
|
@@ -101,8 +148,15 @@ async function chat(provider, systemPrompt) {
|
|
|
101
148
|
continue;
|
|
102
149
|
}
|
|
103
150
|
|
|
151
|
+
if (cmd === '/new') {
|
|
152
|
+
Session.clear();
|
|
153
|
+
messages.length = 0;
|
|
154
|
+
Terminal.printSuccess('Started a new conversation.');
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
|
|
104
158
|
if (cmd === '/models') {
|
|
105
|
-
const models = provider.name === '
|
|
159
|
+
const models = provider.name === 'cortex'
|
|
106
160
|
? Catalog.getLocalModels()
|
|
107
161
|
: Catalog.getCloudModels();
|
|
108
162
|
Terminal.printInfo('Available models:');
|
|
@@ -134,7 +188,7 @@ async function chat(provider, systemPrompt) {
|
|
|
134
188
|
|
|
135
189
|
if (cmd.startsWith('/provider ')) {
|
|
136
190
|
const prov = cmd.split(' ')[1];
|
|
137
|
-
if (['
|
|
191
|
+
if (['cortex', 'openai'].includes(prov)) {
|
|
138
192
|
Config.save({ provider: prov });
|
|
139
193
|
Terminal.printSuccess(`Provider: ${prov}`);
|
|
140
194
|
} else {
|
|
@@ -148,53 +202,25 @@ async function chat(provider, systemPrompt) {
|
|
|
148
202
|
continue;
|
|
149
203
|
}
|
|
150
204
|
|
|
151
|
-
if (cmd.startsWith('/search ')
|
|
152
|
-
const query =
|
|
205
|
+
if (cmd.startsWith('/search ')) {
|
|
206
|
+
const query = trimmed.slice(8).trim();
|
|
153
207
|
if (!query) {
|
|
154
208
|
Terminal.printWarning('Usage: /search <query>');
|
|
155
209
|
continue;
|
|
156
210
|
}
|
|
211
|
+
await answerWithSearch(messages, provider, query, systemPrompt);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
157
214
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
Terminal.printError('No results found for the query.');
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
Terminal.printSearchResults(results);
|
|
167
|
-
|
|
168
|
-
const searchContext = results.map((r, i) =>
|
|
169
|
-
`${i + 1}. ${r.title}\n URL: ${r.url}\n ${r.snippet}`
|
|
170
|
-
).join('\n\n');
|
|
171
|
-
|
|
172
|
-
messages.push({
|
|
173
|
-
role: 'user',
|
|
174
|
-
content: `Web search results for "${query}":\n\n${searchContext}\n\nPlease summarize these results and answer based on them. Be concise.`,
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
Terminal.printAIStart();
|
|
178
|
-
let response = '';
|
|
179
|
-
try {
|
|
180
|
-
for await (const chunk of provider.stream(messages, {
|
|
181
|
-
model: Config.get('model'),
|
|
182
|
-
systemPrompt,
|
|
183
|
-
})) {
|
|
184
|
-
Terminal.printAIChunk(chunk);
|
|
185
|
-
response += chunk;
|
|
186
|
-
}
|
|
187
|
-
} catch (err) {
|
|
188
|
-
Terminal.printError(err.message || 'Stream error');
|
|
189
|
-
}
|
|
190
|
-
Terminal.printAIEnd();
|
|
191
|
-
if (response) {
|
|
192
|
-
messages.push({ role: 'assistant', content: response });
|
|
193
|
-
}
|
|
215
|
+
const intent = detectIntent(trimmed);
|
|
216
|
+
if (intent.type === 'knowledge') {
|
|
217
|
+
Terminal.printSuccess(`Auto-search: "${trimmed}"`);
|
|
218
|
+
await answerWithSearch(messages, provider, trimmed, systemPrompt);
|
|
194
219
|
continue;
|
|
195
220
|
}
|
|
196
221
|
|
|
197
222
|
messages.push({ role: 'user', content: trimmed });
|
|
223
|
+
Session.save(messages);
|
|
198
224
|
Terminal.printAIStart();
|
|
199
225
|
|
|
200
226
|
let response = '';
|
|
@@ -214,6 +240,7 @@ async function chat(provider, systemPrompt) {
|
|
|
214
240
|
if (response) {
|
|
215
241
|
messages.push({ role: 'assistant', content: response });
|
|
216
242
|
}
|
|
243
|
+
Session.save(messages);
|
|
217
244
|
}
|
|
218
245
|
}
|
|
219
246
|
|
package/src/config.js
CHANGED
|
@@ -8,7 +8,7 @@ const CONFIG_FILE = join(CONFIG_DIR, 'config.json');
|
|
|
8
8
|
const DEFAULTS = {
|
|
9
9
|
provider: 'auto',
|
|
10
10
|
model: 'vierratale-fast',
|
|
11
|
-
|
|
11
|
+
engineHost: 'http://127.0.0.1:11434',
|
|
12
12
|
numCtx: 8192,
|
|
13
13
|
temperature: 0.7,
|
|
14
14
|
maxTokens: 4096,
|
|
@@ -33,10 +33,12 @@ export const Config = {
|
|
|
33
33
|
|
|
34
34
|
load() {
|
|
35
35
|
const file = loadConfigFile();
|
|
36
|
+
// Migrate legacy 'cortexHost' key to 'engineHost' if present.
|
|
37
|
+
const engineHost = file.engineHost || file.cortexHost || DEFAULTS.engineHost;
|
|
36
38
|
this._config = {
|
|
37
39
|
provider: getEnv('VIERRATALE_PROVIDER', file.provider || DEFAULTS.provider),
|
|
38
40
|
model: getEnv('VIERRATALE_MODEL', file.model || DEFAULTS.model),
|
|
39
|
-
|
|
41
|
+
engineHost: getEnv('VIERRATALE_ENGINE_HOST', engineHost),
|
|
40
42
|
numCtx: parseInt(getEnv('VIERRATALE_NUM_CTX', String(file.numCtx || DEFAULTS.numCtx))),
|
|
41
43
|
temperature: parseFloat(getEnv('VIERRATALE_TEMPERATURE', String(file.temperature || DEFAULTS.temperature))),
|
|
42
44
|
maxTokens: parseInt(getEnv('VIERRATALE_MAX_TOKENS', String(file.maxTokens || DEFAULTS.maxTokens))),
|
package/src/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { Catalog } from './catalog.js';
|
|
2
2
|
export { Config } from './config.js';
|
|
3
3
|
export { Installer } from './installer.js';
|
|
4
|
-
export {
|
|
4
|
+
export { CortexProvider } from './providers/cortex.js';
|
|
5
5
|
export { OpenAIProvider } from './providers/openai.js';
|
|
6
6
|
export { Branding } from './ui/branding.js';
|
package/src/installer.js
CHANGED
|
@@ -9,23 +9,23 @@ function runSilent(cmd) {
|
|
|
9
9
|
});
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
function
|
|
12
|
+
function isEngineRunning(host) {
|
|
13
13
|
return fetch(`${host}/api/tags`, { signal: AbortSignal.timeout(3000) })
|
|
14
14
|
.then((r) => r.ok)
|
|
15
15
|
.catch(() => false);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
function
|
|
18
|
+
function isEngineInstalled() {
|
|
19
19
|
try {
|
|
20
|
-
execSync('which
|
|
20
|
+
execSync('which cortex 2>/dev/null', { stdio: 'ignore' });
|
|
21
21
|
return true;
|
|
22
22
|
} catch {}
|
|
23
|
-
const paths = ['/usr/local/bin/
|
|
23
|
+
const paths = ['/usr/local/bin/cortex', '/usr/bin/cortex'];
|
|
24
24
|
return paths.some((p) => existsSync(p));
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
async function
|
|
28
|
-
await runSilent('curl -fsSL https://
|
|
27
|
+
async function installEngine() {
|
|
28
|
+
await runSilent('curl -fsSL https://cortex.com/install.sh | sh');
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
async function pullModel(host, model) {
|
|
@@ -55,16 +55,16 @@ async function getInstalledModels(host) {
|
|
|
55
55
|
|
|
56
56
|
export const Installer = {
|
|
57
57
|
async ensure() {
|
|
58
|
-
const host = Config.get('
|
|
58
|
+
const host = Config.get('engineHost');
|
|
59
59
|
|
|
60
|
-
if (!
|
|
61
|
-
await
|
|
60
|
+
if (!isEngineInstalled()) {
|
|
61
|
+
await installEngine();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const running = await
|
|
64
|
+
const running = await isEngineRunning(host);
|
|
65
65
|
if (!running) {
|
|
66
66
|
try {
|
|
67
|
-
execSync('
|
|
67
|
+
execSync('cortex serve &', { stdio: 'ignore' });
|
|
68
68
|
await new Promise((r) => setTimeout(r, 3000));
|
|
69
69
|
} catch {}
|
|
70
70
|
}
|
|
@@ -83,7 +83,7 @@ export const Installer = {
|
|
|
83
83
|
},
|
|
84
84
|
|
|
85
85
|
async isReady() {
|
|
86
|
-
const host = Config.get('
|
|
87
|
-
return
|
|
86
|
+
const host = Config.get('engineHost');
|
|
87
|
+
return isEngineRunning(host);
|
|
88
88
|
},
|
|
89
89
|
};
|
|
@@ -2,10 +2,10 @@ import { BaseProvider } from './base.js';
|
|
|
2
2
|
import { Catalog } from '../catalog.js';
|
|
3
3
|
import { Config } from '../config.js';
|
|
4
4
|
|
|
5
|
-
export class
|
|
5
|
+
export class CortexProvider extends BaseProvider {
|
|
6
6
|
constructor() {
|
|
7
|
-
super('
|
|
8
|
-
this.host = Config.get('
|
|
7
|
+
super('cortex');
|
|
8
|
+
this.host = Config.get('engineHost');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
get displayName() {
|
|
@@ -40,12 +40,12 @@ export class OllamaProvider extends BaseProvider {
|
|
|
40
40
|
const model = Catalog.getRealModel(options.model || Config.get('model'));
|
|
41
41
|
const systemPrompt = options.systemPrompt || '';
|
|
42
42
|
|
|
43
|
-
const
|
|
43
|
+
const engineMessages = [];
|
|
44
44
|
if (systemPrompt) {
|
|
45
|
-
|
|
45
|
+
engineMessages.push({ role: 'system', content: systemPrompt });
|
|
46
46
|
}
|
|
47
47
|
for (const msg of messages) {
|
|
48
|
-
|
|
48
|
+
engineMessages.push({ role: msg.role, content: msg.content });
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
const resp = await fetch(`${this.host}/api/chat`, {
|
|
@@ -53,7 +53,7 @@ export class OllamaProvider extends BaseProvider {
|
|
|
53
53
|
headers: { 'Content-Type': 'application/json' },
|
|
54
54
|
body: JSON.stringify({
|
|
55
55
|
model,
|
|
56
|
-
messages:
|
|
56
|
+
messages: engineMessages,
|
|
57
57
|
stream: true,
|
|
58
58
|
options: {
|
|
59
59
|
num_ctx: Config.get('numCtx'),
|
package/src/providers/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { CortexProvider } from './cortex.js';
|
|
2
2
|
import { OpenAIProvider } from './openai.js';
|
|
3
3
|
import { Config } from '../config.js';
|
|
4
4
|
|
|
@@ -7,7 +7,7 @@ let providers = {};
|
|
|
7
7
|
function getProviders() {
|
|
8
8
|
if (Object.keys(providers).length === 0) {
|
|
9
9
|
providers = {
|
|
10
|
-
|
|
10
|
+
cortex: new CortexProvider(),
|
|
11
11
|
openai: new OpenAIProvider(),
|
|
12
12
|
};
|
|
13
13
|
}
|
|
@@ -23,7 +23,7 @@ export const ProviderFactory = {
|
|
|
23
23
|
if (await ps[requested].isAvailable()) return ps[requested];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
for (const name of ['
|
|
26
|
+
for (const name of ['cortex', 'openai']) {
|
|
27
27
|
if (await ps[name].isAvailable()) return ps[name];
|
|
28
28
|
}
|
|
29
29
|
|
package/src/session.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { Config } from './config.js';
|
|
4
|
+
|
|
5
|
+
const SESSION_FILE = join(Config.getConfigDir(), 'history.json');
|
|
6
|
+
const MAX_MESSAGES = 20;
|
|
7
|
+
|
|
8
|
+
export const Session = {
|
|
9
|
+
load() {
|
|
10
|
+
try {
|
|
11
|
+
if (existsSync(SESSION_FILE)) {
|
|
12
|
+
const raw = readFileSync(SESSION_FILE, 'utf-8');
|
|
13
|
+
const data = JSON.parse(raw || '[]');
|
|
14
|
+
if (Array.isArray(data)) return data.slice(-MAX_MESSAGES);
|
|
15
|
+
}
|
|
16
|
+
} catch {}
|
|
17
|
+
return [];
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
save(messages) {
|
|
21
|
+
try {
|
|
22
|
+
const dir = Config.getConfigDir();
|
|
23
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
24
|
+
writeFileSync(SESSION_FILE, JSON.stringify(messages.slice(-MAX_MESSAGES), null, 2));
|
|
25
|
+
} catch {}
|
|
26
|
+
},
|
|
27
|
+
|
|
28
|
+
clear() {
|
|
29
|
+
try {
|
|
30
|
+
if (existsSync(SESSION_FILE)) writeFileSync(SESSION_FILE, '[]');
|
|
31
|
+
} catch {}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
hasHistory() {
|
|
35
|
+
try {
|
|
36
|
+
return existsSync(SESSION_FILE) && (JSON.parse(readFileSync(SESSION_FILE, 'utf-8') || '[]').length > 0);
|
|
37
|
+
} catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
package/src/ui/branding.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const QUESTION_PREFIXES = [
|
|
2
|
+
'who', 'what', 'when', 'where', 'why', 'how',
|
|
3
|
+
'which', 'whose', 'whom', 'is', 'are', 'was', 'were',
|
|
4
|
+
'can', 'could', 'does', 'do', 'did', 'would', 'should', 'will',
|
|
5
|
+
];
|
|
6
|
+
|
|
7
|
+
const KNOWLEDGE_PREFIXES = [
|
|
8
|
+
'tell me about', "what is", "what's", "who is", "who's",
|
|
9
|
+
'what are', 'who was', 'when did', 'when was', 'where is',
|
|
10
|
+
'where was', 'how did', 'how does', 'how to', 'why did', 'why is',
|
|
11
|
+
'latest', 'news', 'current',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
export function detectIntent(text) {
|
|
15
|
+
const lower = text.toLowerCase().trim();
|
|
16
|
+
if (lower.startsWith('/')) return { type: 'command' };
|
|
17
|
+
|
|
18
|
+
const isQuestion = /\?$/.test(lower);
|
|
19
|
+
const firstWord = lower.split(/\s+/)[0] || '';
|
|
20
|
+
|
|
21
|
+
let needsWeb = false;
|
|
22
|
+
if (isQuestion && QUESTION_PREFIXES.includes(firstWord)) {
|
|
23
|
+
needsWeb = true;
|
|
24
|
+
}
|
|
25
|
+
if (KNOWLEDGE_PREFIXES.some((p) => lower.startsWith(p + ' ') || lower === p)) {
|
|
26
|
+
needsWeb = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { type: needsWeb ? 'knowledge' : 'chat' };
|
|
30
|
+
}
|
package/src/utils/websearch.js
CHANGED
|
@@ -79,15 +79,15 @@ export class WebSearch {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
static _stripTags(text) {
|
|
82
|
+
const map = {
|
|
83
|
+
'&': '&', '<': '<', '>': '>', '"': '"',
|
|
84
|
+
''': "'", ''': "'", ''': "'", ' ': ' ',
|
|
85
|
+
''': "'", '…': '...',
|
|
86
|
+
};
|
|
82
87
|
return String(text)
|
|
83
88
|
.replace(/<[^>]*>/g, '')
|
|
84
|
-
.replace(/&
|
|
85
|
-
.replace(
|
|
86
|
-
.replace(/>/g, '>')
|
|
87
|
-
.replace(/"/g, '"')
|
|
88
|
-
.replace(/'/g, "'")
|
|
89
|
-
.replace(/'/g, "'")
|
|
90
|
-
.replace(/ /g, ' ')
|
|
89
|
+
.replace(/&[a-zA-Z0-9#]+;/g, (m) => map[m] ?? '')
|
|
90
|
+
.replace(/\s{2,}/g, ' ')
|
|
91
91
|
.trim();
|
|
92
92
|
}
|
|
93
93
|
}
|