@neat.is/core 0.4.16 → 0.4.18

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.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  mountBearerAuth,
3
3
  readAuthEnv
4
- } from "./chunk-6CO7C4IU.js";
4
+ } from "./chunk-BGPWBRLU.js";
5
5
 
6
6
  // src/graph.ts
7
7
  import GraphDefault from "graphology";
@@ -454,19 +454,19 @@ function confidenceFromMix(edges, now = Date.now()) {
454
454
  function longestIncomingWalk(graph, start, maxDepth) {
455
455
  let best = { path: [start], edges: [] };
456
456
  const visited = /* @__PURE__ */ new Set([start]);
457
- function step(node, path38, edges) {
458
- if (path38.length > best.path.length) {
459
- best = { path: [...path38], edges: [...edges] };
457
+ function step(node, path39, edges) {
458
+ if (path39.length > best.path.length) {
459
+ best = { path: [...path39], edges: [...edges] };
460
460
  }
461
- if (path38.length - 1 >= maxDepth) return;
461
+ if (path39.length - 1 >= maxDepth) return;
462
462
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
463
463
  for (const [srcId, edge] of incoming) {
464
464
  if (visited.has(srcId)) continue;
465
465
  visited.add(srcId);
466
- path38.push(srcId);
466
+ path39.push(srcId);
467
467
  edges.push(edge);
468
- step(srcId, path38, edges);
469
- path38.pop();
468
+ step(srcId, path39, edges);
469
+ path39.pop();
470
470
  edges.pop();
471
471
  visited.delete(srcId);
472
472
  }
@@ -1093,6 +1093,41 @@ function thresholdForEdgeType(edgeType, overrides) {
1093
1093
  const map = overrides ?? loadStaleThresholdsFromEnv();
1094
1094
  return map[edgeType] ?? FALLBACK_STALE_THRESHOLD_MS;
1095
1095
  }
1096
+ var DEFAULT_INCIDENT_THRESHOLDS = {
1097
+ threshold: 5,
1098
+ windowMs: 6e4
1099
+ };
1100
+ function loadIncidentThresholdsFromEnv() {
1101
+ const raw = process.env.NEAT_INCIDENT_THRESHOLDS;
1102
+ if (!raw) return DEFAULT_INCIDENT_THRESHOLDS;
1103
+ try {
1104
+ const overrides = JSON.parse(raw);
1105
+ const merged = { ...DEFAULT_INCIDENT_THRESHOLDS };
1106
+ if (typeof overrides.threshold === "number" && Number.isFinite(overrides.threshold) && overrides.threshold >= 1) {
1107
+ merged.threshold = Math.floor(overrides.threshold);
1108
+ }
1109
+ if (typeof overrides.windowMs === "number" && Number.isFinite(overrides.windowMs) && overrides.windowMs >= 0) {
1110
+ merged.windowMs = overrides.windowMs;
1111
+ }
1112
+ return merged;
1113
+ } catch (err) {
1114
+ console.warn(
1115
+ `[neat] NEAT_INCIDENT_THRESHOLDS could not be parsed (${err.message}); using defaults`
1116
+ );
1117
+ return DEFAULT_INCIDENT_THRESHOLDS;
1118
+ }
1119
+ }
1120
+ function httpResponseStatus(span) {
1121
+ for (const key of ["http.response.status_code", "http.status_code"]) {
1122
+ const v = span.attributes[key];
1123
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1124
+ if (typeof v === "string") {
1125
+ const n = Number(v);
1126
+ if (Number.isFinite(n)) return n;
1127
+ }
1128
+ }
1129
+ return void 0;
1130
+ }
1096
1131
  function nowIso(ctx) {
1097
1132
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1098
1133
  }
@@ -1506,6 +1541,71 @@ function makeErrorSpanWriter(errorsPath) {
1506
1541
  await fs3.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
1507
1542
  };
1508
1543
  }
1544
+ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
1545
+ const attrs = sanitizeAttributes(span.attributes);
1546
+ const first = firstTimestamp ?? timestamp;
1547
+ const peer = pickAddress(span);
1548
+ const message = count > 1 ? `${count} consecutive HTTP ${statusCode} responses` + (peer ? ` to ${peer}` : "") : `HTTP ${statusCode} response` + (peer ? ` from ${peer}` : "");
1549
+ const ev = {
1550
+ id: `${span.traceId}:${span.spanId}`,
1551
+ timestamp,
1552
+ service: span.service,
1553
+ traceId: span.traceId,
1554
+ spanId: span.spanId,
1555
+ errorType: "http-failure",
1556
+ errorMessage: message,
1557
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
1558
+ affectedNode,
1559
+ httpStatusCode: statusCode,
1560
+ incidentCount: count,
1561
+ firstTimestamp: first,
1562
+ lastTimestamp: timestamp
1563
+ };
1564
+ await appendErrorEvent(ctx, ev);
1565
+ }
1566
+ async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status) {
1567
+ const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
1568
+ if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
1569
+ const peer = pickAddress(span) ?? span.spanId;
1570
+ const key = `${span.service}->${peer}`;
1571
+ const existing = ctx.burstState.get(key);
1572
+ let state;
1573
+ if (existing && nowMs - existing.lastMs <= windowMs) {
1574
+ existing.count += 1;
1575
+ existing.lastTs = ts;
1576
+ existing.lastMs = nowMs;
1577
+ existing.codes.set(status, (existing.codes.get(status) ?? 0) + 1);
1578
+ state = existing;
1579
+ } else {
1580
+ state = {
1581
+ count: 1,
1582
+ firstTs: ts,
1583
+ lastTs: ts,
1584
+ lastMs: nowMs,
1585
+ codes: /* @__PURE__ */ new Map([[status, 1]])
1586
+ };
1587
+ ctx.burstState.set(key, state);
1588
+ }
1589
+ if (state.count < threshold) return;
1590
+ let dominant = status;
1591
+ let max = 0;
1592
+ for (const [code, n] of state.codes) {
1593
+ if (n > max) {
1594
+ max = n;
1595
+ dominant = code;
1596
+ }
1597
+ }
1598
+ await recordFailingResponseIncident(
1599
+ ctx,
1600
+ span,
1601
+ affectedNode,
1602
+ state.lastTs,
1603
+ dominant,
1604
+ state.count,
1605
+ state.firstTs
1606
+ );
1607
+ ctx.burstState.delete(key);
1608
+ }
1509
1609
  async function handleSpan(ctx, span) {
1510
1610
  const ts = span.startTimeIso ?? nowIso(ctx);
1511
1611
  const nowMs = ctx.now ? ctx.now() : Date.now();
@@ -1604,6 +1704,14 @@ async function handleSpan(ctx, span) {
1604
1704
  await appendErrorEvent(ctx, ev);
1605
1705
  }
1606
1706
  }
1707
+ if (span.statusCode !== 2) {
1708
+ const status = httpResponseStatus(span);
1709
+ if (status !== void 0 && status >= 500) {
1710
+ await recordFailingResponseIncident(ctx, span, sourceId, ts, status, 1);
1711
+ } else if (status !== void 0 && status >= 400 && spanMintsObservedEdge(span.kind)) {
1712
+ await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status);
1713
+ }
1714
+ }
1607
1715
  void affectedNode;
1608
1716
  if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
1609
1717
  }
