@zackbart/connecta-eve 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,7 +4,7 @@ Connect an Eve agent to a Connecta agent pool and load its endpoint instructions
4
4
  once per turn. The helper runs in the Eve app; the Connecta server
5
5
  runs separately. No `@zackbart/connecta` dependency is installed by this package.
6
6
 
7
- Requires **Node 24+** and **Eve 1.0.0-rc.1**. Development and consumer
7
+ Requires **Node 24+** and **Eve 1.0.0-rc.4**. Development and consumer
8
8
  checks use that exact release candidate. The peer stays pinned during the RC
9
9
  period; later candidates and stable 1.0 need a compatibility check. Helper
10
10
  releases share Connecta's version and tag; choose the helper version matching
@@ -13,7 +13,7 @@ your server release.
13
13
  ## Install and add two files
14
14
 
15
15
  ```sh
16
- npm install @zackbart/connecta-eve eve@1.0.0-rc.1
16
+ npm install @zackbart/connecta-eve eve@1.0.0-rc.4
17
17
  ```
18
18
 
19
19
  The server must declare your agent, admit it to the named pool and issue its
@@ -31,10 +31,7 @@ Create `agent/connections/connecta.ts`:
31
31
  ```ts
32
32
  import { connectaConnection } from "@zackbart/connecta-eve";
33
33
 
34
- export default connectaConnection({
35
- pool: "assistant",
36
- description: "Shared CRM and tickets for the assistant.",
37
- });
34
+ export default connectaConnection({ pool: "assistant" });
38
35
  ```
39
36
 
40
37
  Create `agent/instructions/connecta.ts`:
@@ -45,6 +42,25 @@ import { connectaInstructions } from "@zackbart/connecta-eve";
45
42
  export default connectaInstructions({ pool: "assistant" });
