@uipath/maestro-builder-sdk 6.16.14 → 6.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/dist/api-index.md +115 -114
  3. package/dist/api-members.md +295 -291
  4. package/dist/bindings-v2.d.ts +39 -1
  5. package/dist/bindings-v2.js +289 -9
  6. package/dist/bpmn/compile-cli.js +11 -2
  7. package/dist/bpmn/decompile.js +2 -1
  8. package/dist/bpmn/registry-fallback.js +9 -7
  9. package/dist/case/compile-cli.js +16 -6
  10. package/dist/case/decompile-cli.js +2 -1
  11. package/dist/case/decompile.js +3 -2
  12. package/dist/case/merge-cli.js +3 -2
  13. package/dist/check-cli.js +2 -30
  14. package/dist/check.js +278 -55
  15. package/dist/cli-library.d.ts +18 -4
  16. package/dist/cli-library.js +24 -6
  17. package/dist/cli-result.d.ts +9 -0
  18. package/dist/cli-result.js +12 -0
  19. package/dist/compile-cli.js +82 -86
  20. package/dist/core/actions.d.ts +71 -17
  21. package/dist/core/agentic-process-forms.d.ts +51 -0
  22. package/dist/core/agentic-process-forms.js +49 -0
  23. package/dist/core/binding-messages.d.ts +5 -0
  24. package/dist/core/cli-library.d.ts +16 -7
  25. package/dist/core/cli-library.js +53 -19
  26. package/dist/core/comment-text.d.ts +29 -0
  27. package/dist/core/comment-text.js +40 -0
  28. package/dist/core/connector-checks.d.ts +18 -2
  29. package/dist/core/connector-checks.js +15 -5
  30. package/dist/core/contribution-bindings.d.ts +73 -0
  31. package/dist/core/contribution-bindings.js +105 -0
  32. package/dist/core/error-port.d.ts +25 -0
  33. package/dist/core/error-port.js +36 -0
  34. package/dist/core/expr-check.js +7 -2
  35. package/dist/core/expr.d.ts +11 -5
  36. package/dist/core/expr.js +9 -3
  37. package/dist/core/json-file.d.ts +1 -0
  38. package/dist/core/json-file.js +12 -0
  39. package/dist/core/library.d.ts +19 -0
  40. package/dist/core/library.js +22 -6
  41. package/dist/core/script-return.js +4 -1
  42. package/dist/core/typed-raw-node.js +1 -0
  43. package/dist/core/typescript-module.d.ts +2 -0
  44. package/dist/core/typescript-module.js +18 -0
  45. package/dist/core-definitions.json +210 -0
  46. package/dist/decompile-cli.js +2 -1
  47. package/dist/decompile.js +64 -24
  48. package/dist/emit.js +4 -3
  49. package/dist/flow-sdk.d.ts +18 -12
  50. package/dist/flow-sdk.js +19 -8
  51. package/dist/flow-step-tree.d.ts +2 -3
  52. package/dist/flow-step-tree.js +2 -3
  53. package/dist/load.js +1 -1
  54. package/dist/merge-cli.js +3 -2
  55. package/dist/merge.d.ts +2 -1
  56. package/dist/merge.js +0 -0
  57. package/dist/path-within.d.ts +33 -0
  58. package/dist/path-within.js +41 -0
  59. package/dist/registry/archive/extract.js +2 -5
  60. package/dist/registry/curated-overlay.js +3 -2
  61. package/dist/registry/generators/descriptors.js +20 -18
  62. package/dist/registry/generators/triggers.js +4 -4
  63. package/dist/registry/prepare/messages.js +2 -1
  64. package/dist/registry/prepare/source-labels.js +60 -1
  65. package/dist/registry/prepare-cli.js +14 -4
  66. package/dist/registry/pull-cli.js +3 -2
  67. package/dist/registry/uip/client.d.ts +28 -3
  68. package/dist/registry/uip/client.js +95 -34
  69. package/dist/registry/uip/host.d.ts +41 -0
  70. package/dist/registry/uip/host.js +312 -0
  71. package/dist/serialize.d.ts +7 -0
  72. package/dist/serialize.js +414 -277
  73. package/dist/workdir.js +1 -1
  74. package/package.json +3 -3
