callman-mcp 0.3.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 (56) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +4 -4
  3. package/dist/client/api.d.ts +15 -1
  4. package/dist/client/api.d.ts.map +1 -1
  5. package/dist/client/api.js +21 -1
  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 +17 -17
  10. package/dist/domain/diagram/build.d.ts.map +1 -1
  11. package/dist/domain/diagram/build.js +3 -3
  12. package/dist/domain/diagram/build.js.map +1 -1
  13. package/dist/domain/diagram/layout.d.ts +3 -1
  14. package/dist/domain/diagram/layout.d.ts.map +1 -1
  15. package/dist/domain/diagram/layout.js +9 -3
  16. package/dist/domain/diagram/layout.js.map +1 -1
  17. package/dist/domain/diagram/patch.d.ts +721 -20
  18. package/dist/domain/diagram/patch.d.ts.map +1 -1
  19. package/dist/domain/diagram/patch.js +53 -0
  20. package/dist/domain/diagram/patch.js.map +1 -1
  21. package/dist/domain/diagram/summarize.d.ts +1 -0
  22. package/dist/domain/diagram/summarize.d.ts.map +1 -1
  23. package/dist/domain/diagram/summarize.js +1 -0
  24. package/dist/domain/diagram/summarize.js.map +1 -1
  25. package/dist/resources/docs/diagramSchema.d.ts +1 -1
  26. package/dist/resources/docs/diagramSchema.d.ts.map +1 -1
  27. package/dist/resources/docs/diagramSchema.js +33 -1
  28. package/dist/resources/docs/diagramSchema.js.map +1 -1
  29. package/dist/resources/docs/overview.d.ts +1 -1
  30. package/dist/resources/docs/overview.d.ts.map +1 -1
  31. package/dist/resources/docs/overview.js +4 -1
  32. package/dist/resources/docs/overview.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/catalog.d.ts.map +1 -1
  36. package/dist/tools/catalog.js +5 -0
  37. package/dist/tools/catalog.js.map +1 -1
  38. package/dist/tools/diagrams.d.ts.map +1 -1
  39. package/dist/tools/diagrams.js +328 -42
  40. package/dist/tools/diagrams.js.map +1 -1
  41. package/dist/tools/lookups.d.ts +5 -1
  42. package/dist/tools/lookups.d.ts.map +1 -1
  43. package/dist/tools/lookups.js +41 -0
  44. package/dist/tools/lookups.js.map +1 -1
  45. package/dist-cjs/client/api.js +21 -1
  46. package/dist-cjs/domain/diagram/build.js +3 -3
  47. package/dist-cjs/domain/diagram/layout.js +10 -3
  48. package/dist-cjs/domain/diagram/patch.js +54 -1
  49. package/dist-cjs/domain/diagram/summarize.js +1 -0
  50. package/dist-cjs/resources/docs/diagramSchema.js +33 -1
  51. package/dist-cjs/resources/docs/overview.js +4 -1
  52. package/dist-cjs/resources/generated/aiPacks.js +2 -2
  53. package/dist-cjs/tools/catalog.js +5 -0
  54. package/dist-cjs/tools/diagrams.js +323 -37
  55. package/dist-cjs/tools/lookups.js +46 -1
  56. package/package.json +2 -2
