cpi-saper-dev-mcp 0.9.0-beta.0 → 0.9.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,20 @@
3
3
  All notable changes of the `cpi-saper-dev-mcp` server. Versions follow [semantic versioning](https://semver.org/);
4
4
  before 1.0, names and behaviour may still change between versions.
5
5
 
6
+ ## 0.9.0-beta.1 (2026-11-04)
7
+
8
+ ### Added
9
+
10
+ - `iflow_edit`: local integration processes: `add_local_process`, `remove_local_process`, Process Call and Looping
11
+ Process Call with `insert_step` `process` (and `loop` for the looping one); `layout` handles iFlows with several
12
+ processes (one pool per process, stacked); a branching edit lays out only its own process.
13
+ - `iflow_validate`: calls pointing to no local process, a looping call without its loop, unquoted values in Camel loop
14
+ conditions.
15
+ - `iflow_edit` `set_start`: timer start (scheduled iFlow) or back to a message start. The schedule is always
16
+ externalized (parameter of type `custom:schedule`): run once, every n minutes or hours, or a cron schedule, with
17
+ start, end and time zone. `params_set` and `cpi_config_set` take schedule objects; `iflow_validate` warns about a
18
+ timer whose schedule is not externalized.
19
+
6
20
  ## 0.9.0-beta.0 (2026-11-02)
7
21
 
8
22
  First public beta. **For development tenants only**: do not connect a production tenant (see the README).
@@ -39,6 +53,6 @@ Read-only and development roles per tenant, allowed packages, confirmation of ch
39
53
 
40
54
  - Tried against one tenant (a BTP trial); other tenants may answer differently in places
41
55
  ([docs/KNOWN_LIMITATIONS.md](docs/KNOWN_LIMITATIONS.md) lists every difference found).
42
- - iFlows with local integration processes cannot be laid out or branched by `iflow_edit`.
56
+ - iFlows with local integration processes cannot be laid out or branched by `iflow_edit` (added in 0.9.0-beta.1).
43
57
  - Not offered by design: creating or deleting packages, editing message mappings, managing locks.
44
58
  - Planned: `lint`, PNG rendering, user roles for sender authorization.
package/README.md CHANGED
@@ -62,7 +62,7 @@ adds no generic file tools.
62
62
 
63
63
  | Tool | What it does | Status |
64
64
  |---|---|---|
65
- | `iflow_edit` | Semantic edits in one all-or-nothing batch: insert a step from the catalog (with settings, headers, properties; for XSLT and message mappings the mapping file, for a Filter its XPath, for a script step a script from a script collection), remove a step, add or remove an adapter (sender to a start event or Poll Enrich, receiver from Request Reply, Send, Content Enricher or an end event; the Sender/Receiver box is reused or created, receivers drawn below their step), add or remove an exception subprocess (steps go inside like anywhere else), add routers (conditions, default route) and parallel or sequential multicasts with a Join, put steps on their routes, edit or remove them, lay out the process (branches in rows, exception subprocess below), change settings of steps, adapters or the iFlow (unknown keys refused), rename, edit Content Modifier headers and properties. Keeps SAP's formatting, lays out the diagram, refuses edits that would break the iFlow, supports a dry run | **Available** |
65
+ | `iflow_edit` | Semantic edits in one all-or-nothing batch: insert a step from the catalog (with settings, headers, properties; for XSLT and message mappings the mapping file, for a Filter its XPath, for a script step a script from a script collection), remove a step, add or remove an adapter (sender to a start event or Poll Enrich, receiver from Request Reply, Send, Content Enricher or an end event; the Sender/Receiver box is reused or created, receivers drawn below their step), add or remove an exception subprocess (steps go inside like anywhere else), add routers (conditions, default route) and parallel or sequential multicasts with a Join, put steps on their routes, edit or remove them, add or remove local integration processes and call them (Process Call, Looping Process Call with its loop), make the start a timer (run once, every n minutes or hours, or cron; start and end, time zone; always externalized so the schedule can be configured), lay out the iFlow (branches in rows, exception subprocess below, one pool per process), change settings of steps, adapters or the iFlow (unknown keys refused), rename, edit Content Modifier headers and properties. Keeps SAP's formatting, lays out the diagram, refuses edits that would break the iFlow, supports a dry run | **Available** |
66
66
  | `params_set` | Declare, change or remove externalized parameters: `parameters.propdef` and `parameters.prop` kept consistent in the designer's format; shows where each `{{name}}` is used | **Available** |
67
67
 
68
68
  ### Check
@@ -92,7 +92,7 @@ development.**
92
92
  | `cpi_deploy` | Deploy the current version and wait; SAP's guideline check as an optional gate; errors are reported with step names, one line per problem. A failed deploy also stops the version that was running (Cloud Integration's behaviour), which the confirmation and result point out | **Available** |
93
93
  | `cpi_undeploy` | Remove a deployed iFlow or script collection from the runtime | **Available** |
94
94
  | `iflow_delete` | Delete an iFlow with its whole version history (for cleaning up test iFlows); refuses deployed iFlows and always asks for confirmation by default | **Available** |
95
- | `cpi_config_set` | Configure parameter values on the tenant like "Configure" in the designer (no new version; the running iFlow uses them after the next deploy); the local `parameters.prop` follows | **Available** |
95
+ | `cpi_config_set` | Configure parameter values on the tenant like "Configure" in the designer (no new version; the running iFlow uses them after the next deploy), timer schedules as a schedule object; the local `parameters.prop` follows | **Available** |
96
96
 
97
97
  ### Script collections (development tenants only)
98
98
 
@@ -161,7 +161,7 @@ for every change instead of the service key's technical client.
161
161
  Requires Node.js 22 or later. The server is published on npm as