@@ -1,3 +1,4 @@
1
+ import { type HostUipClientOptions } from "./host.js";
1
2
  export interface UipRunOptions {
2
3
  /** Working directory for the CLI; defaults to the process's. */
3
4
  cwd?: string;
@@ -23,6 +24,12 @@ export interface UipResult {
23
24
  export interface UipClient {
24
25
  /** Run `argv[0] argv.slice(1)` — the first element is the executable, normally `uip`. */
25
26
  run(argv: readonly string[], options?: UipRunOptions): Promise<UipResult>;
27
+ /**
28
+ * Release whatever the client holds open — the one long-lived `uip` of
29
+ * {@link HostUipClient}. Optional because most clients hold nothing; a
30
+ * command that may have been given one calls `client.close?.()` when done.
31
+ */
32
+ close?(): void;
26
33
  }
27
34
  /** The real thing: spawn the CLI, capture both streams, never throw on a non-zero exit. */
28
35
  export declare class SpawnUipClient implements UipClient {
@@ -50,6 +57,7 @@ export declare class RecordingUipClient implements UipClient {
50
57
  private readonly directory;
51
58
  constructor(inner: UipClient, directory: string);
52
59
  run(argv: readonly string[], options?: UipRunOptions): Promise<UipResult>;
60
+ close(): void;
53
61
  }
54
62
  /**
55
63
  * A client that runs nothing and reports every call as refused.
@@ -93,7 +101,24 @@ export declare class OfflineUipClient implements UipClient {
93
101
  *
94
102
  * Both exist to capture a fixture from a real tenant and to replay one — that
95
103
  * is how `fixtures/library-build/` was taken — and neither is meant to be set
96
- * in a pipeline. They are read here and nowhere else, so this is the only place
97
- * an operator can be told what they do.
104
+ * in a pipeline. They are read by `uipSwitches()` here, for this and
105
+ * {@link runUipSync}, and nowhere else, so this is the only place an operator can
106
+ * be told what they do.
107
+ *
108
+ * A third, `FLOW_SDK_UIP_HOST=0`, turns off the one-process host a caller may
109
+ * ask for with `host` (see `host.ts`): every call then spawns its own `uip`,
110
+ * which is how the registry ran before flow-builder-sdk#965 and the shape to
111
+ * compare against when a prepare misbehaves.
112
+ */
113
+ export declare function defaultUipClient(options?: UipRunOptions & {
114
+ host?: HostUipClientOptions | boolean;
115
+ }): UipClient;
116
+ /**
117
+ * Run `uip` synchronously, for the one caller that cannot await:
118
+ * `bpmn/registry-fallback.ts` resolves an extension type while a builder method runs.
119
+ * Same switches as {@link defaultUipClient} (`UIP_REPLAY_DIR` answers from recordings and
120
+ * throws on an unrecorded call; `UIP_RECORD_DIR` writes each result down) and the same
121
+ * contract as {@link SpawnUipClient}: no stdin, a non-zero exit or a timeout is a result,
122
+ * a missing executable throws the install instruction.
98
123
  */
99
- export declare function defaultUipClient(options?: UipRunOptions): UipClient;
124
+ export declare function runUipSync(argv: readonly string[], options?: UipRunOptions): UipResult;
@@ -1,28 +1,30 @@
1
- /**
2
- * Running `uip`, and standing in for it.
3
- *
4
- * Everything in the registry that talks to the tenant goes through a
5
- * {@link UipClient}. The real one spawns the CLI with a timeout and UTF-8
6
- * decoding. The replay one serves envelopes captured earlier, keyed by the
7
- * exact argv, and fails loudly on a call nobody recorded — which is how prepare
8
- * and the library builder are tested without a tenant, and how the same
9
- * recording drives a byte-for-byte comparison against the Python they replace.
10
- *
11
- * Nothing here judges the response. Whether a non-zero exit is fatal, or a
12
- * `Result: Failure` envelope should be cached, is the caller's rule — the
13
- * Python callers disagreed with each other on purpose, and those rules stay
14
- * beside the call sites that own them.
15
- */
16
- import { spawn } from "node:child_process";
17
1
  import { createHash } from "node:crypto";
18
- import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs";
2
+ import { mkdirSync, readFileSync, existsSync } from "node:fs";
3
+ import { createRequire } from "node:module";
19
4
  import { join } from "node:path";
5
+ import { writeJsonFile } from "../../core/json-file.js";
6
+ import { HostUipClient } from "./host.js";
20
7
  /**
21
8
  * Room for the largest `registry search` a tenant has answered, with margin.
22
9
  * `execFile`'s 1 MiB default cut that stream off, so the port raised it; a
23
10
  * child that exceeds even this is killed, which is what `execFile` did too.
24
11
  */
25
12
  const MAX_OUTPUT_BYTES = 256 * 1024 * 1024;
13
+ let childProcessModule;
14
+ /**
15
+ * `node:child_process`, loaded on first use. `bpmn/registry-fallback.ts` imports this
16
+ * module, so the `/bpmn` and root builder entries reach it; a builder import must not
17
+ * load a process API it may never use (FB009). The specifier stays a literal so the
18
+ * architecture lint sees the edge.
19
+ */
20
+ function childProcess() {
21
+ return (childProcessModule ??= createRequire(import.meta.url)("node:child_process"));
22
+ }
23
+ /** The error for a `uip` that is not there: what to install, not just ENOENT. */
24
+ function cannotStart(executable, error) {
25
+ return new Error(`Could not start ${executable}: ${error.message}. The registry verbs call the ` +
26
+ "UiPath CLI; install it and log in, or put it on PATH.");
27
+ }
26
28
  /** The real thing: spawn the CLI, capture both streams, never throw on a non-zero exit. */
27
29
  export class SpawnUipClient {
28
30
  defaults;
@@ -40,7 +42,7 @@ export class SpawnUipClient {
40
42
  // reads EOF at once and the CLI fails like any other call, which is
41
43
  // what Python's `subprocess.run` gave it: the parent's stdin, already at
42
44
  // EOF on an agent.
43
- const child = spawn(executable, args, {
45
+ const child = childProcess().spawn(executable, args, {
44
46
  cwd: options.cwd ?? this.defaults.cwd,
45
47
  stdio: ["ignore", "pipe", "pipe"],
46
48
  });
@@ -70,8 +72,7 @@ export class SpawnUipClient {
70
72
  if (timer !== undefined)
71
73
  clearTimeout(timer);
72
74
  if (error.code === "ENOENT") {
73
- reject(new Error(`Could not start ${executable}: ${error.message}. The registry verbs call the ` +
74
- "UiPath CLI; install it and log in, or put it on PATH."));
75
+ reject(cannotStart(executable, error));
75
76
  return;
76
77
  }
77
78
  // Every other spawn failure — EACCES on the binary, EAGAIN with no
@@ -118,6 +119,21 @@ export function recordingName(argv) {
118
119
  .slice(0, 60);
119
120
  return `${slug}-${digest}.json`;
120
121
  }
122
+ /** The recording for `argv` in `directory`; throws, naming the argv and the file, when there is none. */
123
+ function readRecording(directory, argv) {
124
+ const path = join(directory, recordingName(argv));
125
+ if (!existsSync(path)) {
126
+ throw new Error(`ReplayUipClient: no recording for\n ${argv.join(" ")}\n(expected ${path}). ` +
127
+ "Record it with RecordingUipClient, or the call is one the code under test should not make. " +
128
+ "A build replays only when UIP_REPLAY_DIR is set; if that was not intended, unset it.");
129
+ }
130
+ return JSON.parse(readFileSync(path, "utf8"));
131
+ }
132
+ /** Write `result` down as the recording for `argv` in `directory`. */
133
+ function writeRecording(directory, argv, result) {
134
+ mkdirSync(directory, { recursive: true });
135
+ writeJsonFile(join(directory, recordingName(argv)), { argv: [...argv], result });
136
+ }
121
137
  /**
122
138
  * Serve recorded results; refuse anything unrecorded.
123
139
  *
@@ -134,13 +150,7 @@ export class ReplayUipClient {
134
150
  const name = recordingName(argv);
135
151
  let recording = this.byName.get(name);
136
152
  if (!recording) {
137
- const path = join(this.directory, name);
138
- if (!existsSync(path)) {
139
- throw new Error(`ReplayUipClient: no recording for\n ${argv.join(" ")}\n(expected ${path}). ` +
140
- "Record it with RecordingUipClient, or the call is one the code under test should not make. " +
141
- "A build replays only when UIP_REPLAY_DIR is set; if that was not intended, unset it.");
142
- }
143
- recording = JSON.parse(readFileSync(path, "utf8"));
153
+ recording = readRecording(this.directory, argv);
144
154
  this.byName.set(name, recording);
145
155
  }
146
156
  return { ...recording.result };
@@ -157,10 +167,12 @@ export class RecordingUipClient {
157
167
  }
158
168
  async run(argv, options) {
159
169
  const result = await this.inner.run(argv, options);
160
- const recording = { argv: [...argv], result };
161
- writeFileSync(join(this.directory, recordingName(argv)), `${JSON.stringify(recording, null, 2)}\n`);
170
+ writeRecording(this.directory, argv, result);
162
171
  return result;
163
172
  }
173
+ close() {
174
+ this.inner.close?.();
175
+ }
164
176
  }
165
177
  /**
166
178
  * A client that runs nothing and reports every call as refused.
@@ -213,14 +225,63 @@ export class OfflineUipClient {
213
225
  *
214
226
  * Both exist to capture a fixture from a real tenant and to replay one — that
215
227
  * is how `fixtures/library-build/` was taken — and neither is meant to be set
216
- * in a pipeline. They are read here and nowhere else, so this is the only place
217
- * an operator can be told what they do.
228
+ * in a pipeline. They are read by `uipSwitches()` here, for this and
229
+ * {@link runUipSync}, and nowhere else, so this is the only place an operator can
230
+ * be told what they do.
231
+ *
232
+ * A third, `FLOW_SDK_UIP_HOST=0`, turns off the one-process host a caller may
233
+ * ask for with `host` (see `host.ts`): every call then spawns its own `uip`,
234
+ * which is how the registry ran before flow-builder-sdk#965 and the shape to
235
+ * compare against when a prepare misbehaves.
218
236
  */
219
237
  export function defaultUipClient(options = {}) {
220
- const replay = process.env.UIP_REPLAY_DIR;
238
+ const { replay, record } = uipSwitches();
221
239
  if (replay)
222
240
  return new ReplayUipClient(replay);
223
- const client = new SpawnUipClient(options);
224
- const record = process.env.UIP_RECORD_DIR;
241
+ const { host, ...runOptions } = options;
242
+ const spawning = new SpawnUipClient(runOptions);
243
+ // One `uip mcp serve` for the command's whole chain of calls, where the
244
+ // caller asked for it and the operator has not switched it off. Serial, so
245
+ // not for the library builder, whose `concurrency` runs calls side by side.
246
+ const client = host && process.env.FLOW_SDK_UIP_HOST !== "0"
247
+ ? new HostUipClient(spawning, typeof host === "object" ? host : {})
248
+ : spawning;
225
249
  return record ? new RecordingUipClient(client, record) : client;
226
250
  }
251
+ /** The replay and record switches (see {@link defaultUipClient}); an empty value is unset. */
252
+ function uipSwitches() {
253
+ return { replay: process.env.UIP_REPLAY_DIR || undefined, record: process.env.UIP_RECORD_DIR || undefined };
254
+ }
255
+ /**
256
+ * Run `uip` synchronously, for the one caller that cannot await:
257
+ * `bpmn/registry-fallback.ts` resolves an extension type while a builder method runs.
258
+ * Same switches as {@link defaultUipClient} (`UIP_REPLAY_DIR` answers from recordings and
259
+ * throws on an unrecorded call; `UIP_RECORD_DIR` writes each result down) and the same
260
+ * contract as {@link SpawnUipClient}: no stdin, a non-zero exit or a timeout is a result,
261
+ * a missing executable throws the install instruction.
262
+ */
263
+ export function runUipSync(argv, options = {}) {
264
+ const { replay, record } = uipSwitches();
265
+ if (replay)
266
+ return { ...readRecording(replay, argv).result };
267
+ const [executable, ...args] = argv;
268
+ const r = childProcess().spawnSync(executable, args, {
269
+ cwd: options.cwd,
270
+ stdio: ["ignore", "pipe", "pipe"],
271
+ encoding: "utf8",
272
+ timeout: options.timeoutMs,
273
+ killSignal: "SIGKILL",
274
+ maxBuffer: MAX_OUTPUT_BYTES,
275
+ });
276
+ const errno = r.error?.code;
277
+ if (errno === "ENOENT")
278
+ throw cannotStart(executable, r.error);
279
+ if (r.error && errno !== "ETIMEDOUT" && errno !== "ENOBUFS")
280
+ throw r.error;
281
+ const result = {
282
+ code: r.status, signal: r.signal, stdout: r.stdout ?? "", stderr: r.stderr ?? "", timedOut: errno === "ETIMEDOUT",
283
+ };
284
+ if (record)
285
+ writeRecording(record, argv, result);
286
+ return result;
287
+ }
@@ -0,0 +1,41 @@
1
+ import type { UipClient, UipResult, UipRunOptions } from "./client.js";
2
+ /** The MCP tool this client calls; a server without it is treated as absent. */
3
+ export declare const HOST_TOOL = "run_argv";
4
+ export interface HostUipClientOptions {
5
+ /**
6
+ * The command that starts the host for `executable` (`argv[0]`, normally
7
+ * `uip`). Defaults to `[executable, "mcp", "serve"]`; tests point it at a
8
+ * fake server.
9
+ */
10
+ hostCommand?: (executable: string) => string[];
11
+ startTimeoutMs?: number;
12
+ /** Where to say why the host was given up on; silent by default. */
13
+ onFallback?: (reason: string) => void;
14
+ }
15
+ /**
16
+ * Run `uip` calls through one `uip mcp serve` process, falling back to
17
+ * `fallback` (the spawning client) whenever the host cannot serve a call.
18
+ */
19
+ export declare class HostUipClient implements UipClient {
20
+ private readonly fallback;
21
+ private readonly options;
22
+ private host;
23
+ private starting;
24
+ private disabled;
25
+ private queue;
26
+ constructor(fallback: UipClient, options?: HostUipClientOptions);
27
+ run(argv: readonly string[], options?: UipRunOptions): Promise<UipResult>;
28
+ /** Shut the host down. Calls still in flight finish first; later calls start a new one. */
29
+ close(): void;
30
+ private runSerially;
31
+ private ensureHost;
32
+ private start;
33
+ /**
34
+ * Spawn per call from here on, silently. This is the path for a CLI whose
35
+ * server predates `run_argv`: a prepare on it works exactly as it did
36
+ * before this client existed, so there is nothing to tell the user.
37
+ */
38
+ private spawnFromNowOn;
39
+ /** Spawn per call from here on, and say why: the host failed. */
40
+ private giveUp;
41
+ }
@@ -0,0 +1,312 @@
1
+ import { createRequire } from "node:module";
2
+ /** The MCP tool this client calls; a server without it is treated as absent. */
3
+ export const HOST_TOOL = "run_argv";
4
+ /** How long the host may take to start and list its tools before the client gives up on it. */
5
+ const DEFAULT_START_TIMEOUT_MS = 20_000;
6
+ /** Stderr the host printed, kept for the message when it dies. */
7
+ const STDERR_KEEP = 4_000;
8
+ /** Thrown inside the client when a call's timeout elapsed; never escapes `run`. */
9
+ class HostTimeout extends Error {
10
+ }
11
+ /** Thrown inside the client when the host or its protocol failed; never escapes `run`. */
12
+ class HostFailure extends Error {
13
+ }
14
+ /**
15
+ * `ref`/`unref` a child's stdio pipe. The streams are typed as plain
16
+ * `Readable`/`Writable`, but a piped stdio stream is a `net.Socket`, which
17
+ * has both; a stream without them (a test's fake) is simply left alone.
18
+ */
19
+ function refStream(stream, on) {
20
+ const socket = stream;
21
+ if (!socket)
22
+ return;
23
+ if (on)
24
+ socket.ref?.();
25
+ else
26
+ socket.unref?.();
27
+ }
28
+ /** A live `uip mcp serve` process and the JSON-RPC conversation with it. */
29
+ class Host {
30
+ child;
31
+ pending = new Map();
32
+ nextId = 1;
33
+ buffer = "";
34
+ stderr = "";
35
+ closed = false;
36
+ /** Set when the client killed the host on purpose, so `close` is not a failure. */
37
+ killedForTimeout = false;
38
+ exitHook;
39
+ constructor(child) {
40
+ this.child = child;
41
+ child.stdout.setEncoding("utf8");
42
+ child.stdout.on("data", (chunk) => this.onData(chunk));
43
+ child.stderr.setEncoding("utf8");
44
+ child.stderr.on("data", (chunk) => {
45
+ this.stderr = (this.stderr + chunk).slice(-STDERR_KEEP);
46
+ });
47
+ child.on("error", (error) => this.fail(new HostFailure(`uip mcp serve: ${error.message}`)));
48
+ child.on("close", (code, signal) => {
49
+ this.closed = true;
50
+ process.removeListener("exit", this.exitHook);
51
+ const why = this.killedForTimeout
52
+ ? new HostTimeout("killed after a call's timeout")
53
+ : new HostFailure(`uip mcp serve exited (code ${code}, signal ${signal})` + (this.stderr.trim() ? `: ${this.stderr.trim()}` : ""));
54
+ this.fail(why);
55
+ });
56
+ // stdin is written only by us; an error there (EPIPE after the server
57
+ // died) is reported through `close`, not as an unhandled event.
58
+ child.stdin.on("error", () => undefined);
59
+ this.exitHook = () => {
60
+ if (!this.closed)
61
+ child.kill("SIGKILL");
62
+ };
63
+ process.once("exit", this.exitHook);
64
+ this.idle();
65
+ }
66
+ get alive() {
67
+ return !this.closed;
68
+ }
69
+ /**
70
+ * Let the SDK process exit while the host sits idle: with nothing in
71
+ * flight nothing here holds the event loop, and when the parent exits the
72
+ * server reads EOF on its stdin and shuts down. A call in flight `ref`s the
73
+ * pipes again, so the loop waits for its answer.
74
+ */
75
+ idle() {
76
+ this.child.unref();
77
+ for (const stream of [this.child.stdout, this.child.stderr, this.child.stdin])
78
+ refStream(stream, false);
79
+ }
80
+ busy() {
81
+ this.child.ref();
82
+ for (const stream of [this.child.stdout, this.child.stderr, this.child.stdin])
83
+ refStream(stream, true);
84
+ }
85
+ onData(chunk) {
86
+ this.buffer += chunk;
87
+ let newline = this.buffer.indexOf("\n");
88
+ while (newline >= 0) {
89
+ const line = this.buffer.slice(0, newline).trim();
90
+ this.buffer = this.buffer.slice(newline + 1);
91
+ if (line !== "")
92
+ this.onLine(line);
93
+ newline = this.buffer.indexOf("\n");
94
+ }
95
+ }
96
+ onLine(line) {
97
+ let message;
98
+ try {
99
+ message = JSON.parse(line);
100
+ }
101
+ catch {
102
+ // Something other than JSON-RPC on the server's stdout — a command that
103
+ // wrote to the real stream instead of its sink. The framing is broken
104
+ // from here on, so every call in flight fails and the client falls back.
105
+ this.fail(new HostFailure(`uip mcp serve wrote something other than JSON-RPC: ${line.slice(0, 200)}`));
106
+ return;
107
+ }
108
+ // A notification has no id; a server-to-client REQUEST (`ping`) has an id
109
+ // of its own numbering and a `method`. Neither is an answer to us, and a
110
+ // request whose id happened to equal a pending call's would otherwise be
111
+ // read as that call's reply and fail it.
112
+ if (typeof message.id !== "number" || "method" in message)
113
+ return;
114
+ const waiting = this.pending.get(message.id);
115
+ if (!waiting)
116
+ return;
117
+ this.pending.delete(message.id);
118
+ if (this.pending.size === 0)
119
+ this.idle();
120
+ waiting.resolve(message);
121
+ }
122
+ /** Reject every call in flight; later calls find `alive` false. */
123
+ fail(error) {
124
+ const waiting = [...this.pending.values()];
125
+ this.pending.clear();
126
+ this.idle();
127
+ for (const entry of waiting)
128
+ entry.reject(error);
129
+ }
130
+ notify(method, params) {
131
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", method, params })}\n`);
132
+ }
133
+ request(method, params, timeoutMs) {
134
+ if (this.closed)
135
+ return Promise.reject(new HostFailure("uip mcp serve is not running"));
136
+ const id = this.nextId++;
137
+ this.busy();
138
+ return new Promise((resolve, reject) => {
139
+ let timer;
140
+ const settle = (fn) => (value) => {
141
+ if (timer !== undefined)
142
+ clearTimeout(timer);
143
+ fn(value);
144
+ };
145
+ this.pending.set(id, { resolve: settle(resolve), reject: settle(reject) });
146
+ if (timeoutMs !== undefined && timeoutMs > 0) {
147
+ timer = setTimeout(() => {
148
+ // A command cannot be cancelled inside the host, so the host goes.
149
+ // `close` then fails this call with the timeout the flag records.
150
+ this.killedForTimeout = true;
151
+ this.child.kill("SIGKILL");
152
+ }, timeoutMs);
153
+ }
154
+ this.child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id, method, params })}\n`);
155
+ });
156
+ }
157
+ /** End the conversation: the server treats stdin EOF as shutdown. */
158
+ close() {
159
+ process.removeListener("exit", this.exitHook);
160
+ if (this.closed)
161
+ return;
162
+ this.child.stdin.end();
163
+ // If it ignores EOF, it does not get to outlive the command.
164
+ const grace = setTimeout(() => {
165
+ if (!this.closed)
166
+ this.child.kill("SIGKILL");
167
+ }, 2_000);
168
+ grace.unref();
169
+ }
170
+ kill() {
171
+ process.removeListener("exit", this.exitHook);
172
+ if (!this.closed)
173
+ this.child.kill("SIGKILL");
174
+ }
175
+ }
176
+ /**
177
+ * Run `uip` calls through one `uip mcp serve` process, falling back to
178
+ * `fallback` (the spawning client) whenever the host cannot serve a call.
179
+ */
180
+ export class HostUipClient {
181
+ fallback;
182
+ options;
183
+ host;
184
+ starting;
185
+ disabled = false;
186
+ queue = Promise.resolve();
187
+ constructor(fallback, options = {}) {
188
+ this.fallback = fallback;
189
+ this.options = options;
190
+ }
191
+ run(argv, options = {}) {
192
+ if (this.disabled || options.cwd !== undefined)
193
+ return this.fallback.run(argv, options);
194
+ const task = this.queue.then(() => this.runSerially(argv, options));
195
+ this.queue = task.then(() => undefined, () => undefined);
196
+ return task;
197
+ }
198
+ /** Shut the host down. Calls still in flight finish first; later calls start a new one. */
199
+ close() {
200
+ const host = this.host;
201
+ this.host = undefined;
202
+ if (host)
203
+ this.queue.then(() => host.close(), () => host.close());
204
+ }
205
+ async runSerially(argv, options) {
206
+ if (this.disabled)
207
+ return this.fallback.run(argv, options);
208
+ const host = await this.ensureHost(argv[0]);
209
+ if (!host)
210
+ return this.fallback.run(argv, options);
211
+ try {
212
+ const response = await host.request("tools/call", { name: HOST_TOOL, arguments: { args: argv.slice(1) } }, options.timeoutMs);
213
+ return toResult(response);
214
+ }
215
+ catch (error) {
216
+ if (error instanceof HostTimeout) {
217
+ this.host = undefined;
218
+ return { code: null, signal: "SIGKILL", stdout: "", stderr: "", timedOut: true };
219
+ }
220
+ this.giveUp(error instanceof Error ? error.message : String(error));
221
+ return this.fallback.run(argv, options);
222
+ }
223
+ }
224
+ async ensureHost(executable) {
225
+ if (this.host?.alive)
226
+ return this.host;
227
+ this.host = undefined;
228
+ if (!this.starting) {
229
+ this.starting = this.start(executable).finally(() => {
230
+ this.starting = undefined;
231
+ });
232
+ }
233
+ return this.starting;
234
+ }
235
+ async start(executable) {
236
+ const command = (this.options.hostCommand ?? ((exe) => [exe, "mcp", "serve"]))(executable);
237
+ const startTimeoutMs = this.options.startTimeoutMs ?? DEFAULT_START_TIMEOUT_MS;
238
+ let child;
239
+ try {
240
+ // Loaded here, not at import: `client.ts` imports this module and the builder
241
+ // entries reach `client.ts`, so a static import would load a process API on
242
+ // every builder import (FB009).
243
+ const { spawn } = createRequire(import.meta.url)("node:child_process");
244
+ child = spawn(command[0], command.slice(1), { stdio: ["pipe", "pipe", "pipe"] });
245
+ }
246
+ catch (error) {
247
+ this.giveUp(`could not start ${command.join(" ")}: ${error instanceof Error ? error.message : String(error)}`);
248
+ return undefined;
249
+ }
250
+ const host = new Host(child);
251
+ try {
252
+ const initialized = await host.request("initialize", {
253
+ protocolVersion: "2024-11-05",
254
+ capabilities: {},
255
+ clientInfo: { name: "maestro-builder-sdk", version: "0" },
256
+ }, startTimeoutMs);
257
+ if (initialized.error)
258
+ throw new HostFailure(`initialize failed: ${initialized.error.message ?? "unknown error"}`);
259
+ host.notify("notifications/initialized");
260
+ const listed = await host.request("tools/list", {}, startTimeoutMs);
261
+ const tools = listed.result?.tools ?? [];
262
+ if (!tools.some((tool) => tool.name === HOST_TOOL)) {
263
+ // An older CLI, not a fault: spawn per call, and say nothing.
264
+ host.close();
265
+ this.spawnFromNowOn();
266
+ return undefined;
267
+ }
268
+ }
269
+ catch (error) {
270
+ host.kill();
271
+ this.giveUp(error instanceof HostTimeout
272
+ ? `uip mcp serve did not answer within ${startTimeoutMs} ms`
273
+ : error instanceof Error
274
+ ? error.message
275
+ : String(error));
276
+ return undefined;
277
+ }
278
+ this.host = host;
279
+ return host;
280
+ }
281
+ /**
282
+ * Spawn per call from here on, silently. This is the path for a CLI whose
283
+ * server predates `run_argv`: a prepare on it works exactly as it did
284
+ * before this client existed, so there is nothing to tell the user.
285
+ */
286
+ spawnFromNowOn() {
287
+ this.disabled = true;
288
+ this.host?.kill();
289
+ this.host = undefined;
290
+ }
291
+ /** Spawn per call from here on, and say why: the host failed. */
292
+ giveUp(reason) {
293
+ if (this.disabled)
294
+ return;
295
+ this.spawnFromNowOn();
296
+ this.options.onFallback?.(`uip host unavailable, spawning each call instead: ${reason}`);
297
+ }
298
+ }
299
+ /** The tool's structured answer as the result every caller already reads. */
300
+ function toResult(response) {
301
+ if (response.error)
302
+ throw new HostFailure(`${HOST_TOOL} failed: ${response.error.message ?? "unknown error"}`);
303
+ const result = response.result;
304
+ const structured = result?.structuredContent;
305
+ if (structured === undefined ||
306
+ typeof structured.stdout !== "string" ||
307
+ typeof structured.stderr !== "string" ||
308
+ typeof structured.exitCode !== "number") {
309
+ throw new HostFailure(`${HOST_TOOL} answered without stdout/stderr/exitCode`);
310
+ }
311
+ return { code: structured.exitCode, signal: null, stdout: structured.stdout, stderr: structured.stderr, timedOut: false };
312
+ }
@@ -1,3 +1,4 @@
1
+ import type { DefinitionReference } from './core/node-classes.js';
1
2
  import type { BuiltFlow } from './flow-sdk.js';
2
3
  import type { Library } from './library.js';
3
4
  import type { Bindings } from './bindings.js';
@@ -87,6 +88,12 @@ export declare function serializeWithSidecars(built: BuiltFlow, opts?: Serialize
87
88
  json: unknown;
88
89
  }>;
89
90
  };
91
+ /**
92
+ * Resolve a contribution's {@link DefinitionReference} to a concrete manifest —
93
+ * exactly (design §2.6/§4.2): the named version resolves or compilation fails,
94
+ * and a contributor that ignored the author's requested version is rejected.
95
+ */
96
+ export declare function resolveContributionDefinition(ref: DefinitionReference, requestedVersion: string | undefined, where: string): any;
90
97
  export declare function serialize(built: BuiltFlow, opts?: SerializeOpts): any;
91
98
  /**
92
99
  * Every node `configuration` that carries Studio Web's split envelope must be