@uipath/maestro-builder-sdk 6.8.5 → 6.9.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.
@@ -18,6 +18,7 @@ import { lowerFirst, pyStr, pyStrip, startsWithUpper } from "../compat/pystr.js"
18
18
  import { mapConcurrent } from "../concurrency.js";
19
19
  import { Refusal } from "../errors.js";
20
20
  import { isTriggersDescribe, isTriggersObjects } from "../uip/commands.js";
21
+ import { failureReason } from "../uip/envelope.js";
21
22
  import { safeCacheName } from "../uip/response-cache.js";
22
23
  import { schemaCacheScope, writeCache } from "./context.js";
23
24
  import { objectsComeFromTheConnection } from "./generic-expand.js";
@@ -66,8 +67,8 @@ async function runJson(client, argv, timeoutMs) {
66
67
  payload = JSON.parse(result.stdout);
67
68
  }
68
69
  catch {
69
- const text = codePointSlice(pyStrip(result.stderr || result.stdout), 0, 300);
70
- throw new EventDiscoveryError(`${head} did not answer JSON: ${text || "no output"}`, result.refused === true);
70
+ const text = codePointSlice(failureReason(result), 0, 300);
71
+ throw new EventDiscoveryError(`${head} did not answer JSON: ${text}`, result.refused === true);
71
72
  }
72
73
  if (get(payload, "Result") !== "Success") {
73
74
  // `refused` is carried here too, although the refusing client in this tree
@@ -5,7 +5,8 @@ import type { UipClient } from "../uip/client.js";
5
5
  * Exact prefix only: the same search returns
6
6
  * `uipath.agent.resource.tool.connector.<key>.<action>` and
7
7
  * `uipath.connector.trigger.<key>.<event>`, which are not actions of this node
8
- * and would read as alternatives an author could pass.
8
+ * and would read as alternatives an author could pass as an action. The events
9
+ * are listed separately, by {@link registryEventIds}.
9
10
  */
10
11
  export declare function registryActionIds(payload: unknown, connectorKey: string): string[];
11
12
  /**
@@ -19,8 +20,44 @@ export declare function registryActionIds(payload: unknown, connectorKey: string
19
20
  */
20
21
  export declare function closestActions(action: string, candidates: readonly string[]): string[];
21
22
  /**
22
- * `_registry_actions`: the action ids the registry serves for `connectorKey`,
23
- * or `[]` when the search fails or does not answer JSON. This runs only on the
24
- * failure path, where an empty answer is itself the message ("returned nothing").
23
+ * {@link closestActions}, kept only where the candidate is plausibly what was
24
+ * meant: it shares MOST of the wanted id's words (`list-all-records` →
25
+ * `list-records`), or it is a near spelling (`record-creatd` → `record-created`,
26
+ * `send-mail` → `send-email`). One shared word is not enough: `record-created`
27
+ * shares "record" with every Data Fabric action, and offering
28
+ * `update-entity-record` as the closest match to an EVENT name points the
29
+ * author at the wrong namespace.
25
30
  */
26
- export declare function registryActions(client: UipClient, connectorKey: string, timeoutMs?: number): Promise<string[]>;
31
+ export declare function plausibleMatches(wanted: string, candidates: readonly string[]): string[];
32
+ /** The two node-type forms a connector event is served in. */
33
+ export type EventForm = "trigger" | "event";
34
+ /** One event id in a `registry search` reply, with the forms it was served in. */
35
+ export interface RegistryEvent {
36
+ id: string;
37
+ /** `trigger` = `uipath.connector.trigger.<key>.<id>` (starts a flow), `event` = `…event…` (waits mid-flow). */
38
+ forms: EventForm[];
39
+ }
40
+ /**
41
+ * The event ids in a `registry search <key>` reply, sorted, each with its forms.
42
+ *
43
+ * The form is kept because the two can come and go separately: on 2026-09-16
44
+ * the Data Fabric `.event.` pair was still served after its `.trigger.` pair
45
+ * was gone, and `prepare` probes the `.trigger.` form.
46
+ */
47
+ export declare function registryEventIds(payload: unknown, connectorKey: string): RegistryEvent[];
48
+ /** What `registry search <key>` served, or why it could not say. */
49
+ export interface RegistryCatalog {
50
+ actions: string[];
51
+ events: RegistryEvent[];
52
+ /**
53
+ * Set when the search itself failed or did not answer JSON. The lists are
54
+ * then empty because nothing was read, NOT because the catalog is empty, and
55
+ * a message must not say "returned nothing".
56
+ */
57
+ failure?: string;
58
+ }
59
+ /**
60
+ * `registry search <key>`, read for both catalogs. This runs only on the
61
+ * failure path, to tell the author what the catalog does serve.
62
+ */
63
+ export declare function registryCatalog(client: UipClient, connectorKey: string, timeoutMs?: number): Promise<RegistryCatalog>;
@@ -3,9 +3,14 @@
3
3
  *
4
4
  * When `registry get uipath.connector.<key>.<action>` finds nothing — and the
5
5
  * event namespaces do not have it either — the one question left is what the
6
- * tenant actually serves for that connector, so the refusal can list it. That
7
- * is a `registry search <key>` against the live catalog rather than a cached
8
- * index, because a stale index is one of the ways this failure happens.
6
+ * catalog actually serves for that connector, so the refusal can list it: its
7
+ * actions AND its events, because the author's name may have meant either.
8
+ * That is one `registry search <key>`.
9
+ *
10
+ * `registry get` and `registry search` both answer from the CLI's local node
11
+ * index and pull the tenant catalog only when that index is missing (uipath-cli
12
+ * `packages/maestro-sdk/src/registry/node-sync-service.ts:307-323`, `:570-597`),
13
+ * so what they list can be stale; the refusal says how to refresh it.
9
14
  *
10
15
  * Ported from `_registry_actions` / `_closest_actions` in
11
16
  * `prepare_connector.py`. The fetch goes through the injected {@link UipClient};
@@ -13,6 +18,7 @@
13
18
  */
14
19
  import { compareCodePoints, sortedStrings } from "../compat/compare.js";
15
20
  import { registrySearch } from "../uip/commands.js";
21
+ import { failureReason, findJsonDocument, tail } from "../uip/envelope.js";
16
22
  import { isPlainObject, pyOr } from "./support.js";
17
23
  /**
18
24
  * The action ids in a `registry search <key>` reply, sorted.
@@ -20,7 +26,8 @@ import { isPlainObject, pyOr } from "./support.js";
20
26
  * Exact prefix only: the same search returns
21
27
  * `uipath.agent.resource.tool.connector.<key>.<action>` and
22
28
  * `uipath.connector.trigger.<key>.<event>`, which are not actions of this node
23
- * and would read as alternatives an author could pass.
29
+ * and would read as alternatives an author could pass as an action. The events
30
+ * are listed separately, by {@link registryEventIds}.
24
31
  */
25
32
  export function registryActionIds(payload, connectorKey) {
26
33
  // `json.loads(stdout).get("Data") or []`: a reply that is not an object has
@@ -74,21 +81,88 @@ export function closestActions(action, candidates) {
74
81
  });
75
82
  return scored.map(({ candidate }) => candidate);
76
83
  }