162
162
  [`cpi-saper-dev-mcp`](https://www.npmjs.com/package/cpi-saper-dev-mcp): your AI client starts it with `npx`, which
163
163
  downloads it on first use. During the beta the examples use `cpi-saper-dev-mcp@beta` (the newest beta); to pin one
164
- version, write for example `cpi-saper-dev-mcp@0.9.0-beta.0`.
164
+ version, write for example `cpi-saper-dev-mcp@0.9.0-beta.1`.
165
165
 
166
166
  Create `.cpi/config.json` in your project folder ([docs/CONFIGURATION.md](docs/CONFIGURATION.md)), then connect the
167
167
  server to your AI client as shown below.
package/bundle/index.js CHANGED
@@ -849,6 +849,24 @@ function validate(doc, { resourceFiles, declaredParameters } = {}) {
849
849
  const need = NEEDS_ADAPTER[cname];
850
850
  if (need && !adapterOn.has(el.getAttribute("id")))
851
851
  add(need.severity, "NO_ADAPTER", `${label} needs ${need.what} (iflow_edit add_adapter)`);
852
+ if (p.activityType === "ProcessCallElement") {
853
+ const target = p.processId ? byTag(doc, "process").find((x) => x.getAttribute("id") === p.processId) : void 0;
854
+ if (!p.processId?.trim())
855
+ add("error", "MISSING_SETTING", `${label}: processId is empty; set the local integration process to call (iflow_edit insert_step "process")`);
856
+ else if (!target)
857
+ add("error", "INVALID_SETTING", `${label}: calls ${p.processId}, which is not a process of this iFlow`);
858
+ else if (readProps(target).processType !== "directCall")
859
+ add("error", "INVALID_SETTING", `${label}: calls ${p.processId}, which is not a local integration process`);
860
+ if (p.subActivityType === "LoopingProcess") {
861
+ const loop = children(el, "standardLoopCharacteristics")[0];
862
+ if (!loop || loop.getAttribute("id") !== p.loopId)
863
+ add("error", "INVALID_SETTING", `${label}: the loop (standardLoopCharacteristics ${p.loopId ?? ""}) is missing; insert the step with "loop"`);
864
+ const cond = loop ? children(loop, "loopCondition")[0]?.textContent?.trim() ?? "" : "";
865
+ if (p.expressionType !== "XML" && /(?:<=|>=|!=|=|<|>)\s*(?!['"$])[^\s')]+/.test(cond)) {
866
+ add("error", "INVALID_SETTING", `${label}: loop condition "${cond}": put the compared value in quotes (e.g. < '3'); SAP's model check refuses it otherwise`);
867
+ }
868
+ }
869
+ }
852
870
  const version = parseVariant(p.cmdVariantUri).version ?? "";
853
871
  if (cname === "GroovyScript" && version.startsWith("2.") && p.script && !p.scriptBundleId?.trim() && !p.script.startsWith("v2/")) {
854
872
  add("error", "INVALID_SETTING", `${label}: a Groovy Script ${version} step takes its script from script/v2/; set script to "v2/${p.script}" and move the file there`);
@@ -868,6 +886,12 @@ function validate(doc, { resourceFiles, declaredParameters } = {}) {
868
886
  add("error", "INVALID_SETTING", `route ${f.getAttribute("id")} (${f.getAttribute("name") ?? ""}): condition "${cond.trim()}" uses ==; Camel Simple compares with = (also !=, >, >=, <, <=, contains, in, regex)`);
869
887
  }
870
888
  }
889
+ for (const t of byTag(doc, "timerEventDefinition")) {
890
+ const key = readProps(t).scheduleKey ?? "";
891
+ if (!/^\{\{[^{}]+\}\}$/.test(key.trim())) {
892
+ add("warning", "SCHEDULE_NOT_EXTERNALIZED", `${t.parentNode.getAttribute("id")}: the timer's schedule is not externalized; iflow_edit set_start writes it as a parameter`);
893
+ }
894
+ }
871
895
  const allowed = byTag(doc, "collaboration")[0] ? readProps(byTag(doc, "collaboration")[0]).allowedHeaderList : void 0;
872
896
  if (allowed?.includes(","))
873
897
  add("warning", "ALLOWED_HEADERS", `allowedHeaderList "${allowed}": headers are separated by | (with commas no header gets through): "${allowed.split(/\s*,\s*/).join("|")}"`);
@@ -1376,6 +1400,149 @@ async function listSecurityMaterial(client, now = /* @__PURE__ */ new Date()) {
1376
1400
  };
1377
1401
  }
1378
1402
 
1403
+ // ../core/dist/schedule.js
1404
+ var UTC_LABEL = "( UTC 0:00 ) Greenwich Mean Time(Etc/GMT)";
1405
+ var DESIGNER_ZONES = { NST: "( UTC 12:00 ) New Zealand Standard Time(NST)" };
1406
+ var DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}$/;
1407
+ function zoneLabel(id) {
1408
+ if (id === "Etc/GMT")
1409
+ return UTC_LABEL;
1410
+ if (DESIGNER_ZONES[id])
1411
+ return DESIGNER_ZONES[id];
1412
+ try {
1413
+ new Intl.DateTimeFormat("en-US", { timeZone: id });
1414
+ } catch {
1415
+ throw new Error(`"${id}" is not a time zone id (use IANA ids such as Europe/Warsaw)`);
1416
+ }
1417
+ const part = (date, style) => new Intl.DateTimeFormat("en-US", { timeZone: id, timeZoneName: style }).formatToParts(date).find((p) => p.type === "timeZoneName").value;
1418
+ const minutes = (o) => {
1419
+ const m = o.match(/GMT([+-])(\d{2}):(\d{2})/);
1420
+ return m ? (m[1] === "-" ? -1 : 1) * (Number(m[2]) * 60 + Number(m[3])) : 0;
1421
+ };
1422
+ const [jan, jul] = [new Date(Date.UTC(2026, 0, 15)), new Date(Date.UTC(2026, 6, 15))];
1423
+ const std = minutes(part(jan, "longOffset")) <= minutes(part(jul, "longOffset")) ? jan : jul;
1424
+ const off = minutes(part(std, "longOffset"));
1425
+ const offText = `${off < 0 ? "-" : ""}${Math.floor(Math.abs(off) / 60)}:${String(Math.abs(off) % 60).padStart(2, "0")}`;
1426
+ return `( UTC ${offText} ) ${part(std, "long")}(${id})`;
1427
+ }
1428
+ var RANGES = [["second", 0, 59], ["minute", 0, 59], ["hour", 0, 23], ["day of month", 1, 31], ["month", 1, 12], ["day of week", 1, 7], ["year", 1970, 2199]];
1429
+ var NAMES = { 4: /^(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)$/, 5: /^(SUN|MON|TUE|WED|THU|FRI|SAT)$/ };
1430
+ function cronFields(cron) {
1431
+ const f = cron.trim().split(/\s+/);
1432
+ if (f.length === 6)
1433
+ f.push("*");
1434
+ if (f.length !== 7)
1435
+ throw new Error(`cron "${cron}" needs 6 or 7 fields: second minute hour day-of-month month day-of-week [year], e.g. "0 15 8 ? * MON-FRI"`);
1436
+ f.forEach((field, i) => {
1437
+ const [name, lo, hi] = RANGES[i];
1438
+ const value = (v) => /^\d+$/.test(v) ? Number(v) >= lo && Number(v) <= hi : !!NAMES[i]?.test(v.toUpperCase());
1439
+ const ok = field === "*" || field === "?" && (i === 3 || i === 5) || field.split(",").every((part) => {
1440
+ const [base, step] = part.split("/");
1441
+ if (step !== void 0 && !/^\d+$/.test(step))
1442
+ return false;
1443
+ if (base === "*")
1444
+ return step !== void 0;
1445
+ const [a, b] = base.split("-");
1446
+ return value(a) && (b === void 0 || value(b)) || i === 3 && /^(L|LW|\d{1,2}W)$/.test(part) || i === 5 && /^[1-7A-Z]{1,3}(#\d|L)$/i.test(part);
1447
+ });
1448
+ if (!ok)
1449
+ throw new Error(`cron "${cron}": "${field}" is not a valid ${name} (${lo}-${hi}, *, lists, ranges, steps like 0/15)`);
1450
+ });
1451
+ if (f[3] === "?" === (f[5] === "?"))
1452
+ throw new Error(`cron "${cron}": exactly one of day-of-month and day-of-week must be "?"`);
1453
+ return f;
1454
+ }
1455
+ function checkSchedule(s) {
1456
+ if (s === "once")
1457
+ return s;
1458
+ const o = s ?? {};
1459
+ if ("every" in o === "cron" in o)
1460
+ throw new Error('a schedule is "once", {every, unit} (simple) or {cron} (advanced)');
1461
+ if ("every" in o && o.unit !== "minutes" && o.unit !== "hours")
1462
+ throw new Error('a simple schedule needs unit "minutes" or "hours"');
1463
+ return o;
1464
+ }
1465
+ function window(s) {
1466
+ for (const [k, v] of [["from", s.from], ["until", s.until]])
1467
+ if (v !== void 0 && !DATE.test(v))
1468
+ throw new Error(`${k} "${v}" must look like 2026-11-05T08:00:00`);
1469
+ if (s.from && s.until && s.from >= s.until)
1470
+ throw new Error(`until ${s.until} is not after from ${s.from}`);
1471
+ const tz = s.timeZone ?? "Etc/GMT";
1472
+ return { tz, label: zoneLabel(tz), from: s.from ?? "", until: s.until ?? "" };
1473
+ }
1474
+ function scheduleTable(input) {
1475
+ const schedule = checkSchedule(input);
1476
+ const esc = (v) => v.replace(/&/g, "&amp;");
1477
+ let rows;
1478
+ if (schedule === "once") {
1479
+ rows = [
1480
+ ["dateType", ""],
1481
+ ["timeType", ""],
1482
+ ["dayValue", ""],
1483
+ ["monthValue", ""],
1484
+ ["yearValue", ""],
1485
+ ["onWeekly", ""],
1486
+ ["onMonthly", ""],
1487
+ ["OnEveryMinute", ""],
1488
+ ["fromInterval", ""],
1489
+ ["toInterval", ""],
1490
+ ["timeZone", UTC_LABEL],
1491
+ ["secondValue", "0"],
1492
+ ["minutesValue", ""],
1493
+ ["hourValue", ""],
1494
+ ["triggerType", "simple"],
1495
+ ["noOfSchedules", "1"],
1496
+ ["schedule1", "fireNow=true"]
1497
+ ];
1498
+ } else {
1499
+ const w = window(schedule);
1500
+ const tail = `&trigger.timeZone=${w.tz}${w.from ? `&trigger.startAt=${w.from}` : ""}${w.until ? `&trigger.endAt=${w.until}` : ""}`;
1501
+ let cron = ["*", "*", "*", "?", "*", "*", "*"];
1502
+ let schedule1;
1503
+ if ("every" in schedule) {
1504
+ const max = schedule.unit === "minutes" ? 59 : 23;
1505
+ if (!Number.isInteger(schedule.every) || schedule.every < 1 || schedule.every > max)
1506
+ throw new Error(`every ${schedule.unit}: a whole number from 1 to ${max}`);
1507
+ schedule1 = `simple.repeat=${schedule.unit === "minutes" ? "MINUTES" : "HOURS"}&simple.every=${schedule.every}${tail}`;
1508
+ } else {
1509
+ cron = cronFields(schedule.cron);
1510
+ schedule1 = `${cron.join("+")}${tail}`;
1511
+ }
1512
+ rows = [
1513
+ ["dateType", "every" in schedule ? "SIMPLE" : "ADVANCED"],
1514
+ ["secondValue", "0"],
1515
+ ["minutesValue", ""],
1516
+ ["hourValue", ""],
1517
+ ["toInterval", "1"],
1518
+ ["fromInterval", "0"],
1519
+ ["OnEveryMinute", "1"],
1520
+ ["timeType", "TIME_INTERVAL"],
1521
+ ["timeZone", w.label],
1522
+ ["throwExceptionOnExpiry", "true"],
1523
+ ["second", cron[0]],
1524
+ ["minute", cron[1]],
1525
+ ["hour", cron[2]],
1526
+ ["day_of_month", cron[3]],
1527
+ ["month", cron[4]],
1528
+ ["dayOfWeek", cron[5]],
1529
+ ["year", cron[6]],
1530
+ ["startAt", w.from],
1531
+ ["endAt", w.until],
1532
+ ["attributeBehaviour", "isThrowExceptionOnExpiryVisible,isScheduleAdvancedVisible,isScheduleAdvancedStartEndVisible,isScheduleSimpleVisible"],
1533
+ ["triggerType", "cron"],
1534
+ ["noOfSchedules", "1"],
1535
+ ["schedule1", schedule1]
1536
+ ];
1537
+ }
1538
+ return rows.map(([k, v]) => `<row><cell>${k}</cell><cell>${esc(v)}</cell></row>`).join("");
1539
+ }
1540
+ function scheduleConfigValue(schedule) {
1541
+ const w = window(schedule);
1542
+ const head = "every" in schedule ? `simple.repeat=${schedule.unit === "minutes" ? "MINUTES" : "HOURS"} --simple.every=${schedule.every}` : cronFields(schedule.cron).join(" ");
1543
+ return `${head} --tz=${w.tz}${w.from ? ` --startAt=${w.from}` : ""}${w.until ? ` --endAt=${w.until}` : ""}`;
1544
+ }
1545
+
1379
1546
  // ../core/dist/iflow/edit.js
1380
1547
  var isEl3 = (n) => !!n && n.nodeType === 1;
1381
1548
  var fmt = (n) => Number.isInteger(n) ? `${n}.0` : String(n);
@@ -2173,6 +2340,153 @@ function removeGateway(doc, ref3) {
2173
2340
  edge.setAttribute("targetElement", `BPMNShape_${next.getAttribute("id")}`);
2174
2341
  return `removed ${gwId}${join15 ? ` and its Join ${join15.getAttribute("id")}` : ""} with ${routes.length} routes; ${inFlow.getAttribute("sourceRef")} leads to ${next.getAttribute("id")} again`;
2175
2342
  }
2343
+ function findLocalProcess(doc, ref3) {
2344
+ const p = findProcess(doc, ref3);
2345
+ if (readProps(p).processType !== "directCall")
2346
+ throw new Error(`${p.getAttribute("id")} "${p.getAttribute("name")}" is not a local integration process`);
2347
+ return p;
2348
+ }
2349
+ var plainEvent = (doc, tag2, id, name, flow) => {
2350
+ const el = elementNS(doc, `bpmn2:${tag2}`, { id, name });
2351
+ const ext = elementNS(doc, "bpmn2:extensionElements");
2352
+ const cname = tag2 === "startEvent" ? "StartEvent" : "EndEvent";
2353
+ ext.appendChild(iflProperty(doc, "cmdVariantUri", `ctype::FlowstepVariant/cname::${cname}`));
2354
+ ext.appendChild(iflProperty(doc, "activityType", cname));
2355
+ el.appendChild(ext);
2356
+ el.appendChild(elementNS(doc, tag2 === "startEvent" ? "bpmn2:outgoing" : "bpmn2:incoming", {}, flow));
2357
+ return el;
2358
+ };
2359
+ function addLocalProcess(doc, { name, processProps }) {
2360
+ const index = idIndex(doc);
2361
+ const collab = byTag(doc, "collaboration")[0];
2362
+ const pools = children(collab, "participant").filter((p) => p.getAttribute("ifl:type") === "IntegrationProcess");
2363
+ const names = new Set(byTag(doc, "process").map((p) => p.getAttribute("name")));
2364
+ let procName2 = name;
2365
+ for (let n = 1; !procName2; n++)
2366
+ if (!names.has(`Local Integration Process ${n}`))
2367
+ procName2 = `Local Integration Process ${n}`;
2368
+ if (names.has(procName2))
2369
+ throw new Error(`a process named "${procName2}" exists already`);
2370
+ const processId = newId(index, "Process");
2371
+ const startId = newId(index, "StartEvent");
2372
+ const endId = newId(index, "EndEvent");
2373
+ const flowId = newId(index, "SequenceFlow");
2374
+ const poolId = `Participant_${processId}`;
2375
+ index.set(poolId, null);
2376
+ const participant = elementNS(doc, "bpmn2:participant", { id: poolId, "ifl:type": "IntegrationProcess", name: procName2, processRef: processId });
2377
+ participant.appendChild(elementNS(doc, "bpmn2:extensionElements"));
2378
+ insertAfterEl(collab, participant, pools.at(-1) ?? children(collab, "participant").at(-1));
2379
+ const proc = withProps(doc, elementNS(doc, "bpmn2:process", { id: processId, name: procName2 }), processProps);
2380
+ proc.appendChild(plainEvent(doc, "startEvent", startId, `Start ${procName2}`, flowId));
2381
+ proc.appendChild(plainEvent(doc, "endEvent", endId, `End ${procName2}`, flowId));
2382
+ proc.appendChild(flowEl(doc, flowId, startId, endId));
2383
+ insertAfterEl(doc.documentElement, proc, byTag(doc, "process").at(-1));
2384
+ const shapes = pools.map((p) => shapeOf(doc, p.getAttribute("id"))).filter((x2) => !!x2).map(bounds);
2385
+ const x = shapes.length ? Math.min(...shapes.map((b) => b.x)) : 250;
2386
+ const y = shapes.length ? Math.max(...byTag(doc, "BPMNShape", NS.bpmndi).map(bounds).map((b) => b.y + b.h)) + 60 : 60;
2387
+ const plane = byTag(doc, "BPMNPlane", NS.bpmndi)[0];
2388
+ let last = byTag(doc, "BPMNShape", NS.bpmndi).at(-1);
2389
+ const put = (id, b) => {
2390
+ const sh = elementNS(doc, "bpmndi:BPMNShape", { bpmnElement: id, id: `BPMNShape_${id}` });
2391
+ sh.appendChild(elementNS(doc, "dc:Bounds", { height: fmt(b.h), width: fmt(b.w), x: fmt(b.x), y: fmt(b.y) }));
2392
+ insertAfterEl(plane, sh, last);
2393
+ last = sh;
2394
+ return b;
2395
+ };
2396
+ put(poolId, { x, y, w: 540, h: 220 });
2397
+ const sb = put(startId, { x: x + 42, y: y + 82, w: 32, h: 32 });
2398
+ const eb = put(endId, { x: x + 453, y: y + 82, w: 32, h: 32 });
2399
+ const edge = elementNS(doc, "bpmndi:BPMNEdge", { bpmnElement: flowId, id: `BPMNEdge_${flowId}`, sourceElement: `BPMNShape_${startId}`, targetElement: `BPMNShape_${endId}` });
2400
+ for (const b of [sb, eb])
2401
+ edge.appendChild(elementNS(doc, "di:waypoint", { x: fmt(b.x + b.w / 2), "xsi:type": "dc:Point", y: fmt(b.y + b.h / 2) }));
2402
+ insertAfterEl(plane, edge, byTag(doc, "BPMNEdge", NS.bpmndi).at(-1) ?? last);
2403
+ return { processId, startId, endId, name: procName2 };
2404
+ }
2405
+ function removeLocalProcess(doc, ref3) {
2406
+ const proc = findLocalProcess(doc, ref3);
2407
+ const id = proc.getAttribute("id");
2408
+ const callers = byTag(doc, "callActivity").filter((c) => readProps(c).activityType === "ProcessCallElement" && readProps(c).processId === id);
2409
+ if (callers.length)
2410
+ throw new Error(`${id} is called by ${callers.map((c) => `${c.getAttribute("id")} "${c.getAttribute("name")}"`).join(", ")}; remove those calls first`);
2411
+ const ids = /* @__PURE__ */ new Set();
2412
+ const collect = (el) => {
2413
+ const x = el.getAttribute("id");
2414
+ if (x)
2415
+ ids.add(x);
2416
+ for (const c of children(el))
2417
+ collect(c);
2418
+ };
2419
+ collect(proc);
2420
+ const adapter = byTag(doc, "messageFlow").find((m) => ids.has(m.getAttribute("sourceRef")) || ids.has(m.getAttribute("targetRef")));
2421
+ if (adapter)
2422
+ throw new Error(`a step in ${id} has an adapter (${adapter.getAttribute("id")}); remove it first (remove_adapter)`);
2423
+ const participant = children(byTag(doc, "collaboration")[0], "participant").find((p) => p.getAttribute("processRef") === id);
2424
+ if (participant)
2425
+ ids.add(participant.getAttribute("id"));
2426
+ for (const di of [...byTag(doc, "BPMNShape", NS.bpmndi), ...byTag(doc, "BPMNEdge", NS.bpmndi)])
2427
+ if (ids.has(di.getAttribute("bpmnElement")))
2428
+ removeEl(di);
2429
+ if (participant)
2430
+ removeEl(participant);
2431
+ removeEl(proc);
2432
+ return `removed local integration process ${id} "${proc.getAttribute("name")}" with its content`;
2433
+ }
2434
+ function setLoop(el, { condition: condition2, max = 100 }) {
2435
+ const doc = el.ownerDocument;
2436
+ for (const old of children(el, "standardLoopCharacteristics"))
2437
+ removeEl(old);
2438
+ const stamp2 = Date.now();
2439
+ const id = `StandardLoopCharacteristics_${stamp2}`;
2440
+ const loop = elementNS(doc, "bpmn2:standardLoopCharacteristics", { id, loopMaximum: String(max) });
2441
+ loop.appendChild(elementNS(doc, "bpmn2:loopCondition", { id: `FormalExpression_${stamp2}`, "xsi:type": "bpmn2:tFormalExpression" }, condition2));
2442
+ insertAfterEl(el, loop, Array.from(el.childNodes).filter(isEl3).at(-1));
2443
+ return id;
2444
+ }
2445
+ function setStart(doc, { process: process2, kind, schedule = "once", parameter }) {
2446
+ const proc = findProcess(doc, process2);
2447
+ if (readProps(proc).processType === "directCall")
2448
+ throw new Error(`${proc.getAttribute("id")} is a local integration process: it is started by a Process Call`);
2449
+ const starts = children(proc, "startEvent");
2450
+ if (starts.length !== 1)
2451
+ throw new Error(`${proc.getAttribute("id")} must have exactly one start event (has ${starts.length})`);
2452
+ const start = starts[0];
2453
+ const id = start.getAttribute("id");
2454
+ const index = idIndex(doc);
2455
+ const docEl = start.ownerDocument;
2456
+ const timer = children(start, "timerEventDefinition")[0];
2457
+ if (kind === "timer") {
2458
+ const sender = byTag(doc, "messageFlow").find((m) => m.getAttribute("targetRef") === id);
2459
+ if (sender)
2460
+ throw new Error(`${id} has a sender adapter (${sender.getAttribute("id")} "${sender.getAttribute("name")}"); a scheduled iFlow has none: remove it first (remove_adapter)`);
2461
+ const table = scheduleTable(schedule);
2462
+ const existing = timer ? readProps(timer).scheduleKey?.match(/^\{\{([^{}]+)\}\}$/)?.[1] : void 0;
2463
+ const name = parameter ?? existing ?? "timer_schedule";
2464
+ if (/[{}]/.test(name) || !name.trim())
2465
+ throw new Error(`"${name}" is not a usable parameter name`);
2466
+ const what = schedule === "once" ? "runs once after deploy" : "every" in schedule ? `every ${schedule.every} ${schedule.unit}` : `cron ${schedule.cron}`;
2467
+ const where = schedule !== "once" && schedule.timeZone ? ` (${schedule.timeZone})` : schedule === "once" ? "" : " (UTC)";
2468
+ if (timer) {
2469
+ setProperties(timer, { scheduleKey: `{{${name}}}` });
2470
+ return { message: `${id}: schedule ${what}${where}, parameter ${name}`, parameter: { name, table } };
2471
+ }
2472
+ for (const c of [...children(start, "extensionElements"), ...children(start, "messageEventDefinition")])
2473
+ removeEl(c);
2474
+ const def = elementNS(docEl, "bpmn2:timerEventDefinition", { id: newId(index, "TimerEventDefinition") });
2475
+ withProps(docEl, def, { scheduleKey: `{{${name}}}`, componentVersion: "1.4", cmdVariantUri: "ctype::FlowstepVariant/cname::intermediatetimer/version::1.4.0", activityType: "StartTimerEvent" });
2476
+ insertAfterEl(start, def, Array.from(start.childNodes).filter(isEl3).at(-1));
2477
+ return { message: `${id}: timer start, ${what}${where}, externalized as parameter ${name}`, parameter: { name, table } };
2478
+ }
2479
+ if (!timer)
2480
+ return { message: `${id} is a message start already` };
2481
+ removeEl(timer);
2482
+ withProps(docEl, start, { cmdVariantUri: "ctype::FlowstepVariant/cname::MessageStartEvent/version::1.0", componentVersion: "1.0" });
2483
+ const ext = children(start, "extensionElements")[0];
2484
+ const inner = indentBefore(children(start, "outgoing")[0] ?? start);
2485
+ start.insertBefore(docEl.createTextNode(inner), ext);
2486
+ prettify(ext, inner.replace(/^\n/, ""));
2487
+ insertAfterEl(start, elementNS(docEl, "bpmn2:messageEventDefinition"), Array.from(start.childNodes).filter(isEl3).at(-1));
2488
+ return { message: `${id}: message start again; connect a sender with add_adapter (the schedule parameter stays in parameters.prop)` };
2489
+ }
2176
2490
  function findProcess(doc, ref3) {
2177
2491
  const processes = byTag(doc, "process");
2178
2492
  if (ref3) {
@@ -2377,11 +2691,129 @@ var isBranching = (doc, flowId) => {
2377
2691
  const src = f?.getAttribute("sourceRef");
2378
2692
  return !!src && byTag(doc, "sequenceFlow").filter((x) => x.getAttribute("sourceRef") === src).length > 1;
2379
2693
  };
2694
+ var poolShapeOf = (doc, proc) => {
2695
+ const id = children(byTag(doc, "collaboration")[0], "participant").find((p) => p.getAttribute("processRef") === proc.getAttribute("id"))?.getAttribute("id");
2696
+ return id ? shapeOf(doc, id) : void 0;
2697
+ };
2698
+ var procName = (proc) => `"${proc.getAttribute("name") ?? proc.getAttribute("id")}"`;
2699
+ var processesInOrder = (doc) => {
2700
+ const all = byTag(doc, "process");
2701
+ const local = (p) => readProps(p).processType === "directCall";
2702
+ return [...all.filter((p) => !local(p)), ...all.filter(local)];
2703
+ };
2704
+ function moveProcess(doc, proc, dx, dy) {
2705
+ if (!dx && !dy)
2706
+ return;
2707
+ const ids = /* @__PURE__ */ new Set();
2708
+ const collect = (el) => {
2709
+ const id = el.getAttribute("id");
2710
+ if (id)
2711
+ ids.add(id);
2712
+ for (const c of children(el))
2713
+ collect(c);
2714
+ };
2715
+ collect(proc);
2716
+ const pool = poolShapeOf(doc, proc);
2717
+ for (const sh of byTag(doc, "BPMNShape", NS.bpmndi)) {
2718
+ if (sh !== pool && !ids.has(sh.getAttribute("bpmnElement")))
2719
+ continue;
2720
+ const b = bounds(sh);
2721
+ b.el.setAttribute("x", fmt(b.x + dx));
2722
+ b.el.setAttribute("y", fmt(b.y + dy));
2723
+ }
2724
+ for (const e of byTag(doc, "BPMNEdge", NS.bpmndi)) {
2725
+ if (!ids.has(e.getAttribute("bpmnElement")))
2726
+ continue;
2727
+ for (const wp of children(e, "waypoint", NS.di)) {
2728
+ wp.setAttribute("x", fmt(Number(wp.getAttribute("x")) + dx));
2729
+ wp.setAttribute("y", fmt(Number(wp.getAttribute("y")) + dy));
2730
+ }
2731
+ }
2732
+ }
2380
2733
  function layoutStraight(doc) {
2381
- const processes = byTag(doc, "process");
2382
- if (processes.length !== 1)
2383
- throw new Error(`layout handles one integration process; this iFlow has ${processes.length} (local processes): use the designer`);
2384
- const proc = processes[0];
2734
+ const procs = processesInOrder(doc);
2735
+ if (!procs.length)
2736
+ throw new Error("the iFlow has no integration process");
2737
+ const lines = [];
2738
+ let below;
2739
+ let left;
2740
+ for (const proc of procs) {
2741
+ const pool = poolShapeOf(doc, proc);
2742
+ if (!pool)
2743
+ throw new Error(`process ${procName(proc)} has no pool in the diagram`);
2744
+ if (below !== void 0) {
2745
+ const b2 = bounds(pool);
2746
+ moveProcess(doc, proc, left - b2.x, below + 60 - b2.y);
2747
+ }
2748
+ const msg = layoutProcess(doc, proc);
2749
+ lines.push(procs.length > 1 ? `${procName(proc)}: ${msg}` : msg);
2750
+ const b = bounds(pool);
2751
+ const ids = /* @__PURE__ */ new Set();
2752
+ const collect = (el) => {
2753
+ const id = el.getAttribute("id");
2754
+ if (id)
2755
+ ids.add(id);
2756
+ for (const c of children(el))
2757
+ collect(c);
2758
+ };
2759
+ collect(proc);
2760
+ const boxes = byTag(doc, "messageFlow").flatMap((m) => [m.getAttribute("sourceRef"), m.getAttribute("targetRef")].filter((x) => !ids.has(x)).filter(() => ids.has(m.getAttribute("sourceRef")) || ids.has(m.getAttribute("targetRef"))));
2761
+ const boxBottoms = boxes.map((id) => shapeOf(doc, id)).filter((x) => !!x).map(bounds).map((x) => x.y + x.h);
2762
+ below = Math.max(b.y + b.h, ...boxBottoms);
2763
+ left ??= b.x;
2764
+ }
2765
+ routeAdapters(doc);
2766
+ keepOnCanvas(doc);
2767
+ return lines.join("; ");
2768
+ }
2769
+ function layoutAround(doc, elementId) {
2770
+ let proc = idIndex(doc).get(elementId) ?? null;
2771
+ while (proc && proc.localName !== "process")
2772
+ proc = proc.parentNode;
2773
+ if (!proc)
2774
+ throw new Error(`${elementId} is not in a process`);
2775
+ const pool = poolShapeOf(doc, proc);
2776
+ if (!pool)
2777
+ throw new Error(`process ${procName(proc)} has no pool in the diagram`);
2778
+ const before = bounds(pool);
2779
+ const mine = /* @__PURE__ */ new Set();
2780
+ const collect = (el) => {
2781
+ const id = el.getAttribute("id");
2782
+ if (id)
2783
+ mine.add(id);
2784
+ for (const c of children(el))
2785
+ collect(c);
2786
+ };
2787
+ collect(proc);
2788
+ for (const mf of byTag(doc, "messageFlow")) {
2789
+ const [a, b] = [mf.getAttribute("sourceRef"), mf.getAttribute("targetRef")];
2790
+ if (mine.has(a) || mine.has(b)) {
2791
+ mine.add(a);
2792
+ mine.add(b);
2793
+ mine.add(mf.getAttribute("id"));
2794
+ }
2795
+ }
2796
+ const msg = layoutProcess(doc, proc);
2797
+ const grow = bounds(pool).y + bounds(pool).h - (before.y + before.h);
2798
+ if (grow > 0 && byTag(doc, "process").length > 1) {
2799
+ for (const sh of byTag(doc, "BPMNShape", NS.bpmndi)) {
2800
+ const b = bounds(sh);
2801
+ if (sh !== pool && !mine.has(sh.getAttribute("bpmnElement")) && b.y >= before.y + before.h)
2802
+ b.el.setAttribute("y", fmt(b.y + grow));
2803
+ }
2804
+ for (const e of byTag(doc, "BPMNEdge", NS.bpmndi)) {
2805
+ if (mine.has(e.getAttribute("bpmnElement")))
2806
+ continue;
2807
+ for (const wp of children(e, "waypoint", NS.di))
2808
+ if (Number(wp.getAttribute("y")) >= before.y + before.h)
2809
+ wp.setAttribute("y", fmt(Number(wp.getAttribute("y")) + grow));
2810
+ }
2811
+ }
2812
+ routeAdapters(doc);
2813
+ keepOnCanvas(doc);
2814
+ return msg;
2815
+ }
2816
+ function layoutProcess(doc, proc) {
2385
2817
  const subs = children(proc, "subProcess");
2386
2818
  const other = subs.find((x) => !isExceptionSub(x));
2387
2819
  if (other)
@@ -2390,7 +2822,7 @@ function layoutStraight(doc) {
2390
2822
  const byId = new Map(nodes.map((n) => [n.getAttribute("id"), n]));
2391
2823
  const starts = nodes.filter((n) => n.localName === "startEvent");
2392
2824
  if (starts.length !== 1)
2393
- throw new Error(`layout needs exactly one start event (has ${starts.length})`);
2825
+ throw new Error(`layout needs exactly one start event in process ${procName(proc)} (has ${starts.length}): use the designer`);
2394
2826
  const flows = children(proc, "sequenceFlow");
2395
2827
  const out = (id) => {
2396
2828
  const order = children(byId.get(id), "outgoing").map((o) => o.textContent?.trim());
@@ -2457,10 +2889,9 @@ function layoutStraight(doc) {
2457
2889
  row2.set(id, used.indexOf(x));
2458
2890
  rows = used.length - 1;
2459
2891
  const collab = byTag(doc, "collaboration")[0];
2460
- const poolId = children(collab, "participant").find((p) => p.getAttribute("processRef") === proc.getAttribute("id"))?.getAttribute("id");
2461
- const poolShape = poolId ? shapeOf(doc, poolId) : void 0;
2892
+ const poolShape = poolShapeOf(doc, proc);
2462
2893
  if (!poolShape)
2463
- throw new Error("the integration process has no diagram shape");
2894
+ throw new Error(`process ${procName(proc)} has no diagram shape`);
2464
2895
  const shapeBounds = new Map(nodes.map((n) => {
2465
2896
  const sh = shapeOf(doc, n.getAttribute("id"));
2466
2897
  if (!sh)
@@ -2554,8 +2985,6 @@ function layoutStraight(doc) {
2554
2985
  placeBox(doc, box, { ...hit[1] }, hit[0].localName === "startEvent" && hit[0].parentNode === proc);
2555
2986
  boxes++;
2556
2987
  }
2557
- routeAdapters(doc);
2558
- keepOnCanvas(doc);
2559
2988
  const where = rows ? `in ${cols} columns and ${rows + 1} rows` : "in one row";
2560
2989
  return `laid out ${nodes.length} elements ${where}${inner ? ` and ${inner.chain.length} in the exception subprocess below` : ""} (pool width ${right + GAP - pool.x}), ${boxes} Sender/Receiver box(es) placed`;
2561
2990
  }
@@ -2617,7 +3046,7 @@ function expressionSettings(cname, expression, p) {
2617
3046
  return expression === void 0 ? {} : { wrapContent: expression };
2618
3047
  }
2619
3048
  var needsMappingFile = (cname, p) => cname === "XSLTMapping" && p.mappingSource === "mappingSrcIflow" || cname === "MessageMapping" && p.mappingReference === "static";
2620
- function applyEdits(doc, ops, catalog) {
3049
+ function applyEdits(doc, ops, catalog, effects = { params: [] }) {
2621
3050
  const log = [];
2622
3051
  ops.forEach((op, i) => {
2623
3052
  const where = `operation ${i + 1} (${op.op})`;
@@ -2654,6 +3083,23 @@ function applyEdits(doc, ops, catalog) {
2654
3083
  throw new Error(`a script from collection ${op.scriptCollection} needs settings.script: its file name in the collection, e.g. "Utils.groovy"`);
2655
3084
  properties.scriptBundleId = op.scriptCollection;
2656
3085
  }
3086
+ if (cname === "NonLoopingProcess" || cname === "LoopingProcess") {
3087
+ if (!op.process)
3088
+ throw new Error(`${entry.label} needs "process": the local integration process to call (add one with add_local_process)`);
3089
+ const target = findLocalProcess(doc, op.process);
3090
+ let own = findElement(doc, after);
3091
+ while (own && own.localName !== "process")
3092
+ own = own.parentNode;
3093
+ if (own === target)
3094
+ throw new Error(`a step in ${target.getAttribute("id")} cannot call its own process`);
3095
+ properties.processId = target.getAttribute("id");
3096
+ if (cname === "LoopingProcess") {
3097
+ if (!op.loop?.condition)
3098
+ throw new Error(`${entry.label} needs "loop": { condition (repeat while true), max (default 100) }`);
3099
+ properties.expressionType = op.loop.expressionType === "XML" ? "XML" : "Non XML";
3100
+ }
3101
+ } else if (op.process || op.loop)
3102
+ throw new Error('"process" and "loop" apply to Process Call and Looping Process Call steps only');
2657
3103
  if (cname === "GroovyScript" && version.startsWith("2.") && properties.script && !op.scriptCollection && !properties.script.startsWith("v2/")) {
2658
3104
  throw new Error(`a Groovy Script ${version} step takes its script from script/v2/: use settings.script "v2/${properties.script}" (and put the file there)`);
2659
3105
  }
@@ -2677,6 +3123,8 @@ function applyEdits(doc, ops, catalog) {
2677
3123
  ...onRoute ? { flowId: onRoute.getAttribute("id") } : {}
2678
3124
  });
2679
3125
  const el = findElement(doc, stepId);
3126
+ if (op.loop && cname === "LoopingProcess")
3127
+ setProperties(el, { loopId: setLoop(el, { condition: op.loop.condition, ...op.loop.max ? { max: op.loop.max } : {} }) });
2680
3128
  if (op.headers?.length)
2681
3129
  editTable(el, "headerTable", { set: op.headers });
2682
3130
  if (op.properties?.length)
@@ -2684,7 +3132,7 @@ function applyEdits(doc, ops, catalog) {
2684
3132
  log.push(`inserted ${stepId} "${op.name ?? entry.label}" (${entry.label} ${version}, ${reason}) ${onRoute ? `at the start of route ${onRoute.getAttribute("id")}` : `after ${after}`}`);
2685
3133
  if (onRoute) {
2686
3134
  try {
2687
- log.push(layoutStraight(doc));
3135
+ log.push(layoutAround(doc, stepId));
2688
3136
  } catch (e) {
2689
3137
  log.push(`not laid out (${e.message}); the new step sits next to ${after}`);
2690
3138
  }
@@ -2734,6 +3182,22 @@ function applyEdits(doc, ops, catalog) {
2734
3182
  log.push(`removed exception subprocess ${removeExceptionSubprocess(doc, op.process)} with its content`);
2735
3183
  break;
2736
3184
  }
3185
+ case "set_start": {
3186
+ const r = setStart(doc, { ...op.process ? { process: op.process } : {}, kind: op.kind, ...op.schedule ? { schedule: op.schedule } : {}, ...op.parameter ? { parameter: op.parameter } : {} });
3187
+ if (r.parameter)
3188
+ effects.params.push({ name: r.parameter.name, value: r.parameter.table, type: "custom:schedule", description: "Schedule of the timer start" });
3189
+ log.push(r.message);
3190
+ break;
3191
+ }
3192
+ case "add_local_process": {
3193
+ const r = addLocalProcess(doc, { ...op.name ? { name: op.name } : {}, processProps: catalogProps(catalog, "LocalIntegrationProcess", doc) });
3194
+ log.push(`added local integration process ${r.processId} "${r.name}": ${r.startId} -> ${r.endId}; insert steps after ${r.startId}, call it with insert_step Process Call "process"`);
3195
+ break;
3196
+ }
3197
+ case "remove_local_process": {
3198
+ log.push(removeLocalProcess(doc, op.process));
3199
+ break;
3200
+ }
2737
3201
  case "add_router": {
2738
3202
  const after = findElement(doc, op.after).getAttribute("id");
2739
3203
  const r = addRouter(doc, after, {
@@ -2744,7 +3208,7 @@ function applyEdits(doc, ops, catalog) {
2744
3208
  endProps: catalogProps(catalog, "MessageEndEvent", doc)
2745
3209
  });
2746
3210
  log.push(`added router ${r.gatewayId} after ${after} with routes ${r.routeIds.join(", ")} (default ${r.routeIds[0]})${r.endIds.length ? `, End events ${r.endIds.join(", ")}` : ""}; insert steps on a route with insert_step "route"`);
2747
- log.push(layoutStraight(doc));
3211
+ log.push(layoutAround(doc, r.gatewayId));
2748
3212
  break;
2749
3213
  }
2750
3214
  case "add_multicast": {
@@ -2760,7 +3224,7 @@ function applyEdits(doc, ops, catalog) {
2760
3224
  endProps: catalogProps(catalog, "MessageEndEvent", doc)
2761
3225
  });
2762
3226
  log.push(`added ${sequential ? "sequential" : "parallel"} multicast ${r.gatewayId} after ${after} with branches ${r.branchIds.join(", ")}${r.joinId ? `, joined in ${r.joinId}` : ""}${r.endIds.length ? `, End events ${r.endIds.join(", ")}` : ""}; insert steps on a branch with insert_step "route"`);
2763
- log.push(layoutStraight(doc));
3227
+ log.push(layoutAround(doc, r.gatewayId));
2764
3228
  break;
2765
3229
  }
2766
3230
  case "set_route": {
@@ -2768,8 +3232,12 @@ function applyEdits(doc, ops, catalog) {
2768
3232
  break;
2769
3233
  }
2770
3234
  case "remove_gateway": {
3235
+ let proc = findElement(doc, op.gateway);
3236
+ while (proc && proc.localName !== "process")
3237
+ proc = proc.parentNode;
2771
3238
  log.push(removeGateway(doc, op.gateway));
2772
- log.push(layoutStraight(doc));
3239
+ if (proc)
3240
+ log.push(layoutAround(doc, proc.getAttribute("id")));
2773
3241
  break;
2774
3242
  }
2775
3243
  case "layout": {
@@ -3539,7 +4007,7 @@ function readChangelog(path, max = 5) {
3539
4007
  // ../core/dist/params.js
3540
4008
  import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "node:fs";
3541
4009
  import { dirname as dirname5, join as join5 } from "node:path";
3542
- var PARAM_TYPES = ["xsd:string", "xsd:integer", "xsd:long", "xsd:boolean", "xsd:date", "xsd:time", "xsd:dateTime"];
4010
+ var PARAM_TYPES = ["xsd:string", "xsd:integer", "xsd:long", "xsd:boolean", "xsd:date", "xsd:time", "xsd:dateTime", "custom:schedule"];
3543
4011
  var rx = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3544
4012
  var blockOf = (name) => new RegExp(`<parameter>(?:(?!</parameter>)[\\s\\S])*?<name>${rx(escapeXml(name))}</name>[\\s\\S]*?</parameter>`);
3545
4013
  var unescapeXml = (s) => s.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&amp;/g, "&");
@@ -4526,6 +4994,12 @@ var operation = z4.discriminatedUnion("op", [
4526
4994
  version: z4.string().optional().describe("Component version; default: the version this iFlow already uses, else the latest"),
4527
4995
  name: z4.string().optional().describe("Step name; default: the catalog label"),
4528
4996
  settings: kv.optional().describe('Settings to change from the catalog defaults; script steps need "script" (file under script/, v2/<name>.groovy for 2.x; with scriptCollection the file name in the collection)'),
4997
+ process: z4.string().optional().describe("Process Call / Looping Process Call: the local integration process to call (id or name; add_local_process creates one)"),
4998
+ loop: z4.object({
4999
+ condition: z4.string().describe("Repeat while this holds, e.g. ${property.hasMore} = 'true'"),
5000
+ max: z4.number().int().positive().optional().describe("At most this many times (default 100)"),
5001
+ expressionType: z4.enum(["NonXML", "XML"]).optional().describe("NonXML: Camel Simple (default); XML: XPath")
5002
+ }).optional().describe("Looping Process Call: the loop"),
4529
5003
  scriptCollection: z4.string().optional().describe('Groovy/JavaScript step: Id of a script collection of the same package the script comes from (settings.script = its file name, e.g. "Utils.groovy"); the manifest requirement is written'),
4530
5004
  mapping: z4.string().optional().describe('XSLT Mapping / Message Mapping: the file under src/main/resources, e.g. "mapping/orders.xsl" (a bare name means mapping/); sets mappinguri, mappingpath and mappingname as the designer does'),
4531
5005
  expression: z4.string().optional().describe(`Filter: the XPath of the nodes to keep, e.g. "/Orders/Order[region='EU']" (stored as wrapContent; xpathType is only the value type: Nodelist, Node, Boolean, Integer, String)`),
@@ -4549,6 +5023,22 @@ var operation = z4.discriminatedUnion("op", [
4549
5023
  name: z4.string().optional().describe('Default "Exception Subprocess 1"'),
4550
5024
  end: z4.enum(["error", "message"]).default("error").describe("error: Error Start -> Error End, the message fails and the sender gets the error; message: Error Start -> End, the subprocess sends the reply (set it with steps)")
4551
5025
  }).describe("Add an exception subprocess (one per process), drawn below the main flow; insert steps into it with insert_step after its Error Start"),
5026
+ z4.object({ op: z4.literal("add_local_process"), name: z4.string().optional().describe('Default "Local Integration Process <n>"') }).describe('Add a local integration process (a pool below the others, Start -> End); insert steps after its start, call it with a Process Call step ("process")'),
5027
+ z4.object({
5028
+ op: z4.literal("set_start"),
5029
+ process: z4.string().optional().describe("Integration process (id or name); default: the main one"),
5030
+ kind: z4.enum(["timer", "message"]).describe("timer: a scheduled iFlow (no sender adapter; remove it first); message: started by a sender adapter again"),
5031
+ schedule: z4.union([z4.literal("once"), z4.object({
5032
+ every: z4.number().int().positive().optional().describe("Simple schedule: every n minutes (1-59) or hours (1-23)"),
5033
+ unit: z4.enum(["minutes", "hours"]).optional(),
5034
+ cron: z4.string().optional().describe('Advanced schedule instead: Quartz cron "second minute hour day-of-month month day-of-week [year]", e.g. "0 15 8 ? * MON-FRI" (weekdays 08:15)'),
5035
+ from: z4.string().optional().describe("Not before, local time in timeZone: 2026-11-05T08:00:00"),
5036
+ until: z4.string().optional().describe("Not after"),
5037
+ timeZone: z4.string().optional().describe("IANA time zone id, e.g. Europe/Warsaw; default UTC")
5038
+ })]).optional().describe('timer: "once" (run once after deploy, the default), a simple schedule {every, unit} or an advanced one {cron}; always externalized into parameters.prop'),
5039
+ parameter: z4.string().optional().describe("timer: name of the schedule parameter (default: the existing one, else timer_schedule)")
5040
+ }).describe("Make the start event a timer start (scheduled iFlow) or a message start"),
5041
+ z4.object({ op: z4.literal("remove_local_process"), process: z4.string().describe("Id or name; refused while a Process Call calls it") }),
4552
5042
  z4.object({ op: z4.literal("remove_exception_subprocess"), process: z4.string().optional() }).describe("Remove the exception subprocess with all steps in it (they must not have adapters)"),
4553
5043
  z4.object({
4554
5044
  op: z4.literal("add_router"),
@@ -4614,7 +5104,7 @@ var clip2 = (s, max) => s.length > max ? `${s.slice(0, max)}
4614
5104
  function registerEditCheckTools(server2) {
4615
5105
  server2.registerTool("iflow_edit", {
4616
5106
  title: "Edit an iFlow",
4617
- description: `Apply semantic edits to a local iFlow in one batch: insert a step from the catalog (with settings, headers, properties), remove a step (its neighbours are reconnected), add or remove an adapter (with its Sender/Receiver box), add or remove an exception subprocess (insert steps into it after its Error Start), add a router or a parallel/sequential multicast (with Join) and put steps on its routes (insert_step "route"), change or remove routes and gateways, lay out the process (branches in rows), change settings of a step, adapter or the iFlow, rename, and add/change/remove Content Modifier headers and properties. All operations succeed or nothing is written; edits that would introduce structural errors are refused. New elements are formatted like SAP's designer writes them and the diagram is laid out. Returns what changed (as iflow_diff) and the new step ids. Script and mapping files are not created: write them yourself (missing ones are listed under "filesToCreate").`,
5107
+ description: `Apply semantic edits to a local iFlow in one batch: insert a step from the catalog (with settings, headers, properties), remove a step (its neighbours are reconnected), add or remove an adapter (with its Sender/Receiver box), add or remove an exception subprocess (insert steps into it after its Error Start), add a router or a parallel/sequential multicast (with Join) and put steps on its routes (insert_step "route"), change or remove routes and gateways, add or remove local integration processes and call them (Process Call / Looping Process Call with "process", "loop"), make the start a timer or a message start (set_start), lay out the iFlow (one pool per process, branches in rows), change settings of a step, adapter or the iFlow, rename, and add/change/remove Content Modifier headers and properties. All operations succeed or nothing is written; edits that would introduce structural errors are refused. New elements are formatted like SAP's designer writes them and the diagram is laid out. Returns what changed (as iflow_diff) and the new step ids. Script and mapping files are not created: write them yourself (missing ones are listed under "filesToCreate").`,
4618
5108
  inputSchema: {
4619
5109
  iflow: iflowArg2,
4620
5110
  operations: z4.array(operation).min(1),
@@ -4627,7 +5117,10 @@ function registerEditCheckTools(server2) {
4627
5117
  const { located, doc } = localIflow(ctx, iflow);
4628
5118
  const before = parseIflw(serializeIflw(doc));
4629
5119
  const known = new Set(validate(before, located).map((i) => `${i.code} ${i.msg}`));
4630
- const log = applyEdits(doc, operations, catalogOf(ctx.config));
5120
+ const effects = { params: [] };
5121
+ const log = applyEdits(doc, operations, catalogOf(ctx.config), effects);
5122
+ if (effects.params.length && !located.dir)
5123
+ throw new Error("a timer schedule is externalized into parameters.prop: edit the iFlow folder, not a loose .iflw");
4631
5124
  const issues = validate(doc, located).filter((i) => !known.has(`${i.code} ${i.msg}`));
4632
5125
  const filesToCreate = issues.filter((i) => i.code === "MISSING_RESOURCE").map((i) => i.msg.replace(/^.* -> /, ""));
4633
5126
  const blocking = issues.filter((i) => i.severity === "error" && i.code !== "MISSING_RESOURCE" && !DEPLOY_CHECKS.has(i.code));
@@ -4638,6 +5131,7 @@ ${blocking.map((i) => ` ${i.code} ${i.msg}`).join("\n")}`);
4638
5131
  }
4639
5132
  if (!dryRun)
4640
5133
  writeFileSync6(located.iflwPath, serializeIflw(doc));
5134
+ const paramChanges = !dryRun && effects.params.length ? setParams(located.dir, effects.params.map((p) => ({ name: p.name, value: p.value, type: p.type, description: p.description }))) : [];
4641
5135
  const manifest = !dryRun && syncCollectionRequirements(located, doc);
4642
5136
  const collections = collectionIssues(located, doc).map((i) => `${i.code} ${i.msg}`);
4643
5137
  const changes = diffIflows(describe(before, { maxLen: 0 }), describe(doc, { maxLen: 0 }), { docA: before, docB: doc });
@@ -4649,6 +5143,7 @@ ${blocking.map((i) => ` ${i.code} ${i.msg}`).join("\n")}`);
4649
5143
  filesToCreate,
4650
5144
  ...beforeDeploy.length ? { beforeDeploy } : {},
4651
5145
  ...manifest ? { manifest: "META-INF/MANIFEST.MF: Require-Capability updated for the script collections used" } : {},
5146
+ ...paramChanges.length ? { parameters: paramChanges.map((c) => c.replace(/ = <row>.*$/, " = (schedule table)")) } : {},
4652
5147
  warnings: [...issues.filter((i) => i.severity === "warning" && !DEPLOY_CHECKS.has(i.code)).map((i) => `${i.code} ${i.msg}`), ...collections]
4653
5148
  });
4654
5149
  } catch (e) {
@@ -5542,9 +6037,17 @@ function registerParamTools(server2) {
5542
6037
  set: z8.array(z8.object({
5543
6038
  name: z8.string(),
5544
6039
  value: z8.string().optional(),
5545
- type: z8.enum(["xsd:string", "xsd:integer", "xsd:long", "xsd:boolean", "xsd:date", "xsd:time", "xsd:dateTime"]).optional().describe("Default xsd:string (kept when changing)"),
6040
+ type: z8.enum(["xsd:string", "xsd:integer", "xsd:long", "xsd:boolean", "xsd:date", "xsd:time", "xsd:dateTime", "custom:schedule"]).optional().describe("Default xsd:string (kept when changing)"),
5546
6041
  required: z8.boolean().optional(),
5547
- description: z8.string().optional()
6042
+ description: z8.string().optional(),
6043
+ schedule: z8.union([z8.literal("once"), z8.object({
6044
+ every: z8.number().int().positive().optional(),
6045
+ unit: z8.enum(["minutes", "hours"]).optional(),
6046
+ cron: z8.string().optional(),
6047
+ from: z8.string().optional(),
6048
+ until: z8.string().optional(),
6049
+ timeZone: z8.string().optional()
6050
+ })]).optional().describe('For a timer schedule parameter (type custom:schedule) instead of value: "once", {every, unit} or {cron}, with from, until, timeZone')
5548
6051
  })).default([]),
5549
6052
  remove: z8.array(z8.string()).default([]).describe("Parameter names to remove (declaration, value and the designer's references)")
5550
6053
  },
@@ -5555,7 +6058,8 @@ function registerParamTools(server2) {
5555
6058
  const { located, doc } = localIflow(ctx, iflow);
5556
6059
  if (!located.dir)
5557
6060
  throw new Error(`${iflow}: parameters belong to an iFlow folder (src/main/resources), not a loose .iflw`);
5558
- const log = set.length || remove.length ? setParams(located.dir, set, remove) : [];
6061
+ const input = set.map(({ schedule, ...p }) => schedule !== void 0 ? { ...p, type: "custom:schedule", value: scheduleTable(checkSchedule(schedule)) } : p);
6062
+ const log = (input.length || remove.length ? setParams(located.dir, input, remove) : []).map((l) => l.replace(/ = <row>.*$/, " = (schedule table)"));
5559
6063
  const used = uses(doc);
5560
6064
  const params = readParams(located.dir).map((p) => ({ ...p, usedBy: used.get(p.name) ?? [] }));
5561
6065
  const declared = new Set(params.map((p) => p.name));
@@ -5601,11 +6105,19 @@ function registerParamTools(server2) {
5601
6105
  description: `Set configured values of an iFlow's externalized parameters on a development tenant (allowed packages only), like "Configure" in the designer: no new version; the running iFlow uses them after the next deploy. Asks for confirmation when "config_set" is in "confirm". When the iFlow is pulled, its local parameters.prop is updated too (unless it has local changes, which a later cpi_push would upload over these values).`,
5602
6106
  inputSchema: {
5603
6107
  iflow: z8.string().describe("iFlow Id"),
5604
- values: z8.record(z8.string(), z8.string()).describe("Parameter name -> value"),
6108
+ values: z8.record(z8.string(), z8.string()).default({}).describe("Parameter name -> value"),
6109
+ schedules: z8.record(z8.string(), z8.object({
6110
+ every: z8.number().int().positive().optional(),
6111
+ unit: z8.enum(["minutes", "hours"]).optional(),
6112
+ cron: z8.string().optional(),
6113
+ from: z8.string().optional(),
6114
+ until: z8.string().optional(),
6115
+ timeZone: z8.string().optional()
6116
+ })).default({}).describe("Schedule parameter name (type custom:schedule) -> {every, unit} or {cron}, with from, until, timeZone"),
5605
6117
  tenant: tenantArg5
5606
6118
  },
5607
6119
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }
5608
- }, async ({ iflow, values, tenant: tenantName }) => {
6120
+ }, async ({ iflow, values: plain, schedules, tenant: tenantName }) => {
5609
6121
  const ctx = await context(server2).catch((e) => e);
5610
6122
  if (ctx instanceof Error)
5611
6123
  return toolError(ctx);
@@ -5614,6 +6126,15 @@ function registerParamTools(server2) {
5614
6126
  const info = await getIflow(api.client, iflow);
5615
6127
  await assertMayChange(api.client, api.tenant, info.packageId);
5616
6128
  const current = new Map((await getConfigurations(api.client, iflow)).map((v) => [v.key, v]));
6129
+ const values = { ...plain };
6130
+ for (const [k, sch] of Object.entries(schedules)) {
6131
+ if (current.get(k) && current.get(k).type !== "custom:schedule")
6132
+ throw new Error(`${k} is not a schedule parameter (${current.get(k).type})`);
6133
+ values[k] = scheduleConfigValue(checkSchedule(sch));
6134
+ }
6135
+ for (const k of Object.keys(plain))
6136
+ if (current.get(k)?.type === "custom:schedule")
6137
+ throw new Error(`${k} is a schedule: pass it in "schedules" as {every, unit} or {cron}`);
5617
6138
  const keys = Object.keys(values);
5618
6139
  if (!keys.length)
5619
6140
  throw new Error("nothing to set: pass values");
@@ -5623,7 +6144,8 @@ function registerParamTools(server2) {
5623
6144
  throw new Error(`${iflow} has no parameter "${k}"; parameters: ${[...current.keys()].filter((x) => !x.startsWith("SAP_")).join(", ") || "none"}`);
5624
6145
  if (k.startsWith("SAP_"))
5625
6146
  throw new Error(`${k} is set by SAP, not configured`);
5626
- checkValue(k, c.type, values[k]);
6147
+ if (c.type !== "custom:schedule")
6148
+ checkValue(k, c.type, values[k]);
5627
6149
  }
5628
6150
  await confirmAction(server2, ctx.config, "config_set", `Configure ${iflow} on tenant "${api.tenant.name}"?
5629
6151
  ` + keys.map((k) => `${k}: ${current.get(k).value} -> ${values[k]}`).join("\n"));
@@ -6077,7 +6599,7 @@ function textconvFile(path) {
6077
6599
  }
6078
6600
 
6079
6601
  // dist/version.js
6080
- var VERSION = "0.9.0-beta.0";
6602
+ var VERSION = "0.9.0-beta.1";
6081
6603
 
6082
6604
  // dist/index.js
6083
6605
  if (process.argv[2] === "textconv") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cpi-saper-dev-mcp",
3
- "version": "0.9.0-beta.0",
3
+ "version": "0.9.0-beta.1",
4
4
  "description": "MCP server that lets an AI agent develop SAP Cloud Integration (CPI) content on development tenants: edit iFlows locally, check, push, deploy and test",
5
5
  "keywords": [
6
6
  "mcp",
@@ -49,6 +49,6 @@
49
49
  "saxon-js": "2.7.0"
50
50
  },
51
51
  "devDependencies": {
52
- "@cpi-saper-dev-mcp/core": "0.9.0-beta.0"
52
+ "@cpi-saper-dev-mcp/core": "0.9.0-beta.1"
53
53
  }
54
54
  }