@zackbart/connecta 0.16.1 → 0.18.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 (46) hide show
  1. package/CHANGELOG.md +180 -0
  2. package/README.md +4 -0
  3. package/dist/catalog-service.d.ts +10 -0
  4. package/dist/catalog-service.js +77 -5
  5. package/dist/catalog.js +114 -12
  6. package/dist/errors.d.ts +4 -6
  7. package/dist/execute.d.ts +7 -0
  8. package/dist/execute.js +262 -168
  9. package/dist/invocation.js +3 -1
  10. package/dist/meta-tools.d.ts +4 -0
  11. package/dist/meta-tools.js +55 -23
  12. package/dist/operator-ui/generated.d.ts +1 -1
  13. package/dist/operator-ui/generated.js +1 -1
  14. package/dist/operator-ui/model.d.ts +3 -1
  15. package/dist/providers/mixpanel.d.ts +3 -5
  16. package/dist/providers/mixpanel.js +73 -5
  17. package/dist/providers/stripe.d.ts +25 -24
  18. package/dist/providers/stripe.js +64 -35
  19. package/dist/registry.d.ts +32 -9
  20. package/dist/registry.js +217 -33
  21. package/dist/routes/mcp.js +6 -0
  22. package/dist/routes/ui.js +1 -1
  23. package/dist/skills.d.ts +5 -1
  24. package/dist/skills.js +206 -30
  25. package/dist/types.d.ts +14 -2
  26. package/dist/ui.js +4 -1
  27. package/dist/version.d.ts +1 -1
  28. package/dist/version.js +1 -1
  29. package/documentation/architecture.md +8 -5
  30. package/documentation/code-mode.md +68 -68
  31. package/documentation/connector-guides.md +29 -27
  32. package/documentation/connectors.md +13 -1
  33. package/documentation/meta-tools.md +53 -19
  34. package/documentation/mixpanel.md +20 -0
  35. package/documentation/notion.md +17 -0
  36. package/documentation/operations.md +24 -21
  37. package/documentation/operator-ui.md +12 -2
  38. package/documentation/provider-audit.md +15 -7
  39. package/documentation/provider-conventions.md +26 -13
  40. package/documentation/stripe.md +66 -59
  41. package/documentation/upgrading.md +46 -4
  42. package/ethos.md +7 -7
  43. package/examples/worker/README.md +4 -3
  44. package/package.json +2 -2
  45. package/templates/node/README.md +7 -0
  46. package/templates/node/package.json +5 -2
