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.

476 lines (465 loc) 12.6 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"; class APISkill { registryPath; restishConfigPath; registry; constructor() { this.registryPath = path.join( os.homedir(), ".stackmemory", "api-registry.json" ); this.restishConfigPath = process.platform === "darwin" ? path.join( os.homedir(), "Library", "Application Support", "restish", "apis.json" ) : path.join(os.homedir(), ".config", "restish", "apis.json"); this.registry = this.loadRegistry(); } loadRegistry() { try { if (fs.existsSync(this.registryPath)) { return JSON.parse(fs.readFileSync(this.registryPath, "utf-8")); } } catch (error) { logger.warn("Failed to load API registry:", error); } return { apis: {}, version: "1.0.0" }; } saveRegistry() { const dir = path.dirname(this.registryPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(this.registryPath, JSON.stringify(this.registry, null, 2)); } /** * Load restish config */ loadRestishConfig() { try { if (fs.existsSync(this.restishConfigPath)) { return JSON.parse(fs.readFileSync(this.restishConfigPath, "utf-8")); } } catch (error) { logger.warn("Failed to load restish config:", error); } return {}; } /** * Save restish config */ saveRestishConfig(config) { const dir = path.dirname(this.restishConfigPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(this.restishConfigPath, JSON.stringify(config, null, 2)); } /** * Check if restish is installed */ checkRestish() { try { execSync("which restish", { stdio: "pipe" }); return true; } catch { return false; } } /** * Add/register a new API */ async add(name, baseUrl, options) { if (!this.checkRestish()) { return { success: false, message: "restish not installed. Run: brew install restish" }; } try { const restishConfig = this.loadRestishConfig(); const apiConfig = { base: baseUrl }; if (options?.spec) { apiConfig.spec_files = [options.spec]; } if (options?.authType === "api-key" && options?.envVar) { apiConfig.profiles = { default: { headers: { [options.headerName || "Authorization"]: `$${options.envVar}` } } }; } restishConfig[name] = apiConfig; this.saveRestishConfig(restishConfig); const config = { name, baseUrl, specUrl: options?.spec, authType: options?.authType || "none", authConfig: { headerName: options?.headerName || "Authorization", envVar: options?.envVar }, registeredAt: (/* @__PURE__ */ new Date()).toISOString() }; if (options?.spec) { config.specUrl = options.spec; } this.registry.apis[name] = config; this.saveRegistry(); return { success: true, message: `API '${name}' registered successfully`, data: { name, baseUrl, authType: config.authType, operations: config.operations?.length || "auto-discovered" } }; } catch (error) { logger.error("Failed to add API:", error); return { success: false, message: `Failed to register API: ${error.message}` }; } } /** * Discover available operations for an API */ discoverOperations(apiName) { try { const output = execSync(`restish ${apiName} --help 2>&1`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); const operations = []; const lines = output.split("\n"); let inCommands = false; for (const line of lines) { if (line.includes("Available Commands:")) { inCommands = true; continue; } if (inCommands && line.trim()) { const match = line.match(/^\s+(\S+)/); if (match && !line.includes("help")) { operations.push(match[1]); } } if (inCommands && line.includes("Flags:")) { break; } } return operations; } catch { return []; } } /** * List registered APIs */ async list() { const apis = Object.values(this.registry.apis); if (apis.length === 0) { return { success: true, message: "No APIs registered. Use /api add <name> <url> to register one.", data: [] }; } return { success: true, message: `${apis.length} API(s) registered`, data: apis.map((api) => ({ name: api.name, baseUrl: api.baseUrl, authType: api.authType, operations: api.operations?.length || "unknown", registeredAt: api.registeredAt })) }; } /** * Show details for a specific API */ async describe(apiName, operation) { const api = this.registry.apis[apiName]; if (!api) { try { const output = execSync(`restish api show ${apiName}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); return { success: true, message: `API '${apiName}' (from restish config)`, data: { raw: output } }; } catch { return { success: false, message: `API '${apiName}' not found` }; } } if (operation) { try { const output = execSync(`restish ${apiName} ${operation} --help`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }); return { success: true, message: `Operation: ${apiName}.${operation}`, data: { operation, help: output } }; } catch { return { success: false, message: `Operation '${operation}' not found for API '${apiName}'` }; } } const operations = this.discoverOperations(apiName); api.operations = operations; this.saveRegistry(); return { success: true, message: `API: ${apiName}`, data: { ...api, operations } }; } /** * Execute an API operation */ async exec(apiName, operation, params, options) { if (!this.checkRestish()) { return { success: false, message: "restish not installed. Run: brew install restish" }; } const api = this.registry.apis[apiName]; if (!api) { return { success: false, message: `API '${apiName}' not registered. Use /api add first.` }; } const urlPath = operation.startsWith("/") ? operation : `/${operation}`; const fullUrl = `${api.baseUrl}${urlPath}`; const args = ["get", fullUrl]; if (options?.raw) { args.push("--rsh-raw"); } if (options?.filter) { args.push("--rsh-filter", options.filter); } if (api?.authConfig?.envVar) { const token = process.env[api.authConfig.envVar]; if (token) { const headerName = api.authConfig.headerName || "Authorization"; args.push("-H", `${headerName}:${token}`); } } if (options?.headers) { for (const [key, value] of Object.entries(options.headers)) { args.push("-H", `${key}:${value}`); } } if (params) { for (const [key, value] of Object.entries(params)) { args.push("-q", `${key}=${String(value)}`); } } args.push("-o", "json"); try { logger.info(`Executing: restish ${args.join(" ")}`); const output = execSync(`restish ${args.join(" ")}`, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], env: process.env }); let data; try { data = JSON.parse(output); } catch { data = output; } return { success: true, message: `${apiName} ${operation} executed`, data }; } catch (error) { const stderr = error.stderr?.toString() || error.message; logger.error(`API exec failed:`, stderr); return { success: false, message: `API call failed: ${stderr}` }; } } /** * Configure authentication for an API */ async auth(apiName, options) { const api = this.registry.apis[apiName]; if (!api) { return { success: false, message: `API '${apiName}' not registered. Use /api add first.` }; } if (options.token) { const envVar = options.envVar || `${apiName.toUpperCase()}_API_KEY`; process.env[envVar] = options.token; api.authType = "api-key"; api.authConfig = { ...api.authConfig, envVar }; this.saveRegistry(); return { success: true, message: `Auth configured for '${apiName}'. Token stored in ${envVar}`, data: { envVar } }; } if (options.oauth) { try { const scopeArg = options.scopes ? `--scopes=${options.scopes.join(",")}` : ""; execSync(`restish api configure ${apiName} --auth=oauth2 ${scopeArg}`, { stdio: "inherit" }); api.authType = "oauth2"; this.saveRegistry(); return { success: true, message: `OAuth2 configured for '${apiName}'` }; } catch (error) { return { success: false, message: `OAuth setup failed: ${error.message}` }; } } return { success: false, message: "Specify --token or --oauth" }; } /** * Remove an API */ async remove(apiName) { if (!this.registry.apis[apiName]) { return { success: false, message: `API '${apiName}' not found` }; } delete this.registry.apis[apiName]; this.saveRegistry(); return { success: true, message: `API '${apiName}' removed` }; } /** * Sync API spec (refresh operations) */ async sync(apiName) { if (!this.checkRestish()) { return { success: false, message: "restish not installed. Run: brew install restish" }; } try { execSync(`restish api sync ${apiName}`, { stdio: "pipe" }); const operations = this.discoverOperations(apiName); if (this.registry.apis[apiName]) { this.registry.apis[apiName].operations = operations; this.saveRegistry(); } return { success: true, message: `API '${apiName}' synced`, data: { operations } }; } catch (error) { return { success: false, message: `Sync failed: ${error.message}` }; } } /** * Get help for the API skill */ getHelp() { return ` /api - OpenAPI-based API access via Restish Commands: /api add <name> <url> [--spec <url>] [--auth-type api-key|oauth2] Register a new API /api list List all registered APIs /api describe <name> [operation] Show API details or specific operation /api exec <name> <operation> [--param value...] Execute an API operation /api auth <name> --token <token> [--env-var NAME] Configure API authentication /api auth <name> --oauth [--scopes scope1,scope2] Configure OAuth2 authentication /api sync <name> Refresh API operations from spec /api remove <name> Remove a registered API Examples: /api add github https://api.github.com --spec https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json /api auth github --token "$GITHUB_TOKEN" /api exec github repos list-for-user --username octocat /api exec github issues list --owner microsoft --repo vscode --state open Built on restish (https://rest.sh) for automatic OpenAPI discovery. `; } } let apiSkillInstance = null; function getAPISkill() { if (!apiSkillInstance) { apiSkillInstance = new APISkill(); } return apiSkillInstance; } export { APISkill, getAPISkill }; //# sourceMappingURL=api-skill.js.map