callman-mcp 0.2.0 → 0.4.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 (63) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/README.md +4 -4
  3. package/dist/client/api.d.ts +16 -1
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/api.js +22 -0
  6. package/dist/client/api.js.map +1 -1
  7. package/dist/client/types.d.ts +14 -0
  8. package/dist/client/types.d.ts.map +1 -1
  9. package/dist/domain/diagram/build.d.ts +122 -73
  10. package/dist/domain/diagram/build.d.ts.map +1 -1
  11. package/dist/domain/diagram/build.js +51 -167
  12. package/dist/domain/diagram/build.js.map +1 -1
  13. package/dist/domain/diagram/layout.d.ts +5 -4
  14. package/dist/domain/diagram/layout.d.ts.map +1 -1
  15. package/dist/domain/diagram/layout.js +18 -45
  16. package/dist/domain/diagram/layout.js.map +1 -1
  17. package/dist/domain/diagram/patch.d.ts +1011 -92
  18. package/dist/domain/diagram/patch.d.ts.map +1 -1
  19. package/dist/domain/diagram/patch.js +113 -273
  20. package/dist/domain/diagram/patch.js.map +1 -1
  21. package/dist/domain/diagram/style.d.ts +4 -12
  22. package/dist/domain/diagram/style.d.ts.map +1 -1
  23. package/dist/domain/diagram/style.js +8 -84
  24. package/dist/domain/diagram/style.js.map +1 -1
  25. package/dist/domain/diagram/summarize.d.ts +26 -24
  26. package/dist/domain/diagram/summarize.d.ts.map +1 -1
  27. package/dist/domain/diagram/summarize.js +21 -35
  28. package/dist/domain/diagram/summarize.js.map +1 -1
  29. package/dist/domain/requestIntegrations.d.ts +20 -20
  30. package/dist/domain/requestShape.d.ts +32 -32
  31. package/dist/resources/docs/diagramSchema.d.ts +1 -1
  32. package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
  33. package/dist/resources/docs/diagramSchema.js +87 -10
  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 +4 -1
  38. package/dist/resources/docs/overview.js.map +1 -1
  39. package/dist/resources/generated/aiPacks.d.ts +1 -1
  40. package/dist/resources/generated/aiPacks.js +2 -2
  41. package/dist/tools/catalog.d.ts.map +1 -1
  42. package/dist/tools/catalog.js +5 -0
  43. package/dist/tools/catalog.js.map +1 -1
  44. package/dist/tools/diagrams.d.ts.map +1 -1
  45. package/dist/tools/diagrams.js +547 -59
  46. package/dist/tools/diagrams.js.map +1 -1
  47. package/dist/tools/lookups.d.ts +5 -1
  48. package/dist/tools/lookups.d.ts.map +1 -1
  49. package/dist/tools/lookups.js +41 -0
  50. package/dist/tools/lookups.js.map +1 -1
  51. package/dist-cjs/client/api.js +22 -0
  52. package/dist-cjs/domain/diagram/build.js +55 -171
  53. package/dist-cjs/domain/diagram/layout.js +19 -45
  54. package/dist-cjs/domain/diagram/patch.js +111 -271
  55. package/dist-cjs/domain/diagram/style.js +11 -88
  56. package/dist-cjs/domain/diagram/summarize.js +23 -35
  57. package/dist-cjs/resources/docs/diagramSchema.js +87 -10
  58. package/dist-cjs/resources/docs/overview.js +4 -1
  59. package/dist-cjs/resources/generated/aiPacks.js +2 -2
  60. package/dist-cjs/tools/catalog.js +5 -0
  61. package/dist-cjs/tools/diagrams.js +539 -51
  62. package/dist-cjs/tools/lookups.js +46 -1
  63. package/package.json +2 -2
@@ -14,74 +14,375 @@ const pagination_js_1 = require("../lib/pagination.js");
14
14
  const lookups_js_1 = require("./lookups.js");
15
15
  const shared_js_1 = require("./shared.js");
16
16
  const directionArg = zod_1.z.enum(["DOWN", "RIGHT", "UP", "LEFT"]).optional().describe("Layout flow direction (default DOWN)");
