@relayflows/sdk 2.0.31 → 2.0.33

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 (95) hide show
  1. package/dist/authored-preflight.d.ts +4 -0
  2. package/dist/authored-preflight.d.ts.map +1 -0
  3. package/dist/authored-preflight.js +50 -0
  4. package/dist/authored-preflight.js.map +1 -0
  5. package/dist/authored-worker-step.d.ts.map +1 -1
  6. package/dist/authored-worker-step.js +3 -2
  7. package/dist/authored-worker-step.js.map +1 -1
  8. package/dist/cli/check.d.ts +10 -2
  9. package/dist/cli/check.d.ts.map +1 -1
  10. package/dist/cli/check.js +16 -137
  11. package/dist/cli/check.js.map +1 -1
  12. package/dist/cli/cli-probe.d.ts +20 -0
  13. package/dist/cli/cli-probe.d.ts.map +1 -0
  14. package/dist/cli/cli-probe.js +166 -0
  15. package/dist/cli/cli-probe.js.map +1 -0
  16. package/dist/cli/cloud-mirror-session.d.ts +97 -0
  17. package/dist/cli/cloud-mirror-session.d.ts.map +1 -0
  18. package/dist/cli/cloud-mirror-session.js +264 -0
  19. package/dist/cli/cloud-mirror-session.js.map +1 -0
  20. package/dist/cli-commands.d.ts +6 -0
  21. package/dist/cli-commands.d.ts.map +1 -1
  22. package/dist/cli-commands.js +1 -0
  23. package/dist/cli-commands.js.map +1 -1
  24. package/dist/cli.d.ts +7 -0
  25. package/dist/cli.d.ts.map +1 -1
  26. package/dist/cli.js +84 -18
  27. package/dist/cli.js.map +1 -1
  28. package/dist/cloud-deploy.d.ts +1 -1
  29. package/dist/cloud-deploy.d.ts.map +1 -1
  30. package/dist/cloud-deploy.js +24 -8
  31. package/dist/cloud-deploy.js.map +1 -1
  32. package/dist/cloud-mirror-ledger.d.ts +40 -0
  33. package/dist/cloud-mirror-ledger.d.ts.map +1 -0
  34. package/dist/cloud-mirror-ledger.js +142 -0
  35. package/dist/cloud-mirror-ledger.js.map +1 -0
  36. package/dist/cloud-mirror-step.d.ts +180 -0
  37. package/dist/cloud-mirror-step.d.ts.map +1 -0
  38. package/dist/cloud-mirror-step.js +523 -0
  39. package/dist/cloud-mirror-step.js.map +1 -0
  40. package/dist/cloud-mirror-transport.d.ts +94 -0
  41. package/dist/cloud-mirror-transport.d.ts.map +1 -0
  42. package/dist/cloud-mirror-transport.js +163 -0
  43. package/dist/cloud-mirror-transport.js.map +1 -0
  44. package/dist/cloud-mirror.d.ts +132 -0
  45. package/dist/cloud-mirror.d.ts.map +1 -0
  46. package/dist/cloud-mirror.js +556 -0
  47. package/dist/cloud-mirror.js.map +1 -0
  48. package/dist/cloud-transcript-codex.d.ts.map +1 -1
  49. package/dist/cloud-transcript-codex.js +90 -4
  50. package/dist/cloud-transcript-codex.js.map +1 -1
  51. package/dist/cloud-transcript-types.d.ts +19 -1
  52. package/dist/cloud-transcript-types.d.ts.map +1 -1
  53. package/dist/cloud-transcript-types.js.map +1 -1
  54. package/dist/cloud-transcript.d.ts +1 -1
  55. package/dist/cloud-transcript.d.ts.map +1 -1
  56. package/dist/cloud-transcript.js +10 -0
  57. package/dist/cloud-transcript.js.map +1 -1
  58. package/dist/helper-reference.d.ts.map +1 -1
  59. package/dist/helper-reference.js +134 -6
  60. package/dist/helper-reference.js.map +1 -1
  61. package/dist/hosted-extension-runtime.js +1 -1
  62. package/dist/hosted-extension-sandbox.js +1 -1
  63. package/dist/package-version.d.ts +10 -0
  64. package/dist/package-version.d.ts.map +1 -0
  65. package/dist/package-version.js +22 -0
  66. package/dist/package-version.js.map +1 -0
  67. package/dist/preflight.d.ts +14 -0
  68. package/dist/preflight.d.ts.map +1 -1
  69. package/dist/preflight.js +18 -3
  70. package/dist/preflight.js.map +1 -1
  71. package/dist/relay-cli.d.ts.map +1 -1
  72. package/dist/relay-cli.js +1 -21
  73. package/dist/relay-cli.js.map +1 -1
  74. package/package.json +2 -2
  75. package/src/authored-preflight.ts +46 -0
  76. package/src/authored-worker-step.ts +4 -3
  77. package/src/cli/check.ts +25 -163
  78. package/src/cli/cli-probe.ts +194 -0
  79. package/src/cli/cloud-mirror-session.ts +329 -0
  80. package/src/cli-commands.ts +1 -0
  81. package/src/cli.ts +94 -19
  82. package/src/cloud-deploy.ts +26 -8
  83. package/src/cloud-mirror-ledger.ts +161 -0
  84. package/src/cloud-mirror-step.ts +650 -0
  85. package/src/cloud-mirror-transport.ts +261 -0
  86. package/src/cloud-mirror.ts +633 -0
  87. package/src/cloud-transcript-codex.ts +96 -4
  88. package/src/cloud-transcript-types.ts +18 -1
  89. package/src/cloud-transcript.ts +9 -1
  90. package/src/helper-reference.ts +131 -6
  91. package/src/hosted-extension-runtime.ts +1 -1
  92. package/src/hosted-extension-sandbox.ts +1 -1
  93. package/src/package-version.ts +23 -0
  94. package/src/preflight.ts +31 -7
  95. package/src/relay-cli.ts +1 -24
