@zackbart/connecta 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/CHANGELOG.md +13 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/SECURITY.md +25 -0
- package/assets/connecta-clay-hero.png +0 -0
- package/dist/activity.d.ts +82 -0
- package/dist/activity.d.ts.map +1 -0
- package/dist/activity.js +51 -0
- package/dist/activity.js.map +1 -0
- package/dist/auth/bearer.d.ts +10 -0
- package/dist/auth/bearer.d.ts.map +1 -0
- package/dist/auth/bearer.js +47 -0
- package/dist/auth/bearer.js.map +1 -0
- package/dist/auth/clerk.d.ts +20 -0
- package/dist/auth/clerk.d.ts.map +1 -0
- package/dist/auth/clerk.js +167 -0
- package/dist/auth/clerk.js.map +1 -0
- package/dist/auth/downstream-oauth.d.ts +78 -0
- package/dist/auth/downstream-oauth.d.ts.map +1 -0
- package/dist/auth/downstream-oauth.js +180 -0
- package/dist/auth/downstream-oauth.js.map +1 -0
- package/dist/catalog.d.ts +11 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +144 -0
- package/dist/catalog.js.map +1 -0
- package/dist/connectors/api.d.ts +34 -0
- package/dist/connectors/api.d.ts.map +1 -0
- package/dist/connectors/api.js +36 -0
- package/dist/connectors/api.js.map +1 -0
- package/dist/connectors/remote-mcp.d.ts +33 -0
- package/dist/connectors/remote-mcp.d.ts.map +1 -0
- package/dist/connectors/remote-mcp.js +255 -0
- package/dist/connectors/remote-mcp.js.map +1 -0
- package/dist/credentials.d.ts +33 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +177 -0
- package/dist/credentials.js.map +1 -0
- package/dist/execute.d.ts +40 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +424 -0
- package/dist/execute.js.map +1 -0
- package/dist/executors/quickjs.d.ts +26 -0
- package/dist/executors/quickjs.d.ts.map +1 -0
- package/dist/executors/quickjs.js +267 -0
- package/dist/executors/quickjs.js.map +1 -0
- package/dist/favicon.d.ts +2 -0
- package/dist/favicon.d.ts.map +1 -0
- package/dist/favicon.js +49 -0
- package/dist/favicon.js.map +1 -0
- package/dist/index.d.ts +85 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-result.d.ts +9 -0
- package/dist/mcp-result.d.ts.map +1 -0
- package/dist/mcp-result.js +35 -0
- package/dist/mcp-result.js.map +1 -0
- package/dist/meta-tools.d.ts +100 -0
- package/dist/meta-tools.d.ts.map +1 -0
- package/dist/meta-tools.js +797 -0
- package/dist/meta-tools.js.map +1 -0
- package/dist/node.d.ts +9 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +57 -0
- package/dist/node.js.map +1 -0
- package/dist/registry.d.ts +78 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +299 -0
- package/dist/registry.js.map +1 -0
- package/dist/server.d.ts +31 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +543 -0
- package/dist/server.js.map +1 -0
- package/dist/skills.d.ts +8 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +60 -0
- package/dist/skills.js.map +1 -0
- package/dist/storage/file.d.ts +8 -0
- package/dist/storage/file.d.ts.map +1 -0
- package/dist/storage/file.js +53 -0
- package/dist/storage/file.js.map +1 -0
- package/dist/storage/memory.d.ts +4 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +29 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/types.d.ts +219 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/ui.d.ts +91 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +1031 -0
- package/dist/ui.js.map +1 -0
- package/package.json +96 -0
- package/src/activity.ts +144 -0
- package/src/auth/bearer.ts +54 -0
- package/src/auth/clerk.ts +226 -0
- package/src/auth/downstream-oauth.ts +202 -0
- package/src/catalog.ts +166 -0
- package/src/connectors/api.ts +80 -0
- package/src/connectors/remote-mcp.ts +324 -0
- package/src/credentials.ts +259 -0
- package/src/execute.ts +550 -0
- package/src/executors/quickjs.ts +321 -0
- package/src/favicon.ts +53 -0
- package/src/index.ts +197 -0
- package/src/mcp-result.ts +43 -0
- package/src/meta-tools.ts +1120 -0
- package/src/node.ts +62 -0
- package/src/registry.ts +400 -0
- package/src/server.ts +758 -0
- package/src/skills.ts +63 -0
- package/src/storage/file.ts +62 -0
- package/src/storage/memory.ts +34 -0
- package/src/types.ts +254 -0
- package/src/ui.ts +1121 -0
package/src/node.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import type { IncomingMessage, Server, ServerResponse } from "node:http";
|
|
3
|
+
import type { Connecta } from "./index.js";
|
|
4
|
+
|
|
5
|
+
export { fileStorage } from "./storage/file.js";
|
|
6
|
+
|
|
7
|
+
async function toRequest(req: IncomingMessage): Promise<Request> {
|
|
8
|
+
const host = req.headers.host ?? "localhost";
|
|
9
|
+
const proto =
|
|
10
|
+
(req.headers["x-forwarded-proto"] as string | undefined) ?? "http";
|
|
11
|
+
const url = `${proto}://${host}${req.url ?? "/"}`;
|
|
12
|
+
const method = req.method ?? "GET";
|
|
13
|
+
const headers = new Headers();
|
|
14
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
15
|
+
if (value == null) continue;
|
|
16
|
+
headers.set(key, Array.isArray(value) ? value.join(", ") : value);
|
|
17
|
+
}
|
|
18
|
+
let body: Uint8Array | undefined;
|
|
19
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
20
|
+
const chunks: Buffer[] = [];
|
|
21
|
+
for await (const chunk of req) chunks.push(chunk as Buffer);
|
|
22
|
+
if (chunks.length) body = new Uint8Array(Buffer.concat(chunks));
|
|
23
|
+
}
|
|
24
|
+
return new Request(url, {
|
|
25
|
+
method,
|
|
26
|
+
headers,
|
|
27
|
+
body,
|
|
28
|
+
// Required by Node's fetch when sending a body stream.
|
|
29
|
+
...(body ? { duplex: "half" } : {}),
|
|
30
|
+
} as RequestInit);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function writeResponse(
|
|
34
|
+
res: ServerResponse,
|
|
35
|
+
response: Response,
|
|
36
|
+
): Promise<void> {
|
|
37
|
+
res.statusCode = response.status;
|
|
38
|
+
response.headers.forEach((value, key) => res.setHeader(key, value));
|
|
39
|
+
const buf = Buffer.from(await response.arrayBuffer());
|
|
40
|
+
res.end(buf);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Serve a Connecta over node:http. Thin adapter: IncomingMessage → Request,
|
|
45
|
+
* Response → ServerResponse. No dependencies beyond node core.
|
|
46
|
+
*/
|
|
47
|
+
export function listen(connecta: Connecta, port: number): Server {
|
|
48
|
+
const server = createServer((req, res) => {
|
|
49
|
+
void (async () => {
|
|
50
|
+
try {
|
|
51
|
+
const request = await toRequest(req);
|
|
52
|
+
const response = await connecta.fetch(request);
|
|
53
|
+
await writeResponse(res, response);
|
|
54
|
+
} catch {
|
|
55
|
+
res.statusCode = 500;
|
|
56
|
+
res.end("Internal Server Error");
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
});
|
|
60
|
+
server.listen(port);
|
|
61
|
+
return server;
|
|
62
|
+
}
|
package/src/registry.ts
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Connector,
|
|
3
|
+
ConnectorContext,
|
|
4
|
+
ConnectorStatus,
|
|
5
|
+
KVStorage,
|
|
6
|
+
Logger,
|
|
7
|
+
ToolDef,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
import type { CredentialVault } from "./credentials.js";
|
|
10
|
+
|
|
11
|
+
const ID_RE = /^[a-z0-9_-]+$/;
|
|
12
|
+
const DEFAULT_TTL_SECONDS = 300;
|
|
13
|
+
const DEFAULT_STALE_SECONDS = 3600;
|
|
14
|
+
const DEFAULT_MAX_RESULT_BYTES = 50_000;
|
|
15
|
+
|
|
16
|
+
interface CacheEntry {
|
|
17
|
+
tools: ToolDef[];
|
|
18
|
+
exp: number; // epoch ms
|
|
19
|
+
staleUntil: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface PersistedCatalog {
|
|
23
|
+
tools: ToolDef[];
|
|
24
|
+
fetchedAt: number;
|
|
25
|
+
expiresAt: number;
|
|
26
|
+
staleUntil: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface HealthObservation {
|
|
30
|
+
lastSuccessAt?: string;
|
|
31
|
+
lastFailureAt?: string;
|
|
32
|
+
lastLatencyMs?: number;
|
|
33
|
+
consecutiveFailures: number;
|
|
34
|
+
lastError?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface RegistryOptions {
|
|
38
|
+
storage: KVStorage;
|
|
39
|
+
logger: Logger;
|
|
40
|
+
credentialVault?: CredentialVault;
|
|
41
|
+
toolCacheTtlSeconds?: number;
|
|
42
|
+
persistToolCatalog?: boolean;
|
|
43
|
+
toolCatalogStaleSeconds?: number;
|
|
44
|
+
/** Cap on inline result size before truncation + get_result paging. Default 50_000. */
|
|
45
|
+
maxResultBytes?: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function namespaced(storage: KVStorage, prefix: string): KVStorage {
|
|
49
|
+
return {
|
|
50
|
+
get: (k) => storage.get(prefix + k),
|
|
51
|
+
set: (k, v, o) => storage.set(prefix + k, v, o),
|
|
52
|
+
delete: (k) => storage.delete(prefix + k),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function msg(err: unknown): string {
|
|
57
|
+
return err instanceof Error ? err.message : String(err);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Holds the connector set, resolves addresses, and caches per-connector tool
|
|
62
|
+
* lists in memory with a TTL. Connector failures are isolated: a broken
|
|
63
|
+
* connector surfaces status "error"; the rest keep working.
|
|
64
|
+
*/
|
|
65
|
+
export class Registry {
|
|
66
|
+
private readonly connectors = new Map<string, Connector>();
|
|
67
|
+
private readonly cache = new Map<string, CacheEntry>();
|
|
68
|
+
private readonly invalidated = new Set<string>();
|
|
69
|
+
private readonly health = new Map<string, HealthObservation>();
|
|
70
|
+
private readonly ttlMs: number;
|
|
71
|
+
private readonly staleMs: number;
|
|
72
|
+
private readonly persistToolCatalog: boolean;
|
|
73
|
+
/** Result-size guard cap threaded to the meta-tools. */
|
|
74
|
+
readonly maxResultBytes: number;
|
|
75
|
+
|
|
76
|
+
constructor(
|
|
77
|
+
connectors: Connector[],
|
|
78
|
+
private readonly opts: RegistryOptions,
|
|
79
|
+
) {
|
|
80
|
+
this.ttlMs =
|
|
81
|
+
(opts.toolCacheTtlSeconds ?? DEFAULT_TTL_SECONDS) * 1000;
|
|
82
|
+
this.staleMs =
|
|
83
|
+
(opts.toolCatalogStaleSeconds ?? DEFAULT_STALE_SECONDS) * 1000;
|
|
84
|
+
this.persistToolCatalog = opts.persistToolCatalog ?? true;
|
|
85
|
+
this.maxResultBytes = opts.maxResultBytes ?? DEFAULT_MAX_RESULT_BYTES;
|
|
86
|
+
for (const c of connectors) {
|
|
87
|
+
if (!ID_RE.test(c.id)) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Invalid connector id "${c.id}": must match ${ID_RE.source}`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (this.connectors.has(c.id)) {
|
|
93
|
+
throw new Error(`Duplicate connector id "${c.id}"`);
|
|
94
|
+
}
|
|
95
|
+
this.connectors.set(c.id, c);
|
|
96
|
+
}
|
|
97
|
+
this.checkConventions(opts.logger);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Warn once per convention violation at construction time. Static only —
|
|
102
|
+
* never calls listTools() (remote connectors are lazy/network); tool-level
|
|
103
|
+
* checks apply to connectors that expose `staticTools` (i.e. api()).
|
|
104
|
+
*/
|
|
105
|
+
private checkConventions(logger: Logger): void {
|
|
106
|
+
for (const c of this.connectors.values()) {
|
|
107
|
+
if (!c.description) {
|
|
108
|
+
logger.warn(
|
|
109
|
+
`[connecta] connector "${c.id}" has no description — add one (convention: "<Service> — <top capabilities>")`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
for (const t of c.staticTools ?? []) {
|
|
113
|
+
const address = `${c.id}.${t.name}`;
|
|
114
|
+
if (!t.description) {
|
|
115
|
+
logger.warn(
|
|
116
|
+
`[connecta] tool "${address}" has no description — add one (convention: imperative one-liner, e.g. "Send an email via Resend")`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (!t.inputSchema) {
|
|
120
|
+
logger.warn(
|
|
121
|
+
`[connecta] tool "${address}" has no inputSchema — add one (convention: { type: "object" } with a description on every property)`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
listConnectors(): Connector[] {
|
|
129
|
+
return [...this.connectors.values()];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
getConnector(id: string): Connector | undefined {
|
|
133
|
+
return this.connectors.get(id);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
contextFor(
|
|
137
|
+
id: string,
|
|
138
|
+
baseUrl: string,
|
|
139
|
+
requestScope: object = {},
|
|
140
|
+
callOptions: { signal?: AbortSignal; timeoutMs?: number } = {},
|
|
141
|
+
): ConnectorContext {
|
|
142
|
+
return {
|
|
143
|
+
storage: namespaced(this.opts.storage, `conn:${id}:`),
|
|
144
|
+
logger: this.opts.logger,
|
|
145
|
+
baseUrl,
|
|
146
|
+
...(this.opts.credentialVault && this.connectors.get(id)?.credential
|
|
147
|
+
? {
|
|
148
|
+
credential: {
|
|
149
|
+
get: (field?: string) =>
|
|
150
|
+
this.opts.credentialVault!.get(id, field),
|
|
151
|
+
getAll: () => this.opts.credentialVault!.getAll(id),
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
: {}),
|
|
155
|
+
requestScope,
|
|
156
|
+
...callOptions,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Storage namespaced to the meta-tool result store (`results:` prefix), kept
|
|
162
|
+
* separate from any connector's `conn:<id>:` namespace. Backs get_result.
|
|
163
|
+
*/
|
|
164
|
+
resultsStorage(): KVStorage {
|
|
165
|
+
return namespaced(this.opts.storage, "results:");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Resolve "<connectorId>.<toolName>" → connector + tool name. */
|
|
169
|
+
resolveAddress(
|
|
170
|
+
address: string,
|
|
171
|
+
): { connector: Connector; toolName: string } | null {
|
|
172
|
+
const dot = address.indexOf(".");
|
|
173
|
+
if (dot <= 0 || dot === address.length - 1) return null;
|
|
174
|
+
const id = address.slice(0, dot);
|
|
175
|
+
const toolName = address.slice(dot + 1);
|
|
176
|
+
const connector = this.connectors.get(id);
|
|
177
|
+
if (!connector) return null;
|
|
178
|
+
return { connector, toolName };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private catalogKey(id: string): string {
|
|
182
|
+
return `catalog:${id}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private validCatalog(raw: string | null): PersistedCatalog | null {
|
|
186
|
+
if (!raw) return null;
|
|
187
|
+
try {
|
|
188
|
+
const value = JSON.parse(raw) as Partial<PersistedCatalog>;
|
|
189
|
+
if (
|
|
190
|
+
!Array.isArray(value.tools) ||
|
|
191
|
+
typeof value.fetchedAt !== "number" ||
|
|
192
|
+
typeof value.expiresAt !== "number" ||
|
|
193
|
+
typeof value.staleUntil !== "number" ||
|
|
194
|
+
!value.tools.every(
|
|
195
|
+
(tool) =>
|
|
196
|
+
tool !== null &&
|
|
197
|
+
typeof tool === "object" &&
|
|
198
|
+
typeof (tool as ToolDef).name === "string",
|
|
199
|
+
)
|
|
200
|
+
) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
return value as PersistedCatalog;
|
|
204
|
+
} catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private async storeCatalog(id: string, tools: ToolDef[]): Promise<void> {
|
|
210
|
+
if (!this.persistToolCatalog) return;
|
|
211
|
+
const fetchedAt = Date.now();
|
|
212
|
+
const catalog: PersistedCatalog = {
|
|
213
|
+
tools,
|
|
214
|
+
fetchedAt,
|
|
215
|
+
expiresAt: fetchedAt + this.ttlMs,
|
|
216
|
+
staleUntil: fetchedAt + this.ttlMs + this.staleMs,
|
|
217
|
+
};
|
|
218
|
+
await this.opts.storage.set(this.catalogKey(id), JSON.stringify(catalog), {
|
|
219
|
+
ttlSeconds: Math.max(
|
|
220
|
+
60,
|
|
221
|
+
Math.ceil((this.ttlMs + this.staleMs) / 1000),
|
|
222
|
+
),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Force a live listTools refresh and replace both catalog cache layers. */
|
|
227
|
+
async refreshTools(
|
|
228
|
+
id: string,
|
|
229
|
+
baseUrl: string,
|
|
230
|
+
requestScope?: object,
|
|
231
|
+
): Promise<ToolDef[]> {
|
|
232
|
+
const connector = this.connectors.get(id);
|
|
233
|
+
if (!connector) throw new Error(`Unknown connector "${id}"`);
|
|
234
|
+
const tools = connector.staticTools
|
|
235
|
+
? connector.staticTools
|
|
236
|
+
: await connector.listTools(
|
|
237
|
+
this.contextFor(id, baseUrl, requestScope),
|
|
238
|
+
);
|
|
239
|
+
const now = Date.now();
|
|
240
|
+
const previous = this.cache.get(id);
|
|
241
|
+
const catalogChanged =
|
|
242
|
+
!previous || JSON.stringify(previous.tools) !== JSON.stringify(tools);
|
|
243
|
+
const shouldPersist =
|
|
244
|
+
!connector.staticTools &&
|
|
245
|
+
(catalogChanged || previous.exp <= now || this.invalidated.has(id));
|
|
246
|
+
this.cache.set(id, {
|
|
247
|
+
tools,
|
|
248
|
+
exp: now + this.ttlMs,
|
|
249
|
+
staleUntil: now + this.ttlMs + this.staleMs,
|
|
250
|
+
});
|
|
251
|
+
this.invalidated.delete(id);
|
|
252
|
+
if (shouldPersist) {
|
|
253
|
+
try {
|
|
254
|
+
await this.storeCatalog(id, tools);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
this.opts.logger.warn(
|
|
257
|
+
`[connecta] connector "${id}" catalog persistence failed: ${msg(err)}`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return tools;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Cached listTools with in-memory + persisted serializable catalog layers. */
|
|
265
|
+
async getTools(
|
|
266
|
+
id: string,
|
|
267
|
+
baseUrl: string,
|
|
268
|
+
requestScope?: object,
|
|
269
|
+
): Promise<ToolDef[]> {
|
|
270
|
+
const connector = this.connectors.get(id);
|
|
271
|
+
if (!connector) throw new Error(`Unknown connector "${id}"`);
|
|
272
|
+
if (connector.staticTools) return connector.staticTools;
|
|
273
|
+
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
const hit = this.cache.get(id);
|
|
276
|
+
if (hit && hit.exp > now) return hit.tools;
|
|
277
|
+
|
|
278
|
+
let stale = hit && hit.staleUntil > now ? hit.tools : undefined;
|
|
279
|
+
if (this.persistToolCatalog && !this.invalidated.has(id)) {
|
|
280
|
+
let persisted: PersistedCatalog | null = null;
|
|
281
|
+
try {
|
|
282
|
+
persisted = this.validCatalog(
|
|
283
|
+
await this.opts.storage.get(this.catalogKey(id)),
|
|
284
|
+
);
|
|
285
|
+
} catch (err) {
|
|
286
|
+
this.opts.logger.warn(
|
|
287
|
+
`[connecta] connector "${id}" catalog read failed: ${msg(err)}`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (persisted && persisted.staleUntil > now) {
|
|
291
|
+
this.cache.set(id, {
|
|
292
|
+
tools: persisted.tools,
|
|
293
|
+
exp: persisted.expiresAt,
|
|
294
|
+
staleUntil: persisted.staleUntil,
|
|
295
|
+
});
|
|
296
|
+
if (persisted.expiresAt > now) return persisted.tools;
|
|
297
|
+
stale = persisted.tools;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
return await this.refreshTools(id, baseUrl, requestScope);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
if (stale) {
|
|
305
|
+
this.opts.logger.warn(
|
|
306
|
+
`[connecta] connector "${id}" catalog refresh failed; serving stale catalog: ${msg(err)}`,
|
|
307
|
+
);
|
|
308
|
+
return stale;
|
|
309
|
+
}
|
|
310
|
+
throw err;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Return a cached catalog without performing storage or network I/O. */
|
|
315
|
+
peekTools(id: string): ToolDef[] | undefined {
|
|
316
|
+
const connector = this.connectors.get(id);
|
|
317
|
+
if (connector?.staticTools) return connector.staticTools;
|
|
318
|
+
const hit = this.cache.get(id);
|
|
319
|
+
return hit && hit.staleUntil > Date.now() ? hit.tools : undefined;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
recordSuccess(id: string, latencyMs: number): void {
|
|
323
|
+
const previous = this.health.get(id);
|
|
324
|
+
this.health.set(id, {
|
|
325
|
+
...previous,
|
|
326
|
+
lastSuccessAt: new Date().toISOString(),
|
|
327
|
+
lastLatencyMs: latencyMs,
|
|
328
|
+
consecutiveFailures: 0,
|
|
329
|
+
lastError: undefined,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
recordFailure(id: string, latencyMs: number, error: unknown): void {
|
|
334
|
+
const previous = this.health.get(id);
|
|
335
|
+
this.health.set(id, {
|
|
336
|
+
...previous,
|
|
337
|
+
lastFailureAt: new Date().toISOString(),
|
|
338
|
+
lastLatencyMs: latencyMs,
|
|
339
|
+
consecutiveFailures: (previous?.consecutiveFailures ?? 0) + 1,
|
|
340
|
+
lastError: msg(error),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
healthFor(id: string): HealthObservation | undefined {
|
|
345
|
+
const observation = this.health.get(id);
|
|
346
|
+
return observation ? { ...observation } : undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** Best-effort connector status for list_connectors. */
|
|
350
|
+
async statusFor(
|
|
351
|
+
id: string,
|
|
352
|
+
baseUrl: string,
|
|
353
|
+
requestScope: object = {},
|
|
354
|
+
): Promise<ConnectorStatus> {
|
|
355
|
+
const connector = this.connectors.get(id);
|
|
356
|
+
if (!connector) return { state: "error", message: "Unknown connector" };
|
|
357
|
+
const ctx = this.contextFor(id, baseUrl, requestScope);
|
|
358
|
+
if (connector.status) {
|
|
359
|
+
try {
|
|
360
|
+
return await connector.status(ctx);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
return { state: "error", message: msg(err) };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
await this.getTools(id, baseUrl, requestScope);
|
|
367
|
+
return { state: "ok" };
|
|
368
|
+
} catch (err) {
|
|
369
|
+
return { state: "error", message: msg(err) };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/** Drop a connector's cached tool list (e.g. after auth completes). */
|
|
374
|
+
invalidate(id: string): void {
|
|
375
|
+
this.cache.delete(id);
|
|
376
|
+
this.invalidated.add(id);
|
|
377
|
+
if (this.persistToolCatalog) {
|
|
378
|
+
void this.opts.storage.delete(this.catalogKey(id)).catch((err) => {
|
|
379
|
+
this.opts.logger.warn(
|
|
380
|
+
`[connecta] connector "${id}" catalog invalidation failed: ${msg(err)}`,
|
|
381
|
+
);
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/** Drop both in-memory and persisted tool catalogs. */
|
|
387
|
+
async invalidateStored(id: string): Promise<void> {
|
|
388
|
+
this.cache.delete(id);
|
|
389
|
+
this.invalidated.add(id);
|
|
390
|
+
if (this.persistToolCatalog) {
|
|
391
|
+
try {
|
|
392
|
+
await this.opts.storage.delete(this.catalogKey(id));
|
|
393
|
+
} catch (err) {
|
|
394
|
+
this.opts.logger.warn(
|
|
395
|
+
`[connecta] connector "${id}" catalog invalidation failed: ${msg(err)}`,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|