@controllerai/cli 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (96) hide show
  1. package/README.md +180 -0
  2. package/dist/api/client.js +330 -0
  3. package/dist/api/client.js.map +1 -0
  4. package/dist/appLinks.js +99 -0
  5. package/dist/appLinks.js.map +1 -0
  6. package/dist/argv.js +124 -0
  7. package/dist/argv.js.map +1 -0
  8. package/dist/commands/agent.js +813 -0
  9. package/dist/commands/agent.js.map +1 -0
  10. package/dist/commands/auth.js +327 -0
  11. package/dist/commands/auth.js.map +1 -0
  12. package/dist/commands/branch.js +441 -0
  13. package/dist/commands/branch.js.map +1 -0
  14. package/dist/commands/connect.js +148 -0
  15. package/dist/commands/connect.js.map +1 -0
  16. package/dist/commands/connection.js +148 -0
  17. package/dist/commands/connection.js.map +1 -0
  18. package/dist/commands/data.js +441 -0
  19. package/dist/commands/data.js.map +1 -0
  20. package/dist/commands/deps.js +63 -0
  21. package/dist/commands/deps.js.map +1 -0
  22. package/dist/commands/doctor.js +102 -0
  23. package/dist/commands/doctor.js.map +1 -0
  24. package/dist/commands/events.js +72 -0
  25. package/dist/commands/events.js.map +1 -0
  26. package/dist/commands/exec.js +306 -0
  27. package/dist/commands/exec.js.map +1 -0
  28. package/dist/commands/expr.js +346 -0
  29. package/dist/commands/expr.js.map +1 -0
  30. package/dist/commands/flow.js +447 -0
  31. package/dist/commands/flow.js.map +1 -0
  32. package/dist/commands/integration.js +102 -0
  33. package/dist/commands/integration.js.map +1 -0
  34. package/dist/commands/node.js +885 -0
  35. package/dist/commands/node.js.map +1 -0
  36. package/dist/commands/organize.js +234 -0
  37. package/dist/commands/organize.js.map +1 -0
  38. package/dist/commands/run.js +437 -0
  39. package/dist/commands/run.js.map +1 -0
  40. package/dist/commands/schema.js +269 -0
  41. package/dist/commands/schema.js.map +1 -0
  42. package/dist/commands/skills.js +1156 -0
  43. package/dist/commands/skills.js.map +1 -0
  44. package/dist/commands/trigger.js +992 -0
  45. package/dist/commands/trigger.js.map +1 -0
  46. package/dist/commands/workflow.js +503 -0
  47. package/dist/commands/workflow.js.map +1 -0
  48. package/dist/config.js +74 -0
  49. package/dist/config.js.map +1 -0
  50. package/dist/context.js +39 -0
  51. package/dist/context.js.map +1 -0
  52. package/dist/errors.js +59 -0
  53. package/dist/errors.js.map +1 -0
  54. package/dist/index.js +125 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/jsSource.js +32 -0
  57. package/dist/jsSource.js.map +1 -0
  58. package/dist/nodeProps.js +177 -0
  59. package/dist/nodeProps.js.map +1 -0
  60. package/dist/output.js +85 -0
  61. package/dist/output.js.map +1 -0
  62. package/dist/version.js +7 -0
  63. package/dist/version.js.map +1 -0
  64. package/package.json +41 -0
  65. package/skills/controllerai-agents/SKILL.md +169 -0
  66. package/skills/controllerai-agents/agents/openai.yaml +4 -0
  67. package/skills/controllerai-agents/references/instructions-and-versioning.md +76 -0
  68. package/skills/controllerai-ai-nodes/SKILL.md +83 -0
  69. package/skills/controllerai-ai-nodes/agents/openai.yaml +4 -0
  70. package/skills/controllerai-ai-nodes/references/selection-and-testing.md +46 -0
  71. package/skills/controllerai-data/SKILL.md +121 -0
  72. package/skills/controllerai-data/agents/openai.yaml +4 -0
  73. package/skills/controllerai-data/references/data-rails.md +46 -0
  74. package/skills/controllerai-expressions/SKILL.md +115 -0
  75. package/skills/controllerai-expressions/agents/openai.yaml +4 -0
  76. package/skills/controllerai-expressions/references/dsl-and-recipes.md +120 -0
  77. package/skills/controllerai-flow-control/SKILL.md +132 -0
  78. package/skills/controllerai-flow-control/agents/openai.yaml +4 -0
  79. package/skills/controllerai-flow-control/references/patterns.md +61 -0
  80. package/skills/controllerai-integration-actions/SKILL.md +95 -0
  81. package/skills/controllerai-integration-actions/agents/openai.yaml +4 -0
  82. package/skills/controllerai-node-config/SKILL.md +138 -0
  83. package/skills/controllerai-node-config/agents/openai.yaml +4 -0
  84. package/skills/controllerai-node-config/references/prop-and-readiness.md +72 -0
  85. package/skills/controllerai-runs/SKILL.md +109 -0
  86. package/skills/controllerai-runs/agents/openai.yaml +4 -0
  87. package/skills/controllerai-runs/references/debugging.md +53 -0
  88. package/skills/controllerai-start/SKILL.md +44 -0
  89. package/skills/controllerai-start/agents/openai.yaml +4 -0
  90. package/skills/controllerai-triggers/SKILL.md +174 -0
  91. package/skills/controllerai-triggers/agents/openai.yaml +4 -0
  92. package/skills/controllerai-triggers/references/lifecycle.md +54 -0
  93. package/skills/controllerai-workflows/SKILL.md +86 -0
  94. package/skills/controllerai-workflows/agents/openai.yaml +4 -0
  95. package/skills/controllerai-workflows/references/command-map.md +64 -0
  96. package/skills/manifest.json +248 -0
