nostr-mcp-server 2.0.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/LICENSE +21 -0
- package/README.md +498 -0
- package/build/__tests__/basic.test.js +87 -0
- package/build/__tests__/error-handling.test.js +145 -0
- package/build/__tests__/format-conversion.test.js +137 -0
- package/build/__tests__/integration.test.js +163 -0
- package/build/__tests__/mocks.js +109 -0
- package/build/__tests__/nip19-conversion.test.js +268 -0
- package/build/__tests__/nips-search.test.js +109 -0
- package/build/__tests__/note-creation.test.js +148 -0
- package/build/__tests__/note-tools-functions.test.js +173 -0
- package/build/__tests__/note-tools-unit.test.js +97 -0
- package/build/__tests__/profile-notes-simple.test.js +78 -0
- package/build/__tests__/profile-postnote.test.js +120 -0
- package/build/__tests__/profile-tools.test.js +90 -0
- package/build/__tests__/relay-specification.test.js +136 -0
- package/build/__tests__/search-nips-simple.test.js +96 -0
- package/build/__tests__/websocket-integration.test.js +257 -0
- package/build/__tests__/zap-tools-simple.test.js +72 -0
- package/build/__tests__/zap-tools-tests.test.js +197 -0
- package/build/index.js +1285 -0
- package/build/nips/nips-tools.js +567 -0
- package/build/nips-tools.js +421 -0
- package/build/note/note-tools.js +296 -0
- package/build/note-tools.js +53 -0
- package/build/profile/profile-tools.js +260 -0
- package/build/utils/constants.js +27 -0
- package/build/utils/conversion.js +332 -0
- package/build/utils/ephemeral-relay.js +438 -0
- package/build/utils/formatting.js +34 -0
- package/build/utils/index.js +6 -0
- package/build/utils/nip19-tools.js +117 -0
- package/build/utils/pool.js +55 -0
- package/build/zap/zap-tools.js +980 -0
- package/build/zap-tools.js +989 -0
- package/package.json +59 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import fetch from "node-fetch";
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
// Cache configuration
|
|
6
|
+
const CACHE_TTL = 1000 * 60 * 60 * 24; // 24 hours
|
|
7
|
+
const CACHE_FILE = path.join(process.cwd(), 'nips-cache.json');
|
|
8
|
+
let nipsCache = [];
|
|
9
|
+
let lastFetchTime = 0;
|
|
10
|
+
let searchIndex = {
|
|
11
|
+
titleIndex: new Map(),
|
|
12
|
+
descriptionIndex: new Map(),
|
|
13
|
+
contentIndex: new Map(),
|
|
14
|
+
numberIndex: new Map(),
|
|
15
|
+
kindIndex: new Map(),
|
|
16
|
+
tagIndex: new Map()
|
|
17
|
+
};
|
|
18
|
+
// Load cache from disk on startup
|
|
19
|
+
function loadCacheFromDisk() {
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(CACHE_FILE)) {
|
|
22
|
+
const cacheData = fs.readFileSync(CACHE_FILE, 'utf8');
|
|
23
|
+
const cacheObj = JSON.parse(cacheData);
|
|
24
|
+
if (cacheObj && Array.isArray(cacheObj.nips) && typeof cacheObj.timestamp === 'number') {
|
|
25
|
+
nipsCache = cacheObj.nips;
|
|
26
|
+
lastFetchTime = cacheObj.timestamp;
|
|
27
|
+
// Check if cache is fresh enough
|
|
28
|
+
if (Date.now() - lastFetchTime < CACHE_TTL) {
|
|
29
|
+
console.log(`Loaded ${nipsCache.length} NIPs from cache file`);
|
|
30
|
+
buildSearchIndex();
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
console.log('Cache file exists but is expired');
|
|
35
|
+
// We'll still use it temporarily but will refresh
|
|
36
|
+
buildSearchIndex();
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
console.error('Error loading cache from disk:', error);
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Save cache to disk
|
|
49
|
+
function saveCacheToDisk() {
|
|
50
|
+
try {
|
|
51
|
+
const cacheObj = {
|
|
52
|
+
nips: nipsCache,
|
|
53
|
+
timestamp: lastFetchTime
|
|
54
|
+
};
|
|
55
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify(cacheObj, null, 2), 'utf8');
|
|
56
|
+
console.log(`Saved ${nipsCache.length} NIPs to cache file`);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.error('Error saving cache to disk:', error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Build search index from nips cache
|
|
63
|
+
function buildSearchIndex() {
|
|
64
|
+
// Reset indexes
|
|
65
|
+
searchIndex = {
|
|
66
|
+
titleIndex: new Map(),
|
|
67
|
+
descriptionIndex: new Map(),
|
|
68
|
+
contentIndex: new Map(),
|
|
69
|
+
numberIndex: new Map(),
|
|
70
|
+
kindIndex: new Map(),
|
|
71
|
+
tagIndex: new Map()
|
|
72
|
+
};
|
|
73
|
+
for (const nip of nipsCache) {
|
|
74
|
+
// Index NIP number
|
|
75
|
+
searchIndex.numberIndex.set(nip.number.toString(), nip.number);
|
|
76
|
+
// Index title words
|
|
77
|
+
const titleWords = nip.title.toLowerCase().split(/\W+/).filter(word => word.length > 0);
|
|
78
|
+
for (const word of titleWords) {
|
|
79
|
+
if (!searchIndex.titleIndex.has(word)) {
|
|
80
|
+
searchIndex.titleIndex.set(word, new Set());
|
|
81
|
+
}
|
|
82
|
+
searchIndex.titleIndex.get(word)?.add(nip.number);
|
|
83
|
+
}
|
|
84
|
+
// Index description words
|
|
85
|
+
const descWords = nip.description.toLowerCase().split(/\W+/).filter(word => word.length > 0);
|
|
86
|
+
for (const word of descWords) {
|
|
87
|
+
if (!searchIndex.descriptionIndex.has(word)) {
|
|
88
|
+
searchIndex.descriptionIndex.set(word, new Set());
|
|
89
|
+
}
|
|
90
|
+
searchIndex.descriptionIndex.get(word)?.add(nip.number);
|
|
91
|
+
}
|
|
92
|
+
// Index content (more selective to save memory)
|
|
93
|
+
const contentWords = new Set(nip.content.toLowerCase()
|
|
94
|
+
.split(/\W+/)
|
|
95
|
+
.filter(word => word.length > 3) // Only index words longer than 3 chars
|
|
96
|
+
);
|
|
97
|
+
for (const word of contentWords) {
|
|
98
|
+
if (!searchIndex.contentIndex.has(word)) {
|
|
99
|
+
searchIndex.contentIndex.set(word, new Set());
|
|
100
|
+
}
|
|
101
|
+
searchIndex.contentIndex.get(word)?.add(nip.number);
|
|
102
|
+
}
|
|
103
|
+
// Index kind
|
|
104
|
+
if (nip.kind !== undefined) {
|
|
105
|
+
if (!searchIndex.kindIndex.has(nip.kind)) {
|
|
106
|
+
searchIndex.kindIndex.set(nip.kind, new Set());
|
|
107
|
+
}
|
|
108
|
+
searchIndex.kindIndex.get(nip.kind)?.add(nip.number);
|
|
109
|
+
}
|
|
110
|
+
// Index tags
|
|
111
|
+
if (nip.tags) {
|
|
112
|
+
for (const tag of nip.tags) {
|
|
113
|
+
const normalizedTag = tag.toLowerCase().trim();
|
|
114
|
+
if (!searchIndex.tagIndex.has(normalizedTag)) {
|
|
115
|
+
searchIndex.tagIndex.set(normalizedTag, new Set());
|
|
116
|
+
}
|
|
117
|
+
searchIndex.tagIndex.get(normalizedTag)?.add(nip.number);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
console.log('Built search index for NIPs');
|
|
122
|
+
}
|
|
123
|
+
// Function to fetch NIPs from GitHub with retries
|
|
124
|
+
async function fetchNipsFromGitHub(retries = 3) {
|
|
125
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
126
|
+
try {
|
|
127
|
+
console.log(`Fetching NIPs from GitHub (attempt ${attempt}/${retries})`);
|
|
128
|
+
// Fetch the NIPs directory listing
|
|
129
|
+
const headers = {};
|
|
130
|
+
// Use conditional request if we already have data
|
|
131
|
+
if (nipsCache.length > 0) {
|
|
132
|
+
headers['If-Modified-Since'] = new Date(lastFetchTime).toUTCString();
|
|
133
|
+
}
|
|
134
|
+
const response = await fetch('https://api.github.com/repos/nostr-protocol/nips/contents', { headers });
|
|
135
|
+
// If not modified, use cache
|
|
136
|
+
if (response.status === 304) {
|
|
137
|
+
console.log('NIPs not modified since last fetch, using cache');
|
|
138
|
+
return nipsCache;
|
|
139
|
+
}
|
|
140
|
+
if (!response.ok)
|
|
141
|
+
throw new Error(`GitHub API error: ${response.statusText}`);
|
|
142
|
+
const files = await response.json();
|
|
143
|
+
// Filter for NIP markdown files
|
|
144
|
+
const nipFiles = files.filter((file) => file.name.match(/^\d+\.md$/) ||
|
|
145
|
+
file.name.match(/^[0-9A-Fa-f]+\.md$/));
|
|
146
|
+
console.log(`Found ${nipFiles.length} NIP files to process`);
|
|
147
|
+
// Process files in batches to avoid overwhelming the GitHub API
|
|
148
|
+
const batchSize = 5;
|
|
149
|
+
const nips = [];
|
|
150
|
+
for (let i = 0; i < nipFiles.length; i += batchSize) {
|
|
151
|
+
const batch = nipFiles.slice(i, i + batchSize);
|
|
152
|
+
const batchPromises = batch.map(file => fetchNipFile(file));
|
|
153
|
+
const batchResults = await Promise.all(batchPromises);
|
|
154
|
+
const validNips = batchResults.filter((nip) => nip !== null);
|
|
155
|
+
nips.push(...validNips);
|
|
156
|
+
// Add a small delay between batches to avoid rate limiting
|
|
157
|
+
if (i + batchSize < nipFiles.length) {
|
|
158
|
+
await new Promise(resolve => setTimeout(resolve, 300));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
console.log(`Successfully processed ${nips.length} NIPs`);
|
|
162
|
+
return nips;
|
|
163
|
+
}
|
|
164
|
+
catch (error) {
|
|
165
|
+
console.error(`Error fetching NIPs from GitHub (attempt ${attempt}/${retries}):`, error);
|
|
166
|
+
if (attempt === retries) {
|
|
167
|
+
// On final retry failure, return cache if available or empty array
|
|
168
|
+
console.warn('All GitHub fetch attempts failed, using cached data if available');
|
|
169
|
+
return nipsCache.length > 0 ? nipsCache : [];
|
|
170
|
+
}
|
|
171
|
+
// Wait before retrying (with increasing backoff)
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, attempt * 1000));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
// Helper to fetch a single NIP file
|
|
178
|
+
async function fetchNipFile(file) {
|
|
179
|
+
try {
|
|
180
|
+
const contentResponse = await fetch(file.download_url);
|
|
181
|
+
if (!contentResponse.ok) {
|
|
182
|
+
console.warn(`Failed to fetch ${file.name}: ${contentResponse.statusText}`);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const content = await contentResponse.text();
|
|
186
|
+
const numberMatch = file.name.match(/^(\d+|[0-9A-Fa-f]+)\.md$/);
|
|
187
|
+
if (!numberMatch)
|
|
188
|
+
return null;
|
|
189
|
+
const numberStr = numberMatch[1];
|
|
190
|
+
const number = numberStr.match(/^[0-9A-Fa-f]+$/) ?
|
|
191
|
+
parseInt(numberStr, 16) :
|
|
192
|
+
parseInt(numberStr, 10);
|
|
193
|
+
const lines = content.split('\n');
|
|
194
|
+
const title = lines[0].replace(/^#\s*/, '').trim();
|
|
195
|
+
const description = lines[1]?.trim() || `NIP-${number} description`;
|
|
196
|
+
const statusMatch = content.match(/Status:\s*(draft|final|deprecated)/i);
|
|
197
|
+
const status = statusMatch ? statusMatch[1].toLowerCase() : "draft";
|
|
198
|
+
const kindMatch = content.match(/Kind:\s*(\d+)/i);
|
|
199
|
+
const kind = kindMatch ? parseInt(kindMatch[1], 10) : undefined;
|
|
200
|
+
const tags = [];
|
|
201
|
+
const tagMatches = content.matchAll(/Tags:\s*([^\n]+)/gi);
|
|
202
|
+
for (const match of tagMatches) {
|
|
203
|
+
tags.push(...match[1].split(',').map((tag) => tag.trim()));
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
number,
|
|
207
|
+
title,
|
|
208
|
+
description,
|
|
209
|
+
status,
|
|
210
|
+
kind,
|
|
211
|
+
tags: tags.length > 0 ? tags : undefined,
|
|
212
|
+
content
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
console.error(`Error processing NIP ${file.name}:`, error);
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
// Function to get NIPs with improved caching
|
|
221
|
+
async function getNips(forceRefresh = false) {
|
|
222
|
+
const now = Date.now();
|
|
223
|
+
// First attempt to load from memory cache
|
|
224
|
+
if (!forceRefresh && nipsCache.length > 0 && now - lastFetchTime < CACHE_TTL) {
|
|
225
|
+
return nipsCache;
|
|
226
|
+
}
|
|
227
|
+
// If no memory cache, try loading from disk
|
|
228
|
+
if (!forceRefresh && nipsCache.length === 0) {
|
|
229
|
+
const loaded = loadCacheFromDisk();
|
|
230
|
+
if (loaded && now - lastFetchTime < CACHE_TTL) {
|
|
231
|
+
return nipsCache;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Fetch fresh data if needed
|
|
235
|
+
try {
|
|
236
|
+
const nips = await fetchNipsFromGitHub();
|
|
237
|
+
// Only update cache if we got new data
|
|
238
|
+
if (nips.length > 0) {
|
|
239
|
+
nipsCache = nips;
|
|
240
|
+
lastFetchTime = now;
|
|
241
|
+
// Save to disk and build search index
|
|
242
|
+
saveCacheToDisk();
|
|
243
|
+
buildSearchIndex();
|
|
244
|
+
}
|
|
245
|
+
return nipsCache;
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
console.error("Error refreshing NIPs:", error);
|
|
249
|
+
// If we already have cached data, use it even if expired
|
|
250
|
+
if (nipsCache.length > 0) {
|
|
251
|
+
console.warn("Using expired cache due to fetch error");
|
|
252
|
+
return nipsCache;
|
|
253
|
+
}
|
|
254
|
+
// Last resort - try to load from disk regardless of timestamp
|
|
255
|
+
if (loadCacheFromDisk()) {
|
|
256
|
+
return nipsCache;
|
|
257
|
+
}
|
|
258
|
+
// No options left
|
|
259
|
+
return [];
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Helper function to calculate relevance score using the search index
|
|
263
|
+
function calculateRelevance(nip, searchTerms) {
|
|
264
|
+
const matchedTerms = [];
|
|
265
|
+
let score = 0;
|
|
266
|
+
// Convert search terms to lowercase for case-insensitive matching
|
|
267
|
+
const lowerSearchTerms = searchTerms.map(term => term.toLowerCase());
|
|
268
|
+
for (const term of lowerSearchTerms) {
|
|
269
|
+
// Check for exact NIP number match (highest priority)
|
|
270
|
+
if (nip.number.toString() === term) {
|
|
271
|
+
score += 10;
|
|
272
|
+
matchedTerms.push(term);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
// Check title matches (high weight)
|
|
276
|
+
if (searchIndex.titleIndex.has(term) &&
|
|
277
|
+
searchIndex.titleIndex.get(term)?.has(nip.number)) {
|
|
278
|
+
score += 3;
|
|
279
|
+
matchedTerms.push(term);
|
|
280
|
+
}
|
|
281
|
+
// Check description matches (medium weight)
|
|
282
|
+
if (searchIndex.descriptionIndex.has(term) &&
|
|
283
|
+
searchIndex.descriptionIndex.get(term)?.has(nip.number)) {
|
|
284
|
+
score += 2;
|
|
285
|
+
if (!matchedTerms.includes(term)) {
|
|
286
|
+
matchedTerms.push(term);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// Check content matches (lower weight)
|
|
290
|
+
if (searchIndex.contentIndex.has(term) &&
|
|
291
|
+
searchIndex.contentIndex.get(term)?.has(nip.number)) {
|
|
292
|
+
score += 1;
|
|
293
|
+
if (!matchedTerms.includes(term)) {
|
|
294
|
+
matchedTerms.push(term);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Check kind match
|
|
298
|
+
if (nip.kind !== undefined && nip.kind.toString() === term) {
|
|
299
|
+
score += 4;
|
|
300
|
+
if (!matchedTerms.includes(term)) {
|
|
301
|
+
matchedTerms.push(term);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// Check tag matches
|
|
305
|
+
if (nip.tags && nip.tags.some(tag => tag.toLowerCase() === term)) {
|
|
306
|
+
score += 3;
|
|
307
|
+
if (!matchedTerms.includes(term)) {
|
|
308
|
+
matchedTerms.push(term);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// Partial matches in title (very important)
|
|
312
|
+
if (nip.title.toLowerCase().includes(term)) {
|
|
313
|
+
score += 2;
|
|
314
|
+
if (!matchedTerms.includes(term)) {
|
|
315
|
+
matchedTerms.push(term);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return { score, matchedTerms };
|
|
320
|
+
}
|
|
321
|
+
// Improved search function
|
|
322
|
+
export async function searchNips(query, limit = 10) {
|
|
323
|
+
// Ensure we have NIPs data and the search index is built
|
|
324
|
+
const nips = await getNips();
|
|
325
|
+
if (nips.length === 0) {
|
|
326
|
+
console.error("No NIPs available for search");
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
// Handle direct NIP number search as a special case
|
|
330
|
+
const nipNumberMatch = query.match(/^(?:NIP-?)?(\d+)$/i);
|
|
331
|
+
if (nipNumberMatch) {
|
|
332
|
+
const nipNumber = parseInt(nipNumberMatch[1], 10);
|
|
333
|
+
const directNip = nips.find(nip => nip.number === nipNumber);
|
|
334
|
+
if (directNip) {
|
|
335
|
+
return [{
|
|
336
|
+
nip: directNip,
|
|
337
|
+
relevance: 100,
|
|
338
|
+
matchedTerms: [nipNumber.toString()]
|
|
339
|
+
}];
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
// Split query into terms and filter out empty strings
|
|
343
|
+
const searchTerms = query.split(/\s+/).filter(term => term.length > 0);
|
|
344
|
+
// Search through all NIPs
|
|
345
|
+
const results = nips.map(nip => {
|
|
346
|
+
const { score, matchedTerms } = calculateRelevance(nip, searchTerms);
|
|
347
|
+
return {
|
|
348
|
+
nip,
|
|
349
|
+
relevance: score,
|
|
350
|
+
matchedTerms
|
|
351
|
+
};
|
|
352
|
+
})
|
|
353
|
+
// Filter out results with no matches
|
|
354
|
+
.filter(result => result.relevance > 0)
|
|
355
|
+
// Sort by relevance (highest first)
|
|
356
|
+
.sort((a, b) => b.relevance - a.relevance)
|
|
357
|
+
// Limit results
|
|
358
|
+
.slice(0, limit);
|
|
359
|
+
return results;
|
|
360
|
+
}
|
|
361
|
+
// Improved function to get a specific NIP by number
|
|
362
|
+
export async function getNipByNumber(number) {
|
|
363
|
+
const nips = await getNips();
|
|
364
|
+
const nipNumber = typeof number === 'string' ? parseInt(number, 10) : number;
|
|
365
|
+
return nips.find(nip => nip.number === nipNumber);
|
|
366
|
+
}
|
|
367
|
+
// Improved function to get NIPs by kind
|
|
368
|
+
export async function getNipsByKind(kind) {
|
|
369
|
+
const nips = await getNips();
|
|
370
|
+
return nips.filter(nip => nip.kind === kind);
|
|
371
|
+
}
|
|
372
|
+
// Improved function to get NIPs by status
|
|
373
|
+
export async function getNipsByStatus(status) {
|
|
374
|
+
const nips = await getNips();
|
|
375
|
+
return nips.filter(nip => nip.status === status);
|
|
376
|
+
}
|
|
377
|
+
// Force a refresh of the NIPs cache
|
|
378
|
+
export async function refreshNipsCache() {
|
|
379
|
+
try {
|
|
380
|
+
await getNips(true);
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
console.error("Error refreshing NIPs cache:", error);
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// Export schema for the search tool
|
|
389
|
+
export const searchNipsSchema = z.object({
|
|
390
|
+
query: z.string().describe("Search query to find relevant NIPs"),
|
|
391
|
+
limit: z.number().min(1).max(50).default(10).describe("Maximum number of results to return"),
|
|
392
|
+
includeContent: z.boolean().default(false).describe("Whether to include the full content of each NIP in the results"),
|
|
393
|
+
});
|
|
394
|
+
// Format a NIP search result with cleaner output
|
|
395
|
+
export function formatNipResult(result, includeContent = false) {
|
|
396
|
+
const { nip, relevance, matchedTerms } = result;
|
|
397
|
+
const lines = [
|
|
398
|
+
`NIP-${nip.number}: ${nip.title}`,
|
|
399
|
+
`Status: ${nip.status}`,
|
|
400
|
+
nip.kind ? `Kind: ${nip.kind}` : null,
|
|
401
|
+
`Description: ${nip.description}`,
|
|
402
|
+
`Relevance Score: ${relevance}`,
|
|
403
|
+
matchedTerms.length > 0 ? `Matched Terms: ${matchedTerms.join(", ")}` : null,
|
|
404
|
+
].filter(Boolean);
|
|
405
|
+
if (includeContent) {
|
|
406
|
+
lines.push("", "Content:", nip.content);
|
|
407
|
+
}
|
|
408
|
+
lines.push("---");
|
|
409
|
+
return lines.join("\n");
|
|
410
|
+
}
|
|
411
|
+
// Initialize by loading cache on module import
|
|
412
|
+
(async () => {
|
|
413
|
+
// Try to load from disk first
|
|
414
|
+
if (!loadCacheFromDisk()) {
|
|
415
|
+
// If disk cache not available or expired, fetch in background
|
|
416
|
+
console.log('Fetching NIPs in background...');
|
|
417
|
+
getNips().catch(error => {
|
|
418
|
+
console.error('Error initializing NIPs cache:', error);
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
})();
|