callman-mcp 0.1.0 → 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 (204) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +45 -16
  3. package/dist/client/api.d.ts +64 -3
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/api.js +67 -2
  6. package/dist/client/api.js.map +1 -1
  7. package/dist/client/errors.d.ts.map +1 -1
  8. package/dist/client/errors.js +33 -7
  9. package/dist/client/errors.js.map +1 -1
  10. package/dist/client/types.d.ts +91 -2
  11. package/dist/client/types.d.ts.map +1 -1
  12. package/dist/context.d.ts +2 -0
  13. package/dist/context.d.ts.map +1 -1
  14. package/dist/context.js +7 -2
  15. package/dist/context.js.map +1 -1
  16. package/dist/domain/contracts.d.ts +893 -0
  17. package/dist/domain/contracts.d.ts.map +1 -0
  18. package/dist/domain/contracts.js +90 -0
  19. package/dist/domain/contracts.js.map +1 -0
  20. package/dist/domain/diagram/build.d.ts +520 -18
  21. package/dist/domain/diagram/build.d.ts.map +1 -1
  22. package/dist/domain/diagram/build.js +65 -19
  23. package/dist/domain/diagram/build.js.map +1 -1
  24. package/dist/domain/diagram/patch.d.ts +624 -51
  25. package/dist/domain/diagram/patch.d.ts.map +1 -1
  26. package/dist/domain/diagram/patch.js +180 -40
  27. package/dist/domain/diagram/patch.js.map +1 -1
  28. package/dist/domain/diagram/style.d.ts +315 -0
  29. package/dist/domain/diagram/style.d.ts.map +1 -0
  30. package/dist/domain/diagram/style.js +124 -0
  31. package/dist/domain/diagram/style.js.map +1 -0
  32. package/dist/domain/diagram/summarize.d.ts +8 -0
  33. package/dist/domain/diagram/summarize.d.ts.map +1 -1
  34. package/dist/domain/diagram/summarize.js +28 -13
  35. package/dist/domain/diagram/summarize.js.map +1 -1
  36. package/dist/domain/requestIntegrations.d.ts +719 -0
  37. package/dist/domain/requestIntegrations.d.ts.map +1 -0
  38. package/dist/domain/requestIntegrations.js +222 -0
  39. package/dist/domain/requestIntegrations.js.map +1 -0
  40. package/dist/domain/requestShape.d.ts +1350 -152
  41. package/dist/domain/requestShape.d.ts.map +1 -1
  42. package/dist/domain/requestShape.js +336 -121
  43. package/dist/domain/requestShape.js.map +1 -1
  44. package/dist/domain/resolve.d.ts +2 -1
  45. package/dist/domain/resolve.d.ts.map +1 -1
  46. package/dist/domain/resolve.js +5 -1
  47. package/dist/domain/resolve.js.map +1 -1
  48. package/dist/domain/scenario/build.d.ts +82 -26
  49. package/dist/domain/scenario/build.d.ts.map +1 -1
  50. package/dist/domain/scenario/build.js +196 -65
  51. package/dist/domain/scenario/build.js.map +1 -1
  52. package/dist/domain/scenario/nodeConfig.d.ts +11 -0
  53. package/dist/domain/scenario/nodeConfig.d.ts.map +1 -0
  54. package/dist/domain/scenario/nodeConfig.js +251 -0
  55. package/dist/domain/scenario/nodeConfig.js.map +1 -0
  56. package/dist/domain/scenario/summarize.d.ts +145 -25
  57. package/dist/domain/scenario/summarize.d.ts.map +1 -1
  58. package/dist/domain/scenario/summarize.js +84 -29
  59. package/dist/domain/scenario/summarize.js.map +1 -1
  60. package/dist/lib/mask.d.ts +0 -18
  61. package/dist/lib/mask.d.ts.map +1 -1
  62. package/dist/lib/mask.js +2 -44
  63. package/dist/lib/mask.js.map +1 -1
  64. package/dist/lib/pagination.d.ts +3 -2
  65. package/dist/lib/pagination.d.ts.map +1 -1
  66. package/dist/lib/pagination.js +13 -8
  67. package/dist/lib/pagination.js.map +1 -1
  68. package/dist/lib/redact.d.ts +12 -0
  69. package/dist/lib/redact.d.ts.map +1 -0
  70. package/dist/lib/redact.js +50 -0
  71. package/dist/lib/redact.js.map +1 -0
  72. package/dist/lib/strict.d.ts +4 -0
  73. package/dist/lib/strict.d.ts.map +1 -0
  74. package/dist/lib/strict.js +14 -0
  75. package/dist/lib/strict.js.map +1 -0
  76. package/dist/prompts/index.js +7 -7
  77. package/dist/prompts/index.js.map +1 -1
  78. package/dist/resources/docs/contracts.d.ts +2 -0
  79. package/dist/resources/docs/contracts.d.ts.map +1 -0
  80. package/dist/resources/docs/contracts.js +74 -0
  81. package/dist/resources/docs/contracts.js.map +1 -0
  82. package/dist/resources/docs/diagramSchema.d.ts +1 -1
  83. package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
  84. package/dist/resources/docs/diagramSchema.js +95 -28
  85. package/dist/resources/docs/diagramSchema.js.map +1 -1
  86. package/dist/resources/docs/overview.d.ts +1 -1
  87. package/dist/resources/docs/overview.d.ts.map +1 -1
  88. package/dist/resources/docs/overview.js +29 -7
  89. package/dist/resources/docs/overview.js.map +1 -1
  90. package/dist/resources/docs/requestSchema.d.ts +1 -1
  91. package/dist/resources/docs/requestSchema.d.ts.map +1 -1
  92. package/dist/resources/docs/requestSchema.js +85 -16
  93. package/dist/resources/docs/requestSchema.js.map +1 -1
  94. package/dist/resources/generated/aiPacks.d.ts +1 -1
  95. package/dist/resources/generated/aiPacks.d.ts.map +1 -1
  96. package/dist/resources/generated/aiPacks.js +3 -2
  97. package/dist/resources/generated/aiPacks.js.map +1 -1
  98. package/dist/resources/index.d.ts.map +1 -1
  99. package/dist/resources/index.js +19 -19
  100. package/dist/resources/index.js.map +1 -1
  101. package/dist/run/executeCollection.d.ts +3 -1
  102. package/dist/run/executeCollection.d.ts.map +1 -1
  103. package/dist/run/executeCollection.js +16 -4
  104. package/dist/run/executeCollection.js.map +1 -1
  105. package/dist/run/summarize.d.ts +17 -6
  106. package/dist/run/summarize.d.ts.map +1 -1
  107. package/dist/run/summarize.js +48 -41
  108. package/dist/run/summarize.js.map +1 -1
  109. package/dist/run/types.d.ts +1 -0
  110. package/dist/run/types.d.ts.map +1 -1
  111. package/dist/server.d.ts +1 -1
  112. package/dist/server.d.ts.map +1 -1
  113. package/dist/server.js +1 -1
  114. package/dist/server.js.map +1 -1
  115. package/dist/tools/catalog.d.ts +1 -1
  116. package/dist/tools/catalog.d.ts.map +1 -1
  117. package/dist/tools/catalog.js +18 -0
  118. package/dist/tools/catalog.js.map +1 -1
  119. package/dist/tools/collections.d.ts.map +1 -1
  120. package/dist/tools/collections.js +53 -41
  121. package/dist/tools/collections.js.map +1 -1
  122. package/dist/tools/contracts.d.ts +4 -0
  123. package/dist/tools/contracts.d.ts.map +1 -0
  124. package/dist/tools/contracts.js +201 -0
  125. package/dist/tools/contracts.js.map +1 -0
  126. package/dist/tools/diagrams.d.ts.map +1 -1
  127. package/dist/tools/diagrams.js +19 -10
  128. package/dist/tools/diagrams.js.map +1 -1
  129. package/dist/tools/environments.d.ts.map +1 -1
  130. package/dist/tools/environments.js +184 -64
  131. package/dist/tools/environments.js.map +1 -1
  132. package/dist/tools/import.d.ts.map +1 -1
  133. package/dist/tools/import.js +111 -20
  134. package/dist/tools/import.js.map +1 -1
  135. package/dist/tools/index.d.ts +2 -1
  136. package/dist/tools/index.d.ts.map +1 -1
  137. package/dist/tools/index.js +19 -1
  138. package/dist/tools/index.js.map +1 -1
  139. package/dist/tools/lookups.d.ts +38 -7
  140. package/dist/tools/lookups.d.ts.map +1 -1
  141. package/dist/tools/lookups.js +113 -10
  142. package/dist/tools/lookups.js.map +1 -1
  143. package/dist/tools/meta.d.ts.map +1 -1
  144. package/dist/tools/meta.js +9 -4
  145. package/dist/tools/meta.js.map +1 -1
  146. package/dist/tools/requests.d.ts.map +1 -1
  147. package/dist/tools/requests.js +138 -50
  148. package/dist/tools/requests.js.map +1 -1
  149. package/dist/tools/run.d.ts.map +1 -1
  150. package/dist/tools/run.js +84 -28
  151. package/dist/tools/run.js.map +1 -1
  152. package/dist/tools/scenarios.d.ts.map +1 -1
  153. package/dist/tools/scenarios.js +238 -75
  154. package/dist/tools/scenarios.js.map +1 -1
  155. package/dist/tools/shared.d.ts +1 -0
  156. package/dist/tools/shared.d.ts.map +1 -1
  157. package/dist/tools/shared.js +9 -1
  158. package/dist/tools/shared.js.map +1 -1
  159. package/dist/tools/workspaces.d.ts.map +1 -1
  160. package/dist/tools/workspaces.js +44 -7
  161. package/dist/tools/workspaces.js.map +1 -1
  162. package/dist-cjs/client/api.js +67 -2
  163. package/dist-cjs/client/errors.js +33 -7
  164. package/dist-cjs/context.js +7 -2
  165. package/dist-cjs/domain/contracts.js +93 -0
  166. package/dist-cjs/domain/diagram/build.js +71 -20
  167. package/dist-cjs/domain/diagram/patch.js +179 -39
  168. package/dist-cjs/domain/diagram/style.js +130 -0
  169. package/dist-cjs/domain/diagram/summarize.js +28 -13
  170. package/dist-cjs/domain/requestIntegrations.js +228 -0
  171. package/dist-cjs/domain/requestShape.js +342 -121
  172. package/dist-cjs/domain/resolve.js +5 -1
  173. package/dist-cjs/domain/scenario/build.js +197 -65
  174. package/dist-cjs/domain/scenario/nodeConfig.js +260 -0
  175. package/dist-cjs/domain/scenario/summarize.js +86 -29
  176. package/dist-cjs/lib/mask.js +3 -48
  177. package/dist-cjs/lib/pagination.js +13 -8
  178. package/dist-cjs/lib/redact.js +53 -0
  179. package/dist-cjs/lib/strict.js +18 -0
  180. package/dist-cjs/prompts/index.js +7 -7
  181. package/dist-cjs/resources/docs/contracts.js +76 -0
  182. package/dist-cjs/resources/docs/diagramSchema.js +95 -28
  183. package/dist-cjs/resources/docs/overview.js +29 -7
  184. package/dist-cjs/resources/docs/requestSchema.js +85 -16
  185. package/dist-cjs/resources/generated/aiPacks.js +3 -2
  186. package/dist-cjs/resources/index.js +17 -17
  187. package/dist-cjs/run/executeCollection.js +16 -4
  188. package/dist-cjs/run/summarize.js +48 -41
  189. package/dist-cjs/server.js +1 -1
  190. package/dist-cjs/tools/catalog.js +18 -0
  191. package/dist-cjs/tools/collections.js +52 -40
  192. package/dist-cjs/tools/contracts.js +204 -0
  193. package/dist-cjs/tools/diagrams.js +19 -10
  194. package/dist-cjs/tools/environments.js +182 -62
  195. package/dist-cjs/tools/import.js +107 -16
  196. package/dist-cjs/tools/index.js +21 -2
  197. package/dist-cjs/tools/lookups.js +123 -11
  198. package/dist-cjs/tools/meta.js +9 -4
  199. package/dist-cjs/tools/requests.js +134 -46
  200. package/dist-cjs/tools/run.js +82 -26
  201. package/dist-cjs/tools/scenarios.js +235 -72
  202. package/dist-cjs/tools/shared.js +11 -2
  203. package/dist-cjs/tools/workspaces.js +43 -6
  204. package/package.json +2 -2