17
+ const sourceArg = zod_1.z
18
+ .object({
19
+ format: zod_1.z.enum(["mermaid", "bpmn", "database"]).describe('"mermaid": Mermaid text; "bpmn": a BPMN 2.0 XML file; "database": the JSON database model of a Database Diagram (get_diagram view "source" of one, or the desktop\'s schema export)'),
20
+ text: zod_1.z
21
+ .string()
22
+ .min(1)
23
+ .max(2_000_000)
24
+ .describe("Mermaid: a flowchart/graph becomes nodes and edges (laid out automatically) and stays attached as the diagram's source; any other Mermaid type is stored as source only. BPMN: the XML is the diagram (its shapes, flows, pools and lanes are derived from it with their positions)"),
25
+ })
26
+ .strict()
27
+ .optional()
28
+ .describe("Create the diagram from Mermaid text, BPMN 2.0 XML or a database model instead of nodes/edges");
29
+ // A BPMN diagram keeps its truth in BPMN 2.0 XML: the model is derived from it.
30
+ const bpmnSource = (text) => ({ format: "bpmn", text, diagramType: "bpmn", grammar: callman_core_1.BPMN_GRAMMAR_VERSION });
31
+ // Typed bpmn.* nodes → a BPMN diagram: the model becomes XML, the XML becomes
32
+ // the diagram's source, and the shapes are derived back from it.
33
+ const bpmnDocumentFromModel = (model, unpositionedNodeIds, base) => {
34
+ const placed = unpositionedNodeIds.length > 0 ? (0, callman_core_1.applyDiagramOps)(model, (0, callman_core_1.placeBpmnNodesOps)(model, unpositionedNodeIds)).doc : model;
35
+ const xml = (0, callman_core_1.bpmnXmlFromDocument)({ ...placed, source: undefined });
36
+ const document = (0, callman_core_1.applyDiagramOps)({ ...base, nodes: [], edges: [], source: undefined }, [{ op: "setSource", source: bpmnSource(xml), resync: true }], { now: now() }).doc;
37
+ return { document, layoutNote: unpositionedNodeIds.length > 0 ? `Written as BPMN 2.0 XML; ${unpositionedNodeIds.length} node(s) placed left to right after their predecessors.` : "Written as BPMN 2.0 XML with the positions given." };
38
+ };
39
+ // A database diagram keeps its truth in the JSON database model.
40
+ const databaseSource = (text) => ({ format: "database", text, diagramType: "database", grammar: callman_core_1.DATABASE_GRAMMAR_VERSION, valid: true });
41
+ const DATABASE_DIALECTS = ["postgres", "mysql", "mssql", "oracle", "mongo", "generic"];
42
+ const toolErrorFromDatabaseEdit = (error) => {
43
+ if (error instanceof callman_core_1.DatabaseEditError)
44
+ throw new errors_js_1.ToolInputError(error.code, error.message);
45
+ throw error;
46
+ };
47
+ // A database model → a database diagram: the model becomes the source, the
48
+ // tables are derived from it and laid out (unrelated tables packed in rows).
49
+ const databaseDocumentFromModel = async (model, base, keepPositions = false) => {
50
+ const applied = (0, callman_core_1.applyDiagramOps)({ ...base, ...(keepPositions ? {} : { nodes: [], edges: [] }), source: undefined }, [{ op: "setSource", source: databaseSource((0, callman_core_1.serializeDatabaseModel)(model)), resync: true }], { now: now() });
51
+ let document = applied.doc;
52
+ if (keepPositions) {
53
+ const fresh = applied.added.nodes.filter((id) => document.nodes.some((node) => node.id === id));
54
+ if (fresh.length > 0)
55
+ document = (0, layout_js_1.placeDatabaseNodes)(document, fresh);
56
+ return { document, layoutNote: fresh.length > 0 ? `${fresh.length} new table(s) placed to the right of the existing ones.` : "Positions kept." };
57
+ }
58
+ if (document.nodes.length === 0)
59
+ return { document, layoutNote: "An empty database diagram." };
60
+ const laid = await (0, layout_js_1.layoutDocument)(document, { request: callman_core_1.DATABASE_LAYOUT_REQUEST });
61
+ return { document: laid.document, layoutNote: laid.warning ?? `${document.nodes.length} table(s) laid out with ${laid.engine} (relationships left to right).` };
62
+ };
63
+ // Tables + relationships as the create tool takes them → a model.
64
+ const databaseModelFromInput = (tables, relationships, dialect) => {
65
+ const edits = [
66
+ ...tables.map((table) => ({ kind: "addTable", table: { ...table, ...(table.columns ? { columns: table.columns.map((column) => ({ ...column, defaultValue: column.defaultValue ?? undefined, comment: column.comment ?? undefined })) } : {}) } })),
67
+ ...relationships.map((relationship) => ({
68
+ kind: "addRelationship",
69
+ ...(relationship.id ? { id: relationship.id } : {}),
70
+ source: relationship.source,
71
+ target: relationship.target,
72
+ ...(relationship.cardinality ? { cardinality: relationship.cardinality } : {}),
73
+ ...(relationship.kind ? { relationshipKind: relationship.kind } : {}),
74
+ ...(relationship.name ? { name: relationship.name } : {}),
75
+ ...(relationship.label ? { label: relationship.label } : {}),
76
+ })),
77
+ ];
78
+ try {
79
+ return (0, callman_core_1.applyDatabaseEdits)((0, callman_core_1.emptyDatabaseModel)(dialect), edits).model;
80
+ }
81
+ catch (error) {
82
+ return toolErrorFromDatabaseEdit(error);
83
+ }
84
+ };
85
+ // A database connection by id or name, in one workspace or across all.
86
+ const findDatabaseConnection = async (ctx, workspace, ref) => {
87
+ const wanted = ref.trim().toLowerCase();
88
+ const workspaces = await (0, lookups_js_1.workspacesForListing)(ctx, workspace);
89
+ const matches = [];
90
+ for (const ws of workspaces) {
91
+ for (const connection of await ctx.api.listConnections(ws.id, "database")) {
92
+ if (connection.id === ref.trim() || connection.name.trim().toLowerCase() === wanted)
93
+ matches.push({ ws, connection });
94
+ }
95
+ }
96
+ if (matches.length === 0)
97
+ throw new errors_js_1.ToolInputError("CONNECTION_NOT_FOUND", `No database connection "${ref}"${workspace ? ` in workspace ${workspace}` : " in your workspaces"}. list_connections shows them (type: database).`);
98
+ const exact = matches.filter((match) => match.connection.id === ref.trim());
99
+ if (exact.length === 1)
100
+ return exact[0];
101
+ if (matches.length > 1) {
102
+ throw new errors_js_1.ToolInputError("AMBIGUOUS_CONNECTION", `"${ref}" matches ${matches.length} database connections; pass its id or a \`workspace\`.`, {
103
+ candidates: matches.map((match) => ({ id: match.connection.id, name: match.connection.name, workspace: (0, lookups_js_1.workspaceTag)(match.ws) })),
104
+ });
105
+ }
106
+ return matches[0];
107
+ };
108
+ const schemaQuery = (options) => ({
109
+ ...(options.schemas && options.schemas.length > 0 ? { schemas: options.schemas.join(",") } : {}),
110
+ ...(options.tables && options.tables.length > 0 ? { tables: options.tables.join(",") } : {}),
111
+ ...(options.includeViews !== undefined ? { includeViews: String(options.includeViews) } : {}),
112
+ ...(options.objectsOnly ? { objectsOnly: "true" } : {}),
113
+ ...(options.maxTables !== undefined ? { maxTables: options.maxTables } : {}),
114
+ });
115
+ const PROVIDER_LABEL = { postgresql: "PostgreSQL", mysql: "MySQL", mssql: "SQL Server", oracle: "Oracle", mongodb: "MongoDB" };
116
+ // Every document entering a tool goes through the shared gate (legacy fields
117
+ // stripped, ids deduped, parents healed) — the same one the studio applies.
17
118
  const loadDocument = (diagram) => {
18
- const parsed = (0, callman_core_1.safeParseDiagramDocument)(diagram.document);
119
+ const result = (0, callman_core_1.normalizeDocument)(diagram.document);
120
+ if (!result.document.nodes && !result.document.edges) {
121
+ throw new errors_js_1.ToolInputError("DIAGRAM_DOCUMENT_INVALID", `The stored diagram "${diagram.name}" is not a valid document.`);
122
+ }
123
+ return result.document;
124
+ };
125
+ const now = () => new Date().toISOString();
126
+ // A source-backed diagram whose Mermaid type is not a flowchart has no model
127
+ // to edit: structural tools refuse it clearly instead of guessing. (BPMN
128
+ // diagrams are editable: their ops patch the XML.)
129
+ const assertEditableStructure = (doc) => {
130
+ if (doc.source && doc.source.format === "mermaid" && doc.source.diagramType !== "flowchart") {
131
+ throw new errors_js_1.ToolInputError("DIAGRAM_SOURCE_UNSUPPORTED_TYPE", `This diagram is a Mermaid ${doc.source.diagramType} kept as source only; nodes and edges cannot be edited through ops. Update its source with update_diagram { source } or read it with get_diagram view:"source".`, { diagramType: doc.source.diagramType });
132
+ }
133
+ };
134
+ // Mermaid text → the document it describes: a flowchart is parsed into
135
+ // nodes/edges (positions come from layout); anything else is kept as
136
+ // source only (the desktop previews it, tools read it back).
137
+ const documentFromSource = async (title, text, existing, direction, format = "mermaid") => {
138
+ if (format === "database") {
139
+ const parsed = (0, callman_core_1.parseDatabaseModel)(text);
140
+ if (!parsed.ok)
141
+ throw new errors_js_1.ToolInputError("DIAGRAM_SOURCE_INVALID", "The text is not a readable database model.", { errors: parsed.errors.slice(0, 20) });
142
+ const base = existing ?? (0, callman_core_1.createEmptyDocument)(title);
143
+ const keep = Boolean(existing && existing.source?.format === "database");
144
+ let built;
145
+ try {
146
+ built = await databaseDocumentFromModel(parsed.model, keep ? existing : { ...base, nodes: [], edges: [] }, keep);
147
+ }
148
+ catch (error) {
149
+ return (0, build_js_1.toToolError)(error);
150
+ }
151
+ return { document: built.document, layoutNote: built.layoutNote, warnings: parsed.warnings };
152
+ }
153
+ if (format === "bpmn") {
154
+ if (!(0, callman_core_1.looksLikeBpmn)(text))
155
+ throw new errors_js_1.ToolInputError("DIAGRAM_SOURCE_INVALID", "The text is not a BPMN 2.0 document (no <definitions> root).");
156
+ const parsed = (0, callman_core_1.parseBpmn)(text);
157
+ if (!parsed.ok) {
158
+ throw new errors_js_1.ToolInputError("DIAGRAM_SOURCE_INVALID", "The BPMN XML could not be read.", { diagnostics: parsed.structure.diagnostics.filter((entry) => entry.severity === "error").slice(0, 20) });
159
+ }
160
+ const base = existing ?? (0, callman_core_1.createEmptyDocument)(title);
161
+ let document;
162
+ try {
163
+ document = (0, callman_core_1.applyDiagramOps)(base, [{ op: "setSource", source: bpmnSource(text), resync: true }], { now: now() }).doc;
164
+ }
165
+ catch (error) {
166
+ return (0, build_js_1.toToolError)(error);
167
+ }
168
+ // Shapes the XML has no diagram interchange for are placed after their predecessors.
169
+ const unplaced = parsed.structure.nodes.filter((node) => !node.position).map((node) => node.id).filter((id) => document.nodes.some((node) => node.id === id));
170
+ if (unplaced.length > 0)
171
+ document = (0, callman_core_1.applyDiagramOps)(document, (0, callman_core_1.placeBpmnNodesOps)(document, unplaced), { now: now() }).doc;
172
+ const warnings = parsed.structure.diagnostics.filter((entry) => entry.severity !== "error").map((entry) => entry.message);
173
+ return { document, layoutNote: unplaced.length > 0 ? `Positions from the BPMN diagram interchange; ${unplaced.length} shape(s) without one were placed.` : "Positions from the BPMN diagram interchange.", warnings };
174
+ }
175
+ if (!(0, callman_core_1.looksLikeMermaid)(text)) {
176
+ throw new errors_js_1.ToolInputError("DIAGRAM_SOURCE_INVALID", "The text does not start with a Mermaid diagram header (flowchart, graph, sequenceDiagram, …).");
177
+ }
178
+ const base = existing ?? (0, callman_core_1.createEmptyDocument)(title);
179
+ if (!(0, callman_core_1.isMermaidFlowchart)(text)) {
180
+ const header = text.replace(/^---[\s\S]*?\n---\s*\n/, "").split("\n").map((line) => line.trim()).find((line) => line !== "" && !line.startsWith("%%")) ?? "";
181
+ const diagramType = header.split(/\s+/)[0] ?? "unknown";
182
+ const source = { format: "mermaid", text, diagramType, grammar: callman_core_1.MERMAID_GRAMMAR_VERSION, valid: true };
183
+ const doc = (0, callman_core_1.applyDiagramOps)(base, [{ op: "setSource", source, resync: false }], { now: now() }).doc;
184
+ return { document: doc, layoutNote: `Stored as Mermaid ${diagramType} source (no nodes to lay out).`, warnings: [] };
185
+ }
186
+ const parsed = (0, callman_core_1.parseMermaid)(text);
19
187
  if (!parsed.ok) {
20
- throw new errors_js_1.ToolInputError("DIAGRAM_DOCUMENT_INVALID", `The stored diagram "${diagram.name}" is not a valid document: ${parsed.errors.join("; ")}`, { errors: parsed.errors });
188
+ throw new errors_js_1.ToolInputError("DIAGRAM_SOURCE_INVALID", "The Mermaid flowchart has syntax errors.", {
189
+ diagnostics: parsed.structure.diagnostics.filter((entry) => entry.severity === "error").slice(0, 20),
190
+ });
21
191
  }
22
- return parsed.document;
192
+ const source = { format: "mermaid", text, diagramType: "flowchart", grammar: callman_core_1.MERMAID_GRAMMAR_VERSION };
193
+ let applied;
194
+ try {
195
+ applied = (0, callman_core_1.applyDiagramOps)(base, [{ op: "setSource", source }], { now: now() });
196
+ }
197
+ catch (error) {
198
+ return (0, build_js_1.toToolError)(error);
199
+ }
200
+ const warnings = parsed.structure.diagnostics.filter((entry) => entry.severity !== "error").map((entry) => entry.message);
201
+ const newIds = applied.added.nodes;
202
+ let document = applied.doc;
203
+ let layoutNote;
204
+ if (newIds.length > 0) {
205
+ if (existing && existing.nodes.length > 0) {
206
+ document = (0, layout_js_1.placeNewNodes)(document, newIds);
207
+ layoutNote = `Placed ${newIds.length} new node(s) below the existing ones.`;
208
+ }
209
+ else {
210
+ const laid = await (0, layout_js_1.layoutDocument)(document, { direction: direction ?? parsed.structure.direction });
211
+ document = laid.document;
212
+ layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
213
+ }
214
+ }
215
+ return { document, layoutNote, warnings };
23
216
  };
