@zackbart/connecta 0.18.3 → 0.19.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 (69) hide show
  1. package/CHANGELOG.md +73 -4
  2. package/dist/catalog-service.d.ts +16 -13
  3. package/dist/catalog-service.js +106 -115
  4. package/dist/catalog.js +29 -46
  5. package/dist/connector-scope.js +2 -7
  6. package/dist/connectors/api.d.ts +4 -16
  7. package/dist/connectors/api.js +19 -46
  8. package/dist/connectors/guarded-fetch.d.ts +9 -23
  9. package/dist/connectors/guarded-fetch.js +38 -76
  10. package/dist/connectors/remote-mcp.js +36 -79
  11. package/dist/errors.d.ts +6 -27
  12. package/dist/errors.js +8 -5
  13. package/dist/execute.d.ts +24 -22
  14. package/dist/execute.js +98 -145
  15. package/dist/executor-result.d.ts +1 -0
  16. package/dist/executor-result.js +4 -11
  17. package/dist/executors/quickjs-child.js +1 -3
  18. package/dist/executors/quickjs-runtime.js +1 -3
  19. package/dist/executors/quickjs.js +1 -3
  20. package/dist/index.js +27 -57
  21. package/dist/invocation.js +113 -183
  22. package/dist/meta-tools.d.ts +15 -28
  23. package/dist/meta-tools.js +33 -89
  24. package/dist/providers/cloudflare.d.ts +2 -18
  25. package/dist/providers/cloudflare.js +1460 -2451
  26. package/dist/providers/linear.d.ts +4 -41
  27. package/dist/providers/linear.js +8 -39
  28. package/dist/providers/mixpanel.d.ts +3 -25
  29. package/dist/providers/mixpanel.js +7 -22
  30. package/dist/providers/notion.d.ts +1 -15
  31. package/dist/providers/notion.js +44 -173
  32. package/dist/providers/revenuecat.d.ts +4 -57
  33. package/dist/providers/revenuecat.js +10 -93
  34. package/dist/providers/stripe.d.ts +1 -12
  35. package/dist/providers/stripe.js +7 -45
  36. package/dist/registry.d.ts +9 -34
  37. package/dist/registry.js +9 -103
  38. package/dist/routes/mcp.js +1 -1
  39. package/dist/routes/oauth.js +3 -3
  40. package/dist/routes/shared.d.ts +15 -15
  41. package/dist/routes/shared.js +1 -3
  42. package/dist/timeout.d.ts +8 -7
  43. package/dist/timeout.js +47 -38
  44. package/dist/types.d.ts +3 -3
  45. package/dist/ui.d.ts +1 -25
  46. package/dist/ui.js +18 -45
  47. package/dist/version.d.ts +1 -1
  48. package/dist/version.js +1 -1
  49. package/documentation/call-admission.md +1 -1
  50. package/documentation/cloudflare.md +1 -1
  51. package/documentation/code-mode.md +7 -7
  52. package/documentation/connectors.md +24 -1
  53. package/documentation/linear.md +1 -1
  54. package/documentation/mixpanel.md +1 -1
  55. package/documentation/notion.md +1 -1
  56. package/documentation/operations.md +18 -14
  57. package/documentation/provider-conventions.md +1 -1
  58. package/documentation/revenuecat.md +1 -1
  59. package/documentation/stripe.md +1 -1
  60. package/documentation/upgrading.md +13 -4
  61. package/ethos.md +75 -121
  62. package/package.json +3 -4
  63. package/templates/node/package.json +1 -1
  64. package/documentation/code-first-exploration.md +0 -292
  65. package/documentation/mcp-2026-07-28.md +0 -46
  66. package/documentation/mcp-ui-design.md +0 -382
  67. package/documentation/program-ui-read-calls.md +0 -213
  68. package/documentation/provider-audit.md +0 -198
  69. package/documentation/rich-output-design.md +0 -211
