callman-mcp 0.4.0 → 0.6.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 (118) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/README.md +43 -11
  3. package/dist/client/api.d.ts +104 -1
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/api.js +137 -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 +257 -0
  11. package/dist/client/types.d.ts.map +1 -1
  12. package/dist/domain/diagram/patch.d.ts +32 -32
  13. package/dist/domain/scenario/build.d.ts.map +1 -1
  14. package/dist/domain/scenario/build.js +68 -17
  15. package/dist/domain/scenario/build.js.map +1 -1
  16. package/dist/domain/scenario/summarize.d.ts +1 -0
  17. package/dist/domain/scenario/summarize.d.ts.map +1 -1
  18. package/dist/domain/scenario/summarize.js +1 -0
  19. package/dist/domain/scenario/summarize.js.map +1 -1
  20. package/dist/domain/uiTest/compose.d.ts +55 -0
  21. package/dist/domain/uiTest/compose.d.ts.map +1 -0
  22. package/dist/domain/uiTest/compose.js +228 -0
  23. package/dist/domain/uiTest/compose.js.map +1 -0
  24. package/dist/domain/uiTest/summarize.d.ts +273 -0
  25. package/dist/domain/uiTest/summarize.d.ts.map +1 -0
  26. package/dist/domain/uiTest/summarize.js +145 -0
  27. package/dist/domain/uiTest/summarize.js.map +1 -0
  28. package/dist/lib/truncate.d.ts +1 -1
  29. package/dist/lib/truncate.d.ts.map +1 -1
  30. package/dist/lib/truncate.js.map +1 -1
  31. package/dist/resources/docs/diagramSchema.d.ts +2 -1
  32. package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
  33. package/dist/resources/docs/diagramSchema.js +30 -3
  34. package/dist/resources/docs/diagramSchema.js.map +1 -1
  35. package/dist/resources/docs/overview.d.ts +1 -1
  36. package/dist/resources/docs/overview.d.ts.map +1 -1
  37. package/dist/resources/docs/overview.js +40 -7
  38. package/dist/resources/docs/overview.js.map +1 -1
  39. package/dist/resources/docs/uiTests.d.ts +2 -0
  40. package/dist/resources/docs/uiTests.d.ts.map +1 -0
  41. package/dist/resources/docs/uiTests.js +101 -0
  42. package/dist/resources/docs/uiTests.js.map +1 -0
  43. package/dist/resources/generated/aiPacks.d.ts +1 -1
  44. package/dist/resources/generated/aiPacks.js +4 -4
  45. package/dist/resources/generated/aiPacks.js.map +1 -1
  46. package/dist/resources/index.d.ts.map +1 -1
  47. package/dist/resources/index.js +15 -4
  48. package/dist/resources/index.js.map +1 -1
  49. package/dist/server.d.ts +1 -1
  50. package/dist/server.d.ts.map +1 -1
  51. package/dist/server.js +2 -2
  52. package/dist/server.js.map +1 -1
  53. package/dist/tools/catalog.d.ts +1 -1
  54. package/dist/tools/catalog.d.ts.map +1 -1
  55. package/dist/tools/catalog.js +37 -0
  56. package/dist/tools/catalog.js.map +1 -1
  57. package/dist/tools/diagrams.d.ts.map +1 -1
  58. package/dist/tools/diagrams.js +4 -1
  59. package/dist/tools/diagrams.js.map +1 -1
  60. package/dist/tools/index.d.ts.map +1 -1
  61. package/dist/tools/index.js +8 -0
  62. package/dist/tools/index.js.map +1 -1
  63. package/dist/tools/lookups.d.ts +19 -1
  64. package/dist/tools/lookups.d.ts.map +1 -1
  65. package/dist/tools/lookups.js +77 -0
  66. package/dist/tools/lookups.js.map +1 -1
  67. package/dist/tools/scenarioAdmin.d.ts +4 -0
  68. package/dist/tools/scenarioAdmin.d.ts.map +1 -0
  69. package/dist/tools/scenarioAdmin.js +142 -0
  70. package/dist/tools/scenarioAdmin.js.map +1 -0
  71. package/dist/tools/scenarios.d.ts +4 -0
  72. package/dist/tools/scenarios.d.ts.map +1 -1
  73. package/dist/tools/scenarios.js +111 -16
  74. package/dist/tools/scenarios.js.map +1 -1
  75. package/dist/tools/shared.d.ts +5 -0
  76. package/dist/tools/shared.d.ts.map +1 -1
  77. package/dist/tools/shared.js +10 -0
  78. package/dist/tools/shared.js.map +1 -1
  79. package/dist/tools/uiTestAdmin.d.ts +4 -0
  80. package/dist/tools/uiTestAdmin.d.ts.map +1 -0
  81. package/dist/tools/uiTestAdmin.js +509 -0
  82. package/dist/tools/uiTestAdmin.js.map +1 -0
  83. package/dist/tools/uiTestRuns.d.ts +24 -0
  84. package/dist/tools/uiTestRuns.d.ts.map +1 -0
  85. package/dist/tools/uiTestRuns.js +367 -0
  86. package/dist/tools/uiTestRuns.js.map +1 -0
  87. package/dist/tools/uiTestShared.d.ts +236 -0
  88. package/dist/tools/uiTestShared.d.ts.map +1 -0
  89. package/dist/tools/uiTestShared.js +187 -0
  90. package/dist/tools/uiTestShared.js.map +1 -0
  91. package/dist/tools/uiTests.d.ts +4 -0
  92. package/dist/tools/uiTests.d.ts.map +1 -0
  93. package/dist/tools/uiTests.js +572 -0
  94. package/dist/tools/uiTests.js.map +1 -0
  95. package/dist-cjs/client/api.js +137 -0
  96. package/dist-cjs/client/errors.js +46 -11
  97. package/dist-cjs/domain/scenario/build.js +68 -17
  98. package/dist-cjs/domain/scenario/summarize.js +1 -0
  99. package/dist-cjs/domain/uiTest/compose.js +242 -0
  100. package/dist-cjs/domain/uiTest/summarize.js +155 -0
  101. package/dist-cjs/resources/docs/diagramSchema.js +32 -4
  102. package/dist-cjs/resources/docs/overview.js +40 -7
  103. package/dist-cjs/resources/docs/uiTests.js +103 -0
  104. package/dist-cjs/resources/generated/aiPacks.js +4 -4
  105. package/dist-cjs/resources/index.js +12 -1
  106. package/dist-cjs/server.js +2 -2
  107. package/dist-cjs/tools/catalog.js +37 -0
  108. package/dist-cjs/tools/diagrams.js +4 -1
  109. package/dist-cjs/tools/index.js +8 -0
  110. package/dist-cjs/tools/lookups.js +83 -1
  111. package/dist-cjs/tools/scenarioAdmin.js +145 -0
  112. package/dist-cjs/tools/scenarios.js +113 -17
  113. package/dist-cjs/tools/shared.js +12 -1
  114. package/dist-cjs/tools/uiTestAdmin.js +512 -0
  115. package/dist-cjs/tools/uiTestRuns.js +372 -0
  116. package/dist-cjs/tools/uiTestShared.js +202 -0
  117. package/dist-cjs/tools/uiTests.js +575 -0
  118. package/package.json +3 -3
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.registerScenarioAdminTools = void 0;
4
+ const zod_1 = require("zod");
5
+ const errors_js_1 = require("../client/errors.js");
6
+ const resolve_js_1 = require("../domain/resolve.js");
7
+ const lookups_js_1 = require("./lookups.js");
8
+ const scenarios_js_1 = require("./scenarios.js");
9
+ const shared_js_1 = require("./shared.js");
10
+ // Scenario folders and the on-prem review workflow (approve / reject, the
11
+ // approved-version history) — what the desktop's scenario sidebar and Review
12
+ // Mode do, for AI tools. list_scenarios already returns the folders.
13
+ const versionsOf = (raw) => (Array.isArray(raw) ? raw : (raw.items ?? []));
14
+ const versionRow = (version) => ({
15
+ version: version.versionNumber,
16
+ name: version.name,
17
+ savedVersion: version.scenarioSaveVersion,
18
+ active: version.isActive,
19
+ submittedBy: version.submittedByLabel,
20
+ submittedAt: version.submittedAt,
21
+ approvedBy: version.approvedByLabel,
22
+ approvedAt: version.approvedAt,
23
+ });
24
+ const cloudOnlyNote = (error) => error instanceof errors_js_1.CallmanApiError && (error.status === 400 || error.status === 404 || error.status === 409) && /cloud|on-prem|onprem/i.test(error.message)
25
+ ? "Cloud has no review workflow — every save is approved at once."
26
+ : null;
27
+ const registerScenarioAdminTools = (server, ctx) => {
28
+ server.registerTool("create_scenario_folder", {
29
+ title: "Create a scenario folder",
30
+ description: "Create a folder in the scenario sidebar (flat — folders do not nest). File scenarios into it with create_scenario / update_scenario `folder`. On-prem, `group` binds the folder to one of your scenario groups.",
31
+ inputSchema: {
32
+ workspace: shared_js_1.workspaceArg,
33
+ name: zod_1.z.string().trim().min(1).max(120),
34
+ group: zod_1.z.string().max(200).optional().describe('On-prem: the scenario group (name or id from list_scenarios `groups`, or "personal")'),
35
+ },
36
+ annotations: shared_js_1.WRITE,
37
+ }, (0, shared_js_1.guarded)(async ({ workspace, name, group }) => {
38
+ const ws = await ctx.resolveWorkspace(workspace);
39
+ const targetGroup = await (0, scenarios_js_1.resolveGroup)(ctx, ws.id, group);
40
+ const created = await ctx.api.createScenarioFolder(ws.id, { name, ...(targetGroup && targetGroup.id !== null ? { groupId: targetGroup.id } : {}) });
41
+ ctx.invalidate("scenarios:");
42
+ return (0, shared_js_1.ok)({ folder: { id: created.id, name: created.name }, ...(targetGroup?.note ? { warnings: [targetGroup.note] } : {}) });
43
+ }));
44
+ server.registerTool("update_scenario_folder", {
45
+ title: "Rename a scenario folder",
46
+ description: "Rename a scenario folder (id or name).",
47
+ inputSchema: { workspace: shared_js_1.workspaceArg, folder: (0, shared_js_1.refArg)("Scenario folder"), name: zod_1.z.string().trim().min(1).max(120) },
48
+ annotations: shared_js_1.IDEMPOTENT_WRITE,
49
+ }, (0, shared_js_1.guarded)(async ({ workspace, folder, name }) => {
50
+ const ws = await ctx.resolveWorkspaceOrNull(workspace);
51
+ const resolved = await (0, lookups_js_1.resolveScenarioFolder)(ctx, ws?.id ?? null, folder);
52
+ const updated = await ctx.api.updateScenarioFolder(ws?.id ?? null, resolved.item.id, { name });
53
+ ctx.invalidate("scenarios:");
54
+ return (0, shared_js_1.ok)({ folder: { id: updated.id ?? resolved.item.id, name: updated.name ?? name }, ...(resolved.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(folder, resolved)] } : {}) });
55
+ }));
56
+ server.registerTool("delete_scenario_folder", {
57
+ title: "Delete a scenario folder",
58
+ description: "Delete a scenario folder (exact name or id). The scenarios inside are kept and move out of the folder. Confirm with the user first.",
59
+ inputSchema: { workspace: shared_js_1.workspaceArg, folder: (0, shared_js_1.refArg)("Scenario folder"), confirm: shared_js_1.confirmArg },
60
+ annotations: shared_js_1.DESTRUCTIVE,
61
+ }, (0, shared_js_1.guarded)(async ({ workspace, folder, confirm }) => {
62
+ (0, shared_js_1.requireConfirm)(confirm, "a scenario folder");
63
+ const ws = await ctx.resolveWorkspaceOrNull(workspace);
64
+ const resolved = await (0, lookups_js_1.resolveScenarioFolder)(ctx, ws?.id ?? null, folder);
65
+ await ctx.api.deleteScenarioFolder(ws?.id ?? null, resolved.item.id);
66
+ ctx.invalidate("scenarios:");
67
+ return (0, shared_js_1.ok)({ deleted: { id: resolved.item.id, name: resolved.item.name }, note: "Its scenarios are now outside any folder." });
68
+ }));
69
+ server.registerTool("review_scenario", {
70
+ title: "Approve or reject a scenario",
71
+ description: "As an approver (on-prem), approve a scenario waiting for review — its current content becomes the approved version that schedules, pipelines and personal-token runs use — or reject it with a note explaining the changes needed. list_scenario_versions shows the approved history.",
72
+ inputSchema: {
73
+ workspace: shared_js_1.workspaceArg,
74
+ scenario: (0, shared_js_1.refArg)("Scenario"),
75
+ decision: zod_1.z.enum(["approve", "reject"]),
76
+ note: zod_1.z.string().trim().max(2000).optional().describe("Required when rejecting"),
77
+ },
78
+ annotations: shared_js_1.WRITE,
79
+ }, (0, shared_js_1.guarded)(async ({ workspace, scenario, decision, note }) => {
80
+ if (decision === "reject" && !note)
81
+ throw new errors_js_1.ToolInputError("NOTE_REQUIRED", "Rejecting needs a note that says what to change.");
82
+ const ws = await ctx.resolveWorkspaceOrNull(workspace);
83
+ const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
84
+ try {
85
+ const result = decision === "approve" ? await ctx.api.approveScenario(ws?.id ?? null, resolved.item.id) : await ctx.api.rejectScenario(ws?.id ?? null, resolved.item.id, note);
86
+ ctx.invalidate("scenarios:");
87
+ return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name, reviewStatus: result.reviewStatus ?? null, approvedVersion: result.approvedVersion ?? null } });
88
+ }
89
+ catch (error) {
90
+ const cloudNote = cloudOnlyNote(error);
91
+ if (cloudNote)
92
+ return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, note: cloudNote });
93
+ throw error;
94
+ }
95
+ }));
96
+ server.registerTool("list_scenario_versions", {
97
+ title: "Scenario approved versions",
98
+ description: "The approved versions of a scenario (on-prem): number, who submitted and approved it, when, and which one is ACTIVE (what schedules and personal-token runs execute). With `version`: that version's graph (nodes and edges).",
99
+ inputSchema: { workspace: shared_js_1.workspaceArg, scenario: (0, shared_js_1.refArg)("Scenario"), version: zod_1.z.number().int().min(1).optional() },
100
+ annotations: shared_js_1.READ,
101
+ }, (0, shared_js_1.guarded)(async ({ workspace, scenario, version }) => {
102
+ const ws = await ctx.resolveWorkspaceOrNull(workspace);
103
+ const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
104
+ try {
105
+ if (version !== undefined) {
106
+ const detail = await ctx.api.getScenarioVersion(ws?.id ?? null, resolved.item.id, version);
107
+ const nodes = (detail.snapshot?.nodes ?? []);
108
+ return (0, shared_js_1.ok)({
109
+ scenario: { id: resolved.item.id, name: resolved.item.name },
110
+ version: versionRow(detail),
111
+ graph: { nodes: nodes.map((node) => ({ id: node.id, type: node.type, label: node.data?.label ?? null })), edges: detail.snapshot?.edges ?? [] },
112
+ });
113
+ }
114
+ const items = versionsOf(await ctx.api.listScenarioVersions(ws?.id ?? null, resolved.item.id));
115
+ return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name, version: resolved.item.version }, versions: items.map(versionRow) });
116
+ }
117
+ catch (error) {
118
+ const cloudNote = cloudOnlyNote(error);
119
+ if (cloudNote)
120
+ return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, versions: [], note: cloudNote });
121
+ throw error;
122
+ }
123
+ }));
124
+ server.registerTool("restore_scenario_version", {
125
+ title: "Restore an approved scenario version",
126
+ description: "Make an earlier approved version the ACTIVE one again (on-prem; approvers): schedules, pipelines and personal-token runs execute it from now on. The scenario's current content and review status are not changed.",
127
+ inputSchema: { workspace: shared_js_1.workspaceArg, scenario: (0, shared_js_1.refArg)("Scenario"), version: zod_1.z.number().int().min(1) },
128
+ annotations: shared_js_1.IDEMPOTENT_WRITE,
129
+ }, (0, shared_js_1.guarded)(async ({ workspace, scenario, version }) => {
130
+ const ws = await ctx.resolveWorkspaceOrNull(workspace);
131
+ const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
132
+ try {
133
+ await ctx.api.restoreScenarioVersion(ws?.id ?? null, resolved.item.id, version);
134
+ ctx.invalidate("scenarios:");
135
+ return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, activeVersion: version });
136
+ }
137
+ catch (error) {
138
+ const cloudNote = cloudOnlyNote(error);
139
+ if (cloudNote)
140
+ return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, note: cloudNote });
141
+ throw error;
142
+ }
143
+ }));
144
+ };
145
+ exports.registerScenarioAdminTools = registerScenarioAdminTools;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerScenarioTools = void 0;
3
+ exports.registerScenarioTools = exports.resolveGroup = void 0;
4
4
  const zod_1 = require("zod");