@@ -0,0 +1,885 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { requireContext } from '../context.js';
3
+ import { validationFailed, validationMessage } from './expr.js';
4
+ import { CaiError, userError } from '../errors.js';
5
+ import { resolveJsSource } from '../jsSource.js';
6
+ import { buildNodeReadiness, expressionReadbackChannel, nodePropsFromPayload, normalizeNodePropWrites, } from '../nodeProps.js';
7
+ import { emit, printKeyValues, printTable, truncate, warn } from '../output.js';
8
+ const readUtf8 = (source, encoding) => readFileSync(source, encoding);
9
+ const loadProps = async (api, workflowId, nodeId) => {
10
+ const props = await api.tool(workflowId, 'get_node_props', { nodeId });
11
+ return nodePropsFromPayload(props);
12
+ };
13
+ /**
14
+ * get_node exposes processOutput after a successful typed run or documented
15
+ * declaration. Until then, add the component's curated/observed type as
16
+ * diagnostic context; readiness deliberately remains unresolved until the
17
+ * runtime adopts it.
18
+ */
19
+ const addResolvedOutputType = async (api, workflowId, record) => {
20
+ const nodeSlug = record.nodeSlug ?? record.data?.nodeSlug;
21
+ const nodeKey = record.key ?? record.data?.key;
22
+ if (!nodeSlug || !nodeKey)
23
+ return record;
24
+ try {
25
+ const component = await api.tool(workflowId, 'get_node_component', {
26
+ nodeSlug,
27
+ nodeKey,
28
+ });
29
+ const resolvedOutputType = component?.component?.outputDataType ?? component?.observedOutputType ?? null;
30
+ return resolvedOutputType ? { ...record, resolvedOutputType } : record;
31
+ }
32
+ catch {
33
+ // Status is still useful for configuration blockers. Fail closed on output
34
+ // readiness when the component type cannot be resolved.
35
+ return record;
36
+ }
37
+ };
38
+ /** Fail closed: a write without current prop metadata is unsafe. */
39
+ const findProp = async (api, workflowId, nodeId, propName) => {
40
+ const rows = await loadProps(api, workflowId, nodeId);
41
+ const prop = rows.find(p => p.name === propName);
42
+ if (!prop) {
43
+ throw userError(`Node ${nodeId} has no prop "${propName}". Re-read \`cai node props ${nodeId}\`; ` +
44
+ 'a selector, connection, or reload may need to be configured first.', 'UNKNOWN_PROP');
45
+ }
46
+ if (prop.enabled === false || prop.source === 'propsCatalog') {
47
+ throw userError(`Prop "${propName}" is optional but not materialized. Run ` +
48
+ `\`cai node enable-prop ${nodeId} --prop ${propName}\`, then re-read props.`, 'PROP_NOT_ENABLED');
49
+ }
50
+ return prop;
51
+ };
52
+ /**
53
+ * Prop types whose stored shape pairs a value with the human label the UI displayed.
54
+ *
55
+ * `STATIC_DROPDOWN` is deliberately EXCLUDED: its options are fixed in the node definition,
56
+ * so the UI stores the value alone and inventing a `valueLabel` is wrong (CFG-005 asserts
57
+ * exactly that). Only remotely-resolved options carry a label.
58
+ */
59
+ const isOptionBackedProp = (prop) => ['DROPDOWN', 'MULTI_SELECT', 'MULTI_SELECT_DROPDOWN', 'REMOTE_OPTIONS'].includes(String(prop.inputType ?? ''));
60
+ /**
61
+ * Multi-selects store `value` as an array AND `valueLabel` as an array of `{value, label}`
62
+ * pairs — not a parallel array of label strings. Single-selects store a plain label string.
63
+ * Getting this wrong saves cleanly and shows blank chips in the UI (found by CFG-012).
64
+ */
65
+ const isMultiSelectProp = (prop) => ['MULTI_SELECT', 'MULTI_SELECT_DROPDOWN'].includes(String(prop.inputType ?? ''));
66
+ /**
67
+ * Remote option providers commonly search human labels rather than opaque ids.
68
+ * A supplied --label is therefore the lookup query, never an unverified value
69
+ * written directly to the node.
70
+ */
71
+ export const optionLookupQueries = (selected, labels) => {
72
+ const supplied = labels === undefined ? [] : Array.isArray(labels) ? labels : [labels];
73
+ if (supplied.length > 0 && supplied.length !== selected.length) {
74
+ throw userError(`Pass one --label per selected value (${selected.length} value(s), ${supplied.length} label(s)).`, 'OPTION_LABEL_COUNT_MISMATCH');
75
+ }
76
+ return selected.map((value, index) => supplied[index]?.trim() || String(value));
77
+ };
78
+ export const parseNodeOptionPage = (raw) => {
79
+ if (raw === undefined)
80
+ return undefined;
81
+ const page = Number(raw);
82
+ if (!Number.isInteger(page) || page < 0) {
83
+ throw userError('--page must be a non-negative integer.', 'BAD_OPTION_PAGE');
84
+ }
85
+ return page;
86
+ };
87
+ /**
88
+ * Prop types persisted as a `TextExpression` wrapping raw text. Their `--value` must reach
89
+ * the backend as a STRING; parsing it first (e.g. a JSON body, which is legal JSON) makes
90
+ * the backend store a bare object the runtime cannot evaluate.
91
+ */
92
+ const isTextExpressionBackedProp = (prop) => ['JSON', 'LONG_TEXT', 'SHORT_TEXT', 'SQL', 'TEXT'].includes(String(prop.inputType ?? ''));
93
+ const fetchPropOptions = async (api, workflowId, nodeId, propName, query) => {
94
+ const data = await api.tool(workflowId, 'get_prop_options', { nodeId, propName, ...(query ? { query } : {}) });
95
+ const rows = Array.isArray(data) ? data : (data?.options ?? []);
96
+ return rows
97
+ .map(o => ({
98
+ value: String(o.value ?? o.id ?? ''),
99
+ label: String(o.label ?? o.name ?? o.value ?? o.id ?? ''),
100
+ }))
101
+ .filter(o => o.value !== '');
102
+ };
103
+ const readObjectOption = (source, flag) => {
104
+ const raw = source.trimStart().startsWith('{') ? source : readFileSync(source, 'utf8');
105
+ let value;
106
+ try {
107
+ value = JSON.parse(raw);
108
+ }
109
+ catch (error) {
110
+ throw userError(`${flag} must be a JSON object or a path to one: ${error instanceof Error ? error.message : error}`, 'BAD_JSON_OPTION');
111
+ }
112
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
113
+ throw userError(`${flag} must contain a JSON object.`, 'BAD_JSON_SHAPE');
114
+ }
115
+ return value;
116
+ };
117
+ /** Read a documented response sample before any workflow or network access. */
118
+ export const readNodeOutputSample = (source, reader = readUtf8) => {
119
+ let raw;
120
+ try {
121
+ raw = reader(source === '-' ? 0 : source, 'utf8');
122
+ }
123
+ catch (error) {
124
+ throw userError(`--sample-file "${source}" is not a readable UTF-8 file: ` +
125
+ `${error instanceof Error ? error.message : error}`, 'BAD_SAMPLE');
126
+ }
127
+ let sample;
128
+ try {
129
+ sample = JSON.parse(raw);
130
+ }
131
+ catch (error) {
132
+ throw userError(`--sample-file must contain valid JSON: ${error instanceof Error ? error.message : error}`, 'BAD_SAMPLE');
133
+ }
134
+ if (!sample || typeof sample !== 'object' || Array.isArray(sample)) {
135
+ throw userError('--sample-file must contain a JSON object.', 'BAD_SAMPLE');
136
+ }
137
+ return sample;
138
+ };
139
+ const nodeProcessOutput = (record) => record.processOutput ?? record.data?.processOutput;
140
+ const renderNodeOutputType = (dataType, declared) => declared && typeof dataType === 'string'
141
+ ? `${dataType} (declared — unverified)`
142
+ : dataType;
143
+ const shellQuote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
144
+ const requireCompletedPropReload = (result, nodeId, propName, retryCommand) => {
145
+ const reload = result;
146
+ if (reload?.propsReloaded !== false)
147
+ return;
148
+ const reloadError = typeof reload.reloadError === 'string' && reload.reloadError.trim()
149
+ ? reload.reloadError.trim()
150
+ : 'The backend did not provide a reload error.';
151
+ const inspectCommand = `cai node props ${nodeId} --json`;
152
+ throw new CaiError(`Prop "${propName}" was updated, but its dependent props were not reloaded: ` +
153
+ `${reloadError}\nThe node may still contain stale dependent values. Retry the reload with ` +
154
+ `\`${retryCommand}\`, then inspect \`${inspectCommand}\` before configuring or running it.`, {
155
+ code: 'PROPS_RELOAD_FAILED',
156
+ detail: {
157
+ nodeId,
158
+ prop: propName,
159
+ propsReloaded: false,
160
+ reloadError,
161
+ recoveryCommands: [retryCommand, inspectCommand],
162
+ },
163
+ });
164
+ };
165
+ const searchScore = (intent, component) => {
166
+ const normalize = (value) => value
167
+ .toLowerCase()
168
+ .replace(/[^a-z0-9]+/g, ' ')
169
+ .trim();
170
+ const query = normalize(intent);
171
+ const terms = query.split(/\s+/).filter(Boolean);
172
+ const name = normalize(component.name ?? '');
173
+ const description = normalize(component.description ?? '');
174
+ const props = normalize((component.propNames ?? []).join(' '));
175
+ let score = name === query ? 100 : name.includes(query) ? 50 : 0;
176
+ for (const term of terms) {
177
+ if (name.split(' ').includes(term))
178
+ score += 12;
179
+ else if (name.includes(term))
180
+ score += 7;
181
+ if (description.includes(term))
182
+ score += 3;
183
+ if (props.includes(term))
184
+ score += 2;
185
+ }
186
+ return score;
187
+ };
188
+ /**
189
+ * `add_node` needs BOTH nodeSlug and nodeKey, but component keys returned by
190
+ * search are already `<nodeSlug>-<component>` — so the slug is recoverable.
191
+ * Prefer an explicit --node, then a catalog lookup, then a prefix split.
192
+ */
193
+ const resolveSlug = async (api, workflowId, componentKey, explicit) => {
194
+ if (explicit)
195
+ return explicit;
196
+ const guess = componentKey.includes('-') ? componentKey.slice(0, componentKey.indexOf('-')) : '';
197
+ // Registry slugs themselves contain underscores, not hyphens, so the first
198
+ // hyphen is the slug boundary for registry components. Verify before trusting.
199
+ for (const candidate of [guess, componentKey.replace(/-[^-]+$/, '')].filter(Boolean)) {
200
+ try {
201
+ const found = await api.tool(workflowId, 'search_node_components', {
202
+ nodeSlug: candidate,
203
+ limit: 100,
204
+ });
205
+ if ((found?.data ?? []).some(c => c.key === componentKey))
206
+ return candidate;
207
+ }
208
+ catch {
209
+ // wrong guess — try the next shape
210
+ }
211
+ }
212
+ throw userError(`Cannot infer the node slug from component key "${componentKey}". Pass --node <slug> ` +
213
+ '(the `nodeSlug` column from `cai node components`).', 'UNRESOLVED_SLUG');
214
+ };
215
+ export const registerNode = (program) => {
216
+ const node = program.command('node').description('Nodes — search, add, configure, wire');
217
+ node
218
+ .command('search')
219
+ .argument('<query>')
220
+ .option('--limit <n>', 'max rows', '10')
221
+ .option('--actions', 'only nodes that have actions')
222
+ .option('--events', 'only nodes that have trigger events')
223
+ .description('Search registry + catalog nodes (ranked by featuredWeight, not relevance)')
224
+ .action(async (query, opts, command) => {
225
+ const { api, workflowId } = requireContext(command);
226
+ const result = await api.tool(workflowId, 'search_nodes', {
227
+ query,
228
+ limit: Number(opts.limit),
229
+ ...(opts.actions ? { hasActions: true } : {}),
230
+ ...(opts.events ? { hasEvents: true } : {}),
231
+ });
232
+ const rows = result?.data ?? [];
233
+ emit({ items: rows, pagination: result?.pagination ?? null }, () => printTable(['slug', 'name', 'source', 'actions', 'events', 'auth', 'weight'], rows.map((n) => [
234
+ n.slug,
235
+ n.name,
236
+ n.source,
237
+ n.actionsCount,
238
+ n.eventsCount,
239
+ n.authRequired ? 'yes' : 'no',
240
+ n.featuredWeight,
241
+ ])));
242
+ });
243
+ node
244
+ .command('find')
245
+ .argument('<intent>', 'capability or action, e.g. "send a Slack message"')
246
+ .option('--node <slug>', 'search only this integration')
247
+ .option('--limit <n>', 'max exact component references', '8')
248
+ .description('Find ranked action components and return exact nodeSlug/nodeKey pairs')
249
+ .action(async (intent, opts, command) => {
250
+ const { api, workflowId } = requireContext(command);
251
+ let slugs;
252
+ if (opts.node) {
253
+ slugs = [opts.node];
254
+ }
255
+ else {
256
+ const nodes = await api.tool(workflowId, 'search_nodes', {
257
+ query: intent,
258
+ hasActions: true,
259
+ limit: 10,
260
+ });
261
+ slugs = (nodes?.data ?? []).map((row) => row.slug).filter(Boolean);
262
+ }
263
+ const batches = await Promise.all(slugs.map(async (nodeSlug) => {
264
+ const result = await api.tool(workflowId, 'search_node_components', {
265
+ nodeSlug,
266
+ query: intent,
267
+ limit: 20,
268
+ });
269
+ return (result?.data ?? []).map((component) => ({
270
+ ...component,
271
+ nodeSlug: component.nodeSlug ?? nodeSlug,
272
+ }));
273
+ }));
274
+ const rows = batches
275
+ .flat()
276
+ .map(component => ({ ...component, score: searchScore(intent, component) }))
277
+ .sort((left, right) => right.score - left.score || left.key.localeCompare(right.key))
278
+ .slice(0, Math.max(1, Number(opts.limit) || 8));
279
+ emit({ intent, items: rows }, () => printTable(['nodeSlug', 'nodeKey', 'action', 'auth', 'score'], rows.map(row => [
280
+ row.nodeSlug,
281
+ row.key,
282
+ row.name,
283
+ row.nodeAuthRequired ? 'yes' : 'no',
284
+ row.score,
285
+ ])));
286
+ });
287
+ node
288
+ .command('components')
289
+ .argument('[query]')
290
+ .requiredOption('--node <slug>', 'node slug whose components to list')
291
+ .option('--limit <n>', 'max rows', '25')
292
+ .description('List a node’s action components (the --key values for `node add`)')
293
+ .action(async (query, opts, command) => {
294
+ const { api, workflowId } = requireContext(command);
295
+ const result = await api.tool(workflowId, 'search_node_components', {
296
+ nodeSlug: opts.node,
297
+ ...(query ? { query } : {}),
298
+ limit: Number(opts.limit),
299
+ });
300
+ const rows = result?.data ?? [];
301
+ emit({ items: rows, pagination: result?.pagination ?? null }, () => printTable(['key', 'name', 'type', 'props'], rows.map(c => [c.key, c.name, c.type, (c.propNames ?? []).join(',')])));
302
+ });
303
+ node
304
+ .command('add')
305
+ .alias('init')
306
+ .requiredOption('--flow <flowId>')
307
+ .requiredOption('--key <componentKey>', 'component key, e.g. registry_http-http-request')
308
+ .option('--node <slug>', 'node slug (inferred from --key when omitted)')
309
+ .option('--name <displayName>', 'display name; also the outgoing connection label')
310
+ .option('--after <nodeId>', 'wire this upstream node into the new node at creation')
311
+ .option('--output <main|error>', 'which system output --after connects from', 'main')
312
+ .option('--connection <id>', 'bind an existing connection')
313
+ .option('--props <jsonOrFile>', 'initial prop map; direct values become {value:...}, advanced per-prop writes pass through')
314
+ .option('--enable-prop <names...>', 'optional prop names to materialize at creation')
315
+ .description('Initialize a node with its known connection, wiring, optional props, and values')
316
+ .action(async (opts, command) => {
317
+ const { api, workflowId } = requireContext(command);
318
+ const nodeSlug = await resolveSlug(api, workflowId, opts.key, opts.node);
319
+ const props = opts.props
320
+ ? normalizeNodePropWrites(readObjectOption(opts.props, '--props'))
321
+ : undefined;
322
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'add_node', {
323
+ flowId: opts.flow,
324
+ nodeSlug,
325
+ nodeKey: opts.key,
326
+ connectionId: opts.connection ? Number(opts.connection) : null,
327
+ ...(opts.name ? { displayName: opts.name } : {}),
328
+ ...(props ? { props } : {}),
329
+ ...(opts.enableProp?.length ? { enableOptionalProps: opts.enableProp } : {}),
330
+ ...(opts.after
331
+ ? { connectFrom: { sourceNodeId: opts.after, sourceOutput: opts.output } }
332
+ : {}),
333
+ }));
334
+ const [rawRecord, currentProps] = await Promise.all([
335
+ api.tool(workflowId, 'get_node', { nodeId: result.nodeId }),
336
+ loadProps(api, workflowId, result.nodeId),
337
+ ]);
338
+ const record = await addResolvedOutputType(api, workflowId, rawRecord);
339
+ const readiness = buildNodeReadiness(result.nodeId, record, currentProps);
340
+ const requestedPropNames = Object.keys(props ?? {});
341
+ const propsReadback = currentProps.filter(prop => requestedPropNames.includes(prop.name ?? prop.key ?? ''));
342
+ emit({ ...result, normalizedProps: props ?? {}, propsReadback, readiness }, () => printKeyValues([
343
+ ['nodeId', result.nodeId],
344
+ ['displayName', result.displayName],
345
+ ['position', JSON.stringify(result.position ?? {})],
346
+ ['propsSet', (result.propsSet ?? []).join(', ') || '(none)'],
347
+ [
348
+ 'configuration blockers',
349
+ readiness.blockedBy.filter(blocker => blocker !== 'output type').join(', ') || '(none)',
350
+ ],
351
+ ['next', `cai node status ${result.nodeId} — verify before expressions`],
352
+ ]));
353
+ });
354
+ node
355
+ .command('component')
356
+ .requiredOption('--node <slug>', 'integration node slug')
357
+ .requiredOption('--key <componentKey>', 'component key from `cai node components`')
358
+ .description('Full static prop schema for a component BEFORE adding it — no node needed')
359
+ .action(async (opts, command) => {
360
+ const { api, workflowId } = requireContext(command);
361
+ const data = await api.tool(workflowId, 'get_node_component', {
362
+ nodeSlug: opts.node,
363
+ nodeKey: opts.key,
364
+ });
365
+ // The payload nests: {node:{slug,name,authRequired}, component:{key,name,
366
+ // description,type,outputDataType}, props:[…]} — nothing useful is at the
367
+ // top level, which is easy to miss when reading only the tool description.
368
+ const props = data?.props ?? data?.component?.props ?? [];
369
+ const component = data?.component ?? {};
370
+ emit(data, () => {
371
+ printKeyValues([
372
+ ['key', component.key ?? opts.key],
373
+ ['name', component.name ?? '-'],
374
+ [
375
+ 'node',
376
+ `${data?.node?.slug ?? opts.node}${data?.node?.authRequired ? ' (auth required)' : ''}`,
377
+ ],
378
+ ['outputType', component.outputDataType ?? data?.observedOutputType ?? '(unresolved)'],
379
+ ['description', truncate(component.description, 70)],
380
+ ]);
381
+ process.stdout.write('\nprops\n');
382
+ printTable(['name', 'inputType', 'required', 'default', 'remoteOptions', 'reloadProps'], props.map(p => [
383
+ p.name,
384
+ p.inputType,
385
+ p.required ? 'yes' : '',
386
+ truncate(p.defaultValue, 24),
387
+ p.remoteOptions ? 'yes — pick with `cai node options` after adding' : '',
388
+ p.reloadProps ? 'yes — reveals dependent props after adding' : '',
389
+ ]));
390
+ });
391
+ });
392
+ node
393
+ .command('duplicate')
394
+ .argument('<nodeId>')
395
+ .description('Copy a node into the same flow (offset position, no incoming edges)')
396
+ .action(async (nodeId, _opts, command) => {
397
+ const { api, workflowId } = requireContext(command);
398
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'duplicate_node', { nodeId }));
399
+ warn('A duplicate copies props but NOT the wiring: any prop expression that referenced an ' +
400
+ 'input of the original now has no such input on the copy. Re-wire it with ' +
401
+ '`cai node connect` and re-read `cai expr context` before trusting its expressions.');
402
+ emit(result, () => printKeyValues([
403
+ ['source', nodeId],
404
+ ['copy', result?.newNodeId ?? result?.nodeId ?? JSON.stringify(result)],
405
+ ]));
406
+ });
407
+ node
408
+ .command('set-flow')
409
+ .argument('<nodeId>', 'a Run flow / Run flow on list node')
410
+ .requiredOption('--flow <flowId>', 'target flow to call')
411
+ .description('Point a subflow node at a flow; its input props are generated from that flow')
412
+ .action(async (nodeId, opts, command) => {
413
+ const { api, workflowId } = requireContext(command);
414
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'set_subflow_node_flow', { nodeId, flowId: opts.flow }));
415
+ warn('The mapping props are synthesized from the target flow’s CURRENT inputs. A flow input ' +
416
+ 'added later does not appear on this node until it is re-linked — re-run this command ' +
417
+ 'after changing the target flow’s inputs.');
418
+ emit(result, () => printKeyValues([
419
+ ['nodeId', nodeId],
420
+ ['flowId', opts.flow],
421
+ ['next', 'cai node props ' + nodeId + ' — one mapping prop per flow input'],
422
+ ]));
423
+ });
424
+ node
425
+ .command('get')
426
+ .argument('<nodeId>')
427
+ .description('Full node record: component, inputs, outputs, connection')
428
+ .action(async (nodeId, _opts, command) => {
429
+ const { api, workflowId } = requireContext(command);
430
+ const data = await api.tool(workflowId, 'get_node', { nodeId });
431
+ emit(data, () => {
432
+ const processOutput = nodeProcessOutput(data);
433
+ printKeyValues([
434
+ ['id', data.id ?? nodeId],
435
+ ['displayName', data.displayName ?? data.data?.displayName],
436
+ ['component', data.nodeComponentKey ?? data.data?.nodeComponentKey],
437
+ ['connectionId', data.connectionId ?? data.data?.connectionId ?? '(none)'],
438
+ [
439
+ 'outputType',
440
+ processOutput?.dataType
441
+ ? renderNodeOutputType(processOutput.dataType, processOutput.declared === true)
442
+ : '(none)',
443
+ ],
444
+ ]);
445
+ const inputs = data.inputs ?? data.data?.inputs ?? [];
446
+ if (inputs.length) {
447
+ process.stdout.write('\ninputs\n');
448
+ printTable(['id', 'displayName', 'dataType'], inputs.map((i) => [i.id, i.displayName, i.dataType]));
449
+ }
450
+ const outputs = data.outputs ?? data.data?.outputs ?? [];
451
+ if (outputs.length) {
452
+ process.stdout.write('\noutputs\n');
453
+ printTable(['id', 'displayName', 'dataType'], outputs.map((o) => [o.id, o.displayName, o.dataType]));
454
+ }
455
+ });
456
+ });
457
+ node
458
+ .command('declare-output')
459
+ .argument('<nodeId>')
460
+ .requiredOption('--sample-file <file|->', 'documented response data JSON object; `-` reads stdin')
461
+ .description('Declare an unverified dynamic output contract from documented response data')
462
+ .action(async (nodeId, opts, command) => {
463
+ const sample = readNodeOutputSample(opts.sampleFile);
464
+ const { api, workflowId } = requireContext(command);
465
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'declare_node_output_type', {
466
+ nodeId,
467
+ sample,
468
+ }));
469
+ emit(result, () => {
470
+ printKeyValues([
471
+ ['dataTypeName', result.dataTypeName],
472
+ ['status', `${result.status} (unverified)`],
473
+ ]);
474
+ process.stdout.write(`\nThis contract comes from documentation, not a run. ` +
475
+ `The first real completed run verifies it automatically. ` +
476
+ `Downstream: re-read \`cai node status ${nodeId}\` and \`cai expr context\`.\n`);
477
+ });
478
+ });
479
+ node
480
+ .command('props')
481
+ .argument('<nodeId>')
482
+ .description('Visible + catalog props with current values and decompiled valueJs')
483
+ .action(async (nodeId, _opts, command) => {
484
+ const { api, workflowId } = requireContext(command);
485
+ const data = await api.tool(workflowId, 'get_node_props', { nodeId });
486
+ const props = nodePropsFromPayload(data);
487
+ emit(data, () => printTable(['name', 'inputType', 'required', 'enabled', 'value', 'valueJs', 'expressionValueJs'], props.map(p => [
488
+ p.name ?? p.key,
489
+ p.inputType,
490
+ p.required ? 'yes' : '',
491
+ p.enabled === false ? 'no' : 'yes',
492
+ truncate(p.value, 40),
493
+ truncate(p.valueJs, 40),
494
+ truncate(p.expressionValueJs, 40),
495
+ ])));
496
+ });
497
+ node
498
+ .command('status')
499
+ .argument('<nodeId>')
500
+ .description('Return deterministic configuration readiness and exact blockers')
501
+ .action(async (nodeId, _opts, command) => {
502
+ const { api, workflowId } = requireContext(command);
503
+ const [record, props] = await Promise.all([
504
+ api.tool(workflowId, 'get_node', { nodeId }),
505
+ loadProps(api, workflowId, nodeId),
506
+ ]);
507
+ const resolvedRecord = await addResolvedOutputType(api, workflowId, record);
508
+ const readiness = buildNodeReadiness(nodeId, resolvedRecord, props);
509
+ const outputTypeDeclared = nodeProcessOutput(resolvedRecord)?.declared === true;
510
+ const payload = {
511
+ ...readiness,
512
+ ...(outputTypeDeclared ? { outputTypeDeclared: true } : {}),
513
+ };
514
+ emit(payload, () => printKeyValues([
515
+ ['nodeId', nodeId],
516
+ [
517
+ 'connection',
518
+ payload.missingConnection ? 'missing' : (payload.connectionId ?? 'not required'),
519
+ ],
520
+ ['required props', payload.missingRequiredProps.join(', ') || 'complete'],
521
+ ['reload selectors', payload.pendingReloadProps.join(', ') || 'complete'],
522
+ [
523
+ 'output type',
524
+ `${payload.outputTypeStatus}${payload.outputTypes.length
525
+ ? ` (${payload.outputTypes
526
+ .map(type => renderNodeOutputType(type, outputTypeDeclared && payload.outputTypeSource === 'processOutput'))
527
+ .join(', ')})`
528
+ : ''} via ${payload.outputTypeSource}`,
529
+ ],
530
+ ['ready for expressions', payload.readyForExpressions ? 'yes' : 'no'],
531
+ ['ready to run', payload.readyForRun ? 'yes' : 'no'],
532
+ [
533
+ 'ready for downstream expressions',
534
+ payload.readyForDownstreamExpressions ? 'yes' : 'no',
535
+ ],
536
+ ['next', payload.next],
537
+ ]));
538
+ });
539
+ node
540
+ .command('set')
541
+ .argument('<nodeId>')
542
+ .requiredOption('--prop <name>', 'prop key from `cai node props`')
543
+ .option('--value <literal>', 'static value (JSON when it parses, else a string)')
544
+ .option('--js <src>', 'expression DSL source, compiled server-side before writing')
545
+ .option('--js-file <path|->', 'read expression DSL source from a UTF-8 file or stdin')
546
+ .option('--label <labels...>', 'live option label(s), one per value; used to search providers that do not index opaque ids')
547
+ .option('--expression', 'deprecated compatibility flag; storage is selected automatically', false)
548
+ .description('Set a prop, selecting and verifying the correct storage shape automatically')
549
+ .action(async (nodeId, opts, command) => {
550
+ const js = resolveJsSource(opts, { required: false });
551
+ const { api, workflowId } = requireContext(command);
552
+ if (js === undefined && opts.value === undefined) {
553
+ throw userError('Pass --value <literal>, --js <expression>, or --js-file <path|->.', 'MISSING_VALUE');
554
+ }
555
+ if (js !== undefined && opts.value !== undefined) {
556
+ throw userError('--value is mutually exclusive with --js and --js-file.', 'CONFLICTING_VALUE');
557
+ }
558
+ const prop = await findProp(api, workflowId, nodeId, opts.prop);
559
+ const persistedExpressionChannel = expressionReadbackChannel(prop);
560
+ if (opts.expression) {
561
+ warn('`--expression` is no longer needed; the CLI selects the correct storage channel.');
562
+ }
563
+ if (js !== undefined) {
564
+ // Compile FIRST. update_node_prop would also compile, but validating as
565
+ // a separate read-only call guarantees a failed expression writes
566
+ // nothing and lets us print the compiler error verbatim.
567
+ const validation = await api.tool(workflowId, 'validate_expression', {
568
+ js,
569
+ nodeId,
570
+ propName: opts.prop,
571
+ });
572
+ if (validationFailed(validation)) {
573
+ throw userError(`Expression did not compile — nothing was written.\n\n${validationMessage(validation)}`, 'EXPRESSION_COMPILE_ERROR');
574
+ }
575
+ for (const w of validation?.warnings ?? [])
576
+ warn(String(w));
577
+ }
578
+ const payload = { nodeId, key: opts.prop };
579
+ if (js !== undefined) {
580
+ // valueJs is intentionally the one public write channel. The backend
581
+ // resolves the prop's real compile target and routes the compiled value
582
+ // into value or expressionValue as appropriate.
583
+ payload.valueJs = js;
584
+ }
585
+ else if (isTextExpressionBackedProp(prop)) {
586
+ // These props persist as a TextExpression wrapping the raw TEXT. The backend does
587
+ // that wrapping only when it receives a string — hand it a parsed object and it
588
+ // stores the object verbatim, producing a shape the runtime cannot evaluate.
589
+ // JSON bodies are the common case: `{"a":1}` is valid JSON, so a general-purpose
590
+ // literal parser eats it. Found by CFG-010.
591
+ payload.value = opts.value;
592
+ }
593
+ else {
594
+ payload.value = parseLiteral(opts.value);
595
+ }
596
+ const suppliedLabels = Array.isArray(opts.label)
597
+ ? opts.label
598
+ : opts.label
599
+ ? [opts.label]
600
+ : [];
601
+ if (suppliedLabels.length && !isOptionBackedProp(prop)) {
602
+ payload.valueLabel = suppliedLabels.length === 1 ? suppliedLabels[0] : suppliedLabels;
603
+ }
604
+ // An option-backed prop carries BOTH a value and the human label the UI showed. Resolve
605
+ // both from live options so a missing or wrongly-shaped label cannot write a
606
+ // half-configured prop (CFG-004, CFG-012). When a provider does not index opaque ids,
607
+ // caller-supplied --label is used only as the search query; the returned value must still
608
+ // match before anything is persisted.
609
+ if (js === undefined) {
610
+ const selected = Array.isArray(payload.value) ? payload.value : [payload.value];
611
+ const queries = optionLookupQueries(selected, suppliedLabels);
612
+ const optionBatches = prop.remoteOptions
613
+ ? await Promise.all(queries.map(query => fetchPropOptions(api, workflowId, nodeId, opts.prop, query)))
614
+ : [
615
+ (prop.options ?? []).map(option => ({
616
+ value: String(option.value ?? option.id ?? ''),
617
+ label: String(option.label ?? option.name ?? option.value ?? option.id ?? ''),
618
+ })),
619
+ ];
620
+ const options = [
621
+ ...new Map(optionBatches.flat().map(option => [option.value, option])).values(),
622
+ ];
623
+ if (prop.remoteOptions && options.length === 0) {
624
+ throw userError(`No live options were returned for ${opts.prop}. Check the connection and earlier ` +
625
+ 'selector props. If you copied an opaque value from `cai node options`, also pass ' +
626
+ 'its displayed label with `--label`; nothing was written.', 'OPTIONS_UNAVAILABLE');
627
+ }
628
+ if (options.length) {
629
+ const labelFor = (value) => options.find(option => option.value === String(value));
630
+ const unknown = selected.filter(value => !labelFor(value)).map(value => String(value));
631
+ if (unknown.length) {
632
+ throw userError(`${unknown.map(value => `"${value}"`).join(', ')} is not a valid option for ` +
633
+ `${opts.prop}. Run \`cai node options ${nodeId} --prop ${opts.prop} --query ` +
634
+ `"<display label>"\`, then retry with the exact returned --value and --label. ` +
635
+ 'Nothing was written.', 'UNKNOWN_OPTION');
636
+ }
637
+ if (isOptionBackedProp(prop)) {
638
+ const resolved = selected.map(value => labelFor(value));
639
+ payload.valueLabel = isMultiSelectProp(prop)
640
+ ? resolved.map(option => ({ value: option.value, label: option.label }))
641
+ : resolved[0].label;
642
+ }
643
+ }
644
+ }
645
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'update_node_prop', payload));
646
+ const retryCommand = `cai node set ${nodeId} --prop ${opts.prop}` +
647
+ (opts.value !== undefined
648
+ ? ` --value ${shellQuote(String(opts.value))}` +
649
+ (suppliedLabels.length
650
+ ? ` --label ${suppliedLabels.map(label => shellQuote(label)).join(' ')}`
651
+ : '')
652
+ : ' --value <same-value>');
653
+ requireCompletedPropReload(result, nodeId, opts.prop, retryCommand);
654
+ const readback = await findProp(api, workflowId, nodeId, opts.prop);
655
+ const persistedChannel = js !== undefined ? persistedExpressionChannel : 'value';
656
+ if (js !== undefined && !readback[persistedChannel]) {
657
+ throw userError(`The write returned successfully but ${opts.prop}.${persistedChannel} did not read back.`, 'READBACK_MISMATCH');
658
+ }
659
+ if (js === undefined && readback.value === undefined) {
660
+ throw userError(`The write returned successfully but ${opts.prop}.value did not read back.`, 'READBACK_MISMATCH');
661
+ }
662
+ emit({
663
+ nodeId,
664
+ prop: opts.prop,
665
+ mode: js !== undefined ? 'js' : 'static',
666
+ persistedChannel,
667
+ readback,
668
+ result,
669
+ }, () => printKeyValues([
670
+ ['nodeId', nodeId],
671
+ ['prop', opts.prop],
672
+ ['mode', js !== undefined ? 'expression (compiled)' : 'static'],
673
+ ['persisted channel', persistedChannel],
674
+ ['verified', 'yes'],
675
+ ]));
676
+ });
677
+ node
678
+ .command('set-connection')
679
+ .argument('<nodeId>')
680
+ .requiredOption('--connection <id>', 'connection id from `cai connection list`')
681
+ .description('Bind a connection to an action node')
682
+ .action(async (nodeId, opts, command) => {
683
+ const { api, workflowId } = requireContext(command);
684
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'update_node_connection', {
685
+ nodeId,
686
+ connectionId: String(opts.connection),
687
+ }));
688
+ emit({ nodeId, connectionId: opts.connection, result }, () => printKeyValues([
689
+ ['nodeId', nodeId],
690
+ ['connectionId', opts.connection],
691
+ ]));
692
+ });
693
+ node
694
+ .command('connect')
695
+ .requiredOption('--from <nodeId>', 'upstream node')
696
+ .requiredOption('--to <nodeId>', 'downstream node')
697
+ .requiredOption('--flow <flowId>', 'flow containing both nodes')
698
+ .option('--output <main|error>', 'which system output to wire from', 'main')
699
+ .description('Wire two nodes; the source output becomes an input on the target')
700
+ .action(async (opts, command) => {
701
+ const { api, workflowId } = requireContext(command);
702
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'connect_nodes', {
703
+ flowId: opts.flow,
704
+ sourceNodeId: opts.from,
705
+ targetNodeId: opts.to,
706
+ sourceOutput: opts.output,
707
+ }));
708
+ emit(result, () => printKeyValues([
709
+ ['from', `${opts.from} (${opts.output})`],
710
+ ['to', opts.to],
711
+ ['result', JSON.stringify(result)],
712
+ ]));
713
+ });
714
+ node
715
+ .command('disconnect')
716
+ .requiredOption('--edge <edgeId>')
717
+ .description('Remove an edge')
718
+ .action(async (opts, command) => {
719
+ const { api, workflowId } = requireContext(command);
720
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'disconnect_nodes', { edgeId: opts.edge }));
721
+ emit(result, () => printKeyValues([['removed', opts.edge]]));
722
+ });
723
+ node
724
+ .command('only-when')
725
+ .argument('<nodeId>')
726
+ .option('--js <expr>', 'boolean expression; the node runs only when it is true')
727
+ .option('--js-file <path|->', 'read the boolean expression from a UTF-8 file or stdin')
728
+ .option('--clear', 'remove all conditions', false)
729
+ .description('Set or clear a node’s conditional-execution rule')
730
+ .action(async (nodeId, opts, command) => {
731
+ const js = resolveJsSource(opts, { required: false });
732
+ const { api, workflowId } = requireContext(command);
733
+ if (js === undefined && !opts.clear) {
734
+ throw userError('Pass --js <expression>, --js-file <path|->, or --clear.', 'MISSING_CONDITION');
735
+ }
736
+ if (js !== undefined && opts.clear) {
737
+ throw userError('--clear is mutually exclusive with --js and --js-file.', 'CONFLICTING_CONDITION');
738
+ }
739
+ if (js !== undefined) {
740
+ const validation = await api.tool(workflowId, 'validate_expression', {
741
+ js,
742
+ nodeId,
743
+ mode: 'onlyWhen',
744
+ });
745
+ if (validationFailed(validation)) {
746
+ throw userError(`onlyWhen expression did not compile — nothing was written.\n\n${validationMessage(validation)}`, 'EXPRESSION_COMPILE_ERROR');
747
+ }
748
+ }
749
+ const onlyWhen = opts.clear
750
+ ? { type: 'group', operator: 'OR', rules: [] }
751
+ : {
752
+ type: 'group',
753
+ operator: 'OR',
754
+ rules: [
755
+ {
756
+ type: 'group',
757
+ operator: 'AND',
758
+ rules: [{ type: 'rule', value: js }],
759
+ },
760
+ ],
761
+ };
762
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'set_node_only_when', { nodeId, onlyWhen }));
763
+ emit({ nodeId, cleared: Boolean(opts.clear), js: js ?? null, result }, () => printKeyValues([
764
+ ['nodeId', nodeId],
765
+ ['onlyWhen', opts.clear ? '(cleared)' : js],
766
+ ]));
767
+ });
768
+ node
769
+ .command('optional-props')
770
+ .argument('<nodeId>')
771
+ .description('List togglable optional props')
772
+ .action(async (nodeId, _opts, command) => {
773
+ const { api, workflowId } = requireContext(command);
774
+ const data = await api.tool(workflowId, 'get_node_optional_props', { nodeId });
775
+ const rows = Array.isArray(data) ? data : (data?.props ?? []);
776
+ emit(data, () => printTable(['name', 'displayName', 'inputType', 'enabled'], rows.map(p => [p.name, p.displayName, p.inputType, p.enabled ? 'yes' : 'no'])));
777
+ });
778
+ node
779
+ .command('enable-prop')
780
+ .argument('<nodeId>')
781
+ .requiredOption('--prop <name>')
782
+ .description('Enable an optional prop so it becomes settable')
783
+ .action(async (nodeId, opts, command) => {
784
+ const { api, workflowId } = requireContext(command);
785
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'enable_optional_prop', { nodeId, propName: opts.prop }));
786
+ emit(result, () => printKeyValues([['enabled', `${nodeId}.${opts.prop}`]]));
787
+ });
788
+ node
789
+ .command('options')
790
+ .argument('<nodeId>')
791
+ .requiredOption('--prop <name>')
792
+ // keep in sync with fetchPropOptions() below, which reuses this tool
793
+ .option('--query <q>', 'search term — strongly preferred over paging')
794
+ .option('--page <n>', '0-based page when not searching')
795
+ .description('Resolved remote dropdown options for a prop')
796
+ .action(async (nodeId, opts, command) => {
797
+ const { api, workflowId } = requireContext(command);
798
+ const page = parseNodeOptionPage(opts.page);
799
+ const data = await api.tool(workflowId, 'get_prop_options', {
800
+ nodeId,
801
+ propName: opts.prop,
802
+ ...(opts.query ? { query: opts.query } : {}),
803
+ ...(page !== undefined ? { page } : {}),
804
+ });
805
+ const rows = data?.options ?? (Array.isArray(data) ? data : []);
806
+ emit(data, () => printTable(['value', 'label'], rows.map(o => [o.value ?? o.id, o.label ?? o.name])));
807
+ });
808
+ node
809
+ .command('continue-on-error')
810
+ .argument('<nodeId>')
811
+ .option('--enable', 'ensure continue-on-error is enabled')
812
+ .option('--disable', 'ensure continue-on-error is disabled')
813
+ .description('Idempotently set continue-on-error after reading the current state')
814
+ .action(async (nodeId, opts, command) => {
815
+ const { api, workflowId } = requireContext(command);
816
+ if (opts.enable && opts.disable) {
817
+ throw userError('--enable and --disable are mutually exclusive.', 'CONFLICTING_STATE');
818
+ }
819
+ if (!opts.enable && !opts.disable) {
820
+ throw userError('Pass --enable or --disable.', 'MISSING_STATE');
821
+ }
822
+ const before = await api.tool(workflowId, 'get_node', { nodeId });
823
+ const current = Boolean(before.continueOnError ?? before.data?.continueOnError);
824
+ const desired = Boolean(opts.enable);
825
+ let result = { unchanged: true };
826
+ if (current !== desired) {
827
+ result = await api.mutate(workflowId, () => api.tool(workflowId, 'toggle_continue_on_error', { nodeId }));
828
+ }
829
+ const after = await api.tool(workflowId, 'get_node', { nodeId });
830
+ const verified = Boolean(after.continueOnError ?? after.data?.continueOnError) === desired;
831
+ if (!verified) {
832
+ throw userError('Continue-on-error did not read back as requested.', 'READBACK_MISMATCH');
833
+ }
834
+ emit({ nodeId, enabled: desired, changed: current !== desired, result }, () => printKeyValues([
835
+ ['nodeId', nodeId],
836
+ ['continue on error', desired ? 'enabled' : 'disabled'],
837
+ ['changed', current !== desired ? 'yes' : 'no'],
838
+ ['verified', 'yes'],
839
+ ]));
840
+ });
841
+ node
842
+ .command('rename')
843
+ .argument('<nodeId>')
844
+ .requiredOption('--name <displayName>')
845
+ .description('Rename a node (also renames its outgoing connection label)')
846
+ .action(async (nodeId, opts, command) => {
847
+ const { api, workflowId } = requireContext(command);
848
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'update_node_display_name', { nodeId, displayName: opts.name }));
849
+ emit(result, () => printKeyValues([
850
+ ['nodeId', nodeId],
851
+ ['displayName', opts.name],
852
+ ]));
853
+ });
854
+ node
855
+ .command('delete')
856
+ .argument('<nodeId>')
857
+ .description('Delete a node')
858
+ .action(async (nodeId, _opts, command) => {
859
+ const { api, workflowId } = requireContext(command);
860
+ const result = await api.mutate(workflowId, () => api.tool(workflowId, 'delete_node', { nodeId }));
861
+ emit(result, () => printKeyValues([['deleted', nodeId]]));
862
+ });
863
+ };
864
+ /** `--value 42` should store a number, `--value hello` a string. */
865
+ export const parseLiteral = (raw) => {
866
+ if (raw === undefined)
867
+ return raw;
868
+ const trimmed = raw.trim();
869
+ if (trimmed === '')
870
+ return raw;
871
+ if (/^(true|false|null)$/.test(trimmed))
872
+ return JSON.parse(trimmed);
873
+ if (/^-?\d+(\.\d+)?$/.test(trimmed))
874
+ return Number(trimmed);
875
+ if (/^[[{"]/.test(trimmed)) {
876
+ try {
877
+ return JSON.parse(trimmed);
878
+ }
879
+ catch {
880
+ return raw;
881
+ }
882
+ }
883
+ return raw;
884
+ };
885
+ //# sourceMappingURL=node.js.map