@@ -3995,6 +4103,65 @@ function grpcEndpointsFromFile(file, serviceDir) {
3995
4103
  return out;
3996
4104
  }
3997
4105
 
4106
+ // src/extract/calls/supabase.ts
4107
+ import path27 from "path";
4108
+ import { infraId as infraId5 } from "@neat.is/types";
4109
+ var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
4110
+ var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
4111
+ var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
4112
+ function hostFromLiteral(literal) {
4113
+ if (!literal) return null;
4114
+ const m = /^https?:\/\/([^/:'"`\s]+)/.exec(literal.trim());
4115
+ if (!m) return null;
4116
+ const host = m[1];
4117
+ if (!/\.supabase\.(co|in)$/i.test(host)) return null;
4118
+ return host;
4119
+ }
4120
+ function readImports2(content) {
4121
+ return {
4122
+ hasSupabaseJs: SUPABASE_JS_IMPORT_RE.test(content),
4123
+ hasSupabaseSsr: SUPABASE_SSR_IMPORT_RE.test(content)
4124
+ };
4125
+ }
4126
+ function constructorMatchesImport(name, ctx) {
4127
+ if (name === "createClient") return ctx.hasSupabaseJs;
4128
+ return ctx.hasSupabaseSsr;
4129
+ }
4130
+ function supabaseEndpointsFromFile(file, serviceDir) {
4131
+ const ctx = readImports2(file.content);
4132
+ if (!ctx.hasSupabaseJs && !ctx.hasSupabaseSsr) return [];
4133
+ const out = [];
4134
+ const seen = /* @__PURE__ */ new Set();
4135
+ SUPABASE_CLIENT_RE.lastIndex = 0;
4136
+ let m;
4137
+ while ((m = SUPABASE_CLIENT_RE.exec(file.content)) !== null) {
4138
+ const ctor = m[1];
4139
+ if (!constructorMatchesImport(ctor, ctx)) continue;
4140
+ const host = hostFromLiteral(m[2]);
4141
+ const name = host ?? "env";
4142
+ if (seen.has(name)) continue;
4143
+ seen.add(name);
4144
+ const line = lineOf(file.content, m[0]);
4145
+ out.push({
4146
+ infraId: infraId5("supabase", name),
4147
+ name,
4148
+ kind: "supabase",
4149
+ edgeType: "CALLS",
4150
+ // `createClient(...)` from @supabase/supabase-js (or createServerClient /
4151
+ // createBrowserClient from @supabase/ssr) with the import in scope — a
4152
+ // framework-aware recognizer matched the SDK shape. Verified-call-site
4153
+ // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
4154
+ confidenceKind: "verified-call-site",
4155
+ evidence: {
4156
+ file: path27.relative(serviceDir, file.path),
4157
+ line,
4158
+ snippet: snippet(file.content, line)
4159
+ }
4160
+ });
4161
+ }
4162
+ return out;
4163
+ }
4164
+
3998
4165
  // src/extract/calls/index.ts
3999
4166
  function edgeTypeFromEndpoint(ep) {
4000
4167
  switch (ep.edgeType) {
@@ -4023,6 +4190,7 @@ async function addExternalEndpointEdges(graph, services) {
4023
4190
  endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
4024
4191
  endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
4025
4192
  endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
4193
+ endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
4026
4194
  }
4027
4195
  if (endpoints.length === 0) continue;
4028
4196
  const seenEdges = /* @__PURE__ */ new Set();
@@ -4093,14 +4261,14 @@ async function addCallEdges(graph, services) {
4093
4261
  }
4094
4262
 
4095
4263
  // src/extract/infra/docker-compose.ts
4096
- import path27 from "path";
4264
+ import path28 from "path";
4097
4265
  import { EdgeType as EdgeType10, Provenance as Provenance8, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
4098
4266
 
4099
4267
  // src/extract/infra/shared.ts
4100
- import { NodeType as NodeType10, infraId as infraId5 } from "@neat.is/types";
4268
+ import { NodeType as NodeType10, infraId as infraId6 } from "@neat.is/types";
4101
4269
  function makeInfraNode(kind, name, provider = "self", extras) {
4102
4270
  return {
4103
- id: infraId5(kind, name),
4271
+ id: infraId6(kind, name),
4104
4272
  type: NodeType10.InfraNode,
4105
4273
  name,
4106
4274
  provider,
@@ -4130,7 +4298,7 @@ function dependsOnList(value) {
4130
4298
  }
4131
4299
  function serviceNameToServiceNode(name, services) {
4132
4300
  for (const s of services) {
4133
- if (s.node.name === name || path27.basename(s.dir) === name) return s.node.id;
4301
+ if (s.node.name === name || path28.basename(s.dir) === name) return s.node.id;
4134
4302
  }
4135
4303
  return null;
4136
4304
  }
@@ -4139,7 +4307,7 @@ async function addComposeInfra(graph, scanPath, services) {
4139
4307
  let edgesAdded = 0;
4140
4308
  let composePath = null;
4141
4309
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4142
- const abs = path27.join(scanPath, name);
4310
+ const abs = path28.join(scanPath, name);
4143
4311
  if (await exists(abs)) {
4144
4312
  composePath = abs;
4145
4313
  break;
@@ -4152,13 +4320,13 @@ async function addComposeInfra(graph, scanPath, services) {
4152
4320
  } catch (err) {
4153
4321
  recordExtractionError(
4154
4322
  "infra docker-compose",
4155
- path27.relative(scanPath, composePath),
4323
+ path28.relative(scanPath, composePath),
4156
4324
  err
4157
4325
  );
4158
4326
  return { nodesAdded, edgesAdded };
4159
4327
  }
4160
4328
  if (!compose?.services) return { nodesAdded, edgesAdded };
4161
- const evidenceFile = path27.relative(scanPath, composePath).split(path27.sep).join("/");
4329
+ const evidenceFile = path28.relative(scanPath, composePath).split(path28.sep).join("/");
4162
4330
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4163
4331
  for (const [composeName, svc] of Object.entries(compose.services)) {
4164
4332
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4199,7 +4367,7 @@ async function addComposeInfra(graph, scanPath, services) {
4199
4367
  }
4200
4368
 
4201
4369
  // src/extract/infra/dockerfile.ts
4202
- import path28 from "path";
4370
+ import path29 from "path";
4203
4371
  import { promises as fs15 } from "fs";
4204
4372
  import { EdgeType as EdgeType11, Provenance as Provenance9, confidenceForExtracted as confidenceForExtracted8 } from "@neat.is/types";
4205
4373
  function runtimeImage(content) {
@@ -4220,7 +4388,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4220
4388
  let nodesAdded = 0;
4221
4389
  let edgesAdded = 0;
4222
4390
  for (const service of services) {
4223
- const dockerfilePath = path28.join(service.dir, "Dockerfile");
4391
+ const dockerfilePath = path29.join(service.dir, "Dockerfile");
4224
4392
  if (!await exists(dockerfilePath)) continue;
4225
4393
  let content;
4226
4394
  try {
@@ -4228,7 +4396,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4228
4396
  } catch (err) {
4229
4397
  recordExtractionError(
4230
4398
  "infra dockerfile",
4231
- path28.relative(scanPath, dockerfilePath),
4399
+ path29.relative(scanPath, dockerfilePath),
4232
4400
  err
4233
4401
  );
4234
4402
  continue;
@@ -4240,7 +4408,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4240
4408
  graph.addNode(node.id, node);
4241
4409
  nodesAdded++;
4242
4410
  }
4243
- const relDockerfile = toPosix2(path28.relative(service.dir, dockerfilePath));
4411
+ const relDockerfile = toPosix2(path29.relative(service.dir, dockerfilePath));
4244
4412
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4245
4413
  graph,
4246
4414
  service.pkg.name,
@@ -4259,7 +4427,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4259
4427
  provenance: Provenance9.EXTRACTED,
4260
4428
  confidence: confidenceForExtracted8("structural"),
4261
4429
  evidence: {
4262
- file: toPosix2(path28.relative(scanPath, dockerfilePath))
4430
+ file: toPosix2(path29.relative(scanPath, dockerfilePath))
4263
4431
  }
4264
4432
  };
4265
4433
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4271,7 +4439,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4271
4439
 
4272
4440
  // src/extract/infra/terraform.ts
4273
4441
  import { promises as fs16 } from "fs";
4274
- import path29 from "path";
4442
+ import path30 from "path";
4275
4443
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
4276
4444
  async function walkTfFiles(start, depth = 0, max = 5) {
4277
4445
  if (depth > max) return [];
@@ -4280,11 +4448,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4280
4448
  for (const entry of entries) {
4281
4449
  if (entry.isDirectory()) {
4282
4450
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
4283
- const child = path29.join(start, entry.name);
4451
+ const child = path30.join(start, entry.name);
4284
4452
  if (await isPythonVenvDir(child)) continue;
4285
4453
  out.push(...await walkTfFiles(child, depth + 1, max));
4286
4454
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
4287
- out.push(path29.join(start, entry.name));
4455
+ out.push(path30.join(start, entry.name));
4288
4456
  }
4289
4457
  }
4290
4458
  return out;
@@ -4311,7 +4479,7 @@ async function addTerraformResources(graph, scanPath) {
4311
4479
 
4312
4480
  // src/extract/infra/k8s.ts
4313
4481
  import { promises as fs17 } from "fs";
4314
- import path30 from "path";
4482
+ import path31 from "path";
4315
4483
  import { parseAllDocuments as parseAllDocuments2 } from "yaml";
4316
4484
  var K8S_KIND_TO_INFRA_KIND = {
4317
4485
  Service: "k8s-service",
@@ -4329,11 +4497,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
4329
4497
  for (const entry of entries) {
4330
4498
  if (entry.isDirectory()) {
4331
4499
  if (IGNORED_DIRS.has(entry.name)) continue;
4332
- const child = path30.join(start, entry.name);
4500
+ const child = path31.join(start, entry.name);
4333
4501
  if (await isPythonVenvDir(child)) continue;
4334
4502
  out.push(...await walkYamlFiles2(child, depth + 1, max));
4335
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path30.extname(entry.name))) {
4336
- out.push(path30.join(start, entry.name));
4503
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path31.extname(entry.name))) {
4504
+ out.push(path31.join(start, entry.name));
4337
4505
  }
4338
4506
  }
4339
4507
  return out;
@@ -4377,11 +4545,11 @@ async function addInfra(graph, scanPath, services) {
4377
4545
  }
4378
4546
 
4379
4547
  // src/extract/index.ts
4380
- import path32 from "path";
4548
+ import path33 from "path";
4381
4549
 
4382
4550
  // src/extract/retire.ts
4383
4551
  import { existsSync as existsSync2 } from "fs";
4384
- import path31 from "path";
4552
+ import path32 from "path";
4385
4553
  import { NodeType as NodeType11, Provenance as Provenance10 } from "@neat.is/types";
4386
4554
  function dropOrphanedFileNodes(graph) {
4387
4555
  const orphans = [];
@@ -4415,11 +4583,11 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4415
4583
  if (edge.provenance !== Provenance10.EXTRACTED) return;
4416
4584
  const evidenceFile = edge.evidence?.file;
4417
4585
  if (!evidenceFile) return;
4418
- if (path31.isAbsolute(evidenceFile)) {
4586
+ if (path32.isAbsolute(evidenceFile)) {
4419
4587
  if (!existsSync2(evidenceFile)) toDrop.push(id);
4420
4588
  return;
4421
4589
  }
4422
- const found = bases.some((base) => existsSync2(path31.join(base, evidenceFile)));
4590
+ const found = bases.some((base) => existsSync2(path32.join(base, evidenceFile)));
4423
4591
  if (!found) toDrop.push(id);
4424
4592
  });
4425
4593
  for (const id of toDrop) graph.dropEdge(id);
@@ -4459,7 +4627,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4459
4627
  }
4460
4628
  const droppedEntries = drainDroppedExtracted();
4461
4629
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4462
- const rejectedPath = path32.join(path32.dirname(opts.errorsPath), "rejected.ndjson");
4630
+ const rejectedPath = path33.join(path33.dirname(opts.errorsPath), "rejected.ndjson");
4463
4631
  try {
4464
4632
  await writeRejectedExtracted(droppedEntries, rejectedPath);
4465
4633
  } catch (err) {
@@ -4736,7 +4904,7 @@ function computeDivergences(graph, opts = {}) {
4736
4904
 
4737
4905
  // src/persist.ts
4738
4906
  import { promises as fs18 } from "fs";
4739
- import path33 from "path";
4907
+ import path34 from "path";
4740
4908
  import { Provenance as Provenance12, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
4741
4909
  var SCHEMA_VERSION = 4;
4742
4910
  function migrateV1ToV2(payload) {
@@ -4773,7 +4941,7 @@ function migrateV2ToV3(payload) {
4773
4941
  return { ...payload, schemaVersion: 3 };
4774
4942
  }
4775
4943
  async function ensureDir(filePath) {
4776
- await fs18.mkdir(path33.dirname(filePath), { recursive: true });
4944
+ await fs18.mkdir(path34.dirname(filePath), { recursive: true });
4777
4945
  }
4778
4946
  async function saveGraphToDisk(graph, outPath) {
4779
4947
  await ensureDir(outPath);
@@ -4923,23 +5091,23 @@ function canonicalJson(value) {
4923
5091
  }
4924
5092
 
4925
5093
  // src/projects.ts
4926
- import path34 from "path";
5094
+ import path35 from "path";
4927
5095
  function pathsForProject(project, baseDir) {
4928
5096
  if (project === DEFAULT_PROJECT) {
4929
5097
  return {
4930
- snapshotPath: path34.join(baseDir, "graph.json"),
4931
- errorsPath: path34.join(baseDir, "errors.ndjson"),
4932
- staleEventsPath: path34.join(baseDir, "stale-events.ndjson"),
4933
- embeddingsCachePath: path34.join(baseDir, "embeddings.json"),
4934
- policyViolationsPath: path34.join(baseDir, "policy-violations.ndjson")
5098
+ snapshotPath: path35.join(baseDir, "graph.json"),
5099
+ errorsPath: path35.join(baseDir, "errors.ndjson"),
5100
+ staleEventsPath: path35.join(baseDir, "stale-events.ndjson"),
5101
+ embeddingsCachePath: path35.join(baseDir, "embeddings.json"),
5102
+ policyViolationsPath: path35.join(baseDir, "policy-violations.ndjson")
4935
5103
  };
4936
5104
  }
4937
5105
  return {
4938
- snapshotPath: path34.join(baseDir, `${project}.json`),
4939
- errorsPath: path34.join(baseDir, `errors.${project}.ndjson`),
4940
- staleEventsPath: path34.join(baseDir, `stale-events.${project}.ndjson`),
4941
- embeddingsCachePath: path34.join(baseDir, `embeddings.${project}.json`),
4942
- policyViolationsPath: path34.join(baseDir, `policy-violations.${project}.ndjson`)
5106
+ snapshotPath: path35.join(baseDir, `${project}.json`),
5107
+ errorsPath: path35.join(baseDir, `errors.${project}.ndjson`),
5108
+ staleEventsPath: path35.join(baseDir, `stale-events.${project}.ndjson`),
5109
+ embeddingsCachePath: path35.join(baseDir, `embeddings.${project}.json`),
5110
+ policyViolationsPath: path35.join(baseDir, `policy-violations.${project}.ndjson`)
4943
5111
  };
4944
5112
  }
4945
5113
  var Projects = class {
@@ -4980,7 +5148,7 @@ function parseExtraProjects(raw) {
4980
5148
  // src/registry.ts
4981
5149
  import { promises as fs20 } from "fs";
4982
5150
  import os2 from "os";
4983
- import path35 from "path";
5151
+ import path36 from "path";
4984
5152
  import {
4985
5153
  RegistryFileSchema
4986
5154
  } from "@neat.is/types";
@@ -4988,17 +5156,122 @@ var LOCK_TIMEOUT_MS = 5e3;
4988
5156
  var LOCK_RETRY_MS = 50;
4989
5157
  function neatHome() {
4990
5158
  const override = process.env.NEAT_HOME;
4991
- if (override && override.length > 0) return path35.resolve(override);
4992
- return path35.join(os2.homedir(), ".neat");
5159
+ if (override && override.length > 0) return path36.resolve(override);
5160
+ return path36.join(os2.homedir(), ".neat");
4993
5161
  }
4994
5162
  function registryPath() {
4995
- return path35.join(neatHome(), "projects.json");
5163
+ return path36.join(neatHome(), "projects.json");
4996
5164
  }
4997
5165
  function registryLockPath() {
4998
- return path35.join(neatHome(), "projects.json.lock");
5166
+ return path36.join(neatHome(), "projects.json.lock");
4999
5167
  }
5000
5168
  function daemonPidPath() {
5001
- return path35.join(neatHome(), "neatd.pid");
5169
+ return path36.join(neatHome(), "neatd.pid");
5170
+ }
5171
+ function daemonsDir() {
5172
+ return path36.join(neatHome(), "daemons");
5173
+ }
5174
+ function isFiniteInt(v) {
5175
+ return typeof v === "number" && Number.isFinite(v);
5176
+ }
5177
+ function parseDaemonRecord(raw) {
5178
+ let obj;
5179
+ try {
5180
+ obj = JSON.parse(raw);
5181
+ } catch {
5182
+ return void 0;
5183
+ }
5184
+ if (typeof obj !== "object" || obj === null) return void 0;
5185
+ const r = obj;
5186
+ const ports = r.ports;
5187
+ if (typeof r.project !== "string" || typeof r.projectPath !== "string" || !isFiniteInt(r.pid) || r.status !== "running" && r.status !== "stopped" || typeof r.startedAt !== "string" || typeof r.neatVersion !== "string" || !ports || !isFiniteInt(ports.rest) || !isFiniteInt(ports.otlp) || !isFiniteInt(ports.web)) {
5188
+ return void 0;
5189
+ }
5190
+ return {
5191
+ project: r.project,
5192
+ projectPath: r.projectPath,
5193
+ pid: r.pid,
5194
+ status: r.status,
5195
+ ports: { rest: ports.rest, otlp: ports.otlp, web: ports.web },
5196
+ startedAt: r.startedAt,
5197
+ neatVersion: r.neatVersion
5198
+ };
5199
+ }
5200
+ var defaultDiscoveryProbe = { isPidAlive: isPidAliveDefault };
5201
+ async function discoverDaemons(probe = defaultDiscoveryProbe) {
5202
+ const dir = daemonsDir();
5203
+ let names;
5204
+ try {
5205
+ names = await fs20.readdir(dir);
5206
+ } catch (err) {
5207
+ if (err.code === "ENOENT") return [];
5208
+ throw err;
5209
+ }
5210
+ const out = [];
5211
+ for (const name of names) {
5212
+ if (!name.endsWith(".json")) continue;
5213
+ const file = path36.join(dir, name);
5214
+ let raw;
5215
+ try {
5216
+ raw = await fs20.readFile(file, "utf8");
5217
+ } catch {
5218
+ continue;
5219
+ }
5220
+ const record = parseDaemonRecord(raw);
5221
+ if (!record) continue;
5222
+ const live = record.status === "running" && probe.isPidAlive(record.pid);
5223
+ out.push({ record, live, source: file });
5224
+ }
5225
+ out.sort((a, b) => a.record.project.localeCompare(b.record.project));
5226
+ return out;
5227
+ }
5228
+ async function removeDaemonRecord(source) {
5229
+ await fs20.unlink(source).catch(() => {
5230
+ });
5231
+ }
5232
+ async function listMachineProjects(probe = defaultDiscoveryProbe) {
5233
+ const discovered = await discoverDaemons(probe);
5234
+ const byPath = /* @__PURE__ */ new Map();
5235
+ for (const d of discovered) {
5236
+ const key = await normalizeProjectPath(d.record.projectPath);
5237
+ byPath.set(key, {
5238
+ project: d.record.project,
5239
+ projectPath: d.record.projectPath,
5240
+ state: d.live ? "running" : "stopped",
5241
+ ports: d.record.ports,
5242
+ pid: d.record.pid
5243
+ });
5244
+ }
5245
+ let legacy = [];
5246
+ try {
5247
+ legacy = (await readRegistry()).projects;
5248
+ } catch {
5249
+ legacy = [];
5250
+ }
5251
+ for (const entry of legacy) {
5252
+ const key = await normalizeProjectPath(entry.path);
5253
+ if (byPath.has(key)) continue;
5254
+ byPath.set(key, {
5255
+ project: entry.name,
5256
+ projectPath: entry.path,
5257
+ state: "registered",
5258
+ registryStatus: entry.status
5259
+ });
5260
+ }
5261
+ return [...byPath.values()].sort((a, b) => a.project.localeCompare(b.project));
5262
+ }
5263
+ async function findDaemonByProject(name, probe = defaultDiscoveryProbe) {
5264
+ const discovered = await discoverDaemons(probe);
5265
+ return discovered.find((d) => d.record.project === name);
5266
+ }
5267
+ function signalDaemonStop(pid) {
5268
+ try {
5269
+ process.kill(pid, "SIGTERM");
5270
+ return true;
5271
+ } catch (err) {
5272
+ if (err.code === "ESRCH") return false;
5273
+ throw err;
5274
+ }
5002
5275
  }
5003
5276
  function isPidAliveDefault(pid) {
5004
5277
  try {
@@ -5058,7 +5331,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
5058
5331
  }
5059
5332
  }
5060
5333
  async function normalizeProjectPath(input) {
5061
- const resolved = path35.resolve(input);
5334
+ const resolved = path36.resolve(input);
5062
5335
  try {
5063
5336
  return await fs20.realpath(resolved);
5064
5337
  } catch {
@@ -5066,7 +5339,7 @@ async function normalizeProjectPath(input) {
5066
5339
  }
5067
5340
  }
5068
5341
  async function writeAtomically(target, contents) {
5069
- await fs20.mkdir(path35.dirname(target), { recursive: true });
5342
+ await fs20.mkdir(path36.dirname(target), { recursive: true });
5070
5343
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5071
5344
  const fd = await fs20.open(tmp, "w");
5072
5345
  try {
@@ -5079,7 +5352,7 @@ async function writeAtomically(target, contents) {
5079
5352
  }
5080
5353
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
5081
5354
  const deadline = Date.now() + timeoutMs;
5082
- await fs20.mkdir(path35.dirname(lockPath), { recursive: true });
5355
+ await fs20.mkdir(path36.dirname(lockPath), { recursive: true });
5083
5356
  let probedHolder = false;
5084
5357
  while (true) {
5085
5358
  try {
@@ -5215,6 +5488,58 @@ async function removeProject(name) {
5215
5488
  return removed;
5216
5489
  });
5217
5490
  }
5491
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
5492
+ var DEFAULT_PRUNE_TTL_MS = 7 * DAY_MS2;
5493
+ function pruneTtlMs() {
5494
+ const raw = process.env.NEAT_REGISTRY_PRUNE_TTL_MS;
5495
+ if (!raw) return DEFAULT_PRUNE_TTL_MS;
5496
+ const n = Number.parseInt(raw, 10);
5497
+ if (Number.isFinite(n) && n >= 0) return n;
5498
+ console.warn(
5499
+ `[neat] NEAT_REGISTRY_PRUNE_TTL_MS could not be parsed (${raw}); using default ${DEFAULT_PRUNE_TTL_MS}ms`
5500
+ );
5501
+ return DEFAULT_PRUNE_TTL_MS;
5502
+ }
5503
+ async function statPathStatus(p) {
5504
+ try {
5505
+ const stat = await fs20.stat(p);
5506
+ return stat.isDirectory() ? "present" : "unknown";
5507
+ } catch (err) {
5508
+ return err.code === "ENOENT" ? "gone" : "unknown";
5509
+ }
5510
+ }
5511
+ async function pruneRegistry(opts = {}) {
5512
+ const ttlMs = opts.ttlMs ?? pruneTtlMs();
5513
+ const statPath = opts.statPath ?? statPathStatus;
5514
+ const now = opts.now ?? Date.now;
5515
+ return withLock(async () => {
5516
+ const reg = await readRegistry();
5517
+ const removed = [];
5518
+ const kept = [];
5519
+ for (const entry of reg.projects) {
5520
+ const status = await statPath(entry.path);
5521
+ if (status !== "gone") {
5522
+ kept.push(entry);
5523
+ continue;
5524
+ }
5525
+ if (ttlMs <= 0) {
5526
+ removed.push(entry);
5527
+ continue;
5528
+ }
5529
+ const lastSeen = Date.parse(entry.lastSeenAt ?? entry.registeredAt);
5530
+ const age = now() - (Number.isFinite(lastSeen) ? lastSeen : 0);
5531
+ if (age > ttlMs) {
5532
+ removed.push(entry);
5533
+ } else {
5534
+ kept.push(entry);
5535
+ }
5536
+ }
5537
+ if (removed.length === 0) return [];
5538
+ reg.projects = kept;
5539
+ await writeRegistry(reg);
5540
+ return removed;
5541
+ });
5542
+ }
5218
5543
 
5219
5544
  // src/api.ts
5220
5545
  import Fastify from "fastify";
@@ -5223,13 +5548,13 @@ import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } f
5223
5548
 
5224
5549
  // src/extend/index.ts
5225
5550
  import { promises as fs22 } from "fs";
5226
- import path37 from "path";
5551
+ import path38 from "path";
5227
5552
  import os3 from "os";
5228
5553
  import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
5229
5554
 
5230
5555
  // src/installers/package-manager.ts
5231
5556
  import { promises as fs21 } from "fs";
5232
- import path36 from "path";
5557
+ import path37 from "path";
5233
5558
  import { spawn } from "child_process";
5234
5559
  var LOCKFILE_PRIORITY = [
5235
5560
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -5251,22 +5576,22 @@ async function exists2(p) {
5251
5576
  }
5252
5577
  }
5253
5578
  async function detectPackageManager(serviceDir) {
5254
- let dir = path36.resolve(serviceDir);
5579
+ let dir = path37.resolve(serviceDir);
5255
5580
  const stops = /* @__PURE__ */ new Set();
5256
5581
  for (let i = 0; i < 64; i++) {
5257
5582
  if (stops.has(dir)) break;
5258
5583
  stops.add(dir);
5259
5584
  for (const candidate of LOCKFILE_PRIORITY) {
5260
- const lockPath = path36.join(dir, candidate.lockfile);
5585
+ const lockPath = path37.join(dir, candidate.lockfile);
5261
5586
  if (await exists2(lockPath)) {
5262
5587
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
5263
5588
  }
5264
5589
  }
5265
- const parent = path36.dirname(dir);
5590
+ const parent = path37.dirname(dir);
5266
5591
  if (parent === dir) break;
5267
5592
  dir = parent;
5268
5593
  }
5269
- return { pm: "npm", cwd: path36.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5594
+ return { pm: "npm", cwd: path37.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5270
5595
  }
5271
5596
  async function runPackageManagerInstall(cmd) {
5272
5597
  return new Promise((resolve) => {
@@ -5315,7 +5640,7 @@ async function fileExists2(p) {
5315
5640
  }
5316
5641
  }
5317
5642
  async function readPackageJson(scanPath) {
5318
- const pkgPath = path37.join(scanPath, "package.json");
5643
+ const pkgPath = path38.join(scanPath, "package.json");
5319
5644
  const raw = await fs22.readFile(pkgPath, "utf8");
5320
5645
  return JSON.parse(raw);
5321
5646
  }
@@ -5326,11 +5651,11 @@ async function findHookFiles(scanPath) {
5326
5651
  ).sort();
5327
5652
  }
5328
5653
  function extendLogPath() {
5329
- return process.env.NEAT_EXTEND_LOG ?? path37.join(os3.homedir(), ".neat", "extend-log.ndjson");
5654
+ return process.env.NEAT_EXTEND_LOG ?? path38.join(os3.homedir(), ".neat", "extend-log.ndjson");
5330
5655
  }
5331
5656
  async function appendExtendLog(entry) {
5332
5657
  const logPath = extendLogPath();
5333
- await fs22.mkdir(path37.dirname(logPath), { recursive: true });
5658
+ await fs22.mkdir(path38.dirname(logPath), { recursive: true });
5334
5659
  await fs22.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
5335
5660
  }
5336
5661
  function splicedContent(fileContent, snippet2) {
@@ -5389,7 +5714,7 @@ function lookupInstrumentation(library, installedVersion) {
5389
5714
  }
5390
5715
  async function describeProjectInstrumentation(ctx) {
5391
5716
  const hookFiles = await findHookFiles(ctx.scanPath);
5392
- const envNeat = await fileExists2(path37.join(ctx.scanPath, ".env.neat"));
5717
+ const envNeat = await fileExists2(path38.join(ctx.scanPath, ".env.neat"));
5393
5718
  const registryInstrPackages = new Set(
5394
5719
  registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
5395
5720
  );
@@ -5411,16 +5736,16 @@ async function applyExtension(ctx, args, options) {
5411
5736
  );
5412
5737
  }
5413
5738
  for (const file of hookFiles) {
5414
- const content = await fs22.readFile(path37.join(ctx.scanPath, file), "utf8");
5739
+ const content = await fs22.readFile(path38.join(ctx.scanPath, file), "utf8");
5415
5740
  if (content.includes(args.registration_snippet)) {
5416
5741
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5417
5742
  }
5418
5743
  }
5419
5744
  const primaryFile = hookFiles[0];
5420
- const primaryPath = path37.join(ctx.scanPath, primaryFile);
5745
+ const primaryPath = path38.join(ctx.scanPath, primaryFile);
5421
5746
  const filesTouched = [];
5422
5747
  const depsAdded = [];
5423
- const pkgPath = path37.join(ctx.scanPath, "package.json");
5748
+ const pkgPath = path38.join(ctx.scanPath, "package.json");
5424
5749
  const pkg = await readPackageJson(ctx.scanPath);
5425
5750
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5426
5751
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -5466,7 +5791,7 @@ async function dryRunExtension(ctx, args) {
5466
5791
  };
5467
5792
  }
5468
5793
  for (const file of hookFiles) {
5469
- const content = await fs22.readFile(path37.join(ctx.scanPath, file), "utf8");
5794
+ const content = await fs22.readFile(path38.join(ctx.scanPath, file), "utf8");
5470
5795
  if (content.includes(args.registration_snippet)) {
5471
5796
  return {
5472
5797
  library: args.library,
@@ -5488,7 +5813,7 @@ async function dryRunExtension(ctx, args) {
5488
5813
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5489
5814
  filesTouched.push("package.json");
5490
5815
  }
5491
- const hookContent = await fs22.readFile(path37.join(ctx.scanPath, primaryFile), "utf8");
5816
+ const hookContent = await fs22.readFile(path38.join(ctx.scanPath, primaryFile), "utf8");
5492
5817
  const patched = splicedContent(hookContent, args.registration_snippet);
5493
5818
  if (patched) {
5494
5819
  filesTouched.push(primaryFile);
@@ -5509,7 +5834,7 @@ async function rollbackExtension(ctx, args) {
5509
5834
  if (!match) {
5510
5835
  return { undone: false, message: "no apply found for library" };
5511
5836
  }
5512
- const pkgPath = path37.join(ctx.scanPath, "package.json");
5837
+ const pkgPath = path38.join(ctx.scanPath, "package.json");
5513
5838
  if (await fileExists2(pkgPath)) {
5514
5839
  const pkg = await readPackageJson(ctx.scanPath);
5515
5840
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -5520,7 +5845,7 @@ async function rollbackExtension(ctx, args) {
5520
5845
  }
5521
5846
  const hookFiles = await findHookFiles(ctx.scanPath);
5522
5847
  for (const file of hookFiles) {
5523
- const filePath = path37.join(ctx.scanPath, file);
5848
+ const filePath = path38.join(ctx.scanPath, file);
5524
5849
  const content = await fs22.readFile(filePath, "utf8");
5525
5850
  if (content.includes(match.registration_snippet)) {
5526
5851
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -5545,6 +5870,7 @@ function handleSse(req, reply, opts) {
5545
5870
  reply.raw.setHeader("Connection", "keep-alive");
5546
5871
  reply.raw.setHeader("X-Accel-Buffering", "no");
5547
5872
  reply.raw.flushHeaders?.();
5873
+ reply.raw.write(":open\n\n");
5548
5874
  let pending = 0;
5549
5875
  let dropped = false;
5550
5876
  const closeConnection = () => {
@@ -6244,6 +6570,10 @@ export {
6244
6570
  parseExtraProjects,
6245
6571
  registryPath,
6246
6572
  registryLockPath,
6573
+ removeDaemonRecord,
6574
+ listMachineProjects,
6575
+ findDaemonByProject,
6576
+ signalDaemonStop,
6247
6577
  normalizeProjectPath,
6248
6578
  writeAtomically,
6249
6579
  readRegistry,
@@ -6254,6 +6584,7 @@ export {
6254
6584
  setStatus,
6255
6585
  touchLastSeen,
6256
6586
  removeProject,
6587
+ pruneRegistry,
6257
6588
  buildApi
6258
6589
  };
6259
- //# sourceMappingURL=chunk-XS4CGNRO.js.map
6590
+ //# sourceMappingURL=chunk-T3X6XJPU.js.map