package/dist/skills.js CHANGED
@@ -1,40 +1,77 @@
1
- export const CONNECTA_INSTRUCTIONS = 'Connecta exposes seven meta-tools. For one read at an unknown address, search_tools with 2–4 distinctive action/object terms and includeSchemas="compact", then one call_tool a lone cold call is cheaper direct than a program. For read-only reduction, multiple or dependent calls, loops, joins, or branches, do not call top-level search_tools: make one execute_code call whose program searches, selects, calls, and reduces; never return discovery for another call. connecta.ui(html) is a guest function inside execute_code, never a connector address or search_tools result; pass one HTML string for display-only, or bind named read-only refresh/drill-down calls in its optional reads argument, and return the same initial summary data the HTML renders. Unannotated, write-capable, or destructive tools stay top level: search_tools, then call_destructive_tool; authorize_connector follows auth_required; get_result follows truncation. If this routing is unfamiliar, fetch skills({ name: "usage" }).';
1
+ export const CONNECTA_INSTRUCTIONS = 'Choose a route before discovery. For one read at an unknown address, use search_tools then call_tool; a known address needs only call_tool. For read-only reduction, multiple or dependent calls, loops, joins, or branches, use one execute_code program that discovers, calls, and returns the reduced answer. Only readOnlyHint: true tools run there. Keep unannotated, write-capable, or destructive work top level: search_tools then call_destructive_tool. After auth_required use authorize_connector. After a truncated direct result use fields or get_result. connecta.ui(html) exists only inside execute_code, not in connector search; return the same summary data the HTML renders. Fetch skills({ name: "usage" }) once for program syntax, selection, repair, examples, and runtime details.';
2
2
  const USAGE_SKILL_BASE = `# Connecta usage
3
3
 
4
4
  ## The surface
5
5
 
6
6
  Seven tools: \`execute_code\`, \`search_tools\`, \`call_tool\`, \`call_destructive_tool\`, \`authorize_connector\`, \`get_result\`, \`skills\`. Broad discovery and multi-call work live in a program, not in top-level tools.
7
7
 
8
- ## Choose the smallest execution tool
8
+ The always-loaded MCP instructions are authoritative for choosing the top-level route. Read this skill at most once per task for the program workflow and recovery details below.
9
9
 
10
- Use exact addresses from discovery; never invent one. Search 2–4 distinctive action/object terms, not the whole request.
10
+ ## Inside a program
11
11
 
12
- - One read at an unknown address: \`search_tools({ query, includeSchemas: "compact" })\`, then \`call_tool\` once one cold call is cheaper direct than a program.
13
- - Anything wider — two or more calls, dependent steps, loops, joins, branching, a whole-catalog browse, or a result to reduce: one \`execute_code\` run.
14
- - Any unannotated, write-capable, or destructive call: \`call_destructive_tool\`, one at a time, after reviewing its schema and consequences.
15
- - Truncated result: retry with \`fields\`, else page it with \`get_result\`.
16
- - \`auth_required\`: \`authorize_connector\`, hand its recovery text to the operator, retry the call.
12
+ Write one plain-JavaScript async arrow function. TypeScript syntax and portable imports do not work. Return JSON-shaped data and reduce large results before returning.
17
13
 
18
- ## Inside a program
14
+ The minimum guest API is:
15
+
16
+ - \`<connectorId>.<toolName>(args)\` calls a sanitized shortcut. Non-identifier characters become \`_\`; leading digits gain \`_\`; reserved words gain a trailing \`_\`.
17
+ - \`connecta.call("connector.tool", args)\` uses the canonical address and returns the unwrapped value.
18
+ - \`connecta.search(args)\` returns \`{ tools, total, offset, limit, hasMore }\`; \`connecta.describe(args)\` returns \`{ tools }\`.
19
+ - \`connecta.batch(calls)\` runs 2–10 independent calls. Each outcome is \`{ address, ok: true, data }\` or \`{ address, ok: false, error, errorDetails }\`.
20
+ - \`console.log(...)\` is captured. \`connecta.emit(block)\` and \`connecta.ui(html, options?)\` produce rich output.
21
+
22
+ ## Discover and select
23
+
24
+ Search inside the run and finish the task there. A discovery-only program wastes a round trip. Use 2–4 distinctive action/object terms, not the full request. Use separate short searches for distinct operations.
25
+
26
+ For top-level \`search_tools\`, omit \`limit\` initially (the default is 10), then page with a limit up to 50 if needed. Empty or whitespace-only queries browse all tools. A non-empty query with no ASCII terms returns no matches; mixed input searches with its ASCII terms. \`includeSchemas: "compact"\` adds bounded input and declared output shapes. Plain objects expose \`inputKeys\`, \`requiredInputKeys\`, and \`outputKeys\`; truncation flags mark incomplete shapes; matches also carry declared annotations.
27
+
28
+ - \`connecta.search({})\` loads all catalogs. Pass \`connector: "<id>"\` when the integration is obvious. Use \`safety: "readOnly"\` for program calls. These inputs filter discovery; they grant no authority.
29
+ - Request \`includeSchemas: "compact"\`. Check address, purpose, annotations, required inputs, truncation, safety, and declared outputs. Never select only because a result ranks first or has fewer required inputs.
30
+ - Supply every \`requiredInputKey\` from the task or a prior result. For dependencies, match the earlier \`outputKey\` to the later required key. An empty required-key list does not permit invented arguments. Missing \`outputKeys\` means inspect \`outputSchema\`.
31
+ - Use \`connecta.describe({ address })\` or \`{ addresses }\` when a compact schema is truncated or insufficient. Use \`format: "json"\` only for exact constraints. Write the property names the schema displays; never guess positions or aliases.
32
+ - Reduce through declared output keys. Do not guess collection roots such as \`items\` or \`results\`. If a match or result key is missing, inspect, re-search, or describe inside the same run instead of returning discovery for another call.
33
+
34
+ Only tools explicitly annotated \`readOnlyHint: true\` are reachable. The catalog, credential, admission, and read-only gates run below the sandbox; code cannot widen its authority.
35
+
36
+ ## Errors and repair
37
+
38
+ Caught Connecta errors expose \`message\`, \`code\`, \`retryable\`, and \`details\`. Batch failures expose the same classification in \`errorDetails\`. Branch on fields, never prose. Do not retry \`retryable: false\`, and do not retry \`rate_limited\` immediately because portable code has no timer.
39
+
40
+ - \`destructive_tool_requires_approval\`: stop the program and use the returned canonical address with top-level \`call_destructive_tool\`.
41
+ - \`auth_required\`: let the failure reach the model, then use top-level \`authorize_connector\`, give its handoff to the operator, and retry after recovery.
42
+ - A truncated direct-call result: retry \`call_tool\` with \`fields\`, or follow its \`get_result\` action. A truncated program result has no page handle; filter, map, or slice inside a new program.
43
+ - Unknown addresses and tools carry scoped search recovery. Use it inside the current run. Do not invent an address.
44
+
45
+ For a direct call, \`fields\` selects JSON dot-paths and \`[]\` traverses arrays, for example \`results[].id\`. Projection misses return \`data\` plus \`$connecta\` feedback. \`resultMode: "value"\` unwraps the result. \`timeoutMs\` sets its deadline. \`maxRetries\` is honored only for safely annotated tools. \`diagnostics: true\` adds timing.
46
+
47
+ \`get_result({ id, offset?, maxBytes? })\` returns \`{ text, offset, nextOffset?, totalBytes }\` for a direct-call result. Both sizes are byte counts: \`maxBytes\` must be a whole number at least 1 and defaults to the deployment cap; \`offset\` must be a whole number at least 0 and defaults to 0. An offset inside a multi-byte character moves back to its first byte, and the response reports the served offset. Follow \`nextOffset\` to reassemble pages. An unknown or expired id is an error.
48
+
49
+ Limits: 20 host calls per run, 10 calls per batch, and a 15-second deadline per host call.
50
+
51
+ ## Runtime portability
52
+
53
+ Portable code uses only connector globals, \`connecta\`, and \`console.*\`. QuickJS blocks imports and lacks fetch, process, timers, crypto, and WebSocket. Dynamic Workers must use only \`{ loader }\`; bindings, modules, or globalOutbound grant ambient authority. With loader only, environment maps are empty; node:fs/http/https are absent; outbound fetch, WebSocket, node:net, and node:tls are denied; DNS is unresolved. Runtime builtins remain through \`import()\` and \`process.getBuiltinModule()\`, including node:path and cloudflare:workers; this set can drift. Timers, process, crypto, WebSocket, and data: fetch remain. Avoid every runtime-only capability because QuickJS fails.
54
+
55
+ ## Examples
56
+
57
+ One read-only call at a known address:
58
+
59
+ \`async () => await connecta.call("crm.get_account", { id: "acct_42" })\`
19
60
 
20
- One async arrow function. The only capabilities are one global per connector (\`<connectorId>.<toolName>(args)\`), the \`connecta\` functions, and \`console.log\`.
61
+ Dependent calls, only when the second needs a value from the first:
21
62
 
22
- - \`connecta.search({})\` loads all catalogs; pass \`connector: "<id>"\` when obvious to load one. \`safety: "readOnly"\` keeps executable calls. Neither grants authority. Matches carry \`address\` and annotations.
23
- - Exact schemas: \`connecta.describe({ address: "connector.tool" })\` for one, \`{ addresses: [...] }\` for many; \`format: "json"\` only for exact constraints.
24
- - Two to ten independent calls: \`connecta.batch([...])\`. Each outcome is \`{ address, ok: true, data }\` or \`{ address, ok: false, error, errorDetails: { code, retryable } }\` — how a program tells a policy refusal from a transient failure.
25
- - Search inside the run, not before it; return only the reduction the answer needs, never raw payloads.
26
- - Only tools annotated \`readOnlyHint: true\` are reachable; the gate, credentials, and admission are enforced below the sandbox — nothing a program does widens its reach.
63
+ \`async () => { const { tools } = await connecta.search({ query: "pipeline run job logs", safety: "readOnly", includeSchemas: "compact" }); const address = (suffix) => { const tool = tools.find((entry) => entry.address.endsWith(suffix)); if (!tool) throw new Error("missing " + suffix); return tool.address; }; const run = await connecta.call(address(".get_run"), { runId: 42 }); const logs = await connecta.call(address(".get_job_logs"), { jobId: run.failedJobId }); return logs.map(({ timestamp, message }) => ({ timestamp, message })); }\`
27
64
 
28
65
  ## Rendering a view
29
66
 
30
- \`connecta.ui(html)\` renders a display-only view on success for the client, never for the model. Fetch first, check the shape in code. On a surprise — empty array, missing key return a trimmed first record instead of rendering: the wrong view becomes the sample you needed. Otherwise render from the variables you return; the model reads the return value, not the view.
67
+ \`connecta.emit\` accepts text, image, or audio blocks and delivers them only on success. \`connecta.ui(html)\` renders one success-only display view outside model context. One argument is display-only. Bind read-only refresh or drill-down calls with \`{ reads: { name: { address, fixedArgs?, viewArgs? } } }\`; page markup calls \`connecta.read(name, args)\`. Admission and one shared budget apply to the UI and emitted content, not separate budgets. Fetch and check the data shape first. On empty or missing data, return a trimmed first record instead of rendering. Otherwise render returned variables and return the same initial summary because the model reads the return value, not the view. A second, invalid, or over-budget UI call throws catchably.
31
68
 
32
69
  `;
