@neat.is/core 0.2.10 → 0.3.1

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.
@@ -0,0 +1,125 @@
1
+ import {
2
+ parseOtlpRequest
3
+ } from "./chunk-4ASCXBZF.js";
4
+
5
+ // src/otel-grpc.ts
6
+ import { fileURLToPath } from "url";
7
+ import path from "path";
8
+ import * as grpc from "@grpc/grpc-js";
9
+ import * as protoLoader from "@grpc/proto-loader";
10
+ function bytesToHex(buf) {
11
+ if (!buf) return "";
12
+ return Buffer.isBuffer(buf) ? buf.toString("hex") : "";
13
+ }
14
+ function nanosToString(n) {
15
+ if (n === void 0 || n === null) return "0";
16
+ return typeof n === "string" ? n : String(n);
17
+ }
18
+ function reshapeAttributes(attrs) {
19
+ const out = (attrs ?? []).map((kv) => ({
20
+ key: kv.key ?? "",
21
+ value: kv.value ? {
22
+ stringValue: kv.value.string_value,
23
+ boolValue: kv.value.bool_value,
24
+ intValue: kv.value.int_value,
25
+ doubleValue: kv.value.double_value,
26
+ arrayValue: kv.value.array_value ? {
27
+ values: (kv.value.array_value.values ?? []).map((v) => ({
28
+ stringValue: v.string_value,
29
+ boolValue: v.bool_value,
30
+ intValue: v.int_value,
31
+ doubleValue: v.double_value
32
+ }))
33
+ } : void 0
34
+ } : void 0
35
+ }));
36
+ return out;
37
+ }
38
+ function reshapeGrpcRequest(req) {
39
+ return {
40
+ resourceSpans: (req.resource_spans ?? []).map((rs) => ({
41
+ resource: rs.resource ? { attributes: reshapeAttributes(rs.resource.attributes) } : void 0,
42
+ scopeSpans: (rs.scope_spans ?? []).map((ss) => ({
43
+ spans: (ss.spans ?? []).map((s) => ({
44
+ traceId: bytesToHex(s.trace_id),
45
+ spanId: bytesToHex(s.span_id),
46
+ parentSpanId: s.parent_span_id ? bytesToHex(s.parent_span_id) : void 0,
47
+ name: s.name,
48
+ kind: s.kind,
49
+ startTimeUnixNano: nanosToString(s.start_time_unix_nano),
50
+ endTimeUnixNano: nanosToString(s.end_time_unix_nano),
51
+ attributes: reshapeAttributes(s.attributes),
52
+ events: (s.events ?? []).map((e) => ({
53
+ name: e.name,
54
+ timeUnixNano: nanosToString(e.time_unix_nano),
55
+ attributes: reshapeAttributes(e.attributes)
56
+ })),
57
+ status: s.status ? { code: s.status.code, message: s.status.message } : void 0
58
+ }))
59
+ }))
60
+ }))
61
+ };
62
+ }
63
+ function resolveProtoRoot() {
64
+ const here = path.dirname(fileURLToPath(import.meta.url));
65
+ return path.resolve(here, "..", "proto");
66
+ }
67
+ function loadTraceService() {
68
+ const protoRoot = resolveProtoRoot();
69
+ const def = protoLoader.loadSync(
70
+ "opentelemetry/proto/collector/trace/v1/trace_service.proto",
71
+ {
72
+ keepCase: true,
73
+ longs: String,
74
+ enums: Number,
75
+ defaults: true,
76
+ oneofs: true,
77
+ includeDirs: [protoRoot]
78
+ }
79
+ );
80
+ const pkg = grpc.loadPackageDefinition(def);
81
+ return pkg.opentelemetry.proto.collector.trace.v1.TraceService.service;
82
+ }
83
+ async function startOtelGrpcReceiver(opts) {
84
+ const server = new grpc.Server();
85
+ const service = loadTraceService();
86
+ server.addService(service, {
87
+ Export: (call, callback) => {
88
+ void (async () => {
89
+ try {
90
+ const reshaped = reshapeGrpcRequest(call.request ?? {});
91
+ const spans = parseOtlpRequest(reshaped);
92
+ for (const span of spans) {
93
+ await opts.onSpan(span);
94
+ }
95
+ callback(null, { partial_success: {} });
96
+ } catch (err) {
97
+ callback({
98
+ code: grpc.status.INTERNAL,
99
+ message: err instanceof Error ? err.message : String(err)
100
+ });
101
+ }
102
+ })();
103
+ }
104
+ });
105
+ const host = opts.host ?? "0.0.0.0";
106
+ const port = opts.port ?? 4317;
107
+ const boundPort = await new Promise((resolve, reject) => {
108
+ server.bindAsync(`${host}:${port}`, grpc.ServerCredentials.createInsecure(), (err, p) => {
109
+ if (err) return reject(err);
110
+ resolve(p);
111
+ });
112
+ });
113
+ return {
114
+ address: `${host}:${boundPort}`,
115
+ stop: () => new Promise((resolve) => {
116
+ server.tryShutdown(() => resolve());
117
+ })
118
+ };
119
+ }
120
+
121
+ export {
122
+ reshapeGrpcRequest,
123
+ startOtelGrpcReceiver
124
+ };
125
+ //# sourceMappingURL=chunk-G3PDTGOW.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/otel-grpc.ts"],"sourcesContent":["import { fileURLToPath } from 'node:url'\nimport path from 'node:path'\nimport * as grpc from '@grpc/grpc-js'\nimport * as protoLoader from '@grpc/proto-loader'\nimport {\n parseOtlpRequest,\n type OtlpTracesRequest,\n type ParsedSpan,\n type SpanHandler,\n} from './otel.js'\n\n// OTLP/gRPC receiver. Sits next to buildOtelReceiver (HTTP/JSON) in otel.ts;\n// shares the same parseOtlpRequest decoder so a span looks identical to the\n// downstream onSpan handler whether it came in over JSON or protobuf.\n//\n// Default OFF — opts.enabled (typically NEAT_OTLP_GRPC=true) decides whether\n// server.ts wires this up. We keep gRPC behind a flag so existing HTTP-only\n// deployments don't get a surprise port binding on upgrade.\n\nexport interface BuildOtelGrpcReceiverOptions {\n onSpan: SpanHandler\n}\n\n// proto-loader output for the trace service has fields like resource_spans,\n// scope_spans, span_id, etc. (snake_case keys, since we leave keepCase: true\n// when loading). The HTTP path uses camelCase JSON, so we shape-shift the\n// gRPC payload onto the HTTP shape and let parseOtlpRequest do the rest.\n//\n// All `bytes` fields arrive as Buffers; the HTTP wire format encodes them as\n// hex strings, so we hex-encode for consistency.\n\ninterface GrpcAnyValue {\n string_value?: string\n bool_value?: boolean\n int_value?: string | number\n double_value?: number\n array_value?: { values?: GrpcAnyValue[] }\n // bytes/kvlist fields exist in the proto but the demo doesn't use them.\n}\n\ninterface GrpcKeyValue {\n key?: string\n value?: GrpcAnyValue\n}\n\ninterface GrpcStatus {\n code?: number\n message?: string\n}\n\ninterface GrpcEvent {\n name?: string\n time_unix_nano?: string | number\n attributes?: GrpcKeyValue[]\n}\n\ninterface GrpcSpan {\n trace_id?: Buffer\n span_id?: Buffer\n parent_span_id?: Buffer\n name?: string\n kind?: number\n start_time_unix_nano?: string | number\n end_time_unix_nano?: string | number\n attributes?: GrpcKeyValue[]\n events?: GrpcEvent[]\n status?: GrpcStatus\n}\n\ninterface GrpcScopeSpans {\n spans?: GrpcSpan[]\n}\n\ninterface GrpcResourceSpans {\n resource?: { attributes?: GrpcKeyValue[] }\n scope_spans?: GrpcScopeSpans[]\n}\n\ninterface GrpcExportRequest {\n resource_spans?: GrpcResourceSpans[]\n}\n\nfunction bytesToHex(buf: Buffer | undefined): string {\n if (!buf) return ''\n return Buffer.isBuffer(buf) ? buf.toString('hex') : ''\n}\n\nfunction nanosToString(n: string | number | undefined): string {\n if (n === undefined || n === null) return '0'\n return typeof n === 'string' ? n : String(n)\n}\n\nfunction reshapeAttributes(\n attrs: GrpcKeyValue[] | undefined,\n): OtlpTracesRequest['resourceSpans'] extends Array<infer R>\n ? R extends { resource?: { attributes?: infer A } }\n ? A\n : never\n : never {\n // Map snake_case oneof fields to the camelCase the JSON path expects.\n const out = (attrs ?? []).map((kv) => ({\n key: kv.key ?? '',\n value: kv.value\n ? {\n stringValue: kv.value.string_value,\n boolValue: kv.value.bool_value,\n intValue: kv.value.int_value,\n doubleValue: kv.value.double_value,\n arrayValue: kv.value.array_value\n ? {\n values: (kv.value.array_value.values ?? []).map((v) => ({\n stringValue: v.string_value,\n boolValue: v.bool_value,\n intValue: v.int_value,\n doubleValue: v.double_value,\n })),\n }\n : undefined,\n }\n : undefined,\n }))\n return out as never\n}\n\nexport function reshapeGrpcRequest(req: GrpcExportRequest): OtlpTracesRequest {\n return {\n resourceSpans: (req.resource_spans ?? []).map((rs) => ({\n resource: rs.resource ? { attributes: reshapeAttributes(rs.resource.attributes) } : undefined,\n scopeSpans: (rs.scope_spans ?? []).map((ss) => ({\n spans: (ss.spans ?? []).map((s) => ({\n traceId: bytesToHex(s.trace_id),\n spanId: bytesToHex(s.span_id),\n parentSpanId: s.parent_span_id ? bytesToHex(s.parent_span_id) : undefined,\n name: s.name,\n kind: s.kind,\n startTimeUnixNano: nanosToString(s.start_time_unix_nano),\n endTimeUnixNano: nanosToString(s.end_time_unix_nano),\n attributes: reshapeAttributes(s.attributes),\n events: (s.events ?? []).map((e) => ({\n name: e.name,\n timeUnixNano: nanosToString(e.time_unix_nano),\n attributes: reshapeAttributes(e.attributes),\n })),\n status: s.status ? { code: s.status.code, message: s.status.message } : undefined,\n })),\n })),\n })),\n }\n}\n\n// Find the bundled .proto tree at packages/core/proto/. The dev server runs\n// from the source tree (tsx); the built bundles run from dist/. tsup keeps\n// source layout, so __dirname-relative resolution works for both — we look two\n// levels up from this file.\nfunction resolveProtoRoot(): string {\n // Built output (CJS) sets __dirname natively; ESM build is bundled by tsup\n // and keeps a dirname injection. import.meta.url is the safe bet.\n const here = path.dirname(fileURLToPath(import.meta.url))\n // src/ → packages/core/proto/, dist/ → packages/core/proto/.\n return path.resolve(here, '..', 'proto')\n}\n\nfunction loadTraceService(): grpc.ServiceDefinition {\n const protoRoot = resolveProtoRoot()\n const def = protoLoader.loadSync(\n 'opentelemetry/proto/collector/trace/v1/trace_service.proto',\n {\n keepCase: true,\n longs: String,\n enums: Number,\n defaults: true,\n oneofs: true,\n includeDirs: [protoRoot],\n },\n )\n const pkg = grpc.loadPackageDefinition(def) as unknown as {\n opentelemetry: {\n proto: {\n collector: {\n trace: {\n v1: {\n TraceService: { service: grpc.ServiceDefinition }\n }\n }\n }\n }\n }\n }\n return pkg.opentelemetry.proto.collector.trace.v1.TraceService.service\n}\n\nexport interface OtelGrpcReceiver {\n // Bound address (host:port) once .start() has resolved. Useful for tests.\n address: string\n // Stop accepting new requests, shut down the server.\n stop: () => Promise<void>\n}\n\nexport async function startOtelGrpcReceiver(\n opts: BuildOtelGrpcReceiverOptions & { host?: string; port?: number },\n): Promise<OtelGrpcReceiver> {\n const server = new grpc.Server()\n const service = loadTraceService()\n\n server.addService(service, {\n Export: (\n call: grpc.ServerUnaryCall<GrpcExportRequest, unknown>,\n callback: grpc.sendUnaryData<{ partial_success: object }>,\n ) => {\n void (async () => {\n try {\n const reshaped = reshapeGrpcRequest(call.request ?? {})\n const spans: ParsedSpan[] = parseOtlpRequest(reshaped)\n for (const span of spans) {\n await opts.onSpan(span)\n }\n callback(null, { partial_success: {} })\n } catch (err) {\n callback({\n code: grpc.status.INTERNAL,\n message: err instanceof Error ? err.message : String(err),\n })\n }\n })()\n },\n })\n\n const host = opts.host ?? '0.0.0.0'\n const port = opts.port ?? 4317\n\n const boundPort = await new Promise<number>((resolve, reject) => {\n server.bindAsync(`${host}:${port}`, grpc.ServerCredentials.createInsecure(), (err, p) => {\n if (err) return reject(err)\n resolve(p)\n })\n })\n\n return {\n address: `${host}:${boundPort}`,\n stop: () =>\n new Promise<void>((resolve) => {\n server.tryShutdown(() => resolve())\n }),\n }\n}\n"],"mappings":"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","names":[]}
package/dist/cli.cjs CHANGED
@@ -4406,6 +4406,10 @@ async function addProject(opts) {
4406
4406
  return entry2;
4407
4407
  });