@@ -32,12 +32,22 @@ export const CODEX_FRAME_TYPES: ReadonlySet<string> = new Set([
32
32
  /** Item types whose lifecycles are matched and whose fields are read. */
33
33
  const SUPPORTED_ITEMS: ReadonlySet<string> = new Set([
34
34
  'agent_message', 'reasoning', 'command_execution', 'file_change', 'mcp_tool_call', 'error',
35
+ // The rest of Codex's `ThreadItemDetails` union
36
+ // (codex-rs/exec/src/exec_events.rs). Without these three, a real agent turn
37
+ // reported them as bare `unknown` placeholders naming a type and a size —
38
+ // honest, but useless: `todo_list` in particular updates continuously
39
+ // through a turn, so it was the most frequent thing a reader could not read.
40
+ 'web_search', 'todo_list', 'collab_tool_call',
35
41
  ]);
36
42
 
37
43
  const ITEM_FRAMES: ReadonlySet<string> = new Set(['item.started', 'item.updated', 'item.completed']);
38
44
 
39
45
  /** Calls are numbered; an `apply_patch` is file activity, not a call. */
40
- const NUMBERED_ITEMS: ReadonlySet<string> = new Set(['command_execution', 'mcp_tool_call']);
46
+ const NUMBERED_ITEMS: ReadonlySet<string> = new Set([
47
+ 'command_execution', 'mcp_tool_call', 'web_search', 'collab_tool_call',
48
+ ]);
49
+ /** Todo entries listed per snapshot; a longer plan is counted, not printed. */
50
+ const TODO_MAX_ITEMS = 12;
41
51
 
42
52
  interface Lifecycle {
43
53
  snapshots: Array<Record<string, unknown>>;
@@ -246,6 +256,65 @@ function mcpEntry(item: Record<string, unknown>, complete: boolean, context: Con
246
256
  });
247
257
  }
248
258
 
259
+ /**
260
+ * `web_search`: a query and what was done with it (`WebSearchItem` — `query`,
261
+ * `action`, optional structured `results`). Rendered as a call rather than
262
+ * prose, so a reader sees the search in sequence beside the commands.
263
+ */
264
+ function webSearchEntry(item: Record<string, unknown>, complete: boolean, context: Context,
265
+ seq: number): TranscriptEntry | null {
266
+ const query = str(item['query']);
267
+ if (query === null) return null;
268
+ const action = isRecord(item['action']) ? str(item['action']['type']) : str(item['action']);
269
+ const results = item['results'];
270
+ const count = Array.isArray(results) ? results.length : null;
271
+ return tool('web_search', bounded(query, context.clean),
272
+ count === null ? null : JSON.stringify(results).length, false, {
273
+ seq, status: action === null ? null : bounded(action, context.clean, 40),
274
+ exit_code: null,
275
+ output_excerpt: count === null ? null : `${count} result${count === 1 ? '' : 's'}`,
276
+ output_truncated: false, complete, error: null,
277
+ });
278
+ }
279
+
280
+ /**
281
+ * `collab_tool_call`: Codex driving other Codex threads (`CollabToolCallItem` —
282
+ * `tool`, `sender_thread_id`, `receiver_thread_ids`, `agents_states`).
283
+ *
284
+ * The per-agent states are reduced to a count per status rather than listed:
285
+ * `agents_states` is a map keyed by agent id, and ids are not something a
286
+ * transcript reader can act on.
287
+ */
288
+ function collabEntry(item: Record<string, unknown>, complete: boolean, context: Context,
289
+ seq: number): TranscriptEntry | null {
290
+ const name = str(item['tool']);
291
+ if (name === null) return null;
292
+ const receivers = Array.isArray(item['receiver_thread_ids']) ? item['receiver_thread_ids'].length : 0;
293
+ const states = isRecord(item['agents_states']) ? item['agents_states'] : null;
294
+ const byStatus = new Map<string, number>();
295
+ if (states !== null) {
296
+ for (const state of Object.values(states)) {
297
+ const status = isRecord(state) ? str(state['status']) : null;
298
+ if (status !== null) byStatus.set(status, (byStatus.get(status) ?? 0) + 1);
299
+ }
300
+ }
301
+ // Redacted like every neighbouring field. A status is provider text, not a
302
+ // closed vocabulary this module controls, and it was the one string here that
303
+ // reached the page without passing the redactor.
304
+ const summary = bounded(
305
+ [...byStatus.entries()].sort().map(([status, n]) => `${n} ${status}`).join(', '),
306
+ context.clean,
307
+ );
308
+ const status = str(item['status']);
309
+ return tool('collab_tool_call',
310
+ bounded(`${name}${receivers > 0 ? ` → ${receivers} thread${receivers === 1 ? '' : 's'}` : ''}`, context.clean),
311
+ null, status === 'failed', {
312
+ seq, status: status === null ? null : bounded(status, context.clean, 40),
313
+ exit_code: null, output_excerpt: summary.length === 0 ? null : summary,
314
+ output_truncated: false, complete, error: null,
315
+ });
316
+ }
317
+
249
318
  function fileChangeEntry(item: Record<string, unknown>, complete: boolean, context: Context): TranscriptEntry | null {
250
319
  const raw = item['changes'];
251
320
  if (!Array.isArray(raw)) return null;
@@ -288,12 +357,35 @@ function itemEntries(item: Record<string, unknown>, complete: boolean, context:
288
357
  if (message === null) return [placeholder(context, itemType)];
289
358
  return [{ kind: 'error', source: 'item', message: bounded(message, context.clean, ERROR_MAX_CHARS) }];
290
359
  }
291
- if (itemType === 'command_execution' || itemType === 'mcp_tool_call') {
360
+ if (itemType === 'todo_list') {
361
+ // A plan, not a call: it carries no status and earns no sequence number.
362
+ // `items[]` is `{text, completed}` (TodoListItem/TodoItem).
363
+ const items = Array.isArray(item['items']) ? item['items'] : null;
364
+ if (items === null) return [placeholder(context, itemType)];
365
+ const listed = items.slice(0, TODO_MAX_ITEMS).flatMap((entry) => {
366
+ const todo = isRecord(entry) ? entry : undefined;
367
+ const text = str(todo?.['text']);
368
+ return text === null ? [] : [{ text: context.clean(text), done: todo!['completed'] === true }];
369
+ });
370
+ // Counted across the whole plan; only the *display* is capped. Counting the
371
+ // prefix rendered a 13-item plan whose last item was done as `0/13`, which
372
+ // is exactly backwards from "the rest are counted, not printed".
373
+ const done = items.filter(entry => isRecord(entry) && entry['completed'] === true).length;
374
+ return [{
375
+ kind: 'todo', total: items.length, done,
376
+ items: listed, ...(items.length > listed.length ? { omitted: items.length - listed.length } : {}),
377
+ ...(complete ? {} : { complete: false }),
378
+ }];
379
+ }
380
+ if (itemType === 'command_execution' || itemType === 'mcp_tool_call'
381
+ || itemType === 'web_search' || itemType === 'collab_tool_call') {
292
382
  // The number is only spent on a call this module could actually read, so a
293
383
  // malformed item leaves no gap in the sequence.
294
384
  const seq = context.state.seq + 1;
295
- const entry = itemType === 'command_execution'
296
- ? commandEntry(item, complete, context, seq) : mcpEntry(item, complete, context, seq);
385
+ const entry = itemType === 'command_execution' ? commandEntry(item, complete, context, seq)
386
+ : itemType === 'mcp_tool_call' ? mcpEntry(item, complete, context, seq)
387
+ : itemType === 'web_search' ? webSearchEntry(item, complete, context, seq)
388
+ : collabEntry(item, complete, context, seq);
297
389
  if (entry === null) return [placeholder(context, itemType)];
298
390
  context.state.seq = seq;
299
391
  return [entry];
@@ -138,6 +138,22 @@ export interface TranscriptResult {
138
138
  cache_creation: number | null;
139
139
  }
140
140
 
141
+ /**
142
+ * Codex's running to-do list (`TodoListItem`). A plan, not a call: it carries
143
+ * no status and takes no sequence number, and it updates repeatedly through a
144
+ * turn — which is why leaving it unparsed was the most visible gap.
145
+ */
146
+ export interface TranscriptTodo {
147
+ kind: 'todo';
148
+ total: number;
149
+ done: number;
150
+ items: Array<{ text: string; done: boolean }>;
151
+ /** Entries past the per-snapshot cap: counted, never printed. */
152
+ omitted?: number;
153
+ /** False when read off a snapshot that never completed. */
154
+ complete?: boolean;
155
+ }
156
+
141
157
  export interface TranscriptUnknown {
142
158
  kind: 'unknown';
143
159
  type: string;
@@ -152,7 +168,8 @@ export interface TranscriptUnparsed {
152
168
  export type TranscriptEntry =
153
169
  | TranscriptAttempt | TranscriptAttemptOmitted | TranscriptInit | TranscriptMessage
154
170
  | TranscriptThinking | TranscriptTool | TranscriptFileChange | TranscriptThread
155
- | TranscriptTurn | TranscriptError | TranscriptResult | TranscriptUnknown | TranscriptUnparsed;
171
+ | TranscriptTurn | TranscriptError | TranscriptResult | TranscriptTodo
172
+ | TranscriptUnknown | TranscriptUnparsed;
156
173
 
157
174
  export interface ParsedTranscript {
158
175
  entries: TranscriptEntry[];
@@ -34,7 +34,7 @@ import { redact } from './redact.js';
34
34
  export type {
35
35
  ParsedTranscript, TranscriptEntry, TranscriptAttempt, TranscriptAttemptOmitted, TranscriptError,
36
36
  TranscriptFileChange, TranscriptInit, TranscriptMessage, TranscriptResult, TranscriptThinking,
37
- TranscriptThread, TranscriptTool, TranscriptToolCodex, TranscriptTurn, TranscriptUnknown,
37
+ TranscriptThread, TranscriptTodo, TranscriptTool, TranscriptToolCodex, TranscriptTurn, TranscriptUnknown,
38
38
  TranscriptUnparsed,
39
39
  } from './cloud-transcript-types.js';
40
40
 
@@ -297,6 +297,14 @@ export function renderAgentTranscript(parsed: ParsedTranscript): string[] {
297
297
  case 'thinking':
298
298
  lines.push(` thinking ${thousands(entry.chars)} chars (not shown)`);
299
299
  break;
300
+ case 'todo': {
301
+ // The plan the agent is working to, as it stood at this snapshot.
302
+ const progress = `${entry.done}/${entry.total}`;
303
+ lines.push(` plan ${progress}${entry.complete === false ? ' (in progress)' : ''}`);
304
+ for (const todo of entry.items) lines.push(` ${todo.done ? '✓' : '·'} ${safe(todo.text)}`);
305
+ if (entry.omitted !== undefined) lines.push(` …${entry.omitted} more`);
306
+ break;
307
+ }
300
308
  case 'tool': {
301
309
  const size = entry.is_error ? 'ERROR'
302
310
  : entry.result_chars === null ? 'no result' : `${thousands(entry.result_chars)} chars`;
@@ -27,7 +27,8 @@ export function helperNamespacesUsed(body: string, root: string): ReadonlySet<st
27
27
  const program = parseFlowBody(body);
28
28
  if (program === null) return textFallback(body, root);
29
29
  const used = new Set<string>();
30
- walk(program, (node) => {
30
+ const scope = { rootFunctionFound: false };
31
+ walkReferences(program, root, false, scope, (node) => {
31
32
  if (node.type !== 'MemberExpression') return;
32
33
  const object = node.object as AstNode | undefined;
33
34
  if (object?.type !== 'Identifier' || object.name !== root) return;
@@ -111,20 +112,144 @@ type AstNode = {
111
112
  [key: string]: unknown;
112
113
  };
113
114
 
114
- /** Depth-first over every child node, without pulling in a second package. */
115
- function walk(node: AstNode, visit: (node: AstNode) => void): void {
116
- visit(node);
115
+ /**
116
+ * Depth-first over references that still resolve to the outer flow context.
117
+ *
118
+ * A nested callback/method parameter or lexical destructuring can reuse the
119
+ * context's spelling while binding an unrelated value. Counting its members
120
+ * would refuse the flow for a mount it never touches. Acorn gives us binding
121
+ * shapes, so scopes are tracked as syntax instead of guessed from text.
122
+ */
123
+ function walkReferences(
124
+ node: AstNode,
125
+ root: string,
126
+ shadowed: boolean,
127
+ state: { rootFunctionFound: boolean },
128
+ visit: (node: AstNode) => void,
129
+ ): void {
130
+ if (isFunction(node)) {
131
+ const parameters = Array.isArray(node.params) ? node.params.filter(isNode) : [];
132
+ const bindsParameter = parameters.some(parameter => patternBinds(parameter, root));
133
+ let bodyHidden = shadowed;
134
+ const isRootFunction = bindsParameter && !state.rootFunctionFound;
135
+ if (isRootFunction) state.rootFunctionFound = true;
136
+ else if (bindsParameter || (isNode(node.id) && patternBinds(node.id, root)) || functionVarBinds(node, root)) {
137
+ bodyHidden = true;
138
+ }
139
+ // Parameter initializers run before the function body and are outside a
140
+ // body-level `var` scope. Keep them visible unless the parameter list
141
+ // itself binds the root name (in which case all parameter references are
142
+ // local to that parameter environment).
143
+ const parameterHidden = shadowed || (bindsParameter && !isRootFunction);
144
+ for (const parameter of parameters) walkReferences(parameter, root, parameterHidden, state, visit);
145
+ const body = isNode(node.body) ? node.body : undefined;
146
+ if (body !== undefined) walkReferences(body, root, bodyHidden, state, visit);
147
+ return;
148
+ }
149
+ let hidden = shadowed;
150
+ if (node.type === 'BlockStatement' && blockBinds(node, root)) {
151
+ hidden = true;
152
+ } else if (node.type === 'CatchClause' && isNode(node.param) && patternBinds(node.param, root)) {
153
+ hidden = true;
154
+ }
155
+
156
+ if (!hidden) visit(node);
117
157
  for (const key of Object.keys(node)) {
118
158
  if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue;
119
159
  const child = node[key];
120
160
  if (Array.isArray(child)) {
121
- for (const entry of child) if (isNode(entry)) walk(entry, visit);
161
+ for (const entry of child) if (isNode(entry)) walkReferences(entry, root, hidden, state, visit);
122
162
  } else if (isNode(child)) {
123
- walk(child, visit);
163
+ walkReferences(child, root, hidden, state, visit);
124
164
  }
125
165
  }
126
166
  }
127
167
 
168
+ function isFunction(node: AstNode): boolean {
169
+ return node.type === 'ArrowFunctionExpression'
170
+ || node.type === 'FunctionExpression'
171
+ || node.type === 'FunctionDeclaration';
172
+ }
173
+
174
+ /** Lexical declarations bind across their complete block, including TDZ. */
175
+ function blockBinds(node: AstNode, root: string): boolean {
176
+ const statements = Array.isArray(node.body) ? node.body.filter(isNode) : [];
177
+ return statements.some((statement) => {
178
+ if (statement.type === 'VariableDeclaration' && statement.kind !== 'var') {
179
+ const declarations = Array.isArray(statement.declarations)
180
+ ? statement.declarations.filter(isNode) : [];
181
+ return declarations.some(declaration => isNode(declaration.id) && patternBinds(declaration.id, root));
182
+ }
183
+ return (statement.type === 'FunctionDeclaration' || statement.type === 'ClassDeclaration')
184
+ && isNode(statement.id) && patternBinds(statement.id, root);
185
+ });
186
+ }
187
+
188
+ /** Identifier occurrences that introduce bindings, including nested patterns. */
189
+ function patternBinds(pattern: AstNode, root: string): boolean {
190
+ switch (pattern.type) {
191
+ case 'Identifier':
192
+ return pattern.name === root;
193
+ case 'AssignmentPattern':
194
+ return isNode(pattern.left) && patternBinds(pattern.left, root);
195
+ case 'RestElement':
196
+ return isNode(pattern.argument) && patternBinds(pattern.argument, root);
197
+ case 'ArrayPattern': {
198
+ const elements = Array.isArray(pattern.elements) ? pattern.elements : [];
199
+ return elements.some(element => isNode(element) && patternBinds(element, root));
200
+ }
201
+ case 'ObjectPattern': {
202
+ const properties = Array.isArray(pattern.properties) ? pattern.properties : [];
203
+ return properties.some(property => {
204
+ if (!isNode(property)) return false;
205
+ if (property.type === 'RestElement') {
206
+ return isNode(property.argument) && patternBinds(property.argument, root);
207
+ }
208
+ // Object keys are labels, not bindings. Only the value side introduces
209
+ // the local (including a default-value AssignmentPattern's left side).
210
+ return property.type === 'Property'
211
+ && isNode(property.value)
212
+ && patternBinds(property.value, root);
213
+ });
214
+ }
215
+ default:
216
+ return false;
217
+ }
218
+ }
219
+
220
+ /** `var` is function-scoped, so a nested function's declaration hides the
221
+ * outer flow context for its entire body, including code before the
222
+ * declaration. Nested functions have their own var scopes and are skipped. */
223
+ function functionVarBinds(node: AstNode, root: string): boolean {
224
+ const body = isNode(node.body) ? node.body : undefined;
225
+ if (body === undefined) return false;
226
+ let found = false;
227
+ const scan = (current: AstNode): void => {
228
+ if (found) return;
229
+ if (current !== body && isFunction(current)) return;
230
+ if (current.type === 'VariableDeclaration' && current.kind === 'var') {
231
+ const declarations = Array.isArray(current.declarations)
232
+ ? current.declarations.filter(isNode) : [];
233
+ if (declarations.some(declaration => isNode(declaration.id) && patternBinds(declaration.id, root))) {
234
+ found = true;
235
+ return;
236
+ }
237
+ }
238
+ for (const key of Object.keys(current)) {
239
+ if (key === 'type' || key === 'start' || key === 'end' || key === 'loc') continue;
240
+ const child = current[key];
241
+ if (Array.isArray(child)) {
242
+ for (const entry of child) if (isNode(entry)) scan(entry);
243
+ } else if (isNode(child)) {
244
+ scan(child);
245
+ }
246
+ if (found) return;
247
+ }
248
+ };
249
+ scan(body);
250
+ return found;
251
+ }
252
+
128
253
  function isNode(value: unknown): value is AstNode {
129
254
  return typeof value === 'object' && value !== null && typeof (value as { type?: unknown }).type === 'string';
130
255
  }
@@ -26,7 +26,7 @@ const REALPATH = realpath;
26
26
  const PATH_DIRNAME = dirname;
27
27
  const PATH_JOIN = join;
28
28
  const PATH_RESOLVE = resolve;
29
- const SOFTWARE_FACTORY_SHA256 = '49c993220b9c34fab2d4b0e51911656f62b8b657f534d988691960d45bb9d9b6';
29
+ const SOFTWARE_FACTORY_SHA256 = 'b97a3466c2affabb61afa655d4b2f726942d753c0466740b64ba26a78da359c6';
30
30
  const ARRAY_IS_ARRAY = Array.isArray;
31
31
  const OBJECT_FREEZE = Object.freeze;
32
32
  const WEAK_MAP_GET = Function.prototype.call.bind(WeakMap.prototype.get) as <K extends object, V>(
@@ -109,7 +109,7 @@ const ADDRESS_SPACE_BYTES = 16 * 1024 * 1024 * 1024;
109
109
  const DATA_BYTES = 3 * 1024 * 1024 * 1024;
110
110
  const SURFACE_RUNTIME_SHA256 = OBJECT_FREEZE({
111
111
  'flow.js': '4aaeacc55de3074f4d121ce7253c3be50a93757e6540ba8159889a9450d1c05c',
112
- 'helpers/providers.js': '7bc62eccaa3a9e786ae0a689bf74160585149e91feef8208e17ef8eca51eed7f',
112
+ 'helpers/providers.js': '4eb06d0d85ca0a3434bb2dbba7407e3d95eef2dfbedaeb0e2de0c0c0ea812457',
113
113
  'provider-trigger.js': 'e2664c65397f93fb486eb6f1e756c7cec3f88b3851d79c23567cad986f80f1ff',
114
114
  'schedule.js': '8fe72f176a75ec0b5f26e12db7a597575c259a2e2cbb59690f9dc20a5e63940b',
115
115
  'triggers.js': '4a3515b571a318f6c7a5661f9310bc9af43e3faf20ea39903a9b363c51258e4c',
@@ -0,0 +1,23 @@
1
+ import { readFileSync } from 'node:fs';
2
+
3
+ /**
4
+ * Read the installed SDK package version from the manifest beside this module.
5
+ *
6
+ * The same relative URL works from `src/` during development and `dist/` in a
7
+ * built or npm-installed package. Reading the manifest at runtime keeps the
8
+ * CLI output tied to the bytes that are actually installed instead of a
9
+ * hardcoded release value that can drift between package files.
10
+ */
11
+ export function packageVersion(): string {
12
+ try {
13
+ const manifest: unknown = JSON.parse(
14
+ readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
15
+ );
16
+ const version = (manifest as { version?: unknown }).version;
17
+ return typeof version === 'string' && version.length > 0 ? version : '0.0.0';
18
+ } catch {
19
+ // Version display is cosmetic. Keep the CLI usable if a host strips the
20
+ // manifest from an otherwise runnable package artifact.
21
+ return '0.0.0';
22
+ }
23
+ }
package/src/preflight.ts CHANGED
@@ -65,7 +65,7 @@ export class CliProbeError extends Error {
65
65
  }
66
66
  }
67
67
 
68
- type CliProbeOutcome =
68
+ export type CliProbeOutcome =
69
69
  | { result: CliProbeResult }
70
70
  | { failure: CliProbeFailureDetail | null };
71
71
 
@@ -89,6 +89,8 @@ export interface PreflightProbes {
89
89
  }
90
90
 
91
91
  export interface PreflightOptions {
92
+ /** Reuse only within one run and one CLI resolution environment; failures are cached too. */
93
+ cliProbeCache?: Map<string, CliProbeOutcome>;
92
94
  pluginSearchStart?: string;
93
95
  /** Validated tools.mcp header and nearest flows.json connections. */
94
96
  mcpServers?: readonly string[];
@@ -99,6 +101,7 @@ export interface PreflightOptions {
99
101
  /** Exact, project-owned model allowlist from the nearest flows.json. */
100
102
  models?: readonly string[];
101
103
  modelRegistryPath?: string;
104
+ probeCache?: Map<string, CliProbeOutcome>;
102
105
  probes: PreflightProbes;
103
106
  }
104
107
 
@@ -194,7 +197,12 @@ async function probeMcp(result: PreflightResult, options: PreflightOptions): Pro
194
197
  return { ...result, ok: !result.diagnostics.some(d => d.severity === 'refusal'), mcpTools: Object.freeze(inventory) };
195
198
  }
196
199
 
197
- function preflightSync(flow: unknown, options: PreflightOptions): PreflightResult {
200
+ type ResolutionOptions = Omit<PreflightOptions, 'probes' | 'probeCache'>;
201
+
202
+ /** Resolve static declarations before any CLI, model, command or trigger probe. */
203
+ export function resolvePreflight(flow: unknown, options: ResolutionOptions): PreflightResult & {
204
+ compiled?: import('./compile.js').CompiledFlowSpec;
205
+ } {
198
206
  // Compile before touching any environment fact. `compileSpec` snapshots raw
199
207
  // input into inert data, validates it against the closed authoring schema,
200
208
  // and lowers `output` sugar into its json_schema gate — so the gate plan
@@ -235,7 +243,7 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
235
243
  const cliResolutionDiagnostics: PreflightDiagnostic[] = [];
236
244
  const resolutions: CliResolution[] = [];
237
245
  const resolutionByStep = new Map<string, CliResolution>();
238
- const cliProbeResults = new Map<string, CliProbeOutcome>();
246
+ const cliProbeResults = options.cliProbeCache ?? new Map<string, CliProbeOutcome>();
239
247
 
240
248
  // A pure fact about the compiled snapshot, collected before anything that
241
249
  // can return early. An unresolved CLI, an unknown model or a bad scope all
@@ -289,6 +297,18 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
289
297
  return { ok: false, gates: compiled.steps.map(inspectStepGate), resolutions, diagnostics };
290
298
  }
291
299
 
300
+ return { ok: true, gates: compiled.steps.map(inspectStepGate), resolutions, diagnostics, compiled };
301
+ }
302
+
303
+ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResult {
304
+ const { compiled, ...result } = resolvePreflight(flow, options);
305
+ if (!result.ok || compiled === undefined) return result;
306
+ const { diagnostics, resolutions } = result;
307
+ const resolutionByStep = new Map(resolutions.map(resolution => [resolution.stepId, resolution]));
308
+ // `cliProbeCache` is the public run-scoped name; `probeCache` is retained as
309
+ // a compatibility alias for the async authored-preflight path.
310
+ const cliProbeResults = options.cliProbeCache ?? options.probeCache ?? new Map<string, CliProbeOutcome>();
311
+
292
312
  for (const step of compiled.steps) {
293
313
  probeNamedGate(step, options.probes, diagnostics);
294
314
  warnOnVacuousGate(step, diagnostics);
@@ -333,7 +353,7 @@ function preflightSync(flow: unknown, options: PreflightOptions): PreflightResul
333
353
  * fact about the filesystem. Missing manifest means no known mounts — a grant
334
354
  * still parses but refuses as `mount_unknown` in that case.
335
355
  */
336
- function scopeDiagnostics(flow: FlowSpec, options: PreflightOptions): PreflightRefusal[] {
356
+ function scopeDiagnostics(flow: FlowSpec, options: ResolutionOptions): PreflightRefusal[] {
337
357
  const workspace = flow.workspace;
338
358
  const toolsFs = flow.tools?.fs;
339
359
  if (workspace === undefined && toolsFs === undefined) return [];
@@ -357,7 +377,7 @@ function scopeDiagnostics(flow: FlowSpec, options: PreflightOptions): PreflightR
357
377
  /** Pure authoring validation: no executable, command, trigger, or daemon probe. */
358
378
  function unknownModelDiagnostics(
359
379
  flow: FlowSpec,
360
- options: PreflightOptions,
380
+ options: ResolutionOptions,
361
381
  resolutionByStep: ReadonlyMap<string, CliResolution> = new Map(),
362
382
  ): PreflightRefusal[] {
363
383
  const diagnostics: PreflightRefusal[] = [];
@@ -471,7 +491,7 @@ function unknownModelMessage(
471
491
  return `Step "${stepId}" declares model "${model}"${cliContext}, but it is not listed in ${source}; add the exact model only after verifying that project is allowed to use it.`;
472
492
  }
473
493
 
474
- function unresolvedCliMessage(stepId: string, options: PreflightOptions): string {
494
+ function unresolvedCliMessage(stepId: string, options: ResolutionOptions): string {
475
495
  const context = options.projectConfigPath !== undefined
476
496
  ? ` Nearest project config "${options.projectConfigPath}" declares no cli; outer configs are shadowed.`
477
497
  : options.projectSearchStart !== undefined
@@ -501,6 +521,10 @@ function resolveCli(
501
521
  return undefined;
502
522
  }
503
523
 
524
+ export function cliProbeKey(resolution: CliResolution, managed = false): string {
525
+ return JSON.stringify([resolution.cli, resolution.source, resolution.model ?? null, managed]);
526
+ }
527
+
504
528
  function probeResolvedCli(
505
529
  resolution: CliResolution,
506
530
  probes: PreflightProbes,
@@ -514,7 +538,7 @@ function probeResolvedCli(
514
538
  // Model is part of the key: the same CLI probed with two different models
515
539
  // is two different questions, and caching on the CLI alone would let a
516
540
  // model that the CLI cannot resolve inherit an earlier model's pass.
517
- const cacheKey = JSON.stringify([resolution.cli, resolution.source, resolution.model ?? null, managed]);
541
+ const cacheKey = cliProbeKey(resolution, managed);
518
542
  let outcome = cache.get(cacheKey);
519
543
  if (outcome === undefined) {
520
544
  try {
package/src/relay-cli.ts CHANGED
@@ -1,7 +1,6 @@
1
- import { readFileSync } from 'node:fs';
2
-
3
1
  import { runCli } from './cli.js';
4
2
  import { CLI_VERBS, CLI_VERB_NAMES, type CliCommandSpec } from './cli-commands.js';
3
+ import { packageVersion } from './package-version.js';
5
4
 
6
5
  /**
7
6
  * The `@relayflows/sdk/relay-cli` entrypoint: a mountable CLI surface.
@@ -56,28 +55,6 @@ function toCommandSpec(verb: CliCommandSpec & { variants?: unknown }): CliComman
56
55
  return spec;
57
56
  }
58
57
 
59
- /**
60
- * Read this package's version from its own manifest.
61
- *
62
- * Resolved from `import.meta.url` rather than imported, because `package.json`
63
- * sits outside `rootDir` and a hardcoded literal would silently go stale at the
64
- * next release. `src/` and `dist/` are both one level below the manifest, so
65
- * the same relative path is correct before and after a build.
66
- */
67
- function packageVersion(): string {
68
- try {
69
- const manifest: unknown = JSON.parse(
70
- readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
71
- );
72
- const version = (manifest as { version?: unknown }).version;
73
- return typeof version === 'string' && version.length > 0 ? version : '0.0.0';
74
- } catch {
75
- // A surface that cannot read its own manifest is still perfectly runnable;
76
- // refusing to mount over a cosmetic field would be the worse failure.
77
- return '0.0.0';
78
- }
79
- }
80
-
81
58
  /**
82
59
  * Build the relayflows CLI surface.
83
60
  *