33
70
  /** Deployment-scoped guide routing appended to the shared usage guide. */
34
71
  const CONNECTOR_GUIDES_SECTION = `
35
72
  ## Per-connector guides
36
73
 
37
- When a connector here ships a deployment-scoped usage guide, \`skills({})\` and discovery return the exact \`guide\` name plus a bounded \`guideSummary\` saying what it covers. Fetch only a listed or carried name with \`skills({ name: <guide> })\`; never infer one from a connector id. \`guideRequired: true\` is a hard stop: fetch before calling. \`guideRequiredReasons\` says why \`connector_required\` and \`approval_required\` stand however you expand the schema; \`schema_truncated\` clears once describe returns the exact one. Otherwise fetch when the summary names a connector-specific sequence, unit, pagination rule, alias, or generic API convention relevant to the task. A read-only call whose compact schema is complete and unambiguous may proceed without fetching an otherwise irrelevant guide. Connector guides do not replace the shared Connecta usage guide and never apply to another deployment implicitly.
74
+ Connector guides appear in \`skills({})\` and discovery with an exact \`guide\` name and bounded \`guideSummary\`. Fetch only a listed or carried name with \`skills({ name: <guide> })\`; never infer one from a connector id. \`guideRequired: true\` is a hard stop. \`guideRequiredReasons\` says why: \`connector_required\` and \`approval_required\` stand after schema expansion; \`schema_truncated\` clears after exact describe. Otherwise fetch for a relevant sequence, unit, pagination rule, alias, or API convention. A read-only call with a complete compact schema may skip an irrelevant guide. Connector guides never apply to another deployment.
38
75
  `;