4408
4408
  }
4409
+ async function getProject(name) {
4410
+ const reg = await readRegistry();
4411
+ return reg.projects.find((p) => p.name === name);
4412
+ }
4409
4413
  async function listProjects() {
4410
4414
  const reg = await readRegistry();
4411
4415
  return reg.projects;
@@ -4542,9 +4546,15 @@ function registerRoutes(scope, ctx) {
4542
4546
  scope.get("/health", async (req, reply) => {
4543
4547
  const proj = resolveProject(registry, req, reply);
4544
4548
  if (!proj) return;
4549
+ const uptimeMs = Date.now() - startedAt;
4545
4550
  return {
4546
- uptime: Math.floor((Date.now() - startedAt) / 1e3),
4551
+ ok: true,
4547
4552
  project: proj.name,
4553
+ uptimeMs,
4554
+ // Legacy fields kept additively. The web shell's StatusBar reads
4555
+ // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
4556
+ // the canonical triple and lets the extras pass through.
4557
+ uptime: Math.floor(uptimeMs / 1e3),
4548
4558
  nodeCount: proj.graph.order,
4549
4559
  edgeCount: proj.graph.size,
4550
4560
  lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
@@ -4564,7 +4574,7 @@ function registerRoutes(scope, ctx) {
4564
4574
  if (!proj.graph.hasNode(id)) {
4565
4575
  return reply.code(404).send({ error: "node not found", id });
4566
4576
  }
4567
- return proj.graph.getNodeAttributes(id);
4577
+ return { node: proj.graph.getNodeAttributes(id) };
4568
4578
  }
4569
4579
  );
4570
4580
  scope.get(
@@ -4581,12 +4591,12 @@ function registerRoutes(scope, ctx) {
4581
4591
  return { inbound, outbound };
4582
4592
  }
4583
4593
  );
4584
- scope.get("/graph/node/:id/dependencies", async (req, reply) => {
4594
+ scope.get("/graph/dependencies/:nodeId", async (req, reply) => {
4585
4595
  const proj = resolveProject(registry, req, reply);
4586
4596
  if (!proj) return;
4587
- const { id } = req.params;
4588
- if (!proj.graph.hasNode(id)) {
4589
- return reply.code(404).send({ error: "node not found", id });
4597
+ const { nodeId } = req.params;
4598
+ if (!proj.graph.hasNode(nodeId)) {
4599
+ return reply.code(404).send({ error: "node not found", id: nodeId });
4590
4600
  }
4591
4601
  const depth = req.query.depth ? Number(req.query.depth) : TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH;
4592
4602
  if (!Number.isFinite(depth) || depth < 1 || depth > TRANSITIVE_DEPENDENCIES_MAX_DEPTH) {
@@ -4594,7 +4604,7 @@ function registerRoutes(scope, ctx) {
4594
4604
  error: `depth must be an integer in [1, ${TRANSITIVE_DEPENDENCIES_MAX_DEPTH}]`
4595
4605
  });
4596
4606
  }
4597
- return getTransitiveDependencies(proj.graph, id, depth);
4607
+ return getTransitiveDependencies(proj.graph, nodeId, depth);
4598
4608
  });
4599
4609
  scope.get("/graph/divergences", async (req, reply) => {
4600
4610
  const proj = resolveProject(registry, req, reply);
@@ -4635,19 +4645,26 @@ function registerRoutes(scope, ctx) {
4635
4645
  const proj = resolveProject(registry, req, reply);
4636
4646
  if (!proj) return;
4637
4647
  const epath = errorsPathFor(proj);
4638
- if (!epath) return [];
4639
- return readErrorEvents(epath);
4648
+ if (!epath) return { count: 0, total: 0, events: [] };
4649
+ const events = await readErrorEvents(epath);
4650
+ const total = events.length;
4651
+ const limit = req.query.limit ? Number(req.query.limit) : 50;
4652
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
4653
+ const sliced = events.slice(0, safeLimit);
4654
+ return { count: sliced.length, total, events: sliced };
4640
4655
  });
4641
- scope.get("/incidents/stale", async (req, reply) => {
4656
+ scope.get("/stale-events", async (req, reply) => {
4642
4657
  const proj = resolveProject(registry, req, reply);
4643
4658
  if (!proj) return;
4644
4659
  const spath = staleEventsPathFor(proj);
4645
- if (!spath) return [];
4660
+ if (!spath) return { count: 0, total: 0, events: [] };
4646
4661
  const events = await readStaleEvents(spath);
4647
4662
  const filtered = req.query.edgeType ? events.filter((e) => e.edgeType === req.query.edgeType) : events;
4648
4663
  const ordered = [...filtered].reverse();
4664
+ const total = ordered.length;
4649
4665
  const limit = req.query.limit ? Number(req.query.limit) : 50;
4650
- return ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
4666
+ const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
4667
+ return { count: sliced.length, total, events: sliced };
4651
4668
  });
4652
4669
  scope.get(
4653
4670
  "/incidents/:nodeId",
@@ -4659,14 +4676,15 @@ function registerRoutes(scope, ctx) {
4659
4676
  return reply.code(404).send({ error: "node not found", id: nodeId });
4660
4677
  }
4661
4678
  const epath = errorsPathFor(proj);
4662
- if (!epath) return [];
4679
+ if (!epath) return { count: 0, total: 0, events: [] };
4663
4680
  const events = await readErrorEvents(epath);
4664
- return events.filter(
4681
+ const filtered = events.filter(
4665
4682
  (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
4666
4683
  );
4684
+ return { count: filtered.length, total: filtered.length, events: filtered };
4667
4685
  }
4668
4686
  );
4669
- scope.get("/traverse/root-cause/:nodeId", async (req, reply) => {
4687
+ scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
4670
4688
  const proj = resolveProject(registry, req, reply);
4671
4689
  if (!proj) return;
4672
4690
  const { nodeId } = req.params;
@@ -4686,7 +4704,7 @@ function registerRoutes(scope, ctx) {
4686
4704
  if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
4687
4705
  return result;
4688
4706
  });
4689
- scope.get("/traverse/blast-radius/:nodeId", async (req, reply) => {
4707
+ scope.get("/graph/blast-radius/:nodeId", async (req, reply) => {
4690
4708
  const proj = resolveProject(registry, req, reply);
4691
4709
  if (!proj) return;
4692
4710
  const { nodeId } = req.params;
@@ -4796,7 +4814,7 @@ function registerRoutes(scope, ctx) {
4796
4814
  if (req.query.policyId) {
4797
4815
  violations = violations.filter((v) => v.policyId === req.query.policyId);
4798
4816
  }
4799
- return violations;
4817
+ return { violations };
4800
4818
  });
4801
4819
  scope.post("/policies/check", async (req, reply) => {
4802
4820
  const proj = resolveProject(registry, req, reply);
@@ -4875,6 +4893,20 @@ async function buildApi(opts) {
4875
4893
  });
4876
4894
  }
4877
4895
  });
4896
+ app.get("/projects/:project", async (req, reply) => {
4897
+ try {
4898
+ const entry2 = await getProject(req.params.project);
4899
+ if (!entry2) {
4900
+ return reply.code(404).send({ error: "project not found", project: req.params.project });
4901
+ }
4902
+ return { project: entry2 };
4903
+ } catch (err) {
4904
+ return reply.code(500).send({
4905
+ error: "failed to read project registry",
4906
+ details: err.message
4907
+ });
4908
+ }
4909
+ });
4878
4910
  registerRoutes(app, routeCtx);
4879
4911
  await app.register(
4880
4912
  async (scope) => {
@@ -5962,7 +5994,7 @@ async function runRootCause(client, input) {
5962
5994
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
5963
5995
  const path38 = projectPath(
5964
5996
  input.project,
5965
- `/traverse/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
5997
+ `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
5966
5998
  );
5967
5999
  try {
5968
6000
  const result = await client.get(path38);
@@ -5993,7 +6025,7 @@ async function runBlastRadius(client, input) {
5993
6025
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
5994
6026
  const path38 = projectPath(
5995
6027
  input.project,
5996
- `/traverse/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
6028
+ `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
5997
6029
  );
5998
6030
  try {
5999
6031
  const result = await client.get(path38);
@@ -6032,7 +6064,7 @@ async function runDependencies(client, input) {
6032
6064
  const depth = input.depth ?? 3;
6033
6065
  const path38 = projectPath(
6034
6066
  input.project,
6035
- `/graph/node/${encodeURIComponent(input.nodeId)}/dependencies?depth=${depth}`
6067
+ `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
6036
6068
  );
6037
6069
  try {
6038
6070
  const result = await client.get(path38);
@@ -6118,7 +6150,8 @@ function formatDuration(ms) {
6118
6150
  async function runIncidents(client, input) {
6119
6151
  const path38 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
6120
6152
  try {
6121
- const events = await client.get(path38);
6153
+ const body = await client.get(path38);
6154
+ const events = body.events;
6122
6155
  if (events.length === 0) {
6123
6156
  return {
6124
6157
  summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
@@ -6132,7 +6165,7 @@ async function runIncidents(client, input) {
6132
6165
  }
6133
6166
  const target = input.nodeId ?? "the project";
6134
6167
  return {
6135
- summary: `${target} has ${events.length} recorded incident${events.length === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
6168
+ summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
6136
6169
  block: blockLines.join("\n"),
6137
6170
  provenance: import_types22.Provenance.OBSERVED
6138
6171
  };
@@ -6228,9 +6261,10 @@ async function runStaleEdges(client, input) {
6228
6261
  if (input.limit !== void 0) params.set("limit", String(input.limit));
6229
6262
  if (input.edgeType) params.set("edgeType", input.edgeType);
6230
6263
  const qs = params.size > 0 ? `?${params.toString()}` : "";
6231
- const events = await client.get(
6232
- projectPath(input.project, `/incidents/stale${qs}`)
6264
+ const body = await client.get(
6265
+ projectPath(input.project, `/stale-events${qs}`)
6233
6266
  );
6267
+ const events = body.events;
6234
6268
  if (events.length === 0) {
6235
6269
  return {
6236
6270
  summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
@@ -6264,9 +6298,10 @@ async function runPolicies(client, input) {
6264
6298
  const params = new URLSearchParams();
6265
6299
  if (input.policyId) params.set("policyId", input.policyId);
6266
6300
  const qs = params.size > 0 ? `?${params.toString()}` : "";
6267
- violations = await client.get(
6301
+ const body = await client.get(
6268
6302
  projectPath(input.project, `/policies/violations${qs}`)
6269
6303
  );
6304
+ violations = body.violations;
6270
6305
  allowed = violations.every((v) => v.onViolation !== "block");
6271
6306
  }
6272
6307
  if (input.nodeId) {