@@ -1,45 +1,62 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.buildScenarioCreateBody = exports.buildScenarioGraph = exports.deepMerge = exports.scenarioInputSchema = exports.scenarioEnvironmentInputSchema = exports.scenarioEdgeInputSchema = exports.scenarioNodeInputSchema = exports.SCENARIO_NODE_TYPES = void 0;
3
+ exports.buildScenarioCreateBody = exports.buildEnvironments = exports.buildScenarioGraph = exports.deepMerge = exports.scenarioInputSchema = exports.scenarioDbSettingsSchema = exports.scenarioDatasetSchema = exports.scenarioEnvironmentInputSchema = exports.scenarioEdgeInputSchema = exports.scenarioNodeInputSchema = exports.SCENARIO_NODE_TYPES = void 0;
4
4
  const callman_core_1 = require("callman-core");
5
5
  const zod_1 = require("zod");
6
6
  const errors_js_1 = require("../../client/errors.js");
7
7
  const ids_js_1 = require("../../lib/ids.js");
8
- const requestShape_js_1 = require("../requestShape.js");
8
+ const strict_js_1 = require("../../lib/strict.js");
9
+ const nodeConfig_js_1 = require("./nodeConfig.js");
9
10
  // From the COMPACT scenario a model writes to the backend's `POST /scenarios`
