@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
JavaScript
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