@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.
Files changed (116) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/LICENSE +21 -0
  3. package/README.md +249 -0
  4. package/SECURITY.md +25 -0
  5. package/assets/connecta-clay-hero.png +0 -0
  6. package/dist/activity.d.ts +82 -0
  7. package/dist/activity.d.ts.map +1 -0
  8. package/dist/activity.js +51 -0
  9. package/dist/activity.js.map +1 -0
  10. package/dist/auth/bearer.d.ts +10 -0
  11. package/dist/auth/bearer.d.ts.map +1 -0
  12. package/dist/auth/bearer.js +47 -0
  13. package/dist/auth/bearer.js.map +1 -0
  14. package/dist/auth/clerk.d.ts +20 -0
  15. package/dist/auth/clerk.d.ts.map +1 -0
  16. package/dist/auth/clerk.js +167 -0
  17. package/dist/auth/clerk.js.map +1 -0
  18. package/dist/auth/downstream-oauth.d.ts +78 -0
  19. package/dist/auth/downstream-oauth.d.ts.map +1 -0
  20. package/dist/auth/downstream-oauth.js +180 -0
  21. package/dist/auth/downstream-oauth.js.map +1 -0
  22. package/dist/catalog.d.ts +11 -0
  23. package/dist/catalog.d.ts.map +1 -0
  24. package/dist/catalog.js +144 -0
  25. package/dist/catalog.js.map +1 -0
  26. package/dist/connectors/api.d.ts +34 -0
  27. package/dist/connectors/api.d.ts.map +1 -0
  28. package/dist/connectors/api.js +36 -0
  29. package/dist/connectors/api.js.map +1 -0
  30. package/dist/connectors/remote-mcp.d.ts +33 -0
  31. package/dist/connectors/remote-mcp.d.ts.map +1 -0
  32. package/dist/connectors/remote-mcp.js +255 -0
  33. package/dist/connectors/remote-mcp.js.map +1 -0
  34. package/dist/credentials.d.ts +33 -0
  35. package/dist/credentials.d.ts.map +1 -0
  36. package/dist/credentials.js +177 -0
  37. package/dist/credentials.js.map +1 -0
  38. package/dist/execute.d.ts +40 -0
  39. package/dist/execute.d.ts.map +1 -0
  40. package/dist/execute.js +424 -0
  41. package/dist/execute.js.map +1 -0
  42. package/dist/executors/quickjs.d.ts +26 -0
  43. package/dist/executors/quickjs.d.ts.map +1 -0
  44. package/dist/executors/quickjs.js +267 -0
  45. package/dist/executors/quickjs.js.map +1 -0
  46. package/dist/favicon.d.ts +2 -0
  47. package/dist/favicon.d.ts.map +1 -0
  48. package/dist/favicon.js +49 -0
  49. package/dist/favicon.js.map +1 -0
  50. package/dist/index.d.ts +85 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +73 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/mcp-result.d.ts +9 -0
  55. package/dist/mcp-result.d.ts.map +1 -0
  56. package/dist/mcp-result.js +35 -0
  57. package/dist/mcp-result.js.map +1 -0
  58. package/dist/meta-tools.d.ts +100 -0
  59. package/dist/meta-tools.d.ts.map +1 -0
  60. package/dist/meta-tools.js +797 -0
  61. package/dist/meta-tools.js.map +1 -0
  62. package/dist/node.d.ts +9 -0
  63. package/dist/node.d.ts.map +1 -0
  64. package/dist/node.js +57 -0
  65. package/dist/node.js.map +1 -0
  66. package/dist/registry.d.ts +78 -0
  67. package/dist/registry.d.ts.map +1 -0
  68. package/dist/registry.js +299 -0
  69. package/dist/registry.js.map +1 -0
  70. package/dist/server.d.ts +31 -0
  71. package/dist/server.d.ts.map +1 -0
  72. package/dist/server.js +543 -0
  73. package/dist/server.js.map +1 -0
  74. package/dist/skills.d.ts +8 -0
  75. package/dist/skills.d.ts.map +1 -0
  76. package/dist/skills.js +60 -0
  77. package/dist/skills.js.map +1 -0
  78. package/dist/storage/file.d.ts +8 -0
  79. package/dist/storage/file.d.ts.map +1 -0
  80. package/dist/storage/file.js +53 -0
  81. package/dist/storage/file.js.map +1 -0
  82. package/dist/storage/memory.d.ts +4 -0
  83. package/dist/storage/memory.d.ts.map +1 -0
  84. package/dist/storage/memory.js +29 -0
  85. package/dist/storage/memory.js.map +1 -0
  86. package/dist/types.d.ts +219 -0
  87. package/dist/types.d.ts.map +1 -0
  88. package/dist/types.js +3 -0
  89. package/dist/types.js.map +1 -0
  90. package/dist/ui.d.ts +91 -0
  91. package/dist/ui.d.ts.map +1 -0
  92. package/dist/ui.js +1031 -0
  93. package/dist/ui.js.map +1 -0
  94. package/package.json +96 -0
  95. package/src/activity.ts +144 -0
  96. package/src/auth/bearer.ts +54 -0
  97. package/src/auth/clerk.ts +226 -0
  98. package/src/auth/downstream-oauth.ts +202 -0
  99. package/src/catalog.ts +166 -0
  100. package/src/connectors/api.ts +80 -0
  101. package/src/connectors/remote-mcp.ts +324 -0
  102. package/src/credentials.ts +259 -0
  103. package/src/execute.ts +550 -0
  104. package/src/executors/quickjs.ts +321 -0
  105. package/src/favicon.ts +53 -0
  106. package/src/index.ts +197 -0
  107. package/src/mcp-result.ts +43 -0
  108. package/src/meta-tools.ts +1120 -0
  109. package/src/node.ts +62 -0
  110. package/src/registry.ts +400 -0
  111. package/src/server.ts +758 -0
  112. package/src/skills.ts +63 -0
  113. package/src/storage/file.ts +62 -0
  114. package/src/storage/memory.ts +34 -0
  115. package/src/types.ts +254 -0
  116. package/src/ui.ts +1121 -0
package/src/skills.ts ADDED
@@ -0,0 +1,63 @@
1
+ export const CONNECTA_INSTRUCTIONS =
2
+ 'Connecta exposes many integrations behind meta-tools. When an address is unknown, start with search_tools and includeSchemas="compact"; use describe_tools only when that schema is insufficient. Use call_tool for one explicitly read-only call, batch_call for 2–10 independent explicitly read-only calls, and execute_code (when available) only for dependent read-only steps, loops, joins, or reducing large results. Unannotated, write-capable, and destructive tools must use call_destructive_tool individually. Use authorize_connector only after auth_required and get_result only for truncated results. For the detailed workflow, call skills({ name: "usage" }) once per task.';
3
+
4
+ export const USAGE_SKILL = `# Connecta usage
5
+
6
+ ## Choose the smallest execution tool
7
+
8
+ - Unknown address: \`search_tools({ query, includeSchemas: "compact" })\`.
9
+ - Schema still unclear: \`describe_tools({ addresses: [...] })\`.
10
+ - One explicitly read-only call: \`call_tool\`.
11
+ - Two to ten independent explicitly read-only calls: \`batch_call\`.
12
+ - Dependent read-only calls, loops, joins, branching, or large-result reduction: \`execute_code\` when available.
13
+ - Any unannotated, write-capable, or destructive call: \`call_destructive_tool\`, individually and only after reviewing its schema and consequences.
14
+ - Truncated result: retry with \`fields\` when possible; otherwise page it with \`get_result\`.
15
+ - \`auth_required\`: use \`authorize_connector\`, have the operator complete consent, then confirm with \`list_connectors\`.
16
+
17
+ Use \`list_connectors({ probe: false })\` for a fast inventory. Use \`probe: true\` only when diagnosing live health or authorization.
18
+
19
+ ## Code mode
20
+
21
+ Use code mode when a later call depends on an earlier result, when joining across connectors, or when filtering or aggregating data in the sandbox will substantially shrink the response. Use \`Promise.all\` or \`connecta.batch\` for independent calls inside one execution.
22
+
23
+ Do not use code mode for one straightforward call, for independent calls already handled by \`batch_call\`, or for any tool not explicitly annotated \`readOnlyHint: true\`. Code mode has a bounded host-call budget and per-call deadline. Return only the reduced value the agent needs; do not return a large upstream payload unchanged.
24
+
25
+ ## Examples
26
+
27
+ These addresses are illustrative; always use the exact address returned by \`search_tools\`.
28
+
29
+ Single call:
30
+ \`\`\`json
31
+ { "address": "crm.get_account", "args": { "id": "acct_123" }, "resultMode": "value" }
32
+ \`\`\`
33
+
34
+ Independent calls:
35
+ \`\`\`json
36
+ { "calls": [
37
+ { "address": "crm.get_account", "args": { "id": "acct_123" } },
38
+ { "address": "billing.list_invoices", "args": { "status": "open" } }
39
+ ] }
40
+ \`\`\`
41
+
42
+ Dependent code with reduction:
43
+ \`\`\`js
44
+ async () => {
45
+ const accounts = await crm.search_accounts({ query: "renewal" });
46
+ const details = await Promise.all(
47
+ accounts.results.slice(0, 5).map((account) =>
48
+ crm.get_account({ id: account.id })
49
+ )
50
+ );
51
+ return details.map(({ id, name, status }) => ({ id, name, status }));
52
+ }
53
+ \`\`\`
54
+ `;
55
+
56
+ export const AVAILABLE_SKILLS = [
57
+ {
58
+ name: "usage",
59
+ description:
60
+ "How to choose among Connecta discovery, direct, batch, destructive, and code-mode tools.",
61
+ content: USAGE_SKILL,
62
+ },
63
+ ] as const;
@@ -0,0 +1,62 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ renameSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { dirname } from "node:path";
9
+ import type { KVStorage } from "../types.js";
10
+
11
+ interface Entry {
12
+ value: string;
13
+ exp?: number; // epoch ms
14
+ }
15
+
16
+ /**
17
+ * JSON-file-backed KVStorage for Node. Loads once, persists on every write via
18
+ * a temp-file + rename (atomic-ish). Only reachable via the "@zackbart/connecta/node"
19
+ * subpath so the main entry stays Workers-clean.
20
+ */
21
+ export function fileStorage(path: string): KVStorage {
22
+ let data: Record<string, Entry> = {};
23
+ if (existsSync(path)) {
24
+ try {
25
+ data = JSON.parse(readFileSync(path, "utf8")) as Record<string, Entry>;
26
+ } catch {
27
+ data = {};
28
+ }
29
+ }
30
+ const persist = () => {
31
+ const dir = dirname(path);
32
+ if (dir) mkdirSync(dir, { recursive: true });
33
+ const tmp = `${path}.tmp`;
34
+ writeFileSync(tmp, JSON.stringify(data));
35
+ renameSync(tmp, path);
36
+ };
37
+ const fresh = (key: string): Entry | null => {
38
+ const e = data[key];
39
+ if (!e) return null;
40
+ if (e.exp && Date.now() > e.exp) {
41
+ delete data[key];
42
+ return null;
43
+ }
44
+ return e;
45
+ };
46
+ return {
47
+ async get(key) {
48
+ return fresh(key)?.value ?? null;
49
+ },
50
+ async set(key, value, opts) {
51
+ data[key] = {
52
+ value,
53
+ exp: opts?.ttlSeconds ? Date.now() + opts.ttlSeconds * 1000 : undefined,
54
+ };
55
+ persist();
56
+ },
57
+ async delete(key) {
58
+ delete data[key];
59
+ persist();
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,34 @@
1
+ import type { KVStorage } from "../types.js";
2
+
3
+ interface Entry {
4
+ value: string;
5
+ exp?: number; // epoch ms
6
+ }
7
+
8
+ /** In-memory KV store with expiry. The default for dev and Node. */
9
+ export function memoryStorage(): KVStorage {
10
+ const map = new Map<string, Entry>();
11
+ const fresh = (key: string): Entry | null => {
12
+ const e = map.get(key);
13
+ if (!e) return null;
14
+ if (e.exp && Date.now() > e.exp) {
15
+ map.delete(key);
16
+ return null;
17
+ }
18
+ return e;
19
+ };
20
+ return {
21
+ async get(key) {
22
+ return fresh(key)?.value ?? null;
23
+ },
24
+ async set(key, value, opts) {
25
+ map.set(key, {
26
+ value,
27
+ exp: opts?.ttlSeconds ? Date.now() + opts.ttlSeconds * 1000 : undefined,
28
+ });
29
+ },
30
+ async delete(key) {
31
+ map.delete(key);
32
+ },
33
+ };
34
+ }
package/src/types.ts ADDED
@@ -0,0 +1,254 @@
1
+ // Core contracts for connecta. Web-API only — no node: imports here.
2
+
3
+ /** A JSON Schema object describing a tool's input. */
4
+ export type JsonSchema = Record<string, unknown>;
5
+
6
+ /** Minimal key/value store — the only state connecta needs. */
7
+ export interface KVStorage {
8
+ get(key: string): Promise<string | null>;
9
+ set(
10
+ key: string,
11
+ value: string,
12
+ opts?: { ttlSeconds?: number },
13
+ ): Promise<void>;
14
+ delete(key: string): Promise<void>;
15
+ }
16
+
17
+ export interface Logger {
18
+ debug(...args: unknown[]): void;
19
+ info(...args: unknown[]): void;
20
+ warn(...args: unknown[]): void;
21
+ error(...args: unknown[]): void;
22
+ }
23
+
24
+ export interface ToolDef {
25
+ name: string; // unique within the connector
26
+ description?: string;
27
+ inputSchema?: JsonSchema;
28
+ /** Optional JSON Schema describing the tool's structured result. */
29
+ outputSchema?: JsonSchema;
30
+ /**
31
+ * Standard MCP tool behavior hints plus provider-specific extensions.
32
+ * Connecta fails closed: only readOnlyHint === true (without a contradictory
33
+ * destructiveHint) may use call_tool, batch_call, or execute_code. Every
34
+ * other tool must cross the call_destructive_tool approval boundary.
35
+ */
36
+ annotations?: ToolAnnotations;
37
+ }
38
+
39
+ export interface ToolAnnotations extends Record<string, unknown> {
40
+ title?: string;
41
+ readOnlyHint?: boolean;
42
+ destructiveHint?: boolean;
43
+ idempotentHint?: boolean;
44
+ openWorldHint?: boolean;
45
+ }
46
+
47
+ export type ConnectorCredentialValues = Record<string, string>;
48
+
49
+ /** Read-only access to the credentials assigned to one connector. */
50
+ export interface ConnectorCredentialAccess {
51
+ /**
52
+ * Returns one decrypted field. Omitting `field` preserves the original
53
+ * single-credential behavior and reads the reserved `value` field.
54
+ */
55
+ get(field?: string): Promise<string | null>;
56
+ /** Returns every decrypted field, or null when nothing is configured. */
57
+ getAll(): Promise<ConnectorCredentialValues | null>;
58
+ }
59
+
60
+ /** Operator-facing description of one named credential field. */
61
+ export interface ConnectorCredentialFieldConfig {
62
+ /** Stable field name used by connector code and the credential API. */
63
+ name: string;
64
+ /** Short field label, e.g. "Account email". */
65
+ label: string;
66
+ /** Plain-language guidance shown in /ui. Never include the credential itself. */
67
+ description?: string;
68
+ /** Input placeholder, e.g. "you@example.com". */
69
+ placeholder?: string;
70
+ /** Browser input type. Defaults to password. */
71
+ inputType?: "email" | "password" | "text";
72
+ }
73
+
74
+ /** Operator-facing description of the credential set a connector needs. */
75
+ export interface ConnectorCredentialConfig {
76
+ /** Short group or field label, e.g. "API token" or "Service credentials". */
77
+ label: string;
78
+ /** Plain-language guidance shown in /ui. Never include the credential itself. */
79
+ description?: string;
80
+ /** Password-field placeholder, e.g. "Paste API token". */
81
+ placeholder?: string;
82
+ /**
83
+ * Named fields for multi-value authentication. Omit to retain the original
84
+ * one-secret credential behavior.
85
+ */
86
+ fields?: ConnectorCredentialFieldConfig[];
87
+ }
88
+
89
+ export interface CredentialTestResult {
90
+ ok: boolean;
91
+ message?: string;
92
+ }
93
+
94
+ export interface ConnectorContext {
95
+ /** Storage namespaced to this connector. */
96
+ storage: KVStorage;
97
+ logger: Logger;
98
+ /** Public base URL of this deployment (origin), used for OAuth callbacks. */
99
+ baseUrl: string;
100
+ /**
101
+ * Read-only access to this connector's operator-managed credential. Present
102
+ * only when the connector declares `credential` and the deployment configures
103
+ * `credentialEncryptionKey`.
104
+ */
105
+ credential?: ConnectorCredentialAccess;
106
+ /**
107
+ * Identity shared by connector calls that belong to one inbound request.
108
+ * Connectors may use it to reuse request-safe resources within that request,
109
+ * but must never retain I/O resources beyond the scope's lifetime.
110
+ *
111
+ * Optional for custom/test contexts; the context object itself is the scope
112
+ * when omitted.
113
+ */
114
+ requestScope?: object;
115
+ /** Best-effort cancellation signal for this individual tool call. */
116
+ signal?: AbortSignal;
117
+ /** Requested tool-call deadline in milliseconds. */
118
+ timeoutMs?: number;
119
+ }
120
+
121
+ export type ConnectorStatusState = "ok" | "auth_required" | "error";
122
+
123
+ export interface ConnectorStatus {
124
+ state: ConnectorStatusState;
125
+ /** When state === "auth_required", the URL the operator should open. */
126
+ authorizationUrl?: string;
127
+ message?: string;
128
+ }
129
+
130
+ /** The whole plugin contract — the one open seam. */
131
+ export interface Connector {
132
+ id: string; // address prefix; [a-z0-9_-]+
133
+ /** Human-readable display name; the stable `id` remains the tool-address prefix. */
134
+ title?: string;
135
+ /** How call_tool wraps results. "mcp" passes the content array through; anything else is JSON-wrapped. */
136
+ kind?: "mcp" | "api";
137
+ description?: string;
138
+ /** Optional operator-managed credential slot rendered inside this connector's /ui card. */
139
+ credential?: ConnectorCredentialConfig;
140
+ /** Optional server-side check used by /ui's Test action. */
141
+ testCredential?(
142
+ value: string,
143
+ ctx: ConnectorContext,
144
+ ): Promise<CredentialTestResult>;
145
+ /** Optional multi-field credential check used by /ui's Test action. */
146
+ testCredentials?(
147
+ values: ConnectorCredentialValues,
148
+ ctx: ConnectorContext,
149
+ ): Promise<CredentialTestResult>;
150
+ /**
151
+ * Statically-known tool defs, exposed by in-code connectors (`api()`) for
152
+ * startup convention checks. Remote connectors omit this — their tools are
153
+ * fetched lazily over the network and are not known at construction time.
154
+ */
155
+ staticTools?: ToolDef[];
156
+ listTools(ctx: ConnectorContext): Promise<ToolDef[]>;
157
+ callTool(
158
+ name: string,
159
+ args: unknown,
160
+ ctx: ConnectorContext,
161
+ ): Promise<unknown>;
162
+ /** Optional connector-level health/auth status for list_connectors. */
163
+ status?(ctx: ConnectorContext): Promise<ConnectorStatus>;
164
+ /**
165
+ * Optional: start (or with force, restart from scratch) a downstream OAuth
166
+ * flow (called by authorize_connector). Present only on connectors that use
167
+ * downstream OAuth. Returns the resulting status — "auth_required" with an
168
+ * authorizationUrl when there is a URL to open, "ok" when already authorized.
169
+ */
170
+ startAuth?(
171
+ ctx: ConnectorContext,
172
+ opts?: { force?: boolean },
173
+ ): Promise<ConnectorStatus>;
174
+ /**
175
+ * Optional: verify the OAuth `state` returned to /oauth/callback/<id> against
176
+ * the value this connector generated when it started the flow. Present only
177
+ * on downstream-OAuth connectors. The callback MUST reject before finishAuth
178
+ * when this returns false — otherwise anyone holding the pending URL could
179
+ * complete consent with their own account.
180
+ */
181
+ verifyState?(state: string | null, ctx: ConnectorContext): Promise<boolean>;
182
+ /** Optional: complete a downstream OAuth flow (called by /oauth/callback/<id>). */
183
+ finishAuth?(code: string, ctx: ConnectorContext): Promise<void>;
184
+ }
185
+
186
+ /** Result of one sandboxed code execution. */
187
+ export interface ExecuteResult {
188
+ result: unknown;
189
+ error?: string;
190
+ logs?: string[];
191
+ }
192
+
193
+ /** A named group of host functions exposed to sandboxed code as a global. */
194
+ export interface ExecutorProvider {
195
+ name: string;
196
+ fns: Record<string, (...args: unknown[]) => Promise<unknown>>;
197
+ }
198
+
199
+ /**
200
+ * Runs model-written JavaScript in a sandbox where the ONLY capabilities are
201
+ * the provider functions — no network, filesystem, env, or timers. Structurally
202
+ * compatible with `DynamicWorkerExecutor` from `@cloudflare/codemode` (Workers);
203
+ * `quickJsExecutor()` from "@zackbart/connecta/quickjs" is the Node implementation.
204
+ * NEVER back this with an unsandboxed eval — the code is untrusted.
205
+ */
206
+ export interface Executor {
207
+ execute(code: string, providers: ExecutorProvider[]): Promise<ExecuteResult>;
208
+ }
209
+
210
+ /** Result of an inbound-auth check. */
211
+ export type AuthResult =
212
+ | { ok: true; userId?: string; subjectId?: string }
213
+ | { ok: false; response: Response };
214
+
215
+ /** Public browser-auth configuration exposed to connecta's status UI. */
216
+ export type UiAuthConfig = {
217
+ kind: "clerk";
218
+ publishableKey: string;
219
+ frontendApiUrl: string;
220
+ signInUrl?: string;
221
+ signUpUrl?: string;
222
+ };
223
+
224
+ /** Optional labels used by Connecta's browser UI and OAuth result pages. */
225
+ export interface ConnectaBranding {
226
+ /** Product label. Defaults to "Connecta". */
227
+ productName?: string;
228
+ /** Organization or owner shown beside the product label. */
229
+ ownerName?: string;
230
+ /** Optional link for the organization or owner label. */
231
+ ownerUrl?: string;
232
+ /** Status-dashboard introduction. */
233
+ description?: string;
234
+ }
235
+
236
+ /** An inbound authentication provider (bearer token, Clerk, ...). */
237
+ export interface InboundAuth {
238
+ kind: string;
239
+ /**
240
+ * Optional browser sign-in configuration. When present, `/ui` uses this
241
+ * provider instead of asking the operator to paste a static bearer secret.
242
+ */
243
+ uiAuth?: UiAuthConfig;
244
+ /** Serve/short-circuit .well-known + OPTIONS. Return null when not handled. */
245
+ handleMetadata?(
246
+ request: Request,
247
+ baseUrl: string,
248
+ ): Response | null | Promise<Response | null>;
249
+ /** Attempt to authorize a request. */
250
+ authorize(
251
+ request: Request,
252
+ baseUrl: string,
253
+ ): AuthResult | Promise<AuthResult>;
254
+ }