callman-mcp 0.3.0 → 0.5.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 (89) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +29 -7
  3. package/dist/client/api.d.ts +83 -1
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/api.js +105 -0
  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 +46 -11
  9. package/dist/client/errors.js.map +1 -1
  10. package/dist/client/types.d.ts +231 -0
  11. package/dist/client/types.d.ts.map +1 -1
  12. package/dist/domain/diagram/patch.d.ts +2 -2
  13. package/dist/domain/uiTest/compose.d.ts +55 -0
  14. package/dist/domain/uiTest/compose.d.ts.map +1 -0
  15. package/dist/domain/uiTest/compose.js +228 -0
  16. package/dist/domain/uiTest/compose.js.map +1 -0
  17. package/dist/domain/uiTest/summarize.d.ts +273 -0
  18. package/dist/domain/uiTest/summarize.d.ts.map +1 -0
  19. package/dist/domain/uiTest/summarize.js +145 -0
  20. package/dist/domain/uiTest/summarize.js.map +1 -0
  21. package/dist/lib/truncate.d.ts +1 -1
  22. package/dist/lib/truncate.d.ts.map +1 -1
  23. package/dist/lib/truncate.js.map +1 -1
  24. package/dist/resources/docs/overview.d.ts +1 -1
  25. package/dist/resources/docs/overview.d.ts.map +1 -1
  26. package/dist/resources/docs/overview.js +12 -3
  27. package/dist/resources/docs/overview.js.map +1 -1
  28. package/dist/resources/docs/uiTests.d.ts +2 -0
  29. package/dist/resources/docs/uiTests.d.ts.map +1 -0
  30. package/dist/resources/docs/uiTests.js +101 -0
  31. package/dist/resources/docs/uiTests.js.map +1 -0
  32. package/dist/resources/generated/aiPacks.d.ts +1 -1
  33. package/dist/resources/generated/aiPacks.js +2 -2
  34. package/dist/resources/index.d.ts.map +1 -1
  35. package/dist/resources/index.js +13 -2
  36. package/dist/resources/index.js.map +1 -1
  37. package/dist/server.d.ts +1 -1
  38. package/dist/server.d.ts.map +1 -1
  39. package/dist/server.js +2 -2
  40. package/dist/server.js.map +1 -1
  41. package/dist/tools/catalog.d.ts +1 -1
  42. package/dist/tools/catalog.d.ts.map +1 -1
  43. package/dist/tools/catalog.js +31 -0
  44. package/dist/tools/catalog.js.map +1 -1
  45. package/dist/tools/index.d.ts.map +1 -1
  46. package/dist/tools/index.js +6 -0
  47. package/dist/tools/index.js.map +1 -1
  48. package/dist/tools/lookups.d.ts +19 -1
  49. package/dist/tools/lookups.d.ts.map +1 -1
  50. package/dist/tools/lookups.js +77 -0
  51. package/dist/tools/lookups.js.map +1 -1
  52. package/dist/tools/shared.d.ts +5 -0
  53. package/dist/tools/shared.d.ts.map +1 -1
  54. package/dist/tools/shared.js +10 -0
  55. package/dist/tools/shared.js.map +1 -1
  56. package/dist/tools/uiTestAdmin.d.ts +4 -0
  57. package/dist/tools/uiTestAdmin.d.ts.map +1 -0
  58. package/dist/tools/uiTestAdmin.js +509 -0
  59. package/dist/tools/uiTestAdmin.js.map +1 -0
  60. package/dist/tools/uiTestRuns.d.ts +24 -0
  61. package/dist/tools/uiTestRuns.d.ts.map +1 -0
  62. package/dist/tools/uiTestRuns.js +367 -0
  63. package/dist/tools/uiTestRuns.js.map +1 -0
  64. package/dist/tools/uiTestShared.d.ts +236 -0
  65. package/dist/tools/uiTestShared.d.ts.map +1 -0
  66. package/dist/tools/uiTestShared.js +187 -0
  67. package/dist/tools/uiTestShared.js.map +1 -0
  68. package/dist/tools/uiTests.d.ts +4 -0
  69. package/dist/tools/uiTests.d.ts.map +1 -0
  70. package/dist/tools/uiTests.js +572 -0
  71. package/dist/tools/uiTests.js.map +1 -0
  72. package/dist-cjs/client/api.js +105 -0
  73. package/dist-cjs/client/errors.js +46 -11
  74. package/dist-cjs/domain/uiTest/compose.js +242 -0
  75. package/dist-cjs/domain/uiTest/summarize.js +155 -0
  76. package/dist-cjs/resources/docs/overview.js +12 -3
  77. package/dist-cjs/resources/docs/uiTests.js +103 -0
  78. package/dist-cjs/resources/generated/aiPacks.js +2 -2
  79. package/dist-cjs/resources/index.js +11 -0
  80. package/dist-cjs/server.js +2 -2
  81. package/dist-cjs/tools/catalog.js +31 -0
  82. package/dist-cjs/tools/index.js +6 -0
  83. package/dist-cjs/tools/lookups.js +83 -1
  84. package/dist-cjs/tools/shared.js +12 -1
  85. package/dist-cjs/tools/uiTestAdmin.js +512 -0
  86. package/dist-cjs/tools/uiTestRuns.js +372 -0
  87. package/dist-cjs/tools/uiTestShared.js +202 -0
  88. package/dist-cjs/tools/uiTests.js +575 -0
  89. package/package.json +3 -3
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.imageFromDataUrl = exports.scheduleView = exports.historyRow = exports.runListRow = exports.runReportView = exports.settingsView = exports.maskedDatasetKeys = exports.flowSummaryView = void 0;
4
+ const callman_core_1 = require("callman-core");
5
+ const mask_js_1 = require("../../lib/mask.js");
6
+ const truncate_js_1 = require("../../lib/truncate.js");
7
+ // Compact UI-test views: a flow row, a run timeline with the failing steps
8
+ // expanded, schedule rows. Screenshots never go into structuredContent —
9
+ // they travel as MCP image blocks (tools/uiTestRuns) or as `has…` flags.
10
+ const time = (value) => {
11
+ if (value === null || value === undefined || value === "")
12
+ return null;
13
+ const date = typeof value === "number" ? new Date(value) : new Date(String(value));
14
+ return Number.isNaN(date.getTime()) ? String(value) : date.toISOString();
15
+ };
16
+ const flowSummaryView = (flow, folderPath) => ({
17
+ id: flow.clientId,
18
+ name: flow.name,
19
+ platform: flow.platform,
20
+ ...(flow.description ? { description: (0, truncate_js_1.clip)(flow.description, 300) } : {}),
21
+ folder: folderPath ?? null,
22
+ tags: flow.tags ?? [],
23
+ testType: flow.testType ?? null,
24
+ severity: flow.severity ?? null,
25
+ version: flow.version ?? null,
26
+ ...(flow.reviewStatus ? { reviewStatus: flow.reviewStatus } : {}),
27
+ ...(flow.activeVersionNumber ? { activeVersionNumber: flow.activeVersionNumber } : {}),
28
+ ...(flow.groupName ? { group: flow.groupName } : {}),
29
+ updatedAt: time(flow.updatedAt),
30
+ });
31
+ exports.flowSummaryView = flowSummaryView;
32
+ const maskedDatasetKeys = (dataset) => Object.entries(dataset ?? {}).filter(([, value]) => (0, mask_js_1.isMasked)(value)).map(([key]) => key);
33
+ exports.maskedDatasetKeys = maskedDatasetKeys;
34
+ const settingsView = (flow) => ({
35
+ ...(flow.platform === "web" ? { browserKind: flow.browserKind ?? null, headless: flow.headless ?? null } : { dismissPopups: flow.dismissPopups ?? [] }),
36
+ runSettings: (0, callman_core_1.resolveRunSettings)(flow.runSettings ?? null),
37
+ });
38
+ exports.settingsView = settingsView;
39
+ const MAX_STEP_ROWS = 300;
40
+ const stepRow = (step, index) => ({
41
+ index: index + 1,
42
+ command: (0, truncate_js_1.clip)(step.command ?? "", 160),
43
+ status: step.status,
44
+ durationMs: step.durationMs ?? null,
45
+ ...(step.error ? { error: (0, truncate_js_1.clip)(step.error, 500) } : {}),
46
+ ...(step.source ? { source: step.source } : {}),
47
+ ...(step.healed ? { healed: (0, truncate_js_1.clip)(step.healed, 200) } : {}),
48
+ ...(step.retries ? { retries: step.retries } : {}),
49
+ });
50
+ const failedStepDetail = (step, index) => ({
51
+ index: index + 1,
52
+ command: (0, truncate_js_1.clip)(step.command ?? "", 400),
53
+ status: step.status,
54
+ error: step.error ? (0, truncate_js_1.clip)(step.error, 4000) : null,
55
+ ...(step.source ? { source: step.source } : {}),
56
+ ...(step.note ? { note: (0, truncate_js_1.clip)(step.note, 500) } : {}),
57
+ ...(step.data !== undefined && step.data !== null ? { data: (0, truncate_js_1.jsonPreview)(step.data, 3000) } : {}),
58
+ ...(step.extracted ? { extracted: (0, truncate_js_1.jsonPreview)(step.extracted, 1500) } : {}),
59
+ ...(step.dataTruncated ? { dataTruncated: true } : {}),
60
+ });
61
+ const isFailed = (status) => status === "failed" || status === "error";
62
+ const runReportView = (report, options = {}) => {
63
+ const steps = report.steps ?? [];
64
+ const counts = steps.reduce((acc, step) => ((acc[step.status] = (acc[step.status] ?? 0) + 1), acc), {});
65
+ const failed = steps.map((step, index) => ({ step, index })).filter(({ step }) => isFailed(step.status));
66
+ const withData = options.allStepData ? steps.map((step, index) => ({ step, index })).filter(({ step }) => !isFailed(step.status) && (step.data != null || step.extracted)) : [];
67
+ return {
68
+ reportId: report.id,
69
+ flow: { id: report.flowClientId, name: report.flowName },
70
+ status: report.status,
71
+ ...(report.executor ? { executor: report.executor } : {}),
72
+ ...(report.platform ? { platform: report.platform } : {}),
73
+ ...(report.relay?.deviceName || report.relay?.deviceId ? { device: report.relay.deviceName ?? report.relay.deviceId } : {}),
74
+ triggerType: report.triggerType ?? null,
75
+ ...(report.scheduleName ? { schedule: report.scheduleName } : {}),
76
+ startedAt: time(report.startedAt),
77
+ finishedAt: time(report.finishedAt),
78
+ durationMs: report.durationMs ?? null,
79
+ ...(report.error ? { error: (0, truncate_js_1.clip)(report.error, 4000) } : {}),
80
+ stepCounts: counts,
81
+ steps: steps.slice(0, MAX_STEP_ROWS).map(stepRow),
82
+ ...(steps.length > MAX_STEP_ROWS ? { stepsTruncated: steps.length } : {}),
83
+ ...(failed.length > 0 ? { failedSteps: failed.slice(0, 10).map(({ step, index }) => failedStepDetail(step, index)) } : {}),
84
+ ...(withData.length > 0 ? { stepData: withData.slice(0, 30).map(({ step, index }) => failedStepDetail(step, index)) } : {}),
85
+ ...(report.datasetIssues?.length ? { datasetIssues: report.datasetIssues } : {}),
86
+ ...(report.warnings?.length ? { warnings: report.warnings } : {}),
87
+ screenshots: {
88
+ failure: Boolean(report.failureScreenshot ?? report.hasFailureScreenshot),
89
+ final: Boolean(report.finalScreenshot ?? report.hasFinalScreenshot),
90
+ },
91
+ ...(options.includeNetwork
92
+ ? { network: (0, truncate_js_1.jsonPreview)(report.network ?? [], 12_000), ...(report.networkTruncated ? { networkTruncated: true } : {}) }
93
+ : (report.network?.length ?? report.networkCount)
94
+ ? { networkEvents: report.network?.length ?? report.networkCount }
95
+ : {}),
96
+ ...(report.fromApprovedSnapshot ? { fromApprovedSnapshot: true, flowVersionNumber: report.flowVersionNumber ?? null } : {}),
97
+ };
98
+ };
99
+ exports.runReportView = runReportView;
100
+ const runListRow = (report) => ({
101
+ reportId: report.id,
102
+ flow: { id: report.flowClientId, name: report.flowName },
103
+ status: report.status,
104
+ ...(report.executor ? { executor: report.executor } : {}),
105
+ triggerType: report.triggerType ?? null,
106
+ startedAt: time(report.startedAt),
107
+ durationMs: report.durationMs ?? null,
108
+ ...(report.error ? { error: (0, truncate_js_1.clip)(report.error, 300) } : {}),
109
+ ...(report.steps ? { failedStep: (() => {
110
+ const index = report.steps.findIndex((step) => isFailed(step.status));
111
+ return index >= 0 ? { index: index + 1, command: (0, truncate_js_1.clip)(report.steps[index].command, 160) } : null;
112
+ })() } : {}),
113
+ });
114
+ exports.runListRow = runListRow;
115
+ const historyRow = (row) => ({
116
+ flow: { id: row.flowClientId, name: row.flowName },
117
+ status: row.status,
118
+ source: row.triggeredBy === "schedule" ? "schedule" : row.triggeredBy ? row.triggeredBy : "desktop",
119
+ platform: row.platform,
120
+ ...(row.deviceId ? { device: row.deviceId } : {}),
121
+ startedAt: time(row.startedAt),
122
+ durationMs: row.durationMs,
123
+ ...(row.error ? { error: (0, truncate_js_1.clip)(row.error, 300) } : {}),
124
+ ...(row.reportId ? { reportId: row.reportId } : {}),
125
+ });
126
+ exports.historyRow = historyRow;
127
+ const scheduleView = (schedule) => ({
128
+ id: schedule.id,
129
+ name: schedule.name,
130
+ flow: { id: schedule.flowClientId, name: schedule.flowName },
131
+ enabled: schedule.enabled,
132
+ triggerType: schedule.triggerType,
133
+ timezone: schedule.timezone,
134
+ ...(schedule.dailyConfig ? { dailyConfig: schedule.dailyConfig } : {}),
135
+ ...(schedule.weeklyConfig ? { weeklyConfig: schedule.weeklyConfig } : {}),
136
+ ...(schedule.monthlyConfig ? { monthlyConfig: schedule.monthlyConfig } : {}),
137
+ ...(schedule.customCronExpression ? { customCronExpression: schedule.customCronExpression } : {}),
138
+ ...(schedule.description ? { description: schedule.description } : {}),
139
+ ...(schedule.executionPolicy ? { executionPolicy: schedule.executionPolicy } : {}),
140
+ ...(schedule.failurePolicy ? { failurePolicy: schedule.failurePolicy } : {}),
141
+ ...(schedule.notificationConfig ? { notificationConfig: schedule.notificationConfig } : {}),
142
+ lastRunAt: time(schedule.lastRunAt),
143
+ lastRunStatus: schedule.lastRunStatus ?? null,
144
+ nextRunAt: time(schedule.nextRunAt),
145
+ ...(schedule.isRunning ? { isRunning: true } : {}),
146
+ });
147
+ exports.scheduleView = scheduleView;
148
+ // `data:image/jpeg;base64,…` → an MCP image block.
149
+ const imageFromDataUrl = (dataUrl) => {
150
+ if (!dataUrl)
151
+ return null;
152
+ const match = /^data:(image\/[a-z0-9.+-]+);base64,(.+)$/i.exec(dataUrl.trim());
153
+ return match ? { mimeType: match[1].toLowerCase(), data: match[2] } : null;
154
+ };
155
+ exports.imageFromDataUrl = imageFromDataUrl;
@@ -4,8 +4,8 @@ exports.OVERVIEW_DOC = void 0;
4
4
  exports.OVERVIEW_DOC = `# Working with Callman through MCP
5
5
 
6
6
  Callman is an API client and test platform (collections of HTTP requests, environments,
7
- multi-step test scenarios, system-design diagrams). This server exposes it to you through
8
- tools; everything you do goes to the same server the Callman desktop app uses, as the user
7
+ multi-step API test scenarios, UI tests for web / Android / iOS, system-design diagrams). This
8
+ server exposes it to you through tools; everything you do goes to the same server the Callman desktop app uses, as the user
9
9
  whose token is configured, limited to that token's permissions and the user's workspace role.
10
10
 
11
11
  ## Start here
@@ -16,7 +16,7 @@ whose token is configured, limited to that token's permissions and the user's wo
16
16
  3. Arguments are strict: a misspelled or unknown argument is an error that lists the valid
17
17
  ones — read it and retry; nothing is silently ignored.
18
18
  4. Names work as references everywhere an id does (collection, folder, request, environment,
19
- scenario, diagram, workspace). Ambiguous names come back as a candidate list — pick one.
19
+ scenario, UI test, diagram, workspace). Ambiguous names come back as a candidate list — pick one.
20
20
  5. Preview big writes with \`dryRun: true\` (bulk create, imports, scenarios, diagrams).
21
21
  6. Deletions need \`confirm: true\` and an exact name or id. Ask the user unless they explicitly
22
22
  requested the deletion.
@@ -59,6 +59,15 @@ Multi-step flows as a graph of typed nodes (request, condition, assertion, set-v
59
59
  db, kafka, script, wait, end …). Read \`callman://docs/scenario-nodes\` before building one;
60
60
  partial node configs are merged over defaults. Runs happen on the server (\`run_scenario\`).
61
61
 
62
+ ## UI tests
63
+ Browser and mobile-app flows (YAML: header, \`---\`, steps) — the desktop app's UI Testing area.
64
+ Read \`callman://docs/ui-tests\` first and \`callman://docs/ui-test-steps\` for every step. Flows are
65
+ linted before they are saved (\`validate_ui_test\`); edit steps by number with
66
+ \`patch_ui_test_steps\`. Web flows run on the server runner when there is one; Android, iOS (and
67
+ web without a runner) run on the user's own Callman desktop app, which must be open with
68
+ "Let AI tools run UI tests on this computer" on — \`list_ui_test_devices\` shows what is
69
+ available. Runs return per-step results; screenshots come back as images on request.
70
+
62
71
  ## Diagrams
63
72
  System-design diagrams use a fixed component catalog — \`callman://diagram/components\`.
64
73
  Build with \`create_diagram\` (auto layout), edit with \`patch_diagram\`, share with
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UI_TESTS_DOC = void 0;
4
+ exports.UI_TESTS_DOC = `# UI tests in Callman
5
+
6
+ UI tests drive a real web browser or a mobile app step by step — the same flows QA builds in
7
+ the desktop app's UI Testing area. Every step (\`tapOn\`, \`inputText\`, \`assertVisible\`,
8
+ \`dbQuery\` …) with its arguments is in \`callman://docs/ui-test-steps\`.
9
+
10
+ ## A flow
11
+ A flow is YAML text: an optional **header**, a \`---\` line, then a **list of steps**.
12
+
13
+ \`\`\`yaml
14
+ url: https://shop.example.com # web: where the browser starts (mobile: appId: com.shop.app)
15
+ ---
16
+ - runFlow: "Login" # precondition: another flow, by EXACT name
17
+ - tapOn: "Catalog"
18
+ - inputText:
19
+ id: "search"
20
+ text: "{{product}}" # dataset placeholder
21
+ - assertVisible:
22
+ text: "{{product}}"
23
+ label: "Search shows the product"
24
+ - dbQuery: # hybrid data step (runs between UI steps)
25
+ connection: orders-db # a workspace connection, by name or id
26
+ query: "select status from orders where id = '{{orderId}}'"
27
+ expect:
28
+ - field: "rows[0].status"
29
+ op: equals
30
+ value: "PAID"
31
+ \`\`\`
32
+
33
+ - **Platforms**: \`web\` (Playwright), \`android\` and \`ios\` (the mobile engine in the desktop app).
34
+ A flow has one platform; steps the platform cannot run are lint warnings.
35
+ - **Header**: \`url\` (web) or \`appId\` (android/ios) is what matters; \`env\`, \`tags\`, \`appActivity\`,
36
+ \`deviceName\`, \`onFlowStart\`/\`onFlowComplete\` are optional.
37
+ - **Preconditions / sub-flows**: \`- runFlow: "Name"\` inlines another flow of the library by its
38
+ exact name (recursively). The leading runFlow lines are the flow's *preconditions*
39
+ (create_ui_test / update_ui_test \`preconditions: ["Login"]\`). Renaming a flow breaks callers —
40
+ update_ui_test lists them. Names are unique per workspace.
41
+ - **Labels**: any mapping step may carry \`label:\` — its name in reports (patch op \`setLabel\`).
42
+
43
+ ## Data
44
+ - **dataset**: key → value, used as \`{{key}}\` anywhere (header included). Values whose key looks
45
+ secret (password, token, apiKey, pin, otp …) come back masked (\`••••ab\`) unless the token holds
46
+ \`environments:reveal\` and you pass \`reveal: true\`. Editing a flow without reveal is fine:
47
+ stored secrets are kept as they are. Never type a masked value yourself — pass the real value
48
+ to change a secret, or leave the key out.
49
+ - **datasetSources**: \`{ key: { connection, query, column? } }\` — the value comes from a database
50
+ query at run time (a database connection of the workspace).
51
+ - **Extracted values**: \`extract\` / \`copyTextFrom\` / data steps save \`{{vars}}\` for later steps.
52
+ - **Hybrid steps**: \`dbQuery\`, \`apiCall\`, \`kafkaPublish\`, \`kafkaCheck\`, \`redisCheck\`, \`runScript\`
53
+ run outside the UI engine, in order. Connections are the workspace's (\`list_connections\`),
54
+ referenced by name or id.
55
+
56
+ ## Settings
57
+ Per flow: \`browserKind\` (chrome / firefox / webkit) and \`headless\` for web desktop runs;
58
+ \`dismissPopups\` (texts auto-dismissed) for mobile; \`runSettings\` { screenshotOnSuccess,
59
+ screenshotOnFailure, autoRetryTransient, captureNetwork } — null = the shared default.
60
+
61
+ ## Where runs happen
62
+ - **Server runner** (\`target: "server"\`): web flows only, when the installation runs one.
63
+ Schedules always use it.
64
+ - **Your desktop app** (\`target: "desktop"\`): the Callman desktop app on the user's computer runs
65
+ the job — Android devices/emulators, iOS simulators/devices, local browsers. It must be open,
66
+ signed in, with **Settings → AI tools → "Let AI tools run UI tests on this computer"** on.
67
+ Desktop runs queue one at a time. \`device\`, \`browserKind\`, \`headless\` and a one-off
68
+ \`dataset\` override apply here.
69
+ - \`target: "auto"\` (default): web → server runner when available, otherwise the desktop.
70
+ - \`list_ui_test_devices\` shows the server runner, the online desktop apps and their devices.
71
+ \`UI_TEST_DESKTOP_OFFLINE\` means no desktop app of the user is online.
72
+
73
+ ## Reports
74
+ \`run_ui_test\` / \`get_ui_test_run\` return per-step rows (command, status, duration, error, the
75
+ sub-flow it came from), failing steps expanded with the data they returned (DB rows, API
76
+ responses, extracted vars), dataset issues and warnings. \`includeScreenshots: true\` adds the
77
+ failure / final screenshots as images; \`includeNetwork: true\` the captured API calls.
78
+ \`list_ui_test_runs\` merges server reports with the history the desktop app recorded itself.
79
+
80
+ ## Schedules
81
+ Web flows only, on the server runner: daily / weekly / monthly at times (HH:mm) or a cron
82
+ expression, with retries and notifications. One call can schedule many flows. On-prem,
83
+ schedules run the flow's APPROVED version.
84
+
85
+ ## Review (on-prem)
86
+ Editing a flow makes it a draft. \`submit_ui_test\` sends it for review; an approver
87
+ \`review_ui_test\` (approve / reject with a note). Each approval is a version
88
+ (\`list_ui_test_versions\`, with a diff against the current content); \`restore_ui_test_version\`
89
+ re-activates an older one. Cloud has no review workflow — flows run as saved.
90
+
91
+ ## Recommended workflow
92
+ 1. \`list_ui_test_devices\` — where can it run (server runner? desktop app + devices?).
93
+ 2. \`list_ui_tests\` — reuse existing flows (Login …) as preconditions instead of copying steps.
94
+ 3. Read \`callman://docs/ui-test-steps\`, draft the steps, then \`validate_ui_test\` (content +
95
+ platform) — or \`create_ui_test\` with \`dryRun: true\`.
96
+ 4. \`create_ui_test\` (lint errors refuse the save; warnings are reported).
97
+ 5. \`run_ui_test\` → read the failing steps; \`get_ui_test_run includeScreenshots: true\` when a
98
+ picture helps.
99
+ 6. Fix with \`patch_ui_test_steps\` (insert / replace / remove / move / setLabel by step number
100
+ from \`get_ui_test\`) and run again.
101
+ 7. Organise with folders (\`create_ui_test_folder\`, \`move_ui_tests\`), schedule web flows
102
+ (\`create_ui_test_schedule\`), submit for approval on-prem.
103
+ `;
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AI_PACKS = exports.AI_PACKS_CORE_VERSION = void 0;
4
- // GENERATED by scripts/build-static-resources.mjs from callman-core@1.29.0 ai-packs — do not edit.
5
- exports.AI_PACKS_CORE_VERSION = "1.29.0";
4
+ // GENERATED by scripts/build-static-resources.mjs from callman-core@1.31.0 ai-packs — do not edit.
5
+ exports.AI_PACKS_CORE_VERSION = "1.31.0";
6
6
  exports.AI_PACKS = {
7
7
  "db-queries": "<!-- GENERATED FILE — do not edit by hand. Source: callman_core (npm run ai:packs). -->\n\n# Callman data access from scripts (`pm.db.query` / `pm.redis.command`)\n\n`pm.db.query({ connectionId, query })` — ALWAYS `await`. `query` is coerced to a string, `{{tokens}}` are resolved, then dispatched by the connection's type. `connectionId` may be omitted to use the request's configured DB connection. IMPORTANT: `pm.db.query` does NOT accept a `params`/bind array — inline values (quote strings) or use `{{env.X}}` tokens; SQL placeholders like `$1` or `?` are NOT bound.\n\n## MongoDB\n`query` MUST be a JSON STRING of a command object (use `JSON.stringify`), NOT a `db.coll.find(...)` shell string. The match key is `filter` (a top-level `query` key is rejected).\n\n```js\nconst body = pm.response.json();\nconst res = await pm.db.query({\n connectionId: \"…\",\n query: JSON.stringify({\n collection: \"users\",\n operation: \"findOne\", // find | findOne | aggregate | countDocuments | insertOne | insertMany | updateOne | updateMany | deleteOne | deleteMany | findOneAndUpdate | findOneAndReplace | findOneAndDelete\n filter: { email: body.data.user.email },\n }),\n});\npm.expect(res.rowCount).to.be.above(0); // existence check\n```\n\nPer-operation fields: `find`/`findOne` use `filter` (+ optional `projection`,`sort`,`limit`,`skip`); `aggregate` uses `pipeline` (array); `insertOne` uses `document`, `insertMany` uses `documents`; `updateOne`/`updateMany`/`findOneAndUpdate` use `filter`+`update` (Mongo update operators, e.g. `{ $set: { … } }`); `deleteOne`/`deleteMany` use `filter`; `findOneAndUpdate`/`findOneAndReplace` accept `returnDocument: \"before\"|\"after\"`. A 24-hex string in a filter is auto-coerced to ObjectId (or use `{ \"$oid\": \"…\" }`). Rows are in `res.rows`; write ops surface counts in `res.rowCount`.\n\n## PostgreSQL / MySQL / Oracle (SQL)\n`query` is plain SQL text. Because `pm.db.query` binds no params, inline the value and quote strings yourself (or use a `{{token}}`):\n\n```js\nconst email = pm.response.json().data.user.email;\nconst res = await pm.db.query({\n connectionId: \"…\",\n query: `SELECT id FROM users WHERE email = '${email}'`,\n});\npm.expect(res.rowCount).to.be.above(0);\n```\n\nNotes: a trailing `;` is stripped (except Oracle PL/SQL blocks, which keep it). **Oracle** returns column names UPPERCASE — read `res.rows[0].EMAIL`, not `.email`. `res.rowCount` is the row count for SELECT and the affected-row count for INSERT/UPDATE/DELETE.\n\n## SQL Server (mssql)\nSingle statement per query; a trailing `;` is stripped; `GO` batch separators are REJECTED (client-tool syntax, not T-SQL). Positional params bind as named inputs: `params[0]` → `@p1`, `params[1]` → `@p2`… (scenario db node only — `pm.db.query` has no params).\n\n```sql\nSELECT TOP 1 id, amount FROM dbo.orders WHERE customer_id = @p1 ORDER BY created_at DESC\n```\n\n## Redis (`pm.redis.command`)\n`command` is a single string. Supported commands: **GET, SET, DEL, EXISTS, TTL, HGET, HSET, HGETALL, HDEL, LPUSH, RPUSH, LRANGE, LLEN, SADD, SMEMBERS, SISMEMBER, INCR, DECR, INCRBY, EXPIRE, PERSIST, TYPE, ZADD, ZRANGE, ZSCORE, SCAN**. Quote a value that contains spaces.\n\nStructured replies land in `res.result` as real values: `HGETALL` → object, `LRANGE`/`SMEMBERS`/`ZRANGE` → array, `EXISTS`/`SISMEMBER`/`EXPIRE`/`PERSIST` → boolean; `res.resultText` is the stringified form. `SCAN pattern [count]` is Callman-managed: the cursor loop runs internally (capped at 1000 keys / 100 iterations) and returns `{ keys, count, truncated }` — no raw cursors.\n\n```js\nconst res = await pm.redis.command({ connectionId: \"…\", command: `GET session:${id}` });\npm.expect(res.success).to.be.true;\n// res.result / res.resultText hold the value; res.commandName, res.key are set.\n```\n",
8
8
  "diagram-components": "<!-- GENERATED FILE — do not edit by hand. Source: callman_core (npm run ai:packs). -->\n\n# Diagram components and edge notation\n\nA diagram node's `type` MUST be one of the keys below (`callman-core` `DIAGRAM_COMPONENT_CATALOG`). Sizes are the defaults the studio uses; `container` types hold children via `parentId`.\n\n## Edge notation\n\n| Semantic | Meaning | Style tuple |\n| --- | --- | --- |\n| `sync` | Request / response call | dashed=false, strokeWidth=1.5 |\n| `async` | Fire-and-forget, event or callback | dashed=true, dashPattern=\"6 4\", strokeWidth=1.5 |\n| `fallback` | Retry, fallback or optional path | dashed=true, dashPattern=\"1 3\", strokeWidth=1.25 |\n| `bus` | Message bus or stream | dashed=false, strokeWidth=3 |\n\n## Generic (`generic`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `line` | Line | 160×0 | connector, edge, link |\n| `arrow` | Arrow | 160×0 | connector, directed, link |\n| `line.dashed` | Dashed Line | 160×0 | dotted, connector |\n| `rect` | Rectangle | 140×64 | box, card |\n| `square` | Square | 90×90 | |\n| `ellipse` | Ellipse | 140×64 | oval |\n| `circle` | Circle | 90×90 | |\n| `diamond` | Diamond | 140×64 | rhombus |\n| `triangle` | Triangle | 140×64 | |\n| `hexagon` | Hexagon | 140×64 | |\n| `pentagon` | Pentagon | 140×64 | |\n| `octagon` | Octagon | 140×64 | |\n| `parallelogram` | Parallelogram | 140×64 | |\n| `trapezoid` | Trapezoid | 140×64 | |\n| `cylinder` | Cylinder | 140×64 | can, tube |\n| `cloud-shape` | Cloud | 140×64 | |\n| `callout` | Callout | 140×64 | speech, bubble |\n| `text` | Text | 120×36 | |\n| `sticky` | Sticky note | 140×100 | postit, comment |\n| `card` | Card | 140×64 | |\n| `frame` | Frame / Container (container) | 220×160 | group, container, boundary |\n| `pool.horizontal` | Pool (horizontal) (swimlane) | 360×220 | swimlane, lane, container, subsystem |\n| `pool.vertical` | Pool (vertical) (swimlane) | 220×360 | swimlane, lane, container, subsystem |\n| `image` | Image | 140×64 | picture |\n\n## Flowchart (`flowchart`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `flow.process` | Process | 140×64 | step, action |\n| `flow.alt-process` | Alternate Process | 140×64 | |\n| `flow.decision` | Decision | 140×100 | if, branch |\n| `flow.terminator` | Start / End | 130×56 | terminator, begin, stop |\n| `flow.data` | Data (I/O) | 140×64 | input, output, io |\n| `flow.document` | Document | 140×64 | |\n| `flow.multi-document` | Multi-Document | 140×64 | |\n| `flow.predefined` | Predefined Process | 140×64 | subroutine |\n| `flow.manual-input` | Manual Input | 140×64 | |\n| `flow.manual-op` | Manual Operation | 140×64 | |\n| `flow.preparation` | Preparation | 140×64 | |\n| `flow.stored-data` | Stored Data | 140×64 | |\n| `flow.database` | Database | 140×64 | db, store |\n| `flow.display` | Display | 140×64 | |\n| `flow.delay` | Delay | 140×64 | |\n| `flow.merge` | Merge | 140×64 | |\n| `flow.or` | Or | 140×64 | |\n| `flow.connector` | On-page Connector | 48×48 | |\n| `flow.off-page` | Off-page Connector | 140×64 | |\n| `flow.annotation` | Annotation | 140×64 | |\n\n## UML (`uml`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `uml.class` | Class | 140×64 | entity |\n| `uml.abstract` | Abstract Class | 140×64 | |\n| `uml.interface` | Interface | 140×64 | |\n| `uml.enum` | Enum | 140×64 | |\n| `uml.object` | Object | 140×64 | |\n| `uml.package` | Package | 140×64 | |\n| `uml.component` | Component | 140×64 | |\n| `uml.node` | Node / Deployment | 140×64 | |\n| `uml.actor` | Actor | 60×100 | user, role |\n| `uml.usecase` | Use Case | 140×64 | |\n| `uml.boundary` | Boundary | 140×64 | |\n| `uml.control` | Control | 140×64 | |\n| `uml.entity` | Entity | 140×64 | |\n| `uml.state` | State | 140×64 | |\n| `uml.initial` | Initial State | 36×36 | |\n| `uml.final` | Final State | 40×40 | |\n| `uml.decision` | Decision | 60×60 | |\n| `uml.note` | Note / Comment | 140×64 | comment |\n\n## ERD (`erd`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `erd.entity` | Entity | 140×64 | table |\n| `erd.weak-entity` | Weak Entity | 140×64 | |\n| `erd.attribute` | Attribute | 140×64 | |\n| `erd.key-attribute` | Key Attribute | 140×64 | pk, primary key |\n| `erd.multivalued` | Multivalued Attribute | 140×64 | |\n| `erd.derived` | Derived Attribute | 140×64 | |\n| `erd.relationship` | Relationship | 140×64 | |\n| `erd.identifying` | Identifying Relationship | 140×64 | |\n\n## Architecture (`networking`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `net.user` | User / Client | 60×100 | client, person |\n| `net.admin` | Admin | 60×100 | |\n| `net.browser` | Browser | 140×64 | |\n| `net.mobile` | Mobile Device | 90×130 | phone |\n| `net.desktop` | Desktop | 140×64 | |\n| `net.iot` | IoT Device | 140×64 | |\n| `net.server` | Server | 140×64 | |\n| `net.service` | Service | 140×64 | microservice |\n| `net.monolith` | Monolith | 140×64 | |\n| `net.api-gateway` | API Gateway | 140×64 | api, gateway |\n| `net.load-balancer` | Load Balancer | 140×64 | lb |\n| `net.reverse-proxy` | Reverse Proxy | 140×64 | proxy |\n| `net.firewall` | Firewall | 140×64 | waf |\n| `net.router` | Router | 140×64 | |\n| `net.switch` | Switch | 140×64 | |\n| `net.dns` | DNS | 140×64 | |\n| `net.cdn` | CDN | 140×64 | edge |\n| `net.cache` | Cache | 140×64 | |\n| `net.queue` | Queue | 140×64 | mq |\n| `net.websocket` | Web Socket | 140×64 | ws |\n| `net.rest` | REST endpoint | 140×64 | http, api |\n| `net.internet` | Internet / Cloud | 140×64 | wan |\n| `net.datacenter` | Datacenter | 140×64 | |\n| `net.container` | Container | 140×64 | |\n| `net.pod` | Pod | 140×64 | |\n| `net.vm` | Virtual Machine | 140×64 | vm |\n\n## Databases (`database`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `db.postgres` | PostgreSQL | 176×56 | postgres, pg |\n| `db.mysql` | MySQL | 176×56 | |\n| `db.mariadb` | MariaDB | 176×56 | |\n| `db.mongodb` | MongoDB | 176×56 | mongo |\n| `db.redis` | Redis | 176×56 | |\n| `db.sqlite` | SQLite | 176×56 | |\n| `db.mssql` | MS SQL Server | 176×56 | sqlserver, mssql |\n| `db.oracle` | Oracle | 176×56 | |\n| `db.cassandra` | Cassandra | 176×56 | |\n| `db.elasticsearch` | Elasticsearch | 176×56 | elastic, es |\n| `db.neo4j` | Neo4j | 176×56 | graph |\n| `db.couchdb` | CouchDB | 176×56 | couch |\n| `db.couchbase` | Couchbase | 176×56 | |\n| `db.clickhouse` | ClickHouse | 176×56 | |\n| `db.cockroachdb` | CockroachDB | 176×56 | cockroach |\n| `db.influxdb` | InfluxDB | 176×56 | influx, tsdb |\n| `db.snowflake` | Snowflake | 176×56 | |\n| `db.supabase` | Supabase | 176×56 | |\n\n## Messaging (`messaging`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `mq.kafka` | Apache Kafka | 176×56 | kafka |\n| `mq.rabbitmq` | RabbitMQ | 176×56 | rabbit, amqp |\n| `mq.nats` | NATS | 176×56 | |\n| `mq.pulsar` | Apache Pulsar | 176×56 | pulsar |\n| `mq.mqtt` | MQTT | 176×56 | |\n| `mq.pubsub` | Pub/Sub | 176×56 | pubsub |\n| `mq.activemq` | ActiveMQ | 176×56 | jms |\n\n## Languages (`language`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `lang.node` | Node.js | 176×56 | node, js runtime |\n| `lang.python` | Python | 176×56 | py |\n| `lang.java` | Java | 176×56 | jvm |\n| `lang.go` | Go | 176×56 | golang |\n| `lang.rust` | Rust | 176×56 | |\n| `lang.dotnet` | C# / .NET | 176×56 | csharp, c# |\n| `lang.typescript` | TypeScript | 176×56 | ts |\n| `lang.javascript` | JavaScript | 176×56 | js |\n| `lang.ruby` | Ruby | 176×56 | |\n| `lang.php` | PHP | 176×56 | |\n| `lang.kotlin` | Kotlin | 176×56 | |\n| `lang.swift` | Swift | 176×56 | |\n| `lang.scala` | Scala | 176×56 | |\n| `lang.elixir` | Elixir | 176×56 | |\n| `lang.cpp` | C++ | 176×56 | cpp |\n\n## Frameworks (`framework`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `fw.express` | Express | 176×56 | |\n| `fw.nestjs` | NestJS | 176×56 | nest |\n| `fw.spring` | Spring Boot | 176×56 | spring |\n| `fw.django` | Django | 176×56 | |\n| `fw.flask` | Flask | 176×56 | |\n| `fw.fastapi` | FastAPI | 176×56 | |\n| `fw.rails` | Rails | 176×56 | ruby on rails |\n| `fw.laravel` | Laravel | 176×56 | |\n| `fw.dotnet` | .NET | 176×56 | |\n| `fw.react` | React | 176×56 | |\n| `fw.vue` | Vue | 176×56 | vuejs |\n| `fw.angular` | Angular | 176×56 | |\n| `fw.next` | Next.js | 176×56 | nextjs |\n| `fw.nuxt` | Nuxt | 176×56 | nuxtjs |\n| `fw.svelte` | Svelte | 176×56 | sveltekit |\n| `fw.electron` | Electron | 176×56 | |\n| `fw.tailwind` | Tailwind | 176×56 | tailwindcss |\n| `fw.graphql` | GraphQL | 176×56 | |\n\n## DevOps / CI-CD (`devops`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `ops.docker` | Docker | 176×56 | |\n| `ops.k8s` | Kubernetes | 176×56 | k8s |\n| `ops.nginx` | Nginx | 176×56 | |\n| `ops.haproxy` | HAProxy | 176×56 | lb |\n| `ops.traefik` | Traefik | 176×56 | traefik |\n| `ops.terraform` | Terraform | 176×56 | iac |\n| `ops.ansible` | Ansible | 176×56 | |\n| `ops.jenkins` | Jenkins | 176×56 | ci |\n| `ops.gha` | GitHub Actions | 176×56 | ci, actions |\n| `ops.gitlab` | GitLab CI | 176×56 | ci |\n| `ops.argocd` | ArgoCD | 176×56 | gitops |\n| `ops.helm` | Helm | 176×56 | |\n| `ops.istio` | Istio | 176×56 | service mesh |\n| `ops.envoy` | Envoy | 176×56 | proxy |\n| `ops.consul` | Consul | 176×56 | |\n| `ops.vault` | Vault | 176×56 | secrets |\n| `ops.pm2` | PM2 | 176×56 | |\n| `ops.maven` | Maven | 176×56 | build |\n\n## Auth / Identity (`auth`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `auth.oidc` | OpenID Connect | 176×56 | oidc |\n| `auth.jwt` | JWT | 176×56 | token |\n| `auth.keycloak` | Keycloak | 176×56 | |\n| `auth.auth0` | Auth0 | 176×56 | |\n| `auth.okta` | Okta | 176×56 | |\n| `auth.firebase` | Firebase Auth | 176×56 | firebase |\n| `auth.oauth` | OAuth 2.0 | 176×56 | oauth |\n| `auth.saml` | SAML | 176×56 | |\n| `auth.ldap` | LDAP | 176×56 | |\n\n## Observability (`observability`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `obs.prometheus` | Prometheus | 176×56 | metrics |\n| `obs.grafana` | Grafana | 176×56 | dashboard |\n| `obs.datadog` | Datadog | 176×56 | |\n| `obs.sentry` | Sentry | 176×56 | errors |\n| `obs.elasticsearch` | Elasticsearch | 176×56 | elk |\n| `obs.logstash` | Logstash | 176×56 | elk |\n| `obs.kibana` | Kibana | 176×56 | elk |\n| `obs.jaeger` | Jaeger | 176×56 | tracing |\n| `obs.otel` | OpenTelemetry | 176×56 | otel, tracing |\n| `obs.newrelic` | New Relic | 176×56 | |\n\n## AWS (`aws`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `aws.ec2` | EC2 | 176×56 | aws |\n| `aws.lambda` | Lambda | 176×56 | aws |\n| `aws.s3` | S3 | 176×56 | aws |\n| `aws.ebs` | EBS | 176×56 | aws |\n| `aws.efs` | EFS | 176×56 | aws |\n| `aws.rds` | RDS | 176×56 | aws |\n| `aws.aurora` | Aurora | 176×56 | aws |\n| `aws.dynamodb` | DynamoDB | 176×56 | aws |\n| `aws.elasticache` | ElastiCache | 176×56 | aws |\n| `aws.redshift` | Redshift | 176×56 | aws |\n| `aws.vpc` | VPC | 176×56 | aws |\n| `aws.subnet` | Subnet | 176×56 | aws |\n| `aws.internet-gateway` | Internet Gateway | 176×56 | aws |\n| `aws.nat-gateway` | NAT Gateway | 176×56 | aws |\n| `aws.elb` | ELB | 176×56 | aws |\n| `aws.alb` | ALB | 176×56 | aws |\n| `aws.nlb` | NLB | 176×56 | aws |\n| `aws.api-gateway` | API Gateway | 176×56 | aws |\n| `aws.cloudfront` | CloudFront | 176×56 | aws |\n| `aws.route-53` | Route 53 | 176×56 | aws |\n| `aws.sqs` | SQS | 176×56 | aws |\n| `aws.sns` | SNS | 176×56 | aws |\n| `aws.eventbridge` | EventBridge | 176×56 | aws |\n| `aws.kinesis` | Kinesis | 176×56 | aws |\n| `aws.step-functions` | Step Functions | 176×56 | aws |\n| `aws.ecs` | ECS | 176×56 | aws |\n| `aws.eks` | EKS | 176×56 | aws |\n| `aws.fargate` | Fargate | 176×56 | aws |\n| `aws.ecr` | ECR | 176×56 | aws |\n| `aws.iam` | IAM | 176×56 | aws |\n| `aws.cognito` | Cognito | 176×56 | aws |\n| `aws.secrets-manager` | Secrets Manager | 176×56 | aws |\n| `aws.kms` | KMS | 176×56 | aws |\n| `aws.cloudwatch` | CloudWatch | 176×56 | aws |\n| `aws.cloudtrail` | CloudTrail | 176×56 | aws |\n| `aws.glue` | Glue | 176×56 | aws |\n| `aws.athena` | Athena | 176×56 | aws |\n| `aws.sagemaker` | SageMaker | 176×56 | aws |\n| `aws.appsync` | AppSync | 176×56 | aws |\n| `aws.amplify` | Amplify | 176×56 | aws |\n\n## Azure (`azure`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `azure.virtual-machine` | Virtual Machine | 176×56 | azure |\n| `azure.vm-scale-set` | VM Scale Set | 176×56 | azure |\n| `azure.app-service` | App Service | 176×56 | azure |\n| `azure.functions` | Functions | 176×56 | azure |\n| `azure.container-instances` | Container Instances | 176×56 | azure |\n| `azure.aks` | AKS | 176×56 | azure |\n| `azure.container-registry` | Container Registry | 176×56 | azure |\n| `azure.blob-storage` | Blob Storage | 176×56 | azure |\n| `azure.files` | Files | 176×56 | azure |\n| `azure.disk` | Disk | 176×56 | azure |\n| `azure.sql-database` | SQL Database | 176×56 | azure |\n| `azure.cosmos-db` | Cosmos DB | 176×56 | azure |\n| `azure.cache-for-redis` | Cache for Redis | 176×56 | azure |\n| `azure.synapse` | Synapse | 176×56 | azure |\n| `azure.virtual-network` | Virtual Network | 176×56 | azure |\n| `azure.load-balancer` | Load Balancer | 176×56 | azure |\n| `azure.application-gateway` | Application Gateway | 176×56 | azure |\n| `azure.front-door` | Front Door | 176×56 | azure |\n| `azure.service-bus` | Service Bus | 176×56 | azure |\n| `azure.event-hub` | Event Hub | 176×56 | azure |\n| `azure.event-grid` | Event Grid | 176×56 | azure |\n| `azure.key-vault` | Key Vault | 176×56 | azure |\n| `azure.entra-id` | Entra ID | 176×56 | azure |\n| `azure.monitor` | Monitor | 176×56 | azure |\n| `azure.application-insights` | Application Insights | 176×56 | azure |\n| `azure.logic-apps` | Logic Apps | 176×56 | azure |\n| `azure.api-management` | API Management | 176×56 | azure |\n\n## GCP (`gcp`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `gcp.compute-engine` | Compute Engine | 176×56 | gcp, google |\n| `gcp.app-engine` | App Engine | 176×56 | gcp, google |\n| `gcp.cloud-functions` | Cloud Functions | 176×56 | gcp, google |\n| `gcp.cloud-run` | Cloud Run | 176×56 | gcp, google |\n| `gcp.gke` | GKE | 176×56 | gcp, google |\n| `gcp.artifact-registry` | Artifact Registry | 176×56 | gcp, google |\n| `gcp.cloud-storage` | Cloud Storage | 176×56 | gcp, google |\n| `gcp.persistent-disk` | Persistent Disk | 176×56 | gcp, google |\n| `gcp.cloud-sql` | Cloud SQL | 176×56 | gcp, google |\n| `gcp.cloud-spanner` | Cloud Spanner | 176×56 | gcp, google |\n| `gcp.firestore` | Firestore | 176×56 | gcp, google |\n| `gcp.bigtable` | Bigtable | 176×56 | gcp, google |\n| `gcp.bigquery` | BigQuery | 176×56 | gcp, google |\n| `gcp.memorystore` | Memorystore | 176×56 | gcp, google |\n| `gcp.vpc` | VPC | 176×56 | gcp, google |\n| `gcp.cloud-load-balancing` | Cloud Load Balancing | 176×56 | gcp, google |\n| `gcp.cloud-cdn` | Cloud CDN | 176×56 | gcp, google |\n| `gcp.cloud-dns` | Cloud DNS | 176×56 | gcp, google |\n| `gcp.pub-sub` | Pub/Sub | 176×56 | gcp, google |\n| `gcp.dataflow` | Dataflow | 176×56 | gcp, google |\n| `gcp.cloud-tasks` | Cloud Tasks | 176×56 | gcp, google |\n| `gcp.iam` | IAM | 176×56 | gcp, google |\n| `gcp.secret-manager` | Secret Manager | 176×56 | gcp, google |\n| `gcp.cloud-kms` | Cloud KMS | 176×56 | gcp, google |\n| `gcp.cloud-monitoring` | Cloud Monitoring | 176×56 | gcp, google |\n| `gcp.vertex-ai` | Vertex AI | 176×56 | gcp, google |\n",
@@ -8,10 +8,12 @@ const requestShape_js_1 = require("../domain/requestShape.js");
8
8
  const summarize_js_2 = require("../domain/scenario/summarize.js");
9
9
  const truncate_js_1 = require("../lib/truncate.js");
10
10
  const lookups_js_1 = require("../tools/lookups.js");
11
+ const uiTestShared_js_1 = require("../tools/uiTestShared.js");
11
12
  const contracts_js_1 = require("./docs/contracts.js");
12
13
  const diagramSchema_js_1 = require("./docs/diagramSchema.js");
13
14
  const overview_js_1 = require("./docs/overview.js");
14
15
  const requestSchema_js_1 = require("./docs/requestSchema.js");
16
+ const uiTests_js_1 = require("./docs/uiTests.js");
15
17
  const aiPacks_js_1 = require("./generated/aiPacks.js");
16
18
  // Resources: reference material a model reads on demand (schemas, the
17
19
  // scripting API, the scenario node reference, the diagram component catalog
@@ -43,6 +45,8 @@ const registerResources = (server, ctx) => {
43
45
  { name: "docs-scripting-db-queries", uri: "callman://docs/scripting/db-queries", title: "Scripting: database queries", description: "How pm.db.* queries work per database provider.", body: () => (0, callman_core_1.renderDbQueryReference)() },
44
46
  { name: "docs-scripting-templating", uri: "callman://docs/scripting/templating", title: "Templating: {{variables}} and generators", description: "Variable resolution order and dynamic generators ({{$uuid}}, {{$randomInt}} …).", body: () => aiPacks_js_1.AI_PACKS.templating ?? "" },
45
47
  { name: "docs-scripting-sandbox", uri: "callman://docs/scripting/sandbox", title: "Scripting: sandbox and feature policy", description: "Globals available in the script sandbox and the feature policy for DB/Kafka capabilities.", body: () => `${aiPacks_js_1.AI_PACKS["sandbox-globals"] ?? ""}\n\n${aiPacks_js_1.AI_PACKS["feature-policy"] ?? ""}` },
48
+ { name: "docs-ui-tests", uri: "callman://docs/ui-tests", title: "UI tests", description: "UI-test concepts: platforms, header, preconditions and sub-flows, datasets and DB sources, hybrid data steps, settings, where runs happen (server runner vs your desktop app), reports and screenshots, schedules, review, masked secrets, and the recommended workflow.", body: () => uiTests_js_1.UI_TESTS_DOC },
49
+ { name: "docs-ui-test-steps", uri: "callman://docs/ui-test-steps", title: "UI-test step reference", description: "Every UI-test step (web / Android / iOS / hybrid data steps): aliases, platforms, forms, arguments and an example — generated from callman-core, the same catalog the linter uses.", body: () => (0, callman_core_1.renderUiTestCommandReference)() },
46
50
  { name: "docs-diagram-schema", uri: "callman://docs/diagram-schema", title: "Diagram schema", description: "Every node and edge field, styling (colors, borders, fonts), containers, edge semantics and colors, patch operations and merge rules, and the stored DiagramDocument.", body: () => diagramSchema_js_1.DIAGRAM_SCHEMA_DOC },
47
51
  ];
48
52
  for (const doc of staticDocs) {
@@ -105,6 +109,13 @@ const registerResources = (server, ctx) => {
105
109
  const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, variable(variables.scenario));
106
110
  return json(uri, { scenario: (0, summarize_js_2.scenarioFullView)(resolved.item) });
107
111
  });