217
+ // Save with the revision we read; on a 409 the caller reloads and reapplies.
218
+ const isConflict = (error) => error instanceof errors_js_1.CallmanApiError && error.status === 409;
24
219
  const registerDiagramTools = (server, ctx) => {
25
220
  server.registerTool("list_diagrams", {
26
221
  title: "List diagrams",
27
- description: "List your system-design diagrams and the ones shared with you (owner, node/edge counts, whether you can edit). Diagrams belong to users and are shared into workspaces — not workspace-scoped.",
28
- inputSchema: { query: zod_1.z.string().max(200).optional(), ...shared_js_1.cursorArgs },
222
+ description: "List your system-design diagrams and the ones shared with you (owner, node/edge counts, whether you can edit), with the folder tree of your library: `folders` are your own folders as paths (\"Architecture/Payments\"), each diagram carries its `folder` path (null = the top level; always null for a diagram shared with you — folders are private to their owner). Diagrams belong to users and are shared into workspaces — not workspace-scoped.",
223
+ inputSchema: {
224
+ query: zod_1.z.string().max(200).optional(),
225
+ folder: zod_1.z.string().max(300).optional().describe('Only the diagrams in this folder of your library (id, name or path such as "Architecture/Payments"); "/" = the top level only'),
226
+ ...shared_js_1.cursorArgs,
227
+ },
29
228
  annotations: shared_js_1.READ,
30
- }, (0, shared_js_1.guarded)(async ({ query, cursor, limit }) => {
31
- const all = await (0, lookups_js_1.loadDiagrams)(ctx);
229
+ }, (0, shared_js_1.guarded)(async ({ query, folder, cursor, limit }) => {
230
+ const [all, rows] = await Promise.all([(0, lookups_js_1.loadDiagrams)(ctx), (0, lookups_js_1.diagramFolderRows)(ctx)]);
32
231
  const needle = query?.trim().toLowerCase();
33
- const filtered = needle ? all.filter((diagram) => diagram.name.toLowerCase().includes(needle)) : all;
34
- const page = (0, pagination_js_1.sliceWithCursor)(filtered, cursor, limit, needle ?? "");
35
- return (0, shared_js_1.ok)({ diagrams: page.items.map(summarize_js_1.diagramListView), total: page.total, nextCursor: page.nextCursor });
232
+ const scope = folder?.trim() === "/" ? { item: null, annotation: undefined } : folder ? await (0, lookups_js_1.resolveDiagramFolder)(ctx, folder) : undefined;
233
+ const filtered = all.filter((diagram) => {
234
+ if (needle && !diagram.name.toLowerCase().includes(needle))
235
+ return false;
236
+ if (scope === undefined)
237
+ return true;
238
+ return (diagram.folderId ?? null) === (scope.item?.id ?? null);
239
+ });
240
+ const page = (0, pagination_js_1.sliceWithCursor)(filtered, cursor, limit, `${needle ?? ""}|${scope?.item?.id ?? (scope ? "/" : "")}`);
241
+ return (0, shared_js_1.ok)({
242
+ diagrams: page.items.map((diagram) => ({ ...(0, summarize_js_1.diagramListView)(diagram), folder: (0, lookups_js_1.diagramFolderPath)(rows, diagram.folderId) })),
243
+ folders: rows.map((row) => ({ id: row.id, name: row.name, parentId: row.parentId, path: row.path })),
244
+ total: page.total,
245
+ nextCursor: page.nextCursor,
246
+ ...(scope?.item && scope.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(folder, scope)] } : {}),
247
+ });
36
248
  }));
