agent-gateway-mcp 0.1.0
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/README.md +303 -0
- package/dist/auth.d.ts +9 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +272 -0
- package/dist/auth.js.map +1 -0
- package/dist/caller.d.ts +9 -0
- package/dist/caller.d.ts.map +1 -0
- package/dist/caller.js +110 -0
- package/dist/caller.js.map +1 -0
- package/dist/config.d.ts +32 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +288 -0
- package/dist/config.js.map +1 -0
- package/dist/discovery.d.ts +6 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +97 -0
- package/dist/discovery.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +509 -0
- package/dist/index.js.map +1 -0
- package/dist/init.d.ts +3 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +206 -0
- package/dist/init.js.map +1 -0
- package/dist/types.d.ts +143 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +27 -0
- package/src/auth.ts +329 -0
- package/src/caller.ts +130 -0
- package/src/config.ts +340 -0
- package/src/discovery.ts +144 -0
- package/src/index.ts +610 -0
- package/src/init.ts +254 -0
- package/src/types.ts +165 -0
- package/tsconfig.json +19 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import type {
|
|
5
|
+
GatewayConfig,
|
|
6
|
+
StoredToken,
|
|
7
|
+
Connection,
|
|
8
|
+
UserIdentity,
|
|
9
|
+
CloudSyncState,
|
|
10
|
+
CloudTokenBundle,
|
|
11
|
+
CacheEntry,
|
|
12
|
+
Manifest,
|
|
13
|
+
CapabilityDetail,
|
|
14
|
+
RegistryDiscoverResponse,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
import { CACHE_TTLS } from "./types.js";
|
|
17
|
+
|
|
18
|
+
// ─── Paths ───────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
const CONFIG_DIR = path.join(os.homedir(), ".agent-gateway");
|
|
21
|
+
const CACHE_DIR = path.join(CONFIG_DIR, "cache");
|
|
22
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
23
|
+
const TOKENS_FILE = path.join(CONFIG_DIR, "tokens.json");
|
|
24
|
+
const MANIFEST_CACHE_DIR = path.join(CACHE_DIR, "manifests");
|
|
25
|
+
const DETAIL_CACHE_DIR = path.join(CACHE_DIR, "details");
|
|
26
|
+
const DISCOVERY_CACHE_DIR = path.join(CACHE_DIR, "discovery");
|
|
27
|
+
|
|
28
|
+
const DEFAULT_CONFIG: GatewayConfig = {
|
|
29
|
+
registry_url: "https://agentdns.dev",
|
|
30
|
+
auth_callback_port: 9876,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Override from CLI args
|
|
34
|
+
let registryUrlOverride: string | undefined;
|
|
35
|
+
|
|
36
|
+
export function setRegistryUrl(url: string): void {
|
|
37
|
+
registryUrlOverride = url;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Directory setup ─────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
function ensureDir(dir: string): void {
|
|
43
|
+
if (!fs.existsSync(dir)) {
|
|
44
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ensureDirs(): void {
|
|
49
|
+
ensureDir(CONFIG_DIR);
|
|
50
|
+
ensureDir(CACHE_DIR);
|
|
51
|
+
ensureDir(MANIFEST_CACHE_DIR);
|
|
52
|
+
ensureDir(DETAIL_CACHE_DIR);
|
|
53
|
+
ensureDir(DISCOVERY_CACHE_DIR);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ─── Config ──────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
export function loadConfig(): GatewayConfig {
|
|
59
|
+
ensureDirs();
|
|
60
|
+
let config: GatewayConfig;
|
|
61
|
+
if (!fs.existsSync(CONFIG_FILE)) {
|
|
62
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
63
|
+
config = { ...DEFAULT_CONFIG };
|
|
64
|
+
} else {
|
|
65
|
+
try {
|
|
66
|
+
const raw = fs.readFileSync(CONFIG_FILE, "utf-8");
|
|
67
|
+
config = { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
68
|
+
} catch {
|
|
69
|
+
config = { ...DEFAULT_CONFIG };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (registryUrlOverride) {
|
|
73
|
+
config.registry_url = registryUrlOverride;
|
|
74
|
+
}
|
|
75
|
+
return config;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function saveConfig(config: GatewayConfig): void {
|
|
79
|
+
ensureDirs();
|
|
80
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ─── Identity ────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
export function getIdentity(): UserIdentity | undefined {
|
|
86
|
+
const config = loadConfig();
|
|
87
|
+
return config.identity;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function storeIdentity(identity: UserIdentity): void {
|
|
91
|
+
const config = loadConfig();
|
|
92
|
+
config.identity = identity;
|
|
93
|
+
saveConfig(config);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function clearIdentity(): void {
|
|
97
|
+
const config = loadConfig();
|
|
98
|
+
delete config.identity;
|
|
99
|
+
delete config.cloud_sync;
|
|
100
|
+
saveConfig(config);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function isInitialized(): boolean {
|
|
104
|
+
const config = loadConfig();
|
|
105
|
+
return !!config.identity;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Token store ─────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
interface TokenStore {
|
|
111
|
+
tokens: Record<string, StoredToken>;
|
|
112
|
+
connections: Record<string, Connection>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadTokenStore(): TokenStore {
|
|
116
|
+
ensureDirs();
|
|
117
|
+
if (!fs.existsSync(TOKENS_FILE)) {
|
|
118
|
+
return { tokens: {}, connections: {} };
|
|
119
|
+
}
|
|
120
|
+
try {
|
|
121
|
+
const raw = fs.readFileSync(TOKENS_FILE, "utf-8");
|
|
122
|
+
return JSON.parse(raw);
|
|
123
|
+
} catch {
|
|
124
|
+
return { tokens: {}, connections: {} };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function saveTokenStore(store: TokenStore): void {
|
|
129
|
+
ensureDirs();
|
|
130
|
+
fs.writeFileSync(TOKENS_FILE, JSON.stringify(store, null, 2));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function getToken(domain: string): StoredToken | undefined {
|
|
134
|
+
const store = loadTokenStore();
|
|
135
|
+
const token = store.tokens[domain];
|
|
136
|
+
if (!token) return undefined;
|
|
137
|
+
if (token.expires_at && Date.now() > token.expires_at) {
|
|
138
|
+
return { ...token, access_token: "" }; // Mark as expired
|
|
139
|
+
}
|
|
140
|
+
return token;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function storeToken(token: StoredToken): void {
|
|
144
|
+
const store = loadTokenStore();
|
|
145
|
+
store.tokens[token.domain] = token;
|
|
146
|
+
saveTokenStore(store);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function removeToken(domain: string): void {
|
|
150
|
+
const store = loadTokenStore();
|
|
151
|
+
delete store.tokens[domain];
|
|
152
|
+
delete store.connections[domain];
|
|
153
|
+
saveTokenStore(store);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getConnection(domain: string): Connection | undefined {
|
|
157
|
+
const store = loadTokenStore();
|
|
158
|
+
return store.connections[domain];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function storeConnection(connection: Connection): void {
|
|
162
|
+
const store = loadTokenStore();
|
|
163
|
+
store.connections[connection.domain] = connection;
|
|
164
|
+
saveTokenStore(store);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function getAllConnections(): Connection[] {
|
|
168
|
+
const store = loadTokenStore();
|
|
169
|
+
return Object.values(store.connections);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function getAllTokens(): StoredToken[] {
|
|
173
|
+
const store = loadTokenStore();
|
|
174
|
+
return Object.values(store.tokens);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Cloud sync ──────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
export async function syncTokensToCloud(): Promise<{ success: boolean; error?: string }> {
|
|
180
|
+
const config = loadConfig();
|
|
181
|
+
if (!config.identity) {
|
|
182
|
+
return { success: false, error: "Not signed in. Run `agent-gateway init` first." };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const store = loadTokenStore();
|
|
186
|
+
const bundle: CloudTokenBundle = {
|
|
187
|
+
tokens: store.tokens,
|
|
188
|
+
connections: store.connections,
|
|
189
|
+
synced_at: new Date().toISOString(),
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const res = await fetch(`${config.registry_url}/api/gateway/sync`, {
|
|
194
|
+
method: "PUT",
|
|
195
|
+
headers: {
|
|
196
|
+
"Content-Type": "application/json",
|
|
197
|
+
Authorization: `Bearer ${config.identity.registry_token}`,
|
|
198
|
+
},
|
|
199
|
+
body: JSON.stringify(bundle),
|
|
200
|
+
signal: AbortSignal.timeout(10000),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
if (!res.ok) {
|
|
204
|
+
return { success: false, error: `Sync failed: HTTP ${res.status}` };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Update sync state
|
|
208
|
+
config.cloud_sync = {
|
|
209
|
+
last_synced_at: bundle.synced_at,
|
|
210
|
+
};
|
|
211
|
+
saveConfig(config);
|
|
212
|
+
return { success: true };
|
|
213
|
+
} catch (err) {
|
|
214
|
+
return {
|
|
215
|
+
success: false,
|
|
216
|
+
error: `Sync failed: ${err instanceof Error ? err.message : "unknown"}`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function syncTokensFromCloud(): Promise<{ success: boolean; count: number; error?: string }> {
|
|
222
|
+
const config = loadConfig();
|
|
223
|
+
if (!config.identity) {
|
|
224
|
+
return { success: false, count: 0, error: "Not signed in. Run `agent-gateway init` first." };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
const res = await fetch(`${config.registry_url}/api/gateway/sync`, {
|
|
229
|
+
method: "GET",
|
|
230
|
+
headers: {
|
|
231
|
+
Authorization: `Bearer ${config.identity.registry_token}`,
|
|
232
|
+
},
|
|
233
|
+
signal: AbortSignal.timeout(10000),
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
if (!res.ok) {
|
|
237
|
+
return { success: false, count: 0, error: `Sync failed: HTTP ${res.status}` };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const bundle = (await res.json()) as CloudTokenBundle;
|
|
241
|
+
const store = loadTokenStore();
|
|
242
|
+
|
|
243
|
+
// Merge: cloud data wins for any conflicts
|
|
244
|
+
let count = 0;
|
|
245
|
+
for (const [domain, token] of Object.entries(bundle.tokens)) {
|
|
246
|
+
if (!store.tokens[domain]) count++;
|
|
247
|
+
store.tokens[domain] = token;
|
|
248
|
+
}
|
|
249
|
+
for (const [domain, conn] of Object.entries(bundle.connections)) {
|
|
250
|
+
store.connections[domain] = conn;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
saveTokenStore(store);
|
|
254
|
+
|
|
255
|
+
config.cloud_sync = {
|
|
256
|
+
last_synced_at: bundle.synced_at,
|
|
257
|
+
};
|
|
258
|
+
saveConfig(config);
|
|
259
|
+
|
|
260
|
+
return { success: true, count };
|
|
261
|
+
} catch (err) {
|
|
262
|
+
return {
|
|
263
|
+
success: false,
|
|
264
|
+
count: 0,
|
|
265
|
+
error: `Sync failed: ${err instanceof Error ? err.message : "unknown"}`,
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Disk cache ──────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
function safeName(key: string): string {
|
|
273
|
+
return key.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function readCacheEntry<T>(dir: string, key: string, ttl: number): T | undefined {
|
|
277
|
+
const filePath = path.join(dir, `${safeName(key)}.json`);
|
|
278
|
+
if (!fs.existsSync(filePath)) return undefined;
|
|
279
|
+
try {
|
|
280
|
+
const raw = fs.readFileSync(filePath, "utf-8");
|
|
281
|
+
const entry = JSON.parse(raw) as CacheEntry<T>;
|
|
282
|
+
if (Date.now() - entry.cached_at > ttl) {
|
|
283
|
+
// Expired — delete and return undefined
|
|
284
|
+
try { fs.unlinkSync(filePath); } catch { /* ignore */ }
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
return entry.data;
|
|
288
|
+
} catch {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function writeCacheEntry<T>(dir: string, key: string, data: T, ttl: number): void {
|
|
294
|
+
ensureDir(dir);
|
|
295
|
+
const filePath = path.join(dir, `${safeName(key)}.json`);
|
|
296
|
+
const entry: CacheEntry<T> = { data, cached_at: Date.now(), ttl };
|
|
297
|
+
try {
|
|
298
|
+
fs.writeFileSync(filePath, JSON.stringify(entry));
|
|
299
|
+
} catch { /* ignore write failures */ }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Manifest cache (24h)
|
|
303
|
+
export function getCachedManifest(domain: string): Manifest | undefined {
|
|
304
|
+
return readCacheEntry<Manifest>(MANIFEST_CACHE_DIR, domain, CACHE_TTLS.manifest);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function setCachedManifest(domain: string, manifest: Manifest): void {
|
|
308
|
+
writeCacheEntry(MANIFEST_CACHE_DIR, domain, manifest, CACHE_TTLS.manifest);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Capability detail cache (1h)
|
|
312
|
+
export function getCachedDetail(domain: string, capability: string): CapabilityDetail | undefined {
|
|
313
|
+
const key = `${domain}__${capability}`;
|
|
314
|
+
return readCacheEntry<CapabilityDetail>(DETAIL_CACHE_DIR, key, CACHE_TTLS.capability);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function setCachedDetail(domain: string, capability: string, detail: CapabilityDetail): void {
|
|
318
|
+
const key = `${domain}__${capability}`;
|
|
319
|
+
writeCacheEntry(DETAIL_CACHE_DIR, key, detail, CACHE_TTLS.capability);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Discovery cache (15min)
|
|
323
|
+
export function getCachedDiscovery(query: string): RegistryDiscoverResponse | undefined {
|
|
324
|
+
return readCacheEntry<RegistryDiscoverResponse>(DISCOVERY_CACHE_DIR, query, CACHE_TTLS.discovery);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function setCachedDiscovery(query: string, results: RegistryDiscoverResponse): void {
|
|
328
|
+
writeCacheEntry(DISCOVERY_CACHE_DIR, query, results, CACHE_TTLS.discovery);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Clear all caches
|
|
332
|
+
export function clearAllCaches(): void {
|
|
333
|
+
for (const dir of [MANIFEST_CACHE_DIR, DETAIL_CACHE_DIR, DISCOVERY_CACHE_DIR]) {
|
|
334
|
+
if (fs.existsSync(dir)) {
|
|
335
|
+
for (const file of fs.readdirSync(dir)) {
|
|
336
|
+
try { fs.unlinkSync(path.join(dir, file)); } catch { /* ignore */ }
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
package/src/discovery.ts
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadConfig,
|
|
3
|
+
getCachedManifest,
|
|
4
|
+
setCachedManifest,
|
|
5
|
+
getCachedDetail,
|
|
6
|
+
setCachedDetail,
|
|
7
|
+
getCachedDiscovery,
|
|
8
|
+
setCachedDiscovery,
|
|
9
|
+
clearAllCaches,
|
|
10
|
+
} from "./config.js";
|
|
11
|
+
import type {
|
|
12
|
+
Manifest,
|
|
13
|
+
CapabilityDetail,
|
|
14
|
+
RegistryDiscoverResponse,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
import { CACHE_TTLS } from "./types.js";
|
|
17
|
+
|
|
18
|
+
// ─── In-memory hot cache (backed by disk) ────────────────────────
|
|
19
|
+
|
|
20
|
+
const memManifestCache = new Map<string, { manifest: Manifest; at: number }>();
|
|
21
|
+
const memDetailCache = new Map<string, { detail: CapabilityDetail; at: number }>();
|
|
22
|
+
const memDiscoveryCache = new Map<string, { result: RegistryDiscoverResponse; at: number }>();
|
|
23
|
+
|
|
24
|
+
// ─── Fetch helper ────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
async function fetchJson<T>(url: string): Promise<T> {
|
|
27
|
+
const res = await fetch(url, {
|
|
28
|
+
headers: { Accept: "application/json" },
|
|
29
|
+
signal: AbortSignal.timeout(10000),
|
|
30
|
+
});
|
|
31
|
+
if (!res.ok) {
|
|
32
|
+
throw new Error(`HTTP ${res.status} ${res.statusText} from ${url}`);
|
|
33
|
+
}
|
|
34
|
+
return res.json() as Promise<T>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ─── Discovery (15min cache) ─────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
export async function discoverByQuery(
|
|
40
|
+
query: string
|
|
41
|
+
): Promise<RegistryDiscoverResponse> {
|
|
42
|
+
// Check in-memory hot cache
|
|
43
|
+
const mem = memDiscoveryCache.get(query);
|
|
44
|
+
if (mem && Date.now() - mem.at < CACHE_TTLS.discovery) {
|
|
45
|
+
return mem.result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check disk cache
|
|
49
|
+
const disk = getCachedDiscovery(query);
|
|
50
|
+
if (disk) {
|
|
51
|
+
memDiscoveryCache.set(query, { result: disk, at: Date.now() });
|
|
52
|
+
return disk;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fetch from registry
|
|
56
|
+
const config = loadConfig();
|
|
57
|
+
const url = `${config.registry_url}/api/discover?q=${encodeURIComponent(query)}`;
|
|
58
|
+
const result = await fetchJson<RegistryDiscoverResponse>(url);
|
|
59
|
+
|
|
60
|
+
// Store in both caches
|
|
61
|
+
memDiscoveryCache.set(query, { result, at: Date.now() });
|
|
62
|
+
setCachedDiscovery(query, result);
|
|
63
|
+
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Manifest (24h cache) ────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export async function fetchManifest(domain: string): Promise<Manifest> {
|
|
70
|
+
// Check in-memory hot cache
|
|
71
|
+
const mem = memManifestCache.get(domain);
|
|
72
|
+
if (mem && Date.now() - mem.at < CACHE_TTLS.manifest) {
|
|
73
|
+
return mem.manifest;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Check disk cache
|
|
77
|
+
const disk = getCachedManifest(domain);
|
|
78
|
+
if (disk) {
|
|
79
|
+
memManifestCache.set(domain, { manifest: disk, at: Date.now() });
|
|
80
|
+
return disk;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Fetch live
|
|
84
|
+
const url = `https://${domain}/.well-known/agent`;
|
|
85
|
+
const manifest = await fetchJson<Manifest>(url);
|
|
86
|
+
|
|
87
|
+
// Store in both caches
|
|
88
|
+
memManifestCache.set(domain, { manifest, at: Date.now() });
|
|
89
|
+
setCachedManifest(domain, manifest);
|
|
90
|
+
|
|
91
|
+
return manifest;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Capability detail (1h cache) ────────────────────────────────
|
|
95
|
+
|
|
96
|
+
export async function fetchCapabilityDetail(
|
|
97
|
+
domain: string,
|
|
98
|
+
capabilityName: string
|
|
99
|
+
): Promise<CapabilityDetail> {
|
|
100
|
+
const cacheKey = `${domain}:${capabilityName}`;
|
|
101
|
+
|
|
102
|
+
// Check in-memory hot cache
|
|
103
|
+
const mem = memDetailCache.get(cacheKey);
|
|
104
|
+
if (mem && Date.now() - mem.at < CACHE_TTLS.capability) {
|
|
105
|
+
return mem.detail;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check disk cache
|
|
109
|
+
const disk = getCachedDetail(domain, capabilityName);
|
|
110
|
+
if (disk) {
|
|
111
|
+
memDetailCache.set(cacheKey, { detail: disk, at: Date.now() });
|
|
112
|
+
return disk;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fetch live
|
|
116
|
+
const manifest = await fetchManifest(domain);
|
|
117
|
+
const cap = manifest.capabilities.find((c) => c.name === capabilityName);
|
|
118
|
+
if (!cap) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Capability '${capabilityName}' not found on ${domain}. Available: ${manifest.capabilities.map((c) => c.name).join(", ")}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const detailUrl = cap.detail_url.startsWith("http")
|
|
125
|
+
? cap.detail_url
|
|
126
|
+
: `${manifest.base_url}${cap.detail_url}`;
|
|
127
|
+
|
|
128
|
+
const detail = await fetchJson<CapabilityDetail>(detailUrl);
|
|
129
|
+
|
|
130
|
+
// Store in both caches
|
|
131
|
+
memDetailCache.set(cacheKey, { detail, at: Date.now() });
|
|
132
|
+
setCachedDetail(domain, capabilityName, detail);
|
|
133
|
+
|
|
134
|
+
return detail;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── Cache management ────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
export function clearCache(): void {
|
|
140
|
+
memManifestCache.clear();
|
|
141
|
+
memDetailCache.clear();
|
|
142
|
+
memDiscoveryCache.clear();
|
|
143
|
+
clearAllCaches();
|
|
144
|
+
}
|