@datasynx/agentic-ai-cartography 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -45,6 +45,7 @@ __export(src_exports, {
45
45
  CredentialConfigSchema: () => CredentialConfigSchema,
46
46
  CsvCostSource: () => CsvCostSource,
47
47
  DEFAULT_ANOMALY_THRESHOLDS: () => DEFAULT_ANOMALY_THRESHOLDS,
48
+ DEFAULT_INGEST_QUOTA: () => DEFAULT_INGEST_QUOTA,
48
49
  DEFAULT_SERVER_NAME: () => DEFAULT_SERVER_NAME,
49
50
  DEFAULT_TENANT: () => DEFAULT_TENANT,
50
51
  DriftConfigSchema: () => DriftConfigSchema,
@@ -66,10 +67,12 @@ __export(src_exports, {
66
67
  ProviderRegistry: () => ProviderRegistry,
67
68
  RELATION_TO_DIRECTION: () => RELATION_TO_DIRECTION,
68
69
  ROLES: () => ROLES,
70
+ RateLimiter: () => RateLimiter,
69
71
  RoleSchema: () => RoleSchema,
70
72
  RuleCheckSchema: () => RuleCheckSchema,
71
73
  RulesetSchema: () => RulesetSchema,
72
74
  SCAN_ARG_PATTERNS: () => SCAN_ARG_PATTERNS,
75
+ SCHEMA_VERSION: () => SCHEMA_VERSION,
73
76
  SDL: () => SDL,
74
77
  SEVERITY_WEIGHT: () => SEVERITY_WEIGHT,
75
78
  SHARING_LEVELS: () => SHARING_LEVELS,
@@ -149,6 +152,7 @@ __export(src_exports, {
149
152
  diffTopology: () => diffTopology,
150
153
  edgesToConnections: () => edgesToConnections,
151
154
  enrichCosts: () => enrichCosts,
155
+ entitiesToYaml: () => entitiesToYaml,
152
156
  evaluateCheck: () => evaluateCheck,
153
157
  evaluateRule: () => evaluateRule,
154
158
  evidenceLine: () => evidenceLine,
@@ -278,6 +282,7 @@ __export(src_exports, {
278
282
  startApi: () => startApi,
279
283
  stripSensitive: () => stripSensitive,
280
284
  timingSafeEqual: () => timingSafeEqual,
285
+ toBackstageEntities: () => toBackstageEntities,
281
286
  validateScanner: () => validateScanner,
282
287
  vscodeDeeplink: () => vscodeDeeplink,
283
288
  zodToJsonSchema: () => zodToJsonSchema
@@ -2848,6 +2853,7 @@ function newAnomalies(base, current) {
2848
2853
 
2849
2854
  // src/db.ts
2850
2855
  var DEFAULT_TENANT = "local";
2856
+ var SCHEMA_VERSION = 15;
2851
2857
  function normalizeTenant(raw) {
2852
2858
  if (raw == null) return DEFAULT_TENANT;
2853
2859
  const cleaned = sanitizeUntrusted(String(raw)).trim().slice(0, 128);
@@ -4252,6 +4258,14 @@ var CartographyDB = class {
4252
4258
  }
4253
4259
  return rows.length;
4254
4260
  }
4261
+ /**
4262
+ * Retention/compaction (4.7): delete audit events older than `olderThan` (ISO 8601).
4263
+ * The audit trail grows unbounded on a busy collector; this bounds it without touching
4264
+ * sessions/nodes/edges. Returns the number of events removed.
4265
+ */
4266
+ pruneEventsOlderThan(olderThan) {
4267
+ return this.db.prepare("DELETE FROM activity_events WHERE timestamp < ?").run(olderThan).changes;
4268
+ }
4255
4269
  // ── Graph queries (read-only context layer) ─────────────────────────────────
4256
4270
  /** Fetch a single node by id within a session. */
4257
4271
  getNode(sessionId, nodeId) {
@@ -4690,6 +4704,9 @@ var SqliteQueryBackend = class {
4690
4704
  node(ctx, id, sessionId) {
4691
4705
  return this.db.getNode(this.resolveSession(ctx, sessionId), id);
4692
4706
  }
4707
+ edges(ctx, sessionId) {
4708
+ return this.db.getEdges(this.resolveSession(ctx, sessionId));
4709
+ }
4693
4710
  dependencies(ctx, id, q, sessionId) {
4694
4711
  const sid = this.resolveSession(ctx, sessionId);
4695
4712
  return this.db.getDependencies(sid, id, {
@@ -4940,7 +4957,7 @@ var ContributorSchema = import_zod5.z.object({
4940
4957
  });
4941
4958
  var IngestEnvelopeSchema = import_zod5.z.object({
4942
4959
  schemaVersion: import_zod5.z.literal(INGEST_SCHEMA_VERSION),
4943
- org: import_zod5.z.string().min(1).optional(),
4960
+ org: import_zod5.z.string().min(1).max(128).optional(),
4944
4961
  items: import_zod5.z.array(import_zod5.z.object({
4945
4962
  contentHash: import_zod5.z.string(),
4946
4963
  kind: import_zod5.z.enum(["node", "edge"]),
@@ -5028,6 +5045,7 @@ function ingestEnvelope(store, envelope, opts = {}) {
5028
5045
 
5029
5046
  // src/central/server.ts
5030
5047
  function createIngestHandler(store, opts = {}) {
5048
+ const quota = opts.quota;
5031
5049
  return (body) => {
5032
5050
  const parsed = IngestEnvelopeSchema.safeParse(body);
5033
5051
  if (!parsed.success) {
@@ -5035,6 +5053,14 @@ function createIngestHandler(store, opts = {}) {
5035
5053
  logWarn("ingest: rejected invalid envelope", { issues });
5036
5054
  return { status: 400, body: { error: "invalid envelope", issues } };
5037
5055
  }
5056
+ if (quota) {
5057
+ const org = normalizeTenant(parsed.data.org ?? opts.defaultOrg);
5058
+ const decision = quota.take(org);
5059
+ if (!decision.allowed) {
5060
+ logWarn("ingest: rate limited", { org, retryAfterSec: decision.retryAfterSec });
5061
+ return { status: 429, body: { error: "too many requests" }, headers: { "retry-after": String(decision.retryAfterSec) } };
5062
+ }
5063
+ }
5038
5064
  try {
5039
5065
  const result = ingestEnvelope(store, parsed.data, opts);
5040
5066
  return { status: 200, body: result };
@@ -5045,6 +5071,39 @@ function createIngestHandler(store, opts = {}) {
5045
5071
  };
5046
5072
  }
5047
5073
 
5074
+ // src/central/quota.ts
5075
+ var DEFAULT_INGEST_QUOTA = { capacity: 120, refillMs: 6e4 };
5076
+ var MAX_KEYS = 1e4;
5077
+ var RateLimiter = class {
5078
+ constructor(cfg = DEFAULT_INGEST_QUOTA, now = () => Date.now()) {
5079
+ this.cfg = cfg;
5080
+ this.now = now;
5081
+ }
5082
+ buckets = /* @__PURE__ */ new Map();
5083
+ /** Consume one token for `key`. Returns whether the request is allowed (+ Retry-After when not). */
5084
+ take(key) {
5085
+ const t = this.now();
5086
+ const ratePerMs = this.cfg.capacity / this.cfg.refillMs;
5087
+ let b = this.buckets.get(key);
5088
+ if (!b) {
5089
+ if (this.buckets.size >= MAX_KEYS) {
5090
+ const oldest = this.buckets.keys().next().value;
5091
+ if (oldest !== void 0) this.buckets.delete(oldest);
5092
+ }
5093
+ b = { tokens: this.cfg.capacity, last: t };
5094
+ this.buckets.set(key, b);
5095
+ }
5096
+ b.tokens = Math.min(this.cfg.capacity, b.tokens + Math.max(0, t - b.last) * ratePerMs);
5097
+ b.last = t;
5098
+ if (b.tokens >= 1) {
5099
+ b.tokens -= 1;
5100
+ return { allowed: true, retryAfterSec: 0 };
5101
+ }
5102
+ const waitMs = (1 - b.tokens) / ratePerMs;
5103
+ return { allowed: false, retryAfterSec: Math.max(1, Math.ceil(waitMs / 1e3)) };
5104
+ }
5105
+ };
5106
+
5048
5107
  // src/scanners/bookmarks.ts
5049
5108
  var PERSONAL = [
5050
5109
  "facebook.",
@@ -5932,7 +5991,7 @@ async function resolveNlQuery(db, sessionId, search, raw, opts) {
5932
5991
 
5933
5992
  // src/mcp/server.ts
5934
5993
  var SERVER_NAME = "cartography";
5935
- var SERVER_VERSION = "2.5.0";
5994
+ var SERVER_VERSION = "2.7.0";
5936
5995
  var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
5937
5996
  var DATA_TYPES = NODE_TYPE_GROUPS.data;
5938
5997
  var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
@@ -6575,6 +6634,16 @@ async function runHttp(factory, opts = {}) {
6575
6634
  const httpServer = import_node_http.default.createServer(async (req, res) => {
6576
6635
  try {
6577
6636
  const url = req.url ?? "";
6637
+ const probePath = new URL(url || "/", "http://probe").pathname;
6638
+ if (probePath === "/healthz") {
6639
+ res.writeHead(200, { "content-type": "application/json" }).end('{"status":"ok"}');
6640
+ return;
6641
+ }
6642
+ if (probePath === "/readyz") {
6643
+ const r = opts.readiness ? opts.readiness() : { ready: true };
6644
+ res.writeHead(r.ready ? 200 : 503, { "content-type": "application/json" }).end(JSON.stringify({ status: r.ready ? "ready" : "unready" }));
6645
+ return;
6646
+ }
6578
6647
  const isIngest = url.startsWith("/ingest") && opts.onIngest !== void 0;
6579
6648
  if (!url.startsWith("/mcp") && !isIngest) {
6580
6649
  res.writeHead(404, { "content-type": "application/json" }).end('{"error":"not found"}');
@@ -6602,7 +6671,7 @@ async function runHttp(factory, opts = {}) {
6602
6671
  return;
6603
6672
  }
6604
6673
  const out = onIngest(value);
6605
- res.writeHead(out.status, { "content-type": "application/json" }).end(JSON.stringify(out.body));
6674
+ res.writeHead(out.status, { "content-type": "application/json", ...out.headers ?? {} }).end(JSON.stringify(out.body));
6606
6675
  return;
6607
6676
  }
6608
6677
  const sessionId = req.headers["mcp-session-id"];
@@ -7465,6 +7534,54 @@ function headerValue(req, name) {
7465
7534
  return v;
7466
7535
  }
7467
7536
 
7537
+ // src/backstage.ts
7538
+ var COMPONENT_TYPES = ["web_service", "container", "pod"];
7539
+ function sanitize(id) {
7540
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
7541
+ }
7542
+ function toBackstageEntities(nodes, edges, opts = {}) {
7543
+ const owner = opts.org ?? "unknown";
7544
+ return nodes.map((node) => {
7545
+ const kind = COMPONENT_TYPES.includes(node.type) ? "Component" : node.type === "api_endpoint" ? "API" : "Resource";
7546
+ const dependsOn = edges.filter((e) => e.sourceId === node.id).map((e) => `resource:default/${sanitize(e.targetId)}`);
7547
+ return {
7548
+ apiVersion: "backstage.io/v1alpha1",
7549
+ kind,
7550
+ metadata: {
7551
+ name: sanitize(node.id),
7552
+ annotations: {
7553
+ "cartography/discovered-at": node.discoveredAt,
7554
+ "cartography/confidence": String(node.confidence)
7555
+ }
7556
+ },
7557
+ spec: {
7558
+ type: node.type,
7559
+ lifecycle: "production",
7560
+ owner: node.owner ?? owner,
7561
+ ...dependsOn.length > 0 ? { dependsOn } : {}
7562
+ }
7563
+ };
7564
+ });
7565
+ }
7566
+ function entitiesToYaml(entities) {
7567
+ return entities.map((e) => {
7568
+ const lines = [
7569
+ `apiVersion: ${e.apiVersion}`,
7570
+ `kind: ${e.kind}`,
7571
+ `metadata:`,
7572
+ ` name: ${e.metadata.name}`,
7573
+ ` annotations:`,
7574
+ ...Object.entries(e.metadata.annotations).map(([k, v]) => ` ${k}: "${v}"`),
7575
+ `spec:`,
7576
+ ` type: ${e.spec.type}`,
7577
+ ` lifecycle: ${e.spec.lifecycle}`,
7578
+ ` owner: ${e.spec.owner}`,
7579
+ ...e.spec.dependsOn && e.spec.dependsOn.length > 0 ? [" dependsOn:", ...e.spec.dependsOn.map((d) => ` - ${d}`)] : []
7580
+ ];
7581
+ return lines.join("\n");
7582
+ }).join("\n---\n");
7583
+ }
7584
+
7468
7585
  // src/api/schemas.ts
7469
7586
  var import_zod8 = require("zod");
7470
7587
  var DIRECTIONS = ["downstream", "upstream", "both"];
@@ -7600,6 +7717,21 @@ var ErrorResponse = import_zod8.z.object({
7600
7717
  error: import_zod8.z.string(),
7601
7718
  code: import_zod8.z.string().optional()
7602
7719
  });
7720
+ var BackstageEntitySchema = import_zod8.z.object({
7721
+ apiVersion: import_zod8.z.literal("backstage.io/v1alpha1"),
7722
+ kind: import_zod8.z.enum(["Component", "API", "Resource"]),
7723
+ metadata: import_zod8.z.object({
7724
+ name: import_zod8.z.string(),
7725
+ annotations: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.string())
7726
+ }),
7727
+ spec: import_zod8.z.object({
7728
+ type: import_zod8.z.string(),
7729
+ lifecycle: import_zod8.z.string(),
7730
+ owner: import_zod8.z.string(),
7731
+ dependsOn: import_zod8.z.array(import_zod8.z.string()).optional()
7732
+ })
7733
+ });
7734
+ var BackstageCatalogResponse = import_zod8.z.object({ entities: import_zod8.z.array(BackstageEntitySchema) });
7603
7735
  var API_SCHEMAS = {
7604
7736
  Node: NodeSchema2,
7605
7737
  Edge: EdgeSchema2,
@@ -7611,10 +7743,13 @@ var API_SCHEMAS = {
7611
7743
  Session: SessionSchema,
7612
7744
  Sessions: SessionsResponse,
7613
7745
  Health: HealthResponse,
7614
- Error: ErrorResponse
7746
+ Error: ErrorResponse,
7747
+ BackstageEntity: BackstageEntitySchema,
7748
+ BackstageCatalog: BackstageCatalogResponse
7615
7749
  };
7616
7750
 
7617
7751
  // src/api/rest.ts
7752
+ var BACKSTAGE_NODE_CAP = 1e3;
7618
7753
  function toApiNode(n) {
7619
7754
  const out = { id: n.id, type: n.type, name: n.name, confidence: n.confidence, tags: n.tags };
7620
7755
  if (n.domain !== void 0) out["domain"] = n.domain;
@@ -7749,6 +7884,14 @@ function handleHealth(ctx, d) {
7749
7884
  const h = d.backend.health(ctx);
7750
7885
  return ok(validateOut(HealthResponse, { status: "ok", version: d.version, store: h.store, sessions: h.sessions }));
7751
7886
  }
7887
+ function handleBackstageCatalog(ctx, d) {
7888
+ return guard(() => {
7889
+ const page = d.backend.nodes(ctx, { limit: BACKSTAGE_NODE_CAP });
7890
+ const edges = d.backend.edges(ctx);
7891
+ const entities = toBackstageEntities(page.nodes, edges, { org: ctx.tenant });
7892
+ return ok(validateOut(BackstageCatalogResponse, { entities }));
7893
+ });
7894
+ }
7752
7895
 
7753
7896
  // src/api/openapi.ts
7754
7897
  function defOf(schema) {
@@ -7905,6 +8048,13 @@ function buildOpenApiDocument(opts) {
7905
8048
  parameters: [TENANT_PARAM],
7906
8049
  responses: { "200": ok2("Sessions", "Sessions"), ...errorResponses() }
7907
8050
  }
8051
+ },
8052
+ "/v1/backstage/catalog": {
8053
+ get: {
8054
+ summary: "The tenant topology as Backstage catalog entities (live data source, 4.6)",
8055
+ parameters: [SESSION_PARAM, TENANT_PARAM],
8056
+ responses: { "200": ok2("BackstageCatalog", "Backstage catalog entities"), ...errorResponses() }
8057
+ }
7908
8058
  }
7909
8059
  }
7910
8060
  };
@@ -8419,6 +8569,8 @@ function dispatchRest(ctx, path, url, deps) {
8419
8569
  return handleDiff(ctx, url, deps);
8420
8570
  case "/v1/sessions":
8421
8571
  return handleSessions(ctx, deps);
8572
+ case "/v1/backstage/catalog":
8573
+ return handleBackstageCatalog(ctx, deps);
8422
8574
  default: {
8423
8575
  const m = DEPENDENCIES_RE.exec(path);
8424
8576
  if (m) return handleDependencies(ctx, decodeURIComponent(m[1]), url, deps);
@@ -9925,7 +10077,7 @@ var MERMAID_CLASSES = {
9925
10077
  saas_tool: "fill:#2a1a2a,stroke:#9a3a9a,color:#daf",
9926
10078
  unknown: "fill:#2a2a2a,stroke:#5a5a5a,color:#aaa"
9927
10079
  };
9928
- function sanitize(id) {
10080
+ function sanitize2(id) {
9929
10081
  return id.replace(/[^a-zA-Z0-9_]/g, "_");
9930
10082
  }
9931
10083
  function nodeLabel(node) {
@@ -9967,14 +10119,14 @@ function generateTopologyMermaid(nodes, edges) {
9967
10119
  const label = LAYER_LABELS[layerKey] ?? layerKey;
9968
10120
  lines.push(` subgraph ${layerKey}["${label}"]`);
9969
10121
  for (const node of layerNodes) {
9970
- lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
10122
+ lines.push(` ${sanitize2(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
9971
10123
  }
9972
10124
  lines.push(" end");
9973
10125
  lines.push("");
9974
10126
  }
9975
10127
  for (const edge of edges) {
9976
- const src = sanitize(edge.sourceId);
9977
- const tgt = sanitize(edge.targetId);
10128
+ const src = sanitize2(edge.sourceId);
10129
+ const tgt = sanitize2(edge.targetId);
9978
10130
  const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
9979
10131
  const arrow = edge.confidence < 0.6 ? `-. "${label}" .->` : `-->|"${label}"|`;
9980
10132
  lines.push(` ${src} ${arrow} ${tgt}`);
@@ -10000,12 +10152,12 @@ function generateDependencyMermaid(nodes, edges) {
10000
10152
  }
10001
10153
  lines.push("");
10002
10154
  for (const node of usedNodes) {
10003
- lines.push(` ${sanitize(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
10155
+ lines.push(` ${sanitize2(node.id)}${nodeLabel(node)}:::${node.type.replace(/_/g, "")}`);
10004
10156
  }
10005
10157
  lines.push("");
10006
10158
  for (const edge of depEdges) {
10007
10159
  const label = EDGE_LABELS[edge.relationship] ?? edge.relationship;
10008
- lines.push(` ${sanitize(edge.sourceId)} -->|"${label}"| ${sanitize(edge.targetId)}`);
10160
+ lines.push(` ${sanitize2(edge.sourceId)} -->|"${label}"| ${sanitize2(edge.targetId)}`);
10009
10161
  }
10010
10162
  return lines.join("\n");
10011
10163
  }
@@ -10056,44 +10208,21 @@ function generateDiffMermaid(diff) {
10056
10208
  ensureEndpoint(e.targetId);
10057
10209
  }
10058
10210
  for (const { node, cls, suffix } of entries.values()) {
10059
- lines.push(` ${sanitize(node.id)}${diffNodeLabel(node, suffix)}:::${cls}`);
10211
+ lines.push(` ${sanitize2(node.id)}${diffNodeLabel(node, suffix)}:::${cls}`);
10060
10212
  }
10061
10213
  lines.push("");
10062
10214
  for (const e of diff.edges.added) {
10063
10215
  const label = EDGE_LABELS[e.relationship] ?? e.relationship;
10064
- lines.push(` ${sanitize(e.sourceId)} ==>|"+ ${label}"| ${sanitize(e.targetId)}`);
10216
+ lines.push(` ${sanitize2(e.sourceId)} ==>|"+ ${label}"| ${sanitize2(e.targetId)}`);
10065
10217
  }
10066
10218
  for (const e of diff.edges.removed) {
10067
10219
  const label = EDGE_LABELS[e.relationship] ?? e.relationship;
10068
- lines.push(` ${sanitize(e.sourceId)} -.->|"- ${label}"| ${sanitize(e.targetId)}`);
10220
+ lines.push(` ${sanitize2(e.sourceId)} -.->|"- ${label}"| ${sanitize2(e.targetId)}`);
10069
10221
  }
10070
10222
  return lines.join("\n");
10071
10223
  }
10072
10224
  function exportBackstageYAML(nodes, edges, org) {
10073
- const owner = org ?? "unknown";
10074
- const docs = [];
10075
- for (const node of nodes) {
10076
- const isComponent = ["web_service", "container", "pod"].includes(node.type);
10077
- const isAPI = node.type === "api_endpoint";
10078
- const kind = isComponent ? "Component" : isAPI ? "API" : "Resource";
10079
- const deps = edges.filter((e) => e.sourceId === node.id).map((e) => ` - resource:default/${sanitize(e.targetId)}`);
10080
- const doc = [
10081
- `apiVersion: backstage.io/v1alpha1`,
10082
- `kind: ${kind}`,
10083
- `metadata:`,
10084
- ` name: ${sanitize(node.id)}`,
10085
- ` annotations:`,
10086
- ` cartography/discovered-at: "${node.discoveredAt}"`,
10087
- ` cartography/confidence: "${node.confidence}"`,
10088
- `spec:`,
10089
- ` type: ${node.type}`,
10090
- ` lifecycle: production`,
10091
- ` owner: ${node.owner ?? owner}`,
10092
- ...deps.length > 0 ? [" dependsOn:", ...deps] : []
10093
- ].join("\n");
10094
- docs.push(doc);
10095
- }
10096
- return docs.join("\n---\n");
10225
+ return entitiesToYaml(toBackstageEntities(nodes, edges, org !== void 0 ? { org } : {}));
10097
10226
  }
10098
10227
  function exportJSON(db, sessionId) {
10099
10228
  const nodes = db.getNodes(sessionId);
@@ -11794,6 +11923,7 @@ function checkClaudePrerequisites() {
11794
11923
  CredentialConfigSchema,
11795
11924
  CsvCostSource,
11796
11925
  DEFAULT_ANOMALY_THRESHOLDS,
11926
+ DEFAULT_INGEST_QUOTA,
11797
11927
  DEFAULT_SERVER_NAME,
11798
11928
  DEFAULT_TENANT,
11799
11929
  DriftConfigSchema,
@@ -11815,10 +11945,12 @@ function checkClaudePrerequisites() {
11815
11945
  ProviderRegistry,
11816
11946
  RELATION_TO_DIRECTION,
11817
11947
  ROLES,
11948
+ RateLimiter,
11818
11949
  RoleSchema,
11819
11950
  RuleCheckSchema,
11820
11951
  RulesetSchema,
11821
11952
  SCAN_ARG_PATTERNS,
11953
+ SCHEMA_VERSION,
11822
11954
  SDL,
11823
11955
  SEVERITY_WEIGHT,
11824
11956
  SHARING_LEVELS,
@@ -11898,6 +12030,7 @@ function checkClaudePrerequisites() {
11898
12030
  diffTopology,
11899
12031
  edgesToConnections,
11900
12032
  enrichCosts,
12033
+ entitiesToYaml,
11901
12034
  evaluateCheck,
11902
12035
  evaluateRule,
11903
12036
  evidenceLine,
@@ -12027,6 +12160,7 @@ function checkClaudePrerequisites() {
12027
12160
  startApi,
12028
12161
  stripSensitive,
12029
12162
  timingSafeEqual,
12163
+ toBackstageEntities,
12030
12164
  validateScanner,
12031
12165
  vscodeDeeplink,
12032
12166
  zodToJsonSchema