@@ -1,8 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AI_PACKS = exports.AI_PACKS_CORE_VERSION = void 0;
4
- // GENERATED by scripts/build-static-resources.mjs from callman-core@1.29.0 ai-packs — do not edit.
5
- exports.AI_PACKS_CORE_VERSION = "1.29.0";
4
+ // GENERATED by scripts/build-static-resources.mjs from callman-core@1.30.0 ai-packs — do not edit.
5
+ exports.AI_PACKS_CORE_VERSION = "1.30.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",
@@ -44,6 +44,8 @@ exports.TOOL_CATALOG = [
44
44
  { name: "delete_data_contract", group: "contracts", scopes: ["requests:delete"], destructive: true },
45
45
  // The safe connection record (no credentials) — readable with either family.
46
46
  { name: "list_connections", group: "connections", scopes: ["requests:read", "scenarios:read"], anyOf: true },
47
+ // The database behind a connection (metadata only) — what a Database Diagram is built from.
48
+ { name: "introspect_database", group: "connections", scopes: ["requests:read", "scenarios:read", "diagrams:write"], anyOf: true },
47
49
  // Documents and new collections → import:write; cURL/.http into an existing
48
50
  // collection → requests:write.
49
51
  { name: "import_content", group: "import", scopes: ["import:write", "requests:write"], anyOf: true },
@@ -72,6 +74,9 @@ exports.TOOL_CATALOG = [
72
74
  { name: "patch_diagram", group: "diagrams", scopes: ["diagrams:write"] },
73
75
  { name: "delete_diagram", group: "diagrams", scopes: ["diagrams:delete"], destructive: true },
74
76
  { name: "share_diagram", group: "diagrams", scopes: ["diagrams:share"] },
77
+ { name: "create_diagram_folder", group: "diagrams", scopes: ["diagrams:write"] },
78
+ { name: "update_diagram_folder", group: "diagrams", scopes: ["diagrams:write"] },
79
+ { name: "delete_diagram_folder", group: "diagrams", scopes: ["diagrams:delete"], destructive: true },
75
80
  ];
76
81
  const toolAvailability = (tokenScopes, allowLocalRun) => {
77
82
  const held = new Set(tokenScopes);
@@ -16,7 +16,7 @@ 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
17
  const sourceArg = zod_1.z
18
18
  .object({
19
- format: zod_1.z.enum(["mermaid", "bpmn"]).describe('"mermaid": Mermaid text; "bpmn": a BPMN 2.0 XML file'),
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
20
  text: zod_1.z
21
21
  .string()
22
22
  .min(1)
@@ -25,7 +25,7 @@ const sourceArg = zod_1.z
25
25
  })
26
26
  .strict()
27
27
  .optional()
28
- .describe("Create the diagram from Mermaid text or BPMN 2.0 XML instead of nodes/edges");
28
+ .describe("Create the diagram from Mermaid text, BPMN 2.0 XML or a database model instead of nodes/edges");
29
29
  // A BPMN diagram keeps its truth in BPMN 2.0 XML: the model is derived from it.
30
30
  const bpmnSource = (text) => ({ format: "bpmn", text, diagramType: "bpmn", grammar: callman_core_1.BPMN_GRAMMAR_VERSION });
31
31
  // Typed bpmn.* nodes → a BPMN diagram: the model becomes XML, the XML becomes
@@ -36,6 +36,83 @@ const bpmnDocumentFromModel = (model, unpositionedNodeIds, base) => {
36
36
  const document = (0, callman_core_1.applyDiagramOps)({ ...base, nodes: [], edges: [], source: undefined }, [{ op: "setSource", source: bpmnSource(xml), resync: true }], { now: now() }).doc;
37
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
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" };
39
116
  // Every document entering a tool goes through the shared gate (legacy fields
40
117
  // stripped, ids deduped, parents healed) — the same one the studio applies.
41
118
  const loadDocument = (diagram) => {
@@ -58,6 +135,21 @@ const assertEditableStructure = (doc) => {
58
135
  // nodes/edges (positions come from layout); anything else is kept as
59
136
  // source only (the desktop previews it, tools read it back).
60
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
+ }
61
153
  if (format === "bpmn") {
62
154
  if (!(0, callman_core_1.looksLikeBpmn)(text))
63
155
  throw new errors_js_1.ToolInputError("DIAGRAM_SOURCE_INVALID", "The text is not a BPMN 2.0 document (no <definitions> root).");
@@ -127,39 +219,59 @@ const isConflict = (error) => error instanceof errors_js_1.CallmanApiError && er
127
219
  const registerDiagramTools = (server, ctx) => {
128
220
  server.registerTool("list_diagrams", {
129
221
  title: "List diagrams",
130
- 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.",
131
- 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
+ },
132
228
  annotations: shared_js_1.READ,
133
- }, (0, shared_js_1.guarded)(async ({ query, cursor, limit }) => {
134
- 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)]);
135
231
  const needle = query?.trim().toLowerCase();
136
- const filtered = needle ? all.filter((diagram) => diagram.name.toLowerCase().includes(needle)) : all;
137
- const page = (0, pagination_js_1.sliceWithCursor)(filtered, cursor, limit, needle ?? "");
138
- 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
+ });
139
248
  }));
