@stackmemoryai/stackmemory 0.5.3 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/claude-sm +6 -0
- package/bin/claude-smd +6 -0
- package/dist/cli/claude-sm-danger.js +20 -0
- package/dist/cli/claude-sm-danger.js.map +7 -0
- package/dist/cli/commands/api.js +228 -0
- package/dist/cli/commands/api.js.map +7 -0
- package/dist/cli/commands/cleanup-processes.js +64 -0
- package/dist/cli/commands/cleanup-processes.js.map +7 -0
- package/dist/cli/commands/hooks.js +294 -0
- package/dist/cli/commands/hooks.js.map +7 -0
- package/dist/cli/commands/shell.js +248 -0
- package/dist/cli/commands/shell.js.map +7 -0
- package/dist/cli/commands/sweep.js +173 -5
- package/dist/cli/commands/sweep.js.map +3 -3
- package/dist/cli/index.js +9 -1
- package/dist/cli/index.js.map +2 -2
- package/dist/hooks/config.js +146 -0
- package/dist/hooks/config.js.map +7 -0
- package/dist/hooks/daemon.js +360 -0
- package/dist/hooks/daemon.js.map +7 -0
- package/dist/hooks/events.js +51 -0
- package/dist/hooks/events.js.map +7 -0
- package/dist/hooks/index.js +4 -0
- package/dist/hooks/index.js.map +7 -0
- package/dist/skills/api-discovery.js +349 -0
- package/dist/skills/api-discovery.js.map +7 -0
- package/dist/skills/api-skill.js +471 -0
- package/dist/skills/api-skill.js.map +7 -0
- package/dist/skills/claude-skills.js +49 -1
- package/dist/skills/claude-skills.js.map +2 -2
- package/dist/utils/process-cleanup.js +132 -0
- package/dist/utils/process-cleanup.js.map +7 -0
- package/package.json +4 -2
- package/scripts/install-sweep-hook.sh +89 -0
- package/templates/claude-hooks/post-edit-sweep.js +437 -0
- package/templates/shell/sweep-complete.zsh +116 -0
- package/templates/shell/sweep-suggest.js +161 -0
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { execSync } from "child_process";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import * as path from "path";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import { logger } from "../core/monitoring/logger.js";
|
|
6
|
+
import { getAPISkill } from "./api-skill.js";
|
|
7
|
+
const API_PATTERNS = [
|
|
8
|
+
// Direct API URLs
|
|
9
|
+
{
|
|
10
|
+
pattern: /https?:\/\/api\.([a-z0-9-]+)\.(com|io|dev|app|co)/,
|
|
11
|
+
nameGroup: 1
|
|
12
|
+
},
|
|
13
|
+
// REST API paths in docs
|
|
14
|
+
{ pattern: /https?:\/\/([a-z0-9-]+)\.com\/api/, nameGroup: 1 },
|
|
15
|
+
// Developer docs
|
|
16
|
+
{ pattern: /https?:\/\/developer\.([a-z0-9-]+)\.com/, nameGroup: 1 },
|
|
17
|
+
// Docs subdomains
|
|
18
|
+
{ pattern: /https?:\/\/docs\.([a-z0-9-]+)\.(com|io|dev)/, nameGroup: 1 }
|
|
19
|
+
];
|
|
20
|
+
const KNOWN_SPECS = {
|
|
21
|
+
github: "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json",
|
|
22
|
+
stripe: "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json",
|
|
23
|
+
twilio: "https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json",
|
|
24
|
+
slack: "https://api.slack.com/specs/openapi/v2/slack_web.json",
|
|
25
|
+
discord: "https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json",
|
|
26
|
+
openai: "https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml",
|
|
27
|
+
anthropic: "https://raw.githubusercontent.com/anthropics/anthropic-sdk-python/main/openapi.json",
|
|
28
|
+
linear: "https://api.linear.app/graphql",
|
|
29
|
+
// GraphQL, not REST
|
|
30
|
+
notion: "https://raw.githubusercontent.com/NotionX/notion-sdk-js/main/openapi.json",
|
|
31
|
+
vercel: "https://openapi.vercel.sh/",
|
|
32
|
+
cloudflare: "https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json",
|
|
33
|
+
// Google Cloud Platform - uses Google Discovery format
|
|
34
|
+
gcp: "https://www.googleapis.com/discovery/v1/apis",
|
|
35
|
+
"gcp-compute": "https://compute.googleapis.com/$discovery/rest?version=v1",
|
|
36
|
+
"gcp-storage": "https://storage.googleapis.com/$discovery/rest?version=v1",
|
|
37
|
+
"gcp-run": "https://run.googleapis.com/$discovery/rest?version=v2",
|
|
38
|
+
"gcp-functions": "https://cloudfunctions.googleapis.com/$discovery/rest?version=v2",
|
|
39
|
+
"gcp-bigquery": "https://bigquery.googleapis.com/$discovery/rest?version=v2",
|
|
40
|
+
"gcp-aiplatform": "https://aiplatform.googleapis.com/$discovery/rest?version=v1",
|
|
41
|
+
// Railway - GraphQL API
|
|
42
|
+
railway: "https://backboard.railway.com/graphql/v2"
|
|
43
|
+
// GraphQL endpoint
|
|
44
|
+
};
|
|
45
|
+
const KNOWN_BASES = {
|
|
46
|
+
github: "https://api.github.com",
|
|
47
|
+
stripe: "https://api.stripe.com",
|
|
48
|
+
twilio: "https://api.twilio.com",
|
|
49
|
+
slack: "https://slack.com/api",
|
|
50
|
+
discord: "https://discord.com/api",
|
|
51
|
+
openai: "https://api.openai.com",
|
|
52
|
+
anthropic: "https://api.anthropic.com",
|
|
53
|
+
linear: "https://api.linear.app",
|
|
54
|
+
notion: "https://api.notion.com",
|
|
55
|
+
vercel: "https://api.vercel.com",
|
|
56
|
+
cloudflare: "https://api.cloudflare.com",
|
|
57
|
+
// Google Cloud Platform
|
|
58
|
+
gcp: "https://www.googleapis.com",
|
|
59
|
+
"gcp-compute": "https://compute.googleapis.com",
|
|
60
|
+
"gcp-storage": "https://storage.googleapis.com",
|
|
61
|
+
"gcp-run": "https://run.googleapis.com",
|
|
62
|
+
"gcp-functions": "https://cloudfunctions.googleapis.com",
|
|
63
|
+
"gcp-bigquery": "https://bigquery.googleapis.com",
|
|
64
|
+
"gcp-aiplatform": "https://aiplatform.googleapis.com",
|
|
65
|
+
// Railway (GraphQL)
|
|
66
|
+
railway: "https://backboard.railway.com/graphql/v2"
|
|
67
|
+
};
|
|
68
|
+
const API_TYPES = {
|
|
69
|
+
railway: "graphql",
|
|
70
|
+
linear: "graphql",
|
|
71
|
+
gcp: "google-discovery",
|
|
72
|
+
"gcp-compute": "google-discovery",
|
|
73
|
+
"gcp-storage": "google-discovery",
|
|
74
|
+
"gcp-run": "google-discovery",
|
|
75
|
+
"gcp-functions": "google-discovery",
|
|
76
|
+
"gcp-bigquery": "google-discovery",
|
|
77
|
+
"gcp-aiplatform": "google-discovery"
|
|
78
|
+
};
|
|
79
|
+
class APIDiscoverySkill {
|
|
80
|
+
discoveryLog;
|
|
81
|
+
discoveredAPIs = /* @__PURE__ */ new Map();
|
|
82
|
+
constructor() {
|
|
83
|
+
this.discoveryLog = path.join(
|
|
84
|
+
os.homedir(),
|
|
85
|
+
".stackmemory",
|
|
86
|
+
"api-discovery.log"
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Analyze a URL for potential API endpoints
|
|
91
|
+
*/
|
|
92
|
+
analyzeUrl(url) {
|
|
93
|
+
if (url.includes("googleapis.com")) {
|
|
94
|
+
const gcpMatch = url.match(/https?:\/\/([a-z]+)\.googleapis\.com/);
|
|
95
|
+
if (gcpMatch) {
|
|
96
|
+
const service = gcpMatch[1];
|
|
97
|
+
const name = `gcp-${service}`;
|
|
98
|
+
return {
|
|
99
|
+
name,
|
|
100
|
+
baseUrl: `https://${service}.googleapis.com`,
|
|
101
|
+
specUrl: KNOWN_SPECS[name] || `https://${service}.googleapis.com/$discovery/rest?version=v1`,
|
|
102
|
+
source: "known",
|
|
103
|
+
confidence: 0.95,
|
|
104
|
+
apiType: "google-discovery"
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (url.includes("railway.com") || url.includes("railway.app")) {
|
|
109
|
+
return {
|
|
110
|
+
name: "railway",
|
|
111
|
+
baseUrl: KNOWN_BASES["railway"],
|
|
112
|
+
specUrl: KNOWN_SPECS["railway"],
|
|
113
|
+
source: "known",
|
|
114
|
+
confidence: 0.95,
|
|
115
|
+
apiType: "graphql"
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
for (const [name, baseUrl] of Object.entries(KNOWN_BASES)) {
|
|
119
|
+
if (url.includes(name) || url.includes(baseUrl)) {
|
|
120
|
+
return {
|
|
121
|
+
name,
|
|
122
|
+
baseUrl,
|
|
123
|
+
specUrl: KNOWN_SPECS[name],
|
|
124
|
+
source: "known",
|
|
125
|
+
confidence: 0.95,
|
|
126
|
+
apiType: API_TYPES[name] || "rest"
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
for (const { pattern, nameGroup } of API_PATTERNS) {
|
|
131
|
+
const match = url.match(pattern);
|
|
132
|
+
if (match) {
|
|
133
|
+
const name = match[nameGroup].toLowerCase();
|
|
134
|
+
const baseUrl = this.inferBaseUrl(url, name);
|
|
135
|
+
return {
|
|
136
|
+
name,
|
|
137
|
+
baseUrl,
|
|
138
|
+
source: "inferred",
|
|
139
|
+
confidence: 0.7,
|
|
140
|
+
apiType: "rest"
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Infer base URL from a discovered URL
|
|
148
|
+
*/
|
|
149
|
+
inferBaseUrl(url, name) {
|
|
150
|
+
const patterns = [
|
|
151
|
+
`https://api.${name}.com`,
|
|
152
|
+
`https://api.${name}.io`,
|
|
153
|
+
`https://${name}.com/api`
|
|
154
|
+
];
|
|
155
|
+
try {
|
|
156
|
+
const urlObj = new URL(url);
|
|
157
|
+
if (urlObj.hostname.startsWith("api.")) {
|
|
158
|
+
return `${urlObj.protocol}//${urlObj.hostname}`;
|
|
159
|
+
}
|
|
160
|
+
if (urlObj.pathname.includes("/api")) {
|
|
161
|
+
return `${urlObj.protocol}//${urlObj.hostname}/api`;
|
|
162
|
+
}
|
|
163
|
+
return `${urlObj.protocol}//${urlObj.hostname}`;
|
|
164
|
+
} catch {
|
|
165
|
+
return patterns[0];
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Try to discover OpenAPI spec for a service
|
|
170
|
+
*/
|
|
171
|
+
async discoverSpec(name, baseUrl) {
|
|
172
|
+
if (KNOWN_SPECS[name]) {
|
|
173
|
+
return KNOWN_SPECS[name];
|
|
174
|
+
}
|
|
175
|
+
const specPaths = [
|
|
176
|
+
"/openapi.json",
|
|
177
|
+
"/openapi.yaml",
|
|
178
|
+
"/swagger.json",
|
|
179
|
+
"/swagger.yaml",
|
|
180
|
+
"/api-docs",
|
|
181
|
+
"/v1/openapi.json",
|
|
182
|
+
"/v2/openapi.json",
|
|
183
|
+
"/docs/openapi.json",
|
|
184
|
+
"/.well-known/openapi.json"
|
|
185
|
+
];
|
|
186
|
+
for (const specPath of specPaths) {
|
|
187
|
+
const specUrl = `${baseUrl}${specPath}`;
|
|
188
|
+
try {
|
|
189
|
+
execSync(`curl -sI --max-time 2 "${specUrl}" | grep -q "200 OK"`, {
|
|
190
|
+
stdio: "pipe"
|
|
191
|
+
});
|
|
192
|
+
return specUrl;
|
|
193
|
+
} catch {
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Process a URL and auto-register if it's an API
|
|
200
|
+
*/
|
|
201
|
+
async processUrl(url, autoRegister = true) {
|
|
202
|
+
const discovered = this.analyzeUrl(url);
|
|
203
|
+
if (!discovered) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
if (this.discoveredAPIs.has(discovered.name)) {
|
|
207
|
+
return this.discoveredAPIs.get(discovered.name);
|
|
208
|
+
}
|
|
209
|
+
if (!discovered.specUrl && discovered.source !== "known") {
|
|
210
|
+
try {
|
|
211
|
+
discovered.specUrl = await this.discoverSpec(discovered.name, discovered.baseUrl) || void 0;
|
|
212
|
+
} catch {
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
this.discoveredAPIs.set(discovered.name, discovered);
|
|
216
|
+
this.logDiscovery(discovered, url);
|
|
217
|
+
if (autoRegister && discovered.confidence >= 0.7) {
|
|
218
|
+
await this.registerAPI(discovered);
|
|
219
|
+
}
|
|
220
|
+
return discovered;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Register a discovered API
|
|
224
|
+
*/
|
|
225
|
+
async registerAPI(api) {
|
|
226
|
+
const skill = getAPISkill();
|
|
227
|
+
try {
|
|
228
|
+
const result = await skill.add(api.name, api.baseUrl, {
|
|
229
|
+
spec: api.specUrl
|
|
230
|
+
});
|
|
231
|
+
if (result.success) {
|
|
232
|
+
logger.info(`Auto-registered API: ${api.name}`);
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
} catch (error) {
|
|
236
|
+
logger.warn(`Failed to auto-register API ${api.name}:`, error);
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Log discovery for debugging
|
|
242
|
+
*/
|
|
243
|
+
logDiscovery(api, sourceUrl) {
|
|
244
|
+
const entry = {
|
|
245
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
246
|
+
api,
|
|
247
|
+
sourceUrl
|
|
248
|
+
};
|
|
249
|
+
try {
|
|
250
|
+
const dir = path.dirname(this.discoveryLog);
|
|
251
|
+
if (!fs.existsSync(dir)) {
|
|
252
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
253
|
+
}
|
|
254
|
+
fs.appendFileSync(this.discoveryLog, JSON.stringify(entry) + "\n");
|
|
255
|
+
} catch (error) {
|
|
256
|
+
logger.warn("Failed to log API discovery:", error);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Get all discovered APIs
|
|
261
|
+
*/
|
|
262
|
+
getDiscoveredAPIs() {
|
|
263
|
+
return Array.from(this.discoveredAPIs.values());
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Suggest API registration based on recent activity
|
|
267
|
+
*/
|
|
268
|
+
async suggestFromContext(recentUrls) {
|
|
269
|
+
const result = {
|
|
270
|
+
discovered: [],
|
|
271
|
+
registered: [],
|
|
272
|
+
skipped: []
|
|
273
|
+
};
|
|
274
|
+
for (const url of recentUrls) {
|
|
275
|
+
const discovered = await this.processUrl(url, false);
|
|
276
|
+
if (discovered) {
|
|
277
|
+
result.discovered.push(discovered);
|
|
278
|
+
const skill = getAPISkill();
|
|
279
|
+
const listResult = await skill.list();
|
|
280
|
+
const existingAPIs = listResult.data || [];
|
|
281
|
+
if (existingAPIs.some((api) => api.name === discovered.name)) {
|
|
282
|
+
result.skipped.push(discovered.name);
|
|
283
|
+
} else if (discovered.confidence >= 0.7) {
|
|
284
|
+
const registered = await this.registerAPI(discovered);
|
|
285
|
+
if (registered) {
|
|
286
|
+
result.registered.push(discovered.name);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Get help text
|
|
295
|
+
*/
|
|
296
|
+
getHelp() {
|
|
297
|
+
const restAPIs = Object.keys(KNOWN_SPECS).filter(
|
|
298
|
+
(s) => !API_TYPES[s] || API_TYPES[s] === "rest"
|
|
299
|
+
);
|
|
300
|
+
const graphqlAPIs = Object.keys(KNOWN_SPECS).filter(
|
|
301
|
+
(s) => API_TYPES[s] === "graphql"
|
|
302
|
+
);
|
|
303
|
+
const gcpAPIs = Object.keys(KNOWN_SPECS).filter(
|
|
304
|
+
(s) => API_TYPES[s] === "google-discovery"
|
|
305
|
+
);
|
|
306
|
+
return `
|
|
307
|
+
API Auto-Discovery
|
|
308
|
+
|
|
309
|
+
Automatically detects and registers APIs when you browse documentation.
|
|
310
|
+
|
|
311
|
+
REST APIs (OpenAPI specs):
|
|
312
|
+
${restAPIs.map((s) => ` - ${s}`).join("\n")}
|
|
313
|
+
|
|
314
|
+
GraphQL APIs:
|
|
315
|
+
${graphqlAPIs.map((s) => ` - ${s}`).join("\n")}
|
|
316
|
+
|
|
317
|
+
Google Cloud Platform (Discovery format):
|
|
318
|
+
${gcpAPIs.map((s) => ` - ${s}`).join("\n")}
|
|
319
|
+
|
|
320
|
+
How It Works:
|
|
321
|
+
1. Monitors URLs you access during development
|
|
322
|
+
2. Identifies API documentation and endpoints
|
|
323
|
+
3. Finds OpenAPI specs automatically
|
|
324
|
+
4. Registers APIs for easy access via /api exec
|
|
325
|
+
|
|
326
|
+
Usage:
|
|
327
|
+
# Check if a URL is a known API
|
|
328
|
+
stackmemory api discover <url>
|
|
329
|
+
|
|
330
|
+
# List discovered APIs
|
|
331
|
+
stackmemory api discovered
|
|
332
|
+
|
|
333
|
+
# Register all discovered APIs
|
|
334
|
+
stackmemory api register-discovered
|
|
335
|
+
`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
let discoveryInstance = null;
|
|
339
|
+
function getAPIDiscovery() {
|
|
340
|
+
if (!discoveryInstance) {
|
|
341
|
+
discoveryInstance = new APIDiscoverySkill();
|
|
342
|
+
}
|
|
343
|
+
return discoveryInstance;
|
|
344
|
+
}
|
|
345
|
+
export {
|
|
346
|
+
APIDiscoverySkill,
|
|
347
|
+
getAPIDiscovery
|
|
348
|
+
};
|
|
349
|
+
//# sourceMappingURL=api-discovery.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/skills/api-discovery.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * API Auto-Discovery Skill\n *\n * Automatically detects API endpoints and OpenAPI specs when Claude\n * reads documentation or API URLs, then registers them for easy access.\n */\n\nimport { execSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport * as os from 'os';\nimport { logger } from '../core/monitoring/logger.js';\nimport { getAPISkill } from './api-skill.js';\n\n// Common API documentation patterns\nconst API_PATTERNS = [\n // Direct API URLs\n {\n pattern: /https?:\\/\\/api\\.([a-z0-9-]+)\\.(com|io|dev|app|co)/,\n nameGroup: 1,\n },\n // REST API paths in docs\n { pattern: /https?:\\/\\/([a-z0-9-]+)\\.com\\/api/, nameGroup: 1 },\n // Developer docs\n { pattern: /https?:\\/\\/developer\\.([a-z0-9-]+)\\.com/, nameGroup: 1 },\n // Docs subdomains\n { pattern: /https?:\\/\\/docs\\.([a-z0-9-]+)\\.(com|io|dev)/, nameGroup: 1 },\n];\n\n// Known OpenAPI spec locations for popular services\nconst KNOWN_SPECS: Record<string, string> = {\n github:\n 'https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json',\n stripe:\n 'https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json',\n twilio:\n 'https://raw.githubusercontent.com/twilio/twilio-oai/main/spec/json/twilio_api_v2010.json',\n slack: 'https://api.slack.com/specs/openapi/v2/slack_web.json',\n discord:\n 'https://raw.githubusercontent.com/discord/discord-api-spec/main/specs/openapi.json',\n openai:\n 'https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml',\n anthropic:\n 'https://raw.githubusercontent.com/anthropics/anthropic-sdk-python/main/openapi.json',\n linear: 'https://api.linear.app/graphql', // GraphQL, not REST\n notion:\n 'https://raw.githubusercontent.com/NotionX/notion-sdk-js/main/openapi.json',\n vercel: 'https://openapi.vercel.sh/',\n cloudflare:\n 'https://raw.githubusercontent.com/cloudflare/api-schemas/main/openapi.json',\n // Google Cloud Platform - uses Google Discovery format\n gcp: 'https://www.googleapis.com/discovery/v1/apis',\n 'gcp-compute': 'https://compute.googleapis.com/$discovery/rest?version=v1',\n 'gcp-storage': 'https://storage.googleapis.com/$discovery/rest?version=v1',\n 'gcp-run': 'https://run.googleapis.com/$discovery/rest?version=v2',\n 'gcp-functions':\n 'https://cloudfunctions.googleapis.com/$discovery/rest?version=v2',\n 'gcp-bigquery': 'https://bigquery.googleapis.com/$discovery/rest?version=v2',\n 'gcp-aiplatform':\n 'https://aiplatform.googleapis.com/$discovery/rest?version=v1',\n // Railway - GraphQL API\n railway: 'https://backboard.railway.com/graphql/v2', // GraphQL endpoint\n};\n\n// Known base URLs for popular services\nconst KNOWN_BASES: Record<string, string> = {\n github: 'https://api.github.com',\n stripe: 'https://api.stripe.com',\n twilio: 'https://api.twilio.com',\n slack: 'https://slack.com/api',\n discord: 'https://discord.com/api',\n openai: 'https://api.openai.com',\n anthropic: 'https://api.anthropic.com',\n linear: 'https://api.linear.app',\n notion: 'https://api.notion.com',\n vercel: 'https://api.vercel.com',\n cloudflare: 'https://api.cloudflare.com',\n // Google Cloud Platform\n gcp: 'https://www.googleapis.com',\n 'gcp-compute': 'https://compute.googleapis.com',\n 'gcp-storage': 'https://storage.googleapis.com',\n 'gcp-run': 'https://run.googleapis.com',\n 'gcp-functions': 'https://cloudfunctions.googleapis.com',\n 'gcp-bigquery': 'https://bigquery.googleapis.com',\n 'gcp-aiplatform': 'https://aiplatform.googleapis.com',\n // Railway (GraphQL)\n railway: 'https://backboard.railway.com/graphql/v2',\n};\n\n// API types for special handling\nconst API_TYPES: Record<string, 'rest' | 'graphql' | 'google-discovery'> = {\n railway: 'graphql',\n linear: 'graphql',\n gcp: 'google-discovery',\n 'gcp-compute': 'google-discovery',\n 'gcp-storage': 'google-discovery',\n 'gcp-run': 'google-discovery',\n 'gcp-functions': 'google-discovery',\n 'gcp-bigquery': 'google-discovery',\n 'gcp-aiplatform': 'google-discovery',\n};\n\nexport interface DiscoveredAPI {\n name: string;\n baseUrl: string;\n specUrl?: string;\n source: 'url' | 'docs' | 'known' | 'inferred';\n confidence: number; // 0-1\n apiType?: 'rest' | 'graphql' | 'google-discovery';\n}\n\nexport interface DiscoveryResult {\n discovered: DiscoveredAPI[];\n registered: string[];\n skipped: string[];\n}\n\nexport class APIDiscoverySkill {\n private discoveryLog: string;\n private discoveredAPIs: Map<string, DiscoveredAPI> = new Map();\n\n constructor() {\n this.discoveryLog = path.join(\n os.homedir(),\n '.stackmemory',\n 'api-discovery.log'\n );\n }\n\n /**\n * Analyze a URL for potential API endpoints\n */\n analyzeUrl(url: string): DiscoveredAPI | null {\n // Check for GCP URLs first (special pattern)\n if (url.includes('googleapis.com')) {\n const gcpMatch = url.match(/https?:\\/\\/([a-z]+)\\.googleapis\\.com/);\n if (gcpMatch) {\n const service = gcpMatch[1];\n const name = `gcp-${service}`;\n return {\n name,\n baseUrl: `https://${service}.googleapis.com`,\n specUrl:\n KNOWN_SPECS[name] ||\n `https://${service}.googleapis.com/$discovery/rest?version=v1`,\n source: 'known',\n confidence: 0.95,\n apiType: 'google-discovery',\n };\n }\n }\n\n // Check for Railway\n if (url.includes('railway.com') || url.includes('railway.app')) {\n return {\n name: 'railway',\n baseUrl: KNOWN_BASES['railway'],\n specUrl: KNOWN_SPECS['railway'],\n source: 'known',\n confidence: 0.95,\n apiType: 'graphql',\n };\n }\n\n // Check if it's a known service\n for (const [name, baseUrl] of Object.entries(KNOWN_BASES)) {\n if (url.includes(name) || url.includes(baseUrl)) {\n return {\n name,\n baseUrl,\n specUrl: KNOWN_SPECS[name],\n source: 'known',\n confidence: 0.95,\n apiType: API_TYPES[name] || 'rest',\n };\n }\n }\n\n // Try to match API patterns\n for (const { pattern, nameGroup } of API_PATTERNS) {\n const match = url.match(pattern);\n if (match) {\n const name = match[nameGroup].toLowerCase();\n const baseUrl = this.inferBaseUrl(url, name);\n\n return {\n name,\n baseUrl,\n source: 'inferred',\n confidence: 0.7,\n apiType: 'rest',\n };\n }\n }\n\n return null;\n }\n\n /**\n * Infer base URL from a discovered URL\n */\n private inferBaseUrl(url: string, name: string): string {\n // Try common patterns\n const patterns = [\n `https://api.${name}.com`,\n `https://api.${name}.io`,\n `https://${name}.com/api`,\n ];\n\n // Extract domain from URL\n try {\n const urlObj = new URL(url);\n if (urlObj.hostname.startsWith('api.')) {\n return `${urlObj.protocol}//${urlObj.hostname}`;\n }\n if (urlObj.pathname.includes('/api')) {\n return `${urlObj.protocol}//${urlObj.hostname}/api`;\n }\n return `${urlObj.protocol}//${urlObj.hostname}`;\n } catch {\n return patterns[0];\n }\n }\n\n /**\n * Try to discover OpenAPI spec for a service\n */\n async discoverSpec(name: string, baseUrl: string): Promise<string | null> {\n // Check known specs first\n if (KNOWN_SPECS[name]) {\n return KNOWN_SPECS[name];\n }\n\n // Try common spec locations\n const specPaths = [\n '/openapi.json',\n '/openapi.yaml',\n '/swagger.json',\n '/swagger.yaml',\n '/api-docs',\n '/v1/openapi.json',\n '/v2/openapi.json',\n '/docs/openapi.json',\n '/.well-known/openapi.json',\n ];\n\n for (const specPath of specPaths) {\n const specUrl = `${baseUrl}${specPath}`;\n try {\n // Quick HEAD request to check if spec exists\n execSync(`curl -sI --max-time 2 \"${specUrl}\" | grep -q \"200 OK\"`, {\n stdio: 'pipe',\n });\n return specUrl;\n } catch {\n // Spec not found at this location\n }\n }\n\n return null;\n }\n\n /**\n * Process a URL and auto-register if it's an API\n */\n async processUrl(\n url: string,\n autoRegister: boolean = true\n ): Promise<DiscoveredAPI | null> {\n const discovered = this.analyzeUrl(url);\n\n if (!discovered) {\n return null;\n }\n\n // Check if already discovered\n if (this.discoveredAPIs.has(discovered.name)) {\n return this.discoveredAPIs.get(discovered.name)!;\n }\n\n // Only probe for spec if it's not a known service (known services already have spec URLs)\n if (!discovered.specUrl && discovered.source !== 'known') {\n // Try to find OpenAPI spec (with timeout protection)\n try {\n discovered.specUrl =\n (await this.discoverSpec(discovered.name, discovered.baseUrl)) ||\n undefined;\n } catch {\n // Spec discovery failed, continue without\n }\n }\n\n this.discoveredAPIs.set(discovered.name, discovered);\n this.logDiscovery(discovered, url);\n\n // Auto-register if enabled and confidence is high enough\n if (autoRegister && discovered.confidence >= 0.7) {\n await this.registerAPI(discovered);\n }\n\n return discovered;\n }\n\n /**\n * Register a discovered API\n */\n async registerAPI(api: DiscoveredAPI): Promise<boolean> {\n const skill = getAPISkill();\n\n try {\n const result = await skill.add(api.name, api.baseUrl, {\n spec: api.specUrl,\n });\n\n if (result.success) {\n logger.info(`Auto-registered API: ${api.name}`);\n return true;\n }\n } catch (error) {\n logger.warn(`Failed to auto-register API ${api.name}:`, error);\n }\n\n return false;\n }\n\n /**\n * Log discovery for debugging\n */\n private logDiscovery(api: DiscoveredAPI, sourceUrl: string): void {\n const entry = {\n timestamp: new Date().toISOString(),\n api,\n sourceUrl,\n };\n\n try {\n const dir = path.dirname(this.discoveryLog);\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true });\n }\n\n fs.appendFileSync(this.discoveryLog, JSON.stringify(entry) + '\\n');\n } catch (error) {\n logger.warn('Failed to log API discovery:', error);\n }\n }\n\n /**\n * Get all discovered APIs\n */\n getDiscoveredAPIs(): DiscoveredAPI[] {\n return Array.from(this.discoveredAPIs.values());\n }\n\n /**\n * Suggest API registration based on recent activity\n */\n async suggestFromContext(recentUrls: string[]): Promise<DiscoveryResult> {\n const result: DiscoveryResult = {\n discovered: [],\n registered: [],\n skipped: [],\n };\n\n for (const url of recentUrls) {\n const discovered = await this.processUrl(url, false);\n\n if (discovered) {\n result.discovered.push(discovered);\n\n // Check if already registered\n const skill = getAPISkill();\n const listResult = await skill.list();\n const existingAPIs = (listResult.data as Array<{ name: string }>) || [];\n\n if (existingAPIs.some((api) => api.name === discovered.name)) {\n result.skipped.push(discovered.name);\n } else if (discovered.confidence >= 0.7) {\n const registered = await this.registerAPI(discovered);\n if (registered) {\n result.registered.push(discovered.name);\n }\n }\n }\n }\n\n return result;\n }\n\n /**\n * Get help text\n */\n getHelp(): string {\n const restAPIs = Object.keys(KNOWN_SPECS).filter(\n (s) => !API_TYPES[s] || API_TYPES[s] === 'rest'\n );\n const graphqlAPIs = Object.keys(KNOWN_SPECS).filter(\n (s) => API_TYPES[s] === 'graphql'\n );\n const gcpAPIs = Object.keys(KNOWN_SPECS).filter(\n (s) => API_TYPES[s] === 'google-discovery'\n );\n\n return `\nAPI Auto-Discovery\n\nAutomatically detects and registers APIs when you browse documentation.\n\nREST APIs (OpenAPI specs):\n${restAPIs.map((s) => ` - ${s}`).join('\\n')}\n\nGraphQL APIs:\n${graphqlAPIs.map((s) => ` - ${s}`).join('\\n')}\n\nGoogle Cloud Platform (Discovery format):\n${gcpAPIs.map((s) => ` - ${s}`).join('\\n')}\n\nHow It Works:\n1. Monitors URLs you access during development\n2. Identifies API documentation and endpoints\n3. Finds OpenAPI specs automatically\n4. Registers APIs for easy access via /api exec\n\nUsage:\n # Check if a URL is a known API\n stackmemory api discover <url>\n\n # List discovered APIs\n stackmemory api discovered\n\n # Register all discovered APIs\n stackmemory api register-discovered\n`;\n }\n}\n\n// Singleton instance\nlet discoveryInstance: APIDiscoverySkill | null = null;\n\nexport function getAPIDiscovery(): APIDiscoverySkill {\n if (!discoveryInstance) {\n discoveryInstance = new APIDiscoverySkill();\n }\n return discoveryInstance;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|