aegiscode 6.0.0 → 6.1.0
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 +69 -45
- package/package.json +2 -2
- package/scripts/demo.mjs +1 -1
- package/src/app.js +100 -24
- package/src/art.js +219 -29
- package/src/commands.js +150 -50
- package/src/fuzzy.js +116 -0
- package/src/markdown.js +303 -0
- package/src/overlays.js +286 -0
- package/src/render.js +235 -120
- package/src/screen.js +196 -21
- package/src/theme.js +102 -66
package/src/commands.js
CHANGED
|
@@ -1,84 +1,123 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Slash commands — the CLI's face on the shared tool registry
|
|
4
|
+
* Slash commands — the CLI's face on the shared tool registry, using the
|
|
5
|
+
* command vocabulary of the sibling `aegiscodex-dev` client.
|
|
5
6
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* tool
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
7
|
+
* Three kinds of entry live in one table:
|
|
8
|
+
*
|
|
9
|
+
* (a) tool-backed — names a tool from `mcp/tools.js` and carries a
|
|
10
|
+
* `build(arg) -> object` that turns the typed argument into the tool's
|
|
11
|
+
* JSON. `test/cli-tools.test.mjs` asserts both directions: no command
|
|
12
|
+
* points at a tool that does not exist, and no tool in the registry is
|
|
13
|
+
* unreachable from the prompt, so a new capability added to the registry
|
|
14
|
+
* shows up here or fails the build.
|
|
15
|
+
* (b) local — no server call; handled in `app.js` by `cmd.local`.
|
|
16
|
+
* (c) unavailable — a `aegiscodex-dev` command whose capability this client
|
|
17
|
+
* genuinely does not have (it needs a local agent loop, Claude Code auth,
|
|
18
|
+
* or a repo tool). These carry an honest `why` and are *not* fakes:
|
|
19
|
+
* `parseLine` returns `{ kind: 'unavailable' }` and `app.js` prints the
|
|
20
|
+
* reason plus a working alternative.
|
|
21
|
+
*
|
|
22
|
+
* The names, aliases, categories and descriptions come from
|
|
23
|
+
* `aegiscodex-dev/src/registry.js` (`COMMANDS`, `CATEGORIES`). Where our
|
|
24
|
+
* capability differs from the reference's (e.g. `memory` here lists *cloud*
|
|
25
|
+
* memory, not the reference's local tier store) the description says what this
|
|
26
|
+
* client actually does rather than copying a sentence that would be untrue.
|
|
12
27
|
*
|
|
13
28
|
* Plain text (no leading `/`) is a prompt: it goes to the pooled brain.
|
|
14
29
|
*/
|
|
15
30
|
|
|
16
31
|
const COMMANDS = [
|
|
32
|
+
// ── AEGIS tool-backed family (aegiscodex-dev's `aegis` category) ──────────
|
|
17
33
|
{
|
|
18
|
-
name: 'ask',
|
|
19
|
-
|
|
20
|
-
|
|
34
|
+
name: 'aegis-ask',
|
|
35
|
+
aliases: ['ask'],
|
|
36
|
+
args: '<question>',
|
|
37
|
+
category: 'aegis',
|
|
38
|
+
desc: 'Ask ÆGIS pooled inference a question (auto-routed)',
|
|
21
39
|
tool: 'aegis_ask',
|
|
22
40
|
build: (arg) => ({ prompt: arg }),
|
|
23
41
|
},
|
|
24
42
|
{
|
|
25
|
-
|
|
26
|
-
|
|
43
|
+
// aegiscodex-dev splits this into `aegis-status` (local memory stats) and
|
|
44
|
+
// `status` (session status); this client has one account-status tool, so
|
|
45
|
+
// both spellings route to it — `status`/`st` as aliases.
|
|
46
|
+
name: 'aegis-status',
|
|
47
|
+
aliases: ['status', 'st'],
|
|
48
|
+
args: '',
|
|
49
|
+
category: 'aegis',
|
|
50
|
+
desc: 'Show account status — API key, plan, account and cloud memory',
|
|
27
51
|
tool: 'aegis_status',
|
|
28
52
|
build: () => ({}),
|
|
29
53
|
},
|
|
30
54
|
{
|
|
31
|
-
name: '
|
|
32
|
-
aliases: ['
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
name: 'balance',
|
|
39
|
-
aliases: ['spend'], // not `cost` — that name is the local session tally below
|
|
40
|
-
help: 'Token-bank balance and recent spend (tokens beside €).',
|
|
41
|
-
tool: 'aegis_balance',
|
|
42
|
-
build: () => ({}),
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
name: 'recall',
|
|
46
|
-
args: '<query>',
|
|
47
|
-
help: 'Search your AEGIS cloud memory.',
|
|
55
|
+
name: 'aegis-recall',
|
|
56
|
+
aliases: ['recall'],
|
|
57
|
+
args: '<topic>',
|
|
58
|
+
category: 'aegis',
|
|
59
|
+
desc: 'Recall cross-session memory about a topic',
|
|
48
60
|
tool: 'aegis_memory_search',
|
|
49
61
|
build: (arg) => ({ query: arg }),
|
|
50
62
|
},
|
|
51
63
|
{
|
|
52
|
-
name: 'remember',
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
name: 'aegis-remember',
|
|
65
|
+
aliases: ['remember'],
|
|
66
|
+
args: '<note>',
|
|
67
|
+
category: 'aegis',
|
|
68
|
+
desc: 'Save a note or decision to cross-session memory',
|
|
55
69
|
tool: 'aegis_memory_save',
|
|
56
70
|
build: (arg) => ({ content: arg }),
|
|
57
71
|
},
|
|
58
72
|
{
|
|
59
73
|
name: 'memory',
|
|
60
74
|
aliases: ['memories'],
|
|
61
|
-
|
|
75
|
+
args: '',
|
|
76
|
+
category: 'aegis',
|
|
77
|
+
desc: 'List the most recent AEGIS cloud-memory entries',
|
|
62
78
|
tool: 'aegis_memory_list',
|
|
63
79
|
build: () => ({}),
|
|
64
80
|
},
|
|
65
81
|
{
|
|
66
|
-
name
|
|
82
|
+
// No reference equivalent (the reference has no importer); the name follows
|
|
83
|
+
// the `/aegis-*` family. Keeps the `--confirm` dry-run semantics: without
|
|
84
|
+
// the flag the tool only reports what it found.
|
|
85
|
+
name: 'aegis-import',
|
|
86
|
+
aliases: ['import'],
|
|
67
87
|
args: '[--confirm]',
|
|
68
|
-
|
|
88
|
+
category: 'aegis',
|
|
89
|
+
desc: 'Import memory from other AI tools on this machine (dry run unless --confirm)',
|
|
69
90
|
tool: 'aegis_memory_import',
|
|
70
91
|
build: (arg) => ({ confirm: /--confirm\b/.test(arg) }),
|
|
71
92
|
},
|
|
72
93
|
{
|
|
94
|
+
// aegiscodex-dev's `model` switches the brain; this client pins a model id
|
|
95
|
+
// (local) and lists the pinnable ids with a tool. `models` is the list
|
|
96
|
+
// action — aegiscodex-dev's `/model list`, surfaced as its own command.
|
|
97
|
+
name: 'models',
|
|
98
|
+
aliases: ['model-list'],
|
|
99
|
+
args: '',
|
|
100
|
+
category: 'model',
|
|
101
|
+
desc: 'List the model ids you can pin with /model',
|
|
102
|
+
tool: 'aegis_list_models',
|
|
103
|
+
build: () => ({}),
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
// aegiscodex-dev has no BYOK surface; these are carried over from the
|
|
107
|
+
// registry's byok tools. `billing` below is the reference's name for the
|
|
108
|
+
// balance read the tool performs.
|
|
73
109
|
name: 'byok',
|
|
74
|
-
|
|
110
|
+
args: '',
|
|
111
|
+
category: 'auth',
|
|
112
|
+
desc: 'Show which providers have your own key configured',
|
|
75
113
|
tool: 'aegis_byok_status',
|
|
76
114
|
build: () => ({}),
|
|
77
115
|
},
|
|
78
116
|
{
|
|
79
117
|
name: 'byok-set',
|
|
80
118
|
args: '<provider>',
|
|
81
|
-
|
|
119
|
+
category: 'auth',
|
|
120
|
+
desc: 'Set YOUR provider key (prompted, never echoed, never in history)',
|
|
82
121
|
tool: 'aegis_byok_set',
|
|
83
122
|
secret: 'key',
|
|
84
123
|
build: (arg) => ({ provider: arg.trim() }),
|
|
@@ -86,14 +125,28 @@ const COMMANDS = [
|
|
|
86
125
|
{
|
|
87
126
|
name: 'byok-rm',
|
|
88
127
|
args: '<provider>',
|
|
89
|
-
|
|
128
|
+
category: 'auth',
|
|
129
|
+
desc: 'Remove a stored provider key',
|
|
90
130
|
tool: 'aegis_byok_set',
|
|
91
131
|
build: (arg) => ({ provider: arg.trim() }),
|
|
92
132
|
},
|
|
93
133
|
{
|
|
134
|
+
// aegiscodex-dev's `billing`; `balance` and `spend` stay routable for
|
|
135
|
+
// muscle memory. Not `cost` — that name is this client's local tally below.
|
|
136
|
+
name: 'billing',
|
|
137
|
+
aliases: ['balance', 'spend'],
|
|
138
|
+
args: '',
|
|
139
|
+
category: 'support',
|
|
140
|
+
desc: 'Show billing info — token-bank balance and recent spend',
|
|
141
|
+
tool: 'aegis_balance',
|
|
142
|
+
build: () => ({}),
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
// The escape hatch for any registry tool, including ones added later.
|
|
94
146
|
name: 'tool',
|
|
95
147
|
args: '<name> [json]',
|
|
96
|
-
|
|
148
|
+
category: 'aegis',
|
|
149
|
+
desc: 'Call any registry tool directly (escape hatch for new tools)',
|
|
97
150
|
generic: true,
|
|
98
151
|
build: (arg) => {
|
|
99
152
|
const sp = arg.indexOf(' ');
|
|
@@ -111,25 +164,65 @@ const COMMANDS = [
|
|
|
111
164
|
},
|
|
112
165
|
},
|
|
113
166
|
|
|
114
|
-
//
|
|
167
|
+
// ── local commands: no server call, handled by app.js ─────────────────────
|
|
115
168
|
{
|
|
116
169
|
name: 'model',
|
|
170
|
+
aliases: ['m'],
|
|
117
171
|
args: '[id]',
|
|
118
|
-
|
|
172
|
+
category: 'model',
|
|
173
|
+
desc: 'Switch AI model — pin an id (no argument shows the pin; `-` clears it)',
|
|
119
174
|
local: 'model',
|
|
120
175
|
},
|
|
121
|
-
{ name: 'stream', args: '[on|off]',
|
|
122
|
-
{ name: 'theme', args: '[dark|light]',
|
|
123
|
-
{ name: '
|
|
124
|
-
{ name: '
|
|
125
|
-
{
|
|
126
|
-
|
|
176
|
+
{ name: 'stream', args: '[on|off]', category: 'model', desc: 'Toggle streaming output', local: 'stream' },
|
|
177
|
+
{ name: 'theme', aliases: ['t'], args: '[dark|light]', category: 'model', desc: 'Change the color theme', local: 'theme' },
|
|
178
|
+
{ name: 'version', aliases: ['v'], args: '', category: 'session', desc: 'Show version', local: 'version' },
|
|
179
|
+
{ name: 'cost', args: '', category: 'data', desc: 'Show the cost of the current session', local: 'cost' },
|
|
180
|
+
{
|
|
181
|
+
name: 'tokens',
|
|
182
|
+
aliases: ['tok'],
|
|
183
|
+
args: '',
|
|
184
|
+
category: 'data',
|
|
185
|
+
desc: 'Show token usage breakdown and estimated spend',
|
|
186
|
+
local: 'tokens',
|
|
187
|
+
},
|
|
188
|
+
{ name: 'clear', aliases: ['cls'], args: '', category: 'session', desc: 'Start a new session with empty context', local: 'clear' },
|
|
189
|
+
{ name: 'help', aliases: ['?', 'h'], args: '', category: 'support', desc: 'Show help', local: 'help' },
|
|
190
|
+
{ name: 'exit', aliases: ['quit'], args: '', category: 'session', desc: 'Exit the CLI', local: 'exit' },
|
|
191
|
+
|
|
192
|
+
// ── unavailable: aegiscodex-dev vocabulary this client cannot honour ───────
|
|
193
|
+
// Not fakes — each names why it is absent and, where one exists, the nearest
|
|
194
|
+
// working command. `parseLine` surfaces these as `kind: 'unavailable'`.
|
|
195
|
+
{ name: 'login', args: '', category: 'auth', desc: 'Sign in to Claude Code', unavailable: true, why: 'sign-in uses Claude Code auth, which this client does not have — it authenticates with your AEGIS key', alt: '/byok-set' },
|
|
196
|
+
{ name: 'logout', args: '', category: 'auth', desc: 'Sign out', unavailable: true, why: 'there is no Claude Code sign-in here to end' },
|
|
197
|
+
{ name: 'doctor', args: '', category: 'support', desc: 'Run diagnostic checks on this environment', unavailable: true, why: 'the diagnostic suite belongs to aegiscodex-dev, not this client', alt: '/status' },
|
|
198
|
+
{ name: 'permissions', args: '[mode] [pattern]', category: 'model', desc: 'Set permissions for tool use', unavailable: true, why: 'tool-permission prompts require a local agent loop this client does not run' },
|
|
199
|
+
{ name: 'mcp', args: '[sub]', category: 'model', desc: 'Show MCP server configuration', unavailable: true, why: 'this CLI is itself an MCP host, so it has no MCP servers to configure', alt: '/tool' },
|
|
200
|
+
{ name: 'skills', aliases: ['sk'], args: '[name|refresh]', category: 'session', desc: 'List skills (SKILL.md in the standard skill dirs)', unavailable: true, why: 'skills run inside the agent loop, which this client does not host' },
|
|
201
|
+
{ name: 'hooks', args: '[status|list]', category: 'model', desc: 'View hooks configuration status and configured hook list', unavailable: true, why: 'hooks are a local agent-loop feature this client does not run' },
|
|
202
|
+
{ name: 'agents', aliases: ['sessions'], args: '[role] [task]', category: 'session', desc: 'Show the agents panel, or run a sub-agent role preset — /agents <role> <task>', unavailable: true, why: 'sub-agents need a local agent loop this client does not host' },
|
|
203
|
+
{ name: 'resume', args: '', category: 'session', desc: 'Switch to a previous session', unavailable: true, why: 'this client keeps no session history to resume' },
|
|
204
|
+
{ name: 'rewind', args: '[N]', category: 'session', desc: 'Revert the conversation to a checkpoint', unavailable: true, why: 'checkpoints belong to a session history this client does not keep' },
|
|
205
|
+
{ name: 'compact', args: '', category: 'session', desc: 'Compact the conversation history', unavailable: true, why: 'there is no local transcript to compact' },
|
|
206
|
+
{ name: 'init', args: '[file]', category: 'workspace', desc: 'Create a CLAUDE.md file in the project', unavailable: true, why: 'project scaffolding is a workspace feature this client does not have', alt: '/aegis-remember' },
|
|
207
|
+
{ name: 'export', args: '[markdown|json] [file|clipboard]', category: 'data', desc: 'Export the current conversation to a file or clipboard', unavailable: true, why: 'this client keeps no transcript to export' },
|
|
208
|
+
{ name: 'vim', args: '[on|off]', category: 'model', desc: 'Toggle vim keymap', unavailable: true, why: 'input is your terminal readline, which has no vim keymap' },
|
|
209
|
+
{ name: 'yolo', args: '[on|off]', category: 'model', desc: 'Toggle YOLO mode — auto-approve all tool executions', unavailable: true, why: 'there is no tool-approval prompt here to auto-approve' },
|
|
210
|
+
{ name: 'confirm', aliases: ['confirmations'], args: '[on|off]', category: 'model', desc: 'Toggle tool-call confirmation prompts', unavailable: true, why: 'there is no tool-approval prompt here to toggle' },
|
|
127
211
|
];
|
|
128
212
|
|
|
213
|
+
// ── Index + the one invariant a table like this must hold ────────────────────
|
|
214
|
+
// A name or alias registered twice would silently shadow the earlier entry, so
|
|
215
|
+
// the collision is a module-load error rather than a runtime surprise. (The
|
|
216
|
+
// reference keeps `builtinNames()` for exactly this; its test asserts the same.)
|
|
217
|
+
|
|
129
218
|
const BY_NAME = new Map();
|
|
130
219
|
for (const c of COMMANDS) {
|
|
131
|
-
|
|
132
|
-
|
|
220
|
+
for (const n of [c.name, ...(c.aliases || [])]) {
|
|
221
|
+
if (BY_NAME.has(n)) {
|
|
222
|
+
throw new Error(`aegiscode: command name or alias /${n} is registered twice`);
|
|
223
|
+
}
|
|
224
|
+
BY_NAME.set(n, c);
|
|
225
|
+
}
|
|
133
226
|
}
|
|
134
227
|
|
|
135
228
|
function findCommand(name) {
|
|
@@ -138,7 +231,11 @@ function findCommand(name) {
|
|
|
138
231
|
|
|
139
232
|
/**
|
|
140
233
|
* Classify one input line.
|
|
141
|
-
* @returns {{kind:'empty'}
|
|
234
|
+
* @returns {{kind:'empty'}
|
|
235
|
+
* |{kind:'command',command:object,arg:string}
|
|
236
|
+
* |{kind:'unavailable',command:object,arg:string}
|
|
237
|
+
* |{kind:'unknown',name:string,text:string}
|
|
238
|
+
* |{kind:'prompt',text:string}}
|
|
142
239
|
*/
|
|
143
240
|
function parseLine(line) {
|
|
144
241
|
const raw = String(line == null ? '' : line);
|
|
@@ -152,6 +249,9 @@ function parseLine(line) {
|
|
|
152
249
|
if (!command) {
|
|
153
250
|
return { kind: 'unknown', name, text: trimmed };
|
|
154
251
|
}
|
|
252
|
+
if (command.unavailable) {
|
|
253
|
+
return { kind: 'unavailable', command, arg };
|
|
254
|
+
}
|
|
155
255
|
return { kind: 'command', command, arg };
|
|
156
256
|
}
|
|
157
257
|
|
package/src/fuzzy.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Fuzzy command matching for the "/" palette (and anything else that needs it).
|
|
4
|
+
//
|
|
5
|
+
// The real Claude Code palette ranks matches by relevance rather than plain
|
|
6
|
+
// prefix: an exact match beats a prefix, a prefix beats a substring, and a
|
|
7
|
+
// substring beats characters scattered through the name. Ties fall back to
|
|
8
|
+
// the source (registry) order so the palette stays stable for empty queries.
|
|
9
|
+
//
|
|
10
|
+
// Scoring tiers (deliberately spaced so no position/length detail in a lower
|
|
11
|
+
// tier can overtake a higher tier):
|
|
12
|
+
// exact match 1000
|
|
13
|
+
// prefix match 900
|
|
14
|
+
// substring 700 − position (earlier substring wins)
|
|
15
|
+
// scattered 50 + bonuses (consecutive runs, name-start hits)
|
|
16
|
+
//
|
|
17
|
+
// Ported verbatim from aegiscodex-dev/src/fuzzy.js (ESM → CommonJS). The tier
|
|
18
|
+
// numbers are a contract: cli-fuzzy.test.mjs asserts the ordering they encode.
|
|
19
|
+
|
|
20
|
+
function fuzzyScore(query, name) {
|
|
21
|
+
const q = String(query).toLowerCase();
|
|
22
|
+
const n = String(name).toLowerCase();
|
|
23
|
+
if (!q) return { matched: true, score: 0 };
|
|
24
|
+
if (n === q) return { matched: true, score: 1000 };
|
|
25
|
+
if (n.startsWith(q)) return { matched: true, score: 900 };
|
|
26
|
+
const at = n.indexOf(q);
|
|
27
|
+
if (at !== -1) return { matched: true, score: 700 - at };
|
|
28
|
+
|
|
29
|
+
// Scattered subsequence: every query char must appear in order.
|
|
30
|
+
let score = 50;
|
|
31
|
+
let prev = -2;
|
|
32
|
+
let last = -1;
|
|
33
|
+
for (const ch of q) {
|
|
34
|
+
const k = n.indexOf(ch, last + 1);
|
|
35
|
+
if (k === -1) return { matched: false, score: 0 };
|
|
36
|
+
if (k === prev + 1) score += 15; // consecutive run — strong signal
|
|
37
|
+
else score += 5;
|
|
38
|
+
if (k === 0) score += 10; // starts at the name — weak signal
|
|
39
|
+
prev = k;
|
|
40
|
+
last = k;
|
|
41
|
+
}
|
|
42
|
+
return { matched: true, score };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The indices (in the original `name`) of the characters that matched
|
|
47
|
+
* `query`, mirroring fuzzyScore's tiers — exact = every index, prefix =
|
|
48
|
+
* leading run, substring = that run, scattered = the scattered picks.
|
|
49
|
+
* Returns [] for an empty query and null when nothing matches.
|
|
50
|
+
* Drives the palette's bold-matched-chars rendering.
|
|
51
|
+
*/
|
|
52
|
+
function fuzzyMatchPositions(query, name) {
|
|
53
|
+
const q = String(query).toLowerCase();
|
|
54
|
+
const n = String(name).toLowerCase();
|
|
55
|
+
if (!q) return [];
|
|
56
|
+
if (n === q) return Array.from({ length: q.length }, (_, i) => i);
|
|
57
|
+
if (n.startsWith(q)) return Array.from({ length: q.length }, (_, i) => i);
|
|
58
|
+
const at = n.indexOf(q);
|
|
59
|
+
if (at !== -1) return Array.from({ length: q.length }, (_, i) => at + i);
|
|
60
|
+
const pos = [];
|
|
61
|
+
let last = -1;
|
|
62
|
+
for (const ch of q) {
|
|
63
|
+
const k = n.indexOf(ch, last + 1);
|
|
64
|
+
if (k === -1) return null;
|
|
65
|
+
pos.push(k);
|
|
66
|
+
last = k;
|
|
67
|
+
}
|
|
68
|
+
return pos;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Rank `items` by how well `nameOf(item)` matches `query`.
|
|
73
|
+
* Empty query returns the items unchanged (preserves registry/palette order).
|
|
74
|
+
* Non-matches are dropped; matches sort by score desc, then source order.
|
|
75
|
+
*/
|
|
76
|
+
function fuzzyRank(query, items, nameOf) {
|
|
77
|
+
const q = String(query || '');
|
|
78
|
+
if (!q) return items;
|
|
79
|
+
return items
|
|
80
|
+
.map((item, idx) => ({ item, idx, m: fuzzyScore(q, nameOf(item)) }))
|
|
81
|
+
.filter((x) => x.m.matched)
|
|
82
|
+
.sort((a, b) => b.m.score - a.m.score || a.idx - b.idx)
|
|
83
|
+
.map((x) => x.item);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Alias-aware ranking. The reference palette searches the command name and its
|
|
88
|
+
* aliases (name weight 3, aliases weight 2.5 — Fuse.js in the reference; here
|
|
89
|
+
* the best fuzzy score across both wins). An item matches when the query scores
|
|
90
|
+
* against at least one of its names; ties keep source order so the palette
|
|
91
|
+
* stays stable.
|
|
92
|
+
*/
|
|
93
|
+
function fuzzyRankWithAliases(query, items, nameOf, aliasesOf) {
|
|
94
|
+
const q = String(query || '');
|
|
95
|
+
if (!q) return items;
|
|
96
|
+
return items
|
|
97
|
+
.map((item, idx) => {
|
|
98
|
+
const names = [nameOf(item), ...((aliasesOf && aliasesOf(item)) || [])];
|
|
99
|
+
let best = null;
|
|
100
|
+
for (const n of names) {
|
|
101
|
+
const m = fuzzyScore(q, n);
|
|
102
|
+
if (m.matched && (!best || m.score > best.score)) best = m;
|
|
103
|
+
}
|
|
104
|
+
return { item, idx, m: best };
|
|
105
|
+
})
|
|
106
|
+
.filter((x) => x.m)
|
|
107
|
+
.sort((a, b) => b.m.score - a.m.score || a.idx - b.idx)
|
|
108
|
+
.map((x) => x.item);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = {
|
|
112
|
+
fuzzyScore,
|
|
113
|
+
fuzzyMatchPositions,
|
|
114
|
+
fuzzyRank,
|
|
115
|
+
fuzzyRankWithAliases,
|
|
116
|
+
};
|