140
249
  server.registerTool("get_diagram", {
141
250
  title: "Get a diagram",
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") },
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") },
144
253
  annotations: shared_js_1.READ,
145
254
  }, (0, shared_js_1.guarded)(async ({ diagram, view }) => {
146
255
  const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
147
256
  const doc = loadDocument(resolved.item);
148
257
  const summary = (0, summarize_js_1.diagramListView)(resolved.item);
258
+ const summaryWithFolder = { ...summary, folder: (0, lookups_js_1.diagramFolderPath)(await (0, lookups_js_1.diagramFolderRows)(ctx), resolved.item.folderId) };
149
259
  const body = view === "summary"
150
- ? summary
260
+ ? summaryWithFolder
151
261
  : view === "graph"
152
262
  ? { ...summary, ...(0, summarize_js_1.diagramGraphView)(doc) }
153
263
  : view === "mermaid"
154
264
  ? { ...summary, ...(0, summarize_js_1.diagramMermaidView)(doc) }
155
265
  : view === "source"
156
266
  ? { ...summary, ...(0, summarize_js_1.diagramSourceView)(doc) }
157
- : { ...summary, ...(0, summarize_js_1.diagramFullView)(doc) };
267
+ : view === "schema"
268
+ ? { ...summary, schema: (0, callman_core_1.databaseSchemaView)(doc) }
269
+ : { ...summary, ...(0, summarize_js_1.diagramFullView)(doc) };
158
270
  return (0, shared_js_1.ok)({ diagram: body, ...(resolved.annotation ? { resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved)] } : {}) });
159
271
  }));
160
272
  server.registerTool("create_diagram", {
161
- title: "Create a system-design diagram",
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.",
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.",
163
275
  inputSchema: {
164
276
  name: zod_1.z.string().min(1).max(200).describe("Diagram name (library entry)"),
165
277
  title: build_js_1.diagramInputSchema.shape.title.optional().describe("Diagram title (defaults to the name)"),
@@ -167,28 +279,73 @@ const registerDiagramTools = (server, ctx) => {
167
279
  edges: build_js_1.diagramInputSchema.shape.edges.optional(),
168
280
  canvas: build_js_1.diagramInputSchema.shape.canvas,
169
281
  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.")'),
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'),
171
298
  autoLayout: zod_1.z.boolean().optional().describe("Force layout even when positions were given"),
172
299
  direction: directionArg,
173
300
  strictTypes: zod_1.z.boolean().optional(),
174
301
  dryRun: shared_js_1.dryRunArg,
175
302
  },
176
303
  annotations: shared_js_1.WRITE,
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.");
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.");
182
313
  let document;
183
314
  let layoutNote;
184
315
  let warnings = [];
185
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 = {};
186
319
  if (source) {
187
320
  const fromSource = await documentFromSource(title ?? name, source.text, null, direction, source.format);
188
321
  document = canvas ? { ...fromSource.document, canvas: { ...fromSource.document.canvas, ...canvas } } : fromSource.document;
189
322
  layoutNote = fromSource.layoutNote;
190
323
  warnings = fromSource.warnings;
191
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
+ }
192
349
  else if (bpmnKind) {
193
350
  const built = (0, build_js_1.buildDiagramDocument)({ title: title ?? name, nodes: nodes ?? [], edges: edges ?? [], canvas }, { strictTypes, kind: "bpmn" });
194
351
  const bpmn = bpmnDocumentFromModel(built.document, built.unpositionedNodeIds, (0, callman_core_1.createEmptyDocument)(title ?? name));
@@ -206,21 +363,26 @@ const registerDiagramTools = (server, ctx) => {
206
363
  layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
207
364
  }
208
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);
209
369
  if (dryRun) {
210
- return (0, shared_js_1.ok)({ dryRun: true, diagram: { name, ...(0, summarize_js_1.diagramGraphView)(document) }, 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 } : {}) });
211
371
  }
212
- 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 } : {}) });
213
373
  ctx.invalidate("diagrams");
