UNPKG

@stackmemoryai/stackmemory

Version:

Project-scoped memory for AI coding tools. Durable context across sessions with MCP integration, frames, smart retrieval, Claude Code skills, and automatic hooks.

355 lines (346 loc) 10.9 kB
import { fileURLToPath as __fileURLToPath } from 'url'; import { dirname as __pathDirname } from 'path'; const __filename = __fileURLToPath(import.meta.url); const __dirname = __pathDirname(__filename); import { execSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; import * as os from "os"; import { logger } from "../core/monitoring/logger.js"; import { getAPISkill } from "./api-skill.js"; const API_PATTERNS = [ // Direct API URLs { pattern: /https?:\/\/api\.([a-z0-9-]+)\.(com|io|dev|app|co)/, nameGroup: 1 }, // REST API paths in docs { pattern: /https?:\/\/([a-z0-9-]+)\.com\/api/, nameGroup: 1 }, // Developer docs { pattern: /https?:\/\/developer\.([a-z0-9-]+)\.com/, nameGroup: 1 }, // Docs subdomains { pattern: /https?:\/\/docs\.([a-z0-9-]+)\.(com|io|dev)/, nameGroup: 1 } ]; const KNOWN_SPECS = { github: "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json", stripe: "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json", twilio: "https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json", slack: "https://api.slack.com/specs/openapi/v2/slack_web.json", discord: "https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json", openai: "https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml", anthropic: "https://raw.githubusercontent.com/anthropics/anthropic-sdk-python/main/openapi.json", linear: "https://api.linear.app/graphql", // GraphQL, not REST notion: "https://raw.githubusercontent.com/NotionX/notion-sdk-js/main/openapi.json", vercel: "https://openapi.vercel.sh/", cloudflare: "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json", // Google Cloud Platform - uses Google Discovery format gcp: "https://www.googleapis.com/discovery/v1/apis", "gcp-compute": "https://compute.googleapis.com/$discovery/rest?version=v1", "gcp-storage": "https://storage.googleapis.com/$discovery/rest?version=v1", "gcp-run": "https://run.googleapis.com/$discovery/rest?version=v2", "gcp-functions": "https://cloudfunctions.googleapis.com/$discovery/rest?version=v2", "gcp-bigquery": "https://bigquery.googleapis.com/$discovery/rest?version=v2", "gcp-aiplatform": "https://aiplatform.googleapis.com/$discovery/rest?version=v1", // Railway - GraphQL API railway: "https://backboard.railway.com/graphql/v2" // GraphQL endpoint }; const KNOWN_BASES = { github: "https://api.github.com", stripe: "https://api.stripe.com", twilio: "https://api.twilio.com", slack: "https://slack.com/api", discord: "https://discord.com/api", openai: "https://api.openai.com", anthropic: "https://api.anthropic.com", linear: "https://api.linear.app", notion: "https://api.notion.com", vercel: "https://api.vercel.com", cloudflare: "https://api.cloudflare.com", // Google Cloud Platform gcp: "https://www.googleapis.com", "gcp-compute": "https://compute.googleapis.com", "gcp-storage": "https://storage.googleapis.com", "gcp-run": "https://run.googleapis.com", "gcp-functions": "https://cloudfunctions.googleapis.com", "gcp-bigquery": "https://bigquery.googleapis.com", "gcp-aiplatform": "https://aiplatform.googleapis.com", // Railway (GraphQL) railway: "https://backboard.railway.com/graphql/v2" }; const API_TYPES = { railway: "graphql", linear: "graphql", gcp: "google-discovery", "gcp-compute": "google-discovery", "gcp-storage": "google-discovery", "gcp-run": "google-discovery", "gcp-functions": "google-discovery", "gcp-bigquery": "google-discovery", "gcp-aiplatform": "google-discovery" }; class APIDiscoverySkill { discoveryLog; discoveredAPIs = /* @__PURE__ */ new Map(); constructor() { this.discoveryLog = path.join( os.homedir(), ".stackmemory", "api-discovery.log" ); } /** * Analyze a URL for potential API endpoints */ analyzeUrl(url) { if (url.includes("googleapis.com")) { const gcpMatch = url.match(/https?:\/\/([a-z]+)\.googleapis\.com/); if (gcpMatch) { const service = gcpMatch[1]; const name = `gcp-${service}`; return { name, baseUrl: `https://${service}.googleapis.com`, specUrl: KNOWN_SPECS[name] || `https://${service}.googleapis.com/$discovery/rest?version=v1`, source: "known", confidence: 0.95, apiType: "google-discovery" }; } } if (url.includes("railway.com") || url.includes("railway.app")) { return { name: "railway", baseUrl: KNOWN_BASES["railway"], specUrl: KNOWN_SPECS["railway"], source: "known", confidence: 0.95, apiType: "graphql" }; } for (const [name, baseUrl] of Object.entries(KNOWN_BASES)) { if (url.includes(name) || url.includes(baseUrl)) { return { name, baseUrl, specUrl: KNOWN_SPECS[name], source: "known", confidence: 0.95, apiType: API_TYPES[name] || "rest" }; } } for (const { pattern, nameGroup } of API_PATTERNS) { const match = url.match(pattern); if (match) { const name = match[nameGroup].toLowerCase(); const baseUrl = this.inferBaseUrl(url, name); return { name, baseUrl, source: "inferred", confidence: 0.7, apiType: "rest" }; } } return null; } /** * Infer base URL from a discovered URL */ inferBaseUrl(url, name) { const patterns = [ `https://api.${name}.com`, `https://api.${name}.io`, `https://${name}.com/api` ]; try { const urlObj = new URL(url); if (urlObj.hostname.startsWith("api.")) { return `${urlObj.protocol}//${urlObj.hostname}`; } if (urlObj.pathname.includes("/api")) { return `${urlObj.protocol}//${urlObj.hostname}/api`; } return `${urlObj.protocol}//${urlObj.hostname}`; } catch { return patterns[0]; } } /** * Try to discover OpenAPI spec for a service */ async discoverSpec(name, baseUrl) { if (KNOWN_SPECS[name]) { return KNOWN_SPECS[name]; } const specPaths = [ "/openapi.json", "/openapi.yaml", "/swagger.json", "/swagger.yaml", "/api-docs", "/v1/openapi.json", "/v2/openapi.json", "/docs/openapi.json", "/.well-known/openapi.json" ]; for (const specPath of specPaths) { const specUrl = `${baseUrl}${specPath}`; try { execSync(`curl -sI --max-time 2 "${specUrl}" | grep -q "200 OK"`, { stdio: "pipe" }); return specUrl; } catch { } } return null; } /** * Process a URL and auto-register if it's an API */ async processUrl(url, autoRegister = true) { const discovered = this.analyzeUrl(url); if (!discovered) { return null; } const existing = this.discoveredAPIs.get(discovered.name); if (existing) { return existing; } if (!discovered.specUrl && discovered.source !== "known") { try { discovered.specUrl = await this.discoverSpec(discovered.name, discovered.baseUrl) || void 0; } catch { } } this.discoveredAPIs.set(discovered.name, discovered); this.logDiscovery(discovered, url); if (autoRegister && discovered.confidence >= 0.7) { await this.registerAPI(discovered); } return discovered; } /** * Register a discovered API */ async registerAPI(api) { const skill = getAPISkill(); try { const result = await skill.add(api.name, api.baseUrl, { spec: api.specUrl }); if (result.success) { logger.info(`Auto-registered API: ${api.name}`); return true; } } catch (error) { logger.warn(`Failed to auto-register API ${api.name}:`, error); } return false; } /** * Log discovery for debugging */ logDiscovery(api, sourceUrl) { const entry = { timestamp: (/* @__PURE__ */ new Date()).toISOString(), api, sourceUrl }; try { const dir = path.dirname(this.discoveryLog); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.appendFileSync(this.discoveryLog, JSON.stringify(entry) + "\n"); } catch (error) { logger.warn("Failed to log API discovery:", error); } } /** * Get all discovered APIs */ getDiscoveredAPIs() { return Array.from(this.discoveredAPIs.values()); } /** * Suggest API registration based on recent activity */ async suggestFromContext(recentUrls) { const result = { discovered: [], registered: [], skipped: [] }; for (const url of recentUrls) { const discovered = await this.processUrl(url, false); if (discovered) { result.discovered.push(discovered); const skill = getAPISkill(); const listResult = await skill.list(); const existingAPIs = listResult.data || []; if (existingAPIs.some((api) => api.name === discovered.name)) { result.skipped.push(discovered.name); } else if (discovered.confidence >= 0.7) { const registered = await this.registerAPI(discovered); if (registered) { result.registered.push(discovered.name); } } } } return result; } /** * Get help text */ getHelp() { const restAPIs = Object.keys(KNOWN_SPECS).filter( (s) => !API_TYPES[s] || API_TYPES[s] === "rest" ); const graphqlAPIs = Object.keys(KNOWN_SPECS).filter( (s) => API_TYPES[s] === "graphql" ); const gcpAPIs = Object.keys(KNOWN_SPECS).filter( (s) => API_TYPES[s] === "google-discovery" ); return ` API Auto-Discovery Automatically detects and registers APIs when you browse documentation. REST APIs (OpenAPI specs): ${restAPIs.map((s) => ` - ${s}`).join("\n")} GraphQL APIs: ${graphqlAPIs.map((s) => ` - ${s}`).join("\n")} Google Cloud Platform (Discovery format): ${gcpAPIs.map((s) => ` - ${s}`).join("\n")} How It Works: 1. Monitors URLs you access during development 2. Identifies API documentation and endpoints 3. Finds OpenAPI specs automatically 4. Registers APIs for easy access via /api exec Usage: # Check if a URL is a known API stackmemory api discover <url> # List discovered APIs stackmemory api discovered # Register all discovered APIs stackmemory api register-discovered `; } } let discoveryInstance = null; function getAPIDiscovery() { if (!discoveryInstance) { discoveryInstance = new APIDiscoverySkill(); } return discoveryInstance; } export { APIDiscoverySkill, getAPIDiscovery }; //# sourceMappingURL=api-discovery.js.map