@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
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";
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