39
76
  /** Shared Connecta routing guidance, byte-identical across deployments. */
40
77
  export const USAGE_SKILL = USAGE_SKILL_BASE + CONNECTOR_GUIDES_SECTION;
@@ -80,17 +117,63 @@ export function connectorGuide(connector) {
80
117
  const content = typeof guide === "string" ? guide : guide?.content;
81
118
  return content && content.trim() !== "" ? content : undefined;
82
119
  }
83
- const SUMMARY_LENGTH = 120;
120
+ /** Discovery budget for one connector-guide summary, including an ellipsis. */
121
+ export const GUIDE_SUMMARY_LENGTH = 120;
84
122
  /** A `---`/`***`/`___` rule, which also opens and closes YAML frontmatter. */
85
123
  const RULE_RE = /^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
86
124
  /** A fenced code block's delimiter. */
87
125
  const FENCE_RE = /^\s*(?:```|~~~)/;
126
+ /** Markdown blocks that end a paragraph without a blank physical line. */
127
+ const HEADING_RE = /^\s*#{1,6}/;
128
+ const LIST_ITEM_RE = /^\s*(?:[-*+]\s+|\d+[.)]\s+)/;
129
+ const SETEXT_UNDERLINE_RE = /^\s*=+\s*$/;
130
+ const TABLE_DELIMITER_RE = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/;
131
+ const OPENS_CLAUSE_RE = /^(?:The|A|An)\b/u;
132
+ const ARTICLE_RE = /^(?:the|a|an)\b/u;
88
133
  /**
89
134
  * Markup that carries no summary text of its own: horizontal rules, HTML
90
135
  * comments, and table rows. Skipped so a guide that opens with one is
91
136
  * summarized by its first real line instead of by punctuation.
92
137
  */
93
138
  const NOT_SUMMARY_RE = /^\s*(?:<!--|\|)|^\s*(?:-{3,}|\*{3,}|_{3,})\s*$/;
139
+ /** A standard Markdown table starts with a pipe-bearing row and delimiter. */
140
+ function startsTable(lines, index) {
141
+ const header = lines[index] ?? "";
142
+ const delimiter = lines[index + 1] ?? "";
143
+ return header.includes("|") && TABLE_DELIMITER_RE.test(delimiter);
144
+ }
145
+ /** True when a sentence-looking period belongs to an abbreviation. */
146
+ function isAbbreviation(text, end) {
147
+ const token = text.slice(0, end).match(/\S+$/u)?.[0] ?? "";
148
+ // These introduce an example or restatement even before a capitalized word.
149
+ if (/^(?:e\.g|i\.e)\.$/iu.test(token))
150
+ return true;
151
+ // An initial or title belongs to the proper name that follows it.
152
+ if (/^[A-Z]\.$/u.test(token))
153
+ return true;
154
+ if (/^(?:Mr|Mrs|Ms|Dr|Prof|Sr|Jr|St)\.$/iu.test(token))
155
+ return true;
156
+ if (!/^(?:[A-Za-z]\.){2,}$/u.test(token) &&
157
+ !/^(?:vs|etc|approx|dept|fig|no)\.$/iu.test(token)) {
158
+ return false;
159
+ }
160
+ // Initialisms can end a sentence or extend a name ("U.S. East region").
161
+ // The mistakes are asymmetric: a false ending presents a fragment as a
162
+ // complete thought, while a missed ending gets an honest ellipsis. Count
163
+ // the period only with narrow evidence of a new clause: an article in one
164
+ // of its first two words. This is grammar evidence, not a starter-word list.
165
+ const following = text
166
+ .slice(end)
167
+ .match(/^[)\]}'"”’]*\s+(\S+)(?:\s+(\S+))?/u);
168
+ if (!following)
169
+ return false;
170
+ const [, nextWord, afterNext] = following;
171
+ const startsClause = nextWord !== undefined &&
172
+ /^\p{Lu}/u.test(nextWord) &&
173
+ (OPENS_CLAUSE_RE.test(nextWord) ||
174
+ (afterNext !== undefined && ARTICLE_RE.test(afterNext)));
175
+ return !startsClause;
176
+ }
94
177
  /** Drop a leading YAML frontmatter block — metadata, not summary text. */
95
178
  function withoutFrontmatter(lines) {
96
179
  let start = 0;
@@ -102,33 +185,107 @@ function withoutFrontmatter(lines) {
102
185
  const close = lines.findIndex((line, i) => i > start && RULE_RE.test(line));
103
186
  return close === -1 ? lines : lines.slice(close + 1);
104
187
  }
188
+ /** Normalize authored and derived summaries under one construction contract. */
189
+ export function normalizeGuideSummary(summary) {
190
+ const normalized = summary.replace(/\s+/g, " ").trim();
191
+ return normalized === "" ? undefined : normalized;
192
+ }
105
193
  /**
106
- * One line describing a guide, for the cheap list view: the guide's first
107
- * meaningful line (heading marks and list bullets stripped), falling back to
108
- * the connector's own description when the guide opens with nothing but
109
- * markup.
194
+ * Shorten a normalized summary at the strongest readable boundary available.
195
+ * A complete sentence needs no ellipsis; clause and word cuts do, so discovery
196
+ * never presents an unfinished fragment as the guide's complete thought.
110
197
  */
111
198
  function boundedSummary(summary) {
112
- const line = summary.replace(/\s+/g, " ").trim();
113
- if (line === "")
199
+ const normalized = normalizeGuideSummary(summary);
200
+ if (!normalized)
114
201
  return undefined;
115
- return line.length <= SUMMARY_LENGTH
116
- ? line
117
- : `${line.slice(0, SUMMARY_LENGTH - 1).trimEnd()}…`;
202
+ if (normalized.length <= GUIDE_SUMMARY_LENGTH)
203
+ return normalized;
204
+ const contentBudget = GUIDE_SUMMARY_LENGTH - 1;
205
+ let sentenceEnd = 0;
206
+ const sentenceBoundary = /[.!?…。!?](?:[)\]}'"”’]+)?(?=\s|$)/gu;
207
+ for (const match of normalized.matchAll(sentenceBoundary)) {
208
+ const end = (match.index ?? 0) + match[0].length;
209
+ if (end > GUIDE_SUMMARY_LENGTH)
210
+ break;
211
+ const punctuationEnd = (match.index ?? 0) + 1;
212
+ if (match[0].startsWith(".") &&
213
+ isAbbreviation(normalized, punctuationEnd)) {
214
+ continue;
215
+ }
216
+ // Do not mistake another short fragment for a useful complete thought.
217
+ if (end >= 24)
218
+ sentenceEnd = end;
219
+ }
220
+ if (sentenceEnd > 0)
221
+ return normalized.slice(0, sentenceEnd);
222
+ const available = normalized.slice(0, contentBudget);
223
+ let clauseEnd = 0;
224
+ const clauseBoundary = /[,;:](?=\s)|\s[—–-](?=\s)/g;
225
+ for (const match of available.matchAll(clauseBoundary)) {
226
+ const end = match.index ?? 0;
227
+ // Prefer a clause only when it retains most of the discovery budget.
228
+ if (end >= 80)
229
+ clauseEnd = end;
230
+ }
231
+ if (clauseEnd > 0) {
232
+ return `${available.slice(0, clauseEnd).trimEnd()}…`;
233
+ }
234
+ const wordEnd = available.search(/\s+\S*$/);
235
+ if (wordEnd > 0) {
236
+ const prefix = available
237
+ .slice(0, wordEnd)
238
+ .trimEnd()
239
+ .replace(/[,;:([{—–-]+$/u, "")
240
+ .trimEnd();
241
+ if (prefix !== "")
242
+ return `${prefix}…`;
243
+ }
244
+ let hardEnd = contentBudget;
245
+ const code = normalized.charCodeAt(hardEnd - 1);
246
+ if (code >= 0xd800 && code <= 0xdbff)
247
+ hardEnd--;
248
+ return `${normalized.slice(0, hardEnd)}…`;
118
249
  }
250
+ /**
251
+ * One thought describing a guide for the cheap list view: the first meaningful
252
+ * paragraph, joined across physical lines, with headings and the connector
253
+ * description as fallbacks when the guide opens with markup alone.
254
+ */
119
255
  function summarizeGuide(connector, guide) {
256
+ const lines = withoutFrontmatter(guide.split("\n"));
120
257
  let inFence = false;
258
+ let inComment = false;
121
259
  let headingFallback;
122
- for (const raw of withoutFrontmatter(guide.split("\n"))) {
260
+ for (let index = 0; index < lines.length; index++) {
261
+ const raw = lines[index] ?? "";
262
+ if (inComment) {
263
+ if (raw.includes("-->"))
264
+ inComment = false;
265
+ continue;
266
+ }
123
267
  if (FENCE_RE.test(raw)) {
124
268
  inFence = !inFence;
125
269
  continue;
126
270
  }
127
271
  if (inFence)
128
272
  continue;
273
+ if (raw.trimStart().startsWith("<!--")) {
274
+ if (!raw.includes("-->"))
275
+ inComment = true;
276
+ continue;
277
+ }
278
+ if (startsTable(lines, index)) {
279
+ index++;
280
+ while (index + 1 < lines.length &&
281
+ (lines[index + 1] ?? "").includes("|")) {
282
+ index++;
283
+ }
284
+ continue;
285
+ }
129
286
  if (raw.trim() === "" || NOT_SUMMARY_RE.test(raw))
130
287
  continue;
131
- const heading = /^\s*#{1,6}/.test(raw);
288
+ const heading = HEADING_RE.test(raw);
132
289
  const line = raw
133
290
  // `\s*` (not `\s+`) so a bare `#` strips to nothing and is skipped, and
134
291
  // an unspaced `#Heading` is still read as a heading.
@@ -142,7 +299,24 @@ function summarizeGuide(connector, guide) {
142
299
  headingFallback ??= boundedSummary(line);
143
300
  continue;
144
301
  }
145
- return boundedSummary(line) ?? line;
302
+ const paragraph = [line];
303
+ while (index + 1 < lines.length) {
304
+ const next = lines[index + 1] ?? "";
305
+ if (next.trim() === "" ||
306
+ startsTable(lines, index + 1) ||
307
+ FENCE_RE.test(next) ||
308
+ HEADING_RE.test(next) ||
309
+ LIST_ITEM_RE.test(next) ||
310
+ SETEXT_UNDERLINE_RE.test(next) ||
311
+ NOT_SUMMARY_RE.test(next)) {
312
+ break;
313
+ }
314
+ paragraph.push(next.trim());
315
+ index++;
316
+ if (paragraph.join(" ").length > GUIDE_SUMMARY_LENGTH)
317
+ break;
318
+ }
319
+ return boundedSummary(paragraph.join(" ")) ?? line;
146
320
  }