112
+ server.registerResource("ui-test", new mcp_js_1.ResourceTemplate("callman://ui-test/{flow}", { list: undefined }), { title: "A UI test", description: "One UI test (id or name): header, settings, dataset (masked), preconditions, numbered steps, lint result and YAML.", mimeType: "application/json" }, async (uri, variables) => {
113
+ const resolved = await (0, lookups_js_1.resolveUiTest)(ctx, null, variable(variables.flow));
114
+ const fresh = await ctx.api.getUiTest(resolved.item.clientId);
115
+ const context = await (0, uiTestShared_js_1.lintContext)(ctx, fresh.workspaceId);
116
+ const lint = (0, uiTestShared_js_1.lintFlow)(fresh.content ?? "", fresh.platform, context, [...Object.keys(fresh.dataset ?? {}), ...Object.keys(fresh.datasetSources ?? {})]);
117
+ return json(uri, { uiTest: await (0, uiTestShared_js_1.flowDetailView)(ctx, fresh, lint, context, { workspaces: await ctx.listWorkspaces() }) });
118
+ });
108
119
  server.registerResource("diagram", new mcp_js_1.ResourceTemplate("callman://diagram/{diagram}", { list: undefined }), { title: "A diagram", description: "One diagram (id or name) as nodes/edges with edge semantics.", mimeType: "application/json" }, async (uri, variables) => {
109
120
  const ref = variable(variables.diagram);
110
121
  const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, ref);
@@ -7,8 +7,8 @@ const index_js_1 = require("./prompts/index.js");
7
7
  const index_js_2 = require("./resources/index.js");
8
8
  const index_js_3 = require("./tools/index.js");
9
9
  exports.SERVER_NAME = "callman";
10
- exports.SERVER_INSTRUCTIONS = `Callman is an API client and test platform: collections of HTTP requests, environments with variables, multi-step test scenarios, and system-design diagrams. You act as the user whose token is configured, within that token's permissions and the user's workspace role.
11
- Start with whoami. The token works in every workspace the user belongs to: lists and searches cover all of them unless you pass \`workspace\`, names are looked up in the default workspace first and then everywhere, and results say which workspace they come from; creating something needs a target (\`workspace\`, the collection it goes into, or the session default — use_workspace). Names work as references (collection, folder, request, environment, scenario, diagram, workspace); ambiguous names return candidates. List before creating — duplicate collection names are refused. Preview large writes with dryRun: true; deletions need confirm: true and the user's agreement. Environment values are masked unless the token holds environments:reveal and you pass reveal: true; never write a masked value back. Arguments are strict — an unknown key is an error listing the valid ones. Read callman://docs/overview for the conventions, callman://docs/request-schema before creating or editing requests, callman://docs/contracts before contract checks, callman://docs/scenario-nodes before scenarios, and callman://docs/diagram-schema + callman://diagram/components before diagrams.`;
10
+ exports.SERVER_INSTRUCTIONS = `Callman is an API client and test platform: collections of HTTP requests, environments with variables, multi-step API test scenarios, UI tests (web / Android / iOS flows), and system-design diagrams. You act as the user whose token is configured, within that token's permissions and the user's workspace role.
11
+ Start with whoami. The token works in every workspace the user belongs to: lists and searches cover all of them unless you pass \`workspace\`, names are looked up in the default workspace first and then everywhere, and results say which workspace they come from; creating something needs a target (\`workspace\`, the collection it goes into, or the session default — use_workspace). Names work as references (collection, folder, request, environment, scenario, UI test, diagram, workspace); ambiguous names return candidates. List before creating — duplicate collection names are refused. Preview large writes with dryRun: true; deletions need confirm: true and the user's agreement. Environment values are masked unless the token holds environments:reveal and you pass reveal: true; never write a masked value back. Arguments are strict — an unknown key is an error listing the valid ones. Read callman://docs/overview for the conventions, callman://docs/request-schema before creating or editing requests, callman://docs/contracts before contract checks, callman://docs/scenario-nodes before scenarios, callman://docs/ui-tests (+ callman://docs/ui-test-steps) before UI tests — they run on the server runner (web) or the user's own desktop app (list_ui_test_devices), and callman://docs/diagram-schema + callman://diagram/components before diagrams.`;
12
12
  const createCallmanMcpServer = (options) => {
13
13
  const version = options.version ?? "0.0.0";
14
14
  const context = (0, context_js_1.createToolContext)({
@@ -65,6 +65,37 @@ exports.TOOL_CATALOG = [
65
65
  { name: "submit_scenario", group: "scenarios", scopes: ["scenarios:write"] },
66
66
  { name: "delete_scenario", group: "scenarios", scopes: ["scenarios:delete"], destructive: true },
67
67
  { name: "run_scenario", group: "scenarios", scopes: ["scenarios:run"] },
68
+ // UI tests (web / Android / iOS flows): ui_tests:* scopes. Runs start on the
69
+ // server runner (web) or the user's desktop app (desktop relay).
70
+ { name: "list_ui_tests", group: "uiTests", scopes: ["ui_tests:read"] },
71
+ { name: "get_ui_test", group: "uiTests", scopes: ["ui_tests:read"] },
72
+ { name: "validate_ui_test", group: "uiTests", scopes: ["ui_tests:read"] },
73
+ { name: "create_ui_test", group: "uiTests", scopes: ["ui_tests:write"] },
74
+ { name: "update_ui_test", group: "uiTests", scopes: ["ui_tests:write"] },
75
+ { name: "patch_ui_test_steps", group: "uiTests", scopes: ["ui_tests:write"] },
76
+ { name: "delete_ui_test", group: "uiTests", scopes: ["ui_tests:delete"], destructive: true },
77
+ { name: "list_ui_test_folders", group: "uiTests", scopes: ["ui_tests:read"] },
78
+ { name: "create_ui_test_folder", group: "uiTests", scopes: ["ui_tests:write"] },
79
+ { name: "update_ui_test_folder", group: "uiTests", scopes: ["ui_tests:write"] },
80
+ { name: "delete_ui_test_folder", group: "uiTests", scopes: ["ui_tests:delete"], destructive: true },
81
+ { name: "move_ui_tests", group: "uiTests", scopes: ["ui_tests:write"] },
82
+ { name: "list_ui_test_devices", group: "uiTests", scopes: ["ui_tests:read"] },
83
+ { name: "run_ui_test", group: "uiTests", scopes: ["ui_tests:run"] },
84
+ { name: "run_ui_tests", group: "uiTests", scopes: ["ui_tests:run"] },
85
+ { name: "get_ui_test_run", group: "uiTests", scopes: ["ui_tests:read"] },
86
+ { name: "list_ui_test_runs", group: "uiTests", scopes: ["ui_tests:read"] },
87
+ { name: "cancel_ui_test_run", group: "uiTests", scopes: ["ui_tests:run"] },
88
+ { name: "list_ui_test_schedules", group: "uiTests", scopes: ["ui_tests:read"] },
89
+ { name: "create_ui_test_schedule", group: "uiTests", scopes: ["ui_tests:write"] },
90
+ { name: "update_ui_test_schedule", group: "uiTests", scopes: ["ui_tests:write"] },
91
+ { name: "delete_ui_test_schedule", group: "uiTests", scopes: ["ui_tests:delete"], destructive: true },
92
+ { name: "run_ui_test_schedule_now", group: "uiTests", scopes: ["ui_tests:run"] },
93
+ { name: "submit_ui_test", group: "uiTests", scopes: ["ui_tests:write"] },
94
+ { name: "review_ui_test", group: "uiTests", scopes: ["ui_tests:write"] },
95
+ { name: "list_ui_test_versions", group: "uiTests", scopes: ["ui_tests:read"] },
96
+ { name: "restore_ui_test_version", group: "uiTests", scopes: ["ui_tests:write"] },
97
+ { name: "export_ui_tests", group: "uiTests", scopes: ["ui_tests:read"] },
98
+ { name: "import_ui_tests", group: "uiTests", scopes: ["ui_tests:write"] },
68
99
  { name: "list_diagrams", group: "diagrams", scopes: ["diagrams:read"] },
69
100
  { name: "get_diagram", group: "diagrams", scopes: ["diagrams:read"] },
70
101
  { name: "create_diagram", group: "diagrams", scopes: ["diagrams:write"] },
@@ -11,6 +11,9 @@ const meta_js_1 = require("./meta.js");
11
11
  const requests_js_1 = require("./requests.js");
12
12
  const run_js_1 = require("./run.js");
13
13
  const scenarios_js_1 = require("./scenarios.js");
14
+ const uiTestAdmin_js_1 = require("./uiTestAdmin.js");
15
+ const uiTestRuns_js_1 = require("./uiTestRuns.js");
16
+ const uiTests_js_1 = require("./uiTests.js");
14
17
  const workspaces_js_1 = require("./workspaces.js");
15
18
  var catalog_js_1 = require("./catalog.js");
16
19
  Object.defineProperty(exports, "TOOL_CATALOG", { enumerable: true, get: function () { return catalog_js_1.TOOL_CATALOG; } });
@@ -43,6 +46,9 @@ const registerAllTools = (host, ctx) => {
43
46
  (0, import_js_1.registerImportTools)(server, ctx);
44
47
  (0, environments_js_1.registerEnvironmentTools)(server, ctx);
45
48
  (0, scenarios_js_1.registerScenarioTools)(server, ctx);
49
+ (0, uiTests_js_1.registerUiTestTools)(server, ctx);
50
+ (0, uiTestRuns_js_1.registerUiTestRunTools)(server, ctx);
51
+ (0, uiTestAdmin_js_1.registerUiTestAdminTools)(server, ctx);
46
52
  (0, diagrams_js_1.registerDiagramTools)(server, ctx);
47
53
  if (ctx.config.allowLocalRun)
48
54
  (0, run_js_1.registerRunTools)(server, ctx);
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sameName = exports.connectionResolverFor = exports.loadConnections = exports.resolveDiagram = exports.loadDiagrams = exports.resolveScenarioFolder = exports.resolveScenario = exports.loadScenarioCatalog = exports.resolveEnvironment = exports.loadEnvironments = exports.resolveRequest = exports.searchRequests = exports.requestIdsInFolder = exports.resolveFolder = exports.loadTree = exports.resolveCollection = exports.findEnvironment = exports.findRequest = exports.findCollection = exports.findInWorkspaces = exports.workspaceTag = exports.workspacesForListing = exports.isAllWorkspaces = exports.loadCollections = void 0;
3
+ exports.sameName = exports.connectionResolverFor = exports.loadConnections = exports.resolveUiTest = exports.resolveUiTestFolder = exports.uiTestFolderSubtree = exports.loadUiTestFolders = exports.loadUiTests = exports.resolveDiagram = exports.loadDiagrams = exports.resolveScenarioFolder = exports.resolveScenario = exports.loadScenarioCatalog = exports.resolveEnvironment = exports.loadEnvironments = exports.resolveRequest = exports.searchRequests = exports.requestIdsInFolder = exports.resolveFolder = exports.loadTree = exports.resolveCollection = exports.findEnvironment = exports.findRequest = exports.findCollection = exports.findInWorkspaces = exports.workspaceTag = exports.workspacesForListing = exports.isAllWorkspaces = exports.loadCollections = void 0;
4
4
  const errors_js_1 = require("../client/errors.js");
5
5
  const requestIntegrations_js_1 = require("../domain/requestIntegrations.js");
6
6
  const resolve_js_1 = require("../domain/resolve.js");
@@ -251,6 +251,88 @@ const resolveDiagram = async (ctx, ref, options = {}) => {
251
251
  return { ...resolved, item: await ctx.api.getDiagram(resolved.item.id) };
252
252
  };
253
253
  exports.resolveDiagram = resolveDiagram;
254
+ const loadUiTests = (ctx) => ctx.cache.getOrSet("uitests:list", () => ctx.api.listUiTests(true));
255
+ exports.loadUiTests = loadUiTests;
256
+ const loadUiTestFolders = (ctx) => ctx.cache.getOrSet("uitests:folders", async () => {
257
+ const raw = await ctx.api.listUiTestFolders();
258
+ const byId = new Map(raw.map((folder) => [folder.clientId, folder]));
259
+ const pathOf = (folder) => {
260
+ const names = [folder.name];
261
+ const seen = new Set([folder.clientId]);
262
+ let parent = folder.parentId ? byId.get(folder.parentId) : undefined;
263
+ while (parent && !seen.has(parent.clientId)) {
264
+ names.unshift(parent.name);
265
+ seen.add(parent.clientId);
266
+ parent = parent.parentId ? byId.get(parent.parentId) : undefined;
267
+ }
268
+ return { path: names.join("/"), depth: names.length - 1 };
269
+ };
270
+ return raw.map((folder) => ({
271
+ id: folder.clientId,
272
+ name: folder.name,
273
+ parentId: folder.parentId ?? null,
274
+ ...pathOf(folder),
275
+ workspaceId: folder.workspaceId,
276
+ groupId: folder.groupId ?? null,
277
+ sortOrder: folder.sortOrder ?? 0,
278
+ }));
279
+ });
280
+ exports.loadUiTestFolders = loadUiTestFolders;
281
+ // Folder ids of `folderId` and every folder below it.
282
+ const uiTestFolderSubtree = (folders, folderId) => {
283
+ const out = new Set([folderId]);
284
+ let grew = true;
285
+ while (grew) {
286
+ grew = false;
287
+ for (const folder of folders) {
288
+ if (folder.parentId && out.has(folder.parentId) && !out.has(folder.id)) {
289
+ out.add(folder.id);
290
+ grew = true;
291
+ }
292
+ }
293
+ }
294
+ return out;
295
+ };
296
+ exports.uiTestFolderSubtree = uiTestFolderSubtree;
297
+ const workspaceFilter = async (ctx, workspace) => workspace && workspace.trim() && !(0, exports.isAllWorkspaces)(workspace) ? ctx.resolveWorkspace(workspace) : null;
298
+ const resolveUiTestFolder = async (ctx, workspace, ref, options = {}) => {
299
+ const ws = await workspaceFilter(ctx, workspace);
300
+ const folders = (await (0, exports.loadUiTestFolders)(ctx)).filter((folder) => !ws || folder.workspaceId === ws.id);
301
+ return (0, resolve_js_1.resolveRef)("UI-test folder", ref, folders, { describe: (folder) => ({ path: folder.path }), aliases: (folder) => [folder.path], exact: options.exact });
302
+ };
303
+ exports.resolveUiTestFolder = resolveUiTestFolder;
304
+ // A flow by clientId or name (unique across the workspaces in scope). The
305
+ // cached list may miss a flow created a moment ago — an unknown ref is tried
306
+ // as a clientId before giving up.
307
+ const resolveUiTest = async (ctx, workspace, ref, options = {}) => {
308
+ const ws = await workspaceFilter(ctx, workspace);
309
+ const [flows, workspaces, folders] = await Promise.all([(0, exports.loadUiTests)(ctx), ctx.listWorkspaces(), (0, exports.loadUiTestFolders)(ctx).catch(() => [])]);
310
+ const workspaceName = new Map(workspaces.map((entry) => [entry.id, entry.name]));
311
+ const folderPath = new Map(folders.map((folder) => [folder.id, folder.path]));
312
+ const items = flows.filter((flow) => !ws || flow.workspaceId === ws.id).map((flow) => ({ ...flow, id: flow.clientId }));
313
+ try {
314
+ return (0, resolve_js_1.resolveRef)("UI test", ref, items, {
315
+ describe: (flow) => ({ platform: flow.platform, folder: flow.folderId ? (folderPath.get(flow.folderId) ?? null) : null, ...(workspaces.length > 1 ? { workspace: workspaceName.get(flow.workspaceId) ?? flow.workspaceId } : {}) }),
316
+ exact: options.exact,
317
+ });
318
+ }
319
+ catch (error) {
320
+ if (!(error instanceof errors_js_1.ToolInputError) || error.code !== "REF_NOT_FOUND" || /\s/.test(ref.trim()))
321
+ throw error;
322
+ try {
323
+ const fresh = await ctx.api.getUiTest(ref.trim());
324
+ if (ws && fresh.workspaceId !== ws.id)
325
+ throw error;
326
+ return { item: { ...fresh, id: fresh.clientId }, matched: "id" };
327
+ }
328
+ catch (lookup) {
329
+ if (lookup instanceof errors_js_1.CallmanApiError && (lookup.status === 404 || lookup.status === 400))
330
+ throw error;
331
+ throw lookup;
332
+ }
333
+ }
334
+ };
335
+ exports.resolveUiTest = resolveUiTest;
254
336
  const loadConnections = (ctx, ws) => ctx.cache.getOrSet(`connections:${ws}`, () => ctx.api.listConnections(ws));
255
337
  exports.loadConnections = loadConnections;
256
338
  // Connection resolver for request integration steps — loaded only when a
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.requireConfirm = exports.dryRunArg = exports.confirmArg = exports.cursorArgs = exports.refArg = exports.workspaceArg = exports.RUN = exports.DESTRUCTIVE = exports.IDEMPOTENT_WRITE = exports.WRITE = exports.READ = exports.guarded = exports.failureOf = exports.toolFailure = exports.ok = void 0;
3
+ exports.requireConfirm = exports.dryRunArg = exports.confirmArg = exports.cursorArgs = exports.refArg = exports.workspaceArg = exports.RUN = exports.DESTRUCTIVE = exports.IDEMPOTENT_WRITE = exports.WRITE = exports.READ = exports.guarded = exports.failureOf = exports.toolFailure = exports.okWithImages = exports.ok = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const errors_js_1 = require("../client/errors.js");
6
6
  const truncate_js_1 = require("../lib/truncate.js");
@@ -17,6 +17,17 @@ const ok = (structured, text) => ({
17
17
  structuredContent: structured,
18
18
  });
19
19
  exports.ok = ok;
20
+ // `ok` plus MCP image blocks (screenshots): the structured result stays JSON,
21
+ // each image follows as { type: "image", data: <base64>, mimeType } preceded
22
+ // by a one-line caption so the model knows which is which.
23
+ const okWithImages = (structured, images) => {
24
+ const result = (0, exports.ok)(structured);
25
+ for (const image of images) {
26
+ result.content.push({ type: "text", text: image.caption }, { type: "image", data: image.data, mimeType: image.mimeType });
27
+ }
28
+ return result;
29
+ };
30
+ exports.okWithImages = okWithImages;
20
31
  const toolFailure = (error) => {
21
32
  let payload;
22
33
  if (error instanceof errors_js_1.CallmanApiError) {