10
- // body. Node configs are deep-merged over core's canonical defaults for the
11
- // node type (the same defaults the builder and the AI assistant use), so a
12
- // partial config (`{ url, method }`) becomes a complete, runnable node; ids
13
- // are short and stable; missing positions are laid out in layers so the
14
- // graph opens tidy in the builder. The backend's zod schema stays the
15
- // arbiter of node config validity.
16
- exports.SCENARIO_NODE_TYPES = [...callman_core_1.SCENARIO_AI_REFERENCE_TYPES, "subscenario"];
17
- exports.scenarioNodeInputSchema = zod_1.z.object({
18
- id: zod_1.z.string().min(1).max(64).optional(),
19
- type: zod_1.z.string().min(1).max(40).describe(`Node type: ${exports.SCENARIO_NODE_TYPES.join(", ")} (see callman://docs/scenario-nodes)`),
20
- label: zod_1.z.string().max(200).optional(),
21
- config: zod_1.z.record(zod_1.z.unknown()).optional().describe("Type-specific config; merged over the node type's defaults"),
22
- position: zod_1.z.object({ x: zod_1.z.number(), y: zod_1.z.number() }).optional(),
23
- });
24
- exports.scenarioEdgeInputSchema = zod_1.z.object({
11
+ // body. A node's config is deep-merged over a BASE: the node's stored config
12
+ // when a node with that id already exists (update / patch — so editing one
13
+ // field never resets the rest), otherwise core's defaults for the type.
14
+ // Ids are short and stable; missing positions are laid out in layers (stored
15
+ // positions are kept). The backend's zod schema stays the arbiter of node
16
+ // config validity; callman://docs/scenario-nodes documents every field.
17
+ exports.SCENARIO_NODE_TYPES = callman_core_1.SCENARIO_NODE_REFERENCE_TYPES;
18
+ // Output roots a label would shadow (warning, not an error).
19
+ const SHADOWING_LABELS = new Set(["response", "lastResponse", "data", "env", "global", "globals", "db", "redis", "kafkaEvent", "kafkaevent"]);
20
+ exports.scenarioNodeInputSchema = (0, strict_js_1.strictObject)({
21
+ id: zod_1.z.string().min(1).max(64).optional().describe("Stable id; generated (n1, n2…) when omitted. Reusing an existing id on update merges into that node"),
22
+ type: zod_1.z.string().min(1).max(40).describe(`Node type: ${exports.SCENARIO_NODE_TYPES.join(", ")} (callman://docs/scenario-nodes)`),
23
+ label: zod_1.z.string().max(200).optional().describe("Unique name; later nodes read its output as {{Label.path}}. Generated when omitted"),
24
+ config: zod_1.z.record(zod_1.z.unknown()).optional().describe("Type-specific config (callman://docs/scenario-nodes), merged over the stored node (same id) or the type's defaults"),
25
+ position: zod_1.z.object({ x: zod_1.z.number(), y: zod_1.z.number() }).strict().optional(),
26
+ }, "node field");
27
+ exports.scenarioEdgeInputSchema = (0, strict_js_1.strictObject)({
25
28
  id: zod_1.z.string().min(1).max(64).optional(),
26
29
  source: zod_1.z.string().min(1).max(64),
27
30
  target: zod_1.z.string().min(1).max(64),
28
- type: zod_1.z.enum(["default", "yes", "no", "error"]).optional().describe("yes/no for condition branches, error for failure paths"),
29
- });
30
- exports.scenarioEnvironmentInputSchema = zod_1.z.object({
31
- id: zod_1.z.string().min(1).max(64).optional(),
32
- name: zod_1.z.string().min(1).max(200),
33
- variables: zod_1.z.array(requestShape_js_1.kvSchema).max(500).default([]),
34
- });
35
- exports.scenarioInputSchema = zod_1.z.object({
36
- name: zod_1.z.string().min(1).max(200),
31
+ type: zod_1.z.enum(["default", "yes", "no", "error"]).optional().describe("yes/no leave a condition node; error leaves any other node on failure"),
32
+ }, "edge field");
33
+ exports.scenarioEnvironmentInputSchema = (0, strict_js_1.strictObject)({
34
+ id: zod_1.z.string().min(1).max(200).optional(),
35
+ name: zod_1.z.string().trim().min(1).max(200),
36
+ variables: zod_1.z
37
+ .array((0, strict_js_1.strictObject)({ key: zod_1.z.string().trim().min(1).max(1000), value: zod_1.z.string().max(100_000).default(""), enabled: zod_1.z.boolean().optional() }, "variable field"))
38
+ .max(1000)
39
+ .default([]),
40
+ }, "environment field");
41
+ exports.scenarioDatasetSchema = (0, strict_js_1.strictObject)({
42
+ columns: zod_1.z.array(zod_1.z.string().max(200)).max(200),
43
+ rows: zod_1.z.array(zod_1.z.record(zod_1.z.string().max(100_000))).max(10_000).describe("Each row: column → value; nodes read {{data.<column>}}; the scenario runs once per row"),
44
+ }, "dataset field")
45
+ .nullable()
46
+ .describe("Data-driven runs: the whole scenario runs once per row (null removes the dataset)");
47
+ exports.scenarioDbSettingsSchema = (0, strict_js_1.strictObject)({ transactionMode: zod_1.z.enum(["none", "rollback", "commit"]).optional() }, "dbSettings field")
48
+ .nullable()
49
+ .describe('For db nodes with useSession: "rollback" wraps session DB work in BEGIN … ROLLBACK, "commit" commits on success, "none" keeps autocommit');
50
+ exports.scenarioInputSchema = (0, strict_js_1.strictObject)({
51
+ name: zod_1.z.string().trim().min(1).max(200),
37
52
  description: zod_1.z.string().max(5000).optional(),
38
53
  priority: zod_1.z.enum(["critical", "high", "medium", "low"]).optional(),
39
54
  nodes: zod_1.z.array(exports.scenarioNodeInputSchema).min(1).max(1000),
40
55
  edges: zod_1.z.array(exports.scenarioEdgeInputSchema).max(2000).default([]),
41
- environments: zod_1.z.array(exports.scenarioEnvironmentInputSchema).max(20).optional(),
42
- });
56
+ environments: zod_1.z.array(exports.scenarioEnvironmentInputSchema).max(100).optional(),
57
+ dataset: exports.scenarioDatasetSchema.optional(),
58
+ dbSettings: exports.scenarioDbSettingsSchema.optional(),
59
+ }, "scenario field");
43
60
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
44
61
  // Objects merge key by key; arrays and scalars from the override win whole.
