@agentled/cli 0.7.1 → 0.7.4

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.
@@ -37,7 +37,26 @@ function formatIssueLine(issue) {
37
37
  async function runAutoValidate(client, workflowId, opts) {
38
38
  let result;
39
39
  try {
40
- result = await client.validateWorkflow(workflowId);
40
+ let pipeline;
41
+ let validationSource = 'live';
42
+ if (opts.source === 'draft' || opts.source === 'auto') {
43
+ try {
44
+ const draft = await client.getDraft(workflowId);
45
+ if (draft?.draft?.config) {
46
+ pipeline = draft.draft.config;
47
+ validationSource = 'draft';
48
+ }
49
+ else if (opts.source === 'draft') {
50
+ throw new Error(`No draft exists for workflow ${workflowId}`);
51
+ }
52
+ }
53
+ catch (e) {
54
+ if (opts.source === 'draft')
55
+ throw e;
56
+ }
57
+ }
58
+ result = await client.validateWorkflow(workflowId, pipeline);
59
+ result = { ...result, source: validationSource };
41
60
  }
42
61
  catch (e) {
43
62
  // The workflow was saved; only the post-hoc validate call failed.
@@ -51,6 +70,9 @@ async function runAutoValidate(client, workflowId, opts) {
51
70
  if (opts.format !== 'json') {
52
71
  const errorCount = result.errors?.length ?? 0;
53
72
  const warnCount = result.warnings?.length ?? 0;
73
+ if (result.source && result.source !== 'live') {
74
+ console.log(` Source: ${result.source}`);
75
+ }
54
76
  if (errorCount === 0 && warnCount === 0) {
55
77
  console.log(` ✓ Validated (0 errors, 0 warnings) — ${workflowId}`);
56
78
  }
@@ -90,6 +112,145 @@ function exitCodeForOutcome(outcome) {
90
112
  return VALIDATION_EXIT_CODE;
91
113
  return 1;
92
114
  }
115
+ const CONFIG_UPDATE_FIELDS = ['name', 'goal', 'description', 'steps', 'context', 'style', 'analyticsConfig'];
116
+ const READ_ONLY_WORKFLOW_FIELDS = new Set([
117
+ '_agentled',
118
+ 'id',
119
+ 'workspaceId',
120
+ 'workspace',
121
+ 'createdAt',
122
+ 'updatedAt',
123
+ 'owner',
124
+ 'executions',
125
+ 'executionInputs',
126
+ 'computedMetrics',
127
+ 'notificationBadge',
128
+ 'agents',
129
+ 'team',
130
+ 'hasDraftSnapshot',
131
+ 'draftSnapshot',
132
+ 'urls',
133
+ ]);
134
+ function normalizeRevision(value) {
135
+ const revision = typeof value === 'number'
136
+ ? value
137
+ : typeof value === 'string'
138
+ ? Number.parseInt(value, 10)
139
+ : Number.NaN;
140
+ return Number.isFinite(revision) && revision >= 0 ? revision : undefined;
141
+ }
142
+ function pickWorkflowConfig(workflow) {
143
+ const config = {};
144
+ for (const key of CONFIG_UPDATE_FIELDS) {
145
+ if (workflow[key] !== undefined)
146
+ config[key] = workflow[key];
147
+ }
148
+ return config;
149
+ }
150
+ function getWorkflowFromResponse(result) {
151
+ return (result?.workflow && typeof result.workflow === 'object') ? result.workflow : result;
152
+ }
153
+ function normalizeWorkflowUpdateFile(parsed) {
154
+ let source;
155
+ let updates = parsed;
156
+ if (parsed?.export?.pipeline) {
157
+ updates = parsed.export.pipeline;
158
+ source = parsed.export.sourceWorkflow;
159
+ }
160
+ else if (parsed?.exportVersion && parsed?.pipeline) {
161
+ updates = parsed.pipeline;
162
+ source = parsed.sourceWorkflow;
163
+ }
164
+ else if (parsed?.workflow) {
165
+ updates = parsed.workflow;
166
+ source = {
167
+ workflowId: parsed.workflow.id,
168
+ updatedAt: parsed.workflow.updatedAt,
169
+ revision: normalizeRevision(parsed.workflow.metadata?.revision),
170
+ status: parsed.workflow.status,
171
+ };
172
+ }
173
+ else if (parsed?._agentled) {
174
+ source = {
175
+ workflowId: parsed._agentled.workflowId,
176
+ updatedAt: parsed._agentled.updatedAt,
177
+ revision: normalizeRevision(parsed._agentled.revision),
178
+ status: parsed._agentled.status,
179
+ };
180
+ }
181
+ const cleaned = {};
182
+ for (const [key, value] of Object.entries(updates || {})) {
183
+ if (!READ_ONLY_WORKFLOW_FIELDS.has(key))
184
+ cleaned[key] = value;
185
+ }
186
+ return { updates: cleaned, source };
187
+ }
188
+ function hasConfigUpdate(updates) {
189
+ return CONFIG_UPDATE_FIELDS.some((field) => Object.prototype.hasOwnProperty.call(updates, field));
190
+ }
191
+ function buildPulledWorkflowFile(workflow) {
192
+ return {
193
+ _agentled: {
194
+ kind: 'workflow-update-file',
195
+ workflowId: workflow.id,
196
+ updatedAt: workflow.updatedAt,
197
+ revision: normalizeRevision(workflow.metadata?.revision),
198
+ status: workflow.status,
199
+ pulledAt: new Date().toISOString(),
200
+ },
201
+ ...pickWorkflowConfig(workflow),
202
+ };
203
+ }
204
+ function diffWorkflowConfig(remote, updates) {
205
+ const changedFields = CONFIG_UPDATE_FIELDS.filter((field) => Object.prototype.hasOwnProperty.call(updates, field)
206
+ && JSON.stringify(remote[field] ?? null) !== JSON.stringify(updates[field] ?? null));
207
+ const remoteSteps = Array.isArray(remote.steps) ? remote.steps : [];
208
+ const nextSteps = Array.isArray(updates.steps) ? updates.steps : remoteSteps;
209
+ const remoteIds = new Set(remoteSteps.map((step) => step?.id).filter(Boolean));
210
+ const nextIds = new Set(nextSteps.map((step) => step?.id).filter(Boolean));
211
+ const addedSteps = Array.from(nextIds).filter((id) => !remoteIds.has(id));
212
+ const removedSteps = Array.from(remoteIds).filter((id) => !nextIds.has(id));
213
+ const changedSteps = nextSteps
214
+ .filter((step) => step?.id && remoteIds.has(step.id))
215
+ .filter((step) => {
216
+ const previous = remoteSteps.find((candidate) => candidate?.id === step.id);
217
+ return JSON.stringify(previous ?? null) !== JSON.stringify(step ?? null);
218
+ })
219
+ .map((step) => step.id);
220
+ return { changedFields, addedSteps, removedSteps, changedSteps };
221
+ }
222
+ async function verifySafeFileUpdate(client, workflowId, filePath, updates, source, opts) {
223
+ if (!hasConfigUpdate(updates) || opts.force)
224
+ return true;
225
+ const remote = getWorkflowFromResponse(await client.getWorkflow(workflowId));
226
+ const expectedUpdatedAt = opts.expectedUpdatedAt || source?.updatedAt;
227
+ const expectedRevision = normalizeRevision(source?.revision);
228
+ const remoteRevision = normalizeRevision(remote.metadata?.revision);
229
+ if (source?.workflowId && source.workflowId !== workflowId) {
230
+ printError(`File was pulled from workflow ${source.workflowId}, not ${workflowId}. Pull the target workflow first or use --force.`);
231
+ return false;
232
+ }
233
+ if (expectedRevision !== undefined && remoteRevision !== undefined) {
234
+ if (expectedRevision !== remoteRevision) {
235
+ printError(`Refusing stale update. File is based on revision ${expectedRevision}, but remote is revision ${remoteRevision}. ` +
236
+ `Run: agentled workflows pull ${workflowId} --output ${filePath} --force, compare/re-apply your edits, then update or replace again.`);
237
+ return false;
238
+ }
239
+ return true;
240
+ }
241
+ if (!expectedUpdatedAt) {
242
+ printError('Refusing config update from a file without a remote revision or updatedAt token. ' +
243
+ `Run: agentled workflows pull ${workflowId} --output ${filePath} --force, re-apply your edits, then run workflows diff/replace. ` +
244
+ 'Use --force only when you intentionally want to overwrite remote config.');
245
+ return false;
246
+ }
247
+ if (expectedUpdatedAt !== remote.updatedAt) {
248
+ printError(`Refusing stale update. File is based on ${expectedUpdatedAt}, but remote is ${remote.updatedAt}. ` +
249
+ `Run: agentled workflows pull ${workflowId} --output ${filePath} --force, compare/re-apply your edits, then update or replace again.`);
250
+ return false;
251
+ }
252
+ return true;
253
+ }
93
254
  export function registerWorkflowCommands(program) {
94
255
  const workflows = program
95
256
  .command('workflows')
@@ -131,25 +292,39 @@ export function registerWorkflowCommands(program) {
131
292
  });
132
293
  workflows
133
294
  .command('pull <id>')
134
- .description('Pull a live workflow into examples/live/<name>-<id>.json and generate a test skeleton at tests/<id>.test.json. Use after `agentled init` to start iterating locally.')
295
+ .description('Pull a workflow into an update-safe local file with a remote updatedAt token. Use this before workflows update --file.')
296
+ .option('--output <path>', 'Write workflow config to a specific path')
135
297
  .option('--force', 'Overwrite existing files', false)
136
298
  .option('--format <fmt>', 'Output format', 'json')
137
299
  .action(async (id, opts) => {
138
300
  try {
139
301
  const client = new AgentledClient();
140
- const workflow = await client.getWorkflow(id);
141
- const pipeline = (workflow.pipeline ?? workflow);
302
+ const result = await client.getWorkflow(id);
303
+ const pipeline = getWorkflowFromResponse(result);
304
+ const updateFile = buildPulledWorkflowFile(pipeline);
142
305
  const wsDir = findWorkspaceDir();
143
306
  const name = String(pipeline.name ?? id).toLowerCase().replace(/[^a-z0-9]+/g, '-');
144
307
  const filename = `${name}-${id}.json`;
145
308
  let savedPath;
146
309
  let testPath = null;
147
310
  let testSkippedReason = null;
148
- if (wsDir) {
311
+ if (opts.output) {
312
+ savedPath = resolve(opts.output);
313
+ if (fs.existsSync(savedPath) && !opts.force) {
314
+ printError(`Refusing to overwrite ${savedPath}. Use --force to overwrite.`);
315
+ return;
316
+ }
317
+ fs.writeFileSync(savedPath, JSON.stringify(updateFile, null, 2) + '\n');
318
+ }
319
+ else if (wsDir) {
149
320
  const liveDir = join(wsDir, 'examples', 'live');
150
321
  fs.mkdirSync(liveDir, { recursive: true });
151
322
  savedPath = join(liveDir, filename);
152
- fs.writeFileSync(savedPath, JSON.stringify(pipeline, null, 2) + '\n');
323
+ if (fs.existsSync(savedPath) && !opts.force) {
324
+ printError(`Refusing to overwrite ${savedPath}. Use --force to overwrite.`);
325
+ return;
326
+ }
327
+ fs.writeFileSync(savedPath, JSON.stringify(updateFile, null, 2) + '\n');
153
328
  const testsDir = join(wsDir, 'tests');
154
329
  fs.mkdirSync(testsDir, { recursive: true });
155
330
  testPath = join(testsDir, `${id}.test.json`);
@@ -163,7 +338,11 @@ export function registerWorkflowCommands(program) {
163
338
  }
164
339
  else {
165
340
  savedPath = resolve(filename);
166
- fs.writeFileSync(savedPath, JSON.stringify(pipeline, null, 2) + '\n');
341
+ if (fs.existsSync(savedPath) && !opts.force) {
342
+ printError(`Refusing to overwrite ${savedPath}. Use --force to overwrite.`);
343
+ return;
344
+ }
345
+ fs.writeFileSync(savedPath, JSON.stringify(updateFile, null, 2) + '\n');
167
346
  }
168
347
  const allSteps = (Array.isArray(pipeline.steps) ? pipeline.steps : []);
169
348
  const testableCount = allSteps.filter((s) => ['aiAction', 'aiActionWithTools', 'appAction', 'code'].includes(String(s.type ?? ''))).length;
@@ -177,6 +356,9 @@ export function registerWorkflowCommands(program) {
177
356
  stepCount: allSteps.length,
178
357
  testableSteps: testableCount,
179
358
  nextSteps: [
359
+ `edit ${savedPath}`,
360
+ `agentled workflows diff ${id} --file ${savedPath}`,
361
+ `agentled workflows replace ${id} --file ${savedPath}`,
180
362
  `agentled workflows start ${id} # run once to capture fixtures`,
181
363
  `agentled fixture capture <execId> --wf ${id}`,
182
364
  `agentled test ${id} # run assertions against fixtures`,
@@ -187,6 +369,52 @@ export function registerWorkflowCommands(program) {
187
369
  printError(e instanceof Error ? e.message : String(e));
188
370
  }
189
371
  });
372
+ workflows
373
+ .command('diff <id>')
374
+ .description('Compare a local workflow update file with the current remote workflow before updating.')
375
+ .requiredOption('--file <path>', 'Path to workflow update JSON file')
376
+ .option('--format <fmt>', 'Output format', 'json')
377
+ .action(async (id, opts) => {
378
+ try {
379
+ const raw = fs.readFileSync(opts.file, 'utf-8');
380
+ const { updates, source } = normalizeWorkflowUpdateFile(JSON.parse(raw));
381
+ const client = new AgentledClient();
382
+ const remote = getWorkflowFromResponse(await client.getWorkflow(id));
383
+ const diff = diffWorkflowConfig(remote, updates);
384
+ const remoteRevision = normalizeRevision(remote.metadata?.revision);
385
+ const sourceRevision = normalizeRevision(source?.revision);
386
+ const sourceMatchesRemote = sourceRevision !== undefined && remoteRevision !== undefined
387
+ ? sourceRevision === remoteRevision
388
+ : source?.updatedAt
389
+ ? source.updatedAt === remote.updatedAt
390
+ : undefined;
391
+ printOutput({
392
+ workflowId: id,
393
+ file: resolve(opts.file),
394
+ remoteRevision,
395
+ fileSourceRevision: sourceRevision,
396
+ remoteUpdatedAt: remote.updatedAt,
397
+ fileSourceUpdatedAt: source?.updatedAt,
398
+ sourceMatchesRemote,
399
+ stale: sourceMatchesRemote === false,
400
+ ...diff,
401
+ summary: {
402
+ changedFields: diff.changedFields.length,
403
+ addedSteps: diff.addedSteps.length,
404
+ removedSteps: diff.removedSteps.length,
405
+ changedSteps: diff.changedSteps.length,
406
+ },
407
+ nextStep: sourceMatchesRemote === false
408
+ ? `Remote changed since this file was pulled. Run: agentled workflows pull ${id} --output ${opts.file} --force, then re-apply your edits.`
409
+ : `If this diff is intended, run: agentled workflows replace ${id} --file ${opts.file}`,
410
+ }, (opts.format ?? 'json'));
411
+ if (sourceMatchesRemote === false)
412
+ process.exitCode = 1;
413
+ }
414
+ catch (e) {
415
+ printError(e instanceof Error ? e.message : String(e));
416
+ }
417
+ });
190
418
  workflows
191
419
  .command('lint <file>')
192
420
  .description('Static gotcha checks for a pipeline JSON file (no API). Catches 11 documented failure modes (criteria/conditions, Gmail label_id, missing tools, email shape, etc.). For platform validators (graph wiring, variable resolution), run `agentled workflows validate <id>` after creating the workflow.')
@@ -296,15 +524,20 @@ export function registerWorkflowCommands(program) {
296
524
  .description('Update an existing workflow (auto-validates unless --skip-validate)')
297
525
  .option('--file <path>', 'Path to updates JSON file')
298
526
  .option('--updates <json>', 'Inline updates JSON')
527
+ .option('--expected-updated-at <timestamp>', 'Require the remote workflow updatedAt to match before applying --file')
528
+ .option('--force', 'Bypass the --file stale-update guard (advanced)')
299
529
  .option('--locale <locale>', 'Locale')
300
530
  .option('--format <fmt>', 'Output format', 'json')
301
531
  .option('--skip-validate', 'Skip the post-update validate call (advanced)')
302
532
  .action(async (id, opts) => {
303
533
  try {
304
534
  let updates;
535
+ let source;
305
536
  if (opts.file) {
306
537
  const raw = fs.readFileSync(opts.file, 'utf-8');
307
- updates = JSON.parse(raw);
538
+ const normalized = normalizeWorkflowUpdateFile(JSON.parse(raw));
539
+ updates = normalized.updates;
540
+ source = normalized.source;
308
541
  }
309
542
  else if (opts.updates) {
310
543
  updates = JSON.parse(opts.updates);
@@ -315,6 +548,14 @@ export function registerWorkflowCommands(program) {
315
548
  }
316
549
  const client = new AgentledClient();
317
550
  const format = (opts.format ?? 'json');
551
+ if (opts.file) {
552
+ const safe = await verifySafeFileUpdate(client, id, opts.file, updates, source, {
553
+ force: opts.force,
554
+ expectedUpdatedAt: opts.expectedUpdatedAt,
555
+ });
556
+ if (!safe)
557
+ return;
558
+ }
318
559
  const result = await client.updateWorkflow(id, updates, opts.locale);
319
560
  if (opts.skipValidate) {
320
561
  printOutput(result, format);
@@ -324,7 +565,10 @@ export function registerWorkflowCommands(program) {
324
565
  // Preserve the update mutation response fields in the JSON
325
566
  // envelope — `result` may contain the refreshed workflow
326
567
  // record, which script consumers parse.
327
- const outcome = await runAutoValidate(client, id, { format });
568
+ const outcome = await runAutoValidate(client, id, {
569
+ format,
570
+ source: result?.editingDraft ? 'draft' : 'live',
571
+ });
328
572
  const validation = outcome.status === 'request-failed'
329
573
  ? { requestFailed: true }
330
574
  : outcome.result;
@@ -338,7 +582,64 @@ export function registerWorkflowCommands(program) {
338
582
  return;
339
583
  }
340
584
  printOutput(result, format);
341
- const outcome = await runAutoValidate(client, id, { format });
585
+ const outcome = await runAutoValidate(client, id, {
586
+ format,
587
+ source: result?.editingDraft ? 'draft' : 'live',
588
+ });
589
+ const code = exitCodeForOutcome(outcome);
590
+ if (code !== 0)
591
+ process.exit(code);
592
+ }
593
+ catch (e) {
594
+ printError(e.message);
595
+ }
596
+ });
597
+ workflows
598
+ .command('replace <id>')
599
+ .description('Replace workflow config fields from a pulled file. Uses true replace semantics for context and validates draft when editing live workflows.')
600
+ .requiredOption('--file <path>', 'Path to workflow JSON from workflows pull/export')
601
+ .option('--expected-updated-at <timestamp>', 'Require the remote workflow updatedAt to match before replacing')
602
+ .option('--force', 'Bypass the stale-file guard (advanced)')
603
+ .option('--locale <locale>', 'Locale')
604
+ .option('--format <fmt>', 'Output format', 'json')
605
+ .option('--skip-validate', 'Skip the post-replace validate call (advanced)')
606
+ .action(async (id, opts) => {
607
+ try {
608
+ const raw = fs.readFileSync(opts.file, 'utf-8');
609
+ const { updates, source } = normalizeWorkflowUpdateFile(JSON.parse(raw));
610
+ const client = new AgentledClient();
611
+ const format = (opts.format ?? 'json');
612
+ const safe = await verifySafeFileUpdate(client, id, opts.file, updates, source, {
613
+ force: opts.force,
614
+ expectedUpdatedAt: opts.expectedUpdatedAt,
615
+ });
616
+ if (!safe)
617
+ return;
618
+ const result = await client.replaceWorkflow(id, updates, opts.locale);
619
+ if (opts.skipValidate) {
620
+ printOutput(result, format);
621
+ return;
622
+ }
623
+ const validateOpts = {
624
+ format,
625
+ source: result?.editingDraft ? 'draft' : 'live',
626
+ };
627
+ if (format === 'json') {
628
+ const outcome = await runAutoValidate(client, id, validateOpts);
629
+ const validation = outcome.status === 'request-failed'
630
+ ? { requestFailed: true }
631
+ : outcome.result;
632
+ const envelope = result && typeof result === 'object'
633
+ ? { ...result, validation }
634
+ : { workflowId: id, validation };
635
+ printOutput(envelope, 'json');
636
+ const code = exitCodeForOutcome(outcome);
637
+ if (code !== 0)
638
+ process.exit(code);
639
+ return;
640
+ }
641
+ printOutput(result, format);
642
+ const outcome = await runAutoValidate(client, id, validateOpts);
342
643
  const code = exitCodeForOutcome(outcome);
343
644
  if (code !== 0)
344
645
  process.exit(code);
@@ -366,6 +667,9 @@ export function registerWorkflowCommands(program) {
366
667
  .description('Validate a workflow. With <id>: server-side validation. With --file/--pipeline and no id: fast client-side preflight (no API call).')
367
668
  .option('--file <path>', 'Path to pipeline JSON file (client-side preflight if no <id> is given)')
368
669
  .option('--pipeline <json>', 'Inline pipeline JSON (overrides the saved pipeline when <id> is given; otherwise runs preflight)')
670
+ .option('--source <src>', 'Validation source when <id> is provided: auto (default) | live | draft', 'auto')
671
+ .option('--draft', 'Validate the current draft config for this workflow')
672
+ .option('--live', 'Validate the live stored workflow config')
369
673
  .option('--format <fmt>', 'Output format', 'json')
370
674
  .action(async (id, opts) => {
371
675
  try {
@@ -411,14 +715,51 @@ export function registerWorkflowCommands(program) {
411
715
  return;
412
716
  }
413
717
  const client = new AgentledClient();
718
+ if (opts.draft && opts.live) {
719
+ printError('Use either --draft or --live, not both.');
720
+ return;
721
+ }
722
+ const source = opts.draft ? 'draft' : opts.live ? 'live' : String(opts.source ?? 'auto');
723
+ if (!['auto', 'live', 'draft'].includes(source)) {
724
+ printError(`Invalid --source "${source}". Must be one of: auto, live, draft.`);
725
+ return;
726
+ }
727
+ // When source is specified, resolve which stored config to validate:
728
+ // - draft: validates the current draft pipeline and errors if none exists
729
+ // - auto: validates draft if present, otherwise live
730
+ // - live: validates the live stored workflow
731
+ let resolvedSource = 'live';
732
+ if (!pipeline && source !== 'live') {
733
+ try {
734
+ const draft = await client.getDraft(id);
735
+ if (draft?.draft?.config) {
736
+ pipeline = draft.draft.config;
737
+ resolvedSource = 'draft';
738
+ }
739
+ else if (source === 'draft') {
740
+ printError(`No draft exists for workflow ${id}.`);
741
+ return;
742
+ }
743
+ }
744
+ catch {
745
+ if (source === 'draft') {
746
+ printError(`No draft exists for workflow ${id}.`);
747
+ return;
748
+ }
749
+ }
750
+ }
414
751
  const result = await client.validateWorkflow(id, pipeline);
415
752
  const format = opts.format;
753
+ const validationSource = opts.pipeline || opts.file
754
+ ? 'inline'
755
+ : (pipeline ? resolvedSource : 'live');
416
756
  if (format === 'json') {
417
- printOutput(result, 'json');
757
+ printOutput({ ...result, source: validationSource }, 'json');
418
758
  }
419
759
  else {
420
760
  const errorCount = result.errors?.length ?? 0;
421
761
  const warnCount = result.warnings?.length ?? 0;
762
+ console.log(` Source: ${validationSource}`);
422
763
  if (errorCount === 0 && warnCount === 0) {
423
764
  console.log(` ✓ Validated (0 errors, 0 warnings) — ${id}`);
424
765
  }
@@ -773,13 +1114,30 @@ export function registerWorkflowCommands(program) {
773
1114
  }
774
1115
  });
775
1116
  workflows
776
- .command('move-step <workflowId> <stepId> <insertAfter>')
777
- .description('Move a step to a new location in a workflow')
1117
+ .command('move-step <workflowId> <stepId> [insertAfter]')
1118
+ .description('Move a step to a new location in a workflow. Use legacy positional <insertAfter>, --after <stepId>, or --position first|last.')
1119
+ .option('--after <stepId>', 'Insert after this step ID')
1120
+ .option('--position <pos>', 'Move to first or last position')
778
1121
  .option('--format <fmt>', 'Output format', 'json')
779
1122
  .action(async (workflowId, stepId, insertAfter, opts) => {
780
1123
  try {
1124
+ const after = opts.after || insertAfter;
1125
+ const position = opts.position;
1126
+ if (!after && !position) {
1127
+ printError('Provide <insertAfter>, --after <stepId>, or --position first|last');
1128
+ return;
1129
+ }
1130
+ if (after && position) {
1131
+ printError('Provide either an insert-after target or --position, not both');
1132
+ return;
1133
+ }
1134
+ if (position && position !== 'first' && position !== 'last') {
1135
+ printError('--position must be "first" or "last"');
1136
+ return;
1137
+ }
781
1138
  const client = new AgentledClient();
782
- const result = await client.moveStep(workflowId, stepId, insertAfter);
1139
+ const target = position ? { position: position } : after;
1140
+ const result = await client.moveStep(workflowId, stepId, target);
783
1141
  printOutput(result, opts.format);
784
1142
  }
785
1143
  catch (e) {