5
5
  const errors_js_1 = require("../client/errors.js");
6
6
  const resolve_js_1 = require("../domain/resolve.js");
@@ -43,7 +43,67 @@ const resolveGroup = async (ctx, ws, ref) => {
43
43
  return { id: null, note: "Scenario groups exist only on-prem — `group` was ignored." };
44
44
  return { id: (0, resolve_js_1.resolveRef)("scenario group", ref, items).item.id };
45
45
  };
46
+ exports.resolveGroup = resolveGroup;
46
47
  const latestScenario = async (ctx, ws, id) => ctx.api.getScenario(ws, id);
48
+ // The version of each scenario this session last READ or wrote. update_scenario
49
+ // replaces the whole graph, so without an explicit expectedVersion it checks
50
+ // against what the model actually saw — a newer save from the desktop (or a
51
+ // teammate) then comes back as a conflict instead of being overwritten.
52
+ const seenVersions = new WeakMap();
53
+ const rememberVersion = (ctx, scenario) => {
54
+ if (!scenario.id || typeof scenario.version !== "number")
55
+ return;
56
+ let map = seenVersions.get(ctx);
57
+ if (!map) {
58
+ map = new Map();
59
+ seenVersions.set(ctx, map);
60
+ }
61
+ map.set(scenario.id, scenario.version);
62
+ };
63
+ const seenVersion = (ctx, id) => seenVersions.get(ctx)?.get(id);
64
+ // scenario-ref nodes name the scenario to link (`config.scenario`: name or
65
+ // id); the server copies it in when the snapshot is empty.
66
+ const resolveScenarioLinks = async (ctx, ws, nodes) => {
67
+ const out = [];
68
+ for (const node of nodes) {
69
+ const ref = node.type.trim() === "scenario-ref" && node.config ? node.config.scenario : undefined;
70
+ if (typeof ref !== "string" || !ref.trim()) {
71
+ out.push(node);
72
+ continue;
73
+ }
74
+ const target = await (0, lookups_js_1.resolveScenario)(ctx, ws, ref);
75
+ const { scenario: _ref, ...rest } = node.config;
76
+ void _ref;
77
+ out.push({ ...node, config: { ...rest, referencedScenarioId: target.item.id, nodes: [], edges: [] } });
78
+ }
79
+ return out;
80
+ };
81
+ // Labels must be unique across the whole graph; a linked scenario brings its
82
+ // own nodes' labels, which this tool can only check after the server copied
83
+ // them in.
84
+ const labelClashWarnings = (nodes) => {
85
+ const owners = new Map();
86
+ const clashes = [];
87
+ const walk = (list, container) => {
88
+ for (const node of list) {
89
+ const label = node.data?.label;
90
+ if (typeof label === "string" && label) {
91
+ const where = container ? `${node.id} (inside ${container})` : node.id;
92
+ const first = owners.get(label);
93
+ if (first)
94
+ clashes.push(`"${label}" is used by ${first} and ${where}`);
95
+ else
96
+ owners.set(label, where);
97
+ }
98
+ const inner = node.data?.config?.nodes;
99
+ if (Array.isArray(inner))
100
+ walk(inner, container ?? node.id);
101
+ }
102
+ };
103
+ walk(nodes, null);
104
+ return clashes.length > 0 ? [`Label clash after linking — later nodes read the wrong output until you rename one: ${clashes.join("; ")}.`] : [];
105
+ };
106
+ const hasLink = (graph) => Boolean(graph?.nodes.some((node) => node.type === "scenario-ref" && Array.isArray(node.data.config.nodes) && node.data.config.nodes.length === 0));
47
107
  const registerScenarioTools = (server, ctx) => {
48
108
  server.registerTool("list_scenarios", {
49
109
  title: "List scenarios",
@@ -92,6 +152,7 @@ const registerScenarioTools = (server, ctx) => {
92
152
  const ws = await ctx.resolveWorkspaceOrNull(workspace);
93
153
  const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
94
154
  const fresh = resolved.matched === "id" && !reveal ? resolved.item : await ctx.api.getScenario(ws?.id ?? null, resolved.item.id, reveal ?? false);
155
+ rememberVersion(ctx, fresh);
95
156
  if (node) {
96
157
  const found = (0, summarize_js_1.scenarioNodeView)(fresh, node, { reveal });
97
158
  if (!found) {
@@ -104,7 +165,7 @@ const registerScenarioTools = (server, ctx) => {
104
165
  }));
105
166
  server.registerTool("get_scenario_report", {
106
167
  title: "Get a scenario run report",
107
- description: "The result of a scenario run: status, summary counts, an error outside any node (unexpectedError), a per-node timeline and every failing node expanded (failed tests, assertion checks with expected vs actual, failed contracts, script error with line, the input as resolved at run time, response, logs). `node` returns one node's result in that detail. Defaults to the latest run; `list: true` lists recent runs instead.",
168
+ description: "The result of a scenario run: status, summary counts, an error outside any node (unexpectedError), a per-node timeline and every failing node expanded (failed tests, assertion checks with expected vs actual, failed contracts, script error with line, the input as resolved at run time, response, logs). `node` returns one node's result in that detail. Defaults to the latest run; `list: true` lists recent runs instead. Covers every run the server knows: server runs, schedules, CLI/pipeline runs, desktop-relay runs (run_scenario target \"desktop\") and runs started in the desktop app (saved there unless the user turned that off) — `triggerType` / `executor` say which.",
108
169
  inputSchema: {
109
170
  workspace: shared_js_1.workspaceArg,
110
171
  scenario: (0, shared_js_1.refArg)("Scenario"),
@@ -125,7 +186,7 @@ const registerScenarioTools = (server, ctx) => {
125
186
  const runs = await ctx.api.listScenarioReports(ws?.id ?? null, resolved.item.id, 5);
126
187
  const latest = runs.items[0];
127
188
  if (!latest)
128
- return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, report: null, note: "This scenario has not been run on the server yet. Use run_scenario." });
189
+ return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, report: null, note: "No saved runs yet. Use run_scenario (target \"desktop\" for what only the user's machine reaches)." });
129
190
  reportId = latest.reportId;
130
191
  }
131
192
  const detail = await ctx.api.getScenarioReport(ws?.id ?? null, resolved.item.id, reportId);
@@ -151,14 +212,22 @@ const registerScenarioTools = (server, ctx) => {
151
212
  }, (0, shared_js_1.guarded)(async ({ workspace, folder, group, dryRun, ...input }) => {
152
213
  const ws = await ctx.resolveWorkspace(workspace);
153
214
  const targetFolder = folder ? await (0, lookups_js_1.resolveScenarioFolder)(ctx, ws.id, folder) : null;
154
- const targetGroup = await resolveGroup(ctx, ws.id, group);
155
- const { body, graph } = (0, build_js_1.buildScenarioCreateBody)(input, targetFolder?.item.id ?? null, targetGroup?.id === null ? undefined : targetGroup?.id);
215
+ const targetGroup = await (0, exports.resolveGroup)(ctx, ws.id, group);
216
+ const linkedInput = { ...input, nodes: await resolveScenarioLinks(ctx, ws.id, input.nodes) };
217
+ const { body, graph } = (0, build_js_1.buildScenarioCreateBody)(linkedInput, targetFolder?.item.id ?? null, targetGroup?.id === null ? undefined : targetGroup?.id);
156
218
  if (dryRun) {
157
219
  return (0, shared_js_1.ok)({ dryRun: true, scenario: { name: body.name, nodes: graph.nodes.map((node) => ({ id: node.id, type: node.type, label: node.data.label, config: node.data.config })), edges: graph.edges }, warnings: graph.warnings });
158
220
  }
159
221
  const created = await ctx.api.createScenario(ws.id, body);
160
222
  ctx.invalidate("scenarios:");
161
- const warnings = [...graph.warnings, ...droppedFieldWarnings(graph, created), ...(created.graph?.warnings ?? []), ...(targetGroup?.note ? [targetGroup.note] : [])];
223
+ rememberVersion(ctx, created);
224
+ const warnings = [
225
+ ...graph.warnings,
226
+ ...droppedFieldWarnings(graph, created),
227
+ ...(created.graph?.warnings ?? []),
228
+ ...(targetGroup?.note ? [targetGroup.note] : []),
229
+ ...(hasLink(graph) ? labelClashWarnings(created.nodes) : []),
230
+ ];
162
231
  return (0, shared_js_1.ok)({
163
232
  scenario: (0, summarize_js_1.scenarioGraphView)(created),
164
233
  ...(warnings.length > 0 ? { warnings } : {}),
@@ -226,12 +295,12 @@ const registerScenarioTools = (server, ctx) => {
226
295
  }
227
296
  }
228
297
  }
229
- const graph = (0, build_js_1.buildScenarioGraph)([...nodes.values()], edges, { existing: current.nodes });
298
+ const graph = (0, build_js_1.buildScenarioGraph)(await resolveScenarioLinks(ctx, ws?.id ?? null, [...nodes.values()]), edges, { existing: current.nodes });
230
299
  return saveGraph(ctx, ws?.id ?? null, current, graph, { expectedVersion, annotation: resolved.annotation ? (0, resolve_js_1.resolvedSummary)(scenario, resolved) : undefined });
231
300
  }));
232
301
  server.registerTool("update_scenario", {
233
302
  title: "Update a scenario",
234
- description: "Update a scenario's name / description / priority / folder / environments / dataset / dbSettings and/or replace its graph (nodes AND edges together). A node whose id already exists MERGES into the stored node (only the config keys you send change, its position is kept); new ids merge over the type's defaults; nodes you leave out are removed. `environments` replaces the whole list (stored ids are kept by name; a masked value sent back unchanged keeps the stored one). For small edits prefer patch_scenario. Version-checked: pass the `version` you read (expectedVersion) or it is fetched first; a conflict comes back structured. On-prem: editing an approved scenario returns it to draft.",
303
+ description: "Update a scenario's name / description / priority / folder / environments / dataset / dbSettings and/or replace its graph (nodes AND edges together). A node whose id already exists MERGES into the stored node (only the config keys you send change, its position is kept); new ids merge over the type's defaults; nodes you leave out are removed. `environments` replaces the whole list (stored ids are kept by name; a masked value sent back unchanged keeps the stored one). For small edits prefer patch_scenario. Version-checked: a graph replace is checked against the `version` you last read (or expectedVersion) — if someone saved since (e.g. in the desktop app) you get a structured conflict instead of overwriting their work. On-prem: editing an approved scenario returns it to draft.",
235
304
  inputSchema: {
236
305
  workspace: shared_js_1.workspaceArg,
237
306
  scenario: (0, shared_js_1.refArg)("Scenario"),
@@ -256,7 +325,7 @@ const registerScenarioTools = (server, ctx) => {
256
325
  const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
257
326
  const current = await latestScenario(ctx, ws?.id ?? null, resolved.item.id);
258
327
  const targetFolder = folder ? await (0, lookups_js_1.resolveScenarioFolder)(ctx, ws?.id ?? null, folder) : null;
259
- const graph = nodes && edges ? (0, build_js_1.buildScenarioGraph)(nodes, edges, { existing: current.nodes }) : null;
328
+ const graph = nodes && edges ? (0, build_js_1.buildScenarioGraph)(await resolveScenarioLinks(ctx, ws?.id ?? null, nodes), edges, { existing: current.nodes }) : null;
260
329
  const extra = {
261
330
  ...(name !== undefined ? { name } : {}),
262
331
  ...(description !== undefined ? { description } : {}),
@@ -268,7 +337,9 @@ const registerScenarioTools = (server, ctx) => {
268
337
  };
269
338
  if (!graph && Object.keys(extra).length === 0)
270
339
  throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass at least one field to change.");
271
- return saveGraph(ctx, ws?.id ?? null, current, graph, { expectedVersion, force, extra, annotation: resolved.annotation ? (0, resolve_js_1.resolvedSummary)(scenario, resolved) : undefined });
340
+ // A whole-graph replace checks against the version this session read.
341
+ const checkedVersion = expectedVersion ?? (graph ? seenVersion(ctx, current.id) : undefined);
342
+ return saveGraph(ctx, ws?.id ?? null, current, graph, { expectedVersion: checkedVersion, force, extra, annotation: resolved.annotation ? (0, resolve_js_1.resolvedSummary)(scenario, resolved) : undefined });
272
343
  }));
273
344
  server.registerTool("submit_scenario", {
274
345
  title: "Submit a scenario for approval",
@@ -310,17 +381,18 @@ const registerScenarioTools = (server, ctx) => {
310
381
  return (0, shared_js_1.ok)({ deleted: { id: resolved.item.id, name: resolved.item.name }, resolved: [(0, resolve_js_1.resolvedSummary)(scenario, resolved)] });
311
382
  }));
312
383
  server.registerTool("run_scenario", {
313
- title: "Run a scenario on the server",
314
- description: "Start a server-side run of a scenario (with one of its inline environments, by name or id; the first one when omitted) and wait for the result (default up to 5 minutes, max 30). Returns the report as in get_scenario_report. A personal token runs the scenario's APPROVED version on-prem — the result says when that differs from the version you edited. If it times out you get the reportId to check later.",
384
+ title: "Run a scenario",
385
+ description: "Run a scenario (with one of its inline environments, by name or id; the first one when omitted) and wait for the result (default up to 5 minutes, max 30). Returns the report as in get_scenario_report. WHERE it runs matters: target \"server\" (default) runs on the Callman server, which reaches only what the server's network reaches — not the user's localhost, port-forwards, VPN-only hosts or internal databases. target \"desktop\" runs it on the user's own Callman desktop app (it must be open and signed in with Settings → AI tools → \"Let AI tools run scenarios on this computer\" on) — use it for local Kafka, local or internal databases and anything only that machine reaches. A personal token runs the scenario's APPROVED version on-prem (either target) — the result says when that differs from the version you edited. If it times out you get the reportId to check later.",
315
386
  inputSchema: {
316
387
  workspace: shared_js_1.workspaceArg,
317
388
  scenario: (0, shared_js_1.refArg)("Scenario"),
389
+ target: zod_1.z.enum(["server", "desktop"]).default("server").describe('"server" (default) or "desktop" — the user\'s own desktop app, for what only their machine reaches'),
318
390
  environment: zod_1.z.string().max(200).optional().describe("One of the scenario's own environments (name or id)"),
319
391
  wait: zod_1.z.boolean().default(true),
320
392
  timeoutSec: zod_1.z.number().int().min(5).max(1800).default(300),
321
393
  },
322
394
  annotations: shared_js_1.RUN,
323
- }, (0, shared_js_1.guarded)(async ({ workspace, scenario, environment, wait, timeoutSec }) => {
395
+ }, (0, shared_js_1.guarded)(async ({ workspace, scenario, target, environment, wait, timeoutSec }) => {
324
396
  const ws = await ctx.resolveWorkspaceOrNull(workspace);
325
397
  const resolved = await (0, lookups_js_1.resolveScenario)(ctx, ws?.id ?? null, scenario);
326
398
  let environmentId;
@@ -333,9 +405,19 @@ const registerScenarioTools = (server, ctx) => {
333
405
  }
334
406
  environmentId = picked.id;
335
407
  }
336
- const trigger = await ctx.api.runScenario(ws?.id ?? null, resolved.item.id, environmentId ? { environmentId } : {});
408
+ const runBody = environmentId ? { environmentId } : {};
409
+ let trigger;
410
+ try {
411
+ trigger = target === "desktop" ? await ctx.api.runScenarioOnDesktop(ws?.id ?? null, resolved.item.id, runBody) : await ctx.api.runScenario(ws?.id ?? null, resolved.item.id, runBody);
412
+ }
413
+ catch (error) {
414
+ if (target === "desktop" && error instanceof errors_js_1.CallmanApiError && error.status === 404 && error.code !== "SCENARIO_NOT_FOUND") {
415
+ throw new errors_js_1.ToolInputError("SCENARIO_DESKTOP_UNSUPPORTED", "This Callman server cannot run scenarios on a desktop yet (update the backend), so only target \"server\" works.");
416
+ }
417
+ throw error;
418
+ }
337
419
  if (!wait)
338
- return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, reportId: trigger.reportId, status: trigger.status, note: "Check with get_scenario_report." });
420
+ return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, reportId: trigger.reportId, status: trigger.status, target, note: "Check with get_scenario_report." });
339
421
  const deadline = Date.now() + timeoutSec * 1000;
340
422
  let polls = 0;
341
423
  let last = null;
@@ -354,7 +436,15 @@ const registerScenarioTools = (server, ctx) => {
354
436
  break;
355
437
  }
356
438
  if (!last || !TERMINAL_STATUSES.has(last.status)) {
357
- return (0, shared_js_1.ok)({ scenario: { id: resolved.item.id, name: resolved.item.name }, reportId: trigger.reportId, status: last?.status ?? "running", timedOut: true, note: `Still running after ${timeoutSec}s — call get_scenario_report with this reportId later.` });
439
+ return (0, shared_js_1.ok)({
440
+ scenario: { id: resolved.item.id, name: resolved.item.name },
441
+ reportId: trigger.reportId,
442
+ status: last?.status ?? "running",
443
+ timedOut: true,
444
+ note: target === "desktop" && (last?.status ?? "queued") === "queued"
445
+ ? `No desktop picked the run up within ${timeoutSec}s — the app must be open with "Let AI tools run scenarios on this computer" on. It is failed automatically after 10 minutes; check with get_scenario_report.`
446
+ : `Still running after ${timeoutSec}s — call get_scenario_report with this reportId later.`,
447
+ });
358
448
  }
359
449
  const warnings = [];
360
450
  if (typeof last.scenarioVersion === "number" && last.scenarioVersion !== resolved.item.version) {
@@ -379,7 +469,13 @@ const saveGraph = async (ctx, ws, current, graph, options) => {
379
469
  try {
380
470
  const updated = await ctx.api.patchScenario(ws, current.id, body);
381
471
  ctx.invalidate("scenarios:");
382
- const warnings = [...(graph?.warnings ?? []), ...(graph ? droppedFieldWarnings(graph, updated) : []), ...(updated.graph?.warnings ?? [])];
472
+ rememberVersion(ctx, updated);
473
+ const warnings = [
474
+ ...(graph?.warnings ?? []),
475
+ ...(graph ? droppedFieldWarnings(graph, updated) : []),
476
+ ...(updated.graph?.warnings ?? []),
477
+ ...(hasLink(graph) ? labelClashWarnings(updated.nodes) : []),
478
+ ];
383
479
  const removed = graph ? current.nodes.filter((node) => !graph.nodes.some((entry) => entry.id === node.id)).map((node) => node.id) : [];
384
480
  return (0, shared_js_1.ok)({
385
481
  scenario: (0, summarize_js_1.scenarioGraphView)(updated),
@@ -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) {