37
249
  server.registerTool("get_diagram", {
38
250
  title: "Get a diagram",
39
- description: "Read a diagram: `summary` (counts, owner, sharing), `graph` (default — nodes with type/label/parent/style and edges with semantic/color, in the same shape the write tools accept: enough to describe, restyle or edit it) or `full` (the complete stored DiagramDocument with positions, sizes and routes, up to 512 KB).",
40
- inputSchema: { diagram: (0, shared_js_1.refArg)("Diagram"), view: zod_1.z.enum(["summary", "graph", "full"]).default("graph") },
251
+ description: "Read a diagram: `summary` (counts, owner, sharing), `graph` (default — nodes with type/label/parent/style and edges with semantic/color/routing/sides, in the same shape the write tools accept: enough to describe, restyle or edit it), `mermaid` (the diagram rendered as Mermaid flowchart text), `source` (the Mermaid text, BPMN 2.0 XML or JSON database model a source-backed diagram keeps), `schema` (a database diagram's tables with columns, keys and relationships as table.column pairs — read it before database ops) or `full` (the complete stored DiagramDocument with positions, sizes and routes, up to 512 KB).",
252
+ inputSchema: { diagram: (0, shared_js_1.refArg)("Diagram"), view: zod_1.z.enum(["summary", "graph", "mermaid", "source", "schema", "full"]).default("graph") },
41
253
  annotations: shared_js_1.READ,
42
254
  }, (0, shared_js_1.guarded)(async ({ diagram, view }) => {
43
255
  const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
44
256
  const doc = loadDocument(resolved.item);
45
257
  const summary = (0, summarize_js_1.diagramListView)(resolved.item);
46
- const body = view === "summary" ? summary : view === "graph" ? { ...summary, ...(0, summarize_js_1.diagramGraphView)(doc) } : { ...summary, ...(0, summarize_js_1.diagramFullView)(doc) };
258
+ const summaryWithFolder = { ...summary, folder: (0, lookups_js_1.diagramFolderPath)(await (0, lookups_js_1.diagramFolderRows)(ctx), resolved.item.folderId) };
259
+ const body = view === "summary"
260
+ ? summaryWithFolder
261
+ : view === "graph"
262
+ ? { ...summary, ...(0, summarize_js_1.diagramGraphView)(doc) }
263
+ : view === "mermaid"
264
+ ? { ...summary, ...(0, summarize_js_1.diagramMermaidView)(doc) }
265
+ : view === "source"
266
+ ? { ...summary, ...(0, summarize_js_1.diagramSourceView)(doc) }
267
+ : view === "schema"
268
+ ? { ...summary, schema: (0, callman_core_1.databaseSchemaView)(doc) }
269
+ : { ...summary, ...(0, summarize_js_1.diagramFullView)(doc) };
47
270
  return (0, shared_js_1.ok)({ diagram: body, ...(resolved.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved)] } : {}) });
48
271
  }));
