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.
- package/CHANGELOG.md +70 -0
- package/README.md +43 -11
- package/dist/client/api.d.ts +104 -1
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +137 -0
- package/dist/client/api.js.map +1 -1
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +46 -11
- package/dist/client/errors.js.map +1 -1
- package/dist/client/types.d.ts +257 -0
- package/dist/client/types.d.ts.map +1 -1
- package/dist/domain/diagram/patch.d.ts +32 -32
- package/dist/domain/scenario/build.d.ts.map +1 -1
- package/dist/domain/scenario/build.js +68 -17
- package/dist/domain/scenario/build.js.map +1 -1
- package/dist/domain/scenario/summarize.d.ts +1 -0
- package/dist/domain/scenario/summarize.d.ts.map +1 -1
- package/dist/domain/scenario/summarize.js +1 -0
- package/dist/domain/scenario/summarize.js.map +1 -1
- package/dist/domain/uiTest/compose.d.ts +55 -0
- package/dist/domain/uiTest/compose.d.ts.map +1 -0
- package/dist/domain/uiTest/compose.js +228 -0
- package/dist/domain/uiTest/compose.js.map +1 -0
- package/dist/domain/uiTest/summarize.d.ts +273 -0
- package/dist/domain/uiTest/summarize.d.ts.map +1 -0
- package/dist/domain/uiTest/summarize.js +145 -0
- package/dist/domain/uiTest/summarize.js.map +1 -0
- package/dist/lib/truncate.d.ts +1 -1
- package/dist/lib/truncate.d.ts.map +1 -1
- package/dist/lib/truncate.js.map +1 -1
- package/dist/resources/docs/diagramSchema.d.ts +2 -1
- package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
- package/dist/resources/docs/diagramSchema.js +30 -3
- package/dist/resources/docs/diagramSchema.js.map +1 -1
- package/dist/resources/docs/overview.d.ts +1 -1
- package/dist/resources/docs/overview.d.ts.map +1 -1
- package/dist/resources/docs/overview.js +40 -7
- package/dist/resources/docs/overview.js.map +1 -1
- package/dist/resources/docs/uiTests.d.ts +2 -0
- package/dist/resources/docs/uiTests.d.ts.map +1 -0
- package/dist/resources/docs/uiTests.js +101 -0
- package/dist/resources/docs/uiTests.js.map +1 -0
- package/dist/resources/generated/aiPacks.d.ts +1 -1
- package/dist/resources/generated/aiPacks.js +4 -4
- package/dist/resources/generated/aiPacks.js.map +1 -1
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +15 -4
- package/dist/resources/index.js.map +1 -1
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +2 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/catalog.d.ts +1 -1
- package/dist/tools/catalog.d.ts.map +1 -1
- package/dist/tools/catalog.js +37 -0
- package/dist/tools/catalog.js.map +1 -1
- package/dist/tools/diagrams.d.ts.map +1 -1
- package/dist/tools/diagrams.js +4 -1
- package/dist/tools/diagrams.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +8 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/lookups.d.ts +19 -1
- package/dist/tools/lookups.d.ts.map +1 -1
- package/dist/tools/lookups.js +77 -0
- package/dist/tools/lookups.js.map +1 -1
- package/dist/tools/scenarioAdmin.d.ts +4 -0
- package/dist/tools/scenarioAdmin.d.ts.map +1 -0
- package/dist/tools/scenarioAdmin.js +142 -0
- package/dist/tools/scenarioAdmin.js.map +1 -0
- package/dist/tools/scenarios.d.ts +4 -0
- package/dist/tools/scenarios.d.ts.map +1 -1
- package/dist/tools/scenarios.js +111 -16
- package/dist/tools/scenarios.js.map +1 -1
- package/dist/tools/shared.d.ts +5 -0
- package/dist/tools/shared.d.ts.map +1 -1
- package/dist/tools/shared.js +10 -0
- package/dist/tools/shared.js.map +1 -1
- package/dist/tools/uiTestAdmin.d.ts +4 -0
- package/dist/tools/uiTestAdmin.d.ts.map +1 -0
- package/dist/tools/uiTestAdmin.js +509 -0
- package/dist/tools/uiTestAdmin.js.map +1 -0
- package/dist/tools/uiTestRuns.d.ts +24 -0
- package/dist/tools/uiTestRuns.d.ts.map +1 -0
- package/dist/tools/uiTestRuns.js +367 -0
- package/dist/tools/uiTestRuns.js.map +1 -0
- package/dist/tools/uiTestShared.d.ts +236 -0
- package/dist/tools/uiTestShared.d.ts.map +1 -0
- package/dist/tools/uiTestShared.js +187 -0
- package/dist/tools/uiTestShared.js.map +1 -0
- package/dist/tools/uiTests.d.ts +4 -0
- package/dist/tools/uiTests.d.ts.map +1 -0
- package/dist/tools/uiTests.js +572 -0
- package/dist/tools/uiTests.js.map +1 -0
- package/dist-cjs/client/api.js +137 -0
- package/dist-cjs/client/errors.js +46 -11
- package/dist-cjs/domain/scenario/build.js +68 -17
- package/dist-cjs/domain/scenario/summarize.js +1 -0
- package/dist-cjs/domain/uiTest/compose.js +242 -0
- package/dist-cjs/domain/uiTest/summarize.js +155 -0
- package/dist-cjs/resources/docs/diagramSchema.js +32 -4
- package/dist-cjs/resources/docs/overview.js +40 -7
- package/dist-cjs/resources/docs/uiTests.js +103 -0
- package/dist-cjs/resources/generated/aiPacks.js +4 -4
- package/dist-cjs/resources/index.js +12 -1
- package/dist-cjs/server.js +2 -2
- package/dist-cjs/tools/catalog.js +37 -0
- package/dist-cjs/tools/diagrams.js +4 -1
- package/dist-cjs/tools/index.js +8 -0
- package/dist-cjs/tools/lookups.js +83 -1
- package/dist-cjs/tools/scenarioAdmin.js +145 -0
- package/dist-cjs/tools/scenarios.js +113 -17
- package/dist-cjs/tools/shared.js +12 -1
- package/dist-cjs/tools/uiTestAdmin.js +512 -0
- package/dist-cjs/tools/uiTestRuns.js +372 -0
- package/dist-cjs/tools/uiTestShared.js +202 -0
- package/dist-cjs/tools/uiTests.js +575 -0
- 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: "
|
|
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 {
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
314
|
-
description: "
|
|
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
|
|
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)({
|
|
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
|
-
|
|
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),
|
package/dist-cjs/tools/shared.js
CHANGED
|
@@ -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) {
|