neuron-inspector 1.2.0 → 1.3.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 (85) hide show
  1. package/dist/collections/assertions.js +11 -0
  2. package/dist/collections/assertions.js.map +1 -1
  3. package/dist/collections/dynamic-vars-core.d.ts +79 -0
  4. package/dist/collections/dynamic-vars-core.js +214 -0
  5. package/dist/collections/dynamic-vars-core.js.map +1 -0
  6. package/dist/collections/dynamic-vars.d.ts +2 -61
  7. package/dist/collections/dynamic-vars.js +6 -146
  8. package/dist/collections/dynamic-vars.js.map +1 -1
  9. package/dist/collections/encoding.d.ts +3 -0
  10. package/dist/collections/encoding.js +20 -0
  11. package/dist/collections/encoding.js.map +1 -0
  12. package/dist/collections/executor.d.ts +114 -5
  13. package/dist/collections/executor.js +315 -91
  14. package/dist/collections/executor.js.map +1 -1
  15. package/dist/collections/index.d.ts +11 -3
  16. package/dist/collections/index.js +7 -1
  17. package/dist/collections/index.js.map +1 -1
  18. package/dist/collections/masking.d.ts +42 -0
  19. package/dist/collections/masking.js +188 -0
  20. package/dist/collections/masking.js.map +1 -0
  21. package/dist/collections/resolver-core.d.ts +74 -0
  22. package/dist/collections/resolver-core.js +309 -0
  23. package/dist/collections/resolver-core.js.map +1 -0
  24. package/dist/collections/resolver.d.ts +9 -62
  25. package/dist/collections/resolver.js +21 -281
  26. package/dist/collections/resolver.js.map +1 -1
  27. package/dist/collections/scenario-run.d.ts +33 -0
  28. package/dist/collections/scenario-run.js +68 -0
  29. package/dist/collections/scenario-run.js.map +1 -0
  30. package/dist/collections/script-api.d.ts +18 -0
  31. package/dist/collections/script-api.js +208 -0
  32. package/dist/collections/script-api.js.map +1 -0
  33. package/dist/collections/scripts.d.ts +1 -8
  34. package/dist/collections/scripts.js +6 -196
  35. package/dist/collections/scripts.js.map +1 -1
  36. package/dist/collections/slug.d.ts +2 -0
  37. package/dist/collections/slug.js +8 -0
  38. package/dist/collections/slug.js.map +1 -0
  39. package/dist/collections/store.d.ts +5 -2
  40. package/dist/collections/store.js +6 -6
  41. package/dist/collections/store.js.map +1 -1
  42. package/dist/collections/tools.d.ts +8 -1
  43. package/dist/collections/tools.js +253 -90
  44. package/dist/collections/tools.js.map +1 -1
  45. package/dist/collections/types.d.ts +42 -3
  46. package/dist/correlation.js +10 -0
  47. package/dist/correlation.js.map +1 -1
  48. package/dist/extension-slot.d.ts +45 -0
  49. package/dist/extension-slot.js +81 -0
  50. package/dist/extension-slot.js.map +1 -0
  51. package/dist/monitor.d.ts +5 -0
  52. package/dist/monitor.js +7 -2
  53. package/dist/monitor.js.map +1 -1
  54. package/dist/server.js +197 -42
  55. package/dist/server.js.map +1 -1
  56. package/dist/tools.js +1 -1
  57. package/dist/tools.js.map +1 -1
  58. package/package.json +3 -2
  59. package/dist/collections/__tests__/assertions.test.d.ts +0 -1
  60. package/dist/collections/__tests__/assertions.test.js +0 -203
  61. package/dist/collections/__tests__/assertions.test.js.map +0 -1
  62. package/dist/collections/__tests__/dynamic-vars.test.d.ts +0 -1
  63. package/dist/collections/__tests__/dynamic-vars.test.js +0 -169
  64. package/dist/collections/__tests__/dynamic-vars.test.js.map +0 -1
  65. package/dist/collections/__tests__/execution.test.d.ts +0 -1
  66. package/dist/collections/__tests__/execution.test.js +0 -271
  67. package/dist/collections/__tests__/execution.test.js.map +0 -1
  68. package/dist/collections/__tests__/integration.test.d.ts +0 -1
  69. package/dist/collections/__tests__/integration.test.js +0 -478
  70. package/dist/collections/__tests__/integration.test.js.map +0 -1
  71. package/dist/collections/__tests__/merge.test.d.ts +0 -1
  72. package/dist/collections/__tests__/merge.test.js +0 -411
  73. package/dist/collections/__tests__/merge.test.js.map +0 -1
  74. package/dist/collections/__tests__/resolver.test.d.ts +0 -1
  75. package/dist/collections/__tests__/resolver.test.js +0 -282
  76. package/dist/collections/__tests__/resolver.test.js.map +0 -1
  77. package/dist/collections/__tests__/scenario.test.d.ts +0 -1
  78. package/dist/collections/__tests__/scenario.test.js +0 -367
  79. package/dist/collections/__tests__/scenario.test.js.map +0 -1
  80. package/dist/collections/__tests__/scripts.test.d.ts +0 -1
  81. package/dist/collections/__tests__/scripts.test.js +0 -245
  82. package/dist/collections/__tests__/scripts.test.js.map +0 -1
  83. package/dist/collections/__tests__/store.test.d.ts +0 -1
  84. package/dist/collections/__tests__/store.test.js +0 -313
  85. package/dist/collections/__tests__/store.test.js.map +0 -1