147
321
  if (headingFallback)
148
322
  return headingFallback;
@@ -155,6 +329,8 @@ export function connectorGuideSummary(connector) {
155
329
  if (!guide)
156
330
  return undefined;
157
331
  const configured = typeof connector.usageGuide === "object"
332
+ // Registry construction rejects over-budget configured summaries. Keep
333
+ // normalization here so direct Connector callers see the same text.
158
334
  ? boundedSummary(connector.usageGuide.summary ?? "")
159
335
  : undefined;
160
336
  return configured ?? summarizeGuide(connector, guide);
package/dist/types.d.ts CHANGED
@@ -180,6 +180,12 @@ export interface CatalogDriftReport extends CatalogDriftCounts {
180
180
  /** When the observation was taken; never when a probe was scheduled. */
181
181
  observedAt: string;
182
182
  }
183
+ /** The last agent-facing catalog read this runtime served for one connector. */
184
+ export interface CatalogAccessObservation {
185
+ /** Fresh means the read used a live or unexpired catalog; stale means SWR. */
186
+ state: "fresh" | "stale";
187
+ observedAt: string;
188
+ }
183
189
  export interface ConnectorStatus {
184
190
  state: ConnectorStatusState;
185
191
  /** When state === "auth_required", the URL the operator should open. */
@@ -193,6 +199,11 @@ export interface ConnectorStatus {
193
199
  * isolate or process has seen nothing, not that nothing drifted.
194
200
  */
195
201
  catalogDrift?: CatalogDriftReport;
202
+ /**
203
+ * The last agent-facing catalog read in this runtime. This payload-free
204
+ * observation is not persisted and operator reads do not replace it.
205
+ */
206
+ catalogAccess?: CatalogAccessObservation;
196
207
  }
197
208
  /** The whole plugin contract — the one open seam. */
198
209
  export interface Connector {
@@ -313,8 +324,9 @@ export interface ConnectorUsageGuide {
313
324
  /** Markdown returned verbatim by `skills({ name: "connector:<id>" })`. */
314
325
  content: string;
315
326
  /**
316
- * Bounded discovery hint describing the conventions the guide covers. When
317
- * omitted, Connecta derives a summary from the guide's first meaningful line.
327
+ * Discovery hint describing the conventions the guide covers. Whitespace is
328
+ * normalized, and a value over 120 characters throws at construction. When
329
+ * omitted, Connecta derives a bounded summary from the first body paragraph.
318
330
  */
319
331
  summary?: string;
320
332
  /**
package/dist/ui.js CHANGED
@@ -374,6 +374,9 @@ export async function buildUiData(registry, baseUrl, serverInfo, credentialVault
374
374
  // `boundedCatalogDrift`, so what lands here cannot carry a name or a
375
375
  // schema even if the plugin seam returned one.
376
376
  ...(status.catalogDrift ? { catalogDrift: status.catalogDrift } : {}),
377
+ ...(status.catalogAccess
378
+ ? { catalogAccess: status.catalogAccess }
379
+ : {}),
377
380
  ...(c.disconnectAuth && c.startAuth ? { oauth: true } : {}),
378
381
  ...(credential ? { credential } : {}),
379
382
  };
@@ -461,7 +464,7 @@ export function renderUiHtml(uiAuth, mcpUrl = "/mcp", branding, nonce, page = "c
461
464
  : `<span class="product">${escapeHtmlAttr(brand.productName)}</span>`
462
465
  : "";
463
466
  const clerkScript = clerk && clerkScriptOrigin
464
- ? `<script${nonceAttr} defer crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(clerk.publishableKey)}" src="${escapeHtmlAttr(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
467
+ ? `<script${nonceAttr} crossorigin="anonymous" data-clerk-publishable-key="${escapeHtmlAttr(clerk.publishableKey)}" src="${escapeHtmlAttr(clerkScriptOrigin)}/npm/@clerk/clerk-js@6/dist/clerk.browser.js"></script>`
465
468
  : "";
466
469
  return `<!doctype html>
467
470
  <html lang="en">
package/dist/version.d.ts CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export declare const CONNECTA_VERSION = "0.16.1";
7
+ export declare const CONNECTA_VERSION = "0.18.0";
package/dist/version.js CHANGED
@@ -4,4 +4,4 @@
4
4
  * a bump that forgets this file fails the build rather than shipping a stale
5
5
  * version to `/health` and to downstream MCP handshakes.
6
6
  */
7
- export const CONNECTA_VERSION = "0.16.1";
7
+ export const CONNECTA_VERSION = "0.18.0";
@@ -26,7 +26,10 @@ ethos invariant, not a style preference: a client retained across requests on
26
26
  Workers is a cross-request capability leak, and a promise awaited after the
27
27
  response is work the runtime may have already torn down. Deferred work has one
28
28
  sanctioned channel — `ctx.waitUntil`, threaded through `fetch(request, env,
29
- ctx)` and used for best-effort activity writes.
29
+ ctx)`. Best-effort activity writes use it. An agent read that already demanded
30
+ an expired catalog refresh may also use it while serving a complete catalog
31
+ inside its stale window. That refresh owns a new scope and deadline; it never
32
+ carries the inbound scope or signal past the request.
30
33
 
31
34
  The registry is deliberately on the long side of that line and the MCP server
32
35
  deliberately on the short side. A fresh `McpServer` per request is what makes
@@ -87,8 +90,8 @@ owns or hands out, and a change usually belongs in exactly one of them:
87
90
 
88
91
  | Module | Owns |
89
92
  | --- | --- |
90
- | `src/registry.ts` | The connector set, id validation, address resolution, connector health, per-connector call limiters, and the drift snapshot. Construction-time refusal of structural mistakes lives here. |
91
- | `src/catalog-service.ts` | Tool listing: cold-load coalescing, TTL, persistence as manifest plus revision-addressed chunks, stale fallback, and the completeness rule a partial catalog is a failure, never a cache write. |
93
+ | `src/registry.ts` | The connector set, address resolution, catalog TTL/persistence/completeness, shared refresh single-flight, connector health, per-connector call limiters, and drift. Construction-time refusals live here. |
94
+ | `src/catalog-service.ts` | Request-local tool listing, search, and describe. It coalesces reads inside one request and opts agent reads into the runtime's deferred catalog channel when one exists. |
92
95
  | `src/invocation.ts` | One tool call: argument validation, call admission, per-attempt timeout, retry with the connector's own `Retry-After` honoured exactly or declined, result unwrapping, size capping, and the activity record. |
93
96
  | `src/catalog.ts` | Ranking, description summarizing, and the compact schema renderer discovery shows. |
94
97
 
@@ -137,7 +140,7 @@ src/
137
140
  apps-shell.ts the one build-time MCP Apps template
138
141
  skills.ts MCP instructions, the usage skill, connector guides
139
142
  registry.ts connector set, addresses, health, call limiters
140
- catalog-service.ts catalog loading, caching, persistence, stale fallback
143
+ catalog-service.ts request-local catalog access, search, and describe
141
144
  catalog.ts ranking, summaries, compact schema rendering
142
145
  invocation.ts one tool call, end to end
143
146
  catalog-drift.ts vetted manifests and the counts a refresh produces
@@ -182,7 +185,7 @@ src/
182
185
  | The core imports no `node:` builtin and reaches no Node-only module | `test/purity.test.ts` |
183
186
  | The published surface matches the same boundary | `test/package-surface.test.ts`, `scripts/check-package.mjs` |
184
187
  | Route order, per-route auth, and byte-exact refusals | `test/server-route-contracts.test.ts` |
185
- | `/mcp` end to end, the open routes, exactly seven tools | `test/server.test.ts`, `test/code-first-surface.test.ts` |
188
+ | `/mcp` end to end, the open routes, exactly seven tools, bounded connector orientation | `test/server.test.ts`, `test/code-first-surface.test.ts` |
186
189
  | Construction-time refusals and the grouped config boundary | `test/config.test.ts`, `test/registry.test.ts` |
187
190
  | Program and top-level calls take the same enforced path | `test/execute.test.ts` |
188
191
  | Both deployment shapes still compile and configure the real thing | `test/deployment-shapes.test.ts`, `npm run check:examples` |