memory-journal-mcp 4.3.1 → 4.4.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/.dockerignore +131 -122
- package/.gitattributes +29 -0
- package/.github/workflows/docker-publish.yml +1 -1
- package/.github/workflows/lint-and-test.yml +1 -2
- package/.github/workflows/secrets-scanning.yml +0 -1
- package/.github/workflows/security-update.yml +6 -6
- package/.vscode/settings.json +17 -15
- package/CHANGELOG.md +1065 -11
- package/DOCKER_README.md +51 -33
- package/Dockerfile +12 -11
- package/README.md +68 -33
- package/SECURITY.md +225 -158
- package/dist/cli.js +7 -0
- package/dist/cli.js.map +1 -1
- package/dist/constants/ServerInstructions.d.ts +1 -1
- package/dist/constants/ServerInstructions.d.ts.map +1 -1
- package/dist/constants/ServerInstructions.js +70 -26
- package/dist/constants/ServerInstructions.js.map +1 -1
- package/dist/constants/icons.d.ts +2 -0
- package/dist/constants/icons.d.ts.map +1 -1
- package/dist/constants/icons.js +6 -0
- package/dist/constants/icons.js.map +1 -1
- package/dist/database/SqliteAdapter.d.ts +51 -10
- package/dist/database/SqliteAdapter.d.ts.map +1 -1
- package/dist/database/SqliteAdapter.js +140 -31
- package/dist/database/SqliteAdapter.js.map +1 -1
- package/dist/filtering/ToolFilter.d.ts +1 -1
- package/dist/filtering/ToolFilter.d.ts.map +1 -1
- package/dist/filtering/ToolFilter.js +7 -1
- package/dist/filtering/ToolFilter.js.map +1 -1
- package/dist/github/GitHubIntegration.d.ts +74 -2
- package/dist/github/GitHubIntegration.d.ts.map +1 -1
- package/dist/github/GitHubIntegration.js +508 -7
- package/dist/github/GitHubIntegration.js.map +1 -1
- package/dist/handlers/prompts/index.js +1 -0
- package/dist/handlers/prompts/index.js.map +1 -1
- package/dist/handlers/resources/index.d.ts.map +1 -1
- package/dist/handlers/resources/index.js +257 -13
- package/dist/handlers/resources/index.js.map +1 -1
- package/dist/handlers/tools/index.d.ts.map +1 -1
- package/dist/handlers/tools/index.js +590 -7
- package/dist/handlers/tools/index.js.map +1 -1
- package/dist/server/McpServer.d.ts +2 -0
- package/dist/server/McpServer.d.ts.map +1 -1
- package/dist/server/McpServer.js +69 -26
- package/dist/server/McpServer.js.map +1 -1
- package/dist/types/index.d.ts +97 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +8 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/security-utils.d.ts +8 -0
- package/dist/utils/security-utils.d.ts.map +1 -1
- package/dist/utils/security-utils.js +32 -0
- package/dist/utils/security-utils.js.map +1 -1
- package/dist/vector/VectorSearchManager.d.ts +2 -1
- package/dist/vector/VectorSearchManager.d.ts.map +1 -1
- package/dist/vector/VectorSearchManager.js +100 -34
- package/dist/vector/VectorSearchManager.js.map +1 -1
- package/docker-compose.yml +46 -37
- package/mcp-config-example.json +0 -2
- package/package.json +12 -11
- package/releases/v4.3.1.md +69 -0
- package/releases/v4.4.0.md +120 -0
- package/server.json +3 -3
- package/src/cli.ts +11 -0
- package/src/constants/ServerInstructions.ts +70 -26
- package/src/constants/icons.ts +7 -0
- package/src/database/SqliteAdapter.ts +162 -32
- package/src/filtering/ToolFilter.ts +7 -1
- package/src/github/GitHubIntegration.ts +588 -8
- package/src/handlers/prompts/index.ts +1 -0
- package/src/handlers/resources/index.ts +318 -12
- package/src/handlers/tools/index.ts +681 -12
- package/src/server/McpServer.ts +79 -37
- package/src/types/index.ts +98 -0
- package/src/utils/logger.ts +10 -1
- package/src/utils/security-utils.ts +35 -0
- package/src/vector/VectorSearchManager.ts +110 -39
- package/tests/constants/icons.test.ts +102 -0
- package/tests/constants/server-instructions.test.ts +549 -0
- package/tests/database/sqlite-adapter.bench.ts +63 -0
- package/tests/database/sqlite-adapter.test.ts +555 -0
- package/tests/filtering/tool-filter.test.ts +266 -0
- package/tests/github/github-integration.test.ts +1024 -0
- package/tests/handlers/github-resource-handlers.test.ts +473 -0
- package/tests/handlers/github-tool-handlers.test.ts +556 -0
- package/tests/handlers/prompt-handlers.test.ts +91 -0
- package/tests/handlers/resource-handlers.test.ts +339 -0
- package/tests/handlers/tool-handlers.test.ts +497 -0
- package/tests/handlers/vector-tool-handlers.test.ts +238 -0
- package/tests/server/mcp-server.bench.ts +55 -0
- package/tests/server/mcp-server.test.ts +675 -0
- package/tests/utils/logger.test.ts +180 -0
- package/tests/utils/mcp-logger.test.ts +212 -0
- package/tests/utils/progress-utils.test.ts +156 -0
- package/tests/utils/security-utils.test.ts +82 -0
- package/tests/vector/vector-search-manager.test.ts +335 -0
- package/tests/vector/vector-search.bench.ts +53 -0
- package/.github/workflows/DOCKER_DEPLOYMENT_SETUP.md +0 -387
- package/.github/workflows/dependabot-auto-merge.yml +0 -42
package/src/server/McpServer.ts
CHANGED
|
@@ -31,14 +31,22 @@ import { getPrompts, getPrompt } from '../handlers/prompts/index.js'
|
|
|
31
31
|
import { generateInstructions } from '../constants/ServerInstructions.js'
|
|
32
32
|
import pkg from '../../package.json' with { type: 'json' }
|
|
33
33
|
|
|
34
|
+
/** Session timeout for stateful HTTP mode (30 minutes) */
|
|
35
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000
|
|
36
|
+
|
|
37
|
+
/** Session timeout sweep interval (5 minutes) */
|
|
38
|
+
const SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1000
|
|
39
|
+
|
|
34
40
|
export interface ServerOptions {
|
|
35
41
|
transport: 'stdio' | 'http'
|
|
36
42
|
port?: number
|
|
43
|
+
host?: string
|
|
37
44
|
dbPath: string
|
|
38
45
|
toolFilter?: string
|
|
39
46
|
defaultProjectNumber?: number
|
|
40
47
|
autoRebuildIndex?: boolean
|
|
41
48
|
statelessHttp?: boolean
|
|
49
|
+
corsOrigin?: string
|
|
42
50
|
}
|
|
43
51
|
|
|
44
52
|
/**
|
|
@@ -107,14 +115,13 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
107
115
|
}
|
|
108
116
|
: undefined
|
|
109
117
|
|
|
118
|
+
// Get all tools once (unfiltered) for both instruction generation and registration
|
|
119
|
+
const allTools = getTools(db, null, vectorManager, github, { defaultProjectNumber })
|
|
120
|
+
const allToolNames = new Set(allTools.map((t) => (t as { name: string }).name))
|
|
121
|
+
|
|
110
122
|
// Generate dynamic instructions based on enabled tools, resources, prompts, and latest entry
|
|
111
123
|
const instructions = generateInstructions(
|
|
112
|
-
filterConfig?.enabledTools ??
|
|
113
|
-
new Set(
|
|
114
|
-
getTools(db, null, vectorManager, github, { defaultProjectNumber }).map(
|
|
115
|
-
(t) => (t as { name: string }).name
|
|
116
|
-
)
|
|
117
|
-
),
|
|
124
|
+
filterConfig?.enabledTools ?? allToolNames,
|
|
118
125
|
resources.map((r) => {
|
|
119
126
|
const res = r as { uri: string; name: string; description?: string }
|
|
120
127
|
return { uri: res.uri, name: res.name, description: res.description }
|
|
@@ -140,8 +147,10 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
140
147
|
}
|
|
141
148
|
)
|
|
142
149
|
|
|
143
|
-
//
|
|
144
|
-
const tools =
|
|
150
|
+
// Apply filter to get the set of tools to register
|
|
151
|
+
const tools = filterConfig
|
|
152
|
+
? getTools(db, filterConfig, vectorManager, github, { defaultProjectNumber })
|
|
153
|
+
: allTools
|
|
145
154
|
for (const tool of tools) {
|
|
146
155
|
const toolDef = tool as {
|
|
147
156
|
name: string
|
|
@@ -394,12 +403,21 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
394
403
|
} else {
|
|
395
404
|
// HTTP transport with SSE support
|
|
396
405
|
const port = options.port ?? 3000
|
|
406
|
+
const host = options.host ?? 'localhost'
|
|
407
|
+
const corsOrigin = options.corsOrigin ?? process.env['MCP_CORS_ORIGIN'] ?? '*'
|
|
397
408
|
const app: Express = express()
|
|
398
409
|
|
|
399
|
-
//
|
|
410
|
+
// Security headers middleware
|
|
411
|
+
app.use((_req: Request, res: Response, next: () => void) => {
|
|
412
|
+
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
413
|
+
res.setHeader('X-Frame-Options', 'DENY')
|
|
414
|
+
next()
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// CORS middleware for browser-based clients (e.g., MCP Inspector)
|
|
418
|
+
// Origin is configurable via --cors-origin flag or MCP_CORS_ORIGIN env var
|
|
400
419
|
app.use((req: Request, res: Response, next: () => void) => {
|
|
401
|
-
|
|
402
|
-
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
420
|
+
res.setHeader('Access-Control-Allow-Origin', corsOrigin)
|
|
403
421
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
|
|
404
422
|
res.setHeader(
|
|
405
423
|
'Access-Control-Allow-Headers',
|
|
@@ -416,22 +434,14 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
416
434
|
next()
|
|
417
435
|
})
|
|
418
436
|
|
|
419
|
-
// JSON body parser
|
|
420
|
-
app.use(express.json())
|
|
437
|
+
// JSON body parser with size limit to prevent memory exhaustion (DoS)
|
|
438
|
+
app.use(express.json({ limit: '1mb' }))
|
|
421
439
|
|
|
422
440
|
// Explicit OPTIONS handler for /mcp - MUST be before other /mcp routes
|
|
423
441
|
// Using app.all to intercept before Express 5's auto-OPTIONS
|
|
424
442
|
app.all('/mcp', (req: Request, res: Response, next: () => void) => {
|
|
425
|
-
//
|
|
426
|
-
|
|
427
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS')
|
|
428
|
-
res.setHeader(
|
|
429
|
-
'Access-Control-Allow-Headers',
|
|
430
|
-
'Content-Type, Accept, mcp-session-id, Last-Event-ID, mcp-protocol-version'
|
|
431
|
-
)
|
|
432
|
-
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id')
|
|
433
|
-
|
|
434
|
-
// For OPTIONS, respond immediately with CORS headers
|
|
443
|
+
// CORS headers are already set by the middleware above.
|
|
444
|
+
// For OPTIONS, respond immediately.
|
|
435
445
|
if (req.method === 'OPTIONS') {
|
|
436
446
|
res.status(204).end()
|
|
437
447
|
return
|
|
@@ -479,11 +489,12 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
479
489
|
})
|
|
480
490
|
|
|
481
491
|
// Start HTTP server
|
|
482
|
-
const httpServer = app.listen(port, () => {
|
|
492
|
+
const httpServer = app.listen(port, host, () => {
|
|
483
493
|
logger.info('MCP server started on HTTP (stateless)', {
|
|
484
494
|
module: 'McpServer',
|
|
485
495
|
port,
|
|
486
|
-
|
|
496
|
+
host,
|
|
497
|
+
endpoint: `http://${host}:${port}/mcp`,
|
|
487
498
|
})
|
|
488
499
|
})
|
|
489
500
|
|
|
@@ -521,8 +532,37 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
521
532
|
// === STATEFUL MODE ===
|
|
522
533
|
// Session-based transport with SSE support for notifications
|
|
523
534
|
|
|
524
|
-
// Session transport storage
|
|
535
|
+
// Session transport storage with last-activity timestamps
|
|
525
536
|
const transports = new Map<string, StreamableHTTPServerTransport>()
|
|
537
|
+
const sessionLastActivity = new Map<string, number>()
|
|
538
|
+
|
|
539
|
+
/** Update the last-activity timestamp for a session */
|
|
540
|
+
const touchSession = (sid: string): void => {
|
|
541
|
+
sessionLastActivity.set(sid, Date.now())
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** Sweep expired sessions (called periodically) */
|
|
545
|
+
const sweepExpiredSessions = (): void => {
|
|
546
|
+
const now = Date.now()
|
|
547
|
+
for (const [sid, lastActivity] of sessionLastActivity) {
|
|
548
|
+
if (now - lastActivity > SESSION_TIMEOUT_MS && transports.has(sid)) {
|
|
549
|
+
logger.info('Expiring idle HTTP session', {
|
|
550
|
+
module: 'McpServer',
|
|
551
|
+
sessionId: sid,
|
|
552
|
+
idleMinutes: Math.round((now - lastActivity) / 60_000),
|
|
553
|
+
})
|
|
554
|
+
const t = transports.get(sid)
|
|
555
|
+
if (t) {
|
|
556
|
+
void t.close()
|
|
557
|
+
}
|
|
558
|
+
transports.delete(sid)
|
|
559
|
+
sessionLastActivity.delete(sid)
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Start session timeout sweep (runs every 5 minutes)
|
|
565
|
+
const sessionSweepTimer = setInterval(sweepExpiredSessions, SESSION_SWEEP_INTERVAL_MS)
|
|
526
566
|
|
|
527
567
|
// POST /mcp - Handle JSON-RPC requests
|
|
528
568
|
app.post('/mcp', (req: Request, res: Response): void => {
|
|
@@ -533,7 +573,8 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
533
573
|
let httpTransport: StreamableHTTPServerTransport | undefined
|
|
534
574
|
|
|
535
575
|
if (sessionId && transports.has(sessionId)) {
|
|
536
|
-
// Reuse existing transport
|
|
576
|
+
// Reuse existing transport and refresh session activity
|
|
577
|
+
touchSession(sessionId)
|
|
537
578
|
httpTransport = transports.get(sessionId)
|
|
538
579
|
} else if (sessionId === undefined && isInitializeRequest(req.body)) {
|
|
539
580
|
// New initialization request - create transport
|
|
@@ -545,6 +586,7 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
545
586
|
sessionId: sid,
|
|
546
587
|
})
|
|
547
588
|
transports.set(sid, newTransport)
|
|
589
|
+
touchSession(sid)
|
|
548
590
|
},
|
|
549
591
|
})
|
|
550
592
|
|
|
@@ -557,6 +599,7 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
557
599
|
sessionId: sid,
|
|
558
600
|
})
|
|
559
601
|
transports.delete(sid)
|
|
602
|
+
sessionLastActivity.delete(sid)
|
|
560
603
|
}
|
|
561
604
|
}
|
|
562
605
|
|
|
@@ -614,6 +657,9 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
614
657
|
return
|
|
615
658
|
}
|
|
616
659
|
|
|
660
|
+
// Refresh session activity on SSE reconnect
|
|
661
|
+
touchSession(sessionId)
|
|
662
|
+
|
|
617
663
|
const lastEventId = req.headers['last-event-id']
|
|
618
664
|
if (lastEventId !== undefined) {
|
|
619
665
|
logger.debug('Client reconnecting with Last-Event-ID', {
|
|
@@ -656,11 +702,12 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
656
702
|
})
|
|
657
703
|
|
|
658
704
|
// Start HTTP server
|
|
659
|
-
const httpServer = app.listen(port, () => {
|
|
705
|
+
const httpServer = app.listen(port, host, () => {
|
|
660
706
|
logger.info('MCP server started on HTTP (stateful)', {
|
|
661
707
|
module: 'McpServer',
|
|
662
708
|
port,
|
|
663
|
-
|
|
709
|
+
host,
|
|
710
|
+
endpoint: `http://${host}:${port}/mcp`,
|
|
664
711
|
})
|
|
665
712
|
})
|
|
666
713
|
|
|
@@ -689,6 +736,8 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
689
736
|
}
|
|
690
737
|
}
|
|
691
738
|
transports.clear()
|
|
739
|
+
sessionLastActivity.clear()
|
|
740
|
+
clearInterval(sessionSweepTimer)
|
|
692
741
|
|
|
693
742
|
httpServer.close()
|
|
694
743
|
db.close()
|
|
@@ -697,14 +746,7 @@ export async function createServer(options: ServerOptions): Promise<void> {
|
|
|
697
746
|
})()
|
|
698
747
|
})
|
|
699
748
|
|
|
700
|
-
//
|
|
701
|
-
// setInterval keeps the event loop active and prevents exit
|
|
702
|
-
setInterval(
|
|
703
|
-
() => {
|
|
704
|
-
// Heartbeat - keeps event loop active
|
|
705
|
-
},
|
|
706
|
-
1000 * 60 * 60
|
|
707
|
-
) // 1 hour interval (just needs to exist)
|
|
749
|
+
// sessionSweepTimer keeps the event loop active (no additional heartbeat needed)
|
|
708
750
|
}
|
|
709
751
|
}
|
|
710
752
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -266,6 +266,17 @@ export interface JournalEntry {
|
|
|
266
266
|
autoContext: string | null
|
|
267
267
|
deletedAt: string | null
|
|
268
268
|
tags: string[]
|
|
269
|
+
// GitHub integration fields
|
|
270
|
+
projectNumber?: number | null
|
|
271
|
+
projectOwner?: string | null
|
|
272
|
+
issueNumber?: number | null
|
|
273
|
+
issueUrl?: string | null
|
|
274
|
+
prNumber?: number | null
|
|
275
|
+
prUrl?: string | null
|
|
276
|
+
prStatus?: string | null
|
|
277
|
+
workflowRunId?: number | null
|
|
278
|
+
workflowName?: string | null
|
|
279
|
+
workflowStatus?: string | null
|
|
269
280
|
}
|
|
270
281
|
|
|
271
282
|
/**
|
|
@@ -298,6 +309,30 @@ export interface Embedding {
|
|
|
298
309
|
modelName: string
|
|
299
310
|
}
|
|
300
311
|
|
|
312
|
+
/**
|
|
313
|
+
* Importance scoring breakdown showing weighted component contributions
|
|
314
|
+
*/
|
|
315
|
+
export interface ImportanceBreakdown {
|
|
316
|
+
/** Significance type contribution (weight: 0.30) */
|
|
317
|
+
significance: number
|
|
318
|
+
/** Relationship count contribution (weight: 0.35) */
|
|
319
|
+
relationships: number
|
|
320
|
+
/** Causal relationship contribution (weight: 0.20) */
|
|
321
|
+
causal: number
|
|
322
|
+
/** Recency decay contribution (weight: 0.15) */
|
|
323
|
+
recency: number
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Importance calculation result with total score and component breakdown
|
|
328
|
+
*/
|
|
329
|
+
export interface ImportanceResult {
|
|
330
|
+
/** Total importance score (0.0-1.0) */
|
|
331
|
+
score: number
|
|
332
|
+
/** Weighted component contributions */
|
|
333
|
+
breakdown: ImportanceBreakdown
|
|
334
|
+
}
|
|
335
|
+
|
|
301
336
|
// ============================================================================
|
|
302
337
|
// GitHub Integration Types
|
|
303
338
|
// ============================================================================
|
|
@@ -320,6 +355,7 @@ export interface GitHubIssue {
|
|
|
320
355
|
title: string
|
|
321
356
|
url: string
|
|
322
357
|
state: 'OPEN' | 'CLOSED'
|
|
358
|
+
milestone?: { number: number; title: string } | null
|
|
323
359
|
}
|
|
324
360
|
|
|
325
361
|
/**
|
|
@@ -332,6 +368,23 @@ export interface GitHubPullRequest {
|
|
|
332
368
|
state: 'OPEN' | 'CLOSED' | 'MERGED'
|
|
333
369
|
}
|
|
334
370
|
|
|
371
|
+
/**
|
|
372
|
+
* GitHub milestone information
|
|
373
|
+
*/
|
|
374
|
+
export interface GitHubMilestone {
|
|
375
|
+
number: number
|
|
376
|
+
title: string
|
|
377
|
+
description: string | null
|
|
378
|
+
state: 'open' | 'closed'
|
|
379
|
+
url: string
|
|
380
|
+
dueOn: string | null
|
|
381
|
+
openIssues: number
|
|
382
|
+
closedIssues: number
|
|
383
|
+
createdAt: string
|
|
384
|
+
updatedAt: string
|
|
385
|
+
creator: string | null
|
|
386
|
+
}
|
|
387
|
+
|
|
335
388
|
/**
|
|
336
389
|
* GitHub workflow run information
|
|
337
390
|
*/
|
|
@@ -359,6 +412,51 @@ export interface ProjectContext {
|
|
|
359
412
|
issues: GitHubIssue[]
|
|
360
413
|
pullRequests: GitHubPullRequest[]
|
|
361
414
|
workflowRuns: GitHubWorkflowRun[]
|
|
415
|
+
milestones: GitHubMilestone[]
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ============================================================================
|
|
419
|
+
// Repository Insights/Traffic Types
|
|
420
|
+
// ============================================================================
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Repository statistics (stars, forks, watchers)
|
|
424
|
+
*/
|
|
425
|
+
export interface RepoStats {
|
|
426
|
+
stars: number
|
|
427
|
+
forks: number
|
|
428
|
+
watchers: number
|
|
429
|
+
openIssues: number
|
|
430
|
+
size: number // KB
|
|
431
|
+
defaultBranch: string
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Aggregated traffic data (14-day rolling)
|
|
436
|
+
*/
|
|
437
|
+
export interface TrafficData {
|
|
438
|
+
clones: { total: number; unique: number; dailyAvg: number }
|
|
439
|
+
views: { total: number; unique: number; dailyAvg: number }
|
|
440
|
+
period: string // e.g. "14 days"
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Traffic referrer source
|
|
445
|
+
*/
|
|
446
|
+
export interface TrafficReferrer {
|
|
447
|
+
referrer: string
|
|
448
|
+
count: number
|
|
449
|
+
uniques: number
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Popular repository path
|
|
454
|
+
*/
|
|
455
|
+
export interface PopularPath {
|
|
456
|
+
path: string
|
|
457
|
+
title: string
|
|
458
|
+
count: number
|
|
459
|
+
uniques: number
|
|
362
460
|
}
|
|
363
461
|
|
|
364
462
|
// ============================================================================
|
package/src/utils/logger.ts
CHANGED
|
@@ -3,8 +3,11 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Centralized logging to stderr only (stdout reserved for MCP protocol).
|
|
5
5
|
* Follows RFC 5424 severity levels.
|
|
6
|
+
* Automatically sanitizes error fields to prevent token leakage.
|
|
6
7
|
*/
|
|
7
8
|
|
|
9
|
+
import { sanitizeErrorForLogging } from './security-utils.js'
|
|
10
|
+
|
|
8
11
|
type LogLevel = 'debug' | 'info' | 'notice' | 'warning' | 'error' | 'critical'
|
|
9
12
|
|
|
10
13
|
interface LogContext {
|
|
@@ -57,7 +60,13 @@ class Logger {
|
|
|
57
60
|
private log(level: LogLevel, message: string, context?: LogContext): void {
|
|
58
61
|
if (!this.shouldLog(level)) return
|
|
59
62
|
|
|
60
|
-
|
|
63
|
+
// Sanitize error fields to prevent token leakage in logs
|
|
64
|
+
let sanitizedContext = context
|
|
65
|
+
if (context?.['error'] != null && typeof context['error'] === 'string') {
|
|
66
|
+
sanitizedContext = { ...context, error: sanitizeErrorForLogging(context['error']) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const formatted = this.formatMessage(level, message, sanitizedContext)
|
|
61
70
|
|
|
62
71
|
// Always write to stderr (stdout is reserved for MCP protocol)
|
|
63
72
|
console.error(formatted)
|
|
@@ -168,3 +168,38 @@ export function assertNoPathTraversal(filename: string): void {
|
|
|
168
168
|
throw new PathTraversalError(filename)
|
|
169
169
|
}
|
|
170
170
|
}
|
|
171
|
+
|
|
172
|
+
// ============================================================================
|
|
173
|
+
// Error Message Sanitization
|
|
174
|
+
// ============================================================================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Patterns that may contain sensitive tokens in error messages.
|
|
178
|
+
* Used to scrub error output before logging.
|
|
179
|
+
*/
|
|
180
|
+
const TOKEN_PATTERNS = [
|
|
181
|
+
// GitHub personal access tokens (classic and fine-grained)
|
|
182
|
+
/ghp_[A-Za-z0-9_]{36,}/g,
|
|
183
|
+
/github_pat_[A-Za-z0-9_]{82,}/g,
|
|
184
|
+
// Authorization headers in error dumps
|
|
185
|
+
/Authorization:\s*(?:token|Bearer)\s+\S+/gi,
|
|
186
|
+
// Generic Bearer tokens
|
|
187
|
+
/Bearer\s+[A-Za-z0-9._\-~+/]+=*/gi,
|
|
188
|
+
] as const
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sanitizes an error message by replacing any detected tokens with '[REDACTED]'.
|
|
192
|
+
* This is a defense-in-depth measure for error logging paths.
|
|
193
|
+
*
|
|
194
|
+
* @param message - The error message to sanitize
|
|
195
|
+
* @returns The sanitized message with tokens replaced
|
|
196
|
+
*/
|
|
197
|
+
export function sanitizeErrorForLogging(message: string): string {
|
|
198
|
+
let sanitized = message
|
|
199
|
+
for (const pattern of TOKEN_PATTERNS) {
|
|
200
|
+
// Reset lastIndex for global regex patterns
|
|
201
|
+
pattern.lastIndex = 0
|
|
202
|
+
sanitized = sanitized.replace(pattern, '[REDACTED]')
|
|
203
|
+
}
|
|
204
|
+
return sanitized
|
|
205
|
+
}
|
|
@@ -20,6 +20,12 @@ const DEFAULT_MODEL = 'Xenova/all-MiniLM-L6-v2'
|
|
|
20
20
|
/** Embedding dimensions for all-MiniLM-L6-v2 */
|
|
21
21
|
const EMBEDDING_DIMENSIONS = 384
|
|
22
22
|
|
|
23
|
+
/** Number of entries to embed concurrently during rebuild */
|
|
24
|
+
const REBUILD_BATCH_SIZE = 5
|
|
25
|
+
|
|
26
|
+
/** Number of entries to fetch per page during rebuild */
|
|
27
|
+
const REBUILD_PAGE_SIZE = 200
|
|
28
|
+
|
|
23
29
|
/** Search result with similarity score */
|
|
24
30
|
export interface SemanticSearchResult {
|
|
25
31
|
entryId: number
|
|
@@ -225,7 +231,8 @@ export class VectorSearchManager {
|
|
|
225
231
|
}
|
|
226
232
|
|
|
227
233
|
/**
|
|
228
|
-
* Rebuild index from database entries
|
|
234
|
+
* Rebuild index from database entries.
|
|
235
|
+
* Uses paginated fetching and parallel batch embedding for performance.
|
|
229
236
|
* @param db - Database adapter
|
|
230
237
|
* @param progress - Optional progress context for notifications
|
|
231
238
|
*/
|
|
@@ -240,66 +247,130 @@ export class VectorSearchManager {
|
|
|
240
247
|
|
|
241
248
|
logger.info('Rebuilding vector index from database...', { module: 'VectorSearch' })
|
|
242
249
|
|
|
243
|
-
// Step 1: Get
|
|
244
|
-
const
|
|
245
|
-
const dbIds = new Set(
|
|
250
|
+
// Step 1: Get total entry count and build ID set for orphan detection
|
|
251
|
+
const totalEntries = db.getActiveEntryCount()
|
|
252
|
+
const dbIds = new Set<string>()
|
|
253
|
+
|
|
254
|
+
// Collect all active entry IDs via pagination (avoids loading all content at once)
|
|
255
|
+
for (let offset = 0; offset < totalEntries; offset += REBUILD_PAGE_SIZE) {
|
|
256
|
+
const page = db.getEntriesPage(offset, REBUILD_PAGE_SIZE)
|
|
257
|
+
for (const entry of page) {
|
|
258
|
+
dbIds.add(String(entry.id))
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Step 2: Clean up existing index items
|
|
263
|
+
// If the index is corrupted (e.g., from a process kill), recreate it
|
|
264
|
+
try {
|
|
265
|
+
const indexItems = await this.index.listItems()
|
|
266
|
+
let orphansRemoved = 0
|
|
267
|
+
for (const item of indexItems) {
|
|
268
|
+
if (!dbIds.has(item.id)) {
|
|
269
|
+
try {
|
|
270
|
+
await this.index.deleteItem(item.id)
|
|
271
|
+
orphansRemoved++
|
|
272
|
+
} catch {
|
|
273
|
+
// Ignore errors during cleanup
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (orphansRemoved > 0) {
|
|
278
|
+
logger.info(`Cleaned up ${String(orphansRemoved)} orphaned vector entries`, {
|
|
279
|
+
module: 'VectorSearch',
|
|
280
|
+
})
|
|
281
|
+
}
|
|
246
282
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
for (const item of indexItems) {
|
|
252
|
-
if (!dbIds.has(item.id)) {
|
|
283
|
+
// Step 3: Delete all remaining items to prepare for clean re-index
|
|
284
|
+
// This avoids the double-delete overhead of calling addEntry (upsert) per item
|
|
285
|
+
const remainingItems = await this.index.listItems()
|
|
286
|
+
for (const item of remainingItems) {
|
|
253
287
|
try {
|
|
254
288
|
await this.index.deleteItem(item.id)
|
|
255
|
-
orphansRemoved++
|
|
256
289
|
} catch {
|
|
257
|
-
// Ignore
|
|
290
|
+
// Ignore
|
|
258
291
|
}
|
|
259
292
|
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
logger.
|
|
293
|
+
} catch (indexError) {
|
|
294
|
+
// Index files are corrupted — recreate from scratch
|
|
295
|
+
logger.warning('Vector index corrupted, recreating...', {
|
|
263
296
|
module: 'VectorSearch',
|
|
297
|
+
error: indexError instanceof Error ? indexError.message : String(indexError),
|
|
264
298
|
})
|
|
299
|
+
// Delete and recreate the vectra index directory
|
|
300
|
+
if (fs.existsSync(this.indexPath)) {
|
|
301
|
+
fs.rmSync(this.indexPath, { recursive: true, force: true })
|
|
302
|
+
fs.mkdirSync(this.indexPath, { recursive: true })
|
|
303
|
+
}
|
|
304
|
+
this.index = new LocalIndex(this.indexPath)
|
|
305
|
+
await this.index.createIndex()
|
|
306
|
+
logger.info('Recreated vector index after corruption', { module: 'VectorSearch' })
|
|
265
307
|
}
|
|
266
308
|
|
|
267
|
-
// Step
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
await sendProgress(progress, 0, total, 'Starting vector index rebuild...')
|
|
309
|
+
// Step 4: Re-index all entries using paginated fetch
|
|
310
|
+
// Embeddings are generated in parallel batches (CPU-bound, safe),
|
|
311
|
+
// but vectra insertions are sequential (file I/O, not concurrency-safe)
|
|
312
|
+
await sendProgress(progress, 0, totalEntries, 'Starting vector index rebuild...')
|
|
272
313
|
|
|
273
314
|
let indexed = 0
|
|
274
|
-
for (
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
315
|
+
for (let offset = 0; offset < totalEntries; offset += REBUILD_PAGE_SIZE) {
|
|
316
|
+
const page = db.getEntriesPage(offset, REBUILD_PAGE_SIZE)
|
|
317
|
+
|
|
318
|
+
// Generate embeddings in parallel batches
|
|
319
|
+
for (let i = 0; i < page.length; i += REBUILD_BATCH_SIZE) {
|
|
320
|
+
const batch = page.slice(i, i + REBUILD_BATCH_SIZE)
|
|
321
|
+
|
|
322
|
+
// Parallel embedding generation
|
|
323
|
+
const embeddings = await Promise.all(
|
|
324
|
+
batch.map(async (entry) => {
|
|
325
|
+
try {
|
|
326
|
+
return { entry, embedding: await this.generateEmbedding(entry.content) }
|
|
327
|
+
} catch {
|
|
328
|
+
return { entry, embedding: null }
|
|
329
|
+
}
|
|
330
|
+
})
|
|
331
|
+
)
|
|
281
332
|
|
|
282
|
-
|
|
283
|
-
|
|
333
|
+
// Sequential vectra insertion (file I/O not concurrency-safe)
|
|
334
|
+
for (const { entry, embedding } of embeddings) {
|
|
335
|
+
if (embedding !== null) {
|
|
336
|
+
try {
|
|
337
|
+
await this.index.insertItem({
|
|
338
|
+
id: String(entry.id),
|
|
339
|
+
vector: embedding,
|
|
340
|
+
metadata: {
|
|
341
|
+
entryId: entry.id,
|
|
342
|
+
contentPreview: entry.content.slice(0, 100),
|
|
343
|
+
},
|
|
344
|
+
})
|
|
345
|
+
indexed++
|
|
346
|
+
} catch (error) {
|
|
347
|
+
logger.error('Failed to insert entry into vector index', {
|
|
348
|
+
module: 'VectorSearch',
|
|
349
|
+
entityId: entry.id,
|
|
350
|
+
error: error instanceof Error ? error.message : String(error),
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
284
355
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
356
|
+
// Report progress every 10 entries to avoid flooding
|
|
357
|
+
if (indexed % 10 === 0 || indexed === totalEntries) {
|
|
358
|
+
await sendProgress(
|
|
359
|
+
progress,
|
|
360
|
+
indexed,
|
|
361
|
+
totalEntries,
|
|
362
|
+
`Indexed ${String(indexed)} of ${String(totalEntries)} entries`
|
|
363
|
+
)
|
|
364
|
+
}
|
|
293
365
|
}
|
|
294
366
|
}
|
|
295
367
|
|
|
296
368
|
// Force index to refresh by re-listing items
|
|
297
369
|
// This ensures the internal query structures are updated and ready for search
|
|
298
|
-
// The 100ms delay was insufficient because it didn't refresh the internal state
|
|
299
370
|
await this.index.listItems()
|
|
300
371
|
|
|
301
372
|
// Final progress
|
|
302
|
-
await sendProgress(progress, indexed,
|
|
373
|
+
await sendProgress(progress, indexed, totalEntries, 'Vector index rebuild complete')
|
|
303
374
|
|
|
304
375
|
logger.info(`Rebuilt vector index with ${String(indexed)} entries`, {
|
|
305
376
|
module: 'VectorSearch',
|