@@ -1,9 +1,103 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import * as YAML from "yaml";
3
3
  import { slugify } from "./store.js";
4
- import { executeEndpoint, executeFolder, executeCollection, executeScenario } from "./executor.js";
4
+ import { executeEndpoint, executeFolder, executeCollection, resolveStepRef, normalizePoll } from "./executor.js";
5
+ import { assertTimeZone } from "./dynamic-vars.js";
6
+ import { pickEnvironment, runInputsFor, runScenario } from "./scenario-run.js";
7
+ import { sanitizeInputDeclarations, mergeInputDeclarations, prepareInputs, describeInputs, SecretMasker } from "./masking.js";
5
8
  import { fromPostmanV21, toPostmanV21 } from "./postman.js";
6
9
  import { mergeCollections } from "./merge.js";
10
+ const INPUTS_SCHEMA = {
11
+ type: "array",
12
+ description: "Values supplied at run time, used as {{name}}. Declarations only: never put a value here. " +
13
+ "Secret inputs are kept in memory for one run and shown as ••• in every result.",
14
+ items: {
15
+ type: "object",
16
+ properties: {
17
+ name: { type: "string" },
18
+ label: { type: "string" },
19
+ description: { type: "string" },
20
+ secret: { type: "boolean", description: "Mask the value everywhere and never store it" },
21
+ required: { type: "boolean", description: "Default true" },
22
+ default: { type: "string", description: "Fallback for non-secret inputs only" },
23
+ },
24
+ required: ["name"],
25
+ },
26
+ };
27
+ const RUN_INPUT_VALUES_SCHEMA = {
28
+ type: "object",
29
+ description: "Values for the declared inputs, e.g. { client_id: '…', client_secret: '…' }. " +
30
+ "Held in memory for this run only; values of secret inputs are masked as ••• in the result.",
31
+ };
32
+ const STEP_SCHEMA = {
33
+ type: "object",
34
+ properties: {
35
+ ref: {
36
+ type: "string",
37
+ description: "Endpoint reference: 'folder/<endpoint name>' or 'folder/METHOD /path' (method case-insensitive)",
38
+ },
39
+ overrides: {
40
+ type: "object",
41
+ description: "Per-step overrides for headers, body, tests, path, variables",
42
+ properties: {
43
+ headers: { type: "object" },
44
+ body: {},
45
+ tests: { type: "array" },
46
+ path: { type: "string" },
47
+ variables: { type: "object" },
48
+ },
49
+ },
50
+ poll: {
51
+ type: "object",
52
+ description: "Repeat this request until every 'until' assertion passes. Each attempt is recorded. " +
53
+ "Fails the step (and halts the scenario) if max_attempts runs out.",
54
+ properties: {
55
+ until: { type: "array", description: "Assertions: [{ check, op, expect }]. op 'in' takes an array expect." },
56
+ interval_ms: { type: "number", description: "Wait between attempts (default 2000, 100–60000)" },
57
+ max_attempts: { type: "number", description: "Default 10, max 120" },
58
+ },
59
+ required: ["until"],
60
+ },
61
+ },
62
+ required: ["ref"],
63
+ };
64
+ const COLLECTION_DATA_SCHEMA = {
65
+ type: "object",
66
+ description: "Run this collection object instead of loading one from disk (used by the extension for its local collections)",
67
+ };
68
+ /** Step refs that won't resolve, so a scenario can be fixed before it runs. */
69
+ function checkStepRefs(collection, steps) {
70
+ const problems = [];
71
+ steps.forEach((step, i) => {
72
+ const r = resolveStepRef(collection, step.ref);
73
+ if ("error" in r)
74
+ problems.push(`step ${i + 1}: ${r.error}`);
75
+ if (step.poll) {
76
+ try {
77
+ normalizePoll(step.poll);
78
+ }
79
+ catch (err) {
80
+ problems.push(`step ${i + 1}: ${err.message}`);
81
+ }
82
+ }
83
+ });
84
+ return problems;
85
+ }
86
+ function validateSteps(raw) {
87
+ if (!Array.isArray(raw))
88
+ throw new Error("steps must be an array");
89
+ return raw.map((step, i) => {
90
+ if (!step || typeof step !== "object" || typeof step.ref !== "string") {
91
+ throw new Error(`step ${i + 1} needs a 'ref'`);
92
+ }
93
+ const s = step;
94
+ if (s.poll !== undefined)
95
+ normalizePoll(s.poll);
96
+ return s;
97
+ });
98
+ }
99
+ /** Cancellation flags for runs started with a run_id. */
100
+ const activeRuns = new Map();
7
101
  export function buildCollectionTools() {
8
102
  return [
9
103
  {
@@ -123,7 +217,8 @@ export function buildCollectionTools() {
123
217
  },
124
218
  extract: {
125
219
  type: "object",
126
- description: "Variables to extract from the response, e.g. { bot_id: 'body.id' }",
220
+ description: "Variables to extract from the response, e.g. { bot_id: 'body.id' }. " +
221
+ "Use { access_token: { path: 'body.access_token', mask: true } } to show the value as ••• in every result for the rest of the run.",
127
222
  },
128
223
  body_mode: {
129
224
  type: "string",
@@ -263,6 +358,8 @@ export function buildCollectionTools() {
263
358
  variables: { type: "object", description: "Environment variables (key-value pairs)" },
264
359
  headers: { type: "object", description: "Environment-specific header overrides" },
265
360
  timeout_ms: { type: "number", description: "Environment-specific timeout" },
361
+ inputs: INPUTS_SCHEMA,
362
+ timezone: { type: "string", description: "IANA zone for {{$today}} and formatted {{$now}}, e.g. 'Africa/Lagos' (default: defaults.timezone, else UTC)" },
266
363
  },
267
364
  required: ["collection", "name", "base_url"],
268
365
  },
@@ -321,6 +418,7 @@ export function buildCollectionTools() {
321
418
  {
322
419
  name: "neuron_api_set_defaults",
323
420
  description: "Set or update collection-level defaults. Merges with existing defaults. " +
421
+ "pre_request / post_response scripts here run before each endpoint's own script (same sandbox and nr API), so endpoints can override what they set. " +
324
422
  "Pass null for auth to clear it. Omit fields to leave them unchanged.",
325
423
  inputSchema: {
326
424
  type: "object",
@@ -328,6 +426,7 @@ export function buildCollectionTools() {
328
426
  collection: { type: "string", description: "Collection id" },
329
427
  headers: { type: "object", description: "Default headers (key-value pairs)" },
330
428
  variables: { type: "object", description: "Default variables (key-value pairs)" },
429
+ timezone: { type: "string", description: "IANA zone for {{$today}} and formatted {{$now}}, e.g. 'Africa/Lagos' (default UTC). An environment's timezone wins." },
331
430
  timeout_ms: { type: "number", description: "Default timeout (ms)" },
332
431
  auth: {
333
432
  type: "object",
@@ -444,34 +543,20 @@ export function buildCollectionTools() {
444
543
  // ── Scenario management ───────────────────────────────────
445
544
  {
446
545
  name: "neuron_api_create_scenario",
447
- description: "Create a new scenario in a collection. Steps reference endpoints as 'folder/method path' (e.g. 'auth/POST /login').",
546
+ description: "Create a new scenario in a collection. Steps reference endpoints as 'folder/<endpoint name>' or " +
547
+ "'folder/METHOD /path' (e.g. 'auth/POST /login'). A step can poll until assertions pass. " +
548
+ "Declare run-time values (client ids, secrets) in 'inputs' and pass them when you run.",
448
549
  inputSchema: {
449
550
  type: "object",
450
551
  properties: {
451
552
  collection: { type: "string", description: "Collection id" },
452
553
  name: { type: "string", description: "Scenario name" },
453
554
  description: { type: "string", description: "Scenario description" },
555
+ inputs: INPUTS_SCHEMA,
454
556
  steps: {
455
557
  type: "array",
456
- description: "Ordered steps. Each step has a 'ref' (folder/METHOD path) and optional overrides.",
457
- items: {
458
- type: "object",
459
- properties: {
460
- ref: { type: "string", description: "Endpoint reference: 'folder/METHOD path'" },
461
- overrides: {
462
- type: "object",
463
- description: "Per-step overrides for headers, body, tests, path, variables",
464
- properties: {
465
- headers: { type: "object" },
466
- body: {},
467
- tests: { type: "array" },
468
- path: { type: "string" },
469
- variables: { type: "object" },
470
- },
471
- },
472
- },
473
- required: ["ref"],
474
- },
558
+ description: "Ordered steps. Each step has a 'ref', optional overrides and an optional poll.",
559
+ items: STEP_SCHEMA,
475
560
  },
476
561
  },
477
562
  required: ["collection", "name", "steps"],
@@ -479,24 +564,18 @@ export function buildCollectionTools() {
479
564
  },
480
565
  {
481
566
  name: "neuron_api_update_scenario",
482
- description: "Update a scenario's description or steps.",
567
+ description: "Update a scenario's description, inputs or steps.",
483
568
  inputSchema: {
484
569
  type: "object",
485
570
  properties: {
486
571
  collection: { type: "string", description: "Collection id" },
487
572
  scenario: { type: "string", description: "Scenario name" },
488
573
  description: { type: "string", description: "New description" },
574
+ inputs: INPUTS_SCHEMA,
489
575
  steps: {
490
576
  type: "array",
491
577
  description: "New ordered steps (replaces existing)",
492
- items: {
493
- type: "object",
494
- properties: {
495
- ref: { type: "string" },
496
- overrides: { type: "object" },
497
- },
498
- required: ["ref"],
499
- },
578
+ items: STEP_SCHEMA,
500
579
  },
501
580
  },
502
581
  required: ["collection", "scenario"],
@@ -551,6 +630,7 @@ export function buildCollectionTools() {
551
630
  follow_authorization: { type: "boolean", description: "Override: retain Authorization on cross-host redirects" },
552
631
  remove_referer: { type: "boolean", description: "Override: remove Referer header on redirect" },
553
632
  encode_url: { type: "boolean", description: "Override: encode URL automatically" },
633
+ inputs: RUN_INPUT_VALUES_SCHEMA,
554
634
  },
555
635
  required: ["collection"],
556
636
  },
@@ -559,16 +639,44 @@ export function buildCollectionTools() {
559
639
  name: "neuron_api_run_scenario",
560
640
  description: "Run a named scenario: an ordered sequence of endpoint calls with variable chaining. " +
561
641
  "Extracted variables from each step are available to subsequent steps via {{variable}} references. " +
562
- "Execution halts on the first step that fails (HTTP error or assertion failure). " +
563
- "Steps can include overrides for headers, body, tests, path, and variables.",
642
+ "Execution halts on the first step that fails (HTTP error, assertion failure, or a poll that never succeeds). " +
643
+ "Steps can include overrides for headers, body, tests, path, and variables, and a poll. " +
644
+ "Pass values for the scenario's/environment's declared inputs in 'inputs'; secret ones are masked as ••• in the result. " +
645
+ "The environment you pass is the one used, whatever the collection's active environment is.",
564
646
  inputSchema: {
565
647
  type: "object",
566
648
  properties: {
567
649
  collection: { type: "string", description: "Collection id" },
568
650
  scenario: { type: "string", description: "Scenario name (key under 'scenarios' in the collection)" },
569
651
  environment: { type: "string", description: "Environment name (defaults to active_environment)" },
652
+ inputs: RUN_INPUT_VALUES_SCHEMA,
653
+ run_id: { type: "string", description: "Optional id so the run can be cancelled with neuron_api_cancel_run" },
654
+ collection_data: COLLECTION_DATA_SCHEMA,
570
655
  },
571
- required: ["collection", "scenario"],
656
+ required: ["scenario"],
657
+ },
658
+ },
659
+ {
660
+ name: "neuron_api_get_run_inputs",
661
+ description: "List the inputs a scenario run will ask for (environment inputs merged with the scenario's). " +
662
+ "Returns declarations only (name, label, secret, required), never values.",
663
+ inputSchema: {
664
+ type: "object",
665
+ properties: {
666
+ collection: { type: "string", description: "Collection id" },
667
+ scenario: { type: "string", description: "Scenario name (omit for environment inputs only)" },
668
+ environment: { type: "string", description: "Environment name (defaults to active_environment)" },
669
+ collection_data: COLLECTION_DATA_SCHEMA,
670
+ },
671
+ },
672
+ },
673
+ {
674
+ name: "neuron_api_cancel_run",
675
+ description: "Stop a scenario run started with a run_id. It halts before its next request or poll attempt.",
676
+ inputSchema: {
677
+ type: "object",
678
+ properties: { run_id: { type: "string" } },
679
+ required: ["run_id"],
572
680
  },
573
681
  },
574
682
  // ── Query and generation ─────────────────────────────────
@@ -693,7 +801,24 @@ export function buildCollectionTools() {
693
801
  }
694
802
  // ── Handler ─────────────────────────────────────────────────────
695
803
  export function buildCollectionHandler(store, httpExecutor, pipeline, assertionEvaluator, scriptRunner) {
696
- return async function handleCollectionTool(name, args) {
804
+ /** Load the collection a run targets: inline data from the caller, else the store. */
805
+ async function loadForRun(args) {
806
+ if (args.collection_data !== undefined) {
807
+ const data = args.collection_data;
808
+ if (!data || typeof data !== "object" || Array.isArray(data))
809
+ return { error: "collection_data must be a collection object" };
810
+ const c = data;
811
+ return { id: args.collection || slugify(c.name || "inline"), collection: c };
812
+ }
813
+ const id = args.collection;
814
+ if (!id)
815
+ return { error: "Provide 'collection' (id) or 'collection_data'" };
816
+ const result = await store.get(id);
817
+ if (!result)
818
+ return { error: `Collection '${id}' not found` };
819
+ return { id, collection: result.collection };
820
+ }
821
+ return async function handleCollectionTool(name, args, hooks) {
697
822
  switch (name) {
698
823
  case "neuron_api_list_collections": {
699
824
  return store.list();
@@ -785,6 +910,13 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
785
910
  env.headers = args.headers;
786
911
  if (args.timeout_ms)
787
912
  env.timeout_ms = args.timeout_ms;
913
+ if (typeof args.timezone === "string" && args.timezone) {
914
+ assertTimeZone(args.timezone);
915
+ env.timezone = args.timezone;
916
+ }
917
+ const envInputs = sanitizeInputDeclarations(args.inputs);
918
+ if (envInputs)
919
+ env.inputs = envInputs;
788
920
  await store.setEnvironment(id, envName, env);
789
921
  return { set: true, collection: id, environment: slugify(envName) };
790
922
  }
@@ -837,6 +969,8 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
837
969
  const { collection: _c, ...defaults } = args;
838
970
  if (Object.keys(defaults).length === 0)
839
971
  return { error: "No defaults provided" };
972
+ if (typeof defaults.timezone === "string" && defaults.timezone)
973
+ assertTimeZone(defaults.timezone);
840
974
  await store.setDefaults(id, defaults);
841
975
  return { set: true, collection: id, fields: Object.keys(defaults) };
842
976
  }
@@ -901,12 +1035,22 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
901
1035
  const id = args.collection;
902
1036
  const scenarioName = args.name;
903
1037
  const scenario = {
904
- steps: args.steps,
1038
+ steps: validateSteps(args.steps),
905
1039
  };
906
1040
  if (args.description)
907
1041
  scenario.description = args.description;
908
- await store.createScenario(id, scenarioName, scenario);
909
- return { created: true, collection: id, scenario: scenarioName, step_count: scenario.steps.length };
1042
+ const scenarioInputs = sanitizeInputDeclarations(args.inputs);
1043
+ if (scenarioInputs)
1044
+ scenario.inputs = scenarioInputs;
1045
+ const updated = await store.createScenario(id, scenarioName, scenario);
1046
+ const warnings = checkStepRefs(updated, scenario.steps);
1047
+ return {
1048
+ created: true,
1049
+ collection: id,
1050
+ scenario: scenarioName,
1051
+ step_count: scenario.steps.length,
1052
+ ...(warnings.length ? { warnings } : {}),
1053
+ };
910
1054
  }
911
1055
  case "neuron_api_update_scenario": {
912
1056
  const id = args.collection;
@@ -915,11 +1059,14 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
915
1059
  if (args.description !== undefined)
916
1060
  updates.description = args.description;
917
1061
  if (args.steps !== undefined)
918
- updates.steps = args.steps;
1062
+ updates.steps = validateSteps(args.steps);
1063
+ if (args.inputs !== undefined)
1064
+ updates.inputs = sanitizeInputDeclarations(args.inputs) ?? [];
919
1065
  if (Object.keys(updates).length === 0)
920
1066
  return { error: "No updates provided" };
921
- await store.updateScenario(id, scenarioName, updates);
922
- return { updated: true, collection: id, scenario: scenarioName };
1067
+ const updated = await store.updateScenario(id, scenarioName, updates);
1068
+ const warnings = checkStepRefs(updated, updated.scenarios?.[scenarioName]?.steps ?? []);
1069
+ return { updated: true, collection: id, scenario: scenarioName, ...(warnings.length ? { warnings } : {}) };
923
1070
  }
924
1071
  case "neuron_api_delete_scenario": {
925
1072
  const id = args.collection;
@@ -938,6 +1085,7 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
938
1085
  name: key,
939
1086
  description: s.description ?? null,
940
1087
  step_count: s.steps?.length ?? 0,
1088
+ inputs: describeInputs(s.inputs ?? []),
941
1089
  })),
942
1090
  };
943
1091
  }
@@ -946,25 +1094,26 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
946
1094
  if (!httpExecutor || !pipeline || !assertionEvaluator) {
947
1095
  return { error: "HTTP executor not configured" };
948
1096
  }
949
- const id = args.collection;
950
- const result = await store.get(id);
951
- if (!result)
952
- return { error: `Collection '${id}' not found` };
953
- const { collection } = result;
954
- // Resolve environment
955
- const envName = args.environment ?? collection.active_environment;
956
- if (!envName) {
957
- return { error: "No environment specified and no active_environment set on the collection" };
958
- }
959
- const environment = collection.environments?.[slugify(envName)];
960
- if (!environment) {
961
- return { error: `Environment '${envName}' not found in collection '${id}'` };
1097
+ const loaded = await loadForRun(args);
1098
+ if ("error" in loaded)
1099
+ return loaded;
1100
+ const { id, collection } = loaded;
1101
+ const picked = pickEnvironment(collection, id, args.environment);
1102
+ if ("error" in picked)
1103
+ return picked;
1104
+ const { key: envKey, environment } = picked;
1105
+ const declared = mergeInputDeclarations(environment.inputs);
1106
+ const prepared = prepareInputs(declared, args.inputs);
1107
+ if (prepared.missing.length) {
1108
+ return { error: `Missing required input(s): ${prepared.missing.join(", ")}`, inputs: describeInputs(declared) };
962
1109
  }
1110
+ const masker = new SecretMasker(prepared.secrets);
1111
+ const runCtx = { environmentName: envKey, inputs: prepared.values, onSecret: (v) => masker.add(v) };
963
1112
  const hasFolder = typeof args.folder === "string";
964
1113
  const hasEndpoint = typeof args.method === "string" && typeof args.path === "string";
965
1114
  // Mode 3: Collection-wide run (no folder specified)
966
1115
  if (!hasFolder) {
967
- return executeCollection({
1116
+ return masker.mask(await executeCollection({
968
1117
  collectionId: id,
969
1118
  collection,
970
1119
  environment,
@@ -972,16 +1121,17 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
972
1121
  pipeline,
973
1122
  assertionEvaluator,
974
1123
  scriptRunner,
975
- });
1124
+ ...runCtx,
1125
+ }));
976
1126
  }
977
1127
  const folderKey = slugify(args.folder);
978
- const folder = collection.folders?.[folderKey];
1128
+ const folder = collection.folders?.[folderKey] ?? collection.folders?.[args.folder];
979
1129
  if (!folder) {
980
1130
  return { error: `Folder '${args.folder}' not found in collection '${id}'` };
981
1131
  }
982
1132
  // Mode 2: Folder run (folder but no method/path)
983
1133
  if (!hasEndpoint) {
984
- return executeFolder({
1134
+ return masker.mask(await executeFolder({
985
1135
  folderKey,
986
1136
  collection,
987
1137
  environment,
@@ -990,7 +1140,8 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
990
1140
  pipeline,
991
1141
  assertionEvaluator,
992
1142
  scriptRunner,
993
- });
1143
+ ...runCtx,
1144
+ }));
994
1145
  }
995
1146
  // Mode 1: Single endpoint
996
1147
  const method = args.method.toUpperCase();
@@ -1023,7 +1174,7 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
1023
1174
  runSettings.encode_url = args.encode_url;
1024
1175
  endpoint.settings = runSettings;
1025
1176
  }
1026
- return executeEndpoint({
1177
+ return masker.mask(await executeEndpoint({
1027
1178
  collection,
1028
1179
  environment,
1029
1180
  folder,
@@ -1032,43 +1183,55 @@ export function buildCollectionHandler(store, httpExecutor, pipeline, assertionE
1032
1183
  pipeline,
1033
1184
  assertionEvaluator,
1034
1185
  scriptRunner,
1035
- });
1186
+ ...runCtx,
1187
+ }));
1036
1188
  }
1037
1189
  case "neuron_api_run_scenario": {
1038
1190
  if (!httpExecutor || !pipeline || !assertionEvaluator) {
1039
1191
  return { error: "HTTP executor not configured" };
1040
1192
  }
1041
- const id = args.collection;
1042
- const result = await store.get(id);
1043
- if (!result)
1044
- return { error: `Collection '${id}' not found` };
1045
- const { collection } = result;
1046
- // Resolve environment
1047
- const envName = args.environment ?? collection.active_environment;
1048
- if (!envName) {
1049
- return { error: "No environment specified and no active_environment set on the collection" };
1050
- }
1051
- const environment = collection.environments?.[slugify(envName)];
1052
- if (!environment) {
1053
- return { error: `Environment '${envName}' not found in collection '${id}'` };
1193
+ const loaded = await loadForRun(args);
1194
+ if ("error" in loaded)
1195
+ return loaded;
1196
+ const runId = typeof args.run_id === "string" && args.run_id ? args.run_id : undefined;
1197
+ const flag = { cancelled: false };
1198
+ if (runId)
1199
+ activeRuns.set(runId, flag);
1200
+ try {
1201
+ return await runScenario({
1202
+ collection: loaded.collection,
1203
+ collectionId: loaded.id,
1204
+ scenario: args.scenario,
1205
+ environment: args.environment,
1206
+ inputs: args.inputs,
1207
+ httpExecutor,
1208
+ pipeline,
1209
+ assertionEvaluator,
1210
+ scriptRunner,
1211
+ isCancelled: () => flag.cancelled,
1212
+ onEvent: hooks?.onEvent && runId ? (event) => hooks.onEvent({ ...event, run_id: runId }) : undefined,
1213
+ });
1054
1214
  }
1055
- // Find scenario
1056
- const scenarioKey = args.scenario;
1057
- const scenario = collection.scenarios?.[scenarioKey];
1058
- if (!scenario) {
1059
- return { error: `Scenario '${scenarioKey}' not found in collection '${id}'` };
1215
+ finally {
1216
+ if (runId)
1217
+ activeRuns.delete(runId);
1060
1218
  }
1061
- return executeScenario({
1062
- scenarioKey,
1063
- steps: scenario.steps,
1064
- collectionId: id,
1065
- collection,
1066
- environment,
1067
- httpExecutor,
1068
- pipeline,
1069
- assertionEvaluator,
1070
- scriptRunner,
1071
- });
1219
+ }
1220
+ case "neuron_api_get_run_inputs": {
1221
+ const loaded = await loadForRun(args);
1222
+ if ("error" in loaded)
1223
+ return loaded;
1224
+ const res = runInputsFor(loaded.collection, loaded.id, args.scenario, args.environment);
1225
+ if ("error" in res)
1226
+ return res;
1227
+ return { collection: loaded.id, environment: res.environment, scenario: args.scenario ?? null, inputs: res.inputs };
1228
+ }
1229
+ case "neuron_api_cancel_run": {
1230
+ const flag = activeRuns.get(args.run_id);
1231
+ if (!flag)
1232
+ return { cancelled: false, error: `No active run '${args.run_id}'` };
1233
+ flag.cancelled = true;
1234
+ return { cancelled: true, run_id: args.run_id };
1072
1235
  }
1073
1236
  // ── Query and generation ─────────────────────────────
1074
1237
  case "neuron_api_query": {