@adityanair98/api-oracle 0.5.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 (119) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +216 -0
  3. package/dist/cli.d.ts +11 -0
  4. package/dist/cli.js +74 -0
  5. package/dist/dashboard/public/app.js +1004 -0
  6. package/dist/dashboard/public/index.html +142 -0
  7. package/dist/dashboard/public/public/app.js +1004 -0
  8. package/dist/dashboard/public/public/index.html +142 -0
  9. package/dist/dashboard/public/public/styles.css +1464 -0
  10. package/dist/dashboard/public/styles.css +1464 -0
  11. package/dist/dashboard/routes/api.d.ts +7 -0
  12. package/dist/dashboard/routes/api.js +245 -0
  13. package/dist/dashboard/server.d.ts +9 -0
  14. package/dist/dashboard/server.js +45 -0
  15. package/dist/index.d.ts +5 -0
  16. package/dist/index.js +23 -0
  17. package/dist/knowledge/db.d.ts +22 -0
  18. package/dist/knowledge/db.js +182 -0
  19. package/dist/knowledge/schema.d.ts +275 -0
  20. package/dist/knowledge/schema.js +135 -0
  21. package/dist/knowledge/scorer.d.ts +63 -0
  22. package/dist/knowledge/scorer.js +314 -0
  23. package/dist/knowledge/search.d.ts +37 -0
  24. package/dist/knowledge/search.js +111 -0
  25. package/dist/knowledge/synonyms.d.ts +36 -0
  26. package/dist/knowledge/synonyms.js +523 -0
  27. package/dist/knowledge/tfidf.d.ts +42 -0
  28. package/dist/knowledge/tfidf.js +138 -0
  29. package/dist/server.d.ts +9 -0
  30. package/dist/server.js +40 -0
  31. package/dist/tools/check-freshness.d.ts +9 -0
  32. package/dist/tools/check-freshness.js +95 -0
  33. package/dist/tools/compare-apis.d.ts +8 -0
  34. package/dist/tools/compare-apis.js +149 -0
  35. package/dist/tools/find-api.d.ts +9 -0
  36. package/dist/tools/find-api.js +120 -0
  37. package/dist/tools/get-setup-guide.d.ts +8 -0
  38. package/dist/tools/get-setup-guide.js +127 -0
  39. package/dist/updater/linter.d.ts +31 -0
  40. package/dist/updater/linter.js +219 -0
  41. package/dist/updater/report.d.ts +29 -0
  42. package/dist/updater/report.js +96 -0
  43. package/dist/updater/staleness.d.ts +39 -0
  44. package/dist/updater/staleness.js +66 -0
  45. package/dist/updater/version-tracker.d.ts +28 -0
  46. package/dist/updater/version-tracker.js +50 -0
  47. package/dist/utils/config.d.ts +11 -0
  48. package/dist/utils/config.js +13 -0
  49. package/dist/utils/logger.d.ts +20 -0
  50. package/dist/utils/logger.js +32 -0
  51. package/package.json +56 -0
  52. package/src/entries/ai/anthropic.json +95 -0
  53. package/src/entries/ai/eleven-labs.json +90 -0
  54. package/src/entries/ai/openai.json +95 -0
  55. package/src/entries/ai/replicate.json +87 -0
  56. package/src/entries/ai/resemble-ai.json +88 -0
  57. package/src/entries/ai/stability-ai.json +89 -0
  58. package/src/entries/analytics/posthog.json +88 -0
  59. package/src/entries/analytics/sentry.json +84 -0
  60. package/src/entries/auth/auth0.json +90 -0
  61. package/src/entries/auth/clerk.json +95 -0
  62. package/src/entries/cms/contentful.json +92 -0
  63. package/src/entries/cms/sanity.json +92 -0
  64. package/src/entries/cms/strapi.json +93 -0
  65. package/src/entries/commerce/medusa.json +91 -0
  66. package/src/entries/commerce/shopify-api.json +91 -0
  67. package/src/entries/communication/sendbird.json +85 -0
  68. package/src/entries/communication/stream-chat.json +94 -0
  69. package/src/entries/database/firebase.json +88 -0
  70. package/src/entries/database/neon.json +94 -0
  71. package/src/entries/database/planetscale.json +95 -0
  72. package/src/entries/database/supabase.json +94 -0
  73. package/src/entries/database/upstash.json +94 -0
  74. package/src/entries/devops/fly-io.json +90 -0
  75. package/src/entries/devops/netlify.json +90 -0
  76. package/src/entries/devops/railway.json +90 -0
  77. package/src/entries/devops/vercel.json +90 -0
  78. package/src/entries/email/mailgun.json +91 -0
  79. package/src/entries/email/postmark.json +91 -0
  80. package/src/entries/email/resend.json +89 -0
  81. package/src/entries/email/sendgrid.json +90 -0
  82. package/src/entries/forms/formspark.json +85 -0
  83. package/src/entries/forms/typeform.json +98 -0
  84. package/src/entries/infrastructure/aws-s3.json +104 -0
  85. package/src/entries/infrastructure/cloudflare-r2.json +92 -0
  86. package/src/entries/infrastructure/cloudflare-workers.json +92 -0
  87. package/src/entries/infrastructure/digital-ocean-spaces.json +87 -0
  88. package/src/entries/integration/nango.json +90 -0
  89. package/src/entries/integration/zapier.json +92 -0
  90. package/src/entries/maps/google-maps.json +89 -0
  91. package/src/entries/maps/mapbox.json +87 -0
  92. package/src/entries/media/deepgram.json +84 -0
  93. package/src/entries/media/imgix.json +84 -0
  94. package/src/entries/media/mux.json +94 -0
  95. package/src/entries/messaging/ably.json +94 -0
  96. package/src/entries/messaging/pusher.json +94 -0
  97. package/src/entries/messaging/twilio.json +94 -0
  98. package/src/entries/messaging/vonage.json +89 -0
  99. package/src/entries/notifications/knock.json +84 -0
  100. package/src/entries/notifications/novu.json +84 -0
  101. package/src/entries/notifications/onesignal.json +84 -0
  102. package/src/entries/payments/lemonsqueezy.json +91 -0
  103. package/src/entries/payments/paddle.json +90 -0
  104. package/src/entries/payments/paypal.json +91 -0
  105. package/src/entries/payments/razorpay.json +85 -0
  106. package/src/entries/payments/square.json +91 -0
  107. package/src/entries/payments/stripe.json +96 -0
  108. package/src/entries/scheduling/cal-com.json +90 -0
  109. package/src/entries/scheduling/calendly.json +90 -0
  110. package/src/entries/search/algolia.json +96 -0
  111. package/src/entries/security/arcjet.json +89 -0
  112. package/src/entries/security/snyk.json +90 -0
  113. package/src/entries/storage/cloudinary.json +93 -0
  114. package/src/entries/storage/uploadthing.json +90 -0
  115. package/src/entries/testing/browserstack.json +86 -0
  116. package/src/entries/testing/checkly.json +89 -0
  117. package/src/entries/workflow/inngest.json +88 -0
  118. package/src/entries/workflow/temporal.json +90 -0
  119. package/src/entries/workflow/trigger-dev.json +89 -0