45
62
  const deepMerge = (base, override) => {
@@ -52,35 +69,131 @@ const deepMerge = (base, override) => {
52
69
  return out;
53
70
  };
54
71
  exports.deepMerge = deepMerge;
55
- const buildScenarioGraph = (nodesInput, edgesInput) => {
56
- const nextNodeId = (0, ids_js_1.sequentialIds)("n", nodesInput.map((node) => node.id).filter((id) => Boolean(id)));
72
+ const labelBase = (type) => type
73
+ .split(/[-_]/)
74
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
75
+ .join("");
76
+ // Labels of stored nodes nested in foreach / subscenario / scenario-ref bodies.
77
+ const nestedLabels = (config) => {
78
+ const out = [];
79
+ for (const node of Array.isArray(config.nodes) ? config.nodes : []) {
80
+ if (!isRecord(node))
81
+ continue;
82
+ const data = isRecord(node.data) ? node.data : {};
83
+ if (typeof data.label === "string" && data.label)
84
+ out.push(data.label);
85
+ if (isRecord(data.config))
86
+ out.push(...nestedLabels(data.config));
87
+ }
88
+ return out;
89
+ };
90
+ const buildScenarioGraph = (nodesInput, edgesInput, options = {}) => {
91
+ const existingById = new Map((options.existing ?? []).map((node) => [node.id, node]));
92
+ const nextNodeId = (0, ids_js_1.sequentialIds)("n", [...nodesInput.map((node) => node.id).filter((id) => Boolean(id)), ...existingById.keys()]);
57
93
  const nextEdgeId = (0, ids_js_1.sequentialIds)("e", edgesInput.map((edge) => edge.id).filter((id) => Boolean(id)));
58
94
  const ids = nodesInput.map((node) => node.id ?? nextNodeId());
59
95
  const duplicate = ids.find((id, index) => ids.indexOf(id) !== index);
60
96
  if (duplicate)
61
97
  throw new errors_js_1.ToolInputError("SCENARIO_DUPLICATE_NODE_ID", `Duplicate node id "${duplicate}".`);
62
98
  const idSet = new Set(ids);
63
- const nodes = nodesInput.map((node, index) => {
99
+ const warnings = [];
100
+ const problems = [];
101
+ const sent = new Map();
102
+ const built = nodesInput.map((node, index) => {
103
+ const id = ids[index];
64
104
  const type = node.type.trim();
65
105
  if (!exports.SCENARIO_NODE_TYPES.includes(type)) {
66
106
  throw new errors_js_1.ToolInputError("SCENARIO_UNKNOWN_NODE_TYPE", `Unknown scenario node type "${type}". Known: ${exports.SCENARIO_NODE_TYPES.join(", ")}.`);
67
107
  }
68
- const defaults = (0, callman_core_1.defaultScenarioNodeConfig)(type);
69
- const config = (0, exports.deepMerge)(defaults ?? {}, node.config ?? {});
108
+ const stored = existingById.get(id);
109
+ const sameNode = stored && stored.type === type ? stored : undefined;
110
+ if (type === "scenario-ref" && !sameNode) {
111
+ throw new errors_js_1.ToolInputError("SCENARIO_REF_READ_ONLY", `Node ${id}: scenario-ref nodes are created by linking a scenario in the desktop app; they can only be kept (send the stored node's id and type).`);
112
+ }
113
+ const base = sameNode?.data?.config ?? (0, callman_core_1.defaultScenarioNodeConfig)(type) ?? {};
114
+ const restored = sameNode ? (0, nodeConfig_js_1.restoreMasked)(node.config ?? {}, base) : (node.config ?? {});
115
+ const unwritable = (0, nodeConfig_js_1.findUnwritableValues)(restored, `${id}.config`);
116
+ if (unwritable.length > 0)
117
+ problems.push(...unwritable);
118
+ const normalized = (0, nodeConfig_js_1.normalizeNodeConfig)(type, restored);
119
+ warnings.push(...normalized.notes.map((note) => `${id}: ${note}`));
120
+ const missing = (0, nodeConfig_js_1.missingRequiredFields)(type, normalized.config, sameNode ? base : null);
121
+ if (missing.length > 0)
122
+ problems.push(`${id} (${type}) needs config.${missing.join(", config.")}`);
123
+ if (Object.keys(normalized.config).length > 0)
124
+ sent.set(id, normalized.config);
125
+ const config = (0, exports.deepMerge)(base, normalized.config);
70
126
  return {
71
- id: ids[index],
127
+ id,
72
128
  type,
73
- position: node.position ?? { x: 0, y: 0 },
74
- data: { ...(node.label ? { label: node.label } : {}), config },
75
- _positioned: Boolean(node.position),
129
+ label: node.label ?? sameNode?.data?.label ?? undefined,
130
+ position: node.position ?? sameNode?.position,
131
+ config,
132
+ // Label rules are enforced for what this call names or adds; a problem
133
+ // already in stored data only warns, so it never blocks an edit.
134
+ touched: node.label !== undefined || !sameNode || node.config !== undefined,
76
135
  };
77
136
  });
137
+ if (problems.length > 0) {
138
+ throw new errors_js_1.ToolInputError("SCENARIO_NODE_INVALID", `Fix these nodes: ${problems.join("; ")}. Masked values sent back unchanged keep the stored secret on update; otherwise pass real values or {{variables}}.`, { problems });
139
+ }
140
+ // Labels: generated when missing, unique across the whole scenario
141
+ // (nested bodies included), never reserved.
142
+ const taken = new Set();
143
+ for (const node of built) {
144
+ if (node.label)
145
+ taken.add(node.label);
146
+ for (const label of nestedLabels(node.config))
147
+ taken.add(label);
148
+ }
149
+ const counters = new Map();
150
+ for (const node of built) {
151
+ if (node.label)
152
+ continue;
153
+ const prefix = labelBase(node.type);
154
+ let n = counters.get(prefix) ?? 0;
155
+ let candidate;
156
+ do {
157
+ n += 1;
158
+ candidate = `${prefix}${n}`;
159
+ } while (taken.has(candidate));
160
+ counters.set(prefix, n);
161
+ node.label = candidate;
162
+ taken.add(candidate);
163
+ }
164
+ const seen = new Map();
165
+ const labelProblems = [];
166
+ const report = (message, touched) => {
167
+ if (touched)
168
+ labelProblems.push(message);
169
+ else
170
+ warnings.push(`Existing problem (not blocking): ${message}`);
171
+ };
172
+ for (const node of built) {
173
+ const label = node.label;
174
+ if ((0, callman_core_1.isReservedScenarioLabel)(label))
175
+ report(`"${label}" (${node.id}) is reserved — its output would be dropped`, node.touched);
176
+ const other = seen.get(label);
177
+ if (other)
178
+ report(`"${label}" is used by ${other.id} and ${node.id}`, node.touched || other.touched);
179
+ seen.set(label, { id: node.id, touched: node.touched });
180
+ for (const nested of nestedLabels(node.config)) {
181
+ const owner = seen.get(nested);
182
+ if (owner && owner.id !== node.id)
183
+ report(`"${nested}" is used by ${owner.id} and inside ${node.id}`, node.touched || owner.touched);
184
+ }
185
+ if (SHADOWING_LABELS.has(label) && node.touched)
186
+ warnings.push(`${node.id}: label "${label}" shadows a built-in root ({{${label}.…}}) — prefer another name`);
187
+ }
188
+ if (labelProblems.length > 0) {
189
+ throw new errors_js_1.ToolInputError("SCENARIO_LABEL_INVALID", `Labels must be unique and not reserved: ${labelProblems.join("; ")}.`);
190
+ }
78
191
  const edges = edgesInput.map((edge) => {
79
192
  for (const endpoint of [edge.source, edge.target]) {
80
193
  if (!idSet.has(endpoint))
81
194
  throw new errors_js_1.ToolInputError("SCENARIO_EDGE_ENDPOINT_NOT_FOUND", `Edge references "${endpoint}" which is not a node id.`);
82
195
  }
83
- return { id: edge.id ?? nextEdgeId(), source: edge.source, target: edge.target, ...(edge.type ? { type: edge.type } : {}) };
196
+ return { id: edge.id ?? nextEdgeId(), source: edge.source, target: edge.target, ...(edge.type && edge.type !== "default" ? { type: edge.type } : {}) };
84
197
  });
85
198
  // Layered positions for the nodes that have none: BFS from the sources.
86
199
  const incoming = new Map(ids.map((id) => [id, 0]));
@@ -103,38 +216,57 @@ const buildScenarioGraph = (nodesInput, edgesInput) => {
103
216
  }
104
217
  }
105
218
  const rowCounter = new Map();
106
- const positioned = nodes.map((node) => {
107
- if (node._positioned)
108
- return node;
109
- const layer = level.get(node.id) ?? 0;
110
- const row = rowCounter.get(layer) ?? 0;
111
- rowCounter.set(layer, row + 1);
112
- return { ...node, position: { x: 80 + layer * 320, y: 80 + row * 170 } };
219
+ const nodes = built.map((node) => {
220
+ let position = node.position;
221
+ if (!position) {
222
+ const layer = level.get(node.id) ?? 0;
223
+ const row = rowCounter.get(layer) ?? 0;
224
+ rowCounter.set(layer, row + 1);
225
+ position = { x: 80 + layer * 320, y: 80 + row * 170 };
226
+ }
227
+ return { id: node.id, type: node.type, position, data: { label: node.label, config: node.config } };
113
228
  });
114
- return {
115
- nodes: positioned.map(({ _positioned, ...node }) => node),
116
- edges,
117
- };
229
+ return { nodes, edges, sent, warnings };
118
230
  };
119
231
  exports.buildScenarioGraph = buildScenarioGraph;
120
- const buildScenarioCreateBody = (input, folderId) => {
232
+ // Environments: stored ids are kept by name so an edit does not orphan them;
233
+ // a masked value sent back unchanged keeps the stored value.
234
+ const buildEnvironments = (input, existing = []) => {
235
+ const nextEnvId = (0, ids_js_1.sequentialIds)("env", existing.map((environment) => environment.id));
236
+ return input.map((environment) => {
237
+ const stored = existing.find((entry) => entry.id === environment.id) ?? existing.find((entry) => entry.name.trim().toLowerCase() === environment.name.trim().toLowerCase());
238
+ const nextVarId = (0, ids_js_1.sequentialIds)("v", (stored?.variables ?? []).map((variable) => variable.id ?? ""));
239
+ const masked = [];
240
+ const variables = environment.variables.map((variable) => {
241
+ const previous = stored?.variables.find((entry) => entry.key === variable.key);
242
+ const value = (0, nodeConfig_js_1.restoreMasked)(variable.value, previous?.value);
243
+ if (value.startsWith("••••"))
244
+ masked.push(variable.key);
245
+ return { id: previous?.id ?? nextVarId(), key: variable.key, value, enabled: variable.enabled ?? previous?.enabled ?? true };
246
+ });
247
+ if (masked.length > 0) {
248
+ throw new errors_js_1.ToolInputError("MASKED_VALUE", `Environment "${environment.name}": masked values cannot be saved (${masked.join(", ")}). Send them back unchanged only for keys that already exist, or pass real values.`, { keys: masked });
249
+ }
250
+ return { id: environment.id ?? stored?.id ?? nextEnvId(), name: environment.name, variables };
251
+ });
252
+ };
253
+ exports.buildEnvironments = buildEnvironments;
254
+ const buildScenarioCreateBody = (input, folderId, groupId) => {
121
255
  const graph = (0, exports.buildScenarioGraph)(input.nodes, input.edges);
122
- const nextEnvId = (0, ids_js_1.sequentialIds)("env");
123
256
  return {
124
- name: input.name.trim(),
125
- ...(input.description ? { description: input.description } : {}),
126
- ...(input.priority ? { priority: input.priority } : {}),
127
- folderId,
128
- nodes: graph.nodes,
129
- edges: graph.edges,
130
- environments: (input.environments ?? []).map((environment) => {
131
- const nextVarId = (0, ids_js_1.sequentialIds)("v");
132
- return {
133
- id: environment.id ?? nextEnvId(),
134
- name: environment.name,
135
- variables: (0, requestShape_js_1.toRows)(environment.variables).map((row) => ({ id: nextVarId(), key: row.key, value: row.value, enabled: row.enabled ?? true })),
136
- };
137
- }),
257
+ graph,
258
+ body: {
259
+ name: input.name.trim(),
260
+ ...(input.description ? { description: input.description } : {}),
261
+ ...(input.priority ? { priority: input.priority } : {}),
262
+ folderId,
263
+ ...(groupId !== undefined ? { groupId } : {}),
264
+ nodes: graph.nodes,
265
+ edges: graph.edges,
266
+ environments: (0, exports.buildEnvironments)(input.environments ?? []),
267
+ ...(input.dataset !== undefined ? { dataset: input.dataset } : {}),
268
+ ...(input.dbSettings !== undefined ? { dbSettings: input.dbSettings } : {}),
269
+ },
138
270
  };
139
271
  };
140
272
  exports.buildScenarioCreateBody = buildScenarioCreateBody;
@@ -0,0 +1,260 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.droppedPaths = exports.maskConfigInPlace = exports.restoreMasked = exports.findUnwritableValues = exports.missingRequiredFields = exports.normalizeNodeConfig = exports.translateRequestConfig = void 0;
4
+ const callman_core_1 = require("callman-core");
5
+ const mask_js_1 = require("../../lib/mask.js");
6
+ // Node-config plumbing between the compact scenario a model writes and the
7
+ // stored config the backend validates (callman://docs/scenario-nodes):
8
+ // * request shapes from the request tools ({ type: "bearer", token },
9
+ // body: {…}) are translated to the scenario form, so a request read with
10
+ // get_request can be dropped into a request node;
11
+ // * convenience fixes that the backend would otherwise reject or ignore
12
+ // (assertion rule ids, expert-mode conditions);
13
+ // * secrets masked IN PLACE for reading (same shape — writable back), and
14
+ // a masked value sent back unchanged restored from the stored config;
15
+ // * a diff of what was sent vs what the server stored, because the
16
+ // backend's node schemas silently strip unknown keys.
17
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
18
+ let counter = 0;
19
+ const newId = (prefix) => `${prefix}_${Date.now().toString(36)}${(counter++).toString(36)}`;
20
+ // ── request / poll.request translation ────────────────────────────────
21
+ const translateAuth = (auth) => {
22
+ if (!isRecord(auth))
23
+ return auth;
24
+ const type = auth.type === "apiKey" ? "apikey" : auth.type;
25
+ const out = { ...auth, type };
26
+ const move = (from, to) => {
27
+ if (from in out && !(to in out))
28
+ out[to] = out[from];
29
+ delete out[from];
30
+ };
31
+ if (type === "bearer")
32
+ move("token", "bearerToken");
33
+ if (type === "basic") {
34
+ move("username", "basicUsername");
35
+ move("password", "basicPassword");
36
+ }
37
+ if (type === "apikey") {
38
+ move("key", "apiKeyKey");
39
+ move("value", "apiKeyValue");
40
+ move("location", "apiKeyLocation");
41
+ }
42
+ if (type === "oauth2" && !isRecord(out.oauth2)) {
43
+ // Flat oauth2 fields (the request tools' shape) → nested, all ten keys.
44
+ const oauth2 = {};
45
+ for (const key of ["grantType", "tokenUrl", "clientId", "clientSecret", "scope", "username", "password", "refreshToken", "clientAuth", "audience"]) {
46
+ oauth2[key] = out[key] ?? (key === "grantType" ? "client_credentials" : key === "clientAuth" ? "body" : "");
47
+ delete out[key];
48
+ }
49
+ out.oauth2 = oauth2;
50
+ }
51
+ return out;
52
+ };
53
+ const RAW_LANGUAGES = new Set(["json", "xml", "text", "html"]);
54
+ const translateBody = (body) => {
55
+ if (body === undefined)
56
+ return undefined;
57
+ if (body === null)
58
+ return { mode: "none" };
59
+ if (typeof body === "string") {
60
+ const state = (0, callman_core_1.inferBodyStateFromRaw)(body, []);
61
+ return { mode: "raw", raw: body, rawLanguage: state.rawLanguage ?? "text" };
62
+ }
63
+ if (isRecord(body) && typeof body.mode === "string") {
64
+ const out = { ...body };
65
+ if (out.mode === "json")
66
+ return { mode: "raw", raw: JSON.stringify(out.json ?? null, null, 2), rawLanguage: "json" };
67
+ if (out.mode === "raw" && typeof out.language === "string" && RAW_LANGUAGES.has(out.language) && out.rawLanguage === undefined) {
68
+ out.rawLanguage = out.language;
69
+ delete out.language;
70
+ }
71
+ if ((out.mode === "formdata" || out.mode === "urlencoded") && Array.isArray(out.entries) && out.formEntries === undefined) {
72
+ out.formEntries = out.entries;
73
+ delete out.entries;
74
+ }
75
+ return out;
76
+ }
77
+ // A bare JSON value — what get_request returns for JSON bodies.
78
+ return { mode: "raw", raw: JSON.stringify(body, null, 2), rawLanguage: "json" };
79
+ };
80
+ // get_request's `scripts: { preRequest, postResponse, tests }` → flat fields.
81
+ const translateScripts = (config) => {
82
+ const scripts = config.scripts;
83
+ if (!isRecord(scripts))
84
+ return;
85
+ if (typeof scripts.preRequest === "string" && config.preRequestScript === undefined)
86
+ config.preRequestScript = scripts.preRequest;
87
+ if (typeof scripts.postResponse === "string" && config.postResponseScript === undefined)
88
+ config.postResponseScript = scripts.postResponse;
89
+ if (typeof scripts.tests === "string" && config.testsScript === undefined)
90
+ config.testsScript = scripts.tests;
91
+ delete config.scripts;
92
+ };
93
+ const translateRequestConfig = (config) => {
94
+ const out = { ...config };
95
+ if ("auth" in out)
96
+ out.auth = translateAuth(out.auth);
97
+ if ("body" in out)
98
+ out.body = translateBody(out.body);
99
+ translateScripts(out);
100
+ return out;
101
+ };
102
+ exports.translateRequestConfig = translateRequestConfig;
103
+ // ── convenience fixes the backend would reject or ignore ──────────────
104
+ const withRuleIds = (node) => {
105
+ if (!isRecord(node))
106
+ return node;
107
+ if (Array.isArray(node.children)) {
108
+ return { id: node.id ?? newId("g"), type: node.type ?? "group", ...node, children: node.children.map(withRuleIds) };
109
+ }
110
+ if ("left" in node && "operator" in node)
111
+ return { id: node.id ?? newId("r"), type: node.type ?? "rule", ...node };
112
+ return node;
113
+ };
114
+ const normalizeNodeConfig = (type, config) => {
115
+ const notes = [];
116
+ let out = { ...config };
117
+ if (type === "request")
118
+ out = (0, exports.translateRequestConfig)(out);
119
+ if (type === "poll" && isRecord(out.request))
120
+ out.request = (0, exports.translateRequestConfig)(out.request);
121
+ if (type === "condition" || type === "poll") {
122
+ const target = type === "poll" && isRecord(out.successCondition) ? { ...out.successCondition } : out;
123
+ if (typeof target.expertExpression === "string" && target.expertExpression.trim() && target.mode === undefined) {
124
+ target.mode = "expert";
125
+ notes.push("expertExpression given without mode — set mode: \"expert\" (builder mode ignores it)");
126
+ }
127
+ if (target.rootGroup !== undefined)
128
+ target.rootGroup = withRuleIds(target.rootGroup);
129
+ if (type === "poll" && isRecord(out.successCondition))
130
+ out.successCondition = target;
131
+ }
132
+ if (type === "assertion" && Array.isArray(out.checks)) {
133
+ out.checks = out.checks.map((check) => {
134
+ if (!isRecord(check))
135
+ return check;
136
+ const id = typeof check.id === "string" ? check.id : newId("c");
137
+ return { id, name: check.name ?? "", enabled: check.enabled ?? true, ...check, rule: withRuleIds(check.rule) };
138
+ });
139
+ }
140
+ return { config: out, notes };
141
+ };
142
+ exports.normalizeNodeConfig = normalizeNodeConfig;
143
+ // ── what the model must supply (defaults are placeholders) ────────────
144
+ const missingRequiredFields = (type, userConfig, base) => {
145
+ const has = (key) => {
146
+ const value = userConfig[key] ?? base?.[key];
147
+ return typeof value === "string" ? value.trim().length > 0 : Array.isArray(value) ? value.length > 0 : value !== undefined && value !== null;
148
+ };
149
+ const missing = [];
150
+ switch (type) {
151
+ case "request":
152
+ if (!has("url"))
153
+ missing.push("url");
154
+ break;
155
+ case "db":
156
+ // The default query is a placeholder (SELECT * FROM users …) — never run it by accident.
157
+ if (!(typeof userConfig.query === "string" && userConfig.query.trim()) && !(base && typeof base.query === "string" && base.query.trim()))
158
+ missing.push("query");
159
+ break;
160
+ case "ai":
161
+ if (!has("connectionId"))
162
+ missing.push("connectionId (list_connections type ai)");
163
+ break;
164
+ case "assertion":
165
+ if (!has("checks"))
166
+ missing.push("checks");
167
+ break;
168
+ default:
169
+ break;
170
+ }
171
+ return missing;
172
+ };
173
+ exports.missingRequiredFields = missingRequiredFields;
174
+ // ── masked / clipped values ───────────────────────────────────────────
175
+ const findUnwritableValues = (value, path = "config", out = []) => {
176
+ if (typeof value === "string" && (0, mask_js_1.isMasked)(value))
177
+ out.push(`${path} (masked ••••)`);
178
+ else if (Array.isArray(value))
179
+ value.forEach((item, index) => (0, exports.findUnwritableValues)(item, `${path}[${index}]`, out));
180
+ else if (isRecord(value)) {
181
+ if (value._truncated === true)
182
+ out.push(`${path} (clipped preview — read it with get_scenario node: <id>)`);
183
+ else
184
+ for (const [key, child] of Object.entries(value))
185
+ (0, exports.findUnwritableValues)(child, `${path}.${key}`, out);
186
+ }
187
+ return out;
188
+ };
189
+ exports.findUnwritableValues = findUnwritableValues;
190
+ // A masked string equal to the mask of the stored value at the same path
191
+ // means "keep what is stored".
192
+ const restoreMasked = (value, stored) => {
193
+ if (typeof value === "string") {
194
+ return (0, mask_js_1.isMasked)(value) && typeof stored === "string" && stored && (0, mask_js_1.maskSecret)(stored) === value ? stored : value;
195
+ }
196
+ if (Array.isArray(value)) {
197
+ return value.map((item, index) => {
198
+ const storedItem = Array.isArray(stored)
199
+ ? (isRecord(item) && typeof item.key === "string" ? stored.find((entry) => isRecord(entry) && entry.key === item.key) : stored[index])
200
+ : undefined;
201
+ return (0, exports.restoreMasked)(item, storedItem);
202
+ });
203
+ }
204
+ if (isRecord(value)) {
205
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, (0, exports.restoreMasked)(child, isRecord(stored) ? stored[key] : undefined)]));
206
+ }
207
+ return value;
208
+ };
209
+ exports.restoreMasked = restoreMasked;
210
+ // ── reading: mask secrets without changing the shape ─────────────────
211
+ const SECRET_FIELDS = new Set([
212
+ "bearerToken",
213
+ "basicPassword",
214
+ "apiKeyValue",
215
+ "clientSecret",
216
+ "password",
217
+ "refreshToken",
218
+ "pfxBase64",
219
+ "keyPem",
220
+ "passphrase",
221
+ "secret",
222
+ "token",
223
+ "apiKey",
224
+ ]);
225
+ const maskConfigInPlace = (value, keyName = "") => {
226
+ if (typeof value === "string")
227
+ return SECRET_FIELDS.has(keyName) && value && !value.startsWith("{{") ? (0, mask_js_1.maskSecret)(value) : value;
228
+ if (Array.isArray(value)) {
229
+ return value.map((item) => {
230
+ // Header rows: { key: "Authorization", value: "…" }.
231
+ if (isRecord(item) && typeof item.key === "string" && typeof item.value === "string" && (0, mask_js_1.isSensitiveHeader)(item.key) && item.value && !item.value.startsWith("{{")) {
232
+ return { ...item, value: (0, mask_js_1.maskSecret)(item.value) };
233
+ }
234
+ return (0, exports.maskConfigInPlace)(item);
235
+ });
236
+ }
237
+ if (isRecord(value))
238
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, (0, exports.maskConfigInPlace)(child, key)]));
239
+ return value;
240
+ };
241
+ exports.maskConfigInPlace = maskConfigInPlace;
242
+ // ── sent vs stored ────────────────────────────────────────────────────
243
+ // Keys present in what we sent but absent from what the server stored —
244
+ // the backend's node schemas strip unknown keys without an error.
245
+ const droppedPaths = (sent, stored, path = "") => {
246
+ if (!isRecord(sent))
247
+ return [];
248
+ if (!isRecord(stored))
249
+ return [];
250
+ const out = [];
251
+ for (const [key, value] of Object.entries(sent)) {
252
+ const here = path ? `${path}.${key}` : key;
253
+ if (!(key in stored))
254
+ out.push(here);
255
+ else if (isRecord(value))
256
+ out.push(...(0, exports.droppedPaths)(value, stored[key], here));
257
+ }
258
+ return out;
259
+ };
260
+ exports.droppedPaths = droppedPaths;