49
272
  server.registerTool("create_diagram", {
50
- title: "Create a system-design diagram",
51
- description: "Create a diagram from typed nodes and edges. Node `type` must come from callman://diagram/components (e.g. net.service, net.api-gateway, db.postgres, mq.kafka, aws.lambda, flow.process); unknown types are rejected with suggestions unless strictTypes: false. Group nodes inside a container node — frame, pool.horizontal or pool.vertical — with `parent`. Style nodes with `style` (fill / stroke / textColor as CSS colors, strokeWidth, dashed, radius, bold, fontSize, opacity…) and edges with `semantic` (sync default, async, fallback, bus) plus an optional `color`. Layout is automatic when nodes have no positions. Read callman://docs/diagram-schema for every field. Keep it readable: 8–25 nodes, short labels.",
273
+ title: "Create a system-design, process or database diagram",
274
+ description: "Create a diagram from typed nodes and edges — or from Mermaid text / BPMN 2.0 XML / a database model (`source`). A BPMN process (`kind: \"bpmn\"`, or nodes typed bpmn.*) is stored as BPMN 2.0 XML the desktop opens in its BPMN editor. A DATABASE diagram (entity-relationship: `kind: \"database\"`, `tables` + `relationships`, nodes typed db.table/db.view/db.collection with the columns in data: { db: { columns } }, or `fromConnection` to read a real database's schema) is stored as the JSON database model the desktop opens on its ERD canvas — tables with columns and keys, relationships with cardinality, laid out left to right. Node `type` must come from callman://diagram/components (e.g. net.service, net.api-gateway, db.postgres, mq.kafka, aws.lambda, flow.process); unknown types are rejected with suggestions unless strictTypes: false. Group nodes inside a container node — frame, pool.horizontal or pool.vertical — with `parent`. Style nodes with `style` (fill / stroke / textColor as CSS colors, strokeWidth, dashed, radius, bold, fontSize, opacity…) and edges with `semantic` (sync default, async, fallback, bus), an optional `color`, `routing` and fixed `sourceSide`/`targetSide`. Layout is automatic when nodes have no positions. Read callman://docs/diagram-schema for every field. Keep it readable: 8–25 nodes, short labels.",
52
275
  inputSchema: {
53
276
  name: zod_1.z.string().min(1).max(200).describe("Diagram name (library entry)"),
54
- ...build_js_1.diagramInputSchema.shape,
277
+ title: build_js_1.diagramInputSchema.shape.title.optional().describe("Diagram title (defaults to the name)"),
278
+ nodes: build_js_1.diagramInputSchema.shape.nodes.optional(),
279
+ edges: build_js_1.diagramInputSchema.shape.edges.optional(),
280
+ canvas: build_js_1.diagramInputSchema.shape.canvas,
281
+ source: sourceArg,
282
+ kind: zod_1.z.enum(["canvas", "bpmn", "database"]).optional().describe('"bpmn": model a business process — nodes use bpmn.* types and the diagram is stored as BPMN 2.0 XML; "database": an entity-relationship diagram stored as the JSON database model (default: canvas; inferred from the node types, from `tables` and from `fromConnection`)'),
283
+ tables: zod_1.z.array(patch_js_1.dbTableInputSchema).max(500).optional().describe("Database diagram: the tables (name, schema?, kind?, columns with dataType / primaryKey / nullable / unique / defaultValue / comment)"),
284
+ relationships: zod_1.z.array(patch_js_1.dbRelationshipInputSchema).max(2000).optional().describe('Database diagram: relationships between the tables — source is the child (foreign-key) side as "table.column", target the parent; cardinality source:target (default N:1)'),
285
+ dialect: zod_1.z.enum(DATABASE_DIALECTS).optional().describe("Database diagram: which database the types are written for"),
286
+ fromConnection: zod_1.z
287
+ .object({
288
+ connection: zod_1.z.string().min(1).max(200).describe("A database connection by id or name (list_connections, type: database)"),
289
+ schemas: zod_1.z.array(zod_1.z.string().min(1).max(120)).max(50).optional().describe("Schemas to read (default: every user schema; MySQL: the connection's database; Oracle: the current schema)"),
290
+ tables: zod_1.z.array(zod_1.z.string().min(1).max(240)).max(2000).optional().describe('Only these tables ("schema.table" or "table"); default: all'),
291
+ includeViews: zod_1.z.boolean().optional(),
292
+ })
293
+ .strict()
294
+ .optional()
295
+ .describe("Database diagram: read a real database's schema through one of the workspace's connections and draw it — tables, columns, keys and the relationships the database declares"),
296
+ workspace: shared_js_1.workspaceArg,
297
+ folder: zod_1.z.string().max(300).optional().describe('File it in a folder of your library (id, name or path such as "Architecture/Payments"; create one with create_diagram_folder). Default: the top level'),
55
298
  autoLayout: zod_1.z.boolean().optional().describe("Force layout even when positions were given"),
56
299
  direction: directionArg,
57
300
  strictTypes: zod_1.z.boolean().optional(),
58
301
  dryRun: shared_js_1.dryRunArg,
59
302
  },
60
303
  annotations: shared_js_1.WRITE,
61
- }, (0, shared_js_1.guarded)(async ({ name, autoLayout, direction, strictTypes, dryRun, ...input }) => {
62
- const built = (0, build_js_1.buildDiagramDocument)(input, { strictTypes });
63
- let document = built.document;
304
+ }, (0, shared_js_1.guarded)(async ({ name, title, nodes, edges, canvas, source, kind, tables, relationships, dialect, fromConnection, workspace, folder, autoLayout, direction, strictTypes, dryRun }) => {
305
+ const targetFolder = folder ? await (0, lookups_js_1.resolveDiagramFolder)(ctx, folder) : null;
306
+ const inputs = [nodes ? "nodes" : null, source ? "source" : null, tables ? "tables" : null, fromConnection ? "fromConnection" : null].filter(Boolean);
307
+ if (inputs.length === 0)
308
+ throw new errors_js_1.ToolInputError("NOTHING_TO_CREATE", 'Pass nodes (+ edges), source { format: "mermaid" | "bpmn" | "database", text }, tables (+ relationships) or fromConnection.');
309
+ if (inputs.length > 1)
310
+ throw new errors_js_1.ToolInputError("CONFLICTING_INPUT", `Pass one of nodes, source, tables or fromConnection — not ${inputs.join(" + ")}.`);
311
+ if (relationships && !tables)
312
+ throw new errors_js_1.ToolInputError("CONFLICTING_INPUT", "relationships go with tables.");
313
+ let document;
64
314
  let layoutNote;
65
- if (autoLayout || built.unpositionedNodeIds.length > 0) {
66
- const laid = await (0, layout_js_1.layoutDocument)(document, { direction });
67
- document = laid.document;
68
- layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
315
+ let warnings = [];
316
+ const bpmnKind = kind === "bpmn" || (kind === undefined && Boolean(nodes && nodes.length > 0 && nodes.every((node) => node.type.startsWith("bpmn."))));
317
+ const databaseKind = kind === "database" || Boolean(tables) || Boolean(fromConnection) || (kind === undefined && Boolean(nodes && nodes.length > 0 && nodes.every((node) => (0, callman_core_1.isDatabaseType)(node.type))));
318
+ let connectionNote = {};
319
+ if (source) {
320
+ const fromSource = await documentFromSource(title ?? name, source.text, null, direction, source.format);
321
+ document = canvas ? { ...fromSource.document, canvas: { ...fromSource.document.canvas, ...canvas } } : fromSource.document;
322
+ layoutNote = fromSource.layoutNote;
323
+ warnings = fromSource.warnings;
324
+ }
325
+ else if (fromConnection) {
326
+ const found = await findDatabaseConnection(ctx, workspace, fromConnection.connection);
327
+ const schema = await ctx.api.getConnectionSchema(found.ws.id, found.connection.id, schemaQuery(fromConnection));
328
+ const model = (0, callman_core_1.databaseModelFromSchema)(schema, { connection: { connectionId: found.connection.id, name: found.connection.name, provider: found.connection.provider } });
329
+ const built = await databaseDocumentFromModel(model, (0, callman_core_1.createEmptyDocument)(title ?? name));
330
+ document = built.document;
331
+ layoutNote = built.layoutNote;
332
+ warnings = [...schema.warnings, ...(schema.truncated ? ["The database has more tables than the cap; the list was cut."] : [])];
333
+ connectionNote = { connection: { id: found.connection.id, name: found.connection.name, provider: PROVIDER_LABEL[found.connection.provider] ?? found.connection.provider, workspace: (0, lookups_js_1.workspaceTag)(found.ws) }, tables: model.entities.length, relationships: model.relationships.length };
334
+ }
335
+ else if (tables) {
336
+ const model = databaseModelFromInput(tables, relationships ?? [], dialect);
337
+ const built = await databaseDocumentFromModel(model, (0, callman_core_1.createEmptyDocument)(title ?? name));
338
+ document = built.document;
339
+ layoutNote = built.layoutNote;
340
+ }
341
+ else if (databaseKind) {
342
+ const built = (0, build_js_1.buildDiagramDocument)({ title: title ?? name, nodes: nodes ?? [], edges: edges ?? [], canvas }, { strictTypes, kind: "database" });
343
+ const model = (0, callman_core_1.databaseModelFromDocument)({ ...built.document, source: undefined }, dialect);
344
+ const database = await databaseDocumentFromModel(model, (0, callman_core_1.createEmptyDocument)(title ?? name));
345
+ document = database.document;
346
+ layoutNote = database.layoutNote;
347
+ warnings = built.warnings;
348
+ }
349
+ else if (bpmnKind) {
350
+ const built = (0, build_js_1.buildDiagramDocument)({ title: title ?? name, nodes: nodes ?? [], edges: edges ?? [], canvas }, { strictTypes, kind: "bpmn" });
351
+ const bpmn = bpmnDocumentFromModel(built.document, built.unpositionedNodeIds, (0, callman_core_1.createEmptyDocument)(title ?? name));
352
+ document = bpmn.document;
353
+ layoutNote = bpmn.layoutNote;
354
+ warnings = built.warnings;
69
355
  }
356
+ else {
357
+ const built = (0, build_js_1.buildDiagramDocument)({ title: title ?? name, nodes: nodes ?? [], edges: edges ?? [], canvas }, { strictTypes });
358
+ document = built.document;
359
+ warnings = built.warnings;
360
+ if (autoLayout || built.unpositionedNodeIds.length > 0) {
361
+ const laid = await (0, layout_js_1.layoutDocument)(document, { direction });
362
+ document = laid.document;
363
+ layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
364
+ }
365
+ }
366
+ if (document.source?.format === "database" && canvas && !source)
367
+ document = { ...document, canvas: { ...document.canvas, ...canvas } };
368
+ const view = document.source?.format === "database" ? { ...(0, summarize_js_1.diagramGraphView)(document), schema: (0, callman_core_1.databaseSchemaView)(document) } : (0, summarize_js_1.diagramGraphView)(document);
70
369
  if (dryRun) {
71
- return (0, shared_js_1.ok)({ dryRun: true, diagram: { name, ...(0, summarize_js_1.diagramGraphView)(document) }, warnings: built.warnings, ...(layoutNote ? { layout: layoutNote } : {}) });
370
+ return (0, shared_js_1.ok)({ dryRun: true, diagram: { name, folder: targetFolder?.item.path ?? null, ...view }, warnings, ...connectionNote, ...(layoutNote ? { layout: layoutNote } : {}) });
72
371
  }
73
- const created = await ctx.api.createDiagram({ name: name.trim(), document });
372
+ const created = await ctx.api.createDiagram({ name: name.trim(), document, ...(targetFolder ? { folderId: targetFolder.item.id } : {}) });
74
373
  ctx.invalidate("diagrams");
75
374
  return (0, shared_js_1.ok)({
76
- diagram: { ...(0, summarize_js_1.diagramListView)(created), ...(0, summarize_js_1.diagramGraphView)(document) },
77
- warnings: built.warnings,
375
+ diagram: { ...(0, summarize_js_1.diagramListView)(created), folder: targetFolder?.item.path ?? null, ...view },
376
+ warnings,
377
+ ...connectionNote,
78
378
  ...(layoutNote ? { layout: layoutNote } : {}),
79
- note: "Saved. Open it in the desktop app (Diagrams → Refresh). Edit with patch_diagram; share with share_diagram.",
379
+ ...(targetFolder?.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(folder, targetFolder)] } : {}),
380
+ note: `Saved${targetFolder ? ` in ${targetFolder.item.path}` : ""}. Open it in the desktop app (Diagrams → Refresh). Edit with patch_diagram; share with share_diagram.`,
80
381
  });
81
382
  }));
82
383
  server.registerTool("update_diagram", {
83
384
  title: "Replace a diagram's content",
84
- description: "Replace a diagram's whole document (nodes + edges, same fields as create_diagram) and/or rename it, retitle it or change its canvas, keeping its identity and sharing. Passing nodes replaces ALL nodes and edges (omitted edges are removed). For small edits — including restyling — prefer patch_diagram.",
385
+ description: "Replace a diagram's whole document (nodes + edges, same fields as create_diagram), replace or attach its Mermaid text, BPMN 2.0 XML or database-model `source`, rename it, retitle it, change its canvas and/or move it to a folder of your library (`folder`; null = the top level; only the owner can move) — keeping its identity and sharing. Passing nodes replaces ALL nodes and edges (omitted edges are removed). Passing `source` re-derives nodes and edges from the text (existing ones keep their place; new ones are placed). For small edits — including restyling — prefer patch_diagram.",
85
386
  inputSchema: {
86
387
  diagram: (0, shared_js_1.refArg)("Diagram"),
87
388
  name: zod_1.z.string().min(1).max(200).optional(),
@@ -89,15 +390,20 @@ const registerDiagramTools = (server, ctx) => {
89
390
  nodes: build_js_1.diagramInputSchema.shape.nodes.optional(),
90
391
  edges: build_js_1.diagramInputSchema.shape.edges.optional(),
91
392
  canvas: build_js_1.diagramInputSchema.shape.canvas,
393
+ source: sourceArg,
394
+ folder: zod_1.z.string().max(300).nullable().optional().describe('Move the diagram into this folder of your library (id, name or path); null = the top level'),
92
395
  autoLayout: zod_1.z.boolean().optional(),
93
396
  direction: directionArg,
94
397
  strictTypes: zod_1.z.boolean().optional(),
95
398
  },
96
399
  annotations: shared_js_1.IDEMPOTENT_WRITE,
97
- }, (0, shared_js_1.guarded)(async ({ diagram, name, title, nodes, edges, canvas, autoLayout, direction, strictTypes }) => {
98
- if (name === undefined && nodes === undefined && title === undefined && canvas === undefined) {
99
- throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass name, title, canvas and/or nodes (+ edges).");
400
+ }, (0, shared_js_1.guarded)(async ({ diagram, name, title, nodes, edges, canvas, source, folder, autoLayout, direction, strictTypes }) => {
401
+ if (name === undefined && nodes === undefined && title === undefined && canvas === undefined && source === undefined && folder === undefined) {
402
+ throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass name, title, canvas, source, folder and/or nodes (+ edges).");
100
403
  }
404
+ if (nodes && source)
405
+ throw new errors_js_1.ToolInputError("CONFLICTING_INPUT", "Pass either nodes or source, not both.");
406
+ const targetFolder = folder ? await (0, lookups_js_1.resolveDiagramFolder)(ctx, folder) : null;
101
407
  const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
102
408
  if (!resolved.item.canEdit)
103
409
  throw new errors_js_1.ToolInputError("DIAGRAM_READ_ONLY", "You have read-only access to this diagram.");
@@ -105,33 +411,79 @@ const registerDiagramTools = (server, ctx) => {
105
411
  let document;
106
412
  let layoutNote;
107
413
  let warnings = [];
108
- if (nodes) {
414
+ if (nodes && existing.source?.format === "database") {
415
+ // A database diagram stays one: the tables given become the new model.
416
+ const built = (0, build_js_1.buildDiagramDocument)({ title: title ?? existing.meta.title, nodes, edges: edges ?? [], canvas: canvas ?? existing.canvas }, { strictTypes, existingId: existing.id, createdAt: existing.meta.createdAt, kind: "database" });
417
+ const previous = (0, callman_core_1.parseDatabaseModel)(existing.source.text).model;
418
+ const model = (0, callman_core_1.databaseModelFromDocument)({ ...built.document, source: undefined }, previous.dialect);
419
+ if (previous.connection)
420
+ model.connection = previous.connection;
421
+ const database = await databaseDocumentFromModel(model, existing, true);
422
+ document = database.document;
423
+ layoutNote = database.layoutNote;
424
+ warnings = [...built.warnings, "The database model was rewritten from the tables given; tables not in the input are gone. Prefer patch_diagram (addTable, addColumn, addRelationship …) for edits."];
425
+ }
426
+ else if (nodes && existing.source?.format === "bpmn") {
427
+ // A BPMN diagram stays BPMN: the new model is written as fresh XML.
428
+ const built = (0, build_js_1.buildDiagramDocument)({ title: title ?? existing.meta.title, nodes, edges: edges ?? [], canvas: canvas ?? existing.canvas }, { strictTypes, existingId: existing.id, createdAt: existing.meta.createdAt, kind: "bpmn" });
429
+ const bpmn = bpmnDocumentFromModel(built.document, built.unpositionedNodeIds, existing);
430
+ document = bpmn.document;
431
+ layoutNote = bpmn.layoutNote;
432
+ warnings = [...built.warnings, "The BPMN XML was rewritten from the input; anything in the file that is not a flow node, flow, pool or lane was dropped. Prefer patch_diagram for edits."];
433
+ }
434
+ else if (nodes) {
435
+ assertEditableStructure(existing);
109
436
  const built = (0, build_js_1.buildDiagramDocument)({ title: title ?? existing.meta.title, nodes, edges: edges ?? [], canvas: canvas ?? existing.canvas }, { strictTypes, existingId: existing.id, createdAt: existing.meta.createdAt });
110
437
  document = built.document;
111
438
  warnings = built.warnings;
112
439
  if (edges === undefined && existing.edges.length > 0) {
113
440
  warnings.push(`edges was omitted, so the ${existing.edges.length} existing edge(s) were removed. Pass edges to keep connections, or use patch_diagram for partial edits.`);
114
441
  }
442
+ if (existing.source)
443
+ warnings.push(`The diagram's ${existing.source.format === "mermaid" ? "Mermaid" : existing.source.format} source was detached: nodes and edges were replaced from the input.`);
115
444
  if (autoLayout || built.unpositionedNodeIds.length > 0) {
116
445
  const laid = await (0, layout_js_1.layoutDocument)(document, { direction });
117
446
  document = laid.document;
118
447
  layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
119
448
  }
120
449
  }
450
+ else if (source) {
451
+ const fromSource = await documentFromSource(title ?? existing.meta.title, source.text, existing, direction, source.format);
452
+ document = fromSource.document;
453
+ layoutNote = fromSource.layoutNote;
454
+ warnings = fromSource.warnings;
455
+ if (title !== undefined || canvas !== undefined) {
456
+ document = {
457
+ ...document,
458
+ ...(canvas !== undefined ? { canvas: { ...document.canvas, ...canvas } } : {}),
459
+ meta: { ...document.meta, ...(title !== undefined ? { title } : {}) },
460
+ };
461
+ }
462
+ }
121
463
  else if (title !== undefined || canvas !== undefined) {
122
464
  document = {
123
465
  ...existing,
124
466
  ...(canvas !== undefined ? { canvas: { ...existing.canvas, ...canvas } } : {}),
125
- meta: { ...existing.meta, ...(title !== undefined ? { title } : {}), updatedAt: new Date().toISOString() },
467
+ meta: { ...existing.meta, ...(title !== undefined ? { title } : {}), updatedAt: now() },
126
468
  };
127
469
  }
128
- const updated = await ctx.api.updateDiagram(resolved.item.id, { ...(name !== undefined ? { name: name.trim() } : {}), ...(document ? { document } : {}) });
470
+ const updated = await ctx.api.updateDiagram(resolved.item.id, {
471
+ ...(name !== undefined ? { name: name.trim() } : {}),
472
+ ...(document ? { document, ...(typeof existing.meta.revision === "number" ? { baseRevision: existing.meta.revision } : {}) } : {}),
473
+ ...(folder !== undefined ? { folderId: targetFolder?.item.id ?? null } : {}),
474
+ });
129
475
  ctx.invalidate("diagrams");
130
- return (0, shared_js_1.ok)({ diagram: { ...(0, summarize_js_1.diagramListView)(updated), ...(0, summarize_js_1.diagramGraphView)(document ?? existing) }, warnings, ...(layoutNote ? { layout: layoutNote } : {}), resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved)] });
476
+ const rows = await (0, lookups_js_1.diagramFolderRows)(ctx);
477
+ return (0, shared_js_1.ok)({
478
+ diagram: { ...(0, summarize_js_1.diagramListView)(updated), folder: (0, lookups_js_1.diagramFolderPath)(rows, updated.folderId), ...(0, summarize_js_1.diagramGraphView)(document ?? existing) },
479
+ warnings,
480
+ ...(layoutNote ? { layout: layoutNote } : {}),
481
+ resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved), ...(targetFolder?.annotation ? [(0, resolve_js_1.resolvedSummary)(folder, targetFolder)] : [])],
482
+ });
131
483
  }));
132
484
  server.registerTool("patch_diagram", {
133
485
  title: "Edit a diagram incrementally",
134
- description: "Apply operations to a diagram without resending it: addNode, updateNode, removeNode (drops its edges), addEdge, updateEdge, removeEdge, setTitle. updateNode `changes` MERGE: style and data keys you pass change, a key set to null is removed. updateEdge can change semantic, color, label, arrows and ends. Moving a node (position/size/parent) re-routes its edges. `relayout`: none | new (place only new nodes, default) | all (re-run automatic layout). Call get_diagram (view: graph) first to see node ids and current styles.",
486
+ description: "Apply operations to a diagram without resending it: addNode, updateNode, removeNode (drops its edges, lifts its children out), addEdge, updateEdge, removeEdge, setTitle, moveNode, reparent, setEdgeRoute (routing, fixed sides, bends), reorder (stacking), setCanvas, group (wrap nodes in a container), ungroup, relayout (a hint: new | all). On a DATABASE diagram use the schema ops — addTable, renameTable, removeTable, setTableComment, addColumn, alterColumn, removeColumn, setPrimaryKey, addRelationship (source = child \"table.column\", target = parent), updateRelationship, removeRelationship, hideTable, showTable — read get_diagram view schema first. updateNode `changes` MERGE: style and data keys you pass change, a key set to null is removed. updateEdge can change semantic, color, label, arrows, sides and ends. Moving a node re-routes its edges. `relayout`: none | new (place only new nodes, default) | all (re-run automatic layout). Call get_diagram (view: graph) first to see node ids and current styles. Ops are documented in callman://docs/diagram-schema.",
135
487
  inputSchema: {
136
488
  diagram: (0, shared_js_1.refArg)("Diagram"),
137
489
  ops: zod_1.z.array(patch_js_1.diagramPatchOpSchema).min(1).max(200),
@@ -144,33 +496,169 @@ const registerDiagramTools = (server, ctx) => {
144
496
  const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
145
497
  if (!resolved.item.canEdit)
146
498
  throw new errors_js_1.ToolInputError("DIAGRAM_READ_ONLY", "You have read-only access to this diagram.");
147
- const existing = loadDocument(resolved.item);
148
- const patched = (0, patch_js_1.applyDiagramPatch)(existing, ops, { strictTypes });
149
- let document = patched.document;
150
- let layoutNote;
151
- if (relayout === "all") {
152
- const laid = await (0, layout_js_1.layoutDocument)(document, { direction });
153
- document = laid.document;
154
- layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
499
+ // Apply on the freshest copy; a 409 (someone saved meanwhile) reloads
500
+ // and reapplies once — ops describe intent, so they replay cleanly.
501
+ const attempt = async (record) => {
502
+ const existing = loadDocument(record);
503
+ assertEditableStructure(existing);
504
+ const patched = (0, patch_js_1.applyDiagramPatch)(existing, ops, { strictTypes });
505
+ let document = patched.document;
506
+ let layoutNote;
507
+ const wantsAll = relayout === "all" || patched.relayout?.scope === "all";
508
+ const layoutDirection = direction ?? patched.relayout?.direction;
509
+ const bpmn = document.source?.format === "bpmn";
510
+ const database = document.source?.format === "database";
511
+ if (wantsAll) {
512
+ // A process reads left to right; the BPMN XML follows the moves. An
513
+ // ERD packs unrelated tables into rows.
514
+ const laid = await (0, layout_js_1.layoutDocument)(document, { direction: layoutDirection ?? (bpmn || database ? "RIGHT" : undefined), ...(database ? { request: callman_core_1.DATABASE_LAYOUT_REQUEST } : {}) });
515
+ document = laid.document;
516
+ layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
517
+ }
518
+ else if (relayout === "new" && patched.addedNodeIds.length > 0) {
519
+ const ids = patched.addedNodeIds.filter((id) => document.nodes.some((node) => node.id === id));
520
+ if (database) {
521
+ document = (0, layout_js_1.placeDatabaseNodes)(document, ids);
522
+ layoutNote = `Placed ${ids.length} new table(s) to the right of the existing ones.`;
523
+ }
524
+ else if (bpmn) {
525
+ document = (0, callman_core_1.applyDiagramOps)(document, (0, callman_core_1.placeBpmnNodesOps)(document, ids), { now: now() }).doc;
526
+ layoutNote = `Placed ${ids.length} new node(s) after their predecessors.`;
527
+ }
528
+ else {
529
+ document = (0, layout_js_1.placeNewNodes)(document, ids);
530
+ layoutNote = `Placed ${ids.length} new node(s) below the existing ones.`;
531
+ }
532
+ }
533
+ const updated = await ctx.api.updateDiagram(record.id, {
534
+ document,
535
+ ...(typeof existing.meta.revision === "number" ? { baseRevision: existing.meta.revision } : {}),
536
+ });
537
+ return { updated, document, patched, layoutNote };
538
+ };
539
+ let outcome;
540
+ let retried = false;
541
+ try {
542
+ outcome = await attempt(resolved.item);
155
543
  }
156
- else if (relayout === "new" && patched.addedNodeIds.length > 0) {
157
- document = (0, layout_js_1.placeNewNodes)(document, patched.addedNodeIds);
158
- layoutNote = `Placed ${patched.addedNodeIds.length} new node(s) below the existing ones.`;
544
+ catch (error) {
545
+ if (!isConflict(error))
546
+ throw error;
547
+ retried = true;
548
+ outcome = await attempt(await ctx.api.getDiagram(resolved.item.id));
159
549
  }
160
- const updated = await ctx.api.updateDiagram(resolved.item.id, { document });
161
550
  ctx.invalidate("diagrams");
551
+ const { updated, document, patched, layoutNote } = outcome;
162
552
  return (0, shared_js_1.ok)({
163
- diagram: { ...(0, summarize_js_1.diagramListView)(updated), ...(0, summarize_js_1.diagramGraphView)(document) },
553
+ diagram: { ...(0, summarize_js_1.diagramListView)(updated), ...(0, summarize_js_1.diagramGraphView)(document), ...(document.source?.format === "database" ? { schema: (0, callman_core_1.databaseSchemaView)(document) } : {}) },
164
554
  applied: ops.length,
165
555
  added: patched.addedNodeIds,
166
556
  removedNodes: patched.removedNodeIds,
167
557
  removedEdges: patched.removedEdgeIds,
168
558
  ...(patched.reroutedEdgeIds.length > 0 ? { reroutedEdges: patched.reroutedEdgeIds } : {}),
169
- warnings: patched.warnings,
559
+ ...(Object.keys(patched.idMap).length > 0 ? { idMap: patched.idMap } : {}),
560
+ warnings: [...patched.warnings, ...(retried ? ["The diagram had been saved by someone else meanwhile; your ops were re-applied on the newer version."] : [])],
170
561
  ...(layoutNote ? { layout: layoutNote } : {}),
171
562
  resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved)],
172
563
  });
173
564
  }));
565
+ server.registerTool("introspect_database", {
566
+ title: "Read a database's schema through a connection",
567
+ description: "Read the database behind one of the workspace's database connections (list_connections, type: database): schemas, tables and views (MongoDB: collections with fields inferred from a sample), columns with types, nullability, defaults and comments, primary and unique keys, foreign keys with referential actions, indexes. Metadata only — never credentials, never data. Use `objectsOnly` to list names first, then read the tables you need; `create_diagram` with `fromConnection` draws the result as a Database Diagram in one step. Runs on the Callman backend, which must be able to reach the database.",
568
+ inputSchema: {
569
+ connection: zod_1.z.string().min(1).max(200).describe("Connection id or name"),
570
+ workspace: shared_js_1.workspaceArg,
571
+ schemas: zod_1.z.array(zod_1.z.string().min(1).max(120)).max(50).optional(),
572
+ tables: zod_1.z.array(zod_1.z.string().min(1).max(240)).max(2000).optional().describe('"schema.table" or "table"'),
573
+ includeViews: zod_1.z.boolean().optional(),
574
+ objectsOnly: zod_1.z.boolean().optional().describe("Names only (no columns / keys) — for a first look at a large database"),
575
+ maxTables: zod_1.z.number().int().min(1).max(20000).optional(),
576
+ },
577
+ annotations: shared_js_1.READ,
578
+ }, (0, shared_js_1.guarded)(async ({ connection, workspace, schemas, tables, includeViews, objectsOnly, maxTables }) => {
579
+ const found = await findDatabaseConnection(ctx, workspace, connection);
580
+ const schema = await ctx.api.getConnectionSchema(found.ws.id, found.connection.id, schemaQuery({ schemas, tables, includeViews, objectsOnly, maxTables }));
581
+ return (0, shared_js_1.ok)({
582
+ connection: { id: found.connection.id, name: found.connection.name, provider: PROVIDER_LABEL[found.connection.provider] ?? found.connection.provider, workspace: (0, lookups_js_1.workspaceTag)(found.ws) },
583
+ schema,
584
+ ...(schema.truncated ? { note: "The database has more tables than the cap; pass schemas/tables to narrow the read or raise maxTables." } : {}),
585
+ });
586
+ }));
587
+ // ── library folders (private to the token's owner) ──────────────────
588
+ const folderView = (row) => ({ id: row.id, name: row.name, parentId: row.parentId, path: row.path });
589
+ server.registerTool("create_diagram_folder", {
590
+ title: "Create a diagram folder",
591
+ description: "Create a folder in your diagram library, optionally inside a parent folder (id, name or path). Folders are yours alone (nobody else sees them; a diagram you share shows at the top level for others), nest up to 8 levels, and sibling names are unique — an existing folder with that name is returned instead of a duplicate. File diagrams with create_diagram / update_diagram `folder`.",
592
+ inputSchema: {
593
+ name: zod_1.z.string().min(1).max(120),
594
+ parent: zod_1.z.string().max(300).optional().describe("Parent folder id, name or path; omit for the top level"),
595
+ },
596
+ annotations: shared_js_1.WRITE,
597
+ }, (0, shared_js_1.guarded)(async ({ name, parent }) => {
598
+ const parentFolder = parent ? await (0, lookups_js_1.resolveDiagramFolder)(ctx, parent) : null;
599
+ const rows = await (0, lookups_js_1.diagramFolderRows)(ctx);
600
+ const trimmed = name.trim();
601
+ const sibling = rows.find((row) => row.parentId === (parentFolder?.item.id ?? null) && (0, ids_js_1.normalizeName)(row.name) === (0, ids_js_1.normalizeName)(trimmed));
602
+ if (sibling) {
603
+ return (0, shared_js_1.ok)({ folder: folderView(sibling), existed: true, note: "A folder with that name already exists there — returned instead of creating a duplicate." });
604
+ }
605
+ const created = await ctx.api.createDiagramFolder({ name: trimmed, parentId: parentFolder?.item.id ?? null });
606
+ ctx.invalidate("diagram-folders");
607
+ const path = parentFolder ? `${parentFolder.item.path}/${created.name}` : created.name;
608
+ return (0, shared_js_1.ok)({
609
+ folder: { id: created.id, name: created.name, parentId: created.parentId, path },
610
+ ...(parentFolder?.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(parent, parentFolder)] } : {}),
611
+ });
612
+ }));
613
+ server.registerTool("update_diagram_folder", {
614
+ title: "Rename or move a diagram folder",
615
+ description: "Rename a folder of your diagram library and/or move it under another folder (parent: null = the top level). A folder cannot be moved into itself or one of its subfolders.",
616
+ inputSchema: {
617
+ folder: (0, shared_js_1.refArg)("Diagram folder", "(or path)"),
618
+ name: zod_1.z.string().min(1).max(120).optional(),
619
+ parent: zod_1.z.string().max(300).nullable().optional().describe("New parent folder (id, name or path); null = the top level"),
620
+ },
621
+ annotations: shared_js_1.IDEMPOTENT_WRITE,
622
+ }, (0, shared_js_1.guarded)(async ({ folder, name, parent }) => {
623
+ if (name === undefined && parent === undefined)
624
+ throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass name and/or parent.");
625
+ const target = await (0, lookups_js_1.resolveDiagramFolder)(ctx, folder);
626
+ const parentFolder = parent ? await (0, lookups_js_1.resolveDiagramFolder)(ctx, parent) : null;
627
+ if (parentFolder && (parentFolder.item.id === target.item.id || parentFolder.item.path.startsWith(`${target.item.path}/`))) {
628
+ throw new errors_js_1.ToolInputError("FOLDER_CYCLE", "A folder cannot be moved into itself or one of its subfolders.");
629
+ }
630
+ const updated = await ctx.api.updateDiagramFolder(target.item.id, {
631
+ ...(name !== undefined ? { name: name.trim() } : {}),
632
+ ...(parent !== undefined ? { parentId: parentFolder?.item.id ?? null } : {}),
633
+ });
634
+ ctx.invalidate("diagram-folders");
635
+ const rows = await (0, lookups_js_1.diagramFolderRows)(ctx);
636
+ const row = rows.find((entry) => entry.id === updated.id);
637
+ return (0, shared_js_1.ok)({
638
+ folder: row ? folderView(row) : { id: updated.id, name: updated.name, parentId: updated.parentId, path: updated.name },
639
+ resolved: [(0, resolve_js_1.resolvedSummary)(folder, target), ...(parentFolder?.annotation ? [(0, resolve_js_1.resolvedSummary)(parent, parentFolder)] : [])],
640
+ });
641
+ }));
642
+ server.registerTool("delete_diagram_folder", {
643
+ title: "Delete a diagram folder",
644
+ description: "Delete a folder of your diagram library and its subfolders. No diagram is deleted: everything inside moves to the deleted folder's parent. Confirm with the user first.",
645
+ inputSchema: { folder: (0, shared_js_1.refArg)("Diagram folder", "(or path)"), confirm: shared_js_1.confirmArg },
646
+ annotations: shared_js_1.DESTRUCTIVE,
647
+ }, (0, shared_js_1.guarded)(async ({ folder, confirm }) => {
648
+ (0, shared_js_1.requireConfirm)(confirm, "a diagram folder");
649
+ const target = await (0, lookups_js_1.resolveDiagramFolder)(ctx, folder, { exact: true });
650
+ const result = await ctx.api.deleteDiagramFolder(target.item.id);
651
+ ctx.invalidate("diagram-folders");
652
+ ctx.invalidate("diagrams");
653
+ const rows = await (0, lookups_js_1.diagramFolderRows)(ctx);
654
+ return (0, shared_js_1.ok)({
655
+ deleted: folderView(target.item),
656
+ deletedFolderCount: result.deletedFolderIds.length,
657
+ movedDiagramCount: result.movedDiagramCount,
658
+ movedTo: (0, lookups_js_1.diagramFolderPath)(rows, result.movedToFolderId) ?? "the top level",
659
+ resolved: [(0, resolve_js_1.resolvedSummary)(folder, target)],
660
+ });
661
+ }));
174
662
  server.registerTool("delete_diagram", {
175
663
  title: "Delete a diagram",
176
664
  description: "Delete a diagram you own. Permanent — confirm with the user first.",