@cccarv82/freya 1.0.44 → 1.0.46

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.
@@ -0,0 +1,61 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ function safeReadJson(filePath) {
5
+ let content;
6
+ try {
7
+ content = fs.readFileSync(filePath, 'utf8');
8
+ } catch (error) {
9
+ return { ok: false, error: { type: 'read', message: error.message, cause: error } };
10
+ }
11
+
12
+ try {
13
+ const json = JSON.parse(content);
14
+ return { ok: true, json };
15
+ } catch (error) {
16
+ return { ok: false, error: { type: 'parse', message: error.message, cause: error } };
17
+ }
18
+ }
19
+
20
+ function timestampForFilename(date = new Date()) {
21
+ return date.toISOString().replace(/[:.]/g, '-');
22
+ }
23
+
24
+ function quarantineCorruptedFile(filePath, reason) {
25
+ const dir = path.dirname(filePath);
26
+ const corruptedDir = path.join(dir, '_corrupted');
27
+ if (!fs.existsSync(corruptedDir)) {
28
+ fs.mkdirSync(corruptedDir, { recursive: true });
29
+ }
30
+
31
+ const parsed = path.parse(filePath);
32
+ const timestamp = timestampForFilename();
33
+ const quarantinedName = `${parsed.name}-${timestamp}${parsed.ext}`;
34
+ const quarantinedPath = path.join(corruptedDir, quarantinedName);
35
+
36
+ try {
37
+ fs.renameSync(filePath, quarantinedPath);
38
+ } catch (error) {
39
+ fs.copyFileSync(filePath, quarantinedPath);
40
+ fs.unlinkSync(filePath);
41
+ }
42
+
43
+ const notePath = `${quarantinedPath}.md`;
44
+ const note = [
45
+ '# Quarantined JSON',
46
+ '',
47
+ `- Original: ${filePath}`,
48
+ `- Quarantined: ${quarantinedPath}`,
49
+ `- Timestamp: ${new Date().toISOString()}`,
50
+ `- Reason: ${reason || 'Unknown JSON parse error'}`
51
+ ].join('\n');
52
+
53
+ fs.writeFileSync(notePath, note, 'utf8');
54
+
55
+ return { quarantinedPath, notePath };
56
+ }
57
+
58
+ module.exports = {
59
+ safeReadJson,
60
+ quarantineCorruptedFile
61
+ };
@@ -0,0 +1,407 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const ID_PATTERNS = [
5
+ /\bPTI\d{4,}-\d+\b/gi,
6
+ /\bINC\d+\b/gi,
7
+ /\bCHG\d+\b/gi
8
+ ];
9
+
10
+ const TEXT_EXTS = new Set(['.md', '.txt', '.log', '.json', '.yaml', '.yml']);
11
+ const TOKEN_RE = /[A-Za-z0-9_-]{3,}/g;
12
+
13
+ const DEFAULT_MAX_SIZE = 2 * 1024 * 1024;
14
+ const DEFAULT_TOKEN_LIMIT = 500;
15
+
16
+ function extractIdTokens(query) {
17
+ const tokens = new Set();
18
+ const q = String(query || '');
19
+ for (const re of ID_PATTERNS) {
20
+ const matches = q.match(re);
21
+ if (matches) {
22
+ for (const m of matches) tokens.add(m.toUpperCase());
23
+ }
24
+ }
25
+ return Array.from(tokens);
26
+ }
27
+
28
+ function tokenizeQuery(query) {
29
+ const tokens = [];
30
+ const q = String(query || '');
31
+ const re = /[A-Za-z0-9_-]{2,}/g;
32
+ let m;
33
+ while ((m = re.exec(q)) !== null) {
34
+ tokens.push(m[0]);
35
+ }
36
+ return tokens;
37
+ }
38
+
39
+ function listFilesRecursive(dir, files = []) {
40
+ let entries;
41
+ try {
42
+ entries = fs.readdirSync(dir, { withFileTypes: true });
43
+ } catch {
44
+ return files;
45
+ }
46
+ for (const ent of entries) {
47
+ const full = path.join(dir, ent.name);
48
+ if (ent.isDirectory()) {
49
+ listFilesRecursive(full, files);
50
+ } else if (ent.isFile()) {
51
+ const ext = path.extname(ent.name).toLowerCase();
52
+ if (TEXT_EXTS.has(ext)) files.push(full);
53
+ }
54
+ }
55
+ return files;
56
+ }
57
+
58
+ function toDateString(ms) {
59
+ try {
60
+ const d = new Date(ms);
61
+ if (Number.isNaN(d.getTime())) return '';
62
+ const y = String(d.getFullYear());
63
+ const m = String(d.getMonth() + 1).padStart(2, '0');
64
+ const day = String(d.getDate()).padStart(2, '0');
65
+ return `${y}-${m}-${day}`;
66
+ } catch {
67
+ return '';
68
+ }
69
+ }
70
+
71
+ function inferDateFromPath(filePath, mtimeMs) {
72
+ const m = String(filePath).match(/\b(\d{4}-\d{2}-\d{2})\b/);
73
+ if (m && m[1]) return m[1];
74
+ return toDateString(mtimeMs);
75
+ }
76
+
77
+ function buildSnippet(text, index, length) {
78
+ if (index < 0) {
79
+ const clean = String(text || '').replace(/\s+/g, ' ').trim();
80
+ return clean.length > 220 ? clean.slice(0, 220) + '…' : clean;
81
+ }
82
+ const raw = String(text || '');
83
+ const start = Math.max(0, index - 80);
84
+ const end = Math.min(raw.length, index + length + 120);
85
+ let snippet = raw.slice(start, end).replace(/\s+/g, ' ').trim();
86
+ if (start > 0) snippet = '…' + snippet;
87
+ if (end < raw.length) snippet = snippet + '…';
88
+ return snippet;
89
+ }
90
+
91
+ function ensureDir(dir) {
92
+ fs.mkdirSync(dir, { recursive: true });
93
+ }
94
+
95
+ function normalizeRelPath(workspaceDir, fullPath) {
96
+ return path.relative(workspaceDir, fullPath).replace(/\\/g, '/');
97
+ }
98
+
99
+ function indexPathFor(workspaceDir) {
100
+ return path.join(workspaceDir, 'data', 'index', 'search-index.json');
101
+ }
102
+
103
+ function listTargetFiles(workspaceDir) {
104
+ const targetDirs = [
105
+ path.join(workspaceDir, 'logs', 'daily'),
106
+ path.join(workspaceDir, 'data', 'tasks'),
107
+ path.join(workspaceDir, 'data', 'Clients'),
108
+ path.join(workspaceDir, 'docs', 'reports')
109
+ ];
110
+
111
+ const files = [];
112
+ for (const dir of targetDirs) {
113
+ if (!fs.existsSync(dir)) continue;
114
+ const list = listFilesRecursive(dir, []);
115
+ for (const file of list) {
116
+ try {
117
+ const st = fs.statSync(file);
118
+ if (!st.isFile()) continue;
119
+ files.push({ file, mtimeMs: st.mtimeMs });
120
+ } catch {
121
+ // ignore
122
+ }
123
+ }
124
+ }
125
+ return files;
126
+ }
127
+
128
+ function extractIdMatches(text) {
129
+ const out = [];
130
+ for (const re of ID_PATTERNS) {
131
+ let m;
132
+ while ((m = re.exec(text)) !== null) {
133
+ out.push({
134
+ key: String(m[0]).toUpperCase(),
135
+ index: m.index,
136
+ length: String(m[0]).length
137
+ });
138
+ }
139
+ re.lastIndex = 0;
140
+ }
141
+ return out;
142
+ }
143
+
144
+ function extractKeywordIndexMap(textLower, tokenLimit) {
145
+ const map = new Map();
146
+ let m;
147
+ while ((m = TOKEN_RE.exec(textLower)) !== null) {
148
+ const key = m[0];
149
+ if (!map.has(key)) {
150
+ map.set(key, m.index);
151
+ if (map.size >= tokenLimit) break;
152
+ }
153
+ }
154
+ TOKEN_RE.lastIndex = 0;
155
+ return map;
156
+ }
157
+
158
+ function addEntry(entriesMap, key, relPath, date, snippet) {
159
+ if (!entriesMap.has(key)) entriesMap.set(key, new Map());
160
+ const fileMap = entriesMap.get(key);
161
+ if (!fileMap.has(relPath)) {
162
+ fileMap.set(relPath, { path: relPath, date, snippet });
163
+ }
164
+ }
165
+
166
+ function removeFileFromEntries(entriesMap, relPath) {
167
+ for (const [key, fileMap] of entriesMap.entries()) {
168
+ if (fileMap.delete(relPath)) {
169
+ if (fileMap.size === 0) entriesMap.delete(key);
170
+ }
171
+ }
172
+ }
173
+
174
+ function indexSingleFile(workspaceDir, file, mtimeMs, opts, entriesMap) {
175
+ const maxSize = Math.max(1024, Number(opts.maxSize || DEFAULT_MAX_SIZE));
176
+ const tokenLimit = Math.max(50, Number(opts.tokenLimit || DEFAULT_TOKEN_LIMIT));
177
+
178
+ let st;
179
+ try {
180
+ st = fs.statSync(file);
181
+ } catch {
182
+ return;
183
+ }
184
+ if (!st.isFile() || st.size > maxSize) return;
185
+
186
+ let text;
187
+ try {
188
+ text = fs.readFileSync(file, 'utf8');
189
+ } catch {
190
+ return;
191
+ }
192
+ if (!text || text.includes('\u0000')) return;
193
+
194
+ const relPath = normalizeRelPath(workspaceDir, file);
195
+ const date = inferDateFromPath(relPath, mtimeMs);
196
+
197
+ const idMatches = extractIdMatches(text);
198
+ for (const match of idMatches) {
199
+ const snippet = buildSnippet(text, match.index, match.length);
200
+ addEntry(entriesMap, match.key, relPath, date, snippet);
201
+ }
202
+
203
+ const textLower = text.toLowerCase();
204
+ const tokenMap = extractKeywordIndexMap(textLower, tokenLimit);
205
+ for (const [token, index] of tokenMap.entries()) {
206
+ if (!token) continue;
207
+ const snippet = buildSnippet(text, index, token.length);
208
+ addEntry(entriesMap, token, relPath, date, snippet);
209
+ }
210
+ }
211
+
212
+ function entriesToMap(entries) {
213
+ const map = new Map();
214
+ if (!Array.isArray(entries)) return map;
215
+ for (const entry of entries) {
216
+ if (!entry || typeof entry.key !== 'string' || !Array.isArray(entry.files)) continue;
217
+ const fileMap = new Map();
218
+ for (const f of entry.files) {
219
+ if (!f || typeof f.path !== 'string') continue;
220
+ fileMap.set(f.path, {
221
+ path: f.path,
222
+ date: typeof f.date === 'string' ? f.date : '',
223
+ snippet: typeof f.snippet === 'string' ? f.snippet : ''
224
+ });
225
+ }
226
+ if (fileMap.size) map.set(entry.key, fileMap);
227
+ }
228
+ return map;
229
+ }
230
+
231
+ function mapToEntries(entriesMap) {
232
+ const out = [];
233
+ const keys = Array.from(entriesMap.keys()).sort();
234
+ for (const key of keys) {
235
+ const fileMap = entriesMap.get(key);
236
+ const files = Array.from(fileMap.values());
237
+ out.push({ key, files });
238
+ }
239
+ return out;
240
+ }
241
+
242
+ function readIndex(indexPath) {
243
+ try {
244
+ if (!fs.existsSync(indexPath)) return null;
245
+ const raw = fs.readFileSync(indexPath, 'utf8');
246
+ const json = JSON.parse(raw);
247
+ if (!json || typeof json !== 'object') return null;
248
+ return json;
249
+ } catch {
250
+ return null;
251
+ }
252
+ }
253
+
254
+ function buildIndex(workspaceDir, opts = {}) {
255
+ const entriesMap = new Map();
256
+ const files = listTargetFiles(workspaceDir);
257
+
258
+ for (const { file, mtimeMs } of files) {
259
+ indexSingleFile(workspaceDir, file, mtimeMs, opts, entriesMap);
260
+ }
261
+
262
+ const metaFiles = {};
263
+ for (const { file, mtimeMs } of files) {
264
+ const relPath = normalizeRelPath(workspaceDir, file);
265
+ metaFiles[relPath] = mtimeMs;
266
+ }
267
+
268
+ const index = {
269
+ meta: {
270
+ lastRun: new Date().toISOString(),
271
+ files: metaFiles
272
+ },
273
+ entries: mapToEntries(entriesMap)
274
+ };
275
+
276
+ const indexPath = indexPathFor(workspaceDir);
277
+ ensureDir(path.dirname(indexPath));
278
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n', 'utf8');
279
+
280
+ return { indexPath, fileCount: files.length, keyCount: index.entries.length };
281
+ }
282
+
283
+ function updateIndex(workspaceDir, opts = {}) {
284
+ const indexPath = indexPathFor(workspaceDir);
285
+ const currentFiles = listTargetFiles(workspaceDir);
286
+ const currentMap = new Map();
287
+ for (const { file, mtimeMs } of currentFiles) {
288
+ const relPath = normalizeRelPath(workspaceDir, file);
289
+ currentMap.set(relPath, { file, mtimeMs });
290
+ }
291
+
292
+ const existing = readIndex(indexPath);
293
+ if (!existing || !existing.meta || !existing.meta.files || !Array.isArray(existing.entries)) {
294
+ return buildIndex(workspaceDir, opts);
295
+ }
296
+
297
+ const entriesMap = entriesToMap(existing.entries);
298
+ const prevFiles = existing.meta.files || {};
299
+
300
+ const removed = new Set();
301
+ for (const relPath of Object.keys(prevFiles)) {
302
+ if (!currentMap.has(relPath)) removed.add(relPath);
303
+ }
304
+
305
+ const changed = [];
306
+ for (const [relPath, info] of currentMap.entries()) {
307
+ const prev = prevFiles[relPath];
308
+ if (!prev || Number(prev) !== Number(info.mtimeMs)) {
309
+ changed.push(info);
310
+ }
311
+ }
312
+
313
+ for (const relPath of removed) {
314
+ removeFileFromEntries(entriesMap, relPath);
315
+ }
316
+
317
+ for (const info of changed) {
318
+ const relPath = normalizeRelPath(workspaceDir, info.file);
319
+ removeFileFromEntries(entriesMap, relPath);
320
+ indexSingleFile(workspaceDir, info.file, info.mtimeMs, opts, entriesMap);
321
+ }
322
+
323
+ const metaFiles = {};
324
+ for (const [relPath, info] of currentMap.entries()) {
325
+ metaFiles[relPath] = info.mtimeMs;
326
+ }
327
+
328
+ const index = {
329
+ meta: {
330
+ lastRun: new Date().toISOString(),
331
+ files: metaFiles
332
+ },
333
+ entries: mapToEntries(entriesMap)
334
+ };
335
+
336
+ ensureDir(path.dirname(indexPath));
337
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2) + '\n', 'utf8');
338
+
339
+ return { indexPath, fileCount: currentFiles.length, keyCount: index.entries.length, changed: changed.length, removed: removed.size };
340
+ }
341
+
342
+ function searchIndex(workspaceDir, query, opts = {}) {
343
+ const q = String(query || '').trim();
344
+ if (!q) return [];
345
+ const limit = Math.max(1, Math.min(20, Number(opts.limit || 8)));
346
+
347
+ const indexPath = indexPathFor(workspaceDir);
348
+ const index = readIndex(indexPath);
349
+ if (!index || !Array.isArray(index.entries)) return [];
350
+
351
+ const entriesMap = new Map();
352
+ for (const entry of index.entries) {
353
+ if (!entry || typeof entry.key !== 'string' || !Array.isArray(entry.files)) continue;
354
+ entriesMap.set(entry.key, entry.files);
355
+ }
356
+
357
+ const idTokens = extractIdTokens(q);
358
+ const tokens = tokenizeQuery(q).map((t) => t.toLowerCase());
359
+ const queryLower = q.toLowerCase();
360
+
361
+ const resultsMap = new Map();
362
+
363
+ function applyMatches(keys, weight) {
364
+ for (const key of keys) {
365
+ if (!key) continue;
366
+ const entryFiles = entriesMap.get(key);
367
+ if (!entryFiles) continue;
368
+ for (const f of entryFiles) {
369
+ if (!f || !f.path) continue;
370
+ const prev = resultsMap.get(f.path) || { file: f.path, date: f.date || '', score: 0, snippet: '' , weight: 0 };
371
+ const bonus = (key === queryLower) ? 10 : 0;
372
+ const nextScore = prev.score + weight + bonus;
373
+ const nextWeight = Math.max(prev.weight, weight);
374
+ const snippet = (nextWeight > prev.weight && f.snippet) ? f.snippet : (prev.snippet || f.snippet || '');
375
+ resultsMap.set(f.path, {
376
+ file: f.path,
377
+ date: f.date || prev.date || '',
378
+ score: nextScore,
379
+ snippet,
380
+ weight: nextWeight
381
+ });
382
+ }
383
+ }
384
+ }
385
+
386
+ applyMatches(idTokens.map((t) => t.toUpperCase()), 100);
387
+ applyMatches(tokens, 2);
388
+
389
+ const results = Array.from(resultsMap.values()).map((r) => {
390
+ const { weight, ...rest } = r;
391
+ return rest;
392
+ });
393
+
394
+ results.sort((a, b) => {
395
+ if (b.score !== a.score) return b.score - a.score;
396
+ return String(b.date || '').localeCompare(String(a.date || ''));
397
+ });
398
+
399
+ return results.slice(0, limit);
400
+ }
401
+
402
+ module.exports = {
403
+ buildIndex,
404
+ updateIndex,
405
+ searchIndex,
406
+ indexPathFor
407
+ };
@@ -0,0 +1,183 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ const ID_PATTERNS = [
5
+ /\bPTI\d{4,}-\d+\b/gi,
6
+ /\bINC\d+\b/gi,
7
+ /\bCHG\d+\b/gi
8
+ ];
9
+
10
+ const TEXT_EXTS = new Set(['.md', '.txt', '.log', '.json', '.yaml', '.yml']);
11
+
12
+ function extractIdTokens(query) {
13
+ const tokens = new Set();
14
+ const q = String(query || '');
15
+ for (const re of ID_PATTERNS) {
16
+ const matches = q.match(re);
17
+ if (matches) {
18
+ for (const m of matches) tokens.add(m.toUpperCase());
19
+ }
20
+ }
21
+ return Array.from(tokens);
22
+ }
23
+
24
+ function tokenizeQuery(query) {
25
+ const tokens = [];
26
+ const q = String(query || '');
27
+ const re = /[A-Za-z0-9_-]{2,}/g;
28
+ let m;
29
+ while ((m = re.exec(q)) !== null) {
30
+ tokens.push(m[0]);
31
+ }
32
+ return tokens;
33
+ }
34
+
35
+ function listFilesRecursive(dir, files = []) {
36
+ let entries;
37
+ try {
38
+ entries = fs.readdirSync(dir, { withFileTypes: true });
39
+ } catch {
40
+ return files;
41
+ }
42
+ for (const ent of entries) {
43
+ const full = path.join(dir, ent.name);
44
+ if (ent.isDirectory()) {
45
+ listFilesRecursive(full, files);
46
+ } else if (ent.isFile()) {
47
+ const ext = path.extname(ent.name).toLowerCase();
48
+ if (TEXT_EXTS.has(ext)) files.push(full);
49
+ }
50
+ }
51
+ return files;
52
+ }
53
+
54
+ function toDateString(ms) {
55
+ try {
56
+ const d = new Date(ms);
57
+ if (Number.isNaN(d.getTime())) return '';
58
+ const y = String(d.getFullYear());
59
+ const m = String(d.getMonth() + 1).padStart(2, '0');
60
+ const day = String(d.getDate()).padStart(2, '0');
61
+ return `${y}-${m}-${day}`;
62
+ } catch {
63
+ return '';
64
+ }
65
+ }
66
+
67
+ function inferDateFromPath(filePath, mtimeMs) {
68
+ const m = String(filePath).match(/\b(\d{4}-\d{2}-\d{2})\b/);
69
+ if (m && m[1]) return m[1];
70
+ return toDateString(mtimeMs);
71
+ }
72
+
73
+ function findFirstMatchIndex(textLower, needles) {
74
+ let best = -1;
75
+ for (const needle of needles) {
76
+ if (!needle) continue;
77
+ const idx = textLower.indexOf(needle);
78
+ if (idx !== -1 && (best === -1 || idx < best)) best = idx;
79
+ }
80
+ return best;
81
+ }
82
+
83
+ function buildSnippet(text, index, length) {
84
+ if (index < 0) {
85
+ const clean = String(text || '').replace(/\s+/g, ' ').trim();
86
+ return clean.length > 220 ? clean.slice(0, 220) + '…' : clean;
87
+ }
88
+ const raw = String(text || '');
89
+ const start = Math.max(0, index - 80);
90
+ const end = Math.min(raw.length, index + length + 120);
91
+ let snippet = raw.slice(start, end).replace(/\s+/g, ' ').trim();
92
+ if (start > 0) snippet = '…' + snippet;
93
+ if (end < raw.length) snippet = snippet + '…';
94
+ return snippet;
95
+ }
96
+
97
+ function scoreText(textLower, queryLower, tokensLower, idTokensLower) {
98
+ let score = 0;
99
+ if (queryLower && textLower.includes(queryLower)) score += 10;
100
+ for (const t of tokensLower) {
101
+ if (!t) continue;
102
+ if (textLower.includes(t)) score += 2;
103
+ }
104
+ for (const id of idTokensLower) {
105
+ if (!id) continue;
106
+ if (textLower.includes(id)) score += 100;
107
+ }
108
+ return score;
109
+ }
110
+
111
+ function searchWorkspace(workspaceDir, query, opts = {}) {
112
+ const q = String(query || '').trim();
113
+ if (!q) return [];
114
+
115
+ const limit = Math.max(1, Math.min(20, Number(opts.limit || 8)));
116
+ const maxSize = Math.max(1024, Number(opts.maxSize || 2 * 1024 * 1024));
117
+
118
+ const targetDirs = [
119
+ path.join(workspaceDir, 'logs', 'daily'),
120
+ path.join(workspaceDir, 'data', 'tasks'),
121
+ path.join(workspaceDir, 'data', 'Clients'),
122
+ path.join(workspaceDir, 'docs', 'reports')
123
+ ];
124
+
125
+ const idTokens = extractIdTokens(q);
126
+ const tokens = tokenizeQuery(q);
127
+ const tokensLower = tokens.map((t) => t.toLowerCase());
128
+ const idTokensLower = idTokens.map((t) => t.toLowerCase());
129
+ const queryLower = q.toLowerCase();
130
+
131
+ const results = [];
132
+ for (const dir of targetDirs) {
133
+ if (!fs.existsSync(dir)) continue;
134
+ const files = listFilesRecursive(dir, []);
135
+ for (const file of files) {
136
+ let st;
137
+ try {
138
+ st = fs.statSync(file);
139
+ } catch {
140
+ continue;
141
+ }
142
+ if (!st.isFile() || st.size > maxSize) continue;
143
+ let text;
144
+ try {
145
+ text = fs.readFileSync(file, 'utf8');
146
+ } catch {
147
+ continue;
148
+ }
149
+ if (!text || text.includes('\u0000')) continue;
150
+ const textLower = text.toLowerCase();
151
+ const score = scoreText(textLower, queryLower, tokensLower, idTokensLower);
152
+ if (score <= 0) continue;
153
+
154
+ const needles = [];
155
+ if (queryLower) needles.push(queryLower);
156
+ for (const t of tokensLower) needles.push(t);
157
+ for (const id of idTokensLower) needles.push(id);
158
+ const idx = findFirstMatchIndex(textLower, needles);
159
+ const snippet = buildSnippet(text, idx, queryLower.length || 12);
160
+ const relPath = path.relative(workspaceDir, file).replace(/\\/g, '/');
161
+ const date = inferDateFromPath(relPath, st.mtimeMs);
162
+
163
+ results.push({
164
+ file: relPath,
165
+ date,
166
+ score,
167
+ snippet
168
+ });
169
+ }
170
+ }
171
+
172
+ results.sort((a, b) => {
173
+ if (b.score !== a.score) return b.score - a.score;
174
+ return String(b.date || '').localeCompare(String(a.date || ''));
175
+ });
176
+
177
+ return results.slice(0, limit);
178
+ }
179
+
180
+ module.exports = {
181
+ extractIdTokens,
182
+ searchWorkspace
183
+ };