mcp-astgl-knowledge 1.0.2 → 1.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.
Files changed (46) hide show
  1. package/README.md +146 -38
  2. package/data/knowledge.db +0 -0
  3. package/dist/alerts.d.ts +22 -0
  4. package/dist/alerts.js +433 -0
  5. package/dist/alerts.js.map +1 -0
  6. package/dist/citation-test.d.ts +14 -0
  7. package/dist/citation-test.js +298 -0
  8. package/dist/citation-test.js.map +1 -0
  9. package/dist/daily-report.d.ts +15 -0
  10. package/dist/daily-report.js +441 -0
  11. package/dist/daily-report.js.map +1 -0
  12. package/dist/discover.js +3 -1
  13. package/dist/discover.js.map +1 -1
  14. package/dist/freshness.d.ts +20 -0
  15. package/dist/freshness.js +508 -0
  16. package/dist/freshness.js.map +1 -0
  17. package/dist/index.d.ts +6 -1
  18. package/dist/index.js +253 -14
  19. package/dist/index.js.map +1 -1
  20. package/dist/ingest-projects.d.ts +16 -0
  21. package/dist/ingest-projects.js +196 -0
  22. package/dist/ingest-projects.js.map +1 -0
  23. package/dist/knowledge-db.d.ts +13 -0
  24. package/dist/knowledge-db.js +156 -0
  25. package/dist/knowledge-db.js.map +1 -0
  26. package/dist/pipeline.d.ts +12 -0
  27. package/dist/pipeline.js +83 -0
  28. package/dist/pipeline.js.map +1 -0
  29. package/dist/query-log.d.ts +15 -0
  30. package/dist/query-log.js +93 -0
  31. package/dist/query-log.js.map +1 -0
  32. package/dist/rate-limit.d.ts +34 -0
  33. package/dist/rate-limit.js +206 -0
  34. package/dist/rate-limit.js.map +1 -0
  35. package/dist/related-articles.d.ts +15 -0
  36. package/dist/related-articles.js +217 -0
  37. package/dist/related-articles.js.map +1 -0
  38. package/dist/search.d.ts +13 -4
  39. package/dist/search.js +274 -39
  40. package/dist/search.js.map +1 -1
  41. package/dist/structure.d.ts +11 -0
  42. package/dist/structure.js +451 -0
  43. package/dist/structure.js.map +1 -0
  44. package/dist/types.d.ts +65 -0
  45. package/dist/types.js.map +1 -1
  46. package/package.json +10 -2
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Knowledge database module for incremental updates.
3
+ * Provides UPSERT operations against knowledge.db without
4
+ * destroying existing data (unlike ingest.ts's DROP/CREATE).
5
+ */
6
+ import { join } from "path";
7
+ import { existsSync, mkdirSync } from "fs";
8
+ import Database from "better-sqlite3";
9
+ import * as sqliteVec from "sqlite-vec";
10
+ const DB_PATH = join(import.meta.dirname, "..", "data", "knowledge.db");
11
+ let db = null;
12
+ // WHAT: Open knowledge.db and run idempotent schema migrations
13
+ // WHY: Adds columns needed by the structuring pipeline without breaking existing data
14
+ export function initKnowledgeDb() {
15
+ const dataDir = join(import.meta.dirname, "..", "data");
16
+ if (!existsSync(dataDir))
17
+ mkdirSync(dataDir, { recursive: true });
18
+ if (!existsSync(DB_PATH)) {
19
+ throw new Error(`Knowledge database not found at ${DB_PATH}. Run 'npm run ingest' first to create the base schema.`);
20
+ }
21
+ db = new Database(DB_PATH);
22
+ sqliteVec.load(db);
23
+ runMigrations(db);
24
+ return db;
25
+ }
26
+ // WHAT: Add new columns and tables needed by the structuring pipeline
27
+ // WHY: ALTER TABLE with try/catch is idempotent — safe to re-run after ingest.ts rebuilds
28
+ function runMigrations(database) {
29
+ const alterColumns = [
30
+ "ALTER TABLE articles ADD COLUMN source_url TEXT",
31
+ "ALTER TABLE articles ADD COLUMN content_type TEXT DEFAULT 'article'",
32
+ "ALTER TABLE articles ADD COLUMN json_ld TEXT",
33
+ "ALTER TABLE articles ADD COLUMN processed_at TEXT",
34
+ "ALTER TABLE articles ADD COLUMN pub_date TEXT",
35
+ "ALTER TABLE articles ADD COLUMN last_reviewed_at TEXT",
36
+ "ALTER TABLE articles ADD COLUMN freshness_status TEXT DEFAULT 'current'",
37
+ ];
38
+ for (const sql of alterColumns) {
39
+ try {
40
+ database.exec(sql);
41
+ }
42
+ catch {
43
+ // Column already exists — expected after first run
44
+ }
45
+ }
46
+ database.exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_articles_url ON articles(url)");
47
+ database.exec(`
48
+ CREATE TABLE IF NOT EXISTS article_qa (
49
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
50
+ article_url TEXT NOT NULL,
51
+ question TEXT NOT NULL,
52
+ answer TEXT NOT NULL,
53
+ UNIQUE(article_url, question)
54
+ )
55
+ `);
56
+ // WHAT: Track ecosystem versions for freshness detection
57
+ // WHY: Comparing current vs previous version detects when articles may be outdated
58
+ database.exec(`
59
+ CREATE TABLE IF NOT EXISTS ecosystem_snapshots (
60
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
61
+ check_type TEXT NOT NULL,
62
+ package_name TEXT NOT NULL,
63
+ current_version TEXT NOT NULL,
64
+ previous_version TEXT,
65
+ checked_at TEXT NOT NULL,
66
+ UNIQUE(check_type, package_name)
67
+ )
68
+ `);
69
+ }
70
+ // WHAT: Insert or replace an article in knowledge.db
71
+ // WHY: Keyed on url so discovered content can coexist with local articles
72
+ export function upsertArticle(database, article) {
73
+ const existing = database
74
+ .prepare("SELECT id FROM articles WHERE url = ?")
75
+ .get(article.url);
76
+ if (existing) {
77
+ database
78
+ .prepare(`UPDATE articles SET title = ?, description = ?, slug = ?,
79
+ source_url = ?, content_type = ?, json_ld = ?, processed_at = ?,
80
+ pub_date = COALESCE(?, pub_date)
81
+ WHERE id = ?`)
82
+ .run(article.title, article.description, article.slug, article.sourceUrl, article.contentType, article.jsonLd, article.processedAt, article.pubDate || null, existing.id);
83
+ }
84
+ else {
85
+ database
86
+ .prepare(`INSERT INTO articles (title, description, url, slug, source_url, content_type, json_ld, processed_at, pub_date)
87
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
88
+ .run(article.title, article.description, article.url, article.slug, article.sourceUrl, article.contentType, article.jsonLd, article.processedAt, article.pubDate || null);
89
+ }
90
+ }
91
+ // WHAT: Update freshness status for an article
92
+ // WHY: Freshness checker sets status without re-processing the full article
93
+ export function updateFreshnessStatus(database, articleUrl, status, reviewedAt) {
94
+ if (reviewedAt) {
95
+ database
96
+ .prepare("UPDATE articles SET freshness_status = ?, last_reviewed_at = ? WHERE url = ?")
97
+ .run(status, reviewedAt, articleUrl);
98
+ }
99
+ else {
100
+ database
101
+ .prepare("UPDATE articles SET freshness_status = ? WHERE url = ?")
102
+ .run(status, articleUrl);
103
+ }
104
+ }
105
+ // WHAT: Delete existing chunks + vectors for an article, insert new ones
106
+ // WHY: sqlite-vec doesn't support INSERT OR REPLACE — delete-then-insert in a transaction
107
+ export function replaceChunksForArticle(database, articleUrl, chunks, embeddings) {
108
+ const replaceAll = database.transaction(() => {
109
+ // Find existing chunk IDs for this article
110
+ const existingChunks = database
111
+ .prepare("SELECT id FROM chunks WHERE article_url = ?")
112
+ .all(articleUrl);
113
+ // Delete vectors for those chunks
114
+ const deleteVec = database.prepare("DELETE FROM vec_chunks WHERE chunk_id = ?");
115
+ for (const chunk of existingChunks) {
116
+ deleteVec.run(chunk.id);
117
+ }
118
+ // Delete the chunks themselves
119
+ database
120
+ .prepare("DELETE FROM chunks WHERE article_url = ?")
121
+ .run(articleUrl);
122
+ // Insert new chunks and vectors
123
+ const insertChunk = database.prepare(`INSERT INTO chunks (article_title, article_url, article_order, section_heading, chunk_type, content)
124
+ VALUES (?, ?, ?, ?, ?, ?)`);
125
+ const insertVec = database.prepare("INSERT INTO vec_chunks (chunk_id, embedding) VALUES (?, ?)");
126
+ for (let i = 0; i < chunks.length; i++) {
127
+ const chunk = chunks[i];
128
+ const result = insertChunk.run(chunk.articleTitle, chunk.articleUrl, chunk.articleOrder, chunk.sectionHeading, chunk.chunkType, chunk.content);
129
+ const chunkId = result.lastInsertRowid;
130
+ insertVec.run(BigInt(chunkId), new Float32Array(embeddings[i]));
131
+ }
132
+ });
133
+ replaceAll();
134
+ }
135
+ // WHAT: Insert extracted Q&A pairs for an article
136
+ // WHY: Normalizes Q&A data for potential future querying
137
+ export function upsertQaPairs(database, articleUrl, pairs) {
138
+ // Clear existing pairs for this article
139
+ database
140
+ .prepare("DELETE FROM article_qa WHERE article_url = ?")
141
+ .run(articleUrl);
142
+ const insert = database.prepare("INSERT OR IGNORE INTO article_qa (article_url, question, answer) VALUES (?, ?, ?)");
143
+ const insertAll = database.transaction(() => {
144
+ for (const pair of pairs) {
145
+ insert.run(articleUrl, pair.question, pair.answer);
146
+ }
147
+ });
148
+ insertAll();
149
+ }
150
+ export function closeKnowledgeDb() {
151
+ if (db) {
152
+ db.close();
153
+ db = null;
154
+ }
155
+ }
156
+ //# sourceMappingURL=knowledge-db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"knowledge-db.js","sourceRoot":"","sources":["../src/knowledge-db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Unified content pipeline: Discovery → Structuring → Indexed.
4
+ *
5
+ * WHAT: Runs discover.ts then structure.ts in sequence with smart skipping
6
+ * WHY: Single entry point for the full publish → queryable pipeline,
7
+ * skips structuring when no new content saves LLM compute
8
+ *
9
+ * Usage: npm run pipeline
10
+ * Designed to run as a single OpenClaw cron job (every 6 hours).
11
+ */
12
+ export {};
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Unified content pipeline: Discovery → Structuring → Indexed.
4
+ *
5
+ * WHAT: Runs discover.ts then structure.ts in sequence with smart skipping
6
+ * WHY: Single entry point for the full publish → queryable pipeline,
7
+ * skips structuring when no new content saves LLM compute
8
+ *
9
+ * Usage: npm run pipeline
10
+ * Designed to run as a single OpenClaw cron job (every 6 hours).
11
+ */
12
+ import { execSync } from "child_process";
13
+ import { join } from "path";
14
+ const PROJECT_DIR = join(import.meta.dirname, "..");
15
+ // WHAT: Run a script and capture its JSON stdout
16
+ // WHY: Each step is its own process — isolates DB handles and crashes
17
+ function runStep(script) {
18
+ return execSync(`tsx ${script}`, {
19
+ cwd: PROJECT_DIR,
20
+ encoding: "utf-8",
21
+ // WHAT: Pipe stdout (JSON) back, let stderr flow to console
22
+ // WHY: stderr has progress logs, stdout has the structured summary
23
+ stdio: ["pipe", "pipe", "inherit"],
24
+ timeout: 300_000, // 5 min per step
25
+ });
26
+ }
27
+ async function main() {
28
+ const pipelineStart = performance.now();
29
+ console.error("=== Content Pipeline Started ===\n");
30
+ const summary = {
31
+ timestamp: new Date().toISOString(),
32
+ discovery: { error: "not run" },
33
+ structuring: { skipped: true, reason: "not run" },
34
+ duration_ms: 0,
35
+ };
36
+ // --- Step 1: Discovery ---
37
+ console.error("--- Step 1: Content Discovery ---");
38
+ let newItems = 0;
39
+ try {
40
+ const discoveryOutput = runStep("src/discover.ts");
41
+ const discoveryResult = JSON.parse(discoveryOutput);
42
+ summary.discovery = discoveryResult;
43
+ newItems = discoveryResult.totals.new;
44
+ console.error(`\nDiscovery complete: ${newItems} new, ${discoveryResult.totals.updated} updated, ${discoveryResult.totals.skipped} skipped\n`);
45
+ }
46
+ catch (err) {
47
+ const message = err instanceof Error ? err.message : String(err);
48
+ console.error(`\nDiscovery FAILED: ${message}\n`);
49
+ summary.discovery = { error: message };
50
+ }
51
+ // --- Step 2: Structuring (conditional) ---
52
+ if (newItems > 0) {
53
+ console.error("--- Step 2: Content Structuring ---");
54
+ try {
55
+ const structuringOutput = runStep("src/structure.ts");
56
+ const structuringResult = JSON.parse(structuringOutput);
57
+ summary.structuring = structuringResult;
58
+ console.error(`\nStructuring complete: ${structuringResult.processed} processed, ${structuringResult.failed} failed\n`);
59
+ }
60
+ catch (err) {
61
+ const message = err instanceof Error ? err.message : String(err);
62
+ console.error(`\nStructuring FAILED: ${message}\n`);
63
+ summary.structuring = { error: message };
64
+ }
65
+ }
66
+ else if ("error" in summary.discovery) {
67
+ console.error("--- Step 2: Skipped (discovery failed) ---\n");
68
+ summary.structuring = { skipped: true, reason: "discovery failed" };
69
+ }
70
+ else {
71
+ console.error("--- Step 2: Skipped (no new content) ---\n");
72
+ summary.structuring = { skipped: true, reason: "no new content discovered" };
73
+ }
74
+ summary.duration_ms = Math.round(performance.now() - pipelineStart);
75
+ console.error(`=== Content Pipeline Complete (${(summary.duration_ms / 1000).toFixed(1)}s) ===`);
76
+ // JSON summary to stdout for OpenClaw logging
77
+ console.log(JSON.stringify(summary, null, 2));
78
+ }
79
+ main().catch((err) => {
80
+ console.error("Pipeline fatal error:", err);
81
+ process.exit(1);
82
+ });
83
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline.js","sourceRoot":"","sources":["../src/pipeline.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Query logging module for MCP analytics.
3
+ *
4
+ * WHAT: Logs every MCP tool invocation with timing, params, and cited content
5
+ * WHY: Enables analytics on what users ask, which content gets cited, and response quality
6
+ *
7
+ * Performance:
8
+ * Entries are buffered in memory and flushed to SQLite in batches
9
+ * (every 5 seconds or when buffer hits 20 entries) to avoid blocking responses.
10
+ */
11
+ import Database from "better-sqlite3";
12
+ import type { QueryLogEntry } from "./types.js";
13
+ export declare function initQueryLog(): InstanceType<typeof Database>;
14
+ export declare function logQuery(entry: QueryLogEntry): void;
15
+ export declare function closeQueryLog(): void;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Query logging module for MCP analytics.
3
+ *
4
+ * WHAT: Logs every MCP tool invocation with timing, params, and cited content
5
+ * WHY: Enables analytics on what users ask, which content gets cited, and response quality
6
+ *
7
+ * Performance:
8
+ * Entries are buffered in memory and flushed to SQLite in batches
9
+ * (every 5 seconds or when buffer hits 20 entries) to avoid blocking responses.
10
+ */
11
+ import { join } from "path";
12
+ import { existsSync, mkdirSync } from "fs";
13
+ import Database from "better-sqlite3";
14
+ const LOG_DB_PATH = join(import.meta.dirname, "..", "data", "query-log.db");
15
+ const FLUSH_INTERVAL_MS = 5_000;
16
+ const FLUSH_BATCH_SIZE = 20;
17
+ let logDb = null;
18
+ let buffer = [];
19
+ let flushTimer = null;
20
+ export function initQueryLog() {
21
+ const dataDir = join(import.meta.dirname, "..", "data");
22
+ if (!existsSync(dataDir))
23
+ mkdirSync(dataDir, { recursive: true });
24
+ logDb = new Database(LOG_DB_PATH);
25
+ logDb.exec(`
26
+ CREATE TABLE IF NOT EXISTS query_log (
27
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
28
+ timestamp TEXT NOT NULL,
29
+ client_id TEXT NOT NULL,
30
+ tool_name TEXT NOT NULL,
31
+ query_params TEXT NOT NULL,
32
+ content_cited TEXT,
33
+ response_time_ms INTEGER NOT NULL,
34
+ confidence_score REAL
35
+ )
36
+ `);
37
+ // WHAT: Index on timestamp + tool_name for common analytics queries
38
+ // WHY: Most reports filter by date range and/or tool
39
+ logDb.exec("CREATE INDEX IF NOT EXISTS idx_query_log_ts ON query_log(timestamp)");
40
+ logDb.exec("CREATE INDEX IF NOT EXISTS idx_query_log_tool ON query_log(tool_name)");
41
+ // WHAT: Start periodic flush timer
42
+ // WHY: Ensures buffered entries are written even during low-traffic periods
43
+ flushTimer = setInterval(flushBuffer, FLUSH_INTERVAL_MS);
44
+ flushTimer.unref(); // Don't keep process alive just for logging
45
+ // WHAT: Flush on process exit
46
+ // WHY: Don't lose buffered entries when the MCP server shuts down
47
+ process.on("beforeExit", flushBuffer);
48
+ return logDb;
49
+ }
50
+ // WHAT: Buffer log entries and flush when threshold is reached
51
+ // WHY: Removes ~1-5ms synchronous INSERT from every tool response
52
+ export function logQuery(entry) {
53
+ if (!logDb)
54
+ return;
55
+ buffer.push(entry);
56
+ if (buffer.length >= FLUSH_BATCH_SIZE) {
57
+ flushBuffer();
58
+ }
59
+ }
60
+ // WHAT: Write all buffered entries to SQLite in a single transaction
61
+ // WHY: Batch INSERT in a transaction is ~10x faster than individual INSERTs
62
+ function flushBuffer() {
63
+ if (!logDb || buffer.length === 0)
64
+ return;
65
+ const entries = buffer.splice(0);
66
+ try {
67
+ const insert = logDb.prepare(`INSERT INTO query_log (timestamp, client_id, tool_name, query_params, content_cited, response_time_ms, confidence_score)
68
+ VALUES (?, ?, ?, ?, ?, ?, ?)`);
69
+ const insertAll = logDb.transaction(() => {
70
+ for (const entry of entries) {
71
+ insert.run(entry.timestamp, entry.clientId, entry.toolName, entry.queryParams, entry.contentCited, entry.responseTimeMs, entry.confidenceScore);
72
+ }
73
+ });
74
+ insertAll();
75
+ }
76
+ catch (err) {
77
+ // WHAT: Log error but don't crash — analytics loss is acceptable
78
+ // WHY: Query logging should never block or crash the MCP server
79
+ console.error(`Query log flush failed (${entries.length} entries lost):`, err instanceof Error ? err.message : err);
80
+ }
81
+ }
82
+ export function closeQueryLog() {
83
+ if (flushTimer) {
84
+ clearInterval(flushTimer);
85
+ flushTimer = null;
86
+ }
87
+ flushBuffer(); // Final flush
88
+ if (logDb) {
89
+ logDb.close();
90
+ logDb = null;
91
+ }
92
+ }
93
+ //# sourceMappingURL=query-log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-log.js","sourceRoot":"","sources":["../src/query-log.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Rate limiting and registration module.
3
+ *
4
+ * WHAT: Enforces per-client daily query limits with public/registered tiers
5
+ * WHY: Protects server resources while incentivizing email registration for higher limits
6
+ *
7
+ * Tiers:
8
+ * Public: 50 queries/day (anonymous, persistent client ID)
9
+ * Registered: 500 queries/day (API key via ASTGL_API_KEY env var)
10
+ */
11
+ export type Tier = "public" | "registered";
12
+ export interface RateLimitResult {
13
+ allowed: boolean;
14
+ tier: Tier;
15
+ clientId: string;
16
+ used: number;
17
+ limit: number;
18
+ remaining: number;
19
+ }
20
+ export interface RegistrationResult {
21
+ success: boolean;
22
+ apiKey?: string;
23
+ email?: string;
24
+ message: string;
25
+ }
26
+ export declare function resolveClient(): {
27
+ clientId: string;
28
+ tier: Tier;
29
+ };
30
+ export declare function initRateLimitDb(): void;
31
+ export declare function checkRateLimit(clientId: string, tier: Tier): RateLimitResult;
32
+ export declare function rateLimitMessage(result: RateLimitResult): string;
33
+ export declare function registerClient(email: string): RegistrationResult;
34
+ export declare function closeRateLimitDb(): void;
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Rate limiting and registration module.
3
+ *
4
+ * WHAT: Enforces per-client daily query limits with public/registered tiers
5
+ * WHY: Protects server resources while incentivizing email registration for higher limits
6
+ *
7
+ * Tiers:
8
+ * Public: 50 queries/day (anonymous, persistent client ID)
9
+ * Registered: 500 queries/day (API key via ASTGL_API_KEY env var)
10
+ */
11
+ import { join } from "path";
12
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
13
+ import { homedir } from "os";
14
+ import { randomUUID, createHash } from "crypto";
15
+ import Database from "better-sqlite3";
16
+ const DATA_DIR = join(import.meta.dirname, "..", "data");
17
+ const RATE_DB_PATH = join(DATA_DIR, "query-log.db");
18
+ const CLIENT_ID_FILE = join(homedir(), ".astgl-client-id");
19
+ const PUBLIC_LIMIT = 50;
20
+ const REGISTERED_LIMIT = 500;
21
+ let rateLimitDb = null;
22
+ // --- Client Identity ---
23
+ // WHAT: Get or create a persistent anonymous client ID
24
+ // WHY: MCP stdio servers are ephemeral — need a stable ID across sessions for rate limiting
25
+ function getOrCreateAnonymousId() {
26
+ if (existsSync(CLIENT_ID_FILE)) {
27
+ const id = readFileSync(CLIENT_ID_FILE, "utf-8").trim();
28
+ if (id)
29
+ return id;
30
+ }
31
+ const id = `anon_${randomUUID()}`;
32
+ try {
33
+ writeFileSync(CLIENT_ID_FILE, id, "utf-8");
34
+ }
35
+ catch {
36
+ // WHAT: Fall back to hostname-based ID if file write fails
37
+ // WHY: Some environments restrict home directory writes (containers, CI)
38
+ return `anon_${createHash("sha256").update(homedir()).digest("hex").slice(0, 16)}`;
39
+ }
40
+ return id;
41
+ }
42
+ // WHAT: Resolve the effective client ID and tier
43
+ // WHY: API key takes precedence over anonymous ID; determines rate limit tier
44
+ export function resolveClient() {
45
+ const apiKey = process.env.ASTGL_API_KEY;
46
+ if (apiKey && apiKey.startsWith("astgl_")) {
47
+ // Validate API key against registrations
48
+ if (rateLimitDb) {
49
+ const reg = rateLimitDb
50
+ .prepare("SELECT email FROM registrations WHERE api_key = ?")
51
+ .get(apiKey);
52
+ if (reg) {
53
+ return { clientId: apiKey, tier: "registered" };
54
+ }
55
+ }
56
+ // Invalid key — fall through to anonymous
57
+ console.error(`Warning: ASTGL_API_KEY provided but not found in registrations.`);
58
+ }
59
+ return { clientId: getOrCreateAnonymousId(), tier: "public" };
60
+ }
61
+ // --- Rate Limiting ---
62
+ export function initRateLimitDb() {
63
+ if (!existsSync(DATA_DIR))
64
+ mkdirSync(DATA_DIR, { recursive: true });
65
+ // WHAT: Reuse query-log.db for rate limit tracking
66
+ // WHY: Query counts are already there — just need a registrations table
67
+ rateLimitDb = new Database(RATE_DB_PATH);
68
+ // Ensure query_log table exists (may not if server hasn't been used yet)
69
+ rateLimitDb.exec(`
70
+ CREATE TABLE IF NOT EXISTS query_log (
71
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
72
+ timestamp TEXT NOT NULL,
73
+ client_id TEXT NOT NULL,
74
+ tool_name TEXT NOT NULL,
75
+ query_params TEXT NOT NULL,
76
+ content_cited TEXT,
77
+ response_time_ms INTEGER NOT NULL,
78
+ confidence_score REAL
79
+ )
80
+ `);
81
+ rateLimitDb.exec("CREATE INDEX IF NOT EXISTS idx_query_log_ts ON query_log(timestamp)");
82
+ rateLimitDb.exec("CREATE INDEX IF NOT EXISTS idx_query_log_tool ON query_log(tool_name)");
83
+ rateLimitDb.exec("CREATE INDEX IF NOT EXISTS idx_query_log_client ON query_log(client_id)");
84
+ rateLimitDb.exec(`
85
+ CREATE TABLE IF NOT EXISTS registrations (
86
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
87
+ email TEXT NOT NULL UNIQUE,
88
+ api_key TEXT NOT NULL UNIQUE,
89
+ created_at TEXT NOT NULL
90
+ )
91
+ `);
92
+ }
93
+ // --- Rate Limit Cache ---
94
+ // WHAT: Cache rate limit results for 5 seconds per client
95
+ // WHY: At 500 queries/day (~1 every 3 min), a 5s cache has zero practical
96
+ // accuracy impact but eliminates a COUNT(*) query from most requests
97
+ const RATE_CACHE_TTL_MS = 5_000;
98
+ const rateLimitCache = new Map();
99
+ // WHAT: Check if a client has queries remaining today
100
+ // WHY: Enforces tier-based daily limits without blocking the server
101
+ export function checkRateLimit(clientId, tier) {
102
+ const limit = tier === "registered" ? REGISTERED_LIMIT : PUBLIC_LIMIT;
103
+ if (!rateLimitDb) {
104
+ return { allowed: true, tier, clientId, used: 0, limit, remaining: limit };
105
+ }
106
+ // Check cache first
107
+ const cacheKey = `${clientId}:${tier}`;
108
+ const cached = rateLimitCache.get(cacheKey);
109
+ if (cached && cached.expiry > Date.now()) {
110
+ return cached.result;
111
+ }
112
+ // WHAT: Count queries for this client today (UTC)
113
+ // WHY: Daily reset at midnight UTC keeps limits predictable
114
+ const today = new Date().toISOString().split("T")[0];
115
+ const todayStart = `${today}T00:00:00.000Z`;
116
+ const tomorrowStart = `${today}T23:59:59.999Z`;
117
+ const row = rateLimitDb
118
+ .prepare(`SELECT COUNT(*) as count FROM query_log
119
+ WHERE client_id = ? AND timestamp >= ? AND timestamp <= ?`)
120
+ .get(clientId, todayStart, tomorrowStart);
121
+ const used = row.count;
122
+ const remaining = Math.max(0, limit - used);
123
+ const result = {
124
+ allowed: used < limit,
125
+ tier,
126
+ clientId,
127
+ used,
128
+ limit,
129
+ remaining,
130
+ };
131
+ rateLimitCache.set(cacheKey, {
132
+ result,
133
+ expiry: Date.now() + RATE_CACHE_TTL_MS,
134
+ });
135
+ return result;
136
+ }
137
+ // WHAT: Format a rate limit exceeded message for the AI client
138
+ // WHY: Clear messaging helps the AI explain the situation and suggest registration
139
+ export function rateLimitMessage(result) {
140
+ if (result.tier === "public") {
141
+ return [
142
+ `Rate limit exceeded: ${result.used}/${result.limit} queries used today (public tier).`,
143
+ "",
144
+ "To get 500 queries/day, register with your email using the `register` tool.",
145
+ "Then add the API key to your MCP config as ASTGL_API_KEY.",
146
+ "",
147
+ "Rate limits reset at midnight UTC.",
148
+ ].join("\n");
149
+ }
150
+ return [
151
+ `Rate limit exceeded: ${result.used}/${result.limit} queries used today (registered tier).`,
152
+ "",
153
+ "Rate limits reset at midnight UTC.",
154
+ ].join("\n");
155
+ }
156
+ // --- Registration ---
157
+ // WHAT: Register an email and generate an API key
158
+ // WHY: Email capture builds an audience; API key enables higher rate limits
159
+ export function registerClient(email) {
160
+ if (!rateLimitDb) {
161
+ return { success: false, message: "Registration database not available." };
162
+ }
163
+ // Basic email validation
164
+ if (!email || !email.includes("@") || !email.includes(".")) {
165
+ return { success: false, message: "Please provide a valid email address." };
166
+ }
167
+ const normalized = email.trim().toLowerCase();
168
+ // Check if already registered
169
+ const existing = rateLimitDb
170
+ .prepare("SELECT api_key FROM registrations WHERE email = ?")
171
+ .get(normalized);
172
+ if (existing) {
173
+ return {
174
+ success: true,
175
+ apiKey: existing.api_key,
176
+ email: normalized,
177
+ message: `Already registered. Your API key: ${existing.api_key}`,
178
+ };
179
+ }
180
+ // Generate API key
181
+ const apiKey = `astgl_${randomUUID().replace(/-/g, "")}`;
182
+ rateLimitDb
183
+ .prepare("INSERT INTO registrations (email, api_key, created_at) VALUES (?, ?, ?)")
184
+ .run(normalized, apiKey, new Date().toISOString());
185
+ return {
186
+ success: true,
187
+ apiKey,
188
+ email: normalized,
189
+ message: [
190
+ `Registered successfully! Your API key: ${apiKey}`,
191
+ "",
192
+ "Add it to your MCP server config to unlock 500 queries/day:",
193
+ "",
194
+ '```json',
195
+ '"env": { "ASTGL_API_KEY": "' + apiKey + '" }',
196
+ '```',
197
+ ].join("\n"),
198
+ };
199
+ }
200
+ export function closeRateLimitDb() {
201
+ if (rateLimitDb) {
202
+ rateLimitDb.close();
203
+ rateLimitDb = null;
204
+ }
205
+ }
206
+ //# sourceMappingURL=rate-limit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rate-limit.js","sourceRoot":"","sources":["../src/rate-limit.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Internal linking automation via vector similarity.
4
+ *
5
+ * WHAT: Computes pairwise article similarity and injects related article links
6
+ * WHY: Cross-referencing boosts SEO, AI discoverability, and reader engagement
7
+ *
8
+ * Usage:
9
+ * npm run related Compute + print JSON map
10
+ * npm run related -- --inject Also inject into Astro markdown frontmatter
11
+ * npm run related -- --top 3 Number of related articles per article (default: 3)
12
+ *
13
+ * Requires: Ollama running with nomic-embed-text
14
+ */
15
+ export {};