mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-08 22:21:07 +00:00
changelog api
This commit is contained in:
parent
0a8c5fa049
commit
8f787763a2
7 changed files with 325 additions and 0 deletions
33
apps/changelog-api/.gitignore
vendored
Normal file
33
apps/changelog-api/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# prod
|
||||
dist/
|
||||
|
||||
# dev
|
||||
.yarn/
|
||||
!.yarn/releases
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/*.code-snippets
|
||||
.idea/workspace.xml
|
||||
.idea/usage.statistics.xml
|
||||
.idea/shelf
|
||||
|
||||
# deps
|
||||
node_modules/
|
||||
.wrangler
|
||||
|
||||
# env
|
||||
.env
|
||||
.env.production
|
||||
.dev.vars
|
||||
|
||||
# logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
21
apps/changelog-api/README.md
Normal file
21
apps/changelog-api/README.md
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
```txt
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
```txt
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
[For generating/synchronizing types based on your Worker configuration run](https://developers.cloudflare.com/workers/wrangler/commands/#types):
|
||||
|
||||
```txt
|
||||
npm run cf-typegen
|
||||
```
|
||||
|
||||
Pass the `CloudflareBindings` as generics when instantiation `Hono`:
|
||||
|
||||
```ts
|
||||
// src/index.ts
|
||||
const app = new Hono<{ Bindings: CloudflareBindings }>()
|
||||
```
|
||||
17
apps/changelog-api/package.json
Normal file
17
apps/changelog-api/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@supermemory/changelog-api",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wrangler dev",
|
||||
"deploy": "wrangler deploy --minify"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20250620.0",
|
||||
"typescript": "^5.8.3",
|
||||
"wrangler": "^4.4.0"
|
||||
}
|
||||
}
|
||||
83
apps/changelog-api/src/index.ts
Normal file
83
apps/changelog-api/src/index.ts
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import { Hono } from "hono"
|
||||
import { cors } from "hono/cors"
|
||||
import { parseChangelog, type ChangelogResponse } from "./parser"
|
||||
|
||||
const MDX_URL =
|
||||
"https://raw.githubusercontent.com/supermemoryai/supermemory/main/apps/docs/changelog/developer-platform.mdx"
|
||||
|
||||
const CACHE_KEY = "changelog-api-v1"
|
||||
const CACHE_TTL_SECONDS = 3600 // 1 hour
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.use(
|
||||
"*",
|
||||
cors({
|
||||
origin: "*",
|
||||
allowMethods: ["GET", "OPTIONS"],
|
||||
allowHeaders: ["Content-Type"],
|
||||
}),
|
||||
)
|
||||
|
||||
async function fetchAndParseChangelog(): Promise<ChangelogResponse> {
|
||||
const response = await fetch(MDX_URL)
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch changelog: ${response.status}`)
|
||||
}
|
||||
const content = await response.text()
|
||||
return parseChangelog(content)
|
||||
}
|
||||
|
||||
async function getCachedChangelog(
|
||||
cacheUrl: string,
|
||||
): Promise<ChangelogResponse | null> {
|
||||
const cache = caches.default
|
||||
const cachedResponse = await cache.match(cacheUrl)
|
||||
|
||||
if (cachedResponse) {
|
||||
return cachedResponse.json()
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async function cacheChangelog(
|
||||
cacheUrl: string,
|
||||
data: ChangelogResponse,
|
||||
): Promise<void> {
|
||||
const cache = caches.default
|
||||
const response = new Response(JSON.stringify(data), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": `public, max-age=${CACHE_TTL_SECONDS}`,
|
||||
},
|
||||
})
|
||||
await cache.put(cacheUrl, response)
|
||||
}
|
||||
|
||||
app.get("/", async (c) => {
|
||||
const cacheUrl = new URL(CACHE_KEY, c.req.url).toString()
|
||||
|
||||
// Try to get from cache first
|
||||
let changelog = await getCachedChangelog(cacheUrl)
|
||||
|
||||
if (!changelog) {
|
||||
// Fetch fresh data
|
||||
changelog = await fetchAndParseChangelog()
|
||||
// Cache it (don't await to not block response)
|
||||
c.executionCtx.waitUntil(cacheChangelog(cacheUrl, changelog))
|
||||
}
|
||||
|
||||
return c.json(changelog, {
|
||||
headers: {
|
||||
"Cache-Control": `public, max-age=${CACHE_TTL_SECONDS}, s-maxage=86400`,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
// Health check endpoint
|
||||
app.get("/health", (c) => {
|
||||
return c.json({ status: "ok" })
|
||||
})
|
||||
|
||||
export default app
|
||||
144
apps/changelog-api/src/parser.ts
Normal file
144
apps/changelog-api/src/parser.ts
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
export interface ChangelogItem {
|
||||
title: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface ChangelogEntry {
|
||||
date: string // "2025-12-30"
|
||||
dateFormatted: string // "December 30, 2025"
|
||||
items: ChangelogItem[]
|
||||
}
|
||||
|
||||
export interface ChangelogResponse {
|
||||
title: string
|
||||
description: string
|
||||
lastUpdated: string // ISO date of most recent entry
|
||||
entries: ChangelogEntry[]
|
||||
total: number
|
||||
}
|
||||
|
||||
function parseFrontmatter(content: string): {
|
||||
frontmatter: Record<string, string>
|
||||
body: string
|
||||
} {
|
||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
|
||||
if (!frontmatterMatch) {
|
||||
return { frontmatter: {}, body: content }
|
||||
}
|
||||
|
||||
const frontmatterStr = frontmatterMatch[1]
|
||||
const body = frontmatterMatch[2]
|
||||
|
||||
const frontmatter: Record<string, string> = {}
|
||||
for (const line of frontmatterStr.split("\n")) {
|
||||
const match = line.match(/^(\w+):\s*"?([^"]*)"?$/)
|
||||
if (match) {
|
||||
frontmatter[match[1]] = match[2]
|
||||
}
|
||||
}
|
||||
|
||||
return { frontmatter, body }
|
||||
}
|
||||
|
||||
function parseDate(dateStr: string): { date: string; dateFormatted: string } {
|
||||
// Parse "December 30, 2025" format
|
||||
const months: Record<string, string> = {
|
||||
January: "01",
|
||||
February: "02",
|
||||
March: "03",
|
||||
April: "04",
|
||||
May: "05",
|
||||
June: "06",
|
||||
July: "07",
|
||||
August: "08",
|
||||
September: "09",
|
||||
October: "10",
|
||||
November: "11",
|
||||
December: "12",
|
||||
}
|
||||
|
||||
const match = dateStr.match(/^(\w+)\s+(\d+),\s+(\d+)$/)
|
||||
if (!match) {
|
||||
return { date: "", dateFormatted: dateStr }
|
||||
}
|
||||
|
||||
const [, month, day, year] = match
|
||||
const monthNum = months[month]
|
||||
if (!monthNum) {
|
||||
console.warn(`Unknown month "${month}" in date "${dateStr}", skipping entry`)
|
||||
return { date: "", dateFormatted: dateStr }
|
||||
}
|
||||
const paddedDay = day.padStart(2, "0")
|
||||
|
||||
return {
|
||||
date: `${year}-${monthNum}-${paddedDay}`,
|
||||
dateFormatted: dateStr,
|
||||
}
|
||||
}
|
||||
|
||||
function parseItems(content: string): ChangelogItem[] {
|
||||
const items: ChangelogItem[] = []
|
||||
|
||||
// Match lines starting with "- **Title:** Description" or "- **Title** Description"
|
||||
const lines = content.split("\n")
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim()
|
||||
if (!trimmed.startsWith("- **")) continue
|
||||
|
||||
// Match "- **Title:** Description" or "- **Title** Description"
|
||||
const match = trimmed.match(/^-\s+\*\*([^*]+)\*\*:?\s*(.*)$/)
|
||||
if (match) {
|
||||
// Remove trailing colon from title if present (e.g., "MCP Context Prompt:" -> "MCP Context Prompt")
|
||||
const title = match[1].trim().replace(/:$/, "")
|
||||
items.push({
|
||||
title,
|
||||
description: match[2].trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}
|
||||
|
||||
export function parseChangelog(content: string): ChangelogResponse {
|
||||
const { frontmatter, body } = parseFrontmatter(content)
|
||||
|
||||
const entries: ChangelogEntry[] = []
|
||||
|
||||
// Split by date headers (## Month Day, Year)
|
||||
const sections = body.split(/^##\s+/m).filter((s) => s.trim())
|
||||
|
||||
for (const section of sections) {
|
||||
const lines = section.split("\n")
|
||||
const dateStr = lines[0].trim()
|
||||
|
||||
// Skip if not a valid date header
|
||||
if (!dateStr.match(/^\w+\s+\d+,\s+\d+$/)) continue
|
||||
|
||||
const { date, dateFormatted } = parseDate(dateStr)
|
||||
const items = parseItems(section)
|
||||
|
||||
if (items.length > 0 && date) {
|
||||
entries.push({
|
||||
date,
|
||||
dateFormatted,
|
||||
items,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sort entries by date descending (most recent first)
|
||||
entries.sort((a, b) => b.date.localeCompare(a.date))
|
||||
|
||||
const totalItems = entries.reduce((sum, entry) => sum + entry.items.length, 0)
|
||||
const lastUpdated = entries[0]?.date || ""
|
||||
|
||||
return {
|
||||
title: frontmatter.title || "Changelog",
|
||||
description: frontmatter.description || "",
|
||||
lastUpdated,
|
||||
entries,
|
||||
total: totalItems,
|
||||
}
|
||||
}
|
||||
18
apps/changelog-api/tsconfig.json
Normal file
18
apps/changelog-api/tsconfig.json
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": ["ESNext"],
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
9
apps/changelog-api/wrangler.jsonc
Normal file
9
apps/changelog-api/wrangler.jsonc
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"$schema": "node_modules/wrangler/config-schema.json",
|
||||
"name": "changelog-api",
|
||||
"main": "src/index.ts",
|
||||
"compatibility_date": "2025-01-01",
|
||||
"observability": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue