callman-mcp 0.2.0 → 0.3.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 (47) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +2 -2
  3. package/dist/client/api.d.ts +1 -0
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/api.js +2 -0
  6. package/dist/client/api.js.map +1 -1
  7. package/dist/domain/diagram/build.d.ts +112 -63
  8. package/dist/domain/diagram/build.d.ts.map +1 -1
  9. package/dist/domain/diagram/build.js +49 -165
  10. package/dist/domain/diagram/build.js.map +1 -1
  11. package/dist/domain/diagram/layout.d.ts +3 -4
  12. package/dist/domain/diagram/layout.d.ts.map +1 -1
  13. package/dist/domain/diagram/layout.js +12 -45
  14. package/dist/domain/diagram/layout.js.map +1 -1
  15. package/dist/domain/diagram/patch.d.ts +298 -80
  16. package/dist/domain/diagram/patch.d.ts.map +1 -1
  17. package/dist/domain/diagram/patch.js +60 -273
  18. package/dist/domain/diagram/patch.js.map +1 -1
  19. package/dist/domain/diagram/style.d.ts +4 -12
  20. package/dist/domain/diagram/style.d.ts.map +1 -1
  21. package/dist/domain/diagram/style.js +8 -84
  22. package/dist/domain/diagram/style.js.map +1 -1
  23. package/dist/domain/diagram/summarize.d.ts +25 -24
  24. package/dist/domain/diagram/summarize.d.ts.map +1 -1
  25. package/dist/domain/diagram/summarize.js +20 -35
  26. package/dist/domain/diagram/summarize.js.map +1 -1
  27. package/dist/domain/requestIntegrations.d.ts +20 -20
  28. package/dist/domain/requestShape.d.ts +32 -32
  29. package/dist/resources/docs/diagramSchema.d.ts +1 -1
  30. package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
  31. package/dist/resources/docs/diagramSchema.js +55 -10
  32. package/dist/resources/docs/diagramSchema.js.map +1 -1
  33. package/dist/resources/generated/aiPacks.d.ts +1 -1
  34. package/dist/resources/generated/aiPacks.js +2 -2
  35. package/dist/tools/diagrams.d.ts.map +1 -1
  36. package/dist/tools/diagrams.js +244 -42
  37. package/dist/tools/diagrams.js.map +1 -1
  38. package/dist-cjs/client/api.js +2 -0
  39. package/dist-cjs/domain/diagram/build.js +53 -169
  40. package/dist-cjs/domain/diagram/layout.js +11 -44
  41. package/dist-cjs/domain/diagram/patch.js +57 -270
  42. package/dist-cjs/domain/diagram/style.js +11 -88
  43. package/dist-cjs/domain/diagram/summarize.js +22 -35
  44. package/dist-cjs/resources/docs/diagramSchema.js +55 -10
  45. package/dist-cjs/resources/generated/aiPacks.js +2 -2
  46. package/dist-cjs/tools/diagrams.js +240 -38
  47. package/package.json +2 -2
@@ -92,39 +92,84 @@ asks for a specific arrangement. Keep diagrams readable: 8–25 nodes, short lab
92
92
  per relationship.
93
93
 
94
94
  ## Editing: patch_diagram ops
95
+ Ops describe CHANGES; the rest of the diagram stays exactly as it is. The op list below is the
96
+ same vocabulary the desktop's AI assistant uses — one implementation (callman-core) applies both.
95
97
  \`\`\`jsonc
96
98
  { "op": "addNode", "node": { "type": "db.redis", "label": "cache", "parent": "k8s" } }
97
99
  { "op": "updateNode", "id": "orders", "changes": { "label": "Orders API", "style": { "fill": "#EFF6FF" } } }
98
100
  { "op": "updateNode", "id": "orders", "changes": { "style": { "fill": null } } } // remove one style key
99
101
  { "op": "updateNode", "id": "orders", "changes": { "style": null } } // clear the style
100
102
  { "op": "removeNode", "id": "bus" } // its edges are removed, its children move to the top level
101
- { "op": "addEdge", "edge": { "source": "orders", "target": "n6", "semantic": "async", "color": "#F59E0B" } }
103
+ { "op": "addEdge", "edge": { "source": "orders", "target": "cache", "semantic": "async", "color": "#F59E0B" } }
102
104
  { "op": "updateEdge", "id": "e2", "changes": { "label": "gRPC", "semantic": "fallback" } } // color is kept
103
105
  { "op": "updateEdge", "id": "e2", "changes": { "color": null } } // back to the default color
104
106
  { "op": "removeEdge", "id": "e3" }
105
107
  { "op": "setTitle", "title": "New title" }
108
+ { "op": "moveNode", "id": "orders", "position": { "x": 320, "y": 80 } }
109
+ { "op": "reparent", "id": "orders", "parent": "k8s" } // null = top level
110
+ { "op": "setEdgeRoute", "id": "e2", "routing": "smart", "sourceSide": "right", "targetSide": "left" }
111
+ { "op": "reorder", "id": "k8s", "index": "back" }
112
+ { "op": "setCanvas", "canvas": { "background": "grid", "defaultRouting": "orthogonal" } }
113
+ { "op": "group", "ids": ["orders", "db"], "label": "Order domain" } // a new frame around them
114
+ { "op": "ungroup", "id": "k8s" } // members stay where they are
115
+ { "op": "relayout", "scope": "new" } // hint: place only the new nodes (or "all")
106
116
  \`\`\`
107
117
  - updateNode \`changes\` accepts: type, label, parent (null/"" = top level), position, size,
108
- style, rotation, locked, hidden, data. \`style\` and \`data\` MERGE into what the node has;
109
- a key set to null is removed.
118
+ style, rotation, locked, hidden, collapsed, data. \`style\` and \`data\` MERGE into what the
119
+ node has; a key set to null is removed.
110
120
  - updateEdge \`changes\` accepts: source, target, label ("" removes it), semantic, color,
111
- routing, markerEnd, markerStart.
121
+ routing, markerEnd, markerStart, sourceSide, targetSide (null = floating).
122
+ - Ids: a node written without an id gets a slug of its label (\`orders-service\`, \`-2\` on a
123
+ clash); the result's \`idMap\` reports any id that had to change.
112
124
  - Moving a node (position, size or parent) or re-pointing an edge drops the saved route of
113
125
  the edges involved; the studio re-routes them (\`reroutedEdges\` in the result).
114
126
  - \`relayout\`: \`new\` (default — only new nodes get a place), \`all\` (re-run layout), \`none\`.
115
127
  - \`get_diagram\` (view \`graph\`) returns nodes and edges in this same shape — ids, parents,
116
- styles, colors — so read it before a patch.
128
+ styles, colors, routing, sides — so read it before a patch.
129
+ - If someone saved the diagram between your read and your patch, the ops are re-applied on
130
+ the newer version automatically (they describe intent, not a whole document).
131
+
132
+ ## Mermaid
133
+ - \`get_diagram\` view \`mermaid\` renders any diagram as a Mermaid flowchart (\`id@{ shape, label }\`
134
+ nodes, \`subgraph\` containers, \`e1@-->\` edge ids, \`-->\`/\`-.->\`/\`==>\` for sync/async/bus).
135
+ - \`create_diagram\` / \`update_diagram\` accept \`source: { format: "mermaid", text }\` instead of
136
+ nodes: a flowchart/graph becomes nodes and edges (laid out automatically) and the text stays
137
+ attached as the diagram's source — later ops keep it in sync (the desktop shows both).
138
+ Bracket shapes map to component types (\`[( )]\` → database, \`{ }\` → decision, \`([ ])\` →
139
+ terminator …); \`click\`, \`%%\` comments and unknown statements are kept untouched.
140
+ - Any other Mermaid type (sequenceDiagram, classDiagram, erDiagram …) is stored as source only:
141
+ \`get_diagram\` view \`source\` returns it, the desktop previews it, and structural ops are
142
+ refused with \`DIAGRAM_SOURCE_UNSUPPORTED_TYPE\`.
143
+
144
+ ## BPMN
145
+ - A BPMN diagram keeps its truth in BPMN 2.0 XML: \`create_diagram\` with \`kind: "bpmn"\` (or nodes
146
+ typed bpmn.*) writes the XML from your nodes and edges; \`source: { format: "bpmn", text }\` stores a
147
+ file you already have (from Camunda Modeler, for example). The desktop opens it in its BPMN editor.
148
+ - Node \`type\` is one of the bpmn.* types listed under DIAGRAM OPS above (bpmn.start-event,
149
+ bpmn.user-task, bpmn.service-task, bpmn.exclusive-gateway, bpmn.parallel-gateway, bpmn.subprocess,
150
+ bpmn.pool, bpmn.lane, bpmn.boundary-event …). \`data.bpmn\` carries the BPMN facts:
151
+ \`eventDefinition\` (message | timer | error | signal | …), \`attachedTo\` (boundary events),
152
+ \`cancelActivity\`, \`isExpanded\`. Edges are sequence flows; \`data.bpmn\` may set
153
+ \`flow: "message"\` (between pools), \`conditionExpression\` and \`isDefault\`.
154
+ - Pools and lanes are containers (\`parent\`); a boundary event's parent is its host's parent.
155
+ New nodes need no position: they are placed after their predecessor.
156
+ - \`patch_diagram\` works as on any diagram: every op lands on the XML as a minimal edit — extension
157
+ elements, annotations and the modeler's formatting stay untouched. \`get_diagram\` view \`source\`
158
+ returns the XML; view \`graph\` shows the derived nodes/edges with their \`bpmn\` facts.
117
159
 
118
160
  ## Stored document (get_diagram view: "full")
119
161
  \`\`\`ts
120
162
  interface DiagramDocument {
121
163
  schemaVersion: 1; id: string;
122
- meta: { title: string; createdAt: string; updatedAt: string };
123
- canvas?: { background?: "grid" | "dots" | "blank"; gridSize?: number };
164
+ meta: { title: string; createdAt: string; updatedAt: string; revision?: number };
165
+ canvas?: { background?: "grid" | "dots" | "blank"; gridSize?: number;
166
+ defaultRouting?: "orthogonal" | "straight" | "curved" | "smart";
167
+ layout?: { algorithm; direction }; viewport?: { x; y; zoom } };
124
168
  nodes: { id; type; position: {x,y}; size: {width,height}; label?; parentId?; rotation?;
125
- locked?; hidden?; style?: DiagramStyle; data? }[];
126
- edges: { id; source?; target?; label?; routing?; style?: DiagramStyle; markerStart?; markerEnd?;
127
- waypoints?; sourceAnchor?; targetAnchor? }[];
169
+ locked?; hidden?; collapsed?; collapsedSize?; style?: DiagramStyle; data? }[];
170
+ edges: { id; source?; target?; label?; labelPosition?; routing?; sourceHandle?; targetHandle?;
171
+ style?: DiagramStyle; markerStart?; markerEnd?; waypoints? }[];
172
+ source?: { format: "mermaid"; text; diagramType; grammar?; valid? };
128
173
  }
129
174
  // Stored colors are { raw: "#3B82F6" } (or a theme { token }); bold/italic/underline are
130
175
  // the bitmask fontStyle (1 | 2 | 4). The tools convert both ways — write plain colors.
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AI_PACKS = exports.AI_PACKS_CORE_VERSION = void 0;
4
- // GENERATED by scripts/build-static-resources.mjs from callman-core@1.28.1 ai-packs — do not edit.
5
- exports.AI_PACKS_CORE_VERSION = "1.28.1";
4
+ // GENERATED by scripts/build-static-resources.mjs from callman-core@1.29.0 ai-packs — do not edit.
5
+ exports.AI_PACKS_CORE_VERSION = "1.29.0";
6
6
  exports.AI_PACKS = {
7
7
  "db-queries": "<!-- GENERATED FILE — do not edit by hand. Source: callman_core (npm run ai:packs). -->\n\n# Callman data access from scripts (`pm.db.query` / `pm.redis.command`)\n\n`pm.db.query({ connectionId, query })` — ALWAYS `await`. `query` is coerced to a string, `{{tokens}}` are resolved, then dispatched by the connection's type. `connectionId` may be omitted to use the request's configured DB connection. IMPORTANT: `pm.db.query` does NOT accept a `params`/bind array — inline values (quote strings) or use `{{env.X}}` tokens; SQL placeholders like `$1` or `?` are NOT bound.\n\n## MongoDB\n`query` MUST be a JSON STRING of a command object (use `JSON.stringify`), NOT a `db.coll.find(...)` shell string. The match key is `filter` (a top-level `query` key is rejected).\n\n```js\nconst body = pm.response.json();\nconst res = await pm.db.query({\n connectionId: \"…\",\n query: JSON.stringify({\n collection: \"users\",\n operation: \"findOne\", // find | findOne | aggregate | countDocuments | insertOne | insertMany | updateOne | updateMany | deleteOne | deleteMany | findOneAndUpdate | findOneAndReplace | findOneAndDelete\n filter: { email: body.data.user.email },\n }),\n});\npm.expect(res.rowCount).to.be.above(0); // existence check\n```\n\nPer-operation fields: `find`/`findOne` use `filter` (+ optional `projection`,`sort`,`limit`,`skip`); `aggregate` uses `pipeline` (array); `insertOne` uses `document`, `insertMany` uses `documents`; `updateOne`/`updateMany`/`findOneAndUpdate` use `filter`+`update` (Mongo update operators, e.g. `{ $set: { … } }`); `deleteOne`/`deleteMany` use `filter`; `findOneAndUpdate`/`findOneAndReplace` accept `returnDocument: \"before\"|\"after\"`. A 24-hex string in a filter is auto-coerced to ObjectId (or use `{ \"$oid\": \"…\" }`). Rows are in `res.rows`; write ops surface counts in `res.rowCount`.\n\n## PostgreSQL / MySQL / Oracle (SQL)\n`query` is plain SQL text. Because `pm.db.query` binds no params, inline the value and quote strings yourself (or use a `{{token}}`):\n\n```js\nconst email = pm.response.json().data.user.email;\nconst res = await pm.db.query({\n connectionId: \"…\",\n query: `SELECT id FROM users WHERE email = '${email}'`,\n});\npm.expect(res.rowCount).to.be.above(0);\n```\n\nNotes: a trailing `;` is stripped (except Oracle PL/SQL blocks, which keep it). **Oracle** returns column names UPPERCASE — read `res.rows[0].EMAIL`, not `.email`. `res.rowCount` is the row count for SELECT and the affected-row count for INSERT/UPDATE/DELETE.\n\n## SQL Server (mssql)\nSingle statement per query; a trailing `;` is stripped; `GO` batch separators are REJECTED (client-tool syntax, not T-SQL). Positional params bind as named inputs: `params[0]` → `@p1`, `params[1]` → `@p2`… (scenario db node only — `pm.db.query` has no params).\n\n```sql\nSELECT TOP 1 id, amount FROM dbo.orders WHERE customer_id = @p1 ORDER BY created_at DESC\n```\n\n## Redis (`pm.redis.command`)\n`command` is a single string. Supported commands: **GET, SET, DEL, EXISTS, TTL, HGET, HSET, HGETALL, HDEL, LPUSH, RPUSH, LRANGE, LLEN, SADD, SMEMBERS, SISMEMBER, INCR, DECR, INCRBY, EXPIRE, PERSIST, TYPE, ZADD, ZRANGE, ZSCORE, SCAN**. Quote a value that contains spaces.\n\nStructured replies land in `res.result` as real values: `HGETALL` → object, `LRANGE`/`SMEMBERS`/`ZRANGE` → array, `EXISTS`/`SISMEMBER`/`EXPIRE`/`PERSIST` → boolean; `res.resultText` is the stringified form. `SCAN pattern [count]` is Callman-managed: the cursor loop runs internally (capped at 1000 keys / 100 iterations) and returns `{ keys, count, truncated }` — no raw cursors.\n\n```js\nconst res = await pm.redis.command({ connectionId: \"…\", command: `GET session:${id}` });\npm.expect(res.success).to.be.true;\n// res.result / res.resultText hold the value; res.commandName, res.key are set.\n```\n",
8
8
  "diagram-components": "<!-- GENERATED FILE — do not edit by hand. Source: callman_core (npm run ai:packs). -->\n\n# Diagram components and edge notation\n\nA diagram node's `type` MUST be one of the keys below (`callman-core` `DIAGRAM_COMPONENT_CATALOG`). Sizes are the defaults the studio uses; `container` types hold children via `parentId`.\n\n## Edge notation\n\n| Semantic | Meaning | Style tuple |\n| --- | --- | --- |\n| `sync` | Request / response call | dashed=false, strokeWidth=1.5 |\n| `async` | Fire-and-forget, event or callback | dashed=true, dashPattern=\"6 4\", strokeWidth=1.5 |\n| `fallback` | Retry, fallback or optional path | dashed=true, dashPattern=\"1 3\", strokeWidth=1.25 |\n| `bus` | Message bus or stream | dashed=false, strokeWidth=3 |\n\n## Generic (`generic`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `line` | Line | 160×0 | connector, edge, link |\n| `arrow` | Arrow | 160×0 | connector, directed, link |\n| `line.dashed` | Dashed Line | 160×0 | dotted, connector |\n| `rect` | Rectangle | 140×64 | box, card |\n| `square` | Square | 90×90 | |\n| `ellipse` | Ellipse | 140×64 | oval |\n| `circle` | Circle | 90×90 | |\n| `diamond` | Diamond | 140×64 | rhombus |\n| `triangle` | Triangle | 140×64 | |\n| `hexagon` | Hexagon | 140×64 | |\n| `pentagon` | Pentagon | 140×64 | |\n| `octagon` | Octagon | 140×64 | |\n| `parallelogram` | Parallelogram | 140×64 | |\n| `trapezoid` | Trapezoid | 140×64 | |\n| `cylinder` | Cylinder | 140×64 | can, tube |\n| `cloud-shape` | Cloud | 140×64 | |\n| `callout` | Callout | 140×64 | speech, bubble |\n| `text` | Text | 120×36 | |\n| `sticky` | Sticky note | 140×100 | postit, comment |\n| `card` | Card | 140×64 | |\n| `frame` | Frame / Container (container) | 220×160 | group, container, boundary |\n| `pool.horizontal` | Pool (horizontal) (swimlane) | 360×220 | swimlane, lane, container, subsystem |\n| `pool.vertical` | Pool (vertical) (swimlane) | 220×360 | swimlane, lane, container, subsystem |\n| `image` | Image | 140×64 | picture |\n\n## Flowchart (`flowchart`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `flow.process` | Process | 140×64 | step, action |\n| `flow.alt-process` | Alternate Process | 140×64 | |\n| `flow.decision` | Decision | 140×100 | if, branch |\n| `flow.terminator` | Start / End | 130×56 | terminator, begin, stop |\n| `flow.data` | Data (I/O) | 140×64 | input, output, io |\n| `flow.document` | Document | 140×64 | |\n| `flow.multi-document` | Multi-Document | 140×64 | |\n| `flow.predefined` | Predefined Process | 140×64 | subroutine |\n| `flow.manual-input` | Manual Input | 140×64 | |\n| `flow.manual-op` | Manual Operation | 140×64 | |\n| `flow.preparation` | Preparation | 140×64 | |\n| `flow.stored-data` | Stored Data | 140×64 | |\n| `flow.database` | Database | 140×64 | db, store |\n| `flow.display` | Display | 140×64 | |\n| `flow.delay` | Delay | 140×64 | |\n| `flow.merge` | Merge | 140×64 | |\n| `flow.or` | Or | 140×64 | |\n| `flow.connector` | On-page Connector | 48×48 | |\n| `flow.off-page` | Off-page Connector | 140×64 | |\n| `flow.annotation` | Annotation | 140×64 | |\n\n## UML (`uml`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `uml.class` | Class | 140×64 | entity |\n| `uml.abstract` | Abstract Class | 140×64 | |\n| `uml.interface` | Interface | 140×64 | |\n| `uml.enum` | Enum | 140×64 | |\n| `uml.object` | Object | 140×64 | |\n| `uml.package` | Package | 140×64 | |\n| `uml.component` | Component | 140×64 | |\n| `uml.node` | Node / Deployment | 140×64 | |\n| `uml.actor` | Actor | 60×100 | user, role |\n| `uml.usecase` | Use Case | 140×64 | |\n| `uml.boundary` | Boundary | 140×64 | |\n| `uml.control` | Control | 140×64 | |\n| `uml.entity` | Entity | 140×64 | |\n| `uml.state` | State | 140×64 | |\n| `uml.initial` | Initial State | 36×36 | |\n| `uml.final` | Final State | 40×40 | |\n| `uml.decision` | Decision | 60×60 | |\n| `uml.note` | Note / Comment | 140×64 | comment |\n\n## ERD (`erd`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `erd.entity` | Entity | 140×64 | table |\n| `erd.weak-entity` | Weak Entity | 140×64 | |\n| `erd.attribute` | Attribute | 140×64 | |\n| `erd.key-attribute` | Key Attribute | 140×64 | pk, primary key |\n| `erd.multivalued` | Multivalued Attribute | 140×64 | |\n| `erd.derived` | Derived Attribute | 140×64 | |\n| `erd.relationship` | Relationship | 140×64 | |\n| `erd.identifying` | Identifying Relationship | 140×64 | |\n\n## Architecture (`networking`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `net.user` | User / Client | 60×100 | client, person |\n| `net.admin` | Admin | 60×100 | |\n| `net.browser` | Browser | 140×64 | |\n| `net.mobile` | Mobile Device | 90×130 | phone |\n| `net.desktop` | Desktop | 140×64 | |\n| `net.iot` | IoT Device | 140×64 | |\n| `net.server` | Server | 140×64 | |\n| `net.service` | Service | 140×64 | microservice |\n| `net.monolith` | Monolith | 140×64 | |\n| `net.api-gateway` | API Gateway | 140×64 | api, gateway |\n| `net.load-balancer` | Load Balancer | 140×64 | lb |\n| `net.reverse-proxy` | Reverse Proxy | 140×64 | proxy |\n| `net.firewall` | Firewall | 140×64 | waf |\n| `net.router` | Router | 140×64 | |\n| `net.switch` | Switch | 140×64 | |\n| `net.dns` | DNS | 140×64 | |\n| `net.cdn` | CDN | 140×64 | edge |\n| `net.cache` | Cache | 140×64 | |\n| `net.queue` | Queue | 140×64 | mq |\n| `net.websocket` | Web Socket | 140×64 | ws |\n| `net.rest` | REST endpoint | 140×64 | http, api |\n| `net.internet` | Internet / Cloud | 140×64 | wan |\n| `net.datacenter` | Datacenter | 140×64 | |\n| `net.container` | Container | 140×64 | |\n| `net.pod` | Pod | 140×64 | |\n| `net.vm` | Virtual Machine | 140×64 | vm |\n\n## Databases (`database`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `db.postgres` | PostgreSQL | 176×56 | postgres, pg |\n| `db.mysql` | MySQL | 176×56 | |\n| `db.mariadb` | MariaDB | 176×56 | |\n| `db.mongodb` | MongoDB | 176×56 | mongo |\n| `db.redis` | Redis | 176×56 | |\n| `db.sqlite` | SQLite | 176×56 | |\n| `db.mssql` | MS SQL Server | 176×56 | sqlserver, mssql |\n| `db.oracle` | Oracle | 176×56 | |\n| `db.cassandra` | Cassandra | 176×56 | |\n| `db.elasticsearch` | Elasticsearch | 176×56 | elastic, es |\n| `db.neo4j` | Neo4j | 176×56 | graph |\n| `db.couchdb` | CouchDB | 176×56 | couch |\n| `db.couchbase` | Couchbase | 176×56 | |\n| `db.clickhouse` | ClickHouse | 176×56 | |\n| `db.cockroachdb` | CockroachDB | 176×56 | cockroach |\n| `db.influxdb` | InfluxDB | 176×56 | influx, tsdb |\n| `db.snowflake` | Snowflake | 176×56 | |\n| `db.supabase` | Supabase | 176×56 | |\n\n## Messaging (`messaging`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `mq.kafka` | Apache Kafka | 176×56 | kafka |\n| `mq.rabbitmq` | RabbitMQ | 176×56 | rabbit, amqp |\n| `mq.nats` | NATS | 176×56 | |\n| `mq.pulsar` | Apache Pulsar | 176×56 | pulsar |\n| `mq.mqtt` | MQTT | 176×56 | |\n| `mq.pubsub` | Pub/Sub | 176×56 | pubsub |\n| `mq.activemq` | ActiveMQ | 176×56 | jms |\n\n## Languages (`language`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `lang.node` | Node.js | 176×56 | node, js runtime |\n| `lang.python` | Python | 176×56 | py |\n| `lang.java` | Java | 176×56 | jvm |\n| `lang.go` | Go | 176×56 | golang |\n| `lang.rust` | Rust | 176×56 | |\n| `lang.dotnet` | C# / .NET | 176×56 | csharp, c# |\n| `lang.typescript` | TypeScript | 176×56 | ts |\n| `lang.javascript` | JavaScript | 176×56 | js |\n| `lang.ruby` | Ruby | 176×56 | |\n| `lang.php` | PHP | 176×56 | |\n| `lang.kotlin` | Kotlin | 176×56 | |\n| `lang.swift` | Swift | 176×56 | |\n| `lang.scala` | Scala | 176×56 | |\n| `lang.elixir` | Elixir | 176×56 | |\n| `lang.cpp` | C++ | 176×56 | cpp |\n\n## Frameworks (`framework`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `fw.express` | Express | 176×56 | |\n| `fw.nestjs` | NestJS | 176×56 | nest |\n| `fw.spring` | Spring Boot | 176×56 | spring |\n| `fw.django` | Django | 176×56 | |\n| `fw.flask` | Flask | 176×56 | |\n| `fw.fastapi` | FastAPI | 176×56 | |\n| `fw.rails` | Rails | 176×56 | ruby on rails |\n| `fw.laravel` | Laravel | 176×56 | |\n| `fw.dotnet` | .NET | 176×56 | |\n| `fw.react` | React | 176×56 | |\n| `fw.vue` | Vue | 176×56 | vuejs |\n| `fw.angular` | Angular | 176×56 | |\n| `fw.next` | Next.js | 176×56 | nextjs |\n| `fw.nuxt` | Nuxt | 176×56 | nuxtjs |\n| `fw.svelte` | Svelte | 176×56 | sveltekit |\n| `fw.electron` | Electron | 176×56 | |\n| `fw.tailwind` | Tailwind | 176×56 | tailwindcss |\n| `fw.graphql` | GraphQL | 176×56 | |\n\n## DevOps / CI-CD (`devops`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `ops.docker` | Docker | 176×56 | |\n| `ops.k8s` | Kubernetes | 176×56 | k8s |\n| `ops.nginx` | Nginx | 176×56 | |\n| `ops.haproxy` | HAProxy | 176×56 | lb |\n| `ops.traefik` | Traefik | 176×56 | traefik |\n| `ops.terraform` | Terraform | 176×56 | iac |\n| `ops.ansible` | Ansible | 176×56 | |\n| `ops.jenkins` | Jenkins | 176×56 | ci |\n| `ops.gha` | GitHub Actions | 176×56 | ci, actions |\n| `ops.gitlab` | GitLab CI | 176×56 | ci |\n| `ops.argocd` | ArgoCD | 176×56 | gitops |\n| `ops.helm` | Helm | 176×56 | |\n| `ops.istio` | Istio | 176×56 | service mesh |\n| `ops.envoy` | Envoy | 176×56 | proxy |\n| `ops.consul` | Consul | 176×56 | |\n| `ops.vault` | Vault | 176×56 | secrets |\n| `ops.pm2` | PM2 | 176×56 | |\n| `ops.maven` | Maven | 176×56 | build |\n\n## Auth / Identity (`auth`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `auth.oidc` | OpenID Connect | 176×56 | oidc |\n| `auth.jwt` | JWT | 176×56 | token |\n| `auth.keycloak` | Keycloak | 176×56 | |\n| `auth.auth0` | Auth0 | 176×56 | |\n| `auth.okta` | Okta | 176×56 | |\n| `auth.firebase` | Firebase Auth | 176×56 | firebase |\n| `auth.oauth` | OAuth 2.0 | 176×56 | oauth |\n| `auth.saml` | SAML | 176×56 | |\n| `auth.ldap` | LDAP | 176×56 | |\n\n## Observability (`observability`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `obs.prometheus` | Prometheus | 176×56 | metrics |\n| `obs.grafana` | Grafana | 176×56 | dashboard |\n| `obs.datadog` | Datadog | 176×56 | |\n| `obs.sentry` | Sentry | 176×56 | errors |\n| `obs.elasticsearch` | Elasticsearch | 176×56 | elk |\n| `obs.logstash` | Logstash | 176×56 | elk |\n| `obs.kibana` | Kibana | 176×56 | elk |\n| `obs.jaeger` | Jaeger | 176×56 | tracing |\n| `obs.otel` | OpenTelemetry | 176×56 | otel, tracing |\n| `obs.newrelic` | New Relic | 176×56 | |\n\n## AWS (`aws`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `aws.ec2` | EC2 | 176×56 | aws |\n| `aws.lambda` | Lambda | 176×56 | aws |\n| `aws.s3` | S3 | 176×56 | aws |\n| `aws.ebs` | EBS | 176×56 | aws |\n| `aws.efs` | EFS | 176×56 | aws |\n| `aws.rds` | RDS | 176×56 | aws |\n| `aws.aurora` | Aurora | 176×56 | aws |\n| `aws.dynamodb` | DynamoDB | 176×56 | aws |\n| `aws.elasticache` | ElastiCache | 176×56 | aws |\n| `aws.redshift` | Redshift | 176×56 | aws |\n| `aws.vpc` | VPC | 176×56 | aws |\n| `aws.subnet` | Subnet | 176×56 | aws |\n| `aws.internet-gateway` | Internet Gateway | 176×56 | aws |\n| `aws.nat-gateway` | NAT Gateway | 176×56 | aws |\n| `aws.elb` | ELB | 176×56 | aws |\n| `aws.alb` | ALB | 176×56 | aws |\n| `aws.nlb` | NLB | 176×56 | aws |\n| `aws.api-gateway` | API Gateway | 176×56 | aws |\n| `aws.cloudfront` | CloudFront | 176×56 | aws |\n| `aws.route-53` | Route 53 | 176×56 | aws |\n| `aws.sqs` | SQS | 176×56 | aws |\n| `aws.sns` | SNS | 176×56 | aws |\n| `aws.eventbridge` | EventBridge | 176×56 | aws |\n| `aws.kinesis` | Kinesis | 176×56 | aws |\n| `aws.step-functions` | Step Functions | 176×56 | aws |\n| `aws.ecs` | ECS | 176×56 | aws |\n| `aws.eks` | EKS | 176×56 | aws |\n| `aws.fargate` | Fargate | 176×56 | aws |\n| `aws.ecr` | ECR | 176×56 | aws |\n| `aws.iam` | IAM | 176×56 | aws |\n| `aws.cognito` | Cognito | 176×56 | aws |\n| `aws.secrets-manager` | Secrets Manager | 176×56 | aws |\n| `aws.kms` | KMS | 176×56 | aws |\n| `aws.cloudwatch` | CloudWatch | 176×56 | aws |\n| `aws.cloudtrail` | CloudTrail | 176×56 | aws |\n| `aws.glue` | Glue | 176×56 | aws |\n| `aws.athena` | Athena | 176×56 | aws |\n| `aws.sagemaker` | SageMaker | 176×56 | aws |\n| `aws.appsync` | AppSync | 176×56 | aws |\n| `aws.amplify` | Amplify | 176×56 | aws |\n\n## Azure (`azure`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `azure.virtual-machine` | Virtual Machine | 176×56 | azure |\n| `azure.vm-scale-set` | VM Scale Set | 176×56 | azure |\n| `azure.app-service` | App Service | 176×56 | azure |\n| `azure.functions` | Functions | 176×56 | azure |\n| `azure.container-instances` | Container Instances | 176×56 | azure |\n| `azure.aks` | AKS | 176×56 | azure |\n| `azure.container-registry` | Container Registry | 176×56 | azure |\n| `azure.blob-storage` | Blob Storage | 176×56 | azure |\n| `azure.files` | Files | 176×56 | azure |\n| `azure.disk` | Disk | 176×56 | azure |\n| `azure.sql-database` | SQL Database | 176×56 | azure |\n| `azure.cosmos-db` | Cosmos DB | 176×56 | azure |\n| `azure.cache-for-redis` | Cache for Redis | 176×56 | azure |\n| `azure.synapse` | Synapse | 176×56 | azure |\n| `azure.virtual-network` | Virtual Network | 176×56 | azure |\n| `azure.load-balancer` | Load Balancer | 176×56 | azure |\n| `azure.application-gateway` | Application Gateway | 176×56 | azure |\n| `azure.front-door` | Front Door | 176×56 | azure |\n| `azure.service-bus` | Service Bus | 176×56 | azure |\n| `azure.event-hub` | Event Hub | 176×56 | azure |\n| `azure.event-grid` | Event Grid | 176×56 | azure |\n| `azure.key-vault` | Key Vault | 176×56 | azure |\n| `azure.entra-id` | Entra ID | 176×56 | azure |\n| `azure.monitor` | Monitor | 176×56 | azure |\n| `azure.application-insights` | Application Insights | 176×56 | azure |\n| `azure.logic-apps` | Logic Apps | 176×56 | azure |\n| `azure.api-management` | API Management | 176×56 | azure |\n\n## GCP (`gcp`)\n\n| Type | Label | Default size | Aliases |\n| --- | --- | --- | --- |\n| `gcp.compute-engine` | Compute Engine | 176×56 | gcp, google |\n| `gcp.app-engine` | App Engine | 176×56 | gcp, google |\n| `gcp.cloud-functions` | Cloud Functions | 176×56 | gcp, google |\n| `gcp.cloud-run` | Cloud Run | 176×56 | gcp, google |\n| `gcp.gke` | GKE | 176×56 | gcp, google |\n| `gcp.artifact-registry` | Artifact Registry | 176×56 | gcp, google |\n| `gcp.cloud-storage` | Cloud Storage | 176×56 | gcp, google |\n| `gcp.persistent-disk` | Persistent Disk | 176×56 | gcp, google |\n| `gcp.cloud-sql` | Cloud SQL | 176×56 | gcp, google |\n| `gcp.cloud-spanner` | Cloud Spanner | 176×56 | gcp, google |\n| `gcp.firestore` | Firestore | 176×56 | gcp, google |\n| `gcp.bigtable` | Bigtable | 176×56 | gcp, google |\n| `gcp.bigquery` | BigQuery | 176×56 | gcp, google |\n| `gcp.memorystore` | Memorystore | 176×56 | gcp, google |\n| `gcp.vpc` | VPC | 176×56 | gcp, google |\n| `gcp.cloud-load-balancing` | Cloud Load Balancing | 176×56 | gcp, google |\n| `gcp.cloud-cdn` | Cloud CDN | 176×56 | gcp, google |\n| `gcp.cloud-dns` | Cloud DNS | 176×56 | gcp, google |\n| `gcp.pub-sub` | Pub/Sub | 176×56 | gcp, google |\n| `gcp.dataflow` | Dataflow | 176×56 | gcp, google |\n| `gcp.cloud-tasks` | Cloud Tasks | 176×56 | gcp, google |\n| `gcp.iam` | IAM | 176×56 | gcp, google |\n| `gcp.secret-manager` | Secret Manager | 176×56 | gcp, google |\n| `gcp.cloud-kms` | Cloud KMS | 176×56 | gcp, google |\n| `gcp.cloud-monitoring` | Cloud Monitoring | 176×56 | gcp, google |\n| `gcp.vertex-ai` | Vertex AI | 176×56 | gcp, google |\n",
@@ -14,13 +14,116 @@ 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"]).describe('"mermaid": Mermaid text; "bpmn": a BPMN 2.0 XML file'),
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 or BPMN 2.0 XML 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
+ // Every document entering a tool goes through the shared gate (legacy fields
40
+ // stripped, ids deduped, parents healed) — the same one the studio applies.
17
41
  const loadDocument = (diagram) => {
18
- const parsed = (0, callman_core_1.safeParseDiagramDocument)(diagram.document);
42
+ const result = (0, callman_core_1.normalizeDocument)(diagram.document);
43
+ if (!result.document.nodes && !result.document.edges) {
44
+ throw new errors_js_1.ToolInputError("DIAGRAM_DOCUMENT_INVALID", `The stored diagram "${diagram.name}" is not a valid document.`);
45
+ }
46
+ return result.document;
47
+ };
48
+ const now = () => new Date().toISOString();
49
+ // A source-backed diagram whose Mermaid type is not a flowchart has no model
50
+ // to edit: structural tools refuse it clearly instead of guessing. (BPMN
51
+ // diagrams are editable: their ops patch the XML.)
52
+ const assertEditableStructure = (doc) => {
53
+ if (doc.source && doc.source.format === "mermaid" && doc.source.diagramType !== "flowchart") {
54
+ 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 });
55
+ }
56
+ };
57
+ // Mermaid text → the document it describes: a flowchart is parsed into
58
+ // nodes/edges (positions come from layout); anything else is kept as
59
+ // source only (the desktop previews it, tools read it back).
60
+ const documentFromSource = async (title, text, existing, direction, format = "mermaid") => {
61
+ if (format === "bpmn") {
62
+ if (!(0, callman_core_1.looksLikeBpmn)(text))
63
+ throw new errors_js_1.ToolInputError("DIAGRAM_SOURCE_INVALID", "The text is not a BPMN 2.0 document (no <definitions> root).");
64
+ const parsed = (0, callman_core_1.parseBpmn)(text);
65
+ if (!parsed.ok) {
66
+ 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) });
67
+ }
68
+ const base = existing ?? (0, callman_core_1.createEmptyDocument)(title);
69
+ let document;
70
+ try {
71
+ document = (0, callman_core_1.applyDiagramOps)(base, [{ op: "setSource", source: bpmnSource(text), resync: true }], { now: now() }).doc;
72
+ }
73
+ catch (error) {
74
+ return (0, build_js_1.toToolError)(error);
75
+ }
76
+ // Shapes the XML has no diagram interchange for are placed after their predecessors.
77
+ const unplaced = parsed.structure.nodes.filter((node) => !node.position).map((node) => node.id).filter((id) => document.nodes.some((node) => node.id === id));
78
+ if (unplaced.length > 0)
79
+ document = (0, callman_core_1.applyDiagramOps)(document, (0, callman_core_1.placeBpmnNodesOps)(document, unplaced), { now: now() }).doc;
80
+ const warnings = parsed.structure.diagnostics.filter((entry) => entry.severity !== "error").map((entry) => entry.message);
81
+ 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 };
82
+ }
83
+ if (!(0, callman_core_1.looksLikeMermaid)(text)) {
84
+ throw new errors_js_1.ToolInputError("DIAGRAM_SOURCE_INVALID", "The text does not start with a Mermaid diagram header (flowchart, graph, sequenceDiagram, …).");
85
+ }
86
+ const base = existing ?? (0, callman_core_1.createEmptyDocument)(title);
87
+ if (!(0, callman_core_1.isMermaidFlowchart)(text)) {
88
+ const header = text.replace(/^---[\s\S]*?\n---\s*\n/, "").split("\n").map((line) => line.trim()).find((line) => line !== "" && !line.startsWith("%%")) ?? "";
89
+ const diagramType = header.split(/\s+/)[0] ?? "unknown";
90
+ const source = { format: "mermaid", text, diagramType, grammar: callman_core_1.MERMAID_GRAMMAR_VERSION, valid: true };
91
+ const doc = (0, callman_core_1.applyDiagramOps)(base, [{ op: "setSource", source, resync: false }], { now: now() }).doc;
92
+ return { document: doc, layoutNote: `Stored as Mermaid ${diagramType} source (no nodes to lay out).`, warnings: [] };
93
+ }
94
+ const parsed = (0, callman_core_1.parseMermaid)(text);
19
95
  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 });
96
+ throw new errors_js_1.ToolInputError("DIAGRAM_SOURCE_INVALID", "The Mermaid flowchart has syntax errors.", {
97
+ diagnostics: parsed.structure.diagnostics.filter((entry) => entry.severity === "error").slice(0, 20),
98
+ });
21
99
  }
22
- return parsed.document;
100
+ const source = { format: "mermaid", text, diagramType: "flowchart", grammar: callman_core_1.MERMAID_GRAMMAR_VERSION };
101
+ let applied;
102
+ try {
103
+ applied = (0, callman_core_1.applyDiagramOps)(base, [{ op: "setSource", source }], { now: now() });
104
+ }
105
+ catch (error) {
106
+ return (0, build_js_1.toToolError)(error);
107
+ }
108
+ const warnings = parsed.structure.diagnostics.filter((entry) => entry.severity !== "error").map((entry) => entry.message);
109
+ const newIds = applied.added.nodes;
110
+ let document = applied.doc;
111
+ let layoutNote;
112
+ if (newIds.length > 0) {
113
+ if (existing && existing.nodes.length > 0) {
114
+ document = (0, layout_js_1.placeNewNodes)(document, newIds);
115
+ layoutNote = `Placed ${newIds.length} new node(s) below the existing ones.`;
116
+ }
117
+ else {
118
+ const laid = await (0, layout_js_1.layoutDocument)(document, { direction: direction ?? parsed.structure.direction });
119
+ document = laid.document;
120
+ layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
121
+ }
122
+ }
123
+ return { document, layoutNote, warnings };
23
124
  };
125
+ // Save with the revision we read; on a 409 the caller reloads and reapplies.
126
+ const isConflict = (error) => error instanceof errors_js_1.CallmanApiError && error.status === 409;
24
127
  const registerDiagramTools = (server, ctx) => {
25
128
  server.registerTool("list_diagrams", {
26
129
  title: "List diagrams",
@@ -36,52 +139,88 @@ const registerDiagramTools = (server, ctx) => {
36
139
  }));
37
140
  server.registerTool("get_diagram", {
38
141
  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") },
142
+ 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 or BPMN 2.0 XML a source-backed diagram keeps) or `full` (the complete stored DiagramDocument with positions, sizes and routes, up to 512 KB).",
143
+ inputSchema: { diagram: (0, shared_js_1.refArg)("Diagram"), view: zod_1.z.enum(["summary", "graph", "mermaid", "source", "full"]).default("graph") },
41
144
  annotations: shared_js_1.READ,
42
145
  }, (0, shared_js_1.guarded)(async ({ diagram, view }) => {
43
146
  const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
44
147
  const doc = loadDocument(resolved.item);
45
148
  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) };
149
+ const body = view === "summary"
150
+ ? summary
151
+ : view === "graph"
152
+ ? { ...summary, ...(0, summarize_js_1.diagramGraphView)(doc) }
153
+ : view === "mermaid"
154
+ ? { ...summary, ...(0, summarize_js_1.diagramMermaidView)(doc) }
155
+ : view === "source"
156
+ ? { ...summary, ...(0, summarize_js_1.diagramSourceView)(doc) }
157
+ : { ...summary, ...(0, summarize_js_1.diagramFullView)(doc) };
47
158
  return (0, shared_js_1.ok)({ diagram: body, ...(resolved.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved)] } : {}) });
48
159
  }));
49
160
  server.registerTool("create_diagram", {
50
161
  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.",
162
+ description: "Create a diagram from typed nodes and edges — or from Mermaid text / BPMN 2.0 XML (`source`). A BPMN process (`kind: \"bpmn\"`, or nodes typed bpmn.*) is stored as BPMN 2.0 XML the desktop opens in its BPMN editor. 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
163
  inputSchema: {
53
164
  name: zod_1.z.string().min(1).max(200).describe("Diagram name (library entry)"),
54
- ...build_js_1.diagramInputSchema.shape,
165
+ title: build_js_1.diagramInputSchema.shape.title.optional().describe("Diagram title (defaults to the name)"),
166
+ nodes: build_js_1.diagramInputSchema.shape.nodes.optional(),
167
+ edges: build_js_1.diagramInputSchema.shape.edges.optional(),
168
+ canvas: build_js_1.diagramInputSchema.shape.canvas,
169
+ source: sourceArg,
170
+ kind: zod_1.z.enum(["canvas", "bpmn"]).optional().describe('"bpmn": model a business process — nodes use bpmn.* types and the diagram is stored as BPMN 2.0 XML (default: canvas; inferred as bpmn when every node type starts with "bpmn.")'),
55
171
  autoLayout: zod_1.z.boolean().optional().describe("Force layout even when positions were given"),
56
172
  direction: directionArg,
57
173
  strictTypes: zod_1.z.boolean().optional(),
58
174
  dryRun: shared_js_1.dryRunArg,
59
175
  },
60
176
  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;
177
+ }, (0, shared_js_1.guarded)(async ({ name, title, nodes, edges, canvas, source, kind, autoLayout, direction, strictTypes, dryRun }) => {
178
+ if (!nodes && !source)
179
+ throw new errors_js_1.ToolInputError("NOTHING_TO_CREATE", "Pass nodes (+ edges) or source { format: \"mermaid\" | \"bpmn\", text }.");
180
+ if (nodes && source)
181
+ throw new errors_js_1.ToolInputError("CONFLICTING_INPUT", "Pass either nodes or source, not both.");
182
+ let document;
64
183
  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}.`;
184
+ let warnings = [];
185
+ const bpmnKind = kind === "bpmn" || (kind === undefined && Boolean(nodes && nodes.length > 0 && nodes.every((node) => node.type.startsWith("bpmn."))));
186
+ if (source) {
187
+ const fromSource = await documentFromSource(title ?? name, source.text, null, direction, source.format);
188
+ document = canvas ? { ...fromSource.document, canvas: { ...fromSource.document.canvas, ...canvas } } : fromSource.document;
189
+ layoutNote = fromSource.layoutNote;
190
+ warnings = fromSource.warnings;
191
+ }
192
+ else if (bpmnKind) {
193
+ const built = (0, build_js_1.buildDiagramDocument)({ title: title ?? name, nodes: nodes ?? [], edges: edges ?? [], canvas }, { strictTypes, kind: "bpmn" });
194
+ const bpmn = bpmnDocumentFromModel(built.document, built.unpositionedNodeIds, (0, callman_core_1.createEmptyDocument)(title ?? name));
195
+ document = bpmn.document;
196
+ layoutNote = bpmn.layoutNote;
197
+ warnings = built.warnings;
198
+ }
199
+ else {
200
+ const built = (0, build_js_1.buildDiagramDocument)({ title: title ?? name, nodes: nodes ?? [], edges: edges ?? [], canvas }, { strictTypes });
201
+ document = built.document;
202
+ warnings = built.warnings;
203
+ if (autoLayout || built.unpositionedNodeIds.length > 0) {
204
+ const laid = await (0, layout_js_1.layoutDocument)(document, { direction });
205
+ document = laid.document;
206
+ layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
207
+ }
69
208
  }
70
209
  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 } : {}) });
210
+ return (0, shared_js_1.ok)({ dryRun: true, diagram: { name, ...(0, summarize_js_1.diagramGraphView)(document) }, warnings, ...(layoutNote ? { layout: layoutNote } : {}) });
72
211
  }
73
212
  const created = await ctx.api.createDiagram({ name: name.trim(), document });
74
213
  ctx.invalidate("diagrams");
75
214
  return (0, shared_js_1.ok)({
76
215
  diagram: { ...(0, summarize_js_1.diagramListView)(created), ...(0, summarize_js_1.diagramGraphView)(document) },
77
- warnings: built.warnings,
216
+ warnings,
78
217
  ...(layoutNote ? { layout: layoutNote } : {}),
79
218
  note: "Saved. Open it in the desktop app (Diagrams → Refresh). Edit with patch_diagram; share with share_diagram.",
80
219
  });
81
220
  }));
82
221
  server.registerTool("update_diagram", {
83
222
  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.",
223
+ description: "Replace a diagram's whole document (nodes + edges, same fields as create_diagram), replace or attach its Mermaid text or BPMN 2.0 XML `source`, 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). 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
224
  inputSchema: {
86
225
  diagram: (0, shared_js_1.refArg)("Diagram"),
87
226
  name: zod_1.z.string().min(1).max(200).optional(),
@@ -89,15 +228,18 @@ const registerDiagramTools = (server, ctx) => {
89
228
  nodes: build_js_1.diagramInputSchema.shape.nodes.optional(),
90
229
  edges: build_js_1.diagramInputSchema.shape.edges.optional(),
91
230
  canvas: build_js_1.diagramInputSchema.shape.canvas,
231
+ source: sourceArg,
92
232
  autoLayout: zod_1.z.boolean().optional(),
93
233
  direction: directionArg,
94
234
  strictTypes: zod_1.z.boolean().optional(),
95
235
  },
96
236
  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).");
237
+ }, (0, shared_js_1.guarded)(async ({ diagram, name, title, nodes, edges, canvas, source, autoLayout, direction, strictTypes }) => {
238
+ if (name === undefined && nodes === undefined && title === undefined && canvas === undefined && source === undefined) {
239
+ throw new errors_js_1.ToolInputError("NOTHING_TO_UPDATE", "Pass name, title, canvas, source and/or nodes (+ edges).");
100
240
  }
241
+ if (nodes && source)
242
+ throw new errors_js_1.ToolInputError("CONFLICTING_INPUT", "Pass either nodes or source, not both.");
101
243
  const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
102
244
  if (!resolved.item.canEdit)
103
245
  throw new errors_js_1.ToolInputError("DIAGRAM_READ_ONLY", "You have read-only access to this diagram.");
@@ -105,33 +247,60 @@ const registerDiagramTools = (server, ctx) => {
105
247
  let document;
106
248
  let layoutNote;
107
249
  let warnings = [];
108
- if (nodes) {
250
+ if (nodes && existing.source?.format === "bpmn") {
251
+ // A BPMN diagram stays BPMN: the new model is written as fresh XML.
252
+ 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" });
253
+ const bpmn = bpmnDocumentFromModel(built.document, built.unpositionedNodeIds, existing);
254
+ document = bpmn.document;
255
+ layoutNote = bpmn.layoutNote;
256
+ 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."];
257
+ }
258
+ else if (nodes) {
259
+ assertEditableStructure(existing);
109
260
  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
261
  document = built.document;
111
262
  warnings = built.warnings;
112
263
  if (edges === undefined && existing.edges.length > 0) {
113
264
  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
265
  }
266
+ if (existing.source)
267
+ warnings.push("The diagram's Mermaid source was detached: nodes and edges were replaced from the input.");
115
268
  if (autoLayout || built.unpositionedNodeIds.length > 0) {
116
269
  const laid = await (0, layout_js_1.layoutDocument)(document, { direction });
117
270
  document = laid.document;
118
271
  layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
119
272
  }
120
273
  }
274
+ else if (source) {
275
+ const fromSource = await documentFromSource(title ?? existing.meta.title, source.text, existing, direction, source.format);
276
+ document = fromSource.document;
277
+ layoutNote = fromSource.layoutNote;
278
+ warnings = fromSource.warnings;
279
+ if (title !== undefined || canvas !== undefined) {
280
+ document = {
281
+ ...document,
282
+ ...(canvas !== undefined ? { canvas: { ...document.canvas, ...canvas } } : {}),
283
+ meta: { ...document.meta, ...(title !== undefined ? { title } : {}) },
284
+ };
285
+ }
286
+ }
121
287
  else if (title !== undefined || canvas !== undefined) {
122
288
  document = {
123
289
  ...existing,
124
290
  ...(canvas !== undefined ? { canvas: { ...existing.canvas, ...canvas } } : {}),
125
- meta: { ...existing.meta, ...(title !== undefined ? { title } : {}), updatedAt: new Date().toISOString() },
291
+ meta: { ...existing.meta, ...(title !== undefined ? { title } : {}), updatedAt: now() },
126
292
  };
127
293
  }
128
- const updated = await ctx.api.updateDiagram(resolved.item.id, { ...(name !== undefined ? { name: name.trim() } : {}), ...(document ? { document } : {}) });
294
+ const updated = await ctx.api.updateDiagram(resolved.item.id, {
295
+ ...(name !== undefined ? { name: name.trim() } : {}),
296
+ ...(document ? { document, ...(typeof existing.meta.revision === "number" ? { baseRevision: existing.meta.revision } : {}) } : {}),
297
+ });
129
298
  ctx.invalidate("diagrams");
130
299
  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)] });
131
300
  }));
132
301
  server.registerTool("patch_diagram", {
133
302
  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.",
303
+ 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). 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
304
  inputSchema: {
136
305
  diagram: (0, shared_js_1.refArg)("Diagram"),
137
306
  ops: zod_1.z.array(patch_js_1.diagramPatchOpSchema).min(1).max(200),
@@ -144,21 +313,53 @@ const registerDiagramTools = (server, ctx) => {
144
313
  const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
145
314
  if (!resolved.item.canEdit)
146
315
  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}.`;
316
+ // Apply on the freshest copy; a 409 (someone saved meanwhile) reloads
317
+ // and reapplies once — ops describe intent, so they replay cleanly.
318
+ const attempt = async (record) => {
319
+ const existing = loadDocument(record);
320
+ assertEditableStructure(existing);
321
+ const patched = (0, patch_js_1.applyDiagramPatch)(existing, ops, { strictTypes });
322
+ let document = patched.document;
323
+ let layoutNote;
324
+ const wantsAll = relayout === "all" || patched.relayout?.scope === "all";
325
+ const layoutDirection = direction ?? patched.relayout?.direction;
326
+ const bpmn = document.source?.format === "bpmn";
327
+ if (wantsAll) {
328
+ // A process reads left to right; the BPMN XML follows the moves.
329
+ const laid = await (0, layout_js_1.layoutDocument)(document, { direction: layoutDirection ?? (bpmn ? "RIGHT" : undefined) });
330
+ document = laid.document;
331
+ layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
332
+ }
333
+ else if (relayout === "new" && patched.addedNodeIds.length > 0) {
334
+ const ids = patched.addedNodeIds.filter((id) => document.nodes.some((node) => node.id === id));
335
+ if (bpmn) {
336
+ document = (0, callman_core_1.applyDiagramOps)(document, (0, callman_core_1.placeBpmnNodesOps)(document, ids), { now: now() }).doc;
337
+ layoutNote = `Placed ${ids.length} new node(s) after their predecessors.`;
338
+ }
339
+ else {
340
+ document = (0, layout_js_1.placeNewNodes)(document, ids);
341
+ layoutNote = `Placed ${ids.length} new node(s) below the existing ones.`;
342
+ }
343
+ }
344
+ const updated = await ctx.api.updateDiagram(record.id, {
345
+ document,
346
+ ...(typeof existing.meta.revision === "number" ? { baseRevision: existing.meta.revision } : {}),
347
+ });
348
+ return { updated, document, patched, layoutNote };
349
+ };
350
+ let outcome;
351
+ let retried = false;
352
+ try {
353
+ outcome = await attempt(resolved.item);
155
354
  }
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.`;
355
+ catch (error) {
356
+ if (!isConflict(error))
357
+ throw error;
358
+ retried = true;
359
+ outcome = await attempt(await ctx.api.getDiagram(resolved.item.id));
159
360
  }
160
- const updated = await ctx.api.updateDiagram(resolved.item.id, { document });
161
361
  ctx.invalidate("diagrams");
362
+ const { updated, document, patched, layoutNote } = outcome;
162
363
  return (0, shared_js_1.ok)({
163
364
  diagram: { ...(0, summarize_js_1.diagramListView)(updated), ...(0, summarize_js_1.diagramGraphView)(document) },
164
365
  applied: ops.length,
@@ -166,7 +367,8 @@ const registerDiagramTools = (server, ctx) => {
166
367
  removedNodes: patched.removedNodeIds,
167
368
  removedEdges: patched.removedEdgeIds,
168
369
  ...(patched.reroutedEdgeIds.length > 0 ? { reroutedEdges: patched.reroutedEdgeIds } : {}),
169
- warnings: patched.warnings,
370
+ ...(Object.keys(patched.idMap).length > 0 ? { idMap: patched.idMap } : {}),
371
+ warnings: [...patched.warnings, ...(retried ? ["The diagram had been saved by someone else meanwhile; your ops were re-applied on the newer version."] : [])],
170
372
  ...(layoutNote ? { layout: layoutNote } : {}),
171
373
  resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved)],
172
374
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "callman-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "MCP server for Callman: lets Claude Code, Claude Desktop, Cursor, Codex and Gemini CLI manage collections, requests, environments, test scenarios and system-design diagrams in your Callman workspace — cloud or on-prem.",
5
5
  "keywords": [
6
6
  "callman",
@@ -62,7 +62,7 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@modelcontextprotocol/sdk": "^1.30.0",
65
- "callman-core": "^1.28.1",
65
+ "callman-core": "^1.29.0",
66
66
  "elkjs": "^0.12.0",
67
67
  "js-yaml": "^4.1.0",
68
68
  "zod": "^3.25.0"