84
+ /** Levenshtein distance, two rows. Short strings only (operation ids). */
85
+ function editDistance(a, b) {
86
+ let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
87
+ for (let i = 1; i <= a.length; i++) {
88
+ const cur = [i];
89
+ for (let j = 1; j <= b.length; j++) {
90
+ cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
91
+ }
92
+ prev = cur;
93
+ }
94
+ return prev[b.length];
95
+ }
77
96
  /**
78
- * `_registry_actions`: the action ids the registry serves for `connectorKey`,
79
- * or `[]` when the search fails or does not answer JSON. This runs only on the
80
- * failure path, where an empty answer is itself the message ("returned nothing").
97
+ * {@link closestActions}, kept only where the candidate is plausibly what was
98
+ * meant: it shares MOST of the wanted id's words (`list-all-records` →
99
+ * `list-records`), or it is a near spelling (`record-creatd` → `record-created`,
100
+ * `send-mail` → `send-email`). One shared word is not enough: `record-created`
101
+ * shares "record" with every Data Fabric action, and offering
102
+ * `update-entity-record` as the closest match to an EVENT name points the
103
+ * author at the wrong namespace.
81
104
  */
82
- export async function registryActions(client, connectorKey, timeoutMs) {
83
- const result = await client.run(registrySearch(connectorKey), timeoutMs === undefined ? undefined : { timeoutMs });
84
- if (result.code !== 0)
105
+ export function plausibleMatches(wanted, candidates) {
106
+ const words = new Set(wanted.split("-"));
107
+ const limit = Math.max(2, Math.floor(wanted.length / 3));
108
+ const kept = closestActions(wanted, candidates)
109
+ .map((candidate, rank) => ({ candidate, rank, distance: editDistance(wanted, candidate) }))
110
+ .filter(({ candidate, distance }) => {
111
+ const shared = [...words].filter((word) => candidate.split("-").includes(word)).length;
112
+ return shared * 2 > words.size || distance <= limit;
113
+ });
114
+ // Nearest spelling first; the word-overlap rank breaks ties.
115
+ kept.sort((a, b) => a.distance - b.distance || a.rank - b.rank);
116
+ return kept.map(({ candidate }) => candidate);
117
+ }
118
+ /**
119
+ * The event ids in a `registry search <key>` reply, sorted, each with its forms.
120
+ *
121
+ * The form is kept because the two can come and go separately: on 2026-09-16
122
+ * the Data Fabric `.event.` pair was still served after its `.trigger.` pair
123
+ * was gone, and `prepare` probes the `.trigger.` form.
124
+ */
125
+ export function registryEventIds(payload, connectorKey) {
126
+ if (!isPlainObject(payload))
85
127
  return [];
86
- let payload;
87
- try {
88
- payload = JSON.parse(result.stdout);
89
- }
90
- catch {
128
+ const data = pyOr(payload.Data, []);
129
+ if (!Array.isArray(data))
91
130
  return [];
131
+ const forms = new Map();
132
+ for (const entry of data) {
133
+ if (!isPlainObject(entry))
134
+ continue;
135
+ const nodeType = pyOr(entry.NodeType, "");
136
+ if (typeof nodeType !== "string")
137
+ continue;
138
+ for (const form of ["trigger", "event"]) {
139
+ const prefix = `uipath.connector.${form}.${connectorKey}.`;
140
+ if (!nodeType.startsWith(prefix))
141
+ continue;
142
+ const rest = nodeType.slice(prefix.length);
143
+ if (rest.includes("."))
144
+ continue;
145
+ if (!forms.has(rest))
146
+ forms.set(rest, new Set());
147
+ forms.get(rest).add(form);
148
+ }
149
+ }
150
+ return sortedStrings(forms.keys()).map((id) => ({
151
+ id,
152
+ forms: ["trigger", "event"].filter((form) => forms.get(id).has(form)),
153
+ }));
154
+ }
155
+ /**
156
+ * `registry search <key>`, read for both catalogs. This runs only on the
157
+ * failure path, to tell the author what the catalog does serve.
158
+ */
159
+ export async function registryCatalog(client, connectorKey, timeoutMs) {
160
+ const result = await client.run(registrySearch(connectorKey), timeoutMs === undefined ? undefined : { timeoutMs });
161
+ if (result.code !== 0)
162
+ return { actions: [], events: [], failure: failureReason(result) };
163
+ const payload = findJsonDocument(result.stdout);
164
+ if (payload === undefined) {
165
+ return { actions: [], events: [], failure: `it did not answer JSON: ${tail(result.stdout.trim()) || "no output"}` };
92
166
  }
93
- return registryActionIds(payload, connectorKey);
167
+ return { actions: registryActionIds(payload, connectorKey), events: registryEventIds(payload, connectorKey) };
94
168
  }
@@ -23,10 +23,9 @@
23
23
  */
24
24
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
25
25
  import { pyJsonStringify } from "../codecs/pyjson.js";
26
- import { pyStrip } from "../compat/pystr.js";
27
26
  import { Refusal, messageOf } from "../errors.js";
28
27
  import { isConnectionsList } from "../uip/commands.js";
29
- import { dataItems, parseEnvelope } from "../uip/envelope.js";
28
+ import { dataItems, failureReason, parseEnvelope } from "../uip/envelope.js";
30
29
  import { isPlainObject, notJsonReason, pyGet, pyOr, pyRepr, pyStrAny, pyTruthy } from "./support.js";
31
30
  /** No usable connection, with the reason and the way forward (`_connections.ConnectionError_`). */
32
31
  export class ConnectionRefusal extends Refusal {
@@ -44,7 +43,7 @@ export class ConnectionRefusal extends Refusal {
44
43
  export async function listConnections(client, options) {
45
44
  const result = await client.run(isConnectionsList({ allFolders: options.allFolders, byoa: options.byoa }));
46
45
  if (result.code !== 0) {
47
- throw new ConnectionRefusal(`listing connections failed: ${pyStrip(result.stderr) || "no output"}`, "CONNECTION_LISTING_FAILED");
46
+ throw new ConnectionRefusal(`listing connections failed: ${failureReason(result)}`, "CONNECTION_LISTING_FAILED");
48
47
  }
49
48
  const envelope = parseEnvelope(result.stdout);
50
49
  if (envelope === undefined) {
@@ -1,4 +1,4 @@
1
- import { type BuildContext } from "../library-build/context.js";
1
+ import { type BuildContext, type RegistryMiss } from "../library-build/context.js";
2
2
  import { type EventFacts } from "../library-build/events.js";
3
3
  import { type Dict } from "../library-build/py.js";
4
4
  import type { LibraryEntry } from "../model/library.js";
@@ -87,10 +87,19 @@ export interface EventPrepareResult {
87
87
  filterFields: unknown[];
88
88
  definition: EventDefinition;
89
89
  }
90
+ /** `<key> <action>` is not an event: the `.trigger.` probe missed, and why. */
91
+ export interface EventMiss {
92
+ missed: RegistryMiss;
93
+ }
90
94
  /**
91
- * `_prepare_event`. Returns `undefined` when `<key> <action>` is not an event
92
- * either, so the caller can raise the ordinary unknown-action refusal;
93
- * everything else is handled here, refusals included. `ctx.connectionId` must
94
- * already be the connection: the trigger definition only answers with one.
95
+ * `_prepare_event`. Returns an {@link EventMiss} when `<key> <action>` is not
96
+ * an event either — carrying the probe's reason, so the caller can put it in
97
+ * the one refusal that names both namespaces; everything else is handled here,
98
+ * refusals included. `ctx.connectionId` must already be the connection: the
99
+ * trigger definition only answers with one.
100
+ *
101
+ * The probe is quiet. The Python printed a `WARN: registry get failed for …`
102
+ * here, and the port printed it with an empty reason, because the CLI's reason
103
+ * is on stdout; the reason now travels in the result instead.
95
104
  */
96
- export declare function prepareEvent(ctx: BuildContext, options: EventPrepareOptions): Promise<EventPrepareResult | undefined>;
105
+ export declare function prepareEvent(ctx: BuildContext, options: EventPrepareOptions): Promise<EventPrepareResult | EventMiss>;
@@ -20,7 +20,7 @@
20
20
  * through its reporter, in the Python's order.
21
21
  */
22
22
  import { extractCanonical, nodeOf } from "../library-build/canonical.js";
23
- import { registryGet } from "../library-build/context.js";
23
+ import { registryProbe } from "../library-build/context.js";
24
24
  import { EventDiscoveryError, eventFacts, mergeDescribeIntoNode, triggersDescribe, triggersObjects, unionEventParameters, } from "../library-build/events.js";
25
25
  import { get, isRecord } from "../library-build/py.js";
26
26
  import { planLibrary } from "../library-build/plan.js";
@@ -114,10 +114,15 @@ function fieldsOf(section) {
114
114
  return Array.isArray(fields) ? fields : [];
115
115
  }
116
116
  /**
117
- * `_prepare_event`. Returns `undefined` when `<key> <action>` is not an event
118
- * either, so the caller can raise the ordinary unknown-action refusal;
119
- * everything else is handled here, refusals included. `ctx.connectionId` must
120
- * already be the connection: the trigger definition only answers with one.
117
+ * `_prepare_event`. Returns an {@link EventMiss} when `<key> <action>` is not
118
+ * an event either — carrying the probe's reason, so the caller can put it in
119
+ * the one refusal that names both namespaces; everything else is handled here,
120
+ * refusals included. `ctx.connectionId` must already be the connection: the
121
+ * trigger definition only answers with one.
122
+ *
123
+ * The probe is quiet. The Python printed a `WARN: registry get failed for …`
124
+ * here, and the port printed it with an empty reason, because the CLI's reason
125
+ * is on stdout; the reason now travels in the result instead.
121
126
  */
122
127
  export async function prepareEvent(ctx, options) {
123
128
  const { connectorKey, action, connectionId, out } = options;
@@ -125,13 +130,16 @@ export async function prepareEvent(ctx, options) {
125
130
  const info = (line) => ctx.reporter.info(line);
126
131
  const warn = (line) => ctx.reporter.warn(line);
127
132
  const { trigger: triggerNodeType, event: eventNodeType } = eventNodeTypes(connectorKey, action);
128
- const cacheName = await registryGet(ctx, triggerNodeType);
129
- if (cacheName === undefined)
130
- return undefined;
131
- const payload = ctx.caches.registry.read(cacheName);
133
+ const probe = await registryProbe(ctx, triggerNodeType);
134
+ if ("miss" in probe)
135
+ return { missed: probe.miss };
136
+ const payload = ctx.caches.registry.read(probe.name);
132
137
  const canonical = extractCanonical(nodeOf(payload));
133
- if (canonical === undefined)
134
- return undefined;
138
+ if (canonical === undefined) {
139
+ // An answer, but not one this code can read as a trigger definition. Not
140
+ // "absent", so the caller must not say it is.
141
+ return { missed: { nodeType: triggerNodeType, reason: "the registry answered, but not with a node definition", notFound: false } };
142
+ }
135
143
  if (pyTruthy(options.fields))
136
144
  throw new PrepareRefusal(eventParentFieldsMessage(), "EVENT_NO_PARENT_FIELDS");
137
145
  if (options.allObjects)
@@ -21,6 +21,7 @@
21
21
  * `core/cli-spelling.ts`) without this module reading the environment.
22
22
  */
23
23
  import { Refusal } from "../errors.js";
24
+ import { type RegistryCatalog } from "./actions.js";
24
25
  export declare const PREFIX = "prepare-connector: ";
25
26
  /** The spelling the Python hard-coded into every pasteable re-run. */
26
27
  export declare const PREPARE_COMMAND = "npx flow-sdk registry prepare";
@@ -45,16 +46,43 @@ export declare function connectionLine(row: Record<string, unknown>, connectionI
45
46
  export declare function bindingsLine(symbolicName: string, bindingsPath: string): string;
46
47
  /** `_missing_generic_schema_message`: a Generic action that expanded to nothing. */
47
48
  export declare function missingGenericSchemaMessage(operation: string, objectName?: string | null): string;
49
+ /** One `registry get` probe that missed, as a refusal quotes it. */
50
+ export interface ProbeMiss {
51
+ nodeType: string;
52
+ /** The CLI's reason, never empty (`failureReason`). */
53
+ reason: string;
54
+ }
48
55
  /**
49
- * `_unresolved_action_message`: what to do when the action id is not a
50
- * registry key. `actions` is what `registry search <key>` served (see
51
- * `actions.ts`); the message is pure over it.
56
+ * What to do when `<key> <name>` is neither an action nor an event: both
57
+ * `registry get` probes answered "Node not found". Pure over its inputs.
58
+ *
59
+ * It names both probes, because the author's name may have meant either
60
+ * namespace and the SDK cannot know which (so the code is OPERATION_UNKNOWN,
61
+ * not ACTION_UNKNOWN), and it lists both catalogs `registry search <key>`
62
+ * served. Until 2026-09 this was an action-only message: it named the action
63
+ * node type, said "Re-run with a registry action id" and listed only actions,
64
+ * because `registryActionIds` removes event ids by design. For
65
+ * `uipath-uipath-dataservice record-created` — an EVENT the catalog had stopped
66
+ * serving — that sent the author after action ids (2026-09-23 eval,
67
+ * `skill-flow-datafabric-trigger-lifecycle`, v2 command 33).
52
68
  *
53
69
  * NOT auto-substituted, deliberately: the baked action id and the registry key
54
70
  * are different namespaces, and guessing between them would emit a node both
55
71
  * offline validators accept and the runtime dispatches to the wrong operation.
56
72
  */
57
- export declare function unresolvedActionMessage(connectorKey: string, action: string, nodeType: string, actions: readonly string[]): string;
73
+ export declare function unresolvedOperationMessage(input: {
74
+ connectorKey: string;
75
+ name: string;
76
+ probes: readonly ProbeMiss[];
77
+ catalog: RegistryCatalog;
78
+ }): string;
79
+ /**
80
+ * A `registry get` probe failed for a reason other than "Node not found" — an
81
+ * error inside the CLI, a refusal, a timeout, a reply that is not JSON. That is
82
+ * not evidence the node is absent, so this refusal says so and carries the
83
+ * CLI's own reasons instead of listing alternatives.
84
+ */
85
+ export declare function registryGetFailedMessage(connectorKey: string, name: string, probes: readonly ProbeMiss[]): string;
58
86
  export declare function noCanonicalEntryMessage(nodeType: string): string;
59
87
  /** The `-f` names do not match any api-type ObjectAction; `specs` are the actions' parent lists. */
60
88
  export declare function parentFieldNamesMismatchMessage(given: readonly string[], specs: ReadonlyArray<readonly [string, readonly string[]]>): string;
@@ -21,7 +21,7 @@
21
21
  * `core/cli-spelling.ts`) without this module reading the environment.
22
22
  */
23
23
  import { Refusal, messageOf } from "../errors.js";
24
- import { closestActions } from "./actions.js";
24
+ import { plausibleMatches } from "./actions.js";
25
25
  import { pyGet, pyOr, pyRepr, pyStrAny, pyTruthy } from "./support.js";
26
26
  export const PREFIX = "prepare-connector: ";
27
27
  /** The spelling the Python hard-coded into every pasteable re-run. */
@@ -74,33 +74,98 @@ export function missingGenericSchemaMessage(operation, objectName) {
74
74
  "not need connection-specific fields, use the baked connector descriptor " +
75
75
  "without custom fields instead.");
76
76
  }
77
+ function probeLines(probes) {
78
+ return probes.map((probe) => ` registry get ${probe.nodeType} → ${probe.reason}`);
79
+ }
80
+ /** `record-created (trigger, event)`: an event id with the forms the registry served it in. */
81
+ function eventLabel(event) {
82
+ return `${event.id} (${event.forms.join(", ")})`;
83
+ }
84
+ /** How many of `wanted`'s hyphen tokens `candidate` shares. */
85
+ function sharedTokens(wanted, candidate) {
86
+ const tokens = new Set(candidate.split("-"));
87
+ return [...new Set(wanted.split("-"))].filter((token) => tokens.has(token)).length;
88
+ }
77
89
  /**
78
- * `_unresolved_action_message`: what to do when the action id is not a
79
- * registry key. `actions` is what `registry search <key>` served (see
80
- * `actions.ts`); the message is pure over it.
90
+ * What to do when `<key> <name>` is neither an action nor an event: both
91
+ * `registry get` probes answered "Node not found". Pure over its inputs.
92
+ *
93
+ * It names both probes, because the author's name may have meant either
94
+ * namespace and the SDK cannot know which (so the code is OPERATION_UNKNOWN,
95
+ * not ACTION_UNKNOWN), and it lists both catalogs `registry search <key>`
96
+ * served. Until 2026-09 this was an action-only message: it named the action
97
+ * node type, said "Re-run with a registry action id" and listed only actions,
98
+ * because `registryActionIds` removes event ids by design. For
99
+ * `uipath-uipath-dataservice record-created` — an EVENT the catalog had stopped
100
+ * serving — that sent the author after action ids (2026-09-23 eval,
101
+ * `skill-flow-datafabric-trigger-lifecycle`, v2 command 33).
81
102
  *
82
103
  * NOT auto-substituted, deliberately: the baked action id and the registry key
83
104
  * are different namespaces, and guessing between them would emit a node both
84
105
  * offline validators accept and the runtime dispatches to the wrong operation.
85
106
  */
86
- export function unresolvedActionMessage(connectorKey, action, nodeType, actions) {
107
+ export function unresolvedOperationMessage(input) {
108
+ const { connectorKey: key, name, probes, catalog } = input;
87
109
  const lines = [
88
- `${PREFIX}\`registry get ${nodeType}\` found no such node.`,
110
+ `${PREFIX}\`${key} ${name}\` is neither an action nor an event the registry serves:`,
111
+ ...probeLines(probes),
89
112
  "",
90
- "The baked library's action id is not always the registry's key — a Generic",
91
- "op is `list-all-records` in the library and materializes per object, while",
92
- "the registry serves the un-expanded form. Re-run with a registry action id.",
113
+ // flow-v1 `packages/flow-core/src/manifest/fetchers.ts:286`, `:336-337`: the
114
+ // node catalog is the typecache Search/Catalog, and a trigger node exists
115
+ // only where it serves one.
116
+ "A Flow node exists only if the tenant's Flow node catalog serves it. An event that",
117
+ "`uip is triggers objects` lists but this registry does not cannot be prepared or",
118
+ "compiled, and hand-writing its node JSON does not make it offered.",
93
119
  ];
94
- if (actions.length === 0) {
95
- lines.push("", `\`registry search ${connectorKey}\` returned nothing. If the catalog looks`, "stale, refresh it first: `uip maestro flow registry pull --force`.");
120
+ if (catalog.failure !== undefined) {
121
+ lines.push("", `\`registry search ${key}\` failed: ${catalog.failure}`, "so what the registry does serve for it could not be listed.");
96
122
  return lines.join("\n");
97
123
  }
98
- const close = closestActions(action, actions);
99
- if (close.length > 0)
100
- lines.push("", `Closest to "${action}": ${close.slice(0, 3).join(", ")}`);
101
- lines.push("", `All actions the registry serves for ${connectorKey}:`, ...actions.map((a) => ` ${a}`));
124
+ if (catalog.events.length > 0) {
125
+ const close = plausibleMatches(name, catalog.events.map((event) => event.id));
126
+ const byId = new Map(catalog.events.map((event) => [event.id, event]));
127
+ if (close.length > 0)
128
+ lines.push("", `Closest event to "${name}": ${close.slice(0, 3).map((id) => eventLabel(byId.get(id))).join(", ")}`);
129
+ lines.push("", `Events the registry serves for ${key}:`, ...catalog.events.map((event) => ` ${eventLabel(event)}`));
130
+ }
131
+ else {
132
+ lines.push("", `\`registry search ${key}\` lists no \`uipath.connector.{trigger,event}.${key}.*\` node: the`, `registry serves no event for ${key}.`);
133
+ }
134
+ if (catalog.actions.length > 0) {
135
+ const close = plausibleMatches(name, catalog.actions);
136
+ if (close.length > 0)
137
+ lines.push("", `Closest action to "${name}": ${close.slice(0, 3).join(", ")}`);
138
+ // The Generic explanation only where the name reads like one of these
139
+ // actions (most of its words shared): `list-all-records` against
140
+ // `list-records`, not `record-created` against `create-entity-record`,
141
+ // where it would point an event request at action ids again.
142
+ const wanted = new Set(name.split("-")).size;
143
+ if (close.length > 0 && sharedTokens(name, close[0]) * 2 > wanted) {
144
+ lines.push("The baked library's action id is not always the registry's key — a Generic", "op is `list-all-records` in the library and materializes per object, while", "the registry serves the un-expanded form. For an action, re-run with a registry action id.");
145
+ }
146
+ lines.push("", `All actions the registry serves for ${key}:`, ...catalog.actions.map((a) => ` ${a}`));
147
+ }
148
+ else {
149
+ lines.push("", `\`registry search ${key}\` lists no action for ${key} either.`);
150
+ }
151
+ lines.push("", "`registry get` and `registry search` answer from the CLI's local copy of the catalog.", "If it looks stale, refresh it first: `uip maestro flow registry pull --force`.");
102
152
  return lines.join("\n");
103
153
  }
154
+ /**
155
+ * A `registry get` probe failed for a reason other than "Node not found" — an
156
+ * error inside the CLI, a refusal, a timeout, a reply that is not JSON. That is
157
+ * not evidence the node is absent, so this refusal says so and carries the
158
+ * CLI's own reasons instead of listing alternatives.
159
+ */
160
+ export function registryGetFailedMessage(connectorKey, name, probes) {
161
+ return [
162
+ `${PREFIX}could not tell whether \`${connectorKey} ${name}\` is an action or an event: a registry`,
163
+ "lookup failed, which does not mean the node is absent.",
164
+ ...probeLines(probes),
165
+ "",
166
+ "Resolve the failure above and re-run prepare.",
167
+ ].join("\n");
168
+ }
104
169
  export function noCanonicalEntryMessage(nodeType) {
105
170
  return `${PREFIX}no canonical entry for ${nodeType} (unexpected registry shape).`;
106
171
  }
@@ -28,17 +28,17 @@ import { resolve } from "node:path";
28
28
  import { PACKAGE_NAME } from "../../package-name.js";
29
29
  import { afterLast } from "../compat/pystr.js";
30
30
  import { extractCanonical, isHttpPassthrough, nodeOf } from "../library-build/canonical.js";
31
- import { createBuildContext, isResourcesList, registryGet } from "../library-build/context.js";
31
+ import { createBuildContext, isResourcesList, registryProbe } from "../library-build/context.js";
32
32
  import { enrichEntry } from "../library-build/enrich.js";
33
33
  import { expandGenericForConnector, genericTemplatesByOp } from "../library-build/generic-expand.js";
34
34
  import { planLibrary } from "../library-build/plan.js";
35
35
  import { MemoryResponseCache } from "../uip/response-cache.js";
36
- import { registryActions } from "./actions.js";
36
+ import { registryCatalog } from "./actions.js";
37
37
  import { discoverConnection, lookupConnection, mergeBindings, restoreBindings, snapshotBindings, } from "./connections.js";
38
38
  import { prepareEvent } from "./event.js";
39
39
  import { descriptorUsage } from "./handoff.js";
40
40
  import { ResolutionsExhausted, allParentsResolved, resolvedParentValues, writeResolutions } from "./ledger.js";
41
- import { PREPARE_COMMAND, PrepareRefusal, bindingsLine, candidateLabel, connectionLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, keptSchemaMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, tsNote, unresolvedActionMessage, } from "./messages.js";
41
+ import { PREPARE_COMMAND, PrepareRefusal, bindingsLine, candidateLabel, connectionLine, generatedModulesLine, genericActionNeedsObjectMessage, inputFieldNote, keptSchemaMessage, lookupVocabularyLines, missingGenericSchemaMessage, missingParentsNote, nextLine, noCanonicalEntryMessage, objectFlagsOnlyForGenericMessage, objectNoteLine, objectRecordedLine, parentFieldNamesMismatchMessage, parentOnlyMessage, prefixed, preparedSummaryLine, rejectedParentFieldsMessage, rerunAgainstLine, resolvedParentsNote, registryGetFailedMessage, tsNote, unresolvedOperationMessage, } from "./messages.js";
42
42
  import { ObjectMatchRefusal, RESOLUTIONS_FILENAME, matchObject, recordObjectResolution } from "./objects.js";
43
43
  import { countInputFields, keepPreparedSchema, lookupSpecsFromOverlay, mergeFilesInto, parentFields as overlayParentFields, schemaActionParents, } from "./overlay.js";
44
44
  import { declaredParents, objectActionRequest, objectActionSpecs, parseFields } from "./parents.js";
@@ -207,8 +207,8 @@ async function prepareOnConnection(options, ports, connected) {
207
207
  const { connectionId, bound, rollback } = connected;
208
208
  const ctx = createBuildContext({ client: ports.client, caches: ports.caches ?? memoryCaches(), reporter: ports.reporter, concurrency: 1, timeoutMs: ports.timeoutMs }, { connectionId, parentFields });
209
209
  const nodeType = `uipath.connector.${connectorKey}.${action}`;
210
- const cacheName = await registryGet(ctx, nodeType, true);
211
- if (cacheName === undefined) {
210
+ const actionProbe = await registryProbe(ctx, nodeType);
211
+ if ("miss" in actionProbe) {
212
212
  // Not an activity. Connector EVENTS live one namespace over, and their
213
213
  // registry definition only answers with a connection, which this command
214
214
  // has by now. Same loop, different twin pair.
@@ -230,13 +230,19 @@ async function prepareOnConnection(options, ports, connected) {
230
230
  bindings: rollback,
231
231
  prepareCommand: command,
232
232
  });
233
- if (event !== undefined) {
233
+ if (!("missed" in event)) {
234
234
  return { kind: "event", nodeType: event.eventNodeType, connectionId, bound, objectName: event.objectName };
235
235
  }
236
- const actions = await registryActions(ports.client, connectorKey, ports.timeoutMs);
237
- throw new PrepareRefusal(unresolvedActionMessage(connectorKey, action, nodeType, actions), "ACTION_UNKNOWN");
236
+ const probes = [actionProbe.miss, event.missed];
237
+ // Only the CLI's "Node not found" says a node is absent. Any other failure
238
+ // of either lookup is reported as the failure it is, never as a verdict.
239
+ if (probes.some((probe) => !probe.notFound)) {
240
+ throw new PrepareRefusal(registryGetFailedMessage(connectorKey, action, probes), "REGISTRY_GET_FAILED");
241
+ }
242
+ const catalog = await registryCatalog(ports.client, connectorKey, ports.timeoutMs);
243
+ throw new PrepareRefusal(unresolvedOperationMessage({ connectorKey, name: action, probes, catalog }), "OPERATION_UNKNOWN");
238
244
  }
239
- const payload = ctx.caches.registry.read(cacheName);
245
+ const payload = ctx.caches.registry.read(actionProbe.name);
240
246
  // The registry's own declaration of parent inputs, read from the same reply.
241
247
  const declared = declaredParents(payload);
242
248
  let entry = extractCanonical(nodeOf(payload));
@@ -22,6 +22,7 @@ import { pyFloatRepr } from "../codecs/pyjson.js";
22
22
  import { pyStrip, pyStripChars, splitOnce } from "../compat/pystr.js";
23
23
  import { Refusal } from "../errors.js";
24
24
  import { isResourcesRunList } from "../uip/commands.js";
25
+ import { failureReason } from "../uip/envelope.js";
25
26
  import { isPlainObject, notJsonReason, pyOr, pyRepr, pyStrAny, pyTruthy } from "./support.js";
26
27
  /**
27
28
  * A scan has to stop somewhere, and the caps must be set against the MEASURED
@@ -361,7 +362,7 @@ export function parseListingDocument(text) {
361
362
  export async function listPage(client, connectorKey, collection, connectionId, query) {
362
363
  const result = await client.run(isResourcesRunList(connectorKey, collection, connectionId, query));
363
364
  if (result.code !== 0) {
364
- throw new ResolutionRefusal(`listing ${collection} failed: ${pyStrip(result.stderr) || "no output"}`, "RESOLVE_LISTING_FAILED");
365
+ throw new ResolutionRefusal(`listing ${collection} failed: ${failureReason(result)}`, "RESOLVE_LISTING_FAILED");
365
366
  }
366
367
  const envelope = parseListingDocument(result.stdout);
367
368
  if (!isPlainObject(envelope)) {