@zackbart/connecta 0.33.0 → 0.34.1

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 (131) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +100 -128
  3. package/SECURITY.md +1 -1
  4. package/bin/version.mjs +1 -1
  5. package/dist/activity.d.ts +3 -0
  6. package/dist/branding.d.ts +7 -0
  7. package/dist/branding.js +76 -9
  8. package/dist/catalog-drift.d.ts +30 -26
  9. package/dist/catalog-drift.js +169 -84
  10. package/dist/catalog-service.js +16 -2
  11. package/dist/config-value-sources.js +6 -1
  12. package/dist/config.d.ts +13 -2
  13. package/dist/config.js +36 -2
  14. package/dist/connector-caller.js +2 -1
  15. package/dist/connectors/api-connector.d.ts +3 -0
  16. package/dist/connectors/api-connector.js +5 -1
  17. package/dist/connectors/option-shapes.js +1 -1
  18. package/dist/connectors/remote-mcp.d.ts +2 -0
  19. package/dist/connectors/remote-mcp.js +5 -1
  20. package/dist/describe-config.d.ts +5 -1
  21. package/dist/describe-config.js +2 -0
  22. package/dist/execute.js +1 -11
  23. package/dist/fix-prompt.js +2 -2
  24. package/dist/index.js +1 -0
  25. package/dist/invocation.js +3 -2
  26. package/dist/membership.d.ts +2 -0
  27. package/dist/meta-tools.js +3 -2
  28. package/dist/oauth-callback-outcome.js +2 -2
  29. package/dist/operator-ui/agents.d.ts +1 -1
  30. package/dist/operator-ui/agents.js +16 -16
  31. package/dist/operator-ui/config-summary.js +565 -0
  32. package/dist/operator-ui/connection-map.js +376 -0
  33. package/dist/operator-ui/endpoints.js +45 -16
  34. package/dist/operator-ui/fix-prompts.js +1 -1
  35. package/dist/operator-ui/generated.js +4 -4
  36. package/dist/operator-ui/health.js +52 -0
  37. package/dist/operator-ui/member-page.js +13 -28
  38. package/dist/operator-ui/overview.js +123 -0
  39. package/dist/operator-ui/view.d.ts +26 -22
  40. package/dist/operator-ui/view.js +130 -96
  41. package/dist/page-styles.js +5 -3
  42. package/dist/provider.d.ts +6 -1
  43. package/dist/provider.js +18 -1
  44. package/dist/providers/_shared/google/workspace.d.ts +9 -4
  45. package/dist/providers/_shared/google/workspace.js +7 -3
  46. package/dist/providers/basecamp/index.d.ts +2 -0
  47. package/dist/providers/breeze/index.d.ts +2 -0
  48. package/dist/providers/ccb/index.d.ts +2 -0
  49. package/dist/providers/ccb/index.js +2 -2
  50. package/dist/providers/cloudflare/index.d.ts +2 -0
  51. package/dist/providers/cloudflare/index.js +5 -1
  52. package/dist/providers/cloudflare/openapi.generated.js +3296 -3206
  53. package/dist/providers/cloudflare/rest.js +19 -0
  54. package/dist/providers/cloudflare/skill.generated.js +2 -2
  55. package/dist/providers/cloudflare/value-safety.js +51 -18
  56. package/dist/providers/docs/index.js +1 -1
  57. package/dist/providers/docs/skill.generated.js +2 -2
  58. package/dist/providers/drive/index.js +1 -1
  59. package/dist/providers/drive/skill.generated.js +2 -2
  60. package/dist/providers/forms/index.js +1 -1
  61. package/dist/providers/forms/skill.generated.js +2 -2
  62. package/dist/providers/gmail/index.js +1 -1
  63. package/dist/providers/gmail/skill.generated.js +2 -2
  64. package/dist/providers/intercom/index.d.ts +24 -0
  65. package/dist/providers/intercom/index.js +121 -0
  66. package/dist/providers/intercom/skill.generated.js +8 -0
  67. package/dist/providers/linear/index.js +204 -19
  68. package/dist/providers/linear/skill.generated.js +2 -2
  69. package/dist/providers/mixpanel/index.d.ts +2 -0
  70. package/dist/providers/mixpanel/index.js +21 -17
  71. package/dist/providers/mixpanel/skill.generated.js +3 -3
  72. package/dist/providers/notion/index.d.ts +2 -0
  73. package/dist/providers/notion/index.js +1 -1
  74. package/dist/providers/notion/skill.generated.js +1 -1
  75. package/dist/providers/overflow/index.d.ts +2 -0
  76. package/dist/providers/overflow/index.js +1 -1
  77. package/dist/providers/overflow/skill.generated.js +2 -2
  78. package/dist/providers/planning-center/index.d.ts +2 -0
  79. package/dist/providers/planning-center/index.js +1 -1
  80. package/dist/providers/planning-center/skill.generated.js +4 -4
  81. package/dist/providers/revenuecat/index.d.ts +5 -3
  82. package/dist/providers/revenuecat/index.js +147 -7
  83. package/dist/providers/revenuecat/skill.generated.js +3 -3
  84. package/dist/providers/sheets/index.js +1 -1
  85. package/dist/providers/sheets/skill.generated.js +2 -2
  86. package/dist/providers/slides/index.js +1 -1
  87. package/dist/providers/slides/skill.generated.js +2 -2
  88. package/dist/providers/stripe/index.d.ts +2 -0
  89. package/dist/providers/stripe/index.js +50 -30
  90. package/dist/providers/stripe/skill.generated.js +2 -1
  91. package/dist/providers/tithely/index.d.ts +2 -0
  92. package/dist/providers/tithely/index.js +1 -1
  93. package/dist/providers/vercel/index.d.ts +2 -0
  94. package/dist/providers/vercel/index.js +2 -2
  95. package/dist/providers/vercel/openapi.generated.js +391 -374
  96. package/dist/providers/vercel/skill.generated.js +2 -2
  97. package/dist/providers/vercel/value-safety.js +14 -4
  98. package/dist/registry.d.ts +10 -14
  99. package/dist/registry.js +77 -62
  100. package/dist/routes/mcp.js +20 -13
  101. package/dist/routes/oauth.js +2 -2
  102. package/dist/routes/ui-data.js +8 -2
  103. package/dist/skills.js +18 -2
  104. package/dist/storage/sql.js +5 -1
  105. package/dist/types.d.ts +36 -11
  106. package/dist/ui.js +9 -4
  107. package/dist/usage-guide.js +1 -1
  108. package/dist/version.d.ts +1 -1
  109. package/dist/version.js +1 -1
  110. package/documentation/README.md +2 -2
  111. package/documentation/architecture.md +66 -18
  112. package/documentation/auth.md +183 -67
  113. package/documentation/code-mode.md +30 -10
  114. package/documentation/deploying.md +5 -1
  115. package/documentation/integrating.md +64 -7
  116. package/documentation/meta-tools.md +38 -24
  117. package/documentation/migration-0.33.md +21 -13
  118. package/documentation/migration-0.34.md +67 -0
  119. package/documentation/operating.md +1 -1
  120. package/documentation/operator-tests.md +52 -20
  121. package/documentation/operator-ui.md +341 -104
  122. package/documentation/provider-auth-migration.md +22 -0
  123. package/examples/worker/AGENTS.md +15 -9
  124. package/examples/worker/README.md +41 -14
  125. package/examples/worker/src/connecta.config.ts +5 -4
  126. package/examples/worker/wrangler.jsonc +1 -1
  127. package/package.json +23 -19
  128. package/templates/node/AGENTS.md +13 -7
  129. package/templates/node/README.md +14 -11
  130. package/templates/node/package.json +1 -1
  131. package/templates/node/src/connecta.config.ts +2 -2
@@ -0,0 +1,565 @@
1
+ // The configuration snapshot as glanceable tiles for Settings and a
2
+ // connector's Configuration card. Pure: it maps `describeConfig()` and its
3
+ // provenance to labels and formatted values, and the components in
4
+ // app/config-tiles.tsx only draw them. A key this file does not know still
5
+ // becomes a tile with a label made from its name, so nothing in the snapshot
6
+ // disappears when the serializer grows a field.
7
+ const NUMBER = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
8
+ /** Bytes in decimal units throughout, so 24,000 reads "24 KB" and 8 MiB reads "8.4 MB". */
9
+ function formatBytes(bytes) {
10
+ for (const [unit, size] of [
11
+ ["GB", 1e9],
12
+ ["MB", 1e6],
13
+ ["KB", 1e3],
14
+ ])
15
+ if (bytes >= size)
16
+ return `${NUMBER.format(bytes / size)} ${unit}`;
17
+ return `${NUMBER.format(bytes)} B`;
18
+ }
19
+ /** The exact count behind a rounded size, for a tooltip. */
20
+ export function exactBytes(bytes) {
21
+ return `${bytes.toLocaleString("en-US")} bytes`;
22
+ }
23
+ const DURATIONS = [
24
+ [86_400_000, "day", "days"],
25
+ [3_600_000, "hr", "hr"],
26
+ [60_000, "min", "min"],
27
+ [1_000, "s", "s"],
28
+ ];
29
+ /** A duration in the largest unit it fills: 60000 is "1 min", 90000 is "90 s". */
30
+ function formatMs(ms) {
31
+ if (ms === 0)
32
+ return "0 s";
33
+ for (const [size, one, many] of DURATIONS)
34
+ if (ms >= size && ms % size === 0)
35
+ return `${NUMBER.format(ms / size)} ${ms === size ? one : many}`;
36
+ for (const [size, , many] of DURATIONS)
37
+ if (ms >= size)
38
+ return `${NUMBER.format(ms / size)} ${many}`;
39
+ return `${NUMBER.format(ms)} ms`;
40
+ }
41
+ /** Format one value. Booleans read On or Off, never true or false; null reads None. */
42
+ export function formatValue(value, format) {
43
+ if (value === null || value === undefined)
44
+ return "None";
45
+ if (typeof value === "boolean")
46
+ return value ? "On" : "Off";
47
+ if (Array.isArray(value))
48
+ return value.length === 0
49
+ ? "None"
50
+ : value.every((item) => typeof item === "string" || typeof item === "number")
51
+ ? value.join(", ")
52
+ : `${NUMBER.format(value.length)} items`;
53
+ if (typeof value === "number" && Number.isFinite(value)) {
54
+ if (format === "bytes")
55
+ return formatBytes(value);
56
+ if (format === "ms")
57
+ return formatMs(value);
58
+ if (format === "seconds")
59
+ return formatMs(value * 1000);
60
+ if (format === "days")
61
+ return `${NUMBER.format(value)} ${value === 1 ? "day" : "days"}`;
62
+ return NUMBER.format(value);
63
+ }
64
+ if (typeof value === "object")
65
+ return `${NUMBER.format(Object.keys(value).length)} items`;
66
+ return String(value);
67
+ }
68
+ const UNIT_SUFFIX = /(Ms|Seconds|Bytes|Days)$/;
69
+ /** The format a key's name implies: `probeTimeoutMs` is a duration, `maxStashBytes` a size. */
70
+ export function inferFormat(key, value) {
71
+ if (typeof value === "boolean")
72
+ return "toggle";
73
+ if (typeof value !== "number")
74
+ return "text";
75
+ const unit = UNIT_SUFFIX.exec(key)?.[1];
76
+ return unit === "Ms"
77
+ ? "ms"
78
+ : unit === "Seconds"
79
+ ? "seconds"
80
+ : unit === "Bytes"
81
+ ? "bytes"
82
+ : unit === "Days"
83
+ ? "days"
84
+ : "count";
85
+ }
86
+ /** `maxStashEntries` reads "Max stash entries"; a unit suffix drops because the value carries it. */
87
+ export function humanizeKey(key) {
88
+ // A word with two capitals is an acronym (OAuth, URL) and keeps its case.
89
+ const words = key
90
+ .replace(UNIT_SUFFIX, "")
91
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
92
+ .split(/[\s_.-]+/)
93
+ .filter(Boolean)
94
+ .map((word) => (/[A-Z].*[A-Z]/.test(word) ? word : word.toLowerCase()))
95
+ .join(" ");
96
+ return words ? words[0].toUpperCase() + words.slice(1) : key;
97
+ }
98
+ /**
99
+ * Labels for every limit the snapshot carries today, by `group.key`. Headline
100
+ * limits draw large in this order; the rest fold into a group card under
101
+ * `short`, a label the card's title makes unambiguous.
102
+ */
103
+ const LIMITS = {
104
+ "calls.defaultTimeoutMs": { label: "Tool timeout", headline: 1 },
105
+ "calls.maxResultBytes": { label: "Result cap", headline: 2 },
106
+ "execute.watchdogMs": { label: "Program timeout", headline: 3 },
107
+ "discovery.catalogTtlSeconds": { label: "Catalog refresh", headline: 4 },
108
+ "execute.maxHostCalls": { label: "Calls per program", headline: 6 },
109
+ "execute.maxWrites": { label: "Writes per program", headline: 7 },
110
+ "requests.concurrency": { label: "Concurrent requests", headline: 8 },
111
+ "discovery.concurrency": { label: "Probe concurrency", short: "Concurrency" },
112
+ "discovery.catalogMinTtlSeconds": { label: "Min refresh" },
113
+ "discovery.catalogMaxTtlSeconds": { label: "Max refresh" },
114
+ "discovery.probeTimeoutMs": { label: "Probe timeout" },
115
+ "results.maxStashBytes": { label: "Stash size" },
116
+ "results.maxStashEntries": { label: "Stash entries" },
117
+ "execute.maxEmittedBytes": { label: "Program output", short: "Output" },
118
+ "execute.maxEmittedBlocks": { label: "Output blocks" },
119
+ "execute.hostCallTimeoutMs": { label: "Host call timeout", short: "Call timeout" },
120
+ "requests.maxQueueSize": { label: "Request queue", short: "Queue" },
121
+ "requests.queueTimeoutMs": { label: "Request queue wait", short: "Queue wait" },
122
+ "requests.retryAfterMs": { label: "Request retry after", short: "Retry after" },
123
+ "requests.maxDurationMs": { label: "Request lifetime", short: "Lifetime" },
124
+ "admission.concurrency": { label: "Concurrent programs", short: "Concurrency" },
125
+ "admission.maxQueueSize": { label: "Program queue", short: "Queue" },
126
+ "admission.queueTimeoutMs": { label: "Program queue wait", short: "Queue wait" },
127
+ "admission.retryAfterMs": { label: "Program retry after", short: "Retry after" },
128
+ };
129
+ /** The card a default limit folds into, by its snapshot group. */
130
+ const DEFAULT_GROUPS = {
131
+ discovery: "Discovery",
132
+ calls: "Results",
133
+ results: "Results",
134
+ execute: "Programs",
135
+ admission: "Programs",
136
+ requests: "Requests",
137
+ };
138
+ function isLimit(value) {
139
+ return (typeof value === "object" &&
140
+ value !== null &&
141
+ "value" in value &&
142
+ "source" in value &&
143
+ Object.keys(value).length === 2);
144
+ }
145
+ /** A limit's tile. A byte size carries its exact count as a title. */
146
+ function limitTile(path, key, limit, sources, label) {
147
+ const id = `config.${path}`;
148
+ const format = inferFormat(key, limit.value);
149
+ return {
150
+ id,
151
+ label: label ?? humanizeKey(key),
152
+ value: limit.value === null ? "No limit" : formatValue(limit.value, format),
153
+ set: (sources[id] ?? limit.source) === "config",
154
+ ...(format === "bytes" && typeof limit.value === "number" ? { title: exactBytes(limit.value) } : {}),
155
+ };
156
+ }
157
+ const SIGN_IN = {
158
+ clerk: { label: "Clerk", mark: "clerk" },
159
+ "cloudflare-access": { label: "Cloudflare Access", mark: "access" },
160
+ access_token: { label: "Agent tokens", mark: "tokens" },
161
+ };
162
+ const STORE = { memory: "Memory", d1: "D1", sqlite: "SQLite", custom: "Custom" };
163
+ /** Known modules; any other `modules.<name>.enabled` becomes a tile named from its key. */
164
+ const MODULES = {
165
+ vault: { label: "Vault", icon: "lock" },
166
+ activity: { label: "Activity", icon: "activity" },
167
+ accessTokens: { label: "Agent tokens", icon: "key" },
168
+ };
169
+ /**
170
+ * Snapshot paths Settings draws, or leaves to the page that owns them
171
+ * (branding to the shell, pools to Endpoints, connectors to their pages,
172
+ * activity reads to Activity). Everything else is "other".
173
+ */
174
+ const KNOWN = [
175
+ "schemaVersion",
176
+ "connectaVersion",
177
+ "server.name",
178
+ "server.version",
179
+ "server.title",
180
+ "server.websiteUrl",
181
+ "server.icons",
182
+ "urls.publicUrl",
183
+ "urls.mcpPath",
184
+ "urls.allowedOrigins",
185
+ "executor",
186
+ "limits",
187
+ "readOnly",
188
+ "maintainers",
189
+ "classification",
190
+ "auth",
191
+ "agents",
192
+ "pools",
193
+ "modules.vault.sealsOAuth",
194
+ "modules.activity.readable",
195
+ "modules.activity.deploymentId",
196
+ "modules.activity.store",
197
+ "storage.configured",
198
+ "storage.kind",
199
+ "branding",
200
+ "deploymentInfo",
201
+ "connectors",
202
+ ];
203
+ function known(path, prefixes) {
204
+ return prefixes.some((prefix) => path === prefix || path.startsWith(`${prefix}.`));
205
+ }
206
+ /** Leaves under `value`, a described limit counting as one leaf and an array as one value. */
207
+ function leaves(value, path) {
208
+ if (value === null || typeof value !== "object" || Array.isArray(value) || isLimit(value))
209
+ return [[path, value]];
210
+ const entries = Object.entries(value);
211
+ return entries.length ? entries.flatMap(([key, item]) => leaves(item, path ? `${path}.${key}` : key)) : [];
212
+ }
213
+ /**
214
+ * A tile for a key nobody mapped: its own name, or its parent's and its own
215
+ * when the name alone is vague. It reads as set only when the snapshot itself
216
+ * says so: the server reports an unknown path as "config" by default.
217
+ */
218
+ function otherTile(path, value, prefix) {
219
+ const parts = path.split(".");
220
+ const key = parts.at(-1);
221
+ const parent = parts.length > 1 ? parts.at(-2) : undefined;
222
+ const inner = isLimit(value) ? value : null;
223
+ const raw = inner ? inner.value : value;
224
+ const format = inferFormat(key, raw);
225
+ return {
226
+ id: `${prefix}.${path}`,
227
+ label: parent && /^(enabled|value|kind|mode|name)$/.test(key)
228
+ ? `${humanizeKey(parent)} ${humanizeKey(key).toLowerCase()}`
229
+ : humanizeKey(key),
230
+ value: formatValue(raw, format),
231
+ set: inner?.source === "config",
232
+ ...(format === "bytes" && typeof raw === "number" ? { title: exactBytes(raw) } : {}),
233
+ };
234
+ }
235
+ function hostOf(url) {
236
+ return url.replace(/^https?:\/\//, "");
237
+ }
238
+ function endpointText(endpoint) {
239
+ if (!endpoint)
240
+ return undefined;
241
+ const host = hostOf(endpoint.origin);
242
+ return endpoint.path && endpoint.path !== "/" ? `${host}${endpoint.path}` : host;
243
+ }
244
+ const plural = (count, one, many = `${one}s`) => `${formatValue(count, "count")} ${count === 1 ? one : many}`;
245
+ /** Settings, as tiles. */
246
+ export function configSummary(config, sources = {}) {
247
+ const name = config.server.title ?? config.server.name;
248
+ const version = config.server.version;
249
+ const deployment = [
250
+ { id: "name", label: "Name", value: version ? `${name} · ${/^v/i.test(version) ? version : `v${version}`}` : name },
251
+ { id: "mode", label: "Mode", value: config.readOnly ? "Read-only" : "Writes allowed" },
252
+ { id: "connecta", label: "Connecta", value: config.connectaVersion, mono: true },
253
+ ];
254
+ const url = endpointText(config.urls.publicUrl);
255
+ if (url)
256
+ deployment.push({ id: "url", label: "URL", value: url, mono: true });
257
+ const origins = config.urls.allowedOrigins;
258
+ deployment.push({
259
+ id: "origins",
260
+ label: "Origins",
261
+ value: origins === "default"
262
+ ? "Default"
263
+ : origins === "*"
264
+ ? "Any"
265
+ : origins.length === 1
266
+ ? hostOf(origins[0])
267
+ : `${origins.length}`,
268
+ mono: Array.isArray(origins) && origins.length === 1,
269
+ });
270
+ if (config.executor.name)
271
+ deployment.push({ id: "sandbox", label: "Sandbox", value: config.executor.name });
272
+ // Unconfigured memory storage loses tokens, credentials and OAuth state on restart.
273
+ const volatile = !config.storage.configured && config.storage.kind === "memory";
274
+ deployment.push({
275
+ id: "storage",
276
+ label: "Storage",
277
+ value: volatile ? "Memory · default" : (STORE[config.storage.kind] ?? config.storage.kind),
278
+ ...(volatile ? { tone: "warn", title: "Lost on restart" } : {}),
279
+ });
280
+ if (config.modules.activity.enabled && config.modules.activity.store)
281
+ deployment.push({
282
+ id: "history",
283
+ label: "History",
284
+ value: STORE[config.modules.activity.store.kind] ?? config.modules.activity.store.kind,
285
+ });
286
+ if (config.modules.activity.deploymentId)
287
+ deployment.push({
288
+ id: "deploymentId",
289
+ label: "Deployment ID",
290
+ value: config.modules.activity.deploymentId,
291
+ mono: true,
292
+ });
293
+ const website = endpointText(config.server.websiteUrl);
294
+ if (website)
295
+ deployment.push({ id: "website", label: "Website", value: website, mono: true });
296
+ if (config.server.icons > 0)
297
+ deployment.push({ id: "icons", label: "Icons", value: formatValue(config.server.icons, "count") });
298
+ if (config.deploymentInfo.length)
299
+ deployment.push({ id: "info", label: "Info", value: config.deploymentInfo.join(", "), mono: true });
300
+ const signIn = config.auth.map((provider, index) => {
301
+ const knownProvider = SIGN_IN[provider.kind];
302
+ const mark = knownProvider?.mark ?? "other";
303
+ return {
304
+ id: `${provider.kind}-${index}`,
305
+ label: knownProvider?.label ?? humanizeKey(provider.kind),
306
+ mark,
307
+ interactive: provider.interactive,
308
+ ...(mark === "tokens" ? { count: plural(config.agents.length, "agent") } : {}),
309
+ };
310
+ });
311
+ // The UI module is on whenever this page draws, so it has no tile.
312
+ const features = [];
313
+ for (const [id, module] of Object.entries(config.modules)) {
314
+ if (id === "ui" || typeof module?.enabled !== "boolean")
315
+ continue;
316
+ const meta = MODULES[id] ?? { label: humanizeKey(id), icon: "box" };
317
+ features.push({
318
+ id,
319
+ label: meta.label,
320
+ icon: meta.icon,
321
+ on: module.enabled,
322
+ ...(id === "vault" && config.modules.vault.sealsOAuth ? { detail: "Seals OAuth" } : {}),
323
+ });
324
+ }
325
+ const all = [];
326
+ const hostCall = config.limits.execute.hostCallTimeoutMs;
327
+ for (const [group, values] of Object.entries(config.limits))
328
+ for (const [key, value] of Object.entries(values)) {
329
+ if (!isLimit(value))
330
+ continue;
331
+ const meta = LIMITS[`${group}.${key}`];
332
+ const path = `limits.${group}.${key}`;
333
+ // A null tool timeout falls back to the host-call timeout at runtime.
334
+ const fallback = group === "calls" && key === "defaultTimeoutMs" && value.value === null && hostCall;
335
+ const tile = fallback
336
+ ? { ...limitTile(path, key, { value: hostCall.value, source: "default" }, {}, meta?.label), set: false }
337
+ : limitTile(path, key, value, sources, meta?.label);
338
+ all.push({ ...tile, headline: meta?.headline ?? 0, group, short: meta?.short });
339
+ }
340
+ if (config.executor.admission !== "executor")
341
+ for (const [key, limit] of Object.entries(config.executor.admission)) {
342
+ if (!isLimit(limit))
343
+ continue;
344
+ const meta = LIMITS[`admission.${key}`];
345
+ all.push({
346
+ ...limitTile(`executor.admission.${key}`, key, limit, sources, meta?.label),
347
+ headline: 0,
348
+ group: "admission",
349
+ short: meta?.short,
350
+ });
351
+ }
352
+ const retention = config.modules.activity.enabled ? config.modules.activity.store?.retentionDays : undefined;
353
+ if (retention !== undefined) {
354
+ // The source map reads "config" only when the store reports its own option as set.
355
+ const id = "config.modules.activity.store.retentionDays";
356
+ all.push({
357
+ id,
358
+ label: "Activity kept",
359
+ value: formatValue(retention, "days"),
360
+ set: sources[id] === "config",
361
+ headline: 5,
362
+ group: "activity",
363
+ short: undefined,
364
+ });
365
+ }
366
+ const strip = ({ headline: _, group: __, short: ___, ...tile }) => tile;
367
+ // Headline limits in a fixed order, then any other limit code set.
368
+ const limits = [
369
+ ...all.filter((tile) => tile.headline).sort((a, b) => a.headline - b.headline),
370
+ ...all.filter((tile) => !tile.headline && tile.set),
371
+ ].map(strip);
372
+ const defaults = [];
373
+ for (const tile of all.filter((item) => !item.headline && !item.set)) {
374
+ const title = DEFAULT_GROUPS[tile.group] ?? humanizeKey(tile.group);
375
+ let group = defaults.find((item) => item.title === title);
376
+ if (!group)
377
+ defaults.push((group = { id: title.toLowerCase().replace(/\W+/g, "-"), title, tiles: [] }));
378
+ group.tiles.push({ ...strip(tile), label: tile.short ?? tile.label });
379
+ }
380
+ const overrides = Object.entries(config.classification).flatMap(([connector, tools]) => Object.entries(tools).map(([tool, classification]) => ({ address: `${connector}.${tool}`, classification })));
381
+ const other = leaves(config, "")
382
+ .filter(([path]) => !known(path, KNOWN) && !/^modules\.[^.]+\.enabled$/.test(path))
383
+ .map(([path, value]) => otherTile(path, value, "config"));
384
+ return { deployment, signIn, features, limits, defaults, overrides, other };
385
+ }
386
+ const SOURCE_KIND = {
387
+ "remote-mcp": { label: "Remote MCP", icon: "plug" },
388
+ api: { label: "HTTP API", icon: "globe" },
389
+ custom: { label: "Custom", icon: "box" },
390
+ };
391
+ const AUTH_MODE = {
392
+ none: "No auth",
393
+ headers: "Headers",
394
+ credential: "API key",
395
+ oauth: "OAuth",
396
+ request: "Request token",
397
+ };
398
+ /**
399
+ * Connector leaves the card draws, plus those the page leaves to another tab:
400
+ * prose (description, guide summary, credential help) and the Tools tab's
401
+ * catalog. Anything else becomes a tile, so a new field is never hidden.
402
+ */
403
+ const CONNECTOR_KNOWN = [
404
+ "readOnly",
405
+ "id",
406
+ "title",
407
+ "description",
408
+ "authScope",
409
+ "source.kind",
410
+ "source.provider",
411
+ "endpoint.origin",
412
+ "endpoint.path",
413
+ "auth.mode",
414
+ "auth.headerNames",
415
+ "auth.header",
416
+ "auth.scheme",
417
+ "auth.scope",
418
+ "auth.clientMetadataUrl",
419
+ "auth.authorizationEndpoint.origin",
420
+ "auth.authorizationEndpoint.path",
421
+ "auth.tokenEndpoint.origin",
422
+ "auth.tokenEndpoint.path",
423
+ "auth.apiOrigins",
424
+ "auth.tokenEndpointAuthMethod",
425
+ "auth.confidentialClient",
426
+ "auth.pkce",
427
+ "auth.authorizationParamNames",
428
+ "auth.tokenRequestHeaderNames",
429
+ "credential.label",
430
+ "credential.description",
431
+ "credential.fields",
432
+ "transport.versionNegotiation",
433
+ "transport.redirects",
434
+ "transport.requireHttps",
435
+ "maxResultBytes",
436
+ "callAdmission",
437
+ "usageGuide.summary",
438
+ "usageGuide.required",
439
+ "tools",
440
+ ];
441
+ /** How a connector authenticates, as one mode badge and mono badges for its headers, origins and OAuth facts. */
442
+ function authBadges(connector) {
443
+ const auth = connector.auth;
444
+ const mode = auth?.mode ?? (connector.credential ? "credential" : undefined);
445
+ if (!mode)
446
+ return [];
447
+ const badges = [
448
+ {
449
+ id: "auth",
450
+ label: connector.credential?.label ?? AUTH_MODE[mode] ?? humanizeKey(mode),
451
+ ...(mode === "none" ? {} : { icon: mode === "oauth" ? "lock" : "key" }),
452
+ },
453
+ ];
454
+ const mono = (id, label, extra = {}) => badges.push({ id, label, mono: true, ...extra });
455
+ const fields = connector.credential?.fields ?? [];
456
+ if (fields.length > 1)
457
+ for (const field of fields)
458
+ mono(`field.${field.name}`, field.name, { title: field.label });
459
+ if (auth?.header)
460
+ mono("header", auth.scheme ? `${auth.header}: ${auth.scheme}` : auth.header);
461
+ for (const name of auth?.headerNames ?? [])
462
+ mono(`headerNames.${name}`, name);
463
+ if (auth?.scope)
464
+ mono("scope", auth.scope, { prefix: "Scope" });
465
+ for (const origin of auth?.apiOrigins ?? [])
466
+ mono(`apiOrigins.${origin}`, `→ ${hostOf(origin)}`, { title: origin });
467
+ const authorize = endpointText(auth?.authorizationEndpoint);
468
+ if (authorize)
469
+ mono("authorize", authorize, { prefix: "Authorize" });
470
+ const token = endpointText(auth?.tokenEndpoint);
471
+ if (token)
472
+ mono("token", token, { prefix: "Token" });
473
+ if (auth?.clientMetadataUrl)
474
+ mono("client", hostOf(auth.clientMetadataUrl), { prefix: "Client" });
475
+ if (auth?.tokenEndpointAuthMethod)
476
+ mono("clientAuth", auth.tokenEndpointAuthMethod);
477
+ if (auth?.confidentialClient)
478
+ badges.push({ id: "secret", label: "Client secret" });
479
+ if (auth?.pkce)
480
+ badges.push({ id: "pkce", label: "PKCE" });
481
+ for (const name of auth?.authorizationParamNames ?? [])
482
+ mono(`param.${name}`, `?${name}`);
483
+ for (const name of auth?.tokenRequestHeaderNames ?? [])
484
+ mono(`tokenHeader.${name}`, name);
485
+ return badges;
486
+ }
487
+ /** A connector's Configuration card: what it is and how it signs in as badges, its limits as tiles. */
488
+ export function connectorSummary(connector, sources = {}) {
489
+ const base = `config.connectors.${connector.id}`;
490
+ const kind = SOURCE_KIND[connector.source.kind] ?? SOURCE_KIND.custom;
491
+ const badges = [{ id: "kind", label: kind.label, icon: kind.icon }];
492
+ if (connector.readOnly)
493
+ badges.push({ id: "readOnly", label: "Read-only" });
494
+ if (connector.source.provider)
495
+ badges.push({ id: "provider", label: connector.source.provider, icon: "box" });
496
+ badges.push(connector.authScope === "personal"
497
+ ? { id: "scope", label: "Per person", icon: "user" }
498
+ : { id: "scope", label: "Shared account", icon: "users" });
499
+ const endpoint = endpointText(connector.endpoint);
500
+ if (endpoint)
501
+ badges.push({ id: "endpoint", label: endpoint, mono: true });
502
+ if (connector.usageGuide)
503
+ badges.push({ id: "guide", label: connector.usageGuide.required ? "Guide required" : "Guide", icon: "book" });
504
+ const set = (path, fallback) => (sources[`${base}.${path}`] ?? (fallback ? "config" : "default")) === "config";
505
+ const tiles = [
506
+ {
507
+ id: `${base}.maxResultBytes`,
508
+ label: "Result cap",
509
+ value: formatValue(connector.maxResultBytes.value, "bytes"),
510
+ set: set("maxResultBytes", connector.maxResultBytes.source === "connector"),
511
+ title: exactBytes(connector.maxResultBytes.value),
512
+ },
513
+ ];
514
+ const admission = connector.callAdmission;
515
+ if (admission?.maxPartitions !== undefined)
516
+ tiles.push({
517
+ id: `${base}.callAdmission.maxPartitions`,
518
+ label: "Partitions",
519
+ value: formatValue(admission.maxPartitions, "count"),
520
+ set: true,
521
+ });
522
+ admission?.rules.forEach((rule, index) => {
523
+ const suffix = admission.rules.length > 1 ? ` ${index + 1}` : "";
524
+ const id = `${base}.callAdmission.rules.${index}`;
525
+ const add = (key, label, value) => tiles.push({ id: `${id}.${key}`, label: `${label}${suffix}`, value, set: true });
526
+ if (rule.maxConcurrency !== undefined)
527
+ add("maxConcurrency", "Concurrency", formatValue(rule.maxConcurrency, "count"));
528
+ if (rule.maxQueueSize !== undefined)
529
+ add("maxQueueSize", "Queue", formatValue(rule.maxQueueSize, "count"));
530
+ if (rule.queueTimeoutMs !== undefined)
531
+ add("queueTimeoutMs", "Queue wait", formatValue(rule.queueTimeoutMs, "ms"));
532
+ if (rule.retryAfterMs !== undefined)
533
+ add("retryAfterMs", "Retry after", formatValue(rule.retryAfterMs, "ms"));
534
+ if (rule.budget)
535
+ add("budget", "Call budget", `${formatValue(rule.budget.maxCalls, "count")} / ${formatValue(rule.budget.windowMs, "ms")}`);
536
+ if (rule.partitioned)
537
+ add("partitioned", "Per caller", "On");
538
+ });
539
+ const transport = connector.transport;
540
+ if (transport?.versionNegotiation)
541
+ tiles.push({
542
+ id: `${base}.transport.versionNegotiation`,
543
+ label: "Protocol",
544
+ value: transport.versionNegotiation === "auto" ? "Auto" : "Legacy",
545
+ set: set("transport.versionNegotiation", false),
546
+ });
547
+ if (transport?.redirects)
548
+ tiles.push({
549
+ id: `${base}.transport.redirects`,
550
+ label: "Redirects",
551
+ value: transport.redirects === "none" ? "None" : "Same origin",
552
+ set: set("transport.redirects", false),
553
+ });
554
+ if (transport?.requireHttps !== undefined)
555
+ tiles.push({
556
+ id: `${base}.transport.requireHttps`,
557
+ label: "HTTPS only",
558
+ value: formatValue(transport.requireHttps, "toggle"),
559
+ set: set("transport.requireHttps", false),
560
+ });
561
+ const other = leaves(connector, "")
562
+ .filter(([path]) => !known(path, CONNECTOR_KNOWN))
563
+ .map(([path, value]) => otherTile(path, value, base));
564
+ return { badges, auth: authBadges(connector), tiles: [...tiles, ...other] };
565
+ }