@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.
- package/dist/authored-preflight.d.ts +4 -0
- package/dist/authored-preflight.d.ts.map +1 -0
- package/dist/authored-preflight.js +50 -0
- package/dist/authored-preflight.js.map +1 -0
- package/dist/authored-worker-step.d.ts.map +1 -1
- package/dist/authored-worker-step.js +3 -2
- package/dist/authored-worker-step.js.map +1 -1
- package/dist/cli/check.d.ts +10 -2
- package/dist/cli/check.d.ts.map +1 -1
- package/dist/cli/check.js +16 -137
- package/dist/cli/check.js.map +1 -1
- package/dist/cli/cli-probe.d.ts +20 -0
- package/dist/cli/cli-probe.d.ts.map +1 -0
- package/dist/cli/cli-probe.js +166 -0
- package/dist/cli/cli-probe.js.map +1 -0
- package/dist/cli/cloud-mirror-session.d.ts +97 -0
- package/dist/cli/cloud-mirror-session.d.ts.map +1 -0
- package/dist/cli/cloud-mirror-session.js +264 -0
- package/dist/cli/cloud-mirror-session.js.map +1 -0
- package/dist/cli-commands.d.ts +6 -0
- package/dist/cli-commands.d.ts.map +1 -1
- package/dist/cli-commands.js +1 -0
- package/dist/cli-commands.js.map +1 -1
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +84 -18
- package/dist/cli.js.map +1 -1
- package/dist/cloud-deploy.d.ts +1 -1
- package/dist/cloud-deploy.d.ts.map +1 -1
- package/dist/cloud-deploy.js +24 -8
- package/dist/cloud-deploy.js.map +1 -1
- package/dist/cloud-mirror-ledger.d.ts +40 -0
- package/dist/cloud-mirror-ledger.d.ts.map +1 -0
- package/dist/cloud-mirror-ledger.js +142 -0
- package/dist/cloud-mirror-ledger.js.map +1 -0
- package/dist/cloud-mirror-step.d.ts +180 -0
- package/dist/cloud-mirror-step.d.ts.map +1 -0
- package/dist/cloud-mirror-step.js +523 -0
- package/dist/cloud-mirror-step.js.map +1 -0
- package/dist/cloud-mirror-transport.d.ts +94 -0
- package/dist/cloud-mirror-transport.d.ts.map +1 -0
- package/dist/cloud-mirror-transport.js +163 -0
- package/dist/cloud-mirror-transport.js.map +1 -0
- package/dist/cloud-mirror.d.ts +132 -0
- package/dist/cloud-mirror.d.ts.map +1 -0
- package/dist/cloud-mirror.js +556 -0
- package/dist/cloud-mirror.js.map +1 -0
- package/dist/cloud-transcript-codex.d.ts.map +1 -1
- package/dist/cloud-transcript-codex.js +90 -4
- package/dist/cloud-transcript-codex.js.map +1 -1
- package/dist/cloud-transcript-types.d.ts +19 -1
- package/dist/cloud-transcript-types.d.ts.map +1 -1
- package/dist/cloud-transcript-types.js.map +1 -1
- package/dist/cloud-transcript.d.ts +1 -1
- package/dist/cloud-transcript.d.ts.map +1 -1
- package/dist/cloud-transcript.js +10 -0
- package/dist/cloud-transcript.js.map +1 -1
- package/dist/helper-reference.d.ts.map +1 -1
- package/dist/helper-reference.js +134 -6
- package/dist/helper-reference.js.map +1 -1
- package/dist/hosted-extension-runtime.js +1 -1
- package/dist/hosted-extension-sandbox.js +1 -1
- package/dist/package-version.d.ts +10 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +22 -0
- package/dist/package-version.js.map +1 -0
- package/dist/preflight.d.ts +14 -0
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +18 -3
- package/dist/preflight.js.map +1 -1
- package/dist/relay-cli.d.ts.map +1 -1
- package/dist/relay-cli.js +1 -21
- package/dist/relay-cli.js.map +1 -1
- package/package.json +2 -2
- package/src/authored-preflight.ts +46 -0
- package/src/authored-worker-step.ts +4 -3
- package/src/cli/check.ts +25 -163
- package/src/cli/cli-probe.ts +194 -0
- package/src/cli/cloud-mirror-session.ts +329 -0
- package/src/cli-commands.ts +1 -0
- package/src/cli.ts +94 -19
- package/src/cloud-deploy.ts +26 -8
- package/src/cloud-mirror-ledger.ts +161 -0
- package/src/cloud-mirror-step.ts +650 -0
- package/src/cloud-mirror-transport.ts +261 -0
- package/src/cloud-mirror.ts +633 -0
- package/src/cloud-transcript-codex.ts +96 -4
- package/src/cloud-transcript-types.ts +18 -1
- package/src/cloud-transcript.ts +9 -1
- package/src/helper-reference.ts +131 -6
- package/src/hosted-extension-runtime.ts +1 -1
- package/src/hosted-extension-sandbox.ts +1 -1
- package/src/package-version.ts +23 -0
- package/src/preflight.ts +31 -7
- 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([
|
|
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 === '
|
|
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
|
-
|
|
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 |
|
|
171
|
+
| TranscriptTurn | TranscriptError | TranscriptResult | TranscriptTodo
|
|
172
|
+
| TranscriptUnknown | TranscriptUnparsed;
|
|
156
173
|
|
|
157
174
|
export interface ParsedTranscript {
|
|
158
175
|
entries: TranscriptEntry[];
|
package/src/cloud-transcript.ts
CHANGED
|
@@ -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`;
|
package/src/helper-reference.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
/**
|
|
115
|
-
|
|
116
|
-
|
|
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))
|
|
161
|
+
for (const entry of child) if (isNode(entry)) walkReferences(entry, root, hidden, state, visit);
|
|
122
162
|
} else if (isNode(child)) {
|
|
123
|
-
|
|
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 = '
|
|
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': '
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
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
|
*
|