214
374
  return (0, shared_js_1.ok)({
215
- diagram: { ...(0, summarize_js_1.diagramListView)(created), ...(0, summarize_js_1.diagramGraphView)(document) },
375
+ diagram: { ...(0, summarize_js_1.diagramListView)(created), folder: targetFolder?.item.path ?? null, ...view },
216
376
  warnings,
377
+ ...connectionNote,
217
378
  ...(layoutNote ? { layout: layoutNote } : {}),
218
- 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.`,
219
381
  });
220
382
  }));
221
383
  server.registerTool("update_diagram", {
222
384
  title: "Replace a diagram's content",
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.",
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.",
224
386
  inputSchema: {
225
387
  diagram: (0, shared_js_1.refArg)("Diagram"),
226
388
  name: zod_1.z.string().min(1).max(200).optional(),
@@ -229,17 +391,19 @@ const registerDiagramTools = (server, ctx) => {
229
391
  edges: build_js_1.diagramInputSchema.shape.edges.optional(),
230
392
  canvas: build_js_1.diagramInputSchema.shape.canvas,
231
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'),
232
395
  autoLayout: zod_1.z.boolean().optional(),
233
396
  direction: directionArg,
234
397
  strictTypes: zod_1.z.boolean().optional(),
235
398
  },
236
399
  annotations: shared_js_1.IDEMPOTENT_WRITE,
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).");
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).");
240
403
  }
241
404
  if (nodes && source)
242
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;
243
407
  const resolved = await (0, lookups_js_1.resolveDiagram)(ctx, diagram);
244
408
  if (!resolved.item.canEdit)
245
409
  throw new errors_js_1.ToolInputError("DIAGRAM_READ_ONLY", "You have read-only access to this diagram.");
@@ -247,7 +411,19 @@ const registerDiagramTools = (server, ctx) => {
247
411
  let document;
248
412
  let layoutNote;
249
413
  let warnings = [];
250
- if (nodes && existing.source?.format === "bpmn") {
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") {
251
427
  // A BPMN diagram stays BPMN: the new model is written as fresh XML.
252
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" });
253
429
  const bpmn = bpmnDocumentFromModel(built.document, built.unpositionedNodeIds, existing);
@@ -264,7 +440,7 @@ const registerDiagramTools = (server, ctx) => {
264
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.`);
265
441
  }
266
442
  if (existing.source)
267
- warnings.push("The diagram's Mermaid source was detached: nodes and edges were replaced from the input.");
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.`);
268
444
  if (autoLayout || built.unpositionedNodeIds.length > 0) {
269
445
  const laid = await (0, layout_js_1.layoutDocument)(document, { direction });
270
446
  document = laid.document;
@@ -294,13 +470,20 @@ const registerDiagramTools = (server, ctx) => {
294
470
  const updated = await ctx.api.updateDiagram(resolved.item.id, {
295
471
  ...(name !== undefined ? { name: name.trim() } : {}),
296
472
  ...(document ? { document, ...(typeof existing.meta.revision === "number" ? { baseRevision: existing.meta.revision } : {}) } : {}),
473
+ ...(folder !== undefined ? { folderId: targetFolder?.item.id ?? null } : {}),
297
474
  });
298
475
  ctx.invalidate("diagrams");
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)] });
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
+ });
300
483
  }));
301
484
  server.registerTool("patch_diagram", {
302
485
  title: "Edit a diagram incrementally",
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.",
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.",
304
487
  inputSchema: {
305
488
  diagram: (0, shared_js_1.refArg)("Diagram"),
306
489
  ops: zod_1.z.array(patch_js_1.diagramPatchOpSchema).min(1).max(200),
@@ -324,15 +507,21 @@ const registerDiagramTools = (server, ctx) => {
324
507
  const wantsAll = relayout === "all" || patched.relayout?.scope === "all";
325
508
  const layoutDirection = direction ?? patched.relayout?.direction;
326
509
  const bpmn = document.source?.format === "bpmn";
510
+ const database = document.source?.format === "database";
327
511
  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) });
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 } : {}) });
330
515
  document = laid.document;
331
516
  layoutNote = laid.warning ?? `Laid out with ${laid.engine}.`;
332
517
  }
333
518
  else if (relayout === "new" && patched.addedNodeIds.length > 0) {
334
519
  const ids = patched.addedNodeIds.filter((id) => document.nodes.some((node) => node.id === id));
335
- if (bpmn) {
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) {
336
525
  document = (0, callman_core_1.applyDiagramOps)(document, (0, callman_core_1.placeBpmnNodesOps)(document, ids), { now: now() }).doc;
337
526
  layoutNote = `Placed ${ids.length} new node(s) after their predecessors.`;
338
527
  }
@@ -361,7 +550,7 @@ const registerDiagramTools = (server, ctx) => {
361
550
  ctx.invalidate("diagrams");
362
551
  const { updated, document, patched, layoutNote } = outcome;
363
552
  return (0, shared_js_1.ok)({
364
- 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) } : {}) },
365
554
  applied: ops.length,
366
555
  added: patched.addedNodeIds,
367
556
  removedNodes: patched.removedNodeIds,
@@ -373,6 +562,103 @@ const registerDiagramTools = (server, ctx) => {
373
562
  resolved: [(0, resolve_js_1.resolvedSummary)(diagram, resolved)],
374
563
  });
375
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
+ }));
376
662
  server.registerTool("delete_diagram", {
377
663
  title: "Delete a diagram",
378
664
  description: "Delete a diagram you own. Permanent — confirm with the user first.",