@@ -0,0 +1,127 @@
1
+ /**
2
+ * MCP Tool: get_api_setup_guide
3
+ * Returns detailed, step-by-step setup instructions for a specific API.
4
+ *
5
+ * Exports: registerGetSetupGuide
6
+ */
7
+ import { z } from "zod";
8
+ import { getApiBySlug } from "../knowledge/search.js";
9
+ import { createLogger } from "../utils/logger.js";
10
+ const logger = createLogger("tool:get-setup-guide");
11
+ const inputSchema = {
12
+ slug: z.string().min(1).describe("The API slug to get setup instructions for, e.g. 'resend', 'stripe', 'clerk'"),
13
+ language: z
14
+ .string()
15
+ .optional()
16
+ .describe("Preferred language for code examples (default: 'typescript'). Must be a language the API supports."),
17
+ };
18
+ export function registerGetSetupGuide(server) {
19
+ server.tool("get_api_setup_guide", "Get complete, step-by-step setup instructions for a specific API including install command, auth setup, and working code examples.", inputSchema, async ({ slug, language }) => {
20
+ logger.info("get_api_setup_guide called", { slug, language });
21
+ try {
22
+ const entry = getApiBySlug(slug);
23
+ if (!entry) {
24
+ return {
25
+ content: [
26
+ {
27
+ type: "text",
28
+ text: JSON.stringify({
29
+ error: `API '${slug}' not found in the knowledge base`,
30
+ suggestion: "Use find_best_api to discover available APIs and their slugs.",
31
+ }),
32
+ },
33
+ ],
34
+ };
35
+ }
36
+ const targetLanguage = (language ?? "typescript").toLowerCase();
37
+ // Select appropriate code examples
38
+ const allExamples = entry.codeExamples;
39
+ const languageExamples = allExamples.filter((ex) => ex.language.toLowerCase() === targetLanguage);
40
+ const examples = languageExamples.length > 0 ? languageExamples : allExamples;
41
+ // Check if the requested language is supported
42
+ const supportedLanguages = [
43
+ entry.sdk.primaryLanguage,
44
+ ...entry.sdk.otherLanguages,
45
+ ];
46
+ const isLanguageSupported = supportedLanguages.some((l) => l.toLowerCase() === targetLanguage);
47
+ // Build the full code example (all examples concatenated with titles)
48
+ const fullCodeExample = examples.length > 0
49
+ ? examples
50
+ .map((ex) => `// === ${ex.title} ===\n${ex.code}${ex.notes ? `\n// NOTE: ${ex.notes}` : ""}`)
51
+ .join("\n\n")
52
+ : "// See documentation at " + entry.website;
53
+ // Build rate limit warning
54
+ const rateLimitWarning = `${entry.rateLimits.tier}: ${entry.rateLimits.limit}. ${entry.rateLimits.notes}. Retry strategy: ${entry.rateLimits.retryStrategy}`;
55
+ // Build pricing summary
56
+ const pricingParts = [
57
+ entry.pricing.freeTier ? `Free tier: ${entry.pricing.freeTier}` : null,
58
+ entry.pricing.startingPrice ? `Paid: ${entry.pricing.startingPrice}` : null,
59
+ entry.pricing.costPer ? `Per unit: ${entry.pricing.costPer}` : null,
60
+ ].filter(Boolean);
61
+ const response = {
62
+ name: entry.name,
63
+ slug: entry.slug,
64
+ website: entry.website,
65
+ category: entry.category,
66
+ description: entry.description,
67
+ // Auth setup
68
+ setupSteps: entry.auth.setupSteps,
69
+ envVarName: entry.auth.envVarName,
70
+ authMethod: entry.auth.method,
71
+ authCodeSnippet: entry.auth.codeSnippet,
72
+ // SDK info
73
+ installCommand: entry.sdk.installCommand,
74
+ importStatement: entry.sdk.importStatement,
75
+ supportedLanguages,
76
+ // Language warning if not supported
77
+ languageNote: !isLanguageSupported
78
+ ? `Note: ${entry.name} does not have an official ${targetLanguage} SDK. Showing ${entry.sdk.primaryLanguage} examples instead. Community clients may exist.`
79
+ : null,
80
+ // Code
81
+ codeExamples: examples.map((ex) => ({
82
+ title: ex.title,
83
+ language: ex.language,
84
+ code: ex.code,
85
+ notes: ex.notes,
86
+ })),
87
+ fullCodeExample,
88
+ // Practical info
89
+ gotchas: entry.gotchas,
90
+ rateLimitWarning,
91
+ pricing: pricingParts.join(". "),
92
+ pricingUrl: entry.pricing.pricingUrl,
93
+ // Reliability
94
+ uptimeGuarantee: entry.reliability.uptimeGuarantee,
95
+ statusPage: entry.reliability.statusPageUrl,
96
+ // Related
97
+ alternatives: entry.alternatives,
98
+ complementaryApis: entry.complementary,
99
+ // Meta
100
+ qualityScore: entry.qualityScore,
101
+ lastVerified: entry.lastVerified,
102
+ };
103
+ return {
104
+ content: [
105
+ {
106
+ type: "text",
107
+ text: JSON.stringify(response, null, 2),
108
+ },
109
+ ],
110
+ };
111
+ }
112
+ catch (err) {
113
+ logger.error("get_api_setup_guide error", err);
114
+ return {
115
+ content: [
116
+ {
117
+ type: "text",
118
+ text: JSON.stringify({
119
+ error: "Internal error while fetching setup guide",
120
+ details: String(err),
121
+ }),
122
+ },
123
+ ],
124
+ };
125
+ }
126
+ });
127
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Entry quality linter for API Oracle.
3
+ *
4
+ * Applies heuristic quality rules beyond Zod schema validation.
5
+ * Checks for common content issues: vague descriptions, thin code examples,
6
+ * missing gotchas detail, suspicious field values, etc.
7
+ *
8
+ * Exports: LintIssue, LintSeverity, LintResult, lintEntry, lintAllEntries
9
+ */
10
+ import type { ApiEntry } from "../knowledge/schema.js";
11
+ export type LintSeverity = "error" | "warning" | "info";
12
+ export interface LintIssue {
13
+ field: string;
14
+ severity: LintSeverity;
15
+ message: string;
16
+ }
17
+ export interface LintResult {
18
+ slug: string;
19
+ name: string;
20
+ issues: LintIssue[];
21
+ /** Entries with zero errors and warnings pass */
22
+ passed: boolean;
23
+ }
24
+ /**
25
+ * Lint a single entry and return all issues found.
26
+ */
27
+ export declare function lintEntry(entry: ApiEntry): LintResult;
28
+ /**
29
+ * Lint all entries. Returns results sorted: failed first, then passed.
30
+ */
31
+ export declare function lintAllEntries(entries: ApiEntry[]): LintResult[];
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Entry quality linter for API Oracle.
3
+ *
4
+ * Applies heuristic quality rules beyond Zod schema validation.
5
+ * Checks for common content issues: vague descriptions, thin code examples,
6
+ * missing gotchas detail, suspicious field values, etc.
7
+ *
8
+ * Exports: LintIssue, LintSeverity, LintResult, lintEntry, lintAllEntries
9
+ */
10
+ const RULES = [
11
+ // ── Description quality ─────────────────────────────────────────────────
12
+ (e) => {
13
+ const issues = [];
14
+ if (e.description.length < 80) {
15
+ issues.push({
16
+ field: "description",
17
+ severity: "warning",
18
+ message: `Description is short (${e.description.length} chars). Aim for 100+ chars of useful context.`,
19
+ });
20
+ }
21
+ return issues;
22
+ },
23
+ // ── Code examples quality ───────────────────────────────────────────────
24
+ (e) => {
25
+ const issues = [];
26
+ if (e.codeExamples.length < 2) {
27
+ issues.push({
28
+ field: "codeExamples",
29
+ severity: "warning",
30
+ message: "Only 1 code example. 2+ examples showing different use cases are recommended.",
31
+ });
32
+ }
33
+ for (const ex of e.codeExamples) {
34
+ if (ex.code.split("\n").length < 5) {
35
+ issues.push({
36
+ field: `codeExamples[${ex.title}].code`,
37
+ severity: "warning",
38
+ message: `Code example "${ex.title}" is very short (< 5 lines). Consider a more complete example.`,
39
+ });
40
+ }
41
+ if (!ex.notes || ex.notes.trim().length < 20) {
42
+ issues.push({
43
+ field: `codeExamples[${ex.title}].notes`,
44
+ severity: "info",
45
+ message: `Code example "${ex.title}" has sparse notes. Add context about what the example demonstrates.`,
46
+ });
47
+ }
48
+ }
49
+ return issues;
50
+ },
51
+ // ── Gotchas quality ─────────────────────────────────────────────────────
52
+ (e) => {
53
+ const issues = [];
54
+ for (const gotcha of e.gotchas) {
55
+ if (gotcha.length < 50) {
56
+ issues.push({
57
+ field: "gotchas",
58
+ severity: "warning",
59
+ message: `Gotcha is very short: "${gotcha.slice(0, 40)}...". Each gotcha should explain WHY it matters.`,
60
+ });
61
+ }
62
+ }
63
+ if (e.gotchas.length < 3) {
64
+ issues.push({
65
+ field: "gotchas",
66
+ severity: "info",
67
+ message: `Only ${e.gotchas.length} gotchas. Consider adding a 3rd with a less obvious pitfall.`,
68
+ });
69
+ }
70
+ return issues;
71
+ },
72
+ // ── Pricing completeness ─────────────────────────────────────────────────
73
+ (e) => {
74
+ const issues = [];
75
+ if (e.pricing.model === "paid" && e.pricing.startingPrice === null) {
76
+ issues.push({
77
+ field: "pricing.startingPrice",
78
+ severity: "warning",
79
+ message: "Pricing model is 'paid' but startingPrice is null. Add a price if publicly listed.",
80
+ });
81
+ }
82
+ if (e.pricing.model === "usage_based" && e.pricing.costPer === null) {
83
+ issues.push({
84
+ field: "pricing.costPer",
85
+ severity: "info",
86
+ message: "Usage-based pricing with no costPer breakdown. Add $/unit info if available.",
87
+ });
88
+ }
89
+ return issues;
90
+ },
91
+ // ── Setup steps quality ──────────────────────────────────────────────────
92
+ (e) => {
93
+ const issues = [];
94
+ if (e.auth.setupSteps.length < 3) {
95
+ issues.push({
96
+ field: "auth.setupSteps",
97
+ severity: "info",
98
+ message: `Only ${e.auth.setupSteps.length} setup step(s). Consider adding more detail for a complete setup guide.`,
99
+ });
100
+ }
101
+ return issues;
102
+ },
103
+ // ── Quality score vs justification consistency ───────────────────────────
104
+ (e) => {
105
+ const issues = [];
106
+ if (e.qualityScore >= 9 && e.qualityJustification.length < 80) {
107
+ issues.push({
108
+ field: "qualityJustification",
109
+ severity: "info",
110
+ message: `Score is ${e.qualityScore}/10 but justification is brief. Explain what earns this high rating.`,
111
+ });
112
+ }
113
+ if (e.qualityScore <= 4 && e.qualityJustification.length < 80) {
114
+ issues.push({
115
+ field: "qualityJustification",
116
+ severity: "info",
117
+ message: `Score is ${e.qualityScore}/10. Explain specifically why the score is low to help users make decisions.`,
118
+ });
119
+ }
120
+ return issues;
121
+ },
122
+ // ── Alternatives and complementary relationships ─────────────────────────
123
+ (e) => {
124
+ const issues = [];
125
+ if (e.alternatives.length === 0 && e.qualityScore < 9) {
126
+ issues.push({
127
+ field: "alternatives",
128
+ severity: "info",
129
+ message: "No alternatives listed. Most APIs have at least one competitor worth mentioning.",
130
+ });
131
+ }
132
+ if (e.complementary.length === 0) {
133
+ issues.push({
134
+ field: "complementary",
135
+ severity: "info",
136
+ message: "No complementary APIs listed. Most APIs work well with at least one other service.",
137
+ });
138
+ }
139
+ return issues;
140
+ },
141
+ // ── Reliability info ─────────────────────────────────────────────────────
142
+ (e) => {
143
+ const issues = [];
144
+ if (e.reliability.uptimeGuarantee === null && e.pricing.model !== "open_source") {
145
+ issues.push({
146
+ field: "reliability.uptimeGuarantee",
147
+ severity: "info",
148
+ message: "No uptime guarantee listed. Check the provider's SLA page.",
149
+ });
150
+ }
151
+ if (e.reliability.statusPageUrl === null && e.pricing.model !== "open_source") {
152
+ issues.push({
153
+ field: "reliability.statusPageUrl",
154
+ severity: "info",
155
+ message: "No status page URL. Most hosted APIs have a status page — check status.[provider].com.",
156
+ });
157
+ }
158
+ return issues;
159
+ },
160
+ // ── Use cases coverage ───────────────────────────────────────────────────
161
+ (e) => {
162
+ const issues = [];
163
+ if (e.useCases.length < 3) {
164
+ issues.push({
165
+ field: "useCases",
166
+ severity: "info",
167
+ message: `Only ${e.useCases.length} use case(s). Consider adding more to improve search discoverability.`,
168
+ });
169
+ }
170
+ const hasPerfect = e.useCases.some((u) => u.fit === "perfect");
171
+ if (!hasPerfect) {
172
+ issues.push({
173
+ field: "useCases",
174
+ severity: "warning",
175
+ message: "No use case has fit='perfect'. At least one core use case should be a perfect fit.",
176
+ });
177
+ }
178
+ return issues;
179
+ },
180
+ ];
181
+ // ─── Core Functions ───────────────────────────────────────────────────────────
182
+ /**
183
+ * Lint a single entry and return all issues found.
184
+ */
185
+ export function lintEntry(entry) {
186
+ const issues = [];
187
+ for (const rule of RULES) {
188
+ issues.push(...rule(entry));
189
+ }
190
+ const passed = !issues.some((i) => i.severity === "error" || i.severity === "warning");
191
+ return { slug: entry.slug, name: entry.name, issues, passed };
192
+ }
193
+ /**
194
+ * Lint all entries. Returns results sorted: failed first, then passed.
195
+ */
196
+ export function lintAllEntries(entries) {
197
+ return entries
198
+ .map(lintEntry)
199
+ .sort((a, b) => {
200
+ // Sort by issue count descending, then by slug
201
+ const aScore = issueScore(a);
202
+ const bScore = issueScore(b);
203
+ if (bScore !== aScore)
204
+ return bScore - aScore;
205
+ return a.slug.localeCompare(b.slug);
206
+ });
207
+ }
208
+ function issueScore(result) {
209
+ let score = 0;
210
+ for (const issue of result.issues) {
211
+ if (issue.severity === "error")
212
+ score += 100;
213
+ else if (issue.severity === "warning")
214
+ score += 10;
215
+ else
216
+ score += 1;
217
+ }
218
+ return score;
219
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Report formatter for the API Oracle auto-updater.
3
+ *
4
+ * Formats staleness info and version diffs into a human-readable
5
+ * markdown/text report for the refresh-report script and MCP tool.
6
+ *
7
+ * Exports: FreshnessReport, buildFreshnessReport, formatReport
8
+ */
9
+ import type { StalenessInfo, StalenessLevel } from "./staleness.js";
10
+ import type { VersionDiff } from "./version-tracker.js";
11
+ export interface FreshnessReport {
12
+ generatedAt: string;
13
+ totalEntries: number;
14
+ summary: Record<StalenessLevel, number>;
15
+ staleEntries: StalenessInfo[];
16
+ recentlyUpdated: VersionDiff[];
17
+ }
18
+ /**
19
+ * Build a structured freshness report from staleness data.
20
+ */
21
+ export declare function buildFreshnessReport(allInfos: StalenessInfo[], versionDiffs?: VersionDiff[]): FreshnessReport;
22
+ /**
23
+ * Format a freshness report as a human-readable markdown string.
24
+ */
25
+ export declare function formatReport(report: FreshnessReport): string;
26
+ /**
27
+ * Format a brief staleness summary suitable for an MCP tool response.
28
+ */
29
+ export declare function formatBriefSummary(report: FreshnessReport): string;
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Report formatter for the API Oracle auto-updater.
3
+ *
4
+ * Formats staleness info and version diffs into a human-readable
5
+ * markdown/text report for the refresh-report script and MCP tool.
6
+ *
7
+ * Exports: FreshnessReport, buildFreshnessReport, formatReport
8
+ */
9
+ // ─── Builders ─────────────────────────────────────────────────────────────────
10
+ /**
11
+ * Build a structured freshness report from staleness data.
12
+ */
13
+ export function buildFreshnessReport(allInfos, versionDiffs = []) {
14
+ const summary = {
15
+ fresh: 0,
16
+ aging: 0,
17
+ stale: 0,
18
+ critical: 0,
19
+ };
20
+ for (const info of allInfos) {
21
+ summary[info.level]++;
22
+ }
23
+ const staleEntries = allInfos.filter((i) => i.level === "stale" || i.level === "critical" || i.level === "aging");
24
+ return {
25
+ generatedAt: new Date().toISOString(),
26
+ totalEntries: allInfos.length,
27
+ summary,
28
+ staleEntries,
29
+ recentlyUpdated: versionDiffs,
30
+ };
31
+ }
32
+ // ─── Formatters ───────────────────────────────────────────────────────────────
33
+ const LEVEL_EMOJI = {
34
+ fresh: "✅",
35
+ aging: "🟡",
36
+ stale: "🟠",
37
+ critical: "🔴",
38
+ };
39
+ /**
40
+ * Format a freshness report as a human-readable markdown string.
41
+ */
42
+ export function formatReport(report) {
43
+ const lines = [];
44
+ lines.push("# API Oracle — Freshness Report");
45
+ lines.push(`Generated: ${report.generatedAt}`);
46
+ lines.push("");
47
+ lines.push("## Summary");
48
+ lines.push(`Total entries: **${report.totalEntries}**`);
49
+ lines.push("");
50
+ lines.push(`| Status | Count | Threshold |`, `|--------|-------|-----------|`, `| ${LEVEL_EMOJI.fresh} Fresh | ${report.summary.fresh} | Verified < 90 days ago |`, `| ${LEVEL_EMOJI.aging} Aging | ${report.summary.aging} | 90–180 days ago |`, `| ${LEVEL_EMOJI.stale} Stale | ${report.summary.stale} | 180–365 days ago |`, `| ${LEVEL_EMOJI.critical} Critical | ${report.summary.critical} | > 365 days ago |`);
51
+ if (report.staleEntries.length === 0) {
52
+ lines.push("");
53
+ lines.push("## All entries are fresh — nothing to update! 🎉");
54
+ }
55
+ else {
56
+ lines.push("");
57
+ lines.push("## Entries Needing Attention");
58
+ lines.push(`| Slug | Name | Last Verified | Age (days) | Status |`, `|------|------|--------------|------------|--------|`);
59
+ for (const entry of report.staleEntries) {
60
+ const emoji = LEVEL_EMOJI[entry.level];
61
+ lines.push(`| ${entry.slug} | ${entry.name} | ${entry.lastVerified} | ${entry.daysSinceVerified} | ${emoji} ${entry.level} |`);
62
+ }
63
+ }
64
+ if (report.recentlyUpdated.length > 0) {
65
+ lines.push("");
66
+ lines.push("## Recently Updated Entries");
67
+ lines.push(`| Slug | Name | Change | Version |`, `|------|------|--------|---------|`);
68
+ for (const diff of report.recentlyUpdated) {
69
+ const versionStr = diff.changeType === "new"
70
+ ? `new (v${diff.currentVersion})`
71
+ : `v${diff.previousVersion ?? "?"} → v${diff.currentVersion}`;
72
+ lines.push(`| ${diff.slug} | ${diff.name} | ${diff.changeType} | ${versionStr} |`);
73
+ }
74
+ }
75
+ return lines.join("\n");
76
+ }
77
+ /**
78
+ * Format a brief staleness summary suitable for an MCP tool response.
79
+ */
80
+ export function formatBriefSummary(report) {
81
+ const { summary, totalEntries } = report;
82
+ const issues = summary.aging + summary.stale + summary.critical;
83
+ if (issues === 0) {
84
+ return `All ${totalEntries} entries are fresh (verified within 90 days). No updates needed.`;
85
+ }
86
+ const parts = [];
87
+ if (summary.critical > 0)
88
+ parts.push(`${summary.critical} critical (>1 year old)`);
89
+ if (summary.stale > 0)
90
+ parts.push(`${summary.stale} stale (6–12 months old)`);
91
+ if (summary.aging > 0)
92
+ parts.push(`${summary.aging} aging (3–6 months old)`);
93
+ return (`${totalEntries} entries total: ${summary.fresh} fresh, ${issues} need attention — ` +
94
+ parts.join(", ") +
95
+ ".");
96
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Staleness checker for API Oracle knowledge base entries.
3
+ *
4
+ * Determines how "fresh" each entry is based on its lastVerified date
5
+ * and exposes structured staleness metadata per entry.
6
+ *
7
+ * Exports: StalenessLevel, StalenessInfo, checkStaleness, checkAllEntries
8
+ */
9
+ import type { ApiEntry } from "../knowledge/schema.js";
10
+ export type StalenessLevel = "fresh" | "aging" | "stale" | "critical";
11
+ export interface StalenessInfo {
12
+ slug: string;
13
+ name: string;
14
+ lastVerified: string;
15
+ daysSinceVerified: number;
16
+ level: StalenessLevel;
17
+ /** ISO date of when this was computed */
18
+ checkedAt: string;
19
+ }
20
+ /**
21
+ * Compute staleness level from a day count.
22
+ */
23
+ export declare function getStalenessLevel(daysSince: number): StalenessLevel;
24
+ /**
25
+ * Compute the number of days between a date string (YYYY-MM-DD) and today.
26
+ */
27
+ export declare function daysSince(dateStr: string, today?: Date): number;
28
+ /**
29
+ * Check staleness for a single entry.
30
+ */
31
+ export declare function checkStaleness(entry: ApiEntry, today?: Date): StalenessInfo;
32
+ /**
33
+ * Check staleness for all entries, sorted from most stale to freshest.
34
+ */
35
+ export declare function checkAllEntries(entries: ApiEntry[], today?: Date): StalenessInfo[];
36
+ /**
37
+ * Filter entries by staleness level.
38
+ */
39
+ export declare function filterByLevel(infos: StalenessInfo[], level: StalenessLevel): StalenessInfo[];
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Staleness checker for API Oracle knowledge base entries.
3
+ *
4
+ * Determines how "fresh" each entry is based on its lastVerified date
5
+ * and exposes structured staleness metadata per entry.
6
+ *
7
+ * Exports: StalenessLevel, StalenessInfo, checkStaleness, checkAllEntries
8
+ */
9
+ // ─── Thresholds ───────────────────────────────────────────────────────────────
10
+ // How many days since lastVerified before an entry is considered each level.
11
+ const THRESHOLDS = {
12
+ fresh: 90, // < 90 days → fresh
13
+ aging: 180, // 90–180 days → aging
14
+ stale: 365, // 180–365 days → stale
15
+ // > 365 days → critical
16
+ };
17
+ // ─── Core Functions ───────────────────────────────────────────────────────────
18
+ /**
19
+ * Compute staleness level from a day count.
20
+ */
21
+ export function getStalenessLevel(daysSince) {
22
+ if (daysSince < THRESHOLDS.fresh)
23
+ return "fresh";
24
+ if (daysSince < THRESHOLDS.aging)
25
+ return "aging";
26
+ if (daysSince < THRESHOLDS.stale)
27
+ return "stale";
28
+ return "critical";
29
+ }
30
+ /**
31
+ * Compute the number of days between a date string (YYYY-MM-DD) and today.
32
+ */
33
+ export function daysSince(dateStr, today = new Date()) {
34
+ const then = new Date(dateStr + "T00:00:00Z");
35
+ const todayUtc = Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate());
36
+ const diff = todayUtc - then.getTime();
37
+ return Math.max(0, Math.floor(diff / (1000 * 60 * 60 * 24)));
38
+ }
39
+ /**
40
+ * Check staleness for a single entry.
41
+ */
42
+ export function checkStaleness(entry, today) {
43
+ const days = daysSince(entry.lastVerified, today);
44
+ return {
45
+ slug: entry.slug,
46
+ name: entry.name,
47
+ lastVerified: entry.lastVerified,
48
+ daysSinceVerified: days,
49
+ level: getStalenessLevel(days),
50
+ checkedAt: new Date().toISOString(),
51
+ };
52
+ }
53
+ /**
54
+ * Check staleness for all entries, sorted from most stale to freshest.
55
+ */
56
+ export function checkAllEntries(entries, today) {
57
+ return entries
58
+ .map((e) => checkStaleness(e, today))
59
+ .sort((a, b) => b.daysSinceVerified - a.daysSinceVerified);
60
+ }
61
+ /**
62
+ * Filter entries by staleness level.
63
+ */
64
+ export function filterByLevel(infos, level) {
65
+ return infos.filter((i) => i.level === level);
66
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Version tracker for API Oracle entries.
3
+ *
4
+ * Reads the current entryVersion from each entry and compares to a stored
5
+ * baseline to detect entries that have been updated since the last review.
6
+ *
7
+ * Exports: VersionSnapshot, VersionDiff, buildSnapshot, diffSnapshot
8
+ */
9
+ import type { ApiEntry } from "../knowledge/schema.js";
10
+ /** A map of slug → entryVersion, stored as a JSON snapshot. */
11
+ export type VersionSnapshot = Record<string, number>;
12
+ export interface VersionDiff {
13
+ slug: string;
14
+ name: string;
15
+ previousVersion: number | null;
16
+ currentVersion: number;
17
+ /** "new" = entry added since snapshot; "updated" = version incremented */
18
+ changeType: "new" | "updated";
19
+ }
20
+ /**
21
+ * Build a version snapshot from a list of entries.
22
+ */
23
+ export declare function buildSnapshot(entries: ApiEntry[]): VersionSnapshot;
24
+ /**
25
+ * Diff a current entry list against a stored snapshot.
26
+ * Returns entries that are new or have an incremented version.
27
+ */
28
+ export declare function diffSnapshot(entries: ApiEntry[], previousSnapshot: VersionSnapshot): VersionDiff[];