@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.
package/dist/cli.cjs CHANGED
@@ -58,9 +58,9 @@ function mountBearerAuth(app, opts) {
58
58
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
59
59
  const publicRead = opts.publicRead === true;
60
60
  app.addHook("preHandler", (req, reply, done) => {
61
- const path50 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
+ const path53 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
62
62
  for (const suffix of suffixes) {
63
- if (path50 === suffix || path50.endsWith(suffix)) {
63
+ if (path53 === suffix || path53.endsWith(suffix)) {
64
64
  done();
65
65
  return;
66
66
  }
@@ -188,8 +188,8 @@ function reshapeGrpcRequest(req) {
188
188
  };
189
189
  }
190
190
  function resolveProtoRoot() {
191
- const here = import_node_path40.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
192
- return import_node_path40.default.resolve(here, "..", "proto");
191
+ const here = import_node_path41.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
192
+ return import_node_path41.default.resolve(here, "..", "proto");
193
193
  }
194
194
  function loadTraceService() {
195
195
  const protoRoot = resolveProtoRoot();
@@ -257,13 +257,13 @@ async function startOtelGrpcReceiver(opts) {
257
257
  })
258
258
  };
259
259
  }
260
- var import_node_url2, import_node_path40, import_node_crypto2, grpc, protoLoader;
260
+ var import_node_url2, import_node_path41, import_node_crypto2, grpc, protoLoader;
261
261
  var init_otel_grpc = __esm({
262
262
  "src/otel-grpc.ts"() {
263
263
  "use strict";
264
264
  init_cjs_shims();
265
265
  import_node_url2 = require("url");
266
- import_node_path40 = __toESM(require("path"), 1);
266
+ import_node_path41 = __toESM(require("path"), 1);
267
267
  import_node_crypto2 = require("crypto");
268
268
  grpc = __toESM(require("@grpc/grpc-js"), 1);
269
269
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -373,10 +373,10 @@ function parseOtlpRequest(body) {
373
373
  return out;
374
374
  }
375
375
  function loadProtoRoot() {
376
- const here = import_node_path41.default.dirname((0, import_node_url3.fileURLToPath)(importMetaUrl));
377
- const protoRoot = import_node_path41.default.resolve(here, "..", "proto");
376
+ const here = import_node_path42.default.dirname((0, import_node_url3.fileURLToPath)(importMetaUrl));
377
+ const protoRoot = import_node_path42.default.resolve(here, "..", "proto");
378
378
  const root = new import_protobufjs.default.Root();
379
- root.resolvePath = (_origin, target) => import_node_path41.default.resolve(protoRoot, target);
379
+ root.resolvePath = (_origin, target) => import_node_path42.default.resolve(protoRoot, target);
380
380
  root.loadSync(
381
381
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
382
382
  { keepCase: true }
@@ -474,8 +474,10 @@ async function buildOtelReceiver(opts) {
474
474
  for (const s of spans) projectQueue.push({ project, span: s });
475
475
  projectDrainPromise = projectDrainPromise.then(() => drainProject());
476
476
  };
477
+ const offersProjectRouting = opts.onProjectSpan !== void 0;
477
478
  const legacyEndpointWarned = /* @__PURE__ */ new Set();
478
479
  function warnLegacyEndpoint(serviceName) {
480
+ if (!offersProjectRouting) return;
479
481
  if (legacyEndpointWarned.has(serviceName)) return;
480
482
  legacyEndpointWarned.add(serviceName);
481
483
  console.warn(
@@ -572,12 +574,12 @@ async function buildOtelReceiver(opts) {
572
574
  };
573
575
  return decorated;
574
576
  }
575
- var import_node_path41, import_node_url3, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
577
+ var import_node_path42, import_node_url3, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
576
578
  var init_otel = __esm({
577
579
  "src/otel.ts"() {
578
580
  "use strict";
579
581
  init_cjs_shims();
580
- import_node_path41 = __toESM(require("path"), 1);
582
+ import_node_path42 = __toESM(require("path"), 1);
581
583
  import_node_url3 = require("url");
582
584
  import_fastify2 = __toESM(require("fastify"), 1);
583
585
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -595,6 +597,7 @@ var init_otel = __esm({
595
597
  var cli_exports = {};
596
598
  __export(cli_exports, {
597
599
  CLAUDE_SKILL_CONFIG: () => CLAUDE_SKILL_CONFIG,
600
+ ProjectResolutionError: () => ProjectResolutionError,
598
601
  QUERY_VERBS: () => QUERY_VERBS,
599
602
  commandPrefix: () => commandPrefix,
600
603
  isNpxInvocation: () => isNpxInvocation,
@@ -602,6 +605,7 @@ __export(cli_exports, {
602
605
  parseArgs: () => parseArgs,
603
606
  printBanner: () => printBanner,
604
607
  readPackageVersion: () => readPackageVersion,
608
+ resolveProjectForVerb: () => resolveProjectForVerb,
605
609
  runInit: () => runInit,
606
610
  runQueryVerb: () => runQueryVerb,
607
611
  runSkill: () => runSkill,
@@ -609,8 +613,8 @@ __export(cli_exports, {
609
613
  });
610
614
  module.exports = __toCommonJS(cli_exports);
611
615
  init_cjs_shims();
612
- var import_node_path49 = __toESM(require("path"), 1);
613
- var import_node_fs32 = require("fs");
616
+ var import_node_path52 = __toESM(require("path"), 1);
617
+ var import_node_fs34 = require("fs");
614
618
 
615
619
  // src/banner.ts
616
620
  init_cjs_shims();
@@ -1175,19 +1179,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1175
1179
  function longestIncomingWalk(graph, start, maxDepth) {
1176
1180
  let best = { path: [start], edges: [] };
1177
1181
  const visited = /* @__PURE__ */ new Set([start]);
1178
- function step(node, path50, edges) {
1179
- if (path50.length > best.path.length) {
1180
- best = { path: [...path50], edges: [...edges] };
1182
+ function step(node, path53, edges) {
1183
+ if (path53.length > best.path.length) {
1184
+ best = { path: [...path53], edges: [...edges] };
1181
1185
  }
1182
- if (path50.length - 1 >= maxDepth) return;
1186
+ if (path53.length - 1 >= maxDepth) return;
1183
1187
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1184
1188
  for (const [srcId, edge] of incoming) {
1185
1189
  if (visited.has(srcId)) continue;
1186
1190
  visited.add(srcId);
1187
- path50.push(srcId);
1191
+ path53.push(srcId);
1188
1192
  edges.push(edge);
1189
- step(srcId, path50, edges);
1190
- path50.pop();
1193
+ step(srcId, path53, edges);
1194
+ path53.pop();
1191
1195
  edges.pop();
1192
1196
  visited.delete(srcId);
1193
1197
  }
@@ -1727,6 +1731,41 @@ function thresholdForEdgeType(edgeType, overrides) {
1727
1731
  const map = overrides ?? loadStaleThresholdsFromEnv();
1728
1732
  return map[edgeType] ?? FALLBACK_STALE_THRESHOLD_MS;
1729
1733
  }
1734
+ var DEFAULT_INCIDENT_THRESHOLDS = {
1735
+ threshold: 5,
1736
+ windowMs: 6e4
1737
+ };
1738
+ function loadIncidentThresholdsFromEnv() {
1739
+ const raw = process.env.NEAT_INCIDENT_THRESHOLDS;
1740
+ if (!raw) return DEFAULT_INCIDENT_THRESHOLDS;
1741
+ try {
1742
+ const overrides = JSON.parse(raw);
1743
+ const merged = { ...DEFAULT_INCIDENT_THRESHOLDS };
1744
+ if (typeof overrides.threshold === "number" && Number.isFinite(overrides.threshold) && overrides.threshold >= 1) {
1745
+ merged.threshold = Math.floor(overrides.threshold);
1746
+ }
1747
+ if (typeof overrides.windowMs === "number" && Number.isFinite(overrides.windowMs) && overrides.windowMs >= 0) {
1748
+ merged.windowMs = overrides.windowMs;
1749
+ }
1750
+ return merged;
1751
+ } catch (err) {
1752
+ console.warn(
1753
+ `[neat] NEAT_INCIDENT_THRESHOLDS could not be parsed (${err.message}); using defaults`
1754
+ );
1755
+ return DEFAULT_INCIDENT_THRESHOLDS;
1756
+ }
1757
+ }
1758
+ function httpResponseStatus(span) {
1759
+ for (const key of ["http.response.status_code", "http.status_code"]) {
1760
+ const v = span.attributes[key];
1761
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1762
+ if (typeof v === "string") {
1763
+ const n = Number(v);
1764
+ if (Number.isFinite(n)) return n;
1765
+ }
1766
+ }
1767
+ return void 0;
1768
+ }
1730
1769
  function nowIso(ctx) {
1731
1770
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1732
1771
  }
@@ -2140,6 +2179,71 @@ function makeErrorSpanWriter(errorsPath) {
2140
2179
  await import_node_fs4.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
2141
2180
  };
2142
2181
  }
2182
+ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
2183
+ const attrs = sanitizeAttributes(span.attributes);
2184
+ const first = firstTimestamp ?? timestamp;
2185
+ const peer = pickAddress(span);
2186
+ const message = count > 1 ? `${count} consecutive HTTP ${statusCode} responses` + (peer ? ` to ${peer}` : "") : `HTTP ${statusCode} response` + (peer ? ` from ${peer}` : "");
2187
+ const ev = {
2188
+ id: `${span.traceId}:${span.spanId}`,
2189
+ timestamp,
2190
+ service: span.service,
2191
+ traceId: span.traceId,
2192
+ spanId: span.spanId,
2193
+ errorType: "http-failure",
2194
+ errorMessage: message,
2195
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2196
+ affectedNode,
2197
+ httpStatusCode: statusCode,
2198
+ incidentCount: count,
2199
+ firstTimestamp: first,
2200
+ lastTimestamp: timestamp
2201
+ };
2202
+ await appendErrorEvent(ctx, ev);
2203
+ }
2204
+ async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
2205
+ const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
2206
+ if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
2207
+ const peer = pickAddress(span) ?? span.spanId;
2208
+ const key = `${span.service}->${peer}`;
2209
+ const existing = ctx.burstState.get(key);
2210
+ let state;
2211
+ if (existing && nowMs - existing.lastMs <= windowMs) {
2212
+ existing.count += 1;
2213
+ existing.lastTs = ts;
2214
+ existing.lastMs = nowMs;
2215
+ existing.codes.set(status2, (existing.codes.get(status2) ?? 0) + 1);
2216
+ state = existing;
2217
+ } else {
2218
+ state = {
2219
+ count: 1,
2220
+ firstTs: ts,
2221
+ lastTs: ts,
2222
+ lastMs: nowMs,
2223
+ codes: /* @__PURE__ */ new Map([[status2, 1]])
2224
+ };
2225
+ ctx.burstState.set(key, state);
2226
+ }
2227
+ if (state.count < threshold) return;
2228
+ let dominant = status2;
2229
+ let max = 0;
2230
+ for (const [code, n] of state.codes) {
2231
+ if (n > max) {
2232
+ max = n;
2233
+ dominant = code;
2234
+ }
2235
+ }
2236
+ await recordFailingResponseIncident(
2237
+ ctx,
2238
+ span,
2239
+ affectedNode,
2240
+ state.lastTs,
2241
+ dominant,
2242
+ state.count,
2243
+ state.firstTs
2244
+ );
2245
+ ctx.burstState.delete(key);
2246
+ }
2143
2247
  async function handleSpan(ctx, span) {
2144
2248
  const ts = span.startTimeIso ?? nowIso(ctx);
2145
2249
  const nowMs = ctx.now ? ctx.now() : Date.now();
@@ -2238,6 +2342,14 @@ async function handleSpan(ctx, span) {
2238
2342
  await appendErrorEvent(ctx, ev);
2239
2343
  }
2240
2344
  }
2345
+ if (span.statusCode !== 2) {
2346
+ const status2 = httpResponseStatus(span);
2347
+ if (status2 !== void 0 && status2 >= 500) {
2348
+ await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
2349
+ } else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
2350
+ await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
2351
+ }
2352
+ }
2241
2353
  void affectedNode;
2242
2354
  if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
2243
2355
  }
@@ -4269,7 +4381,7 @@ async function addConfigNodes(graph, services, scanPath) {
4269
4381
 
4270
4382
  // src/extract/calls/index.ts
4271
4383
  init_cjs_shims();
4272
- var import_types16 = require("@neat.is/types");
4384
+ var import_types17 = require("@neat.is/types");
4273
4385
 
4274
4386
  // src/extract/calls/http.ts
4275
4387
  init_cjs_shims();
@@ -4622,15 +4734,75 @@ function grpcEndpointsFromFile(file, serviceDir) {
4622
4734
  return out;
4623
4735
  }
4624
4736
 
4737
+ // src/extract/calls/supabase.ts
4738
+ init_cjs_shims();
4739
+ var import_node_path28 = __toESM(require("path"), 1);
4740
+ var import_types16 = require("@neat.is/types");
4741
+ var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
4742
+ var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
4743
+ var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
4744
+ function hostFromLiteral(literal) {
4745
+ if (!literal) return null;
4746
+ const m = /^https?:\/\/([^/:'"`\s]+)/.exec(literal.trim());
4747
+ if (!m) return null;
4748
+ const host = m[1];
4749
+ if (!/\.supabase\.(co|in)$/i.test(host)) return null;
4750
+ return host;
4751
+ }
4752
+ function readImports2(content) {
4753
+ return {
4754
+ hasSupabaseJs: SUPABASE_JS_IMPORT_RE.test(content),
4755
+ hasSupabaseSsr: SUPABASE_SSR_IMPORT_RE.test(content)
4756
+ };
4757
+ }
4758
+ function constructorMatchesImport(name, ctx) {
4759
+ if (name === "createClient") return ctx.hasSupabaseJs;
4760
+ return ctx.hasSupabaseSsr;
4761
+ }
4762
+ function supabaseEndpointsFromFile(file, serviceDir) {
4763
+ const ctx = readImports2(file.content);
4764
+ if (!ctx.hasSupabaseJs && !ctx.hasSupabaseSsr) return [];
4765
+ const out = [];
4766
+ const seen = /* @__PURE__ */ new Set();
4767
+ SUPABASE_CLIENT_RE.lastIndex = 0;
4768
+ let m;
4769
+ while ((m = SUPABASE_CLIENT_RE.exec(file.content)) !== null) {
4770
+ const ctor = m[1];
4771
+ if (!constructorMatchesImport(ctor, ctx)) continue;
4772
+ const host = hostFromLiteral(m[2]);
4773
+ const name = host ?? "env";
4774
+ if (seen.has(name)) continue;
4775
+ seen.add(name);
4776
+ const line = lineOf(file.content, m[0]);
4777
+ out.push({
4778
+ infraId: (0, import_types16.infraId)("supabase", name),
4779
+ name,
4780
+ kind: "supabase",
4781
+ edgeType: "CALLS",
4782
+ // `createClient(...)` from @supabase/supabase-js (or createServerClient /
4783
+ // createBrowserClient from @supabase/ssr) with the import in scope — a
4784
+ // framework-aware recognizer matched the SDK shape. Verified-call-site
4785
+ // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
4786
+ confidenceKind: "verified-call-site",
4787
+ evidence: {
4788
+ file: import_node_path28.default.relative(serviceDir, file.path),
4789
+ line,
4790
+ snippet: snippet(file.content, line)
4791
+ }
4792
+ });
4793
+ }
4794
+ return out;
4795
+ }
4796
+
4625
4797
  // src/extract/calls/index.ts
4626
4798
  function edgeTypeFromEndpoint(ep) {
4627
4799
  switch (ep.edgeType) {
4628
4800
  case "PUBLISHES_TO":
4629
- return import_types16.EdgeType.PUBLISHES_TO;
4801
+ return import_types17.EdgeType.PUBLISHES_TO;
4630
4802
  case "CONSUMES_FROM":
4631
- return import_types16.EdgeType.CONSUMES_FROM;
4803
+ return import_types17.EdgeType.CONSUMES_FROM;
4632
4804
  default:
4633
- return import_types16.EdgeType.CALLS;
4805
+ return import_types17.EdgeType.CALLS;
4634
4806
  }
4635
4807
  }
4636
4808
  function isAwsKind(kind) {
@@ -4650,6 +4822,7 @@ async function addExternalEndpointEdges(graph, services) {
4650
4822
  endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
4651
4823
  endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
4652
4824
  endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
4825
+ endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
4653
4826
  }
4654
4827
  if (endpoints.length === 0) continue;
4655
4828
  const seenEdges = /* @__PURE__ */ new Set();
@@ -4657,7 +4830,7 @@ async function addExternalEndpointEdges(graph, services) {
4657
4830
  if (!graph.hasNode(ep.infraId)) {
4658
4831
  const node = {
4659
4832
  id: ep.infraId,
4660
- type: import_types16.NodeType.InfraNode,
4833
+ type: import_types17.NodeType.InfraNode,
4661
4834
  name: ep.name,
4662
4835
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4663
4836
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4669,7 +4842,7 @@ async function addExternalEndpointEdges(graph, services) {
4669
4842
  nodesAdded++;
4670
4843
  }
4671
4844
  const edgeType = edgeTypeFromEndpoint(ep);
4672
- const confidence = (0, import_types16.confidenceForExtracted)(ep.confidenceKind);
4845
+ const confidence = (0, import_types17.confidenceForExtracted)(ep.confidenceKind);
4673
4846
  const relFile = toPosix2(ep.evidence.file);
4674
4847
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4675
4848
  graph,
@@ -4679,7 +4852,7 @@ async function addExternalEndpointEdges(graph, services) {
4679
4852
  );
4680
4853
  nodesAdded += n;
4681
4854
  edgesAdded += e;
4682
- if (!(0, import_types16.passesExtractedFloor)(confidence)) {
4855
+ if (!(0, import_types17.passesExtractedFloor)(confidence)) {
4683
4856
  noteExtractedDropped({
4684
4857
  source: fileNodeId,
4685
4858
  target: ep.infraId,
@@ -4699,7 +4872,7 @@ async function addExternalEndpointEdges(graph, services) {
4699
4872
  source: fileNodeId,
4700
4873
  target: ep.infraId,
4701
4874
  type: edgeType,
4702
- provenance: import_types16.Provenance.EXTRACTED,
4875
+ provenance: import_types17.Provenance.EXTRACTED,
4703
4876
  confidence,
4704
4877
  evidence: ep.evidence
4705
4878
  };
@@ -4724,16 +4897,16 @@ init_cjs_shims();
4724
4897
 
4725
4898
  // src/extract/infra/docker-compose.ts
4726
4899
  init_cjs_shims();
4727
- var import_node_path28 = __toESM(require("path"), 1);
4728
- var import_types18 = require("@neat.is/types");
4900
+ var import_node_path29 = __toESM(require("path"), 1);
4901
+ var import_types19 = require("@neat.is/types");
4729
4902
 
4730
4903
  // src/extract/infra/shared.ts
4731
4904
  init_cjs_shims();
4732
- var import_types17 = require("@neat.is/types");
4905
+ var import_types18 = require("@neat.is/types");
4733
4906
  function makeInfraNode(kind, name, provider = "self", extras) {
4734
4907
  return {
4735
- id: (0, import_types17.infraId)(kind, name),
4736
- type: import_types17.NodeType.InfraNode,
4908
+ id: (0, import_types18.infraId)(kind, name),
4909
+ type: import_types18.NodeType.InfraNode,
4737
4910
  name,
4738
4911
  provider,
4739
4912
  kind,
@@ -4762,7 +4935,7 @@ function dependsOnList(value) {
4762
4935
  }
4763
4936
  function serviceNameToServiceNode(name, services) {
4764
4937
  for (const s of services) {
4765
- if (s.node.name === name || import_node_path28.default.basename(s.dir) === name) return s.node.id;
4938
+ if (s.node.name === name || import_node_path29.default.basename(s.dir) === name) return s.node.id;
4766
4939
  }
4767
4940
  return null;
4768
4941
  }
@@ -4771,7 +4944,7 @@ async function addComposeInfra(graph, scanPath, services) {
4771
4944
  let edgesAdded = 0;
4772
4945
  let composePath = null;
4773
4946
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4774
- const abs = import_node_path28.default.join(scanPath, name);
4947
+ const abs = import_node_path29.default.join(scanPath, name);
4775
4948
  if (await exists(abs)) {
4776
4949
  composePath = abs;
4777
4950
  break;
@@ -4784,13 +4957,13 @@ async function addComposeInfra(graph, scanPath, services) {
4784
4957
  } catch (err) {
4785
4958
  recordExtractionError(
4786
4959
  "infra docker-compose",
4787
- import_node_path28.default.relative(scanPath, composePath),
4960
+ import_node_path29.default.relative(scanPath, composePath),
4788
4961
  err
4789
4962
  );
4790
4963
  return { nodesAdded, edgesAdded };
4791
4964
  }
4792
4965
  if (!compose?.services) return { nodesAdded, edgesAdded };
4793
- const evidenceFile = import_node_path28.default.relative(scanPath, composePath).split(import_node_path28.default.sep).join("/");
4966
+ const evidenceFile = import_node_path29.default.relative(scanPath, composePath).split(import_node_path29.default.sep).join("/");
4794
4967
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4795
4968
  for (const [composeName, svc] of Object.entries(compose.services)) {
4796
4969
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4812,15 +4985,15 @@ async function addComposeInfra(graph, scanPath, services) {
4812
4985
  for (const dep of dependsOnList(svc.depends_on)) {
4813
4986
  const targetId = composeNameToNodeId.get(dep);
4814
4987
  if (!targetId) continue;
4815
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types18.EdgeType.DEPENDS_ON);
4988
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types19.EdgeType.DEPENDS_ON);
4816
4989
  if (graph.hasEdge(edgeId)) continue;
4817
4990
  const edge = {
4818
4991
  id: edgeId,
4819
4992
  source: sourceId,
4820
4993
  target: targetId,
4821
- type: import_types18.EdgeType.DEPENDS_ON,
4822
- provenance: import_types18.Provenance.EXTRACTED,
4823
- confidence: (0, import_types18.confidenceForExtracted)("structural"),
4994
+ type: import_types19.EdgeType.DEPENDS_ON,
4995
+ provenance: import_types19.Provenance.EXTRACTED,
4996
+ confidence: (0, import_types19.confidenceForExtracted)("structural"),
4824
4997
  evidence: { file: evidenceFile }
4825
4998
  };
4826
4999
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4832,9 +5005,9 @@ async function addComposeInfra(graph, scanPath, services) {
4832
5005
 
4833
5006
  // src/extract/infra/dockerfile.ts
4834
5007
  init_cjs_shims();
4835
- var import_node_path29 = __toESM(require("path"), 1);
5008
+ var import_node_path30 = __toESM(require("path"), 1);
4836
5009
  var import_node_fs16 = require("fs");
4837
- var import_types19 = require("@neat.is/types");
5010
+ var import_types20 = require("@neat.is/types");
4838
5011
  function runtimeImage(content) {
4839
5012
  const lines = content.split("\n");
4840
5013
  let last = null;
@@ -4853,7 +5026,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4853
5026
  let nodesAdded = 0;
4854
5027
  let edgesAdded = 0;
4855
5028
  for (const service of services) {
4856
- const dockerfilePath = import_node_path29.default.join(service.dir, "Dockerfile");
5029
+ const dockerfilePath = import_node_path30.default.join(service.dir, "Dockerfile");
4857
5030
  if (!await exists(dockerfilePath)) continue;
4858
5031
  let content;
4859
5032
  try {
@@ -4861,7 +5034,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4861
5034
  } catch (err) {
4862
5035
  recordExtractionError(
4863
5036
  "infra dockerfile",
4864
- import_node_path29.default.relative(scanPath, dockerfilePath),
5037
+ import_node_path30.default.relative(scanPath, dockerfilePath),
4865
5038
  err
4866
5039
  );
4867
5040
  continue;
@@ -4873,7 +5046,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4873
5046
  graph.addNode(node.id, node);
4874
5047
  nodesAdded++;
4875
5048
  }
4876
- const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
5049
+ const relDockerfile = toPosix2(import_node_path30.default.relative(service.dir, dockerfilePath));
4877
5050
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4878
5051
  graph,
4879
5052
  service.pkg.name,
@@ -4882,17 +5055,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4882
5055
  );
4883
5056
  nodesAdded += fn;
4884
5057
  edgesAdded += fe;
4885
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types19.EdgeType.RUNS_ON);
5058
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types20.EdgeType.RUNS_ON);
4886
5059
  if (!graph.hasEdge(edgeId)) {
4887
5060
  const edge = {
4888
5061
  id: edgeId,
4889
5062
  source: fileNodeId,
4890
5063
  target: node.id,
4891
- type: import_types19.EdgeType.RUNS_ON,
4892
- provenance: import_types19.Provenance.EXTRACTED,
4893
- confidence: (0, import_types19.confidenceForExtracted)("structural"),
5064
+ type: import_types20.EdgeType.RUNS_ON,
5065
+ provenance: import_types20.Provenance.EXTRACTED,
5066
+ confidence: (0, import_types20.confidenceForExtracted)("structural"),
4894
5067
  evidence: {
4895
- file: toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath))
5068
+ file: toPosix2(import_node_path30.default.relative(scanPath, dockerfilePath))
4896
5069
  }
4897
5070
  };
4898
5071
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4905,7 +5078,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4905
5078
  // src/extract/infra/terraform.ts
4906
5079
  init_cjs_shims();
4907
5080
  var import_node_fs17 = require("fs");
4908
- var import_node_path30 = __toESM(require("path"), 1);
5081
+ var import_node_path31 = __toESM(require("path"), 1);
4909
5082
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
4910
5083
  async function walkTfFiles(start, depth = 0, max = 5) {
4911
5084
  if (depth > max) return [];
@@ -4914,11 +5087,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4914
5087
  for (const entry2 of entries) {
4915
5088
  if (entry2.isDirectory()) {
4916
5089
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
4917
- const child = import_node_path30.default.join(start, entry2.name);
5090
+ const child = import_node_path31.default.join(start, entry2.name);
4918
5091
  if (await isPythonVenvDir(child)) continue;
4919
5092
  out.push(...await walkTfFiles(child, depth + 1, max));
4920
5093
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
4921
- out.push(import_node_path30.default.join(start, entry2.name));
5094
+ out.push(import_node_path31.default.join(start, entry2.name));
4922
5095
  }
4923
5096
  }
4924
5097
  return out;
@@ -4946,7 +5119,7 @@ async function addTerraformResources(graph, scanPath) {
4946
5119
  // src/extract/infra/k8s.ts
4947
5120
  init_cjs_shims();
4948
5121
  var import_node_fs18 = require("fs");
4949
- var import_node_path31 = __toESM(require("path"), 1);
5122
+ var import_node_path32 = __toESM(require("path"), 1);
4950
5123
  var import_yaml3 = require("yaml");
4951
5124
  var K8S_KIND_TO_INFRA_KIND = {
4952
5125
  Service: "k8s-service",
@@ -4964,11 +5137,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
4964
5137
  for (const entry2 of entries) {
4965
5138
  if (entry2.isDirectory()) {
4966
5139
  if (IGNORED_DIRS.has(entry2.name)) continue;
4967
- const child = import_node_path31.default.join(start, entry2.name);
5140
+ const child = import_node_path32.default.join(start, entry2.name);
4968
5141
  if (await isPythonVenvDir(child)) continue;
4969
5142
  out.push(...await walkYamlFiles2(child, depth + 1, max));
4970
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path31.default.extname(entry2.name))) {
4971
- out.push(import_node_path31.default.join(start, entry2.name));
5143
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path32.default.extname(entry2.name))) {
5144
+ out.push(import_node_path32.default.join(start, entry2.name));
4972
5145
  }
4973
5146
  }
4974
5147
  return out;
@@ -5012,17 +5185,17 @@ async function addInfra(graph, scanPath, services) {
5012
5185
  }
5013
5186
 
5014
5187
  // src/extract/index.ts
5015
- var import_node_path33 = __toESM(require("path"), 1);
5188
+ var import_node_path34 = __toESM(require("path"), 1);
5016
5189
 
5017
5190
  // src/extract/retire.ts
5018
5191
  init_cjs_shims();
5019
5192
  var import_node_fs19 = require("fs");
5020
- var import_node_path32 = __toESM(require("path"), 1);
5021
- var import_types20 = require("@neat.is/types");
5193
+ var import_node_path33 = __toESM(require("path"), 1);
5194
+ var import_types21 = require("@neat.is/types");
5022
5195
  function dropOrphanedFileNodes(graph) {
5023
5196
  const orphans = [];
5024
5197
  graph.forEachNode((id, attrs) => {
5025
- if (attrs.type !== import_types20.NodeType.FileNode) return;
5198
+ if (attrs.type !== import_types21.NodeType.FileNode) return;
5026
5199
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5027
5200
  orphans.push(id);
5028
5201
  }
@@ -5035,7 +5208,7 @@ function retireEdgesByFile(graph, file) {
5035
5208
  const toDrop = [];
5036
5209
  graph.forEachEdge((id, attrs) => {
5037
5210
  const edge = attrs;
5038
- if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
5211
+ if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5039
5212
  if (!edge.evidence?.file) return;
5040
5213
  if (edge.evidence.file === normalized) toDrop.push(id);
5041
5214
  });
@@ -5048,14 +5221,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5048
5221
  const bases = [scanPath, ...serviceDirs];
5049
5222
  graph.forEachEdge((id, attrs) => {
5050
5223
  const edge = attrs;
5051
- if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
5224
+ if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5052
5225
  const evidenceFile = edge.evidence?.file;
5053
5226
  if (!evidenceFile) return;
5054
- if (import_node_path32.default.isAbsolute(evidenceFile)) {
5227
+ if (import_node_path33.default.isAbsolute(evidenceFile)) {
5055
5228
  if (!(0, import_node_fs19.existsSync)(evidenceFile)) toDrop.push(id);
5056
5229
  return;
5057
5230
  }
5058
- const found = bases.some((base) => (0, import_node_fs19.existsSync)(import_node_path32.default.join(base, evidenceFile)));
5231
+ const found = bases.some((base) => (0, import_node_fs19.existsSync)(import_node_path33.default.join(base, evidenceFile)));
5059
5232
  if (!found) toDrop.push(id);
5060
5233
  });
5061
5234
  for (const id of toDrop) graph.dropEdge(id);
@@ -5095,7 +5268,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5095
5268
  }
5096
5269
  const droppedEntries = drainDroppedExtracted();
5097
5270
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
5098
- const rejectedPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.errorsPath), "rejected.ndjson");
5271
+ const rejectedPath = import_node_path34.default.join(import_node_path34.default.dirname(opts.errorsPath), "rejected.ndjson");
5099
5272
  try {
5100
5273
  await writeRejectedExtracted(droppedEntries, rejectedPath);
5101
5274
  } catch (err) {
@@ -5129,7 +5302,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5129
5302
 
5130
5303
  // src/divergences.ts
5131
5304
  init_cjs_shims();
5132
- var import_types21 = require("@neat.is/types");
5305
+ var import_types22 = require("@neat.is/types");
5133
5306
  function bucketKey(source, target, type) {
5134
5307
  return `${type}|${source}|${target}`;
5135
5308
  }
@@ -5137,22 +5310,22 @@ function bucketEdges(graph) {
5137
5310
  const buckets = /* @__PURE__ */ new Map();
5138
5311
  graph.forEachEdge((id, attrs) => {
5139
5312
  const e = attrs;
5140
- const parsed = (0, import_types21.parseEdgeId)(id);
5313
+ const parsed = (0, import_types22.parseEdgeId)(id);
5141
5314
  const provenance = parsed?.provenance ?? e.provenance;
5142
5315
  const key = bucketKey(e.source, e.target, e.type);
5143
5316
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5144
5317
  switch (provenance) {
5145
- case import_types21.Provenance.EXTRACTED:
5318
+ case import_types22.Provenance.EXTRACTED:
5146
5319
  cur.extracted = e;
5147
5320
  break;
5148
- case import_types21.Provenance.OBSERVED:
5321
+ case import_types22.Provenance.OBSERVED:
5149
5322
  cur.observed = e;
5150
5323
  break;
5151
- case import_types21.Provenance.INFERRED:
5324
+ case import_types22.Provenance.INFERRED:
5152
5325
  cur.inferred = e;
5153
5326
  break;
5154
5327
  default:
5155
- if (e.provenance === import_types21.Provenance.STALE) cur.stale = e;
5328
+ if (e.provenance === import_types22.Provenance.STALE) cur.stale = e;
5156
5329
  }
5157
5330
  buckets.set(key, cur);
5158
5331
  });
@@ -5161,7 +5334,7 @@ function bucketEdges(graph) {
5161
5334
  function nodeIsFrontier(graph, nodeId) {
5162
5335
  if (!graph.hasNode(nodeId)) return false;
5163
5336
  const attrs = graph.getNodeAttributes(nodeId);
5164
- return attrs.type === import_types21.NodeType.FrontierNode;
5337
+ return attrs.type === import_types22.NodeType.FrontierNode;
5165
5338
  }
5166
5339
  function clampConfidence(n) {
5167
5340
  if (!Number.isFinite(n)) return 0;
@@ -5182,7 +5355,7 @@ function gradedConfidence(edge) {
5182
5355
  }
5183
5356
  function detectMissingDivergences(graph, bucket) {
5184
5357
  const out = [];
5185
- if (bucket.type === import_types21.EdgeType.CONTAINS) return out;
5358
+ if (bucket.type === import_types22.EdgeType.CONTAINS) return out;
5186
5359
  if (bucket.extracted && !bucket.observed) {
5187
5360
  if (!nodeIsFrontier(graph, bucket.target)) {
5188
5361
  out.push({
@@ -5223,7 +5396,7 @@ function declaredHostFor(svc) {
5223
5396
  function hasExtractedConfiguredBy(graph, svcId) {
5224
5397
  for (const edgeId of graph.outboundEdges(svcId)) {
5225
5398
  const e = graph.getEdgeAttributes(edgeId);
5226
- if (e.type === import_types21.EdgeType.CONFIGURED_BY && e.provenance === import_types21.Provenance.EXTRACTED) {
5399
+ if (e.type === import_types22.EdgeType.CONFIGURED_BY && e.provenance === import_types22.Provenance.EXTRACTED) {
5227
5400
  return true;
5228
5401
  }
5229
5402
  }
@@ -5236,10 +5409,10 @@ function detectHostMismatch(graph, svcId, svc) {
5236
5409
  const out = [];
5237
5410
  for (const edgeId of graph.outboundEdges(svcId)) {
5238
5411
  const edge = graph.getEdgeAttributes(edgeId);
5239
- if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
5240
- if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
5412
+ if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5413
+ if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
5241
5414
  const target = graph.getNodeAttributes(edge.target);
5242
- if (target.type !== import_types21.NodeType.DatabaseNode) continue;
5415
+ if (target.type !== import_types22.NodeType.DatabaseNode) continue;
5243
5416
  const observedHost = target.host?.trim();
5244
5417
  if (!observedHost) continue;
5245
5418
  if (observedHost === declaredHost) continue;
@@ -5261,10 +5434,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5261
5434
  const deps = svc.dependencies ?? {};
5262
5435
  for (const edgeId of graph.outboundEdges(svcId)) {
5263
5436
  const edge = graph.getEdgeAttributes(edgeId);
5264
- if (edge.type !== import_types21.EdgeType.CONNECTS_TO) continue;
5265
- if (edge.provenance !== import_types21.Provenance.OBSERVED) continue;
5437
+ if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5438
+ if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
5266
5439
  const target = graph.getNodeAttributes(edge.target);
5267
- if (target.type !== import_types21.NodeType.DatabaseNode) continue;
5440
+ if (target.type !== import_types22.NodeType.DatabaseNode) continue;
5268
5441
  for (const pair of compatPairs()) {
5269
5442
  if (pair.engine !== target.engine) continue;
5270
5443
  const declared = deps[pair.driver];
@@ -5325,7 +5498,7 @@ function computeDivergences(graph, opts = {}) {
5325
5498
  }
5326
5499
  graph.forEachNode((nodeId, attrs) => {
5327
5500
  const n = attrs;
5328
- if (n.type !== import_types21.NodeType.ServiceNode) return;
5501
+ if (n.type !== import_types22.NodeType.ServiceNode) return;
5329
5502
  const svc = n;
5330
5503
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5331
5504
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -5358,7 +5531,7 @@ function computeDivergences(graph, opts = {}) {
5358
5531
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5359
5532
  return a.target.localeCompare(b.target);
5360
5533
  });
5361
- return import_types21.DivergenceResultSchema.parse({
5534
+ return import_types22.DivergenceResultSchema.parse({
5362
5535
  divergences: filtered,
5363
5536
  totalAffected: filtered.length,
5364
5537
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5368,8 +5541,8 @@ function computeDivergences(graph, opts = {}) {
5368
5541
  // src/persist.ts
5369
5542
  init_cjs_shims();
5370
5543
  var import_node_fs20 = require("fs");
5371
- var import_node_path34 = __toESM(require("path"), 1);
5372
- var import_types22 = require("@neat.is/types");
5544
+ var import_node_path35 = __toESM(require("path"), 1);
5545
+ var import_types23 = require("@neat.is/types");
5373
5546
  var SCHEMA_VERSION = 4;
5374
5547
  function migrateV1ToV2(payload) {
5375
5548
  const nodes = payload.graph.nodes;
@@ -5391,12 +5564,12 @@ function migrateV2ToV3(payload) {
5391
5564
  for (const edge of edges) {
5392
5565
  const attrs = edge.attributes;
5393
5566
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5394
- attrs.provenance = import_types22.Provenance.OBSERVED;
5567
+ attrs.provenance = import_types23.Provenance.OBSERVED;
5395
5568
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5396
5569
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5397
5570
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5398
5571
  if (type && source && target) {
5399
- const newId = (0, import_types22.observedEdgeId)(source, target, type);
5572
+ const newId = (0, import_types23.observedEdgeId)(source, target, type);
5400
5573
  attrs.id = newId;
5401
5574
  if (edge.key) edge.key = newId;
5402
5575
  }
@@ -5405,7 +5578,7 @@ function migrateV2ToV3(payload) {
5405
5578
  return { ...payload, schemaVersion: 3 };
5406
5579
  }
5407
5580
  async function ensureDir(filePath) {
5408
- await import_node_fs20.promises.mkdir(import_node_path34.default.dirname(filePath), { recursive: true });
5581
+ await import_node_fs20.promises.mkdir(import_node_path35.default.dirname(filePath), { recursive: true });
5409
5582
  }
5410
5583
  async function saveGraphToDisk(graph, outPath) {
5411
5584
  await ensureDir(outPath);
@@ -5481,7 +5654,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5481
5654
  // src/gitignore.ts
5482
5655
  init_cjs_shims();
5483
5656
  var import_node_fs21 = require("fs");
5484
- var import_node_path35 = __toESM(require("path"), 1);
5657
+ var import_node_path36 = __toESM(require("path"), 1);
5485
5658
  var NEAT_OUT_LINE = "neat-out/";
5486
5659
  var NEAT_HEADER = "# NEAT \u2014 machine-local snapshots and events";
5487
5660
  function isNeatOutLine(line) {
@@ -5489,7 +5662,7 @@ function isNeatOutLine(line) {
5489
5662
  return trimmed === "neat-out/" || trimmed === "neat-out";
5490
5663
  }
5491
5664
  async function ensureNeatOutIgnored(projectDir) {
5492
- const file = import_node_path35.default.join(projectDir, ".gitignore");
5665
+ const file = import_node_path36.default.join(projectDir, ".gitignore");
5493
5666
  let existing = null;
5494
5667
  try {
5495
5668
  existing = await import_node_fs21.promises.readFile(file, "utf8");
@@ -5516,7 +5689,7 @@ ${NEAT_OUT_LINE}
5516
5689
 
5517
5690
  // src/summary.ts
5518
5691
  init_cjs_shims();
5519
- var import_types23 = require("@neat.is/types");
5692
+ var import_types24 = require("@neat.is/types");
5520
5693
  function renderOtelEnvBlock() {
5521
5694
  return [
5522
5695
  "for prod OTel routing, set these in your deploy platform's env:",
@@ -5526,19 +5699,19 @@ function renderOtelEnvBlock() {
5526
5699
  }
5527
5700
  function findIncompatServices(nodes) {
5528
5701
  return nodes.filter(
5529
- (n) => n.type === import_types23.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
5702
+ (n) => n.type === import_types24.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
5530
5703
  );
5531
5704
  }
5532
5705
  function servicesWithoutObserved(nodes, edges) {
5533
5706
  const seen = /* @__PURE__ */ new Set();
5534
5707
  for (const e of edges) {
5535
- if (e.provenance === import_types23.Provenance.OBSERVED) {
5708
+ if (e.provenance === import_types24.Provenance.OBSERVED) {
5536
5709
  seen.add(e.source);
5537
5710
  seen.add(e.target);
5538
5711
  }
5539
5712
  }
5540
5713
  return nodes.filter(
5541
- (n) => n.type === import_types23.NodeType.ServiceNode && !seen.has(n.id)
5714
+ (n) => n.type === import_types24.NodeType.ServiceNode && !seen.has(n.id)
5542
5715
  );
5543
5716
  }
5544
5717
  function formatDivergence(d) {
@@ -5613,26 +5786,26 @@ function formatIncompat(inc) {
5613
5786
  // src/watch.ts
5614
5787
  init_cjs_shims();
5615
5788
  var import_node_fs27 = __toESM(require("fs"), 1);
5616
- var import_node_path43 = __toESM(require("path"), 1);
5789
+ var import_node_path44 = __toESM(require("path"), 1);
5617
5790
  var import_chokidar = __toESM(require("chokidar"), 1);
5618
5791
 
5619
5792
  // src/api.ts
5620
5793
  init_cjs_shims();
5621
5794
  var import_fastify = __toESM(require("fastify"), 1);
5622
5795
  var import_cors = __toESM(require("@fastify/cors"), 1);
5623
- var import_types25 = require("@neat.is/types");
5796
+ var import_types26 = require("@neat.is/types");
5624
5797
 
5625
5798
  // src/extend/index.ts
5626
5799
  init_cjs_shims();
5627
5800
  var import_node_fs23 = require("fs");
5628
- var import_node_path37 = __toESM(require("path"), 1);
5801
+ var import_node_path38 = __toESM(require("path"), 1);
5629
5802
  var import_node_os2 = __toESM(require("os"), 1);
5630
5803
  var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
5631
5804
 
5632
5805
  // src/installers/package-manager.ts
5633
5806
  init_cjs_shims();
5634
5807
  var import_node_fs22 = require("fs");
5635
- var import_node_path36 = __toESM(require("path"), 1);
5808
+ var import_node_path37 = __toESM(require("path"), 1);
5636
5809
  var import_node_child_process = require("child_process");
5637
5810
  var LOCKFILE_PRIORITY = [
5638
5811
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -5654,22 +5827,22 @@ async function exists2(p) {
5654
5827
  }
5655
5828
  }
5656
5829
  async function detectPackageManager(serviceDir) {
5657
- let dir = import_node_path36.default.resolve(serviceDir);
5830
+ let dir = import_node_path37.default.resolve(serviceDir);
5658
5831
  const stops = /* @__PURE__ */ new Set();
5659
5832
  for (let i = 0; i < 64; i++) {
5660
5833
  if (stops.has(dir)) break;
5661
5834
  stops.add(dir);
5662
5835
  for (const candidate of LOCKFILE_PRIORITY) {
5663
- const lockPath = import_node_path36.default.join(dir, candidate.lockfile);
5836
+ const lockPath = import_node_path37.default.join(dir, candidate.lockfile);
5664
5837
  if (await exists2(lockPath)) {
5665
5838
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
5666
5839
  }
5667
5840
  }
5668
- const parent = import_node_path36.default.dirname(dir);
5841
+ const parent = import_node_path37.default.dirname(dir);
5669
5842
  if (parent === dir) break;
5670
5843
  dir = parent;
5671
5844
  }
5672
- return { pm: "npm", cwd: import_node_path36.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5845
+ return { pm: "npm", cwd: import_node_path37.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5673
5846
  }
5674
5847
  async function runPackageManagerInstall(cmd) {
5675
5848
  return new Promise((resolve) => {
@@ -5718,7 +5891,7 @@ async function fileExists2(p) {
5718
5891
  }
5719
5892
  }
5720
5893
  async function readPackageJson(scanPath) {
5721
- const pkgPath = import_node_path37.default.join(scanPath, "package.json");
5894
+ const pkgPath = import_node_path38.default.join(scanPath, "package.json");
5722
5895
  const raw = await import_node_fs23.promises.readFile(pkgPath, "utf8");
5723
5896
  return JSON.parse(raw);
5724
5897
  }
@@ -5729,11 +5902,11 @@ async function findHookFiles(scanPath) {
5729
5902
  ).sort();
5730
5903
  }
5731
5904
  function extendLogPath() {
5732
- return process.env.NEAT_EXTEND_LOG ?? import_node_path37.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5905
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path38.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5733
5906
  }
5734
5907
  async function appendExtendLog(entry2) {
5735
5908
  const logPath = extendLogPath();
5736
- await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(logPath), { recursive: true });
5909
+ await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(logPath), { recursive: true });
5737
5910
  await import_node_fs23.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
5738
5911
  }
5739
5912
  function splicedContent(fileContent, snippet2) {
@@ -5792,7 +5965,7 @@ function lookupInstrumentation(library, installedVersion) {
5792
5965
  }
5793
5966
  async function describeProjectInstrumentation(ctx) {
5794
5967
  const hookFiles = await findHookFiles(ctx.scanPath);
5795
- const envNeat = await fileExists2(import_node_path37.default.join(ctx.scanPath, ".env.neat"));
5968
+ const envNeat = await fileExists2(import_node_path38.default.join(ctx.scanPath, ".env.neat"));
5796
5969
  const registryInstrPackages = new Set(
5797
5970
  (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
5798
5971
  );
@@ -5814,16 +5987,16 @@ async function applyExtension(ctx, args, options) {
5814
5987
  );
5815
5988
  }
5816
5989
  for (const file of hookFiles) {
5817
- const content = await import_node_fs23.promises.readFile(import_node_path37.default.join(ctx.scanPath, file), "utf8");
5990
+ const content = await import_node_fs23.promises.readFile(import_node_path38.default.join(ctx.scanPath, file), "utf8");
5818
5991
  if (content.includes(args.registration_snippet)) {
5819
5992
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5820
5993
  }
5821
5994
  }
5822
5995
  const primaryFile = hookFiles[0];
5823
- const primaryPath = import_node_path37.default.join(ctx.scanPath, primaryFile);
5996
+ const primaryPath = import_node_path38.default.join(ctx.scanPath, primaryFile);
5824
5997
  const filesTouched = [];
5825
5998
  const depsAdded = [];
5826
- const pkgPath = import_node_path37.default.join(ctx.scanPath, "package.json");
5999
+ const pkgPath = import_node_path38.default.join(ctx.scanPath, "package.json");
5827
6000
  const pkg = await readPackageJson(ctx.scanPath);
5828
6001
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5829
6002
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -5869,7 +6042,7 @@ async function dryRunExtension(ctx, args) {
5869
6042
  };
5870
6043
  }
5871
6044
  for (const file of hookFiles) {
5872
- const content = await import_node_fs23.promises.readFile(import_node_path37.default.join(ctx.scanPath, file), "utf8");
6045
+ const content = await import_node_fs23.promises.readFile(import_node_path38.default.join(ctx.scanPath, file), "utf8");
5873
6046
  if (content.includes(args.registration_snippet)) {
5874
6047
  return {
5875
6048
  library: args.library,
@@ -5891,7 +6064,7 @@ async function dryRunExtension(ctx, args) {
5891
6064
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5892
6065
  filesTouched.push("package.json");
5893
6066
  }
5894
- const hookContent = await import_node_fs23.promises.readFile(import_node_path37.default.join(ctx.scanPath, primaryFile), "utf8");
6067
+ const hookContent = await import_node_fs23.promises.readFile(import_node_path38.default.join(ctx.scanPath, primaryFile), "utf8");
5895
6068
  const patched = splicedContent(hookContent, args.registration_snippet);
5896
6069
  if (patched) {
5897
6070
  filesTouched.push(primaryFile);
@@ -5912,7 +6085,7 @@ async function rollbackExtension(ctx, args) {
5912
6085
  if (!match) {
5913
6086
  return { undone: false, message: "no apply found for library" };
5914
6087
  }
5915
- const pkgPath = import_node_path37.default.join(ctx.scanPath, "package.json");
6088
+ const pkgPath = import_node_path38.default.join(ctx.scanPath, "package.json");
5916
6089
  if (await fileExists2(pkgPath)) {
5917
6090
  const pkg = await readPackageJson(ctx.scanPath);
5918
6091
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -5923,7 +6096,7 @@ async function rollbackExtension(ctx, args) {
5923
6096
  }
5924
6097
  const hookFiles = await findHookFiles(ctx.scanPath);
5925
6098
  for (const file of hookFiles) {
5926
- const filePath = import_node_path37.default.join(ctx.scanPath, file);
6099
+ const filePath = import_node_path38.default.join(ctx.scanPath, file);
5927
6100
  const content = await import_node_fs23.promises.readFile(filePath, "utf8");
5928
6101
  if (content.includes(match.registration_snippet)) {
5929
6102
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -6016,23 +6189,23 @@ function canonicalJson(value) {
6016
6189
 
6017
6190
  // src/projects.ts
6018
6191
  init_cjs_shims();
6019
- var import_node_path38 = __toESM(require("path"), 1);
6192
+ var import_node_path39 = __toESM(require("path"), 1);
6020
6193
  function pathsForProject(project, baseDir) {
6021
6194
  if (project === DEFAULT_PROJECT) {
6022
6195
  return {
6023
- snapshotPath: import_node_path38.default.join(baseDir, "graph.json"),
6024
- errorsPath: import_node_path38.default.join(baseDir, "errors.ndjson"),
6025
- staleEventsPath: import_node_path38.default.join(baseDir, "stale-events.ndjson"),
6026
- embeddingsCachePath: import_node_path38.default.join(baseDir, "embeddings.json"),
6027
- policyViolationsPath: import_node_path38.default.join(baseDir, "policy-violations.ndjson")
6196
+ snapshotPath: import_node_path39.default.join(baseDir, "graph.json"),
6197
+ errorsPath: import_node_path39.default.join(baseDir, "errors.ndjson"),
6198
+ staleEventsPath: import_node_path39.default.join(baseDir, "stale-events.ndjson"),
6199
+ embeddingsCachePath: import_node_path39.default.join(baseDir, "embeddings.json"),
6200
+ policyViolationsPath: import_node_path39.default.join(baseDir, "policy-violations.ndjson")
6028
6201
  };
6029
6202
  }
6030
6203
  return {
6031
- snapshotPath: import_node_path38.default.join(baseDir, `${project}.json`),
6032
- errorsPath: import_node_path38.default.join(baseDir, `errors.${project}.ndjson`),
6033
- staleEventsPath: import_node_path38.default.join(baseDir, `stale-events.${project}.ndjson`),
6034
- embeddingsCachePath: import_node_path38.default.join(baseDir, `embeddings.${project}.json`),
6035
- policyViolationsPath: import_node_path38.default.join(baseDir, `policy-violations.${project}.ndjson`)
6204
+ snapshotPath: import_node_path39.default.join(baseDir, `${project}.json`),
6205
+ errorsPath: import_node_path39.default.join(baseDir, `errors.${project}.ndjson`),
6206
+ staleEventsPath: import_node_path39.default.join(baseDir, `stale-events.${project}.ndjson`),
6207
+ embeddingsCachePath: import_node_path39.default.join(baseDir, `embeddings.${project}.json`),
6208
+ policyViolationsPath: import_node_path39.default.join(baseDir, `policy-violations.${project}.ndjson`)
6036
6209
  };
6037
6210
  }
6038
6211
  var Projects = class {
@@ -6070,23 +6243,128 @@ var Projects = class {
6070
6243
  init_cjs_shims();
6071
6244
  var import_node_fs25 = require("fs");
6072
6245
  var import_node_os3 = __toESM(require("os"), 1);
6073
- var import_node_path39 = __toESM(require("path"), 1);
6074
- var import_types24 = require("@neat.is/types");
6246
+ var import_node_path40 = __toESM(require("path"), 1);
6247
+ var import_types25 = require("@neat.is/types");
6075
6248
  var LOCK_TIMEOUT_MS = 5e3;
6076
6249
  var LOCK_RETRY_MS = 50;
6077
6250
  function neatHome() {
6078
6251
  const override = process.env.NEAT_HOME;
6079
- if (override && override.length > 0) return import_node_path39.default.resolve(override);
6080
- return import_node_path39.default.join(import_node_os3.default.homedir(), ".neat");
6252
+ if (override && override.length > 0) return import_node_path40.default.resolve(override);
6253
+ return import_node_path40.default.join(import_node_os3.default.homedir(), ".neat");
6081
6254
  }
6082
6255
  function registryPath() {
6083
- return import_node_path39.default.join(neatHome(), "projects.json");
6256
+ return import_node_path40.default.join(neatHome(), "projects.json");
6084
6257
  }
6085
6258
  function registryLockPath() {
6086
- return import_node_path39.default.join(neatHome(), "projects.json.lock");
6259
+ return import_node_path40.default.join(neatHome(), "projects.json.lock");
6087
6260
  }
6088
6261
  function daemonPidPath() {
6089
- return import_node_path39.default.join(neatHome(), "neatd.pid");
6262
+ return import_node_path40.default.join(neatHome(), "neatd.pid");
6263
+ }
6264
+ function daemonsDir() {
6265
+ return import_node_path40.default.join(neatHome(), "daemons");
6266
+ }
6267
+ function isFiniteInt(v) {
6268
+ return typeof v === "number" && Number.isFinite(v);
6269
+ }
6270
+ function parseDaemonRecord(raw) {
6271
+ let obj;
6272
+ try {
6273
+ obj = JSON.parse(raw);
6274
+ } catch {
6275
+ return void 0;
6276
+ }
6277
+ if (typeof obj !== "object" || obj === null) return void 0;
6278
+ const r = obj;
6279
+ const ports = r.ports;
6280
+ 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)) {
6281
+ return void 0;
6282
+ }
6283
+ return {
6284
+ project: r.project,
6285
+ projectPath: r.projectPath,
6286
+ pid: r.pid,
6287
+ status: r.status,
6288
+ ports: { rest: ports.rest, otlp: ports.otlp, web: ports.web },
6289
+ startedAt: r.startedAt,
6290
+ neatVersion: r.neatVersion
6291
+ };
6292
+ }
6293
+ var defaultDiscoveryProbe = { isPidAlive: isPidAliveDefault };
6294
+ async function discoverDaemons(probe = defaultDiscoveryProbe) {
6295
+ const dir = daemonsDir();
6296
+ let names;
6297
+ try {
6298
+ names = await import_node_fs25.promises.readdir(dir);
6299
+ } catch (err) {
6300
+ if (err.code === "ENOENT") return [];
6301
+ throw err;
6302
+ }
6303
+ const out = [];
6304
+ for (const name of names) {
6305
+ if (!name.endsWith(".json")) continue;
6306
+ const file = import_node_path40.default.join(dir, name);
6307
+ let raw;
6308
+ try {
6309
+ raw = await import_node_fs25.promises.readFile(file, "utf8");
6310
+ } catch {
6311
+ continue;
6312
+ }
6313
+ const record = parseDaemonRecord(raw);
6314
+ if (!record) continue;
6315
+ const live = record.status === "running" && probe.isPidAlive(record.pid);
6316
+ out.push({ record, live, source: file });
6317
+ }
6318
+ out.sort((a, b) => a.record.project.localeCompare(b.record.project));
6319
+ return out;
6320
+ }
6321
+ async function removeDaemonRecord(source) {
6322
+ await import_node_fs25.promises.unlink(source).catch(() => {
6323
+ });
6324
+ }
6325
+ async function listMachineProjects(probe = defaultDiscoveryProbe) {
6326
+ const discovered = await discoverDaemons(probe);
6327
+ const byPath = /* @__PURE__ */ new Map();
6328
+ for (const d of discovered) {
6329
+ const key = await normalizeProjectPath(d.record.projectPath);
6330
+ byPath.set(key, {
6331
+ project: d.record.project,
6332
+ projectPath: d.record.projectPath,
6333
+ state: d.live ? "running" : "stopped",
6334
+ ports: d.record.ports,
6335
+ pid: d.record.pid
6336
+ });
6337
+ }
6338
+ let legacy = [];
6339
+ try {
6340
+ legacy = (await readRegistry()).projects;
6341
+ } catch {
6342
+ legacy = [];
6343
+ }
6344
+ for (const entry2 of legacy) {
6345
+ const key = await normalizeProjectPath(entry2.path);
6346
+ if (byPath.has(key)) continue;
6347
+ byPath.set(key, {
6348
+ project: entry2.name,
6349
+ projectPath: entry2.path,
6350
+ state: "registered",
6351
+ registryStatus: entry2.status
6352
+ });
6353
+ }
6354
+ return [...byPath.values()].sort((a, b) => a.project.localeCompare(b.project));
6355
+ }
6356
+ async function findDaemonByProject(name, probe = defaultDiscoveryProbe) {
6357
+ const discovered = await discoverDaemons(probe);
6358
+ return discovered.find((d) => d.record.project === name);
6359
+ }
6360
+ function signalDaemonStop(pid) {
6361
+ try {
6362
+ process.kill(pid, "SIGTERM");
6363
+ return true;
6364
+ } catch (err) {
6365
+ if (err.code === "ESRCH") return false;
6366
+ throw err;
6367
+ }
6090
6368
  }
6091
6369
  function isPidAliveDefault(pid) {
6092
6370
  try {
@@ -6146,7 +6424,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
6146
6424
  }
6147
6425
  }
6148
6426
  async function normalizeProjectPath(input) {
6149
- const resolved = import_node_path39.default.resolve(input);
6427
+ const resolved = import_node_path40.default.resolve(input);
6150
6428
  try {
6151
6429
  return await import_node_fs25.promises.realpath(resolved);
6152
6430
  } catch {
@@ -6154,7 +6432,7 @@ async function normalizeProjectPath(input) {
6154
6432
  }
6155
6433
  }
6156
6434
  async function writeAtomically(target, contents) {
6157
- await import_node_fs25.promises.mkdir(import_node_path39.default.dirname(target), { recursive: true });
6435
+ await import_node_fs25.promises.mkdir(import_node_path40.default.dirname(target), { recursive: true });
6158
6436
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
6159
6437
  const fd = await import_node_fs25.promises.open(tmp, "w");
6160
6438
  try {
@@ -6167,7 +6445,7 @@ async function writeAtomically(target, contents) {
6167
6445
  }
6168
6446
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
6169
6447
  const deadline = Date.now() + timeoutMs;
6170
- await import_node_fs25.promises.mkdir(import_node_path39.default.dirname(lockPath), { recursive: true });
6448
+ await import_node_fs25.promises.mkdir(import_node_path40.default.dirname(lockPath), { recursive: true });
6171
6449
  let probedHolder = false;
6172
6450
  while (true) {
6173
6451
  try {
@@ -6220,10 +6498,10 @@ async function readRegistry() {
6220
6498
  throw err;
6221
6499
  }
6222
6500
  const parsed = JSON.parse(raw);
6223
- return import_types24.RegistryFileSchema.parse(parsed);
6501
+ return import_types25.RegistryFileSchema.parse(parsed);
6224
6502
  }
6225
6503
  async function writeRegistry(reg) {
6226
- const validated = import_types24.RegistryFileSchema.parse(reg);
6504
+ const validated = import_types25.RegistryFileSchema.parse(reg);
6227
6505
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6228
6506
  }
6229
6507
  var ProjectNameCollisionError = class extends Error {
@@ -6294,6 +6572,58 @@ async function removeProject(name) {
6294
6572
  return removed;
6295
6573
  });
6296
6574
  }
6575
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
6576
+ var DEFAULT_PRUNE_TTL_MS = 7 * DAY_MS2;
6577
+ function pruneTtlMs() {
6578
+ const raw = process.env.NEAT_REGISTRY_PRUNE_TTL_MS;
6579
+ if (!raw) return DEFAULT_PRUNE_TTL_MS;
6580
+ const n = Number.parseInt(raw, 10);
6581
+ if (Number.isFinite(n) && n >= 0) return n;
6582
+ console.warn(
6583
+ `[neat] NEAT_REGISTRY_PRUNE_TTL_MS could not be parsed (${raw}); using default ${DEFAULT_PRUNE_TTL_MS}ms`
6584
+ );
6585
+ return DEFAULT_PRUNE_TTL_MS;
6586
+ }
6587
+ async function statPathStatus(p) {
6588
+ try {
6589
+ const stat = await import_node_fs25.promises.stat(p);
6590
+ return stat.isDirectory() ? "present" : "unknown";
6591
+ } catch (err) {
6592
+ return err.code === "ENOENT" ? "gone" : "unknown";
6593
+ }
6594
+ }
6595
+ async function pruneRegistry(opts = {}) {
6596
+ const ttlMs = opts.ttlMs ?? pruneTtlMs();
6597
+ const statPath = opts.statPath ?? statPathStatus;
6598
+ const now = opts.now ?? Date.now;
6599
+ return withLock(async () => {
6600
+ const reg = await readRegistry();
6601
+ const removed = [];
6602
+ const kept = [];
6603
+ for (const entry2 of reg.projects) {
6604
+ const status2 = await statPath(entry2.path);
6605
+ if (status2 !== "gone") {
6606
+ kept.push(entry2);
6607
+ continue;
6608
+ }
6609
+ if (ttlMs <= 0) {
6610
+ removed.push(entry2);
6611
+ continue;
6612
+ }
6613
+ const lastSeen = Date.parse(entry2.lastSeenAt ?? entry2.registeredAt);
6614
+ const age = now() - (Number.isFinite(lastSeen) ? lastSeen : 0);
6615
+ if (age > ttlMs) {
6616
+ removed.push(entry2);
6617
+ } else {
6618
+ kept.push(entry2);
6619
+ }
6620
+ }
6621
+ if (removed.length === 0) return [];
6622
+ reg.projects = kept;
6623
+ await writeRegistry(reg);
6624
+ return removed;
6625
+ });
6626
+ }
6297
6627
 
6298
6628
  // src/streaming.ts
6299
6629
  init_cjs_shims();
@@ -6307,6 +6637,7 @@ function handleSse(req, reply, opts) {
6307
6637
  reply.raw.setHeader("Connection", "keep-alive");
6308
6638
  reply.raw.setHeader("X-Accel-Buffering", "no");
6309
6639
  reply.raw.flushHeaders?.();
6640
+ reply.raw.write(":open\n\n");
6310
6641
  let pending = 0;
6311
6642
  let dropped = false;
6312
6643
  const closeConnection = () => {
@@ -6486,11 +6817,11 @@ function registerRoutes(scope, ctx) {
6486
6817
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6487
6818
  const parsed = [];
6488
6819
  for (const c of candidates) {
6489
- const r = import_types25.DivergenceTypeSchema.safeParse(c);
6820
+ const r = import_types26.DivergenceTypeSchema.safeParse(c);
6490
6821
  if (!r.success) {
6491
6822
  return reply.code(400).send({
6492
6823
  error: `unknown divergence type "${c}"`,
6493
- allowed: import_types25.DivergenceTypeSchema.options
6824
+ allowed: import_types26.DivergenceTypeSchema.options
6494
6825
  });
6495
6826
  }
6496
6827
  parsed.push(r.data);
@@ -6703,7 +7034,7 @@ function registerRoutes(scope, ctx) {
6703
7034
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6704
7035
  let violations = await log.readAll();
6705
7036
  if (req.query.severity) {
6706
- const sev = import_types25.PolicySeveritySchema.safeParse(req.query.severity);
7037
+ const sev = import_types26.PolicySeveritySchema.safeParse(req.query.severity);
6707
7038
  if (!sev.success) {
6708
7039
  return reply.code(400).send({
6709
7040
  error: "invalid severity",
@@ -6720,7 +7051,7 @@ function registerRoutes(scope, ctx) {
6720
7051
  scope.post("/policies/check", async (req, reply) => {
6721
7052
  const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6722
7053
  if (!proj) return;
6723
- const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req.body ?? {});
7054
+ const parsed = import_types26.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6724
7055
  if (!parsed.success) {
6725
7056
  return reply.code(400).send({
6726
7057
  error: "invalid /policies/check body",
@@ -6964,7 +7295,7 @@ init_otel_grpc();
6964
7295
  // src/search.ts
6965
7296
  init_cjs_shims();
6966
7297
  var import_node_fs26 = require("fs");
6967
- var import_node_path42 = __toESM(require("path"), 1);
7298
+ var import_node_path43 = __toESM(require("path"), 1);
6968
7299
  var import_node_crypto3 = require("crypto");
6969
7300
  var DEFAULT_LIMIT = 10;
6970
7301
  var NOMIC_DIM = 768;
@@ -7103,7 +7434,7 @@ async function readCache(cachePath) {
7103
7434
  }
7104
7435
  }
7105
7436
  async function writeCache(cachePath, cache) {
7106
- await import_node_fs26.promises.mkdir(import_node_path42.default.dirname(cachePath), { recursive: true });
7437
+ await import_node_fs26.promises.mkdir(import_node_path43.default.dirname(cachePath), { recursive: true });
7107
7438
  await import_node_fs26.promises.writeFile(cachePath, JSON.stringify(cache));
7108
7439
  }
7109
7440
  var VectorIndex = class {
@@ -7260,8 +7591,8 @@ var ALL_PHASES = [
7260
7591
  ];
7261
7592
  function classifyChange(relPath) {
7262
7593
  const phases = /* @__PURE__ */ new Set();
7263
- const base = import_node_path43.default.basename(relPath).toLowerCase();
7264
- const segments = relPath.split(import_node_path43.default.sep).map((s) => s.toLowerCase());
7594
+ const base = import_node_path44.default.basename(relPath).toLowerCase();
7595
+ const segments = relPath.split(import_node_path44.default.sep).map((s) => s.toLowerCase());
7265
7596
  if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
7266
7597
  phases.add("services");
7267
7598
  phases.add("aliases");
@@ -7382,9 +7713,9 @@ function countWatchableDirs(scanPath, limit) {
7382
7713
  for (const e of entries) {
7383
7714
  if (count >= limit) return;
7384
7715
  if (!e.isDirectory()) continue;
7385
- if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path43.default.join(dir, e.name) + import_node_path43.default.sep))) continue;
7716
+ if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path44.default.join(dir, e.name) + import_node_path44.default.sep))) continue;
7386
7717
  count++;
7387
- if (depth < 2) visit(import_node_path43.default.join(dir, e.name), depth + 1);
7718
+ if (depth < 2) visit(import_node_path44.default.join(dir, e.name), depth + 1);
7388
7719
  }
7389
7720
  };
7390
7721
  visit(scanPath, 0);
@@ -7402,8 +7733,8 @@ async function startWatch(graph, opts) {
7402
7733
  const projectName = opts.project ?? DEFAULT_PROJECT;
7403
7734
  await loadGraphFromDisk(graph, opts.outPath);
7404
7735
  const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
7405
- const policyFilePath = import_node_path43.default.join(opts.scanPath, "policy.json");
7406
- const policyViolationsPath = import_node_path43.default.join(import_node_path43.default.dirname(opts.outPath), "policy-violations.ndjson");
7736
+ const policyFilePath = import_node_path44.default.join(opts.scanPath, "policy.json");
7737
+ const policyViolationsPath = import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), "policy-violations.ndjson");
7407
7738
  let policies = [];
7408
7739
  try {
7409
7740
  policies = await loadPolicyFile(policyFilePath);
@@ -7454,7 +7785,7 @@ async function startWatch(graph, opts) {
7454
7785
  assertBindAuthority(host, auth.authToken);
7455
7786
  const port = opts.port ?? 8080;
7456
7787
  const otelPort = opts.otelPort ?? 4318;
7457
- const cachePath = opts.embeddingsCachePath ?? import_node_path43.default.join(import_node_path43.default.dirname(opts.outPath), "embeddings.json");
7788
+ const cachePath = opts.embeddingsCachePath ?? import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), "embeddings.json");
7458
7789
  let searchIndex;
7459
7790
  try {
7460
7791
  searchIndex = await buildSearchIndex(graph, { cachePath });
@@ -7472,7 +7803,7 @@ async function startWatch(graph, opts) {
7472
7803
  // Paths are derived from the explicit options the watch caller passes
7473
7804
  // — pathsForProject is only used to fill in the embeddings/snapshot
7474
7805
  // fields so the registry shape is complete.
7475
- ...pathsForProject(projectName, import_node_path43.default.dirname(opts.outPath)),
7806
+ ...pathsForProject(projectName, import_node_path44.default.dirname(opts.outPath)),
7476
7807
  snapshotPath: opts.outPath,
7477
7808
  errorsPath: opts.errorsPath,
7478
7809
  staleEventsPath: opts.staleEventsPath
@@ -7559,9 +7890,9 @@ async function startWatch(graph, opts) {
7559
7890
  };
7560
7891
  const onPath = (absPath) => {
7561
7892
  if (shouldIgnore(absPath)) return;
7562
- const rel = import_node_path43.default.relative(opts.scanPath, absPath);
7893
+ const rel = import_node_path44.default.relative(opts.scanPath, absPath);
7563
7894
  if (!rel || rel.startsWith("..")) return;
7564
- pendingPaths.add(rel.split(import_node_path43.default.sep).join("/"));
7895
+ pendingPaths.add(rel.split(import_node_path44.default.sep).join("/"));
7565
7896
  const phases = classifyChange(rel);
7566
7897
  if (phases.size === 0) {
7567
7898
  for (const p of ALL_PHASES) pending.add(p);
@@ -7616,7 +7947,7 @@ async function startWatch(graph, opts) {
7616
7947
  // src/deploy/detect.ts
7617
7948
  init_cjs_shims();
7618
7949
  var import_node_fs28 = require("fs");
7619
- var import_node_path44 = __toESM(require("path"), 1);
7950
+ var import_node_path45 = __toESM(require("path"), 1);
7620
7951
  var import_node_child_process2 = require("child_process");
7621
7952
  var import_node_crypto4 = require("crypto");
7622
7953
  function generateToken() {
@@ -7716,7 +8047,7 @@ async function runDeploy(opts = {}) {
7716
8047
  const token = generateToken();
7717
8048
  switch (substrate) {
7718
8049
  case "docker-compose": {
7719
- const artifactPath = import_node_path44.default.join(cwd, "docker-compose.neat.yml");
8050
+ const artifactPath = import_node_path45.default.join(cwd, "docker-compose.neat.yml");
7720
8051
  const contents = emitDockerCompose(cwd);
7721
8052
  await import_node_fs28.promises.writeFile(artifactPath, contents, "utf8");
7722
8053
  return {
@@ -7724,11 +8055,11 @@ async function runDeploy(opts = {}) {
7724
8055
  artifactPath,
7725
8056
  token,
7726
8057
  contents,
7727
- startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path44.default.basename(artifactPath)} up -d`
8058
+ startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path45.default.basename(artifactPath)} up -d`
7728
8059
  };
7729
8060
  }
7730
8061
  case "systemd": {
7731
- const artifactPath = import_node_path44.default.join(cwd, "neat.service");
8062
+ const artifactPath = import_node_path45.default.join(cwd, "neat.service");
7732
8063
  const contents = emitSystemdUnit(cwd);
7733
8064
  await import_node_fs28.promises.writeFile(artifactPath, contents, "utf8");
7734
8065
  return {
@@ -7764,15 +8095,56 @@ init_cjs_shims();
7764
8095
  // src/installers/javascript.ts
7765
8096
  init_cjs_shims();
7766
8097
  var import_node_fs29 = require("fs");
7767
- var import_node_path45 = __toESM(require("path"), 1);
8098
+ var import_node_path46 = __toESM(require("path"), 1);
7768
8099
  var import_semver2 = __toESM(require("semver"), 1);
7769
8100
 
7770
8101
  // src/installers/templates.ts
7771
8102
  init_cjs_shims();
7772
8103
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
7773
- var OTEL_INIT_STAMP = "// neat-template-version: 5 \u2014 layered file-first capture (ADR-090) + http/json protocol pin (#468).";
8104
+ var OTEL_INIT_STAMP = "// neat-template-version: 6 \u2014 daemon.json endpoint resolution (ADR-096) + layered file-first capture (ADR-090).";
7774
8105
  var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer ' + process.env.NEAT_OTEL_TOKEN";
7775
8106
  var OTEL_OTLP_PROTOCOL_JS = "process.env.OTEL_EXPORTER_OTLP_PROTOCOL ||= 'http/json'";
8107
+ var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
8108
+ try {
8109
+ if (process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return
8110
+ const __neatFs = require('node:fs')
8111
+ const __neatPath = require('node:path')
8112
+ let __neatDir = process.cwd()
8113
+ for (let __i = 0; __i < 8; __i++) {
8114
+ try {
8115
+ const __rec = JSON.parse(__neatFs.readFileSync(__neatPath.join(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
8116
+ if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
8117
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
8118
+ break
8119
+ }
8120
+ } catch (_e) {}
8121
+ const __parent = __neatPath.dirname(__neatDir)
8122
+ if (__parent === __neatDir) break
8123
+ __neatDir = __parent
8124
+ }
8125
+ } catch (_e) {}
8126
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
8127
+ })()`;
8128
+ var OTEL_ESM_NODE_IMPORTS = "import { readFileSync as __neatReadFileSync } from 'node:fs'\nimport { join as __neatJoin, dirname as __neatDirname } from 'node:path'";
8129
+ var OTEL_ENDPOINT_RESOLVER_ESM = `;(function () {
8130
+ try {
8131
+ if (process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || process.env.OTEL_EXPORTER_OTLP_ENDPOINT) return
8132
+ let __neatDir = process.cwd()
8133
+ for (let __i = 0; __i < 8; __i++) {
8134
+ try {
8135
+ const __rec = JSON.parse(__neatReadFileSync(__neatJoin(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
8136
+ if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
8137
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
8138
+ break
8139
+ }
8140
+ } catch (_e) {}
8141
+ const __parent = __neatDirname(__neatDir)
8142
+ if (__parent === __neatDir) break
8143
+ __neatDir = __parent
8144
+ }
8145
+ } catch (_e) {}
8146
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
8147
+ })()`;
7776
8148
  function neatCaptureSource(ts) {
7777
8149
  const spanT = ts ? ": any" : "";
7778
8150
  const strOpt = ts ? ": string | undefined" : "";
@@ -8061,7 +8433,7 @@ try {
8061
8433
  var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
8062
8434
  ${OTEL_INIT_STAMP}
8063
8435
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8064
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8436
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
8065
8437
  ${OTEL_OTLP_PROTOCOL_JS}
8066
8438
  ${OTEL_OTLP_HEADERS_JS}
8067
8439
 
@@ -8079,8 +8451,9 @@ ${neatWireCaptureSource(false)}
8079
8451
  `;
8080
8452
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
8081
8453
  ${OTEL_INIT_STAMP}
8454
+ ${OTEL_ESM_NODE_IMPORTS}
8082
8455
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8083
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8456
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8084
8457
  ${OTEL_OTLP_PROTOCOL_JS}
8085
8458
  ${OTEL_OTLP_HEADERS_JS}
8086
8459
 
@@ -8103,8 +8476,9 @@ ${OTEL_INIT_STAMP}
8103
8476
  // strict user tsconfig would reject; suppressing type-checking here keeps the
8104
8477
  // generated file from breaking the host project's \`tsc\` gate (#427) without
8105
8478
  // constraining the runtime logic. The file is regenerated, never hand-edited.
8479
+ ${OTEL_ESM_NODE_IMPORTS}
8106
8480
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8107
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8481
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8108
8482
  ${OTEL_OTLP_PROTOCOL_JS}
8109
8483
  ${OTEL_OTLP_HEADERS_JS}
8110
8484
 
@@ -8126,11 +8500,14 @@ ${registrations.join("\n")}
8126
8500
  `;
8127
8501
  return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
8128
8502
  }
8129
- function renderEnvNeat(serviceName, projectName) {
8503
+ function renderEnvNeat(serviceName, _projectName) {
8130
8504
  return [
8131
8505
  "# Generated by `neat init --apply` (ADR-069).",
8132
8506
  `OTEL_SERVICE_NAME=${serviceName}`,
8133
- `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/projects/${projectName}/v1/traces`,
8507
+ "# Advisory only \u2014 the generated otel-init resolves the live endpoint from",
8508
+ "# <project>/neat-out/daemon.json (ports.otlp) at boot (ADR-096). This is the",
8509
+ "# canonical default the daemon takes when its first-choice OTLP port is free.",
8510
+ "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces",
8134
8511
  "OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
8135
8512
  "# Set NEAT_OTEL_TOKEN to the daemon's OTLP secret to authenticate exported spans (#410).",
8136
8513
  "# NEAT_OTEL_TOKEN=",
@@ -8153,8 +8530,9 @@ export async function register() {
8153
8530
  }
8154
8531
  `;
8155
8532
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
8533
+ ${OTEL_ESM_NODE_IMPORTS}
8156
8534
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8157
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8535
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8158
8536
  ${OTEL_OTLP_PROTOCOL_JS}
8159
8537
  ${OTEL_OTLP_HEADERS_JS}
8160
8538
 
@@ -8167,7 +8545,7 @@ new NodeSDK({ instrumentations }).start()
8167
8545
  `;
8168
8546
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
8169
8547
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8170
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8548
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
8171
8549
  ${OTEL_OTLP_PROTOCOL_JS}
8172
8550
  ${OTEL_OTLP_HEADERS_JS}
8173
8551
 
@@ -8186,8 +8564,9 @@ ${registrations.join("\n")}
8186
8564
  }
8187
8565
  var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074). OpenTelemetry SDK hook.";
8188
8566
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
8567
+ ${OTEL_ESM_NODE_IMPORTS}
8189
8568
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8190
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8569
+ ${OTEL_ENDPOINT_RESOLVER_ESM}
8191
8570
  ${OTEL_OTLP_PROTOCOL_JS}
8192
8571
  ${OTEL_OTLP_HEADERS_JS}
8193
8572
 
@@ -8202,7 +8581,7 @@ sdk.start()
8202
8581
  `;
8203
8582
  var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
8204
8583
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
8205
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
8584
+ ${OTEL_ENDPOINT_RESOLVER_CJS}
8206
8585
  ${OTEL_OTLP_PROTOCOL_JS}
8207
8586
  ${OTEL_OTLP_HEADERS_JS}
8208
8587
 
@@ -8311,11 +8690,11 @@ var OTEL_ENV = {
8311
8690
  value: "http://localhost:4318/projects/<project>/v1/traces"
8312
8691
  };
8313
8692
  function serviceNodeName(pkg, serviceDir) {
8314
- return pkg.name ?? import_node_path45.default.basename(serviceDir);
8693
+ return pkg.name ?? import_node_path46.default.basename(serviceDir);
8315
8694
  }
8316
8695
  function projectToken(pkg, serviceDir, project) {
8317
8696
  if (project && project.length > 0) return project;
8318
- return pkg.name ?? import_node_path45.default.basename(serviceDir);
8697
+ return pkg.name ?? import_node_path46.default.basename(serviceDir);
8319
8698
  }
8320
8699
  async function readJsonFile(p) {
8321
8700
  try {
@@ -8328,16 +8707,16 @@ async function readJsonFile(p) {
8328
8707
  async function detectRuntimeKind(pkgRoot, pkg) {
8329
8708
  const deps = allDeps(pkg);
8330
8709
  if ("react-native" in deps || "expo" in deps) return "react-native";
8331
- const appJson = await readJsonFile(import_node_path45.default.join(pkgRoot, "app.json"));
8710
+ const appJson = await readJsonFile(import_node_path46.default.join(pkgRoot, "app.json"));
8332
8711
  if (appJson && typeof appJson === "object" && "expo" in appJson) {
8333
8712
  return "react-native";
8334
8713
  }
8335
- if (await exists3(import_node_path45.default.join(pkgRoot, "vite.config.js")) || await exists3(import_node_path45.default.join(pkgRoot, "vite.config.ts")) || await exists3(import_node_path45.default.join(pkgRoot, "vite.config.mjs")) || "vite" in deps) {
8714
+ if (await exists3(import_node_path46.default.join(pkgRoot, "vite.config.js")) || await exists3(import_node_path46.default.join(pkgRoot, "vite.config.ts")) || await exists3(import_node_path46.default.join(pkgRoot, "vite.config.mjs")) || "vite" in deps) {
8336
8715
  return "browser-bundle";
8337
8716
  }
8338
- if (await exists3(import_node_path45.default.join(pkgRoot, "wrangler.toml"))) return "cloudflare-workers";
8339
- if (await exists3(import_node_path45.default.join(pkgRoot, "bun.lockb"))) return "bun";
8340
- if (await exists3(import_node_path45.default.join(pkgRoot, "deno.json")) || await exists3(import_node_path45.default.join(pkgRoot, "deno.lock"))) {
8717
+ if (await exists3(import_node_path46.default.join(pkgRoot, "wrangler.toml"))) return "cloudflare-workers";
8718
+ if (await exists3(import_node_path46.default.join(pkgRoot, "bun.lockb"))) return "bun";
8719
+ if (await exists3(import_node_path46.default.join(pkgRoot, "deno.json")) || await exists3(import_node_path46.default.join(pkgRoot, "deno.lock"))) {
8341
8720
  return "deno";
8342
8721
  }
8343
8722
  const engines = pkg.engines ?? {};
@@ -8346,7 +8725,7 @@ async function detectRuntimeKind(pkgRoot, pkg) {
8346
8725
  }
8347
8726
  async function readPackageJson2(serviceDir) {
8348
8727
  try {
8349
- const raw = await import_node_fs29.promises.readFile(import_node_path45.default.join(serviceDir, "package.json"), "utf8");
8728
+ const raw = await import_node_fs29.promises.readFile(import_node_path46.default.join(serviceDir, "package.json"), "utf8");
8350
8729
  return JSON.parse(raw);
8351
8730
  } catch {
8352
8731
  return null;
@@ -8390,7 +8769,7 @@ function needsVersionUpgrade(installed, expected) {
8390
8769
  var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
8391
8770
  async function findNextConfig(serviceDir) {
8392
8771
  for (const name of NEXT_CONFIG_CANDIDATES) {
8393
- const candidate = import_node_path45.default.join(serviceDir, name);
8772
+ const candidate = import_node_path46.default.join(serviceDir, name);
8394
8773
  if (await exists3(candidate)) return candidate;
8395
8774
  }
8396
8775
  return null;
@@ -8417,7 +8796,7 @@ function hasRemixDependency(pkg) {
8417
8796
  }
8418
8797
  async function findRemixEntry(serviceDir) {
8419
8798
  for (const rel of REMIX_ENTRY_CANDIDATES) {
8420
- const candidate = import_node_path45.default.join(serviceDir, rel);
8799
+ const candidate = import_node_path46.default.join(serviceDir, rel);
8421
8800
  if (await exists3(candidate)) return candidate;
8422
8801
  }
8423
8802
  return null;
@@ -8429,14 +8808,14 @@ function hasSvelteKitDependency(pkg) {
8429
8808
  }
8430
8809
  async function findSvelteKitHooks(serviceDir) {
8431
8810
  for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
8432
- const candidate = import_node_path45.default.join(serviceDir, rel);
8811
+ const candidate = import_node_path46.default.join(serviceDir, rel);
8433
8812
  if (await exists3(candidate)) return candidate;
8434
8813
  }
8435
8814
  return null;
8436
8815
  }
8437
8816
  async function findSvelteKitConfig(serviceDir) {
8438
8817
  for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
8439
- const candidate = import_node_path45.default.join(serviceDir, rel);
8818
+ const candidate = import_node_path46.default.join(serviceDir, rel);
8440
8819
  if (await exists3(candidate)) return candidate;
8441
8820
  }
8442
8821
  return null;
@@ -8447,7 +8826,7 @@ function hasNuxtDependency(pkg) {
8447
8826
  }
8448
8827
  async function findNuxtConfig(serviceDir) {
8449
8828
  for (const name of NUXT_CONFIG_CANDIDATES) {
8450
- const candidate = import_node_path45.default.join(serviceDir, name);
8829
+ const candidate = import_node_path46.default.join(serviceDir, name);
8451
8830
  if (await exists3(candidate)) return candidate;
8452
8831
  }
8453
8832
  return null;
@@ -8458,7 +8837,7 @@ function hasAstroDependency(pkg) {
8458
8837
  }
8459
8838
  async function findAstroConfig(serviceDir) {
8460
8839
  for (const name of ASTRO_CONFIG_CANDIDATES) {
8461
- const candidate = import_node_path45.default.join(serviceDir, name);
8840
+ const candidate = import_node_path46.default.join(serviceDir, name);
8462
8841
  if (await exists3(candidate)) return candidate;
8463
8842
  }
8464
8843
  return null;
@@ -8472,7 +8851,7 @@ function parseNextMajor(range) {
8472
8851
  return Number.isFinite(n) ? n : null;
8473
8852
  }
8474
8853
  async function isTypeScriptProject(serviceDir) {
8475
- return exists3(import_node_path45.default.join(serviceDir, "tsconfig.json"));
8854
+ return exists3(import_node_path46.default.join(serviceDir, "tsconfig.json"));
8476
8855
  }
8477
8856
  var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
8478
8857
  var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
@@ -8518,7 +8897,7 @@ function entryFromScript(script) {
8518
8897
  }
8519
8898
  async function resolveEntry(serviceDir, pkg) {
8520
8899
  if (typeof pkg.main === "string" && pkg.main.length > 0) {
8521
- const candidate = import_node_path45.default.resolve(serviceDir, pkg.main);
8900
+ const candidate = import_node_path46.default.resolve(serviceDir, pkg.main);
8522
8901
  if (await exists3(candidate)) return candidate;
8523
8902
  }
8524
8903
  if (pkg.bin) {
@@ -8532,36 +8911,36 @@ async function resolveEntry(serviceDir, pkg) {
8532
8911
  if (typeof first === "string") binEntry = first;
8533
8912
  }
8534
8913
  if (binEntry) {
8535
- const candidate = import_node_path45.default.resolve(serviceDir, binEntry);
8914
+ const candidate = import_node_path46.default.resolve(serviceDir, binEntry);
8536
8915
  if (await exists3(candidate)) return candidate;
8537
8916
  }
8538
8917
  }
8539
8918
  const startEntry = entryFromScript(pkg.scripts?.start);
8540
8919
  if (startEntry) {
8541
- const candidate = import_node_path45.default.resolve(serviceDir, startEntry);
8920
+ const candidate = import_node_path46.default.resolve(serviceDir, startEntry);
8542
8921
  if (await exists3(candidate)) return candidate;
8543
8922
  }
8544
8923
  const devEntry = entryFromScript(pkg.scripts?.dev);
8545
8924
  if (devEntry) {
8546
- const candidate = import_node_path45.default.resolve(serviceDir, devEntry);
8925
+ const candidate = import_node_path46.default.resolve(serviceDir, devEntry);
8547
8926
  if (await exists3(candidate)) return candidate;
8548
8927
  }
8549
8928
  for (const rel of SRC_INDEX_CANDIDATES) {
8550
- const candidate = import_node_path45.default.join(serviceDir, rel);
8929
+ const candidate = import_node_path46.default.join(serviceDir, rel);
8551
8930
  if (await exists3(candidate)) return candidate;
8552
8931
  }
8553
8932
  for (const rel of SRC_NAMED_CANDIDATES) {
8554
- const candidate = import_node_path45.default.join(serviceDir, rel);
8933
+ const candidate = import_node_path46.default.join(serviceDir, rel);
8555
8934
  if (await exists3(candidate)) return candidate;
8556
8935
  }
8557
8936
  for (const name of INDEX_CANDIDATES) {
8558
- const candidate = import_node_path45.default.join(serviceDir, name);
8937
+ const candidate = import_node_path46.default.join(serviceDir, name);
8559
8938
  if (await exists3(candidate)) return candidate;
8560
8939
  }
8561
8940
  return null;
8562
8941
  }
8563
8942
  function dispatchEntry(entryFile, pkg) {
8564
- const ext = import_node_path45.default.extname(entryFile).toLowerCase();
8943
+ const ext = import_node_path46.default.extname(entryFile).toLowerCase();
8565
8944
  if (ext === ".ts" || ext === ".tsx") return "ts";
8566
8945
  if (ext === ".mjs") return "esm";
8567
8946
  if (ext === ".cjs") return "cjs";
@@ -8578,9 +8957,9 @@ function otelInitContents(flavor) {
8578
8957
  return OTEL_INIT_CJS;
8579
8958
  }
8580
8959
  function injectionLine(flavor, entryFile, otelInitFile) {
8581
- let rel = import_node_path45.default.relative(import_node_path45.default.dirname(entryFile), otelInitFile);
8960
+ let rel = import_node_path46.default.relative(import_node_path46.default.dirname(entryFile), otelInitFile);
8582
8961
  if (!rel.startsWith(".")) rel = `./${rel}`;
8583
- rel = rel.split(import_node_path45.default.sep).join("/");
8962
+ rel = rel.split(import_node_path46.default.sep).join("/");
8584
8963
  if (flavor === "cjs") return `require('${rel}')`;
8585
8964
  if (flavor === "esm") return `import '${rel}'`;
8586
8965
  const tsRel = rel.replace(/\.ts$/, "");
@@ -8593,23 +8972,23 @@ function lineIsOtelInjection(line) {
8593
8972
  }
8594
8973
  async function detectsSrcLayout(serviceDir) {
8595
8974
  const [hasSrcApp, hasSrcPages, hasRootApp, hasRootPages] = await Promise.all([
8596
- exists3(import_node_path45.default.join(serviceDir, "src", "app")),
8597
- exists3(import_node_path45.default.join(serviceDir, "src", "pages")),
8598
- exists3(import_node_path45.default.join(serviceDir, "app")),
8599
- exists3(import_node_path45.default.join(serviceDir, "pages"))
8975
+ exists3(import_node_path46.default.join(serviceDir, "src", "app")),
8976
+ exists3(import_node_path46.default.join(serviceDir, "src", "pages")),
8977
+ exists3(import_node_path46.default.join(serviceDir, "app")),
8978
+ exists3(import_node_path46.default.join(serviceDir, "pages"))
8600
8979
  ]);
8601
8980
  return (hasSrcApp || hasSrcPages) && !hasRootApp && !hasRootPages;
8602
8981
  }
8603
8982
  async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
8604
8983
  const useTs = await isTypeScriptProject(serviceDir);
8605
8984
  const srcLayout = await detectsSrcLayout(serviceDir);
8606
- const baseDir = srcLayout ? import_node_path45.default.join(serviceDir, "src") : serviceDir;
8607
- const instrumentationFile = import_node_path45.default.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
8608
- const instrumentationNodeFile = import_node_path45.default.join(
8985
+ const baseDir = srcLayout ? import_node_path46.default.join(serviceDir, "src") : serviceDir;
8986
+ const instrumentationFile = import_node_path46.default.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
8987
+ const instrumentationNodeFile = import_node_path46.default.join(
8609
8988
  baseDir,
8610
8989
  useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
8611
8990
  );
8612
- const envNeatFile = import_node_path45.default.join(baseDir, ".env.neat");
8991
+ const envNeatFile = import_node_path46.default.join(baseDir, ".env.neat");
8613
8992
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
8614
8993
  const dependencyEdits = [];
8615
8994
  for (const sdk of SDK_PACKAGES) {
@@ -8714,7 +9093,7 @@ function buildDependencyEdits(pkg, manifestPath) {
8714
9093
  return edits;
8715
9094
  }
8716
9095
  async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
8717
- const envNeatFile = import_node_path45.default.join(serviceDir, ".env.neat");
9096
+ const envNeatFile = import_node_path46.default.join(serviceDir, ".env.neat");
8718
9097
  if (!await exists3(envNeatFile)) {
8719
9098
  generatedFiles.push({
8720
9099
  file: envNeatFile,
@@ -8749,7 +9128,7 @@ function fileImportsOtelHook(raw, specifiers) {
8749
9128
  }
8750
9129
  async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
8751
9130
  const useTs = await isTypeScriptProject(serviceDir);
8752
- const otelServerFile = import_node_path45.default.join(
9131
+ const otelServerFile = import_node_path46.default.join(
8753
9132
  serviceDir,
8754
9133
  useTs ? "app/otel.server.ts" : "app/otel.server.js"
8755
9134
  );
@@ -8806,11 +9185,11 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
8806
9185
  }
8807
9186
  async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
8808
9187
  const useTs = await isTypeScriptProject(serviceDir);
8809
- const otelInitFile = import_node_path45.default.join(
9188
+ const otelInitFile = import_node_path46.default.join(
8810
9189
  serviceDir,
8811
9190
  useTs ? "src/otel-init.ts" : "src/otel-init.js"
8812
9191
  );
8813
- const resolvedHooksFile = hooksFile ?? import_node_path45.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
9192
+ const resolvedHooksFile = hooksFile ?? import_node_path46.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
8814
9193
  const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
8815
9194
  const generatedFiles = [];
8816
9195
  const entrypointEdits = [];
@@ -8872,11 +9251,11 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
8872
9251
  }
8873
9252
  async function planNuxt(serviceDir, pkg, manifestPath, project) {
8874
9253
  const useTs = await isTypeScriptProject(serviceDir);
8875
- const otelPluginFile = import_node_path45.default.join(
9254
+ const otelPluginFile = import_node_path46.default.join(
8876
9255
  serviceDir,
8877
9256
  useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
8878
9257
  );
8879
- const otelInitFile = import_node_path45.default.join(
9258
+ const otelInitFile = import_node_path46.default.join(
8880
9259
  serviceDir,
8881
9260
  useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
8882
9261
  );
@@ -8927,19 +9306,19 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
8927
9306
  var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
8928
9307
  async function findAstroMiddleware(serviceDir) {
8929
9308
  for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
8930
- const candidate = import_node_path45.default.join(serviceDir, rel);
9309
+ const candidate = import_node_path46.default.join(serviceDir, rel);
8931
9310
  if (await exists3(candidate)) return candidate;
8932
9311
  }
8933
9312
  return null;
8934
9313
  }
8935
9314
  async function planAstro(serviceDir, pkg, manifestPath, project) {
8936
9315
  const useTs = await isTypeScriptProject(serviceDir);
8937
- const otelInitFile = import_node_path45.default.join(
9316
+ const otelInitFile = import_node_path46.default.join(
8938
9317
  serviceDir,
8939
9318
  useTs ? "src/otel-init.ts" : "src/otel-init.js"
8940
9319
  );
8941
9320
  const existingMiddleware = await findAstroMiddleware(serviceDir);
8942
- const middlewareFile = existingMiddleware ?? import_node_path45.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
9321
+ const middlewareFile = existingMiddleware ?? import_node_path46.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
8943
9322
  const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
8944
9323
  const generatedFiles = [];
8945
9324
  const entrypointEdits = [];
@@ -9035,7 +9414,7 @@ async function findFrameworkDispatch(serviceDir, pkg, manifestPath, project) {
9035
9414
  }
9036
9415
  async function plan(serviceDir, opts) {
9037
9416
  const pkg = await readPackageJson2(serviceDir);
9038
- const manifestPath = import_node_path45.default.join(serviceDir, "package.json");
9417
+ const manifestPath = import_node_path46.default.join(serviceDir, "package.json");
9039
9418
  const project = opts?.project;
9040
9419
  const empty = {
9041
9420
  language: "javascript",
@@ -9070,8 +9449,8 @@ async function plan(serviceDir, opts) {
9070
9449
  return { ...empty, libOnly: true };
9071
9450
  }
9072
9451
  const flavor = dispatchEntry(entryFile, pkg);
9073
- const otelInitFile = import_node_path45.default.join(import_node_path45.default.dirname(entryFile), otelInitFilename(flavor));
9074
- const envNeatFile = import_node_path45.default.join(serviceDir, ".env.neat");
9452
+ const otelInitFile = import_node_path46.default.join(import_node_path46.default.dirname(entryFile), otelInitFilename(flavor));
9453
+ const envNeatFile = import_node_path46.default.join(serviceDir, ".env.neat");
9075
9454
  const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
9076
9455
  const dependencyEdits = [];
9077
9456
  for (const sdk of SDK_PACKAGES) {
@@ -9140,13 +9519,13 @@ async function plan(serviceDir, opts) {
9140
9519
  };
9141
9520
  }
9142
9521
  function isAllowedWritePath(serviceDir, target) {
9143
- const rel = import_node_path45.default.relative(serviceDir, target);
9522
+ const rel = import_node_path46.default.relative(serviceDir, target);
9144
9523
  if (rel.startsWith("..")) return false;
9145
- const base = import_node_path45.default.basename(target);
9524
+ const base = import_node_path46.default.basename(target);
9146
9525
  if (base === "package.json") return true;
9147
9526
  if (base === ".env.neat") return true;
9148
9527
  if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
9149
- const relPosix = rel.split(import_node_path45.default.sep).join("/");
9528
+ const relPosix = rel.split(import_node_path46.default.sep).join("/");
9150
9529
  if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) {
9151
9530
  if (relPosix === base) return true;
9152
9531
  if (relPosix === `src/${base}`) return true;
@@ -9163,7 +9542,7 @@ function isAllowedWritePath(serviceDir, target) {
9163
9542
  return false;
9164
9543
  }
9165
9544
  async function writeAtomic(file, contents) {
9166
- await import_node_fs29.promises.mkdir(import_node_path45.default.dirname(file), { recursive: true });
9545
+ await import_node_fs29.promises.mkdir(import_node_path46.default.dirname(file), { recursive: true });
9167
9546
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
9168
9547
  await import_node_fs29.promises.writeFile(tmp, contents, "utf8");
9169
9548
  await import_node_fs29.promises.rename(tmp, file);
@@ -9347,7 +9726,7 @@ async function rollback(installPlan, originals, createdFiles) {
9347
9726
  ...removed.map((f) => `removed: ${f}`),
9348
9727
  ""
9349
9728
  ];
9350
- const rollbackPath = import_node_path45.default.join(installPlan.serviceDir, "neat-rollback.patch");
9729
+ const rollbackPath = import_node_path46.default.join(installPlan.serviceDir, "neat-rollback.patch");
9351
9730
  await import_node_fs29.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
9352
9731
  }
9353
9732
  function injectInstrumentationHook(raw) {
@@ -9378,7 +9757,7 @@ var javascriptInstaller = {
9378
9757
  // src/installers/python.ts
9379
9758
  init_cjs_shims();
9380
9759
  var import_node_fs30 = require("fs");
9381
- var import_node_path46 = __toESM(require("path"), 1);
9760
+ var import_node_path47 = __toESM(require("path"), 1);
9382
9761
  var SDK_PACKAGES2 = [
9383
9762
  { name: "opentelemetry-distro", version: ">=0.49b0" },
9384
9763
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
@@ -9399,7 +9778,7 @@ async function exists4(p) {
9399
9778
  async function detect2(serviceDir) {
9400
9779
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
9401
9780
  for (const m of markers) {
9402
- if (await exists4(import_node_path46.default.join(serviceDir, m))) return true;
9781
+ if (await exists4(import_node_path47.default.join(serviceDir, m))) return true;
9403
9782
  }
9404
9783
  return false;
9405
9784
  }
@@ -9409,7 +9788,7 @@ function reqPackageName(line) {
9409
9788
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
9410
9789
  }
9411
9790
  async function planRequirementsTxtEdits(serviceDir) {
9412
- const file = import_node_path46.default.join(serviceDir, "requirements.txt");
9791
+ const file = import_node_path47.default.join(serviceDir, "requirements.txt");
9413
9792
  if (!await exists4(file)) return null;
9414
9793
  const raw = await import_node_fs30.promises.readFile(file, "utf8");
9415
9794
  const presentNames = new Set(
@@ -9419,7 +9798,7 @@ async function planRequirementsTxtEdits(serviceDir) {
9419
9798
  return { manifest: file, missing: [...missing] };
9420
9799
  }
9421
9800
  async function planProcfileEdits(serviceDir) {
9422
- const procfile = import_node_path46.default.join(serviceDir, "Procfile");
9801
+ const procfile = import_node_path47.default.join(serviceDir, "Procfile");
9423
9802
  if (!await exists4(procfile)) return [];
9424
9803
  const raw = await import_node_fs30.promises.readFile(procfile, "utf8");
9425
9804
  const edits = [];
@@ -9508,7 +9887,7 @@ async function apply2(installPlan) {
9508
9887
  if (raw === void 0) {
9509
9888
  throw new Error(`python installer: cannot read ${file} during apply`);
9510
9889
  }
9511
- const base = import_node_path46.default.basename(file);
9890
+ const base = import_node_path47.default.basename(file);
9512
9891
  if (base === "requirements.txt") {
9513
9892
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
9514
9893
  if (edits.length > 0) {
@@ -9547,7 +9926,7 @@ async function rollback2(installPlan, originals) {
9547
9926
  ...restored.map((f) => `restored: ${f}`),
9548
9927
  ""
9549
9928
  ];
9550
- const rollbackPath = import_node_path46.default.join(installPlan.serviceDir, "neat-rollback.patch");
9929
+ const rollbackPath = import_node_path47.default.join(installPlan.serviceDir, "neat-rollback.patch");
9551
9930
  await import_node_fs30.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
9552
9931
  }
9553
9932
  var pythonInstaller = {
@@ -9671,19 +10050,51 @@ function renderPatch(sections) {
9671
10050
 
9672
10051
  // src/orchestrator.ts
9673
10052
  init_cjs_shims();
9674
- var import_node_fs31 = require("fs");
10053
+ var import_node_fs33 = require("fs");
9675
10054
  var import_node_http = __toESM(require("http"), 1);
9676
10055
  var import_node_net = __toESM(require("net"), 1);
9677
- var import_node_path47 = __toESM(require("path"), 1);
10056
+ var import_node_path50 = __toESM(require("path"), 1);
9678
10057
  var import_node_child_process3 = require("child_process");
9679
10058
  var import_node_readline = __toESM(require("readline"), 1);
10059
+
10060
+ // src/daemon.ts
10061
+ init_cjs_shims();
10062
+ var import_node_fs32 = require("fs");
10063
+ var import_node_path49 = __toESM(require("path"), 1);
10064
+ var import_node_module = require("module");
10065
+ init_otel();
10066
+ init_auth();
10067
+
10068
+ // src/unrouted.ts
10069
+ init_cjs_shims();
10070
+ var import_node_fs31 = require("fs");
10071
+ var import_node_path48 = __toESM(require("path"), 1);
10072
+
10073
+ // src/daemon.ts
10074
+ function daemonJsonPath(scanPath) {
10075
+ return import_node_path49.default.join(scanPath, "neat-out", "daemon.json");
10076
+ }
10077
+ async function readDaemonRecord(scanPath) {
10078
+ try {
10079
+ const raw = await import_node_fs32.promises.readFile(daemonJsonPath(scanPath), "utf8");
10080
+ const parsed = JSON.parse(raw);
10081
+ if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
10082
+ return parsed;
10083
+ }
10084
+ return null;
10085
+ } catch {
10086
+ return null;
10087
+ }
10088
+ }
10089
+
10090
+ // src/orchestrator.ts
9680
10091
  async function extractAndPersist(opts) {
9681
10092
  const services = await discoverServices(opts.scanPath);
9682
10093
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
9683
10094
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
9684
10095
  resetGraph(graphKey);
9685
10096
  const graph = getGraph(graphKey);
9686
- const projectPaths = pathsForProject(graphKey, import_node_path47.default.join(opts.scanPath, "neat-out"));
10097
+ const projectPaths = pathsForProject(graphKey, import_node_path50.default.join(opts.scanPath, "neat-out"));
9687
10098
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
9688
10099
  errorsPath: projectPaths.errorsPath
9689
10100
  });
@@ -9737,7 +10148,7 @@ async function applyInstallersOver(services, project, options = {}) {
9737
10148
  console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
9738
10149
  } else if (outcome.outcome === "react-native") {
9739
10150
  reactNative++;
9740
- const svcName = import_node_path47.default.basename(svc.dir);
10151
+ const svcName = import_node_path50.default.basename(svc.dir);
9741
10152
  console.log(
9742
10153
  `neat: ${svc.dir} detected as React Native / Expo
9743
10154
  The installer doesn't cover this runtime deterministically.
@@ -9748,7 +10159,7 @@ async function applyInstallersOver(services, project, options = {}) {
9748
10159
  );
9749
10160
  } else if (outcome.outcome === "bun") {
9750
10161
  bun++;
9751
- const svcName = import_node_path47.default.basename(svc.dir);
10162
+ const svcName = import_node_path50.default.basename(svc.dir);
9752
10163
  console.log(
9753
10164
  `neat: ${svc.dir} detected as Bun
9754
10165
  The installer doesn't cover this runtime deterministically.
@@ -9759,7 +10170,7 @@ async function applyInstallersOver(services, project, options = {}) {
9759
10170
  );
9760
10171
  } else if (outcome.outcome === "deno") {
9761
10172
  deno++;
9762
- const svcName = import_node_path47.default.basename(svc.dir);
10173
+ const svcName = import_node_path50.default.basename(svc.dir);
9763
10174
  console.log(
9764
10175
  `neat: ${svc.dir} detected as Deno
9765
10176
  The installer doesn't cover this runtime deterministically.
@@ -9770,7 +10181,7 @@ async function applyInstallersOver(services, project, options = {}) {
9770
10181
  );
9771
10182
  } else if (outcome.outcome === "cloudflare-workers") {
9772
10183
  cloudflareWorkers++;
9773
- const svcName = import_node_path47.default.basename(svc.dir);
10184
+ const svcName = import_node_path50.default.basename(svc.dir);
9774
10185
  console.log(
9775
10186
  `neat: ${svc.dir} detected as Cloudflare Workers
9776
10187
  The installer doesn't cover this runtime deterministically.
@@ -9781,7 +10192,7 @@ async function applyInstallersOver(services, project, options = {}) {
9781
10192
  );
9782
10193
  } else if (outcome.outcome === "electron") {
9783
10194
  electron++;
9784
- const svcName = import_node_path47.default.basename(svc.dir);
10195
+ const svcName = import_node_path50.default.basename(svc.dir);
9785
10196
  console.log(
9786
10197
  `neat: ${svc.dir} detected as Electron
9787
10198
  The installer doesn't cover this runtime deterministically.
@@ -9862,10 +10273,6 @@ async function fetchDaemonHealth(restPort) {
9862
10273
  });
9863
10274
  });
9864
10275
  }
9865
- async function checkDaemonHealth(restPort) {
9866
- const body = await fetchDaemonHealth(restPort);
9867
- return body !== null;
9868
- }
9869
10276
  async function probeProjectHealth(restPort, name) {
9870
10277
  return new Promise((resolve) => {
9871
10278
  const req = import_node_http.default.get(
@@ -9941,12 +10348,6 @@ async function isPortFree(port) {
9941
10348
  server.listen(port, "127.0.0.1");
9942
10349
  });
9943
10350
  }
9944
- async function probePortsFree() {
9945
- for (const port of NEAT_PORTS) {
9946
- if (!await isPortFree(port)) return { free: false, held: port };
9947
- }
9948
- return { free: true };
9949
- }
9950
10351
  function formatPortCollisionMessage(port) {
9951
10352
  return [
9952
10353
  `neat: port ${port} is in use; the NEAT daemon needs it.`,
@@ -9954,11 +10355,82 @@ function formatPortCollisionMessage(port) {
9954
10355
  ` \`lsof -i :${port}\` to find the holding process.`
9955
10356
  ];
9956
10357
  }
9957
- function spawnDaemonDetached() {
9958
- const here = import_node_path47.default.dirname(new URL(importMetaUrl).pathname);
10358
+ var PORT_ALLOCATION_ATTEMPTS = 8;
10359
+ var PORT_STRIDE = 1;
10360
+ async function tripleFree(ports) {
10361
+ for (const p of [ports.rest, ports.otlp, ports.web]) {
10362
+ if (!await isPortFree(p)) return false;
10363
+ }
10364
+ return true;
10365
+ }
10366
+ async function allocatePorts() {
10367
+ const [baseRest, baseOtlp, baseWeb] = NEAT_PORTS;
10368
+ for (let i = 0; i < PORT_ALLOCATION_ATTEMPTS; i++) {
10369
+ const candidate = {
10370
+ rest: baseRest + i * PORT_STRIDE,
10371
+ otlp: baseOtlp + i * PORT_STRIDE,
10372
+ web: baseWeb + i * PORT_STRIDE
10373
+ };
10374
+ if (await tripleFree(candidate)) return candidate;
10375
+ }
10376
+ return null;
10377
+ }
10378
+ async function persistedPortsFor(scanPath) {
10379
+ const record = await readDaemonRecord(scanPath);
10380
+ if (!record) return null;
10381
+ return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
10382
+ }
10383
+ async function acquireSpawnLock(scanPath) {
10384
+ const lockPath = import_node_path50.default.join(scanPath, "neat-out", "daemon.spawn.lock");
10385
+ await import_node_fs33.promises.mkdir(import_node_path50.default.dirname(lockPath), { recursive: true });
10386
+ const STALE_LOCK_MS = 6e4;
10387
+ try {
10388
+ const fd = await import_node_fs33.promises.open(lockPath, "wx");
10389
+ await fd.writeFile(`${process.pid}
10390
+ `, "utf8");
10391
+ await fd.close();
10392
+ return async () => {
10393
+ await import_node_fs33.promises.unlink(lockPath).catch(() => {
10394
+ });
10395
+ };
10396
+ } catch (err) {
10397
+ if (err.code !== "EEXIST") return null;
10398
+ try {
10399
+ const stat = await import_node_fs33.promises.stat(lockPath);
10400
+ if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
10401
+ await import_node_fs33.promises.unlink(lockPath).catch(() => {
10402
+ });
10403
+ return acquireSpawnLock(scanPath);
10404
+ }
10405
+ } catch {
10406
+ return acquireSpawnLock(scanPath);
10407
+ }
10408
+ return null;
10409
+ }
10410
+ }
10411
+ async function waitForPeerDaemon(restPort, project, timeoutMs) {
10412
+ const deadline = Date.now() + timeoutMs;
10413
+ while (Date.now() < deadline) {
10414
+ if (await healthIsForProject(restPort, project)) return true;
10415
+ await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
10416
+ }
10417
+ return healthIsForProject(restPort, project);
10418
+ }
10419
+ async function healthIsForProject(restPort, project) {
10420
+ const body = await fetchDaemonHealth(restPort);
10421
+ if (body === null) return false;
10422
+ const named = body.project;
10423
+ if (typeof named === "string") return named === project;
10424
+ if (Array.isArray(body.projects)) {
10425
+ return body.projects.some((p) => p.name === project);
10426
+ }
10427
+ return false;
10428
+ }
10429
+ function spawnDaemonDetached(spec) {
10430
+ const here = import_node_path50.default.dirname(new URL(importMetaUrl).pathname);
9959
10431
  const candidates = [
9960
- import_node_path47.default.join(here, "neatd.cjs"),
9961
- import_node_path47.default.join(here, "neatd.js")
10432
+ import_node_path50.default.join(here, "neatd.cjs"),
10433
+ import_node_path50.default.join(here, "neatd.js")
9962
10434
  ];
9963
10435
  let entry2 = null;
9964
10436
  const fsSync = require("fs");
@@ -9978,6 +10450,13 @@ function spawnDaemonDetached() {
9978
10450
  if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
9979
10451
  env.HOST = "127.0.0.1";
9980
10452
  }
10453
+ if (spec) {
10454
+ env.NEAT_PROJECT = spec.project;
10455
+ env.NEAT_PROJECT_PATH = spec.projectPath;
10456
+ env.PORT = String(spec.ports.rest);
10457
+ env.OTEL_PORT = String(spec.ports.otlp);
10458
+ env.NEAT_WEB_PORT = String(spec.ports.web);
10459
+ }
9981
10460
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entry2, "start"], {
9982
10461
  detached: true,
9983
10462
  // stderr inherits the orchestrator's fd so the daemon's
@@ -10016,7 +10495,7 @@ async function runOrchestrator(opts) {
10016
10495
  browser: "skipped"
10017
10496
  }
10018
10497
  };
10019
- const stat = await import_node_fs31.promises.stat(opts.scanPath).catch(() => null);
10498
+ const stat = await import_node_fs33.promises.stat(opts.scanPath).catch(() => null);
10020
10499
  if (!stat || !stat.isDirectory()) {
10021
10500
  console.error(`neat: ${opts.scanPath} is not a directory`);
10022
10501
  result.exitCode = 2;
@@ -10097,48 +10576,75 @@ async function runOrchestrator(opts) {
10097
10576
  result.exitCode = 1;
10098
10577
  }
10099
10578
  }
10100
- const restPort = Number(process.env.PORT ?? 8080);
10101
10579
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
10102
- if (await checkDaemonHealth(restPort)) {
10580
+ const persistedPorts = await persistedPortsFor(opts.scanPath);
10581
+ let allocated = null;
10582
+ if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
10103
10583
  result.steps.daemon = "already-running";
10584
+ allocated = persistedPorts;
10104
10585
  } else {
10105
- const probe = await probePortsFree();
10106
- if (!probe.free) {
10107
- for (const line of formatPortCollisionMessage(probe.held)) {
10586
+ if (persistedPorts && await isPortFree(persistedPorts.rest) && await tripleFree(persistedPorts)) {
10587
+ allocated = persistedPorts;
10588
+ } else {
10589
+ allocated = await allocatePorts();
10590
+ }
10591
+ if (!allocated) {
10592
+ for (const line of formatPortCollisionMessage(NEAT_PORTS[0])) {
10108
10593
  console.error(line);
10109
10594
  }
10110
10595
  result.exitCode = 3;
10111
10596
  return result;
10112
10597
  }
10113
- try {
10114
- spawnDaemonDetached();
10115
- } catch (err) {
10116
- console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
10117
- result.exitCode = 1;
10118
- return result;
10119
- }
10120
- const ready = await waitForDaemonReady(restPort, timeoutMs);
10121
- result.steps.daemon = ready.ready ? "spawned" : "timed-out";
10122
- if (!ready.ready) {
10123
- console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
10124
- if (ready.stillBootstrapping.length > 0) {
10125
- console.error(
10126
- `neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`
10127
- );
10598
+ const release = await acquireSpawnLock(opts.scanPath);
10599
+ if (!release) {
10600
+ const reused = await waitForPeerDaemon(allocated.rest, currentProjectName, timeoutMs);
10601
+ if (reused) {
10602
+ result.steps.daemon = "already-running";
10603
+ } else {
10604
+ console.error("neat: another `neat` is spawning this project but its daemon did not come up in time");
10605
+ result.exitCode = 1;
10606
+ return result;
10128
10607
  }
10129
- if (ready.brokenProjects.length > 0) {
10130
- console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
10608
+ } else {
10609
+ try {
10610
+ if (await healthIsForProject(allocated.rest, currentProjectName)) {
10611
+ result.steps.daemon = "already-running";
10612
+ } else {
10613
+ spawnDaemonDetached({
10614
+ project: currentProjectName,
10615
+ projectPath: opts.scanPath,
10616
+ ports: allocated
10617
+ });
10618
+ const ready = await waitForDaemonReady(allocated.rest, timeoutMs);
10619
+ result.steps.daemon = ready.ready ? "spawned" : "timed-out";
10620
+ if (!ready.ready) {
10621
+ console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
10622
+ if (ready.stillBootstrapping.length > 0) {
10623
+ console.error(`neat: still bootstrapping: ${ready.stillBootstrapping.join(", ")}`);
10624
+ }
10625
+ if (ready.brokenProjects.length > 0) {
10626
+ console.error(`neat: broken projects: ${ready.brokenProjects.join(", ")}`);
10627
+ }
10628
+ result.exitCode = 1;
10629
+ return result;
10630
+ }
10631
+ if (ready.brokenProjects.length > 0) {
10632
+ console.warn(
10633
+ `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
10634
+ );
10635
+ }
10636
+ }
10637
+ } catch (err) {
10638
+ console.error(`neat: daemon spawn failed \u2014 ${err.message}`);
10639
+ result.exitCode = 1;
10640
+ return result;
10641
+ } finally {
10642
+ await release();
10131
10643
  }
10132
- result.exitCode = 1;
10133
- return result;
10134
- }
10135
- if (ready.brokenProjects.length > 0) {
10136
- console.warn(
10137
- `neat: ${ready.brokenProjects.length} project(s) reported broken: ${ready.brokenProjects.join(", ")}`
10138
- );
10139
10644
  }
10140
10645
  }
10141
- const dashboardUrl = opts.dashboardUrl ?? "http://localhost:6328";
10646
+ const webPort = allocated?.web ?? NEAT_PORTS[2];
10647
+ const dashboardUrl = opts.dashboardUrl ?? `http://localhost:${webPort}`;
10142
10648
  if (opts.noOpen || !process.stdout.isTTY) {
10143
10649
  result.steps.browser = "skipped";
10144
10650
  } else {
@@ -10173,11 +10679,11 @@ function printSummary(result, graph, dashboardUrl) {
10173
10679
 
10174
10680
  // src/cli-verbs.ts
10175
10681
  init_cjs_shims();
10176
- var import_node_path48 = __toESM(require("path"), 1);
10682
+ var import_node_path51 = __toESM(require("path"), 1);
10177
10683
 
10178
10684
  // src/cli-client.ts
10179
10685
  init_cjs_shims();
10180
- var import_types26 = require("@neat.is/types");
10686
+ var import_types27 = require("@neat.is/types");
10181
10687
  var HttpError = class extends Error {
10182
10688
  constructor(status2, message, responseBody = "") {
10183
10689
  super(message);
@@ -10202,10 +10708,10 @@ function createHttpClient(baseUrl, bearerToken) {
10202
10708
  const root = baseUrl.replace(/\/$/, "");
10203
10709
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
10204
10710
  return {
10205
- async get(path50) {
10711
+ async get(path53) {
10206
10712
  let res;
10207
10713
  try {
10208
- res = await fetch(`${root}${path50}`, {
10714
+ res = await fetch(`${root}${path53}`, {
10209
10715
  headers: { ...authHeader }
10210
10716
  });
10211
10717
  } catch (err) {
@@ -10217,16 +10723,16 @@ function createHttpClient(baseUrl, bearerToken) {
10217
10723
  const body = await res.text().catch(() => "");
10218
10724
  throw new HttpError(
10219
10725
  res.status,
10220
- `${res.status} ${res.statusText} on GET ${path50}: ${body}`,
10726
+ `${res.status} ${res.statusText} on GET ${path53}: ${body}`,
10221
10727
  body
10222
10728
  );
10223
10729
  }
10224
10730
  return await res.json();
10225
10731
  },
10226
- async post(path50, body) {
10732
+ async post(path53, body) {
10227
10733
  let res;
10228
10734
  try {
10229
- res = await fetch(`${root}${path50}`, {
10735
+ res = await fetch(`${root}${path53}`, {
10230
10736
  method: "POST",
10231
10737
  headers: { "content-type": "application/json", ...authHeader },
10232
10738
  body: JSON.stringify(body)
@@ -10240,7 +10746,7 @@ function createHttpClient(baseUrl, bearerToken) {
10240
10746
  const text = await res.text().catch(() => "");
10241
10747
  throw new HttpError(
10242
10748
  res.status,
10243
- `${res.status} ${res.statusText} on POST ${path50}: ${text}`,
10749
+ `${res.status} ${res.statusText} on POST ${path53}: ${text}`,
10244
10750
  text
10245
10751
  );
10246
10752
  }
@@ -10254,12 +10760,12 @@ function projectPath(project, suffix) {
10254
10760
  }
10255
10761
  async function runRootCause(client, input) {
10256
10762
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
10257
- const path50 = projectPath(
10763
+ const path53 = projectPath(
10258
10764
  input.project,
10259
10765
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
10260
10766
  );
10261
10767
  try {
10262
- const result = await client.get(path50);
10768
+ const result = await client.get(path53);
10263
10769
  const arrowPath = result.traversalPath.join(" \u2190 ");
10264
10770
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
10265
10771
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -10285,12 +10791,12 @@ async function runRootCause(client, input) {
10285
10791
  }
10286
10792
  async function runBlastRadius(client, input) {
10287
10793
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
10288
- const path50 = projectPath(
10794
+ const path53 = projectPath(
10289
10795
  input.project,
10290
10796
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
10291
10797
  );
10292
10798
  try {
10293
- const result = await client.get(path50);
10799
+ const result = await client.get(path53);
10294
10800
  if (result.totalAffected === 0) {
10295
10801
  return {
10296
10802
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -10319,17 +10825,17 @@ async function runBlastRadius(client, input) {
10319
10825
  }
10320
10826
  }
10321
10827
  function formatBlastEntry(n) {
10322
- const tag = n.edgeProvenance === import_types26.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
10828
+ const tag = n.edgeProvenance === import_types27.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
10323
10829
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
10324
10830
  }
10325
10831
  async function runDependencies(client, input) {
10326
10832
  const depth = input.depth ?? 3;
10327
- const path50 = projectPath(
10833
+ const path53 = projectPath(
10328
10834
  input.project,
10329
10835
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
10330
10836
  );
10331
10837
  try {
10332
- const result = await client.get(path50);
10838
+ const result = await client.get(path53);
10333
10839
  if (result.total === 0) {
10334
10840
  return {
10335
10841
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -10365,9 +10871,9 @@ async function runObservedDependencies(client, input) {
10365
10871
  const edges = await client.get(
10366
10872
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
10367
10873
  );
10368
- const observed = edges.outbound.filter((e) => e.provenance === import_types26.Provenance.OBSERVED);
10874
+ const observed = edges.outbound.filter((e) => e.provenance === import_types27.Provenance.OBSERVED);
10369
10875
  if (observed.length === 0) {
10370
- const hasExtracted = edges.outbound.some((e) => e.provenance === import_types26.Provenance.EXTRACTED);
10876
+ const hasExtracted = edges.outbound.some((e) => e.provenance === import_types27.Provenance.EXTRACTED);
10371
10877
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
10372
10878
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
10373
10879
  }
@@ -10375,7 +10881,7 @@ async function runObservedDependencies(client, input) {
10375
10881
  return {
10376
10882
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
10377
10883
  block: blockLines.join("\n"),
10378
- provenance: import_types26.Provenance.OBSERVED
10884
+ provenance: import_types27.Provenance.OBSERVED
10379
10885
  };
10380
10886
  } catch (err) {
10381
10887
  if (err instanceof HttpError && err.status === 404) {
@@ -10410,9 +10916,9 @@ function formatDuration(ms) {
10410
10916
  return `${Math.round(h / 24)}d`;
10411
10917
  }
10412
10918
  async function runIncidents(client, input) {
10413
- const path50 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
10919
+ const path53 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
10414
10920
  try {
10415
- const body = await client.get(path50);
10921
+ const body = await client.get(path53);
10416
10922
  const events = body.events;
10417
10923
  if (events.length === 0) {
10418
10924
  return {
@@ -10429,7 +10935,7 @@ async function runIncidents(client, input) {
10429
10935
  return {
10430
10936
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
10431
10937
  block: blockLines.join("\n"),
10432
- provenance: import_types26.Provenance.OBSERVED
10938
+ provenance: import_types27.Provenance.OBSERVED
10433
10939
  };
10434
10940
  } catch (err) {
10435
10941
  if (err instanceof HttpError && err.status === 404) {
@@ -10538,7 +11044,7 @@ async function runStaleEdges(client, input) {
10538
11044
  return {
10539
11045
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
10540
11046
  block: blockLines.join("\n"),
10541
- provenance: import_types26.Provenance.STALE
11047
+ provenance: import_types27.Provenance.STALE
10542
11048
  };
10543
11049
  }
10544
11050
  async function runPolicies(client, input) {
@@ -10702,13 +11208,13 @@ async function resolveProjectEntry(opts) {
10702
11208
  const cwd = opts.cwd ?? process.cwd();
10703
11209
  const resolvedCwd = await normalizeProjectPath(cwd);
10704
11210
  for (const entry2 of entries) {
10705
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path48.default.sep}`)) {
11211
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path51.default.sep}`)) {
10706
11212
  return entry2;
10707
11213
  }
10708
11214
  }
10709
11215
  return null;
10710
11216
  }
10711
- async function checkDaemonHealth2(baseUrl) {
11217
+ async function checkDaemonHealth(baseUrl) {
10712
11218
  try {
10713
11219
  const res = await fetch(`${baseUrl.replace(/\/$/, "")}/health`, {
10714
11220
  signal: AbortSignal.timeout(1500)
@@ -10809,7 +11315,7 @@ async function runSync(opts) {
10809
11315
  }
10810
11316
  } else {
10811
11317
  const daemonUrl = opts.daemonUrl ?? process.env.NEAT_API_URL ?? "http://localhost:8080";
10812
- const healthy = await checkDaemonHealth2(daemonUrl);
11318
+ const healthy = await checkDaemonHealth(daemonUrl);
10813
11319
  if (healthy) {
10814
11320
  try {
10815
11321
  await pushSnapshotToRemote({
@@ -10852,7 +11358,7 @@ async function runSync(opts) {
10852
11358
  }
10853
11359
 
10854
11360
  // src/cli.ts
10855
- var import_types27 = require("@neat.is/types");
11361
+ var import_types28 = require("@neat.is/types");
10856
11362
  function isNpxInvocation() {
10857
11363
  if (process.env.npm_command === "exec") return true;
10858
11364
  const execpath = process.env.npm_execpath ?? "";
@@ -10883,12 +11389,18 @@ function usage() {
10883
11389
  console.log(" watch <path> Start neat-core, watch <path>, re-extract on changes.");
10884
11390
  console.log(" PORT (default 8080), OTEL_PORT (4318), HOST (0.0.0.0)");
10885
11391
  console.log(" control listeners. NEAT_OTLP_GRPC=true also opens 4317.");
10886
- console.log(" list List every project registered in the machine-level registry.");
10887
- console.log(" pause <name> Mark a project paused \u2014 daemon stops watching until resumed.");
10888
- console.log(" resume <name> Mark a project active again.");
11392
+ console.log(" list Report the daemons running on this machine (alias: ps).");
11393
+ console.log(" Reads ~/.neat/daemons/ and folds in any registered");
11394
+ console.log(" project no daemon has self-described yet.");
11395
+ console.log(" ps Alias of list.");
11396
+ console.log(" pause <name> Stop a project's daemon until it is started again.");
11397
+ console.log(" resume <name> Bring a paused project back; for a stopped daemon, re-run");
11398
+ console.log(" `neat <path>` to start it.");
10889
11399
  console.log(" uninstall <name>");
10890
- console.log(" Remove a project from the registry. Does not touch");
11400
+ console.log(" Stop a project's daemon and retire it. Does not touch");
10891
11401
  console.log(" neat-out/, policy.json, or any user file.");
11402
+ console.log(" prune Drop registry entries whose path is gone from disk.");
11403
+ console.log(" Flags: --json emit the removed list as JSON");
10892
11404
  console.log(" version Print the installed @neat.is/core version and exit.");
10893
11405
  console.log(" Aliases: --version, -v.");
10894
11406
  console.log(" skill Install or print the Claude Code MCP drop-in.");
@@ -10948,6 +11460,7 @@ function usage() {
10948
11460
  console.log("");
10949
11461
  console.log("environment:");
10950
11462
  console.log(" NEAT_API_URL base URL for the core REST API (default http://localhost:8080)");
11463
+ console.log(" alias: NEAT_CORE_URL (the name the MCP server reads)");
10951
11464
  console.log(" NEAT_PROJECT project name when --project isn't passed");
10952
11465
  }
10953
11466
  var STRING_FLAGS = [
@@ -11081,6 +11594,14 @@ function printVersion() {
11081
11594
  process.stdout.write(`${readPackageVersion()}
11082
11595
  `);
11083
11596
  }
11597
+ function formatMachineProjectRow(r) {
11598
+ if (r.ports) {
11599
+ const where = r.pid !== void 0 ? ` pid=${r.pid}` : "";
11600
+ return `${r.project} ${r.state} rest=${r.ports.rest} otlp=${r.ports.otlp} web=${r.ports.web} ${r.projectPath}${where}`;
11601
+ }
11602
+ const status2 = r.registryStatus ? ` (${r.registryStatus})` : "";
11603
+ return `${r.project} ${r.state}${status2} ${r.projectPath}`;
11604
+ }
11084
11605
  function printDiscoveryReport(opts, services) {
11085
11606
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
11086
11607
  const mode = opts.dryRun ? "dry-run" : opts.apply ? "apply" : "patch-only";
@@ -11119,7 +11640,7 @@ async function buildPatchSections(services, project) {
11119
11640
  }
11120
11641
  async function runInit(opts) {
11121
11642
  const written = [];
11122
- const stat = await import_node_fs32.promises.stat(opts.scanPath).catch(() => null);
11643
+ const stat = await import_node_fs34.promises.stat(opts.scanPath).catch(() => null);
11123
11644
  if (!stat || !stat.isDirectory()) {
11124
11645
  console.error(`neat init: ${opts.scanPath} is not a directory`);
11125
11646
  return { exitCode: 2, writtenFiles: written };
@@ -11128,13 +11649,13 @@ async function runInit(opts) {
11128
11649
  printDiscoveryReport(opts, services);
11129
11650
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
11130
11651
  const patch = renderPatch(sections);
11131
- const patchPath = import_node_path49.default.join(opts.scanPath, "neat.patch");
11652
+ const patchPath = import_node_path52.default.join(opts.scanPath, "neat.patch");
11132
11653
  if (opts.dryRun) {
11133
- await import_node_fs32.promises.writeFile(patchPath, patch, "utf8");
11654
+ await import_node_fs34.promises.writeFile(patchPath, patch, "utf8");
11134
11655
  written.push(patchPath);
11135
11656
  console.log(`dry-run: patch written to ${patchPath}`);
11136
- const gitignorePath = import_node_path49.default.join(opts.scanPath, ".gitignore");
11137
- const gitignoreExists = await import_node_fs32.promises.stat(gitignorePath).then(() => true).catch(() => false);
11657
+ const gitignorePath = import_node_path52.default.join(opts.scanPath, ".gitignore");
11658
+ const gitignoreExists = await import_node_fs34.promises.stat(gitignorePath).then(() => true).catch(() => false);
11138
11659
  const verb = gitignoreExists ? "append" : "create";
11139
11660
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
11140
11661
  console.log("rerun without --dry-run to register and snapshot.");
@@ -11145,9 +11666,9 @@ async function runInit(opts) {
11145
11666
  const graph = getGraph(graphKey);
11146
11667
  const projectPaths = pathsForProject(
11147
11668
  graphKey,
11148
- import_node_path49.default.join(opts.scanPath, "neat-out")
11669
+ import_node_path52.default.join(opts.scanPath, "neat-out")
11149
11670
  );
11150
- const errorsPath = import_node_path49.default.join(import_node_path49.default.dirname(opts.outPath), import_node_path49.default.basename(projectPaths.errorsPath));
11671
+ const errorsPath = import_node_path52.default.join(import_node_path52.default.dirname(opts.outPath), import_node_path52.default.basename(projectPaths.errorsPath));
11151
11672
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
11152
11673
  await saveGraphToDisk(graph, opts.outPath);
11153
11674
  written.push(opts.outPath);
@@ -11226,7 +11747,7 @@ async function runInit(opts) {
11226
11747
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
11227
11748
  }
11228
11749
  } else {
11229
- await import_node_fs32.promises.writeFile(patchPath, patch, "utf8");
11750
+ await import_node_fs34.promises.writeFile(patchPath, patch, "utf8");
11230
11751
  written.push(patchPath);
11231
11752
  }
11232
11753
  }
@@ -11259,16 +11780,16 @@ var CLAUDE_SKILL_CONFIG = {
11259
11780
  command: "npx",
11260
11781
  args: ["-y", "@neat.is/mcp"],
11261
11782
  env: {
11262
- NEAT_API_URL: "http://localhost:8080"
11783
+ NEAT_CORE_URL: "http://localhost:8080"
11263
11784
  }
11264
11785
  }
11265
11786
  }
11266
11787
  };
11267
11788
  function claudeConfigPath() {
11268
11789
  const override = process.env.NEAT_CLAUDE_CONFIG;
11269
- if (override && override.length > 0) return import_node_path49.default.resolve(override);
11790
+ if (override && override.length > 0) return import_node_path52.default.resolve(override);
11270
11791
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
11271
- return import_node_path49.default.join(home, ".claude.json");
11792
+ return import_node_path52.default.join(home, ".claude.json");
11272
11793
  }
11273
11794
  async function runSkill(opts) {
11274
11795
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -11280,7 +11801,7 @@ async function runSkill(opts) {
11280
11801
  const target = claudeConfigPath();
11281
11802
  let existing = {};
11282
11803
  try {
11283
- existing = JSON.parse(await import_node_fs32.promises.readFile(target, "utf8"));
11804
+ existing = JSON.parse(await import_node_fs34.promises.readFile(target, "utf8"));
11284
11805
  } catch (err) {
11285
11806
  if (err.code !== "ENOENT") {
11286
11807
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -11292,8 +11813,8 @@ async function runSkill(opts) {
11292
11813
  ...existing,
11293
11814
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
11294
11815
  };
11295
- await import_node_fs32.promises.mkdir(import_node_path49.default.dirname(target), { recursive: true });
11296
- await import_node_fs32.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
11816
+ await import_node_fs34.promises.mkdir(import_node_path52.default.dirname(target), { recursive: true });
11817
+ await import_node_fs34.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
11297
11818
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
11298
11819
  console.log("restart Claude Code to pick up the new MCP server.");
11299
11820
  return { exitCode: 0 };
@@ -11305,6 +11826,9 @@ async function runSkill(opts) {
11305
11826
  console.log("");
11306
11827
  console.log("Manual install: copy mcpServers.neat from --print-config into ~/.claude.json,");
11307
11828
  console.log("then restart Claude Code. See packages/claude-skill/SKILL.md for the tool list.");
11829
+ console.log("");
11830
+ console.log("The MCP server reads NEAT_CORE_URL for the daemon URL \u2014 point it at a");
11831
+ console.log("non-default daemon by editing that value in the generated config.");
11308
11832
  return { exitCode: 0 };
11309
11833
  }
11310
11834
  async function main() {
@@ -11339,12 +11863,12 @@ async function main() {
11339
11863
  console.error("neat init: --apply and --dry-run are mutually exclusive");
11340
11864
  process.exit(2);
11341
11865
  }
11342
- const scanPath = import_node_path49.default.resolve(target);
11866
+ const scanPath = import_node_path52.default.resolve(target);
11343
11867
  const projectExplicit = parsed.project !== null;
11344
- const projectName = projectExplicit ? project : import_node_path49.default.basename(scanPath);
11868
+ const projectName = projectExplicit ? project : import_node_path52.default.basename(scanPath);
11345
11869
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
11346
- const fallback = pathsForProject(projectKey, import_node_path49.default.join(scanPath, "neat-out")).snapshotPath;
11347
- const outPath = import_node_path49.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
11870
+ const fallback = pathsForProject(projectKey, import_node_path52.default.join(scanPath, "neat-out")).snapshotPath;
11871
+ const outPath = import_node_path52.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
11348
11872
  const result = await runInit({
11349
11873
  scanPath,
11350
11874
  outPath,
@@ -11365,21 +11889,21 @@ async function main() {
11365
11889
  usage();
11366
11890
  process.exit(2);
11367
11891
  }
11368
- const scanPath = import_node_path49.default.resolve(target);
11369
- const stat = await import_node_fs32.promises.stat(scanPath).catch(() => null);
11892
+ const scanPath = import_node_path52.default.resolve(target);
11893
+ const stat = await import_node_fs34.promises.stat(scanPath).catch(() => null);
11370
11894
  if (!stat || !stat.isDirectory()) {
11371
11895
  console.error(`neat watch: ${scanPath} is not a directory`);
11372
11896
  process.exit(2);
11373
11897
  }
11374
- const projectPaths = pathsForProject(project, import_node_path49.default.join(scanPath, "neat-out"));
11375
- const outPath = import_node_path49.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
11376
- const errorsPath = import_node_path49.default.resolve(
11377
- process.env.NEAT_ERRORS_PATH ?? import_node_path49.default.join(import_node_path49.default.dirname(outPath), import_node_path49.default.basename(projectPaths.errorsPath))
11898
+ const projectPaths = pathsForProject(project, import_node_path52.default.join(scanPath, "neat-out"));
11899
+ const outPath = import_node_path52.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
11900
+ const errorsPath = import_node_path52.default.resolve(
11901
+ process.env.NEAT_ERRORS_PATH ?? import_node_path52.default.join(import_node_path52.default.dirname(outPath), import_node_path52.default.basename(projectPaths.errorsPath))
11378
11902
  );
11379
- const staleEventsPath = import_node_path49.default.resolve(
11380
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path49.default.join(import_node_path49.default.dirname(outPath), import_node_path49.default.basename(projectPaths.staleEventsPath))
11903
+ const staleEventsPath = import_node_path52.default.resolve(
11904
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path52.default.join(import_node_path52.default.dirname(outPath), import_node_path52.default.basename(projectPaths.staleEventsPath))
11381
11905
  );
11382
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path49.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
11906
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path52.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
11383
11907
  const handle = await startWatch(getGraph(project), {
11384
11908
  scanPath,
11385
11909
  outPath,
@@ -11406,16 +11930,14 @@ async function main() {
11406
11930
  process.on("SIGINT", shutdown);
11407
11931
  return;
11408
11932
  }
11409
- if (cmd === "list") {
11410
- const projects = await listProjects();
11411
- if (projects.length === 0) {
11412
- console.log("no projects registered. run `neat init <path>` to register one.");
11933
+ if (cmd === "list" || cmd === "ps") {
11934
+ const rows = await listMachineProjects();
11935
+ if (rows.length === 0) {
11936
+ console.log("no daemons running and no projects registered. run `neat init <path>` to register one.");
11413
11937
  return;
11414
11938
  }
11415
- for (const p of projects) {
11416
- const seen = p.lastSeenAt ? p.lastSeenAt : "never";
11417
- const langs = p.languages.length > 0 ? p.languages.join(",") : "-";
11418
- console.log(`${p.name} ${p.status} ${langs} ${p.path} last-seen=${seen}`);
11939
+ for (const r of rows) {
11940
+ console.log(formatMachineProjectRow(r));
11419
11941
  }
11420
11942
  return;
11421
11943
  }
@@ -11426,6 +11948,15 @@ async function main() {
11426
11948
  usage();
11427
11949
  process.exit(2);
11428
11950
  }
11951
+ const daemon = await findDaemonByProject(name);
11952
+ if (daemon) {
11953
+ if (daemon.live && signalDaemonStop(daemon.record.pid)) {
11954
+ console.log(`paused: ${name} (${daemon.record.projectPath}) \u2014 stopped daemon pid ${daemon.record.pid}`);
11955
+ } else {
11956
+ console.log(`paused: ${name} (${daemon.record.projectPath}) \u2014 daemon was not running`);
11957
+ }
11958
+ return;
11959
+ }
11429
11960
  try {
11430
11961
  const entry2 = await setStatus(name, "paused");
11431
11962
  console.log(`paused: ${entry2.name} (${entry2.path})`);
@@ -11442,6 +11973,15 @@ async function main() {
11442
11973
  usage();
11443
11974
  process.exit(2);
11444
11975
  }
11976
+ const daemon = await findDaemonByProject(name);
11977
+ if (daemon) {
11978
+ if (daemon.live) {
11979
+ console.log(`resume: ${name} (${daemon.record.projectPath}) \u2014 daemon already running`);
11980
+ } else {
11981
+ console.log(`resume: ${name} (${daemon.record.projectPath}) \u2014 run \`neat ${daemon.record.projectPath}\` to start its daemon again`);
11982
+ }
11983
+ return;
11984
+ }
11445
11985
  try {
11446
11986
  const entry2 = await setStatus(name, "active");
11447
11987
  console.log(`resumed: ${entry2.name} (${entry2.path})`);
@@ -11463,15 +12003,36 @@ async function main() {
11463
12003
  usage();
11464
12004
  process.exit(2);
11465
12005
  }
12006
+ const daemon = await findDaemonByProject(name);
11466
12007
  const removed = await removeProject(name);
11467
- if (!removed) {
12008
+ if (!daemon && !removed) {
11468
12009
  console.error(`neat uninstall: no project named "${name}"`);
11469
12010
  process.exit(1);
11470
12011
  }
11471
- console.log(`unregistered: ${removed.name} (${removed.path})`);
12012
+ const projectPath2 = daemon?.record.projectPath ?? removed?.path ?? "(unknown path)";
12013
+ if (daemon) {
12014
+ if (daemon.live && signalDaemonStop(daemon.record.pid)) {
12015
+ console.log(`uninstall: ${name} \u2014 stopped daemon pid ${daemon.record.pid}`);
12016
+ }
12017
+ await removeDaemonRecord(daemon.source);
12018
+ }
12019
+ console.log(`unregistered: ${name} (${projectPath2})`);
11472
12020
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
11473
12021
  return;
11474
12022
  }
12023
+ if (cmd === "prune") {
12024
+ const removed = await pruneRegistry({ ttlMs: 0 });
12025
+ if (parsed.json) {
12026
+ console.log(JSON.stringify(removed.map((p) => ({ name: p.name, path: p.path })), null, 2));
12027
+ return;
12028
+ }
12029
+ if (removed.length === 0) {
12030
+ console.log("nothing to prune \u2014 every registered project path still exists.");
12031
+ return;
12032
+ }
12033
+ console.log(`pruned ${removed.length} project${removed.length === 1 ? "" : "s"}: ${removed.map((p) => p.name).join(", ")}`);
12034
+ return;
12035
+ }
11475
12036
  if (cmd === "deploy") {
11476
12037
  const artifact = await runDeploy();
11477
12038
  const block = renderOtelEnvBlock2(artifact.token);
@@ -11525,11 +12086,11 @@ async function main() {
11525
12086
  process.exit(1);
11526
12087
  }
11527
12088
  async function tryOrchestrator(cmd, parsed) {
11528
- const scanPath = import_node_path49.default.resolve(cmd);
11529
- const stat = await import_node_fs32.promises.stat(scanPath).catch(() => null);
12089
+ const scanPath = import_node_path52.default.resolve(cmd);
12090
+ const stat = await import_node_fs34.promises.stat(scanPath).catch(() => null);
11530
12091
  if (!stat || !stat.isDirectory()) return null;
11531
12092
  const projectExplicit = parsed.project !== null;
11532
- const projectName = projectExplicit ? parsed.project : import_node_path49.default.basename(scanPath);
12093
+ const projectName = projectExplicit ? parsed.project : import_node_path52.default.basename(scanPath);
11533
12094
  const result = await runOrchestrator({
11534
12095
  scanPath,
11535
12096
  project: projectName,
@@ -11559,12 +12120,44 @@ function resolveProjectFlag(parsed) {
11559
12120
  if (env && env.length > 0 && env !== DEFAULT_PROJECT) return env;
11560
12121
  return void 0;
11561
12122
  }
12123
+ var ProjectResolutionError = class extends Error {
12124
+ constructor(message, exitCode = 2) {
12125
+ super(message);
12126
+ this.exitCode = exitCode;
12127
+ this.name = "ProjectResolutionError";
12128
+ }
12129
+ exitCode;
12130
+ };
12131
+ async function resolveProjectForVerb(client, parsed) {
12132
+ const explicit = resolveProjectFlag(parsed);
12133
+ if (explicit) return explicit;
12134
+ const projects = await client.get("/projects");
12135
+ if (projects.some((p) => p.name === DEFAULT_PROJECT)) {
12136
+ return void 0;
12137
+ }
12138
+ if (projects.length === 1) {
12139
+ return projects[0].name;
12140
+ }
12141
+ if (projects.length === 0) {
12142
+ throw new ProjectResolutionError(
12143
+ "No projects are registered with the daemon yet. Run `npx neat.is` in a repo to build a graph first, then re-run this command."
12144
+ );
12145
+ }
12146
+ const names = projects.map((p) => p.name).sort().map((n) => ` ${n}`).join("\n");
12147
+ throw new ProjectResolutionError(
12148
+ `Several projects are registered and none is named "default", so I can't pick one for you.
12149
+ Pass --project <name> to choose:
12150
+ ${names}`
12151
+ );
12152
+ }
12153
+ function resolveDaemonUrl() {
12154
+ return process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL ?? "http://localhost:8080";
12155
+ }
11562
12156
  async function runQueryVerb(cmd, parsed) {
11563
- const baseUrl = process.env.NEAT_API_URL ?? "http://localhost:8080";
12157
+ const baseUrl = resolveDaemonUrl();
11564
12158
  const client = createHttpClient(baseUrl, resolveAuthToken());
11565
- const project = resolveProjectFlag(parsed);
11566
12159
  const positional = parsed.positional;
11567
- let work;
12160
+ let makeWork;
11568
12161
  switch (cmd) {
11569
12162
  case "root-cause": {
11570
12163
  const node = positional[0];
@@ -11572,7 +12165,7 @@ async function runQueryVerb(cmd, parsed) {
11572
12165
  console.error("neat root-cause: missing <node-id>");
11573
12166
  return 2;
11574
12167
  }
11575
- work = runRootCause(client, {
12168
+ makeWork = (project) => runRootCause(client, {
11576
12169
  errorNode: node,
11577
12170
  ...parsed.errorId ? { errorId: parsed.errorId } : {},
11578
12171
  ...project ? { project } : {}
@@ -11585,7 +12178,7 @@ async function runQueryVerb(cmd, parsed) {
11585
12178
  console.error("neat blast-radius: missing <node-id>");
11586
12179
  return 2;
11587
12180
  }
11588
- work = runBlastRadius(client, {
12181
+ makeWork = (project) => runBlastRadius(client, {
11589
12182
  nodeId: node,
11590
12183
  ...parsed.depth !== null ? { depth: parsed.depth } : {},
11591
12184
  ...project ? { project } : {}
@@ -11598,7 +12191,7 @@ async function runQueryVerb(cmd, parsed) {
11598
12191
  console.error("neat dependencies: missing <node-id>");
11599
12192
  return 2;
11600
12193
  }
11601
- work = runDependencies(client, {
12194
+ makeWork = (project) => runDependencies(client, {
11602
12195
  nodeId: node,
11603
12196
  ...parsed.depth !== null ? { depth: parsed.depth } : {},
11604
12197
  ...project ? { project } : {}
@@ -11611,14 +12204,14 @@ async function runQueryVerb(cmd, parsed) {
11611
12204
  console.error("neat observed-dependencies: missing <node-id>");
11612
12205
  return 2;
11613
12206
  }
11614
- work = runObservedDependencies(client, {
12207
+ makeWork = (project) => runObservedDependencies(client, {
11615
12208
  nodeId: node,
11616
12209
  ...project ? { project } : {}
11617
12210
  });
11618
12211
  break;
11619
12212
  }
11620
12213
  case "incidents": {
11621
- work = runIncidents(client, {
12214
+ makeWork = (project) => runIncidents(client, {
11622
12215
  ...positional[0] ? { nodeId: positional[0] } : {},
11623
12216
  ...parsed.limit !== null ? { limit: parsed.limit } : {},
11624
12217
  ...project ? { project } : {}
@@ -11631,7 +12224,7 @@ async function runQueryVerb(cmd, parsed) {
11631
12224
  console.error("neat search: missing <query>");
11632
12225
  return 2;
11633
12226
  }
11634
- work = runSearch(client, { query: q, ...project ? { project } : {} });
12227
+ makeWork = (project) => runSearch(client, { query: q, ...project ? { project } : {} });
11635
12228
  break;
11636
12229
  }
11637
12230
  case "diff": {
@@ -11640,14 +12233,14 @@ async function runQueryVerb(cmd, parsed) {
11640
12233
  console.error("neat diff: --against <snapshot-path> is required");
11641
12234
  return 2;
11642
12235
  }
11643
- work = runDiff(client, {
12236
+ makeWork = (project) => runDiff(client, {
11644
12237
  againstSnapshot: against,
11645
12238
  ...project ? { project } : {}
11646
12239
  });
11647
12240
  break;
11648
12241
  }
11649
12242
  case "stale-edges": {
11650
- work = runStaleEdges(client, {
12243
+ makeWork = (project) => runStaleEdges(client, {
11651
12244
  ...parsed.limit !== null ? { limit: parsed.limit } : {},
11652
12245
  ...parsed.edgeType ? { edgeType: parsed.edgeType } : {},
11653
12246
  ...project ? { project } : {}
@@ -11666,7 +12259,7 @@ async function runQueryVerb(cmd, parsed) {
11666
12259
  return 2;
11667
12260
  }
11668
12261
  }
11669
- work = runPolicies(client, {
12262
+ makeWork = (project) => runPolicies(client, {
11670
12263
  ...parsed.node ? { nodeId: parsed.node } : {},
11671
12264
  ...hypothetical ? { hypotheticalAction: hypothetical } : {},
11672
12265
  ...project ? { project } : {}
@@ -11679,10 +12272,10 @@ async function runQueryVerb(cmd, parsed) {
11679
12272
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
11680
12273
  const out = [];
11681
12274
  for (const p of parts) {
11682
- const r = import_types27.DivergenceTypeSchema.safeParse(p);
12275
+ const r = import_types28.DivergenceTypeSchema.safeParse(p);
11683
12276
  if (!r.success) {
11684
12277
  console.error(
11685
- `neat divergences: unknown --type "${p}". allowed: ${import_types27.DivergenceTypeSchema.options.join(", ")}`
12278
+ `neat divergences: unknown --type "${p}". allowed: ${import_types28.DivergenceTypeSchema.options.join(", ")}`
11686
12279
  );
11687
12280
  return 2;
11688
12281
  }
@@ -11690,7 +12283,7 @@ async function runQueryVerb(cmd, parsed) {
11690
12283
  }
11691
12284
  typeFilter = out;
11692
12285
  }
11693
- work = runDivergences(client, {
12286
+ makeWork = (project) => runDivergences(client, {
11694
12287
  ...typeFilter ? { type: typeFilter } : {},
11695
12288
  ...parsed.minConfidence !== null ? { minConfidence: parsed.minConfidence } : {},
11696
12289
  ...parsed.node ? { node: parsed.node } : {},
@@ -11703,16 +12296,21 @@ async function runQueryVerb(cmd, parsed) {
11703
12296
  return 2;
11704
12297
  }
11705
12298
  try {
11706
- const result = await work;
12299
+ const project = await resolveProjectForVerb(client, parsed);
12300
+ const result = await makeWork(project);
11707
12301
  if (parsed.json) process.stdout.write(formatJson(result) + "\n");
11708
12302
  else process.stdout.write(formatHuman(result) + "\n");
11709
12303
  return 0;
11710
12304
  } catch (err) {
12305
+ if (err instanceof ProjectResolutionError) {
12306
+ console.error(`neat ${cmd}: ${err.message}`);
12307
+ return err.exitCode;
12308
+ }
11711
12309
  if (err instanceof HttpError) {
11712
12310
  const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
11713
12311
  console.error(`neat ${cmd}: ${detail.trim()}`);
11714
12312
  } else if (err instanceof TransportError) {
11715
- console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${process.env.NEAT_API_URL ?? "http://localhost:8080"})`);
12313
+ console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${resolveDaemonUrl()})`);
11716
12314
  } else {
11717
12315
  console.error(`neat ${cmd}: ${err.message}`);
11718
12316
  }
@@ -11729,6 +12327,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
11729
12327
  // Annotate the CommonJS export names for ESM import in node:
11730
12328
  0 && (module.exports = {
11731
12329
  CLAUDE_SKILL_CONFIG,
12330
+ ProjectResolutionError,
11732
12331
  QUERY_VERBS,
11733
12332
  commandPrefix,
11734
12333
  isNpxInvocation,
@@ -11736,6 +12335,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
11736
12335
  parseArgs,
11737
12336
  printBanner,
11738
12337
  readPackageVersion,
12338
+ resolveProjectForVerb,
11739
12339
  runInit,
11740
12340
  runQueryVerb,
11741
12341
  runSkill,