46
43
  ```
47
44
 
45
+ `description` is optional. Eve shows it in its catalog listing and matches its
46
+ words against every search query for every tool of the connection, so the default
47
+ is a fixed neutral line. If you set one, describe the domain in prose, such as
48
+ `"Member records, giving and event registration."`. Do not list connectors or
49
+ operations: on a three-connector meta pool, `"CRM, invoices and tickets"` made the
50
+ query `"invoices"` return all six routing tools, 9.4 KB measured, and tells the
51
+ model nothing about the operations behind them. The instructions carry the roster.
52
+ A string such as
53
+ `"Company tools through Connecta"` tells the model nothing, so the helper warns
54
+ at startup.
55
+
56
+ Eve names a connection after its file. If yours is not `connecta.ts`, say so in
57
+ the instructions, or the model is told tool names that do not exist:
58
+
59
+ ```ts
60
+ // agent/connections/workspace.ts gives the model workspace__* tools.
61
+ export default connectaInstructions({ pool: "assistant", connection: "workspace" });
62
+ ```
63
+
48
64
  Both factories accept `url` to override `CONNECTA_URL`. With `pool`, the URL is
49
65
  `<url>/agent/<pool>`; without `pool`, supply the complete endpoint URL. Pool
50
66
  names must match `[a-z0-9_-]+`. `connectaEndpoint({ url, pool })` returns the same
@@ -56,6 +72,16 @@ again for each request, so rotation requires no process restart. A `getToken`
56
72
  callback can return a string or a promise of one; it overrides the environment.
57
73
  No user's identity is forwarded.
58
74
 
75
+ `headers` adds request headers beside the bearer, such as a service token for an
76
+ edge in front of Connecta. Pass the same value to every factory. A function runs
77
+ for each request, so a rotated secret needs no restart. On guidance and skill
78
+ requests, the helper's own `Authorization` and MCP protocol headers always win.
79
+
80
+ ```ts
81
+ const headers = () => ({ "X-Edge-Token": process.env.EDGE_TOKEN ?? "" });
82
+ export default connectaConnection({ pool: "assistant", headers });
83
+ ```
84
+
59
85
  ## Choosing the pool at runtime
60
86
 
61
87
  Pass the same selector to both factories when the pool depends on the chat.
@@ -90,16 +116,62 @@ Each selected name must match `[a-z0-9_-]+`. A throwing selector or an invalid
90
116
  name fails the turn, even when guidance is cached. Handle lookup failures in
91
117
  your selector if it should fall back to a least-privileged pool.
92
118
 
93
- ## Upgrading to Eve 1.0 RC
119
+ ## Gating guidance and connections
120
+
121
+ Pass `when(ctx)` to load guidance only for eligible callers or turns. The helper
122
+ runs this predicate inside Eve's dynamic definition before selecting a pool,
123
+ reading a token, or using cached guidance. False or a thrown/rejected predicate
124
+ returns no guidance and performs no guidance fetch. It is evaluated before
125
+ every model call, including repeated evaluations within a turn.
94
126
 
95
- Update the app to `eve@1.0.0-rc.1` and reinstall to refresh its lockfile.
96
- The helper's connection, dynamic pool, instructions and approval APIs use Eve's
97
- stable entry points and require no authoring changes for this RC. Eve's
127
+ ```ts
128
+ export default connectaInstructions({
129
+ pool: "assistant",
130
+ when: (ctx) => ctx.session.auth.current?.principalId === "alice",
131
+ });
132
+ ```
133
+
134
+ `connectaConnection` accepts the same predicate and returns a dynamic connection
135
+ when it is provided. False or a policy failure removes that connection for the
136
+ current evaluation. The two factories evaluate their predicates independently;
137
+ use consistent trusted policy data as with a dynamic pool selector. This policy
138
+ controls what the app offers; Connecta still enforces endpoint authorization.
139
+ No Eve dynamic-value unwrapping is required.
140
+
141
+ ## Upgrading to Eve 1.0.0-rc.4
142
+
143
+ Update the app to `eve@1.0.0-rc.4`, reinstall to refresh its lockfile, then
144
+ rebuild and test it before deploying. The helper's connection, dynamic pool,
145
+ instructions and approval APIs use Eve's stable entry points and require no
146
+ authoring changes for this RC. Eve's
98
147
  [1.0 stability policy](https://eve.dev/docs/reference/typescript-api#stability-policy)
99
148
  does not cover `experimental` fields or internal modules.
100
149
 
101
- From 0.90, review the intervening
102
- [release notes](https://github.com/vercel/eve/blob/eve%401.0.0-rc.1/packages/eve/CHANGELOG.md):
150
+ From rc.1, review Eve's
151
+ [rc.2](https://github.com/vercel/eve/releases/tag/eve%401.0.0-rc.2),
152
+ [rc.3](https://github.com/vercel/eve/releases/tag/eve%401.0.0-rc.3) and
153
+ [rc.4](https://github.com/vercel/eve/releases/tag/eve%401.0.0-rc.4) release notes.
154
+ These change app-owned code and deployments the helper does not manage:
155
+
156
+ - **Rollback.** Session checkpoints move to version 19 (rc.3). A deployment from
157
+ before rc.3 refuses to take over a session rc.3 or later wrote, so that session
158
+ stays on the deployment that wrote it. New runs also use Workflow spec 9; set
159
+ `WORKFLOW_SPEC_VERSION=8` to keep creating spec 8 runs.
160
+ - **File tools.** Every agent gets a default `edit_file` tool (rc.4) that changes
161
+ sandbox files without `write_file`. If you disable, wrap or gate `write_file`,
162
+ do the same for `edit_file`. Connecta's own tools are unaffected.
163
+ - **Builds.** Eve moves to a Nitro 3 release candidate and declares Rolldown
164
+ itself (rc.3). Importing `nitro/storage`, `nitro/kv` or `nitro/database` from
165
+ agent code now fails the build, as does an asset import that leaves the authored
166
+ package.
167
+ - **Code extension.** `code__grep`, `code__worker` and `code__apply_patch` are gone
168
+ (rc.3, rc.4). Follow Eve's notes if your app mounts `eve/extensions/code`.
169
+ - **Dynamic tools.** A dynamic tool no longer replaces a static tool of the same
170
+ name (rc.3). Dynamic connections, including this helper's, still override a
171
+ static connection of the same name.
172
+
173
+ From 0.90, also review the
174
+ [rc.1 release notes](https://github.com/vercel/eve/blob/eve%401.0.0-rc.1/packages/eve/CHANGELOG.md):
103
175
  `session.clear()` now cancels running turns and tasks, and object-form connection
104
176
  approvals are now preserved at runtime. RC.1 also requires `@vercel/connect`
105
177
  `^2.7.0` when that optional integration is installed; older Connect releases
@@ -143,12 +215,38 @@ any tool whose `readOnlyHint` is not `true`, including writable `execute_code`.
143
215
  `approval`, either a policy or an approval configuration, takes precedence over
144
216
  `gateWrites`.
145
217
 
146
- The gate uses the host's cached annotations. If a read becomes a write before
147
- dispatch, Connecta applies its current classification; a writable pool can
148
- execute the write without a fresh approval. A read-only pool refuses it.
218
+ An agent-wide `approval` in Eve's `agent.ts` runs beside the connection's policy
219
+ and the stricter result wins. It can add a gate to Connecta tools, including under
220
+ the default `never()`, and cannot remove one that `gateWrites` set.
221
+
222
+ The gate uses the host's cached annotations. If a native tool becomes a write, its cached read name is refused before dispatch. Refresh `tools/list` and use the write's name; approval follows the host's policy. Read-only pools refuse writes.
149
223
 
150
224
  ## Endpoint instructions
151
225
 
226
+ Eve hides MCP tools behind `eve__tool` under a `<connection>__` prefix, and
227
+ `eve__search` covers listed tools only. Connecta's text is host-neutral, so the
228
+ helper places an Eve guide before it, built from the endpoint's
229
+ [facts](https://github.com/zackbart/connecta/blob/main/documentation/meta-tools.md#endpoint-facts):
230
+
231
+ - **Meta pools:** call `eve__tool({ name: "connecta__execute_code", input })`
232
+ directly, with a short example program. `eve__search` sees the routing tools and
233
+ never a downstream operation, so its empty result does not mean a capability is
234
+ missing. Reachable connectors are listed by id.
235
+ - **Native pools:** a read is `connecta__<connector>__<tool>` and a write
236
+ `connecta__<connector>__WRITE__<tool>`. Scope a search with
237
+ `"connecta__<connector>__"`, use `execute_code` for several, dependent or reduced
238
+ calls (omitted when the pool has none), and when tools are missing get a link from
239
+ `connecta__authorize_connector` for the user or operator. Eve lists tools again on
240
+ each step, so the repair needs no reconnect, unlike a host that keeps its first
241
+ list.
242
+ - **Older servers without facts:** only the naming rule.
243
+
244
+ The guide adds about 600 bytes with three connectors. Its roster stays under 512
245
+ bytes: titles go first, then whole entries give way to a `+N more` count. Names
246
+ in Connecta's text and in `nextAction` stay unprefixed, and the guide says so.
247
+ The helper warns once per endpoint when a server named `connecta` is on a
248
+ different major or minor release from the helper.
249
+
152
250
  Instructions use `server/discover` (MCP 2026-07-28), falling back to legacy
153
251
  `initialize` when discovery returns a response without guidance, including a
154
252
  non-OK status or an unsupported-method response. Network errors and timeouts
@@ -203,7 +301,7 @@ custom audience, even when Vercel signed them.
203
301
 
204
302
  In a source checkout with dependencies installed and both packages built, run
205
303
  `pnpm --filter @zackbart/connecta-eve run probe:catalog --output /absolute/path/outside/checkout`
206
- on Node 24. This boots real Eve 1.0.0-rc.1 with a packed, installed helper against
304
+ on Node 24. This boots real Eve 1.0.0-rc.4 with a packed, installed helper against
207
305
  seeded local catalogs of 40, 100, 250, 500, and 1,000 downstream tools on the
208
306
  meta, native, and native-plus-code surfaces. Native listings include three
209
307
  Connecta tools beyond the downstream count. A 1,001-tool native catalog must
@@ -213,8 +311,11 @@ The probe uses Eve's public language-model interface with a deterministic local
213
311
  transport, without model credentials or paid calls. It measures construction,
214
312
  cold and warm discovery, schema and result bytes, call counts, concurrent-call
215
313
  isolation, bounded-result paging, and process memory. Fresh child processes
216
- isolate each matrix cell; Eve startup is separate from turn latency. Reports
217
- retain actual Eve events and transport inputs. Token usage and model task
314
+ isolate each matrix cell; Eve startup is separate from turn latency. Each turn
315
+ also checks what the helper's guide claims about Eve: guidance arrives as its own
316
+ system message on every model call, `eve__search` on a meta pool reaches only
317
+ routing tools, native writes are named `connecta__<connector>__WRITE__<tool>`, and
318
+ every step lists tools again. Reports retain actual Eve events and transport inputs. Token usage and model task
218
319
  quality remain unmeasured; these results establish local runtime contracts,
219
320
  not live Vercel workload admission or production model performance.
220
321
 
@@ -224,3 +325,30 @@ checkout. Its preparation command packs and installs the current core/helper,
224
325
  checks local signed controls and cancellation, and creates no live resources.
225
326
  Live execution requires separate owner authorization for the concrete account,
226
327
  project, bounds, cost ceiling and cleanup. Keep its evidence outside the checkout.
328
+
329
+ ### Deferred connector guide (opt-in)
330
+
331
+ In `agent/skills/crm.ts`:
332
+
333
+ ```ts
334
+ import { connectaSkill } from "@zackbart/connecta-eve";
335
+
336
+ export default connectaSkill({
337
+ pool: "crm",
338
+ name: "connector:crm",
339
+ description: "Use when working with CRM person identifiers and conventions.",
340
+ when: (ctx) => ctx.session.auth !== null,
341
+ });
342
+ ```
343
+
344
+ This uses the pinned Eve 1.0.0-rc.4 public `defineSkill` and `defineDynamic`
345
+ APIs. Eve's deferred discovery searches the description; loading adds only
346
+ instructions. It never installs tools. Use the same caller predicate and pool
347
+ selector as the connection. A false or throwing predicate skips selection,
348
+ token resolution and fetching. The public API accepts content rather than a
349
+ load callback, so fetching happens once per endpoint/guide/turn through Eve's
350
+ session memo; inclusion in model context is lazy. Failed responses omit the
351
+ skill, including network, timeout and malformed-response failures; turn
352
+ cancellation still propagates. A successful guide can be reused while the same
353
+ endpoint/guide/turn memo is retained. A new turn fetches again; failure removes
354
+ the skill rather than serving the prior turn's guide. The guide must already be visible through that endpoint's `skills` tool.
package/dist/index.d.ts CHANGED
@@ -14,14 +14,31 @@ export interface ConnectaEndpointOptions {
14
14
  * no restart. Defaults to the `CONNECTA_ACCESS_TOKEN` environment variable.
15
15
  */
16
16
  getToken?: () => string | Promise<string>;
17
+ /**
18
+ * Extra request headers, such as a service token for an edge in front of
19
+ * Connecta. A function is resolved on every request. These never replace
20
+ * the helper's bearer or MCP protocol headers on guidance requests.
21
+ */
22
+ headers?: ConnectaHeaders;
17
23
  }
24
+ /** Fixed headers, or a resolver called on every request. */
25
+ export type ConnectaHeaders = Readonly<Record<string, string>> | (() => Readonly<Record<string, string>> | Promise<Readonly<Record<string, string>>>);
18
26
  /** App-owned pool policy; use `ctx.memo` when its result should stay fixed. */
19
27
  export type ConnectaPoolSelector = (ctx: DynamicContext) => string | Promise<string>;
28
+ /** App-owned caller/turn policy, evaluated before selecting a pool or fetching guidance. */
29
+ export type ConnectaPredicate = (ctx: DynamicContext) => boolean | Promise<boolean>;
20
30
  export interface ConnectaConnectionOptions extends Omit<ConnectaEndpointOptions, "pool"> {
21
31
  /** A fixed pool, or a policy Eve evaluates before every model call. */
22
32
  pool?: string | ConnectaPoolSelector;
23
- /** What the connection offers, for the model choosing which connection to search. */
24
- description: string;
33
+ /** Omit this value when false or when the policy throws. Evaluated every model call. */
34
+ when?: ConnectaPredicate;
35
+ /**
36
+ * What the connection offers, in Eve's catalog listing and in the words every
37
+ * tool of this connection matches in `eve__search`. Defaults to a fixed
38
+ * neutral line. Describe the domain in prose; do not list connectors or
39
+ * operations here, because a query naming one then matches every tool.
40
+ */
41
+ description?: string;
25
42
  /** Eve's approval for every Connecta tool: a policy or configuration. Defaults to `never()`. */
26
43
  approval?: Approval;
27
44
  /**
@@ -34,6 +51,13 @@ export interface ConnectaConnectionOptions extends Omit<ConnectaEndpointOptions,
34
51
  export interface ConnectaInstructionsOptions extends Omit<ConnectaEndpointOptions, "pool"> {
35
52
  /** A fixed pool, or the same policy used by the connection. */
36
53
  pool?: string | ConnectaPoolSelector;
54
+ /** Omit this value when false or when the policy throws. Evaluated every model call. */
55
+ when?: ConnectaPredicate;
56
+ /**
57
+ * The Eve connection's name, which is its file under `agent/connections/`.
58
+ * The model calls `<connection>__<tool>`. Default `connecta`.
59
+ */
60
+ connection?: string;
37
61
  /** MCP client name for instruction requests. Default `connecta-eve-instructions`. */
38
62
  clientName?: string;
39
63
  /** How long fetched instructions are reused. Default ten minutes. */
@@ -52,11 +76,15 @@ export declare const gateConnectaWrites: ApprovalPolicy;
52
76
  * The Eve connection for a Connecta agent endpoint, with app-owned auth:
53
77
  * one agent token, sent as a bearer and re-read on every request.
54
78
  */
79
+ export declare function connectaConnection(options: ConnectaConnectionOptions & {
80
+ when: ConnectaPredicate;
81
+ }): ReturnType<typeof defineDynamicConnection>;
55
82
  export declare function connectaConnection(options: ConnectaConnectionOptions & {
56
83
  pool: ConnectaPoolSelector;
57
84
  }): ReturnType<typeof defineDynamicConnection>;
58
85
  export declare function connectaConnection(options: ConnectaConnectionOptions & {
59
86
  pool?: string;
87
+ when?: undefined;
60
88
  }): McpClientConnectionDefinition;
61
89
  export declare function connectaConnection(options: ConnectaConnectionOptions): McpClientConnectionDefinition | ReturnType<typeof defineDynamicConnection>;
62
90
  /**
@@ -67,4 +95,18 @@ export declare function connectaConnection(options: ConnectaConnectionOptions):
67
95
  * model call; a memo keyed on the endpoint and turn keeps one result for each.
68
96
  * Pool policy runs independently of the connection and owns its memo lifetime.
69
97
  */
70
- export declare function connectaInstructions(options?: ConnectaInstructionsOptions): import("eve/connections").DynamicSentinel<import("eve/instructions").DynamicInstructionsResult, DynamicContext>;
98
+ export declare function connectaInstructions(options?: ConnectaInstructionsOptions): import("eve/skills").DynamicSentinel<import("eve/instructions").DynamicInstructionsResult, DynamicContext>;
99
+ /** One explicitly selected Connecta guide projected as a deferred Eve skill. */
100
+ export interface ConnectaSkillOptions extends Omit<ConnectaInstructionsOptions, "connection"> {
101
+ /** Exact skills-tool name, for example `connector:crm`. */
102
+ name: string;
103
+ /** Short task selector Eve can search without loading the guide. */
104
+ description: string;
105
+ }
106
+ /**
107
+ * Opt-in deferred instructions, installed from an agent/skills module.
108
+ * Resolves only after caller policy, with no tools added by loading the skill.
109
+ * Eve's public API takes content, not a load callback: fetching is per turn;
110
+ * only model-context inclusion is lazy.
111
+ */
112
+ export declare function connectaSkill(options: ConnectaSkillOptions): import("eve/skills").DynamicSentinel<import("eve/skills").DynamicSkillResult, DynamicContext>;
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
+ import { defineDynamic as defineDynamicSkill, defineSkill } from "eve/skills";
1
2
  import { defineDynamic as defineDynamicConnection, defineMcpClientConnection, } from "eve/connections";
2
3
  import { defineDynamic, defineInstructions } from "eve/instructions";
3
4
  import { never } from "eve/tools/approval";
5
+ import { CONNECTA_VERSION } from "./version.js";
4
6
  function environment(name) {
5
7
  const value = globalThis.process?.env?.[name];
6
8
  return value?.trim() || undefined;
@@ -32,21 +34,43 @@ async function tokenFor(options) {
32
34
  */
33
35
  export const gateConnectaWrites = ({ toolAnnotations }) => toolAnnotations?.readOnlyHint === true ? "not-applicable" : "user-approval";
34
36
  export function connectaConnection(options) {
37
+ warnOnGenericDescription(options.description);
35
38
  const { pool, ...rest } = options;
36
- if (typeof pool === "function") {
39
+ if (typeof pool === "function" || options.when) {
37
40
  return defineDynamicConnection(async (ctx) => {
38
- const selected = await pool(ctx);
39
- validatePool(selected);
40
- return connectionDefinition(options, connectaEndpoint({ ...rest, pool: selected }), selected);
41
+ if (!(await enabled(options.when, ctx)))
42
+ return null;
43
+ // Only a static `pool` may be omitted; connectaEndpoint validates it. A
44
+ // selector must name a valid pool, or the turn fails instead of using the base URL.
45
+ let selected = pool;
46
+ if (typeof pool === "function") {
47
+ selected = await pool(ctx);
48
+ validatePool(selected);
49
+ }
50
+ return connectionDefinition(options, connectaEndpoint({ ...rest, ...(selected === undefined ? {} : { pool: selected }) }), selected);
41
51
  });
42
52
  }
43
53
  return connectionDefinition(options, connectaEndpoint(pool === undefined ? rest : { ...rest, pool }));
44
54
  }
55
+ /**
56
+ * Eve matches these words against every search query, and a short word also
57
+ * matches longer query words ("use" hits "users"), so this avoids common stems.
58
+ */
59
+ const DEFAULT_DESCRIPTION = "Connecta integration gateway.";
60
+ /** "Company tools through Connecta" and other strings any Connecta connection could claim. */
61
+ const GENERIC_DESCRIPTION = /\b(?:tools?|services?|integrations?|apps?)\s+(?:through|via|from|using|on|in|with)\s+connecta\b|^\W*(?:(?:the|our|all|my|company|connecta|mcp|tools?|services?|integrations?|connections?|gateway|server|access|through|via|using)\W*)*$/i;
62
+ function warnOnGenericDescription(description) {
63
+ if (description === undefined || !GENERIC_DESCRIPTION.test(description))
64
+ return;
65
+ console.warn(`[connecta-eve] Connection description ${JSON.stringify(description)} gives the model no domain information. Omit it, or say what this agent reaches in prose without listing connectors.`);
66
+ }
45
67
  function connectionDefinition(options, endpoint, instanceKey) {
68
+ const { headers } = options;
46
69
  return defineMcpClientConnection({
47
70
  url: endpoint,
48
71
  ...(instanceKey === undefined ? {} : { instanceKey }),
49
- description: options.description,
72
+ description: options.description ?? DEFAULT_DESCRIPTION,
73
+ ...(headers === undefined ? {} : { headers: typeof headers === "function" ? () => headers() : headers }),
50
74
  auth: {
51
75
  principalType: "app",
52
76
  getToken: async () => ({ token: await tokenFor(options) }),
@@ -54,18 +78,116 @@ function connectionDefinition(options, endpoint, instanceKey) {
54
78
  approval: options.approval ?? (options.gateWrites ? gateConnectaWrites : never()),
55
79
  });
56
80
  }
57
- /** Connecta's MCP `instructions` for one endpoint, or null when unavailable. */
81
+ async function enabled(when, ctx) {
82
+ try {
83
+ return when ? (await when(ctx)) === true : true;
84
+ }
85
+ catch {
86
+ return false;
87
+ }
88
+ }
89
+ /** Older servers omit the extension; anything malformed reads the same way. */
90
+ function endpointFacts(value) {
91
+ const facts = value;
92
+ if (facts?.surface !== "meta" && facts?.surface !== "native")
93
+ return undefined;
94
+ if (typeof facts.executeCode !== "boolean" || !Array.isArray(facts.connectors))
95
+ return undefined;
96
+ const text = (field) => (typeof field === "string" ? field.replace(/\s+/g, " ").trim() : "");
97
+ return {
98
+ surface: facts.surface,
99
+ executeCode: facts.executeCode,
100
+ readOnly: facts.readOnly === true,
101
+ connectors: facts.connectors.flatMap((connector) => {
102
+ const id = text(connector?.id);
103
+ if (!id)
104
+ return [];
105
+ const title = text(connector.title);
106
+ const readFirst = text(connector.readFirst);
107
+ return [{ id, ...(title ? { title } : {}), ...(readFirst ? { readFirst } : {}) }];
108
+ }),
109
+ };
110
+ }
111
+ const ROSTER_MAX_BYTES = 512;
112
+ /** Detail degrades before names do, as the server's own inventory does. */
113
+ function connectorRoster(facts, prefix) {
114
+ const fits = (line) => new TextEncoder().encode(line).length <= ROSTER_MAX_BYTES;
115
+ const entry = ({ id, title, readFirst }, withTitle) => {
116
+ const notes = [
117
+ ...(withTitle && title ? [title] : []),
118
+ // A native tool's description already carries its guide pointer.
119
+ ...(readFirst && facts.surface === "meta" ? [`read first: ${prefix}skills { name: "${readFirst}" }`] : []),
120
+ ];
121
+ return notes.length ? `${id} (${notes.join("; ")})` : id;
122
+ };
123
+ for (const withTitle of [true, false]) {
124
+ const line = facts.connectors.map((connector) => entry(connector, withTitle)).join(", ");
125
+ if (fits(line))
126
+ return line;
127
+ }
128
+ const entries = facts.connectors.map((connector) => entry(connector, false));
129
+ let shown = 0;
130
+ while (fits(`${entries.slice(0, shown + 1).join(", ")}; +${entries.length - shown - 1} more`))
131
+ shown++;
132
+ return `${entries.slice(0, shown).join(", ")}${shown ? "; " : ""}+${entries.length - shown} more`;
133
+ }
134
+ /**
135
+ * How Eve's deferred catalog reaches this endpoint's tools. Connecta's own text
136
+ * names tools as MCP lists them; Eve adds the connection prefix and hides them
137
+ * behind `eve__tool`, and its search covers listed tools only.
138
+ */
139
+ function eveGuide(connection, facts) {
140
+ const prefix = `${connection}__`;
141
+ if (!facts) {
142
+ return `Connecta tools are deferred: call eve__tool({ name: "${prefix}<tool>", input }). Names in this guidance and in nextAction omit the "${prefix}" prefix.`;
143
+ }
144
+ const roster = facts.connectors.length ? connectorRoster(facts, prefix) : undefined;
145
+ if (facts.surface === "meta") {
146
+ return `Connecta tools are deferred: call eve__tool({ name: "${prefix}<tool>", input }). Names in this guidance and in nextAction omit the "${prefix}" prefix. Go straight to ${prefix}execute_code without eve__search, input { code: "async () => { const { tools } = await connecta.search({ query: 'list invoices', safety: 'readOnly' }); const r = await connecta.call(tools[0].address, {}); return r.data; }" }. eve__search sees only Connecta's routing tools, never downstream operations, so empty results do not mean a capability is missing; search inside the program.${roster ? ` Reachable: ${roster}.` : ""}`;
147
+ }
148
+ return `Connecta tools are deferred: call eve__tool({ name: "${prefix}<connector>__<tool>", input })${facts.readOnly ? "" : `, with writes named "${prefix}<connector>__WRITE__<tool>"`}. Find them with eve__search; scope one connector with "${prefix}<connector>__", not "<connector>__". Names in this guidance and in nextAction omit "${prefix}".${facts.executeCode ? ` For several, dependent or reduced calls, go straight to ${prefix}execute_code with { code: "async () => { ... }" }, using each tool's Program address.` : ""} Tools missing: ${prefix}authorize_connector returns a link; give it to the user or operator once and stop. Eve lists tools again every step, so a repair needs no reconnect.${roster ? ` Connectors: ${roster}.` : ""}`;
149
+ }
150
+ /** Release lines a helper and server must share; a patch difference is compatible. */
151
+ const releaseLine = (version) => typeof version === "string" ? /^(\d+\.\d+)\./.exec(version)?.[1] : undefined;
152
+ /** An endpoint may embed credentials or a token in its URL, path included; logs get only its origin. */
153
+ function loggableEndpoint(endpoint) {
154
+ try {
155
+ return new URL(endpoint).origin;
156
+ }
157
+ catch {
158
+ return "the configured endpoint";
159
+ }
160
+ }
161
+ /** A deployment that renamed its server may version it independently, so only `connecta` is compared. */
162
+ function warnOnVersionSkew(endpoint, server) {
163
+ if (server?.name !== "connecta")
164
+ return;
165
+ const line = releaseLine(server.version);
166
+ if (line === undefined || line === releaseLine(CONNECTA_VERSION))
167
+ return;
168
+ // The version is remote text, so only its validated numeric line reaches logs.
169
+ console.warn(`[connecta-eve] Helper ${CONNECTA_VERSION} and Connecta ${line}.x at ${loggableEndpoint(endpoint)} are different releases. Install the helper matching your server.`);
170
+ }
171
+ /** Headers for a guidance request. The caller's extras can never replace what the request itself needs. */
172
+ async function guidanceHeaders(options, own) {
173
+ const extra = typeof options.headers === "function" ? await options.headers() : options.headers;
174
+ const headers = new Headers(extra);
175
+ for (const [name, value] of Object.entries(own))
176
+ headers.set(name, value);
177
+ return Object.fromEntries(headers);
178
+ }
179
+ /** Connecta's MCP `instructions` and endpoint facts for one endpoint, or null when unavailable. */
58
180
  async function instructionRequest(options, endpoint, method, signal) {
59
181
  const modern = method === "server/discover";
60
182
  const clientInfo = { name: options.clientName ?? "connecta-eve-instructions", version: "1" };
61
183
  const response = await (options.fetch ?? fetch)(endpoint, {
62
184
  method: "POST",
63
- headers: {
185
+ headers: await guidanceHeaders(options, {
64
186
  Authorization: `Bearer ${await tokenFor(options)}`,
65
187
  "Content-Type": "application/json",
66
188
  Accept: "application/json, text/event-stream",
67
189
  ...(modern ? { "MCP-Protocol-Version": "2026-07-28", "Mcp-Method": method } : {}),
68
- },
190
+ }),
69
191
  body: JSON.stringify({
70
192
  jsonrpc: "2.0",
71
193
  id: 1,
@@ -98,8 +220,15 @@ async function instructionRequest(options, endpoint, method, signal) {
98
220
  ?.slice("data:".length);
99
221
  if (!json)
100
222
  return null;
101
- const instructions = JSON.parse(json).result?.instructions;
102
- return typeof instructions === "string" && instructions.trim() ? instructions.trim() : null;
223
+ const { result } = JSON.parse(json);
224
+ const instructions = result?.instructions;
225
+ if (typeof instructions !== "string" || !instructions.trim())
226
+ return null;
227
+ return {
228
+ instructions: instructions.trim(),
229
+ facts: endpointFacts(result?.capabilities?.extensions?.["dev.connecta/endpoint"]),
230
+ server: result?._meta?.["io.modelcontextprotocol/serverInfo"] ?? result?.serverInfo,
231
+ };
103
232
  }
104
233
  async function fetchConnectaInstructions(options, endpoint, signal) {
105
234
  // Only a completed discovery response can justify legacy negotiation.
@@ -117,6 +246,10 @@ async function fetchConnectaInstructions(options, endpoint, signal) {
117
246
  */
118
247
  export function connectaInstructions(options = {}) {
119
248
  const ttlMs = options.ttlMs ?? 600_000;
249
+ const connection = options.connection ?? "connecta";
250
+ if (!/^[A-Za-z0-9._-]+$/.test(connection)) {
251
+ throw new Error(`Connecta connection "${connection}" must be the file name under agent/connections.`);
252
+ }
120
253
  const endpoints = new Map();
121
254
  const current = async (endpoint, signal) => {
122
255
  signal.throwIfAborted();
@@ -130,7 +263,13 @@ export function connectaInstructions(options = {}) {
130
263
  return cached?.content ?? null;
131
264
  let fresh;
132
265
  try {
133
- fresh = await fetchConnectaInstructions(options, endpoint, signal);
266
+ const guidance = await fetchConnectaInstructions(options, endpoint, signal);
267
+ if (guidance && !state.versionChecked) {
268
+ state.versionChecked = true;
269
+ warnOnVersionSkew(endpoint, guidance.server);
270
+ }
271
+ // Eve's half comes first: it says how to read every name in Connecta's own text.
272
+ fresh = guidance && `${eveGuide(connection, guidance.facts)}\n${guidance.instructions}`;
134
273
  }
135
274
  catch {
136
275
  // Eve's cancellation is not an endpoint failure: rethrowing keeps it out
@@ -144,6 +283,8 @@ export function connectaInstructions(options = {}) {
144
283
  return fresh ?? cached?.content ?? null;
145
284
  };
146
285
  return defineDynamic(async (ctx) => {
286
+ if (!(await enabled(options.when, ctx)))
287
+ return null;
147
288
  const { pool, ...rest } = options;
148
289
  let endpoint;
149
290
  if (typeof pool === "function") {
@@ -164,3 +305,75 @@ export function connectaInstructions(options = {}) {
164
305
  return content ? defineInstructions({ content: `Connecta guidance:\n${content}` }) : null;
165
306
  });
166
307
  }
308
+ /**
309
+ * Opt-in deferred instructions, installed from an agent/skills module.
310
+ * Resolves only after caller policy, with no tools added by loading the skill.
311
+ * Eve's public API takes content, not a load callback: fetching is per turn;
312
+ * only model-context inclusion is lazy.
313
+ */
314
+ export function connectaSkill(options) {
315
+ if (!options.name.trim() || !options.description.trim())
316
+ throw new Error("A skill needs a name and description.");
317
+ return defineDynamicSkill(async (ctx) => {
318
+ if (!(await enabled(options.when, ctx)))
319
+ return null;
320
+ const { pool, ...endpointOptions } = options;
321
+ const selected = typeof pool === "function" ? await pool(ctx) : pool;
322
+ if (typeof options.pool === "function")
323
+ validatePool(selected);
324
+ const endpoint = connectaEndpoint({ ...endpointOptions, ...(selected === undefined ? {} : { pool: selected }) });
325
+ const content = await ctx.memo(["connecta-skill", endpoint, options.name, ctx.turn?.id ?? null], async ({ abortSignal }) => {
326
+ try {
327
+ const response = await (options.fetch ?? fetch)(endpoint, {
328
+ method: "POST",
329
+ headers: await guidanceHeaders(options, {
330
+ Authorization: `Bearer ${await tokenFor(options)}`,
331
+ "Content-Type": "application/json",
332
+ Accept: "application/json, text/event-stream",
333
+ "MCP-Protocol-Version": "2026-07-28",
334
+ "Mcp-Method": "tools/call",
335
+ "Mcp-Name": "skills",
336
+ }),
337
+ body: JSON.stringify({
338
+ jsonrpc: "2.0",
339
+ id: 1,
340
+ method: "tools/call",
341
+ params: {
342
+ name: "skills",
343
+ arguments: { name: options.name },
344
+ _meta: {
345
+ "io.modelcontextprotocol/protocolVersion": "2026-07-28",
346
+ "io.modelcontextprotocol/clientCapabilities": {},
347
+ },
348
+ },
349
+ }),
350
+ signal: AbortSignal.any([abortSignal, AbortSignal.timeout(5_000)]),
351
+ });
352
+ if (!response.ok)
353
+ return null;
354
+ const text = await response.text();
355
+ const json = text.trimStart().startsWith("{")
356
+ ? text
357
+ : text
358
+ .split("\n")
359
+ .find((line) => line.startsWith("data:"))
360
+ ?.slice(5);
361
+ if (!json)
362
+ return null;
363
+ const reply = JSON.parse(json);
364
+ if (!reply.result || reply.result.isError)
365
+ return null;
366
+ return (reply.result.content
367
+ ?.filter((block) => block.type === "text")
368
+ .map((block) => block.text ?? "")
369
+ .join("\n") || null);
370
+ }
371
+ catch {
372
+ // Endpoint failures omit optional guidance; turn cancellation still propagates.
373
+ abortSignal.throwIfAborted();
374
+ return null;
375
+ }
376
+ });
377
+ return content ? defineSkill({ description: options.description, content, deferred: true }) : null;
378
+ });
379
+ }
@@ -0,0 +1 @@
1
+ export declare const CONNECTA_VERSION = "0.42.0";
@@ -0,0 +1,3 @@
1
+ // Generated from package.json by scripts/generate-version.mjs.
2
+ // Workers-safe: no runtime filesystem read or deployment override.
3
+ export const CONNECTA_VERSION = "0.42.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zackbart/connecta-eve",
3
- "version": "0.40.0",
3
+ "version": "0.42.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Eve connection and instructions helpers for Connecta agent endpoints.",
@@ -28,11 +28,11 @@
28
28
  }
29
29
  },
30
30
  "peerDependencies": {
31
- "eve": "1.0.0-rc.1"
31
+ "eve": "1.0.0-rc.4"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^24.19.2",
35
- "eve": "1.0.0-rc.1",
35
+ "eve": "1.0.0-rc.4",
36
36
  "typescript": "^6.0.3",
37
37
  "vite": "^8.3.4",
38
38
  "vitest": "^5.0.4"