package/CHANGELOG.md CHANGED
@@ -2,6 +2,68 @@
2
2
 
3
3
  All notable changes to this package are documented here.
4
4
 
5
+ ## 0.19.0 — 2026-08-25
6
+
7
+ This release is a smaller, simpler package with no behavioral change for a
8
+ deployment. A deployment can ignore everything here. The one packaging change
9
+ is to maintainer history: six finished design records now live in the repository
10
+ at their GitHub URLs instead of shipping in the npm package, cutting 91 KB from
11
+ the package. The constitution is a fifth of its former size and keeps the same
12
+ decisions. The core shrank from 28,756 to 27,417 lines and the tests from 42,341
13
+ to 40,375 before the final pure-suite splits, which put 198 lines back — a
14
+ copied preamble the next pass moves into a fixture (#479). Two dead paths
15
+ finally left with the rest: write-only health accessors on `RegistryView`,
16
+ residue from #179, and the v1 persisted-catalog reader, unreachable since
17
+ 2026-07-28.
18
+
19
+ ### Changed
20
+
21
+ - **Providers.** Every Cloudflare tool is built through `cfTool()` and
22
+ `compact()`, so the repeated schema headers, scope properties, and
23
+ conditional-spread projections are written once; the paged-list, delete-ack,
24
+ and passthrough handlers are shared while every tool keeps its own literal
25
+ name, description, annotations, and schemas; the authentication `Symbol` is
26
+ gone because the scheme is fixed per connector (#465, #467). Cloudflare and
27
+ Notion share one Retry-After parser and one guarded JSON accessor, and
28
+ `callCloudflareContent` still classifies an unreadable error body by status
29
+ (#468). The hosted providers' comment blocks point at their guides instead of
30
+ restating them, and `api()` omits undefined options once (#463).
31
+ - **Core.** Internal option types accept `undefined`, so `createConnecta` and
32
+ the registration helpers forward configuration directly (#462). One deadline,
33
+ sleep, and message helper serve the whole call path; `invocation.ts` lost its
34
+ hand-rolled controller, timer, and listener choreography with Retry-After,
35
+ per-phase timing, and health exclusion unchanged (#466). The catalog,
36
+ execution, remote-MCP, and operator-data paths share their failure envelopes,
37
+ close helpers, and result shapers (#472).
38
+ - **Package and docs.** Six finished design records moved to
39
+ [`records/`](https://github.com/zackbart/connecta/tree/main/records), outside
40
+ the published files, and packed-link validation folded into the documentation
41
+ checker: −1,342 shipped lines, −91 KB (#464). `ethos.md` is the constitution
42
+ again — refusals, invariants, and what this is, at 1,188 words instead of
43
+ 3,174, with every decision intact and a word cap replacing the line cap
44
+ (#471).
45
+ - **Tests.** Shared fixtures replace 122 hand-rolled connector literals, five
46
+ fake downstream MCP servers, four fake Clerk auths, and the per-suite JSON-RPC
47
+ readers, deferreds, and logger spies (#473, #474, #475); the six
48
+ provider-registry suites are one `describe.each` and the provider error
49
+ mappings are tables (#476); repeated cases are `it.each` tables and the
50
+ duplicated assertion layers are gone (#477); the two largest suites are split
51
+ by subject (#460). Every `it()` and `expect()` that pins behavior survived —
52
+ 3,400 fewer lines, no assertion dropped.
53
+ - **Declined with numbers**, so nobody re-runs the experiment: a shared bounded
54
+ queue under both admission controllers measured −17 lines for a
55
+ hook-parameterised abstraction (#453); Effect as the core effect system
56
+ measured −4% of the core for +75 KB gzip and a second async paradigm (#470).
57
+
58
+ ### Removed
59
+
60
+ - The write-only `HealthLog` and its `RegistryView` accessors: `healthFor`,
61
+ `hasObservedSuccess`, `observedSuccessAt`, and `peekTools`. `refreshTools` is
62
+ now private. This was residue from the removed proactive credential-liveness
63
+ design (#179).
64
+ - The v1 persisted-catalog reader, unreachable since 2026-07-28. Its fixtures
65
+ now use the v2 manifest-and-chunks format.
66
+
5
67
  ## 0.18.3 — 2026-08-25
6
68
 
7
69
  This change gives code mode memory where downstream MCP catalogs are usually
@@ -29,6 +91,13 @@ one or several real results from masquerading as a provider contract.
29
91
  live deployment audits: BePresent had 246/378 tools without output schemas,
30
92
  while OneMany's maintained connectors declared all 90 (#442).
31
93
 
94
+ ### Changed
95
+
96
+ - Removed the write-only per-connector call health log. Payload-free call
97
+ outcomes remain available through the activity sink.
98
+ - Persisted catalog reads now require the version 2 manifest-and-chunks shape;
99
+ version 1 single-value catalogs are ignored and refreshed.
100
+
32
101
  ## 0.18.2 — 2026-08-18
33
102
 
34
103
  This patch closes the gap the RevenueCat rollout exposed on the same day 0.18.1
@@ -752,7 +821,7 @@ it back byte-identical after each operator mutation.
752
821
  constructor, schemas, validation path, handlers, and catalog service run, and
753
822
  only `fetch` is a probe that records the request (#350).
754
823
  - **Five provider audit reports** in
755
- [`documentation/provider-audit.md`](./documentation/provider-audit.md), with
824
+ [`records/provider-audit.md`](https://github.com/zackbart/connecta/blob/main/records/provider-audit.md), with
756
825
  a verdict per convention, the fix for every miss, and every accepted
757
826
  exception recorded with its argument (#342).
758
827
  - **A convention test over the shipped surface.**
@@ -1535,7 +1604,7 @@ The channel is additive — a program that never calls `connecta.ui` produces th
1535
1604
  byte-for-byte prior response, no executor changed to carry it, and there is no
1536
1605
  new budget knob. Deployments do gain a `resources` capability and one extension
1537
1606
  declaration, both of which the design requires before any host will render.
1538
- The design record is `documentation/mcp-ui-design.md` (#266); the contract is
1607
+ The design record is [`records/mcp-ui-design.md`](https://github.com/zackbart/connecta/blob/main/records/mcp-ui-design.md) (#266); the contract is
1539
1608
  `code-mode.md`'s "Rendered output" clauses (#277).
1540
1609
 
1541
1610
  ### Added
@@ -1611,7 +1680,7 @@ that cannot be projected, like a screenshot a downstream tool returned. The
1611
1680
  channel is additive: a program that never emits produces the byte-for-byte
1612
1681
  prior response, no executor changed to carry it, and deployments that do
1613
1682
  nothing get sensible budgets. The design record is
1614
- `documentation/rich-output-design.md` (#267); the contract is `code-mode.md`'s
1683
+ [`records/rich-output-design.md`](https://github.com/zackbart/connecta/blob/main/records/rich-output-design.md) (#267); the contract is `code-mode.md`'s
1615
1684
  "Emitted output" clauses (#270).
1616
1685
 
1617
1686
  Agent recovery is now executable data instead of prose at the remaining local
@@ -2024,7 +2093,7 @@ tests confirm SDK v2 client close does not make it redundant.
2024
2093
  advances the existing generation fence before the SDK starts a fresh grant;
2025
2094
  pre-upgrade credentials bind in place on their first validated read.
2026
2095
  - **A complete in-repo disposition of the revision** in
2027
- [`documentation/mcp-2026-07-28.md`](./documentation/mcp-2026-07-28.md),
2096
+ [`records/mcp-2026-07-28.md`](https://github.com/zackbart/connecta/blob/main/records/mcp-2026-07-28.md),
2028
2097
  including the declined and gated surfaces.
2029
2098
 
2030
2099
  ### Changed
@@ -151,13 +151,13 @@ export declare class CatalogService {
151
151
  private readonly loaded;
152
152
  private readonly loading;
153
153
  constructor(registry: RegistryView, baseUrl: string, options?: {
154
- requestScope?: object;
155
- probeTimeoutMs?: number;
156
- concurrency?: number;
154
+ requestScope?: object | undefined;
155
+ probeTimeoutMs?: number | undefined;
156
+ concurrency?: number | undefined;
157
157
  /** The discovery route recovery records name. Default `search_tools`. */
158
- searchRoute?: SearchRoute;
158
+ searchRoute?: SearchRoute | undefined;
159
159
  /** Runtime-owned tail for stale-while-revalidate catalog reads. */
160
- defer?: DeferredWork;
160
+ defer?: DeferredWork | undefined;
161
161
  });
162
162
  /**
163
163
  * Send a caller back to discovery through the surface it can actually reach.
@@ -177,6 +177,9 @@ export declare class CatalogService {
177
177
  loadConnector(id: string, callOptions?: ConnectorOperationOptions): Promise<ToolDef[]>;
178
178
  private loadForDiscovery;
179
179
  private outputSchema;
180
+ private unknownAddressFailure;
181
+ private catalogLoadFailure;
182
+ private unknownToolFailure;
180
183
  resolveTool(address: string, callOptions?: ConnectorOperationOptions): Promise<CatalogResolution>;
181
184
  /**
182
185
  * Resolve the JavaScript-safe property used by a lazy code-mode namespace
@@ -206,6 +209,10 @@ export declare function groupedSearchResult(page: CatalogSearchPage): {
206
209
  };
207
210
  matchMode?: "partial";
208
211
  nextOffset?: number;
212
+ total: number;
213
+ offset: number;
214
+ limit: number;
215
+ hasMore: boolean;
209
216
  connectors: {
210
217
  id: string;
211
218
  title?: string;
@@ -213,10 +220,6 @@ export declare function groupedSearchResult(page: CatalogSearchPage): {
213
220
  guideSummary?: string;
214
221
  tools: CatalogSearchEntry["tool"][];
215
222
  }[];
216
- total: number;
217
- offset: number;
218
- limit: number;
219
- hasMore: boolean;
220
223
  };
221
224
  export declare function flatSearchResult(page: CatalogSearchPage): {
222
225
  queryAnalysis?: {
@@ -237,6 +240,10 @@ export declare function flatSearchResult(page: CatalogSearchPage): {
237
240
  };
238
241
  matchMode?: "partial";
239
242
  nextOffset?: number;
243
+ total: number;
244
+ offset: number;
245
+ limit: number;
246
+ hasMore: boolean;
240
247
  tools: {
241
248
  guideSummary?: string;
242
249
  guide?: string;
@@ -255,9 +262,5 @@ export declare function flatSearchResult(page: CatalogSearchPage): {
255
262
  guideRequired?: true;
256
263
  guideRequiredReasons?: GuideRequiredReason[];
257
264
  }[];
258
- total: number;
259
- offset: number;
260
- limit: number;
261
- hasMore: boolean;
262
265
  };
263
266
  export {};
@@ -2,7 +2,7 @@ import { compactDiscoverySchema, compactSchema, lexicalCorpusStatistics, lexical
2
2
  import { mapSettledWithConcurrency, resolveDiscoveryConcurrency, } from "./concurrency.js";
3
3
  import { boundedEchoText, classifyCallError, framingError, } from "./errors.js";
4
4
  import { connectorGuide, connectorGuideRequired, connectorGuideSummary, connectorSkillName, } from "./skills.js";
5
- import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, withAbortableTimeout, } from "./timeout.js";
5
+ import { DEFAULT_PROBE_TIMEOUT_MS, normalizeTimeoutMs, withDeadline, } from "./timeout.js";
6
6
  import { isExplicitlyReadOnly } from "./tool-safety.js";
7
7
  export const DEFAULT_SEARCH_LIMIT = 8;
8
8
  export const MAX_SEARCH_LIMIT = 100;
@@ -261,10 +261,13 @@ export class CatalogService {
261
261
  return loading;
262
262
  }
263
263
  loadForDiscovery(id, label) {
264
- return withAbortableTimeout((signal) => this.loadConnector(id, {
264
+ return withDeadline((signal) => this.loadConnector(id, {
265
265
  signal,
266
266
  timeoutMs: this.probeTimeoutMs,
267
- }), this.probeTimeoutMs, label);
267
+ }), {
268
+ timeoutMs: this.probeTimeoutMs,
269
+ timeoutError: new Error(`${label} timed out after ${this.probeTimeoutMs}ms`),
270
+ });
268
271
  }
269
272
  outputSchema(connectorId, tool) {
270
273
  if (tool.outputSchema)
@@ -274,17 +277,42 @@ export class CatalogService {
274
277
  ? { schema: observed, source: "observed" }
275
278
  : {};
276
279
  }
280
+ unknownAddressFailure(address, query) {
281
+ return {
282
+ ok: false,
283
+ error: {
284
+ ...framingError("unknown_address", `Unknown address "${boundedEchoText(address)}"`),
285
+ nextAction: this.searchRecovery({ query: recoveryQuery(query) }, "Find the configured canonical address before retrying."),
286
+ },
287
+ catalogMs: 0,
288
+ };
289
+ }
290
+ catalogLoadFailure(cause, started, connector, toolName) {
291
+ return {
292
+ ok: false,
293
+ error: classifyCallError(cause, "catalog_lookup_failed"),
294
+ catalogMs: Date.now() - started,
295
+ connector,
296
+ toolName,
297
+ cause,
298
+ };
299
+ }
300
+ unknownToolFailure(toolName, connector, started) {
301
+ return {
302
+ ok: false,
303
+ error: {
304
+ ...framingError("unknown_tool", `Unknown tool "${boundedEchoText(toolName)}" on connector "${connector.id}"`),
305
+ nextAction: this.searchRecovery({ query: recoveryQuery(toolName), connector: connector.id }, "Find the connector's current canonical tool address."),
306
+ },
307
+ catalogMs: Date.now() - started,
308
+ connector,
309
+ toolName,
310
+ };
311
+ }
277
312
  async resolveTool(address, callOptions = {}) {
278
313
  const resolved = this.registry.resolveAddress(address);
279
314
  if (!resolved) {
280
- return {
281
- ok: false,
282
- error: {
283
- ...framingError("unknown_address", `Unknown address "${boundedEchoText(address)}"`),
284
- nextAction: this.searchRecovery({ query: recoveryQuery(address) }, "Find the configured canonical address before retrying."),
285
- },
286
- catalogMs: 0,
287
- };
315
+ return this.unknownAddressFailure(address, address);
288
316
  }
289
317
  const started = Date.now();
290
318
  let tools;
@@ -292,30 +320,11 @@ export class CatalogService {
292
320
  tools = await this.loadConnector(resolved.connector.id, callOptions);
293
321
  }
294
322
  catch (cause) {
295
- return {
296
- ok: false,
297
- error: classifyCallError(cause, "catalog_lookup_failed"),
298
- catalogMs: Date.now() - started,
299
- connector: resolved.connector,
300
- toolName: resolved.toolName,
301
- cause,
302
- };
323
+ return this.catalogLoadFailure(cause, started, resolved.connector, resolved.toolName);
303
324
  }
304
325
  const definition = tools.find((tool) => tool.name === resolved.toolName);
305
326
  if (!definition) {
306
- return {
307
- ok: false,
308
- error: {
309
- ...framingError("unknown_tool", `Unknown tool "${boundedEchoText(resolved.toolName)}" on connector "${resolved.connector.id}"`),
310
- nextAction: this.searchRecovery({
311
- query: recoveryQuery(resolved.toolName),
312
- connector: resolved.connector.id,
313
- }, "Find the connector's current canonical tool address."),
314
- },
315
- catalogMs: Date.now() - started,
316
- connector: resolved.connector,
317
- toolName: resolved.toolName,
318
- };
327
+ return this.unknownToolFailure(resolved.toolName, resolved.connector, started);
319
328
  }
320
329
  return {
321
330
  ok: true,
@@ -335,14 +344,7 @@ export class CatalogService {
335
344
  async resolveToolAlias(connectorId, alias, aliasFor, callOptions = {}) {
336
345
  const connector = this.registry.getConnector(connectorId);
337
346
  if (!connector) {
338
- return {
339
- ok: false,
340
- error: {
341
- ...framingError("unknown_address", `Unknown address "${boundedEchoText(`${connectorId}.${alias}`)}"`),
342
- nextAction: this.searchRecovery({ query: recoveryQuery(alias) }, "Find the configured canonical address before retrying."),
343
- },
344
- catalogMs: 0,
345
- };
347
+ return this.unknownAddressFailure(`${connectorId}.${alias}`, alias);
346
348
  }
347
349
  const started = Date.now();
348
350
  let tools;
@@ -350,27 +352,11 @@ export class CatalogService {
350
352
  tools = await this.loadConnector(connector.id, callOptions);
351
353
  }
352
354
  catch (cause) {
353
- return {
354
- ok: false,
355
- error: classifyCallError(cause, "catalog_lookup_failed"),
356
- catalogMs: Date.now() - started,
357
- connector,
358
- toolName: alias,
359
- cause,
360
- };
355
+ return this.catalogLoadFailure(cause, started, connector, alias);
361
356
  }
362
357
  const [definition, ...collisions] = tools.filter((tool) => aliasFor(tool.name) === alias);
363
358
  if (!definition) {
364
- return {
365
- ok: false,
366
- error: {
367
- ...framingError("unknown_tool", `Unknown tool "${boundedEchoText(alias)}" on connector "${connector.id}"`),
368
- nextAction: this.searchRecovery({ query: recoveryQuery(alias), connector: connector.id }, "Find the connector's current canonical tool address."),
369
- },
370
- catalogMs: Date.now() - started,
371
- connector,
372
- toolName: alias,
373
- };
359
+ return this.unknownToolFailure(alias, connector, started);
374
360
  }
375
361
  if (collisions.length > 0) {
376
362
  const names = [definition, ...collisions]
@@ -494,18 +480,19 @@ export class CatalogService {
494
480
  a.order - b.order);
495
481
  });
496
482
  const pageMatches = matches.slice(offset, offset + limit);
483
+ const withSchemas = args.includeSchemas;
497
484
  const entries = pageMatches.map((match) => {
498
- const output = args.includeSchemas
485
+ const output = withSchemas
499
486
  ? this.outputSchema(match.connector.id, match.tool)
500
487
  : {};
501
488
  const input = match.tool.inputSchema ?? { type: "object" };
502
- const renderedInput = args.includeSchemas
503
- ? renderSearchSchema(input, args.includeSchemas)
489
+ const renderedInput = withSchemas
490
+ ? renderSearchSchema(input, withSchemas)
504
491
  : undefined;
505
- const renderedOutput = args.includeSchemas && output.schema
506
- ? renderSearchSchema(output.schema, args.includeSchemas)
492
+ const renderedOutput = withSchemas && output.schema
493
+ ? renderSearchSchema(output.schema, withSchemas)
507
494
  : undefined;
508
- const schemaKeys = args.includeSchemas && args.includeSchemaKeys
495
+ const schemaKeys = withSchemas && args.includeSchemaKeys
509
496
  ? schemaKeyMetadata(input, output.schema)
510
497
  : undefined;
511
498
  const description = summarizeDiscoveryDescription(match.tool.description, args.fullDescriptions === true);
@@ -523,7 +510,7 @@ export class CatalogService {
523
510
  name: match.tool.name,
524
511
  address: `${match.connector.id}.${match.tool.name}`,
525
512
  ...(description !== undefined ? { description } : {}),
526
- ...(args.includeSchemas
513
+ ...(withSchemas
527
514
  ? {
528
515
  inputSchema: renderedInput?.schema,
529
516
  }
@@ -531,12 +518,12 @@ export class CatalogService {
531
518
  ...(renderedInput?.truncated
532
519
  ? { inputSchemaTruncated: true }
533
520
  : {}),
534
- ...(args.includeSchemas && output.schema
521
+ ...(withSchemas && output.schema
535
522
  ? {
536
523
  outputSchema: renderedOutput?.schema,
537
524
  }
538
525
  : {}),
539
- ...(args.includeSchemas && output.source
526
+ ...(withSchemas && output.source
540
527
  ? { outputSchemaSource: output.source }
541
528
  : {}),
542
529
  ...(renderedOutput?.truncated
@@ -671,41 +658,46 @@ export class CatalogService {
671
658
  (queryTerms.length > 0
672
659
  ? matchMode === "partial"
673
660
  : unknownConnectorGuidance !== undefined || unavailableCatalogs > 0);
674
- const guidance = unsearchableQuery
675
- ? (unknownConnectorGuidance ??
676
- (scopedConnector && unavailableCatalogs > 0
661
+ const searchGuidance = () => {
662
+ if (unknownConnectorGuidance)
663
+ return unknownConnectorGuidance;
664
+ if (unsearchableQuery) {
665
+ return scopedConnector && unavailableCatalogs > 0
677
666
  ? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
678
- : "The query contained no searchable lexical terms. Use 2–4 ASCII action/object terms, or browse with an empty query."))
679
- : queryTerms.length === 0
680
- ? // A browse has no terms to advise about, so it stays silent unless
681
- // the scope itself failed: the guidance on a scoped miss recommends
682
- // browsing with an empty query, and that advice must not lead into a
683
- // dead end that looks like a connector with no tools.
684
- (unknownConnectorGuidance ??
685
- (unavailableCatalogs === 0
686
- ? undefined
687
- : scopedConnector
688
- ? `Connector "${scopedConnector.id}" could not be browsed because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
689
- : `${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable, so this browse is incomplete. Scope by connector to see the typed reason.`))
690
- : matches.length === 0
691
- ? (unknownConnectorGuidance ??
692
- (scopedConnector
693
- ? unavailableCatalogs > 0
694
- ? `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
695
- : scopedGuide?.required
696
- ? `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Fetch queryAnalysis.guide before calling, then refine terms or browse with an empty query.${filterRecovery}`
697
- : `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`
698
- : (identityGuidance ??
699
- (unavailableCatalogs === 0
700
- ? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
701
- : `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`))))
702
- : matchMode === "partial"
703
- ? scopedConnector
704
- ? `No single tool on connector "${scopedConnector.id}" matched every term. Split distinct intents into separate searches.`
705
- : unavailableCatalogs === 0
706
- ? "No single tool matched every term. Split distinct intents into separate searches."
707
- : "No single tool matched every term in the catalogs that answered. Split distinct intents into separate searches."
708
- : undefined;
667
+ : "The query contained no searchable lexical terms. Use 2–4 ASCII action/object terms, or browse with an empty query.";
668
+ }
669
+ if (queryTerms.length === 0) {
670
+ if (unavailableCatalogs === 0)
671
+ return undefined;
672
+ return scopedConnector
673
+ ? `Connector "${scopedConnector.id}" could not be browsed because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`
674
+ : `${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable, so this browse is incomplete. Scope by connector to see the typed reason.`;
675
+ }
676
+ if (matches.length === 0) {
677
+ if (scopedConnector) {
678
+ if (unavailableCatalogs > 0) {
679
+ return `Connector "${scopedConnector.id}" could not be searched because its catalog was unavailable. Inspect catalogError for the typed reason and recovery detail.`;
680
+ }
681
+ return scopedGuide?.required
682
+ ? `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Fetch queryAnalysis.guide before calling, then refine terms or browse with an empty query.${filterRecovery}`
683
+ : `No matching ${safetyLabel}capability was found on connector "${scopedConnector.id}". Refine terms or browse it with an empty query.${filterRecovery}`;
684
+ }
685
+ if (identityGuidance)
686
+ return identityGuidance;
687
+ return unavailableCatalogs === 0
688
+ ? `No matching ${safetyLabel}capability is configured in this deployment. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`
689
+ : `No matching ${safetyLabel}capability was found in the catalogs that answered; ${unavailableCatalogs} connector catalog${unavailableCatalogs === 1 ? " was" : "s were"} unavailable. Refine terms, scope by connector, or browse with an empty query.${filterRecovery}`;
690
+ }
691
+ if (matchMode !== "partial")
692
+ return undefined;
693
+ if (scopedConnector) {
694
+ return `No single tool on connector "${scopedConnector.id}" matched every term. Split distinct intents into separate searches.`;
695
+ }
696
+ return unavailableCatalogs === 0
697
+ ? "No single tool matched every term. Split distinct intents into separate searches."
698
+ : "No single tool matched every term in the catalogs that answered. Split distinct intents into separate searches.";
699
+ };
700
+ const guidance = searchGuidance();
709
701
  return {
710
702
  entries,
711
703
  total: matches.length,
@@ -859,6 +851,17 @@ export class CatalogService {
859
851
  });
860
852
  }
861
853
  }
854
+ function pageTail(page) {
855
+ return {
856
+ total: page.total,
857
+ offset: page.offset,
858
+ limit: page.limit,
859
+ hasMore: page.hasMore,
860
+ ...(page.nextOffset !== undefined ? { nextOffset: page.nextOffset } : {}),
861
+ ...(page.matchMode ? { matchMode: page.matchMode } : {}),
862
+ ...(page.queryAnalysis ? { queryAnalysis: page.queryAnalysis } : {}),
863
+ };
864
+ }
862
865
  export function groupedSearchResult(page) {
863
866
  const groups = [];
864
867
  const byConnector = new Map();
@@ -884,13 +887,7 @@ export function groupedSearchResult(page) {
884
887
  }
885
888
  return {
886
889
  connectors: groups,
887
- total: page.total,
888
- offset: page.offset,
889
- limit: page.limit,
890
- hasMore: page.hasMore,
891
- ...(page.nextOffset !== undefined ? { nextOffset: page.nextOffset } : {}),
892
- ...(page.matchMode ? { matchMode: page.matchMode } : {}),
893
- ...(page.queryAnalysis ? { queryAnalysis: page.queryAnalysis } : {}),
890
+ ...pageTail(page),
894
891
  };
895
892
  }
896
893
  export function flatSearchResult(page) {
@@ -902,12 +899,6 @@ export function flatSearchResult(page) {
902
899
  ? { guideSummary: entry.guideSummary }
903
900
  : {}),
904
901
  })),
905
- total: page.total,
906
- offset: page.offset,
907
- limit: page.limit,
908
- hasMore: page.hasMore,
909
- ...(page.nextOffset !== undefined ? { nextOffset: page.nextOffset } : {}),
910
- ...(page.matchMode ? { matchMode: page.matchMode } : {}),
911
- ...(page.queryAnalysis ? { queryAnalysis: page.queryAnalysis } : {}),
902
+ ...pageTail(page),
912
903
  };
913
904
  }