@neat.is/core 0.7.8 → 0.7.10

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
@@ -61,8 +61,8 @@ function mountBearerAuth(app, opts) {
61
61
  ]);
62
62
  const publicRead = opts.publicRead === true;
63
63
  app.addHook("preHandler", (req, reply, done) => {
64
- const path82 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
- if (exactUnauthPaths.has(path82) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path82)) {
64
+ const path84 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
+ if (exactUnauthPaths.has(path84) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path84)) {
66
66
  done();
67
67
  return;
68
68
  }
@@ -415,8 +415,8 @@ function websocketChannelPathOf(attrs) {
415
415
  const v = attrs[key];
416
416
  if (typeof v === "string" && v.length > 0) {
417
417
  const q = v.indexOf("?");
418
- const path82 = q === -1 ? v : v.slice(0, q);
419
- if (path82.length > 0) return path82;
418
+ const path84 = q === -1 ? v : v.slice(0, q);
419
+ if (path84.length > 0) return path84;
420
420
  }
421
421
  }
422
422
  return void 0;
@@ -504,13 +504,18 @@ function loadProtobufResponseEncoder() {
504
504
  );
505
505
  return exportTraceServiceResponseType;
506
506
  }
507
- function encodeProtobufResponseBody() {
508
- if (cachedProtobufResponseBody) return cachedProtobufResponseBody;
507
+ function encodeProtobufResponseBody(rejected, message) {
509
508
  const Type = loadProtobufResponseEncoder();
510
- const msg = Type.create({});
511
- const encoded = Type.encode(msg).finish();
512
- cachedProtobufResponseBody = Buffer.from(encoded);
513
- return cachedProtobufResponseBody;
509
+ if (!rejected) {
510
+ if (cachedProtobufResponseBody) return cachedProtobufResponseBody;
511
+ const msg2 = Type.create({});
512
+ cachedProtobufResponseBody = Buffer.from(Type.encode(msg2).finish());
513
+ return cachedProtobufResponseBody;
514
+ }
515
+ const msg = Type.fromObject({
516
+ partial_success: { rejected_spans: rejected, error_message: message ?? "" }
517
+ });
518
+ return Buffer.from(Type.encode(msg).finish());
514
519
  }
515
520
  async function decodeProtobufBody(buf) {
516
521
  const Type = loadProtobufDecoder();
@@ -625,6 +630,13 @@ async function buildOtelReceiver(opts) {
625
630
  }
626
631
  return reply.code(200).header("content-type", "application/json").send({ partialSuccess: {} });
627
632
  }
633
+ function sendOtlpPartial(reply, flavor, rejected, message) {
634
+ if (flavor === "protobuf") {
635
+ const buf = encodeProtobufResponseBody(rejected, message);
636
+ return reply.code(200).header("content-type", "application/x-protobuf").send(buf);
637
+ }
638
+ return reply.code(200).header("content-type", "application/json").send({ partialSuccess: { rejectedSpans: rejected, errorMessage: message } });
639
+ }
628
640
  app.addContentTypeParser(
629
641
  "application/x-protobuf",
630
642
  { parseAs: "buffer", bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },
@@ -652,6 +664,10 @@ async function buildOtelReceiver(opts) {
652
664
  }
653
665
  }
654
666
  enqueue(spans);
667
+ if (opts.classifyBareRoutability) {
668
+ const { rejected, message } = opts.classifyBareRoutability(spans);
669
+ if (rejected > 0) return sendOtlpPartial(reply, result.flavor, rejected, message);
670
+ }
655
671
  return sendOtlpSuccess(reply, result.flavor);
656
672
  });
657
673
  app.post("/projects/:project/v1/traces", async (req, reply) => {
@@ -755,9 +771,9 @@ __export(cli_exports, {
755
771
  });
756
772
  module.exports = __toCommonJS(cli_exports);
757
773
  init_cjs_shims();
758
- var import_node_path81 = __toESM(require("path"), 1);
774
+ var import_node_path83 = __toESM(require("path"), 1);
759
775
  var import_node_os8 = __toESM(require("os"), 1);
760
- var import_node_fs46 = require("fs");
776
+ var import_node_fs48 = require("fs");
761
777
 
762
778
  // src/banner.ts
763
779
  init_cjs_shims();
@@ -1325,19 +1341,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1325
1341
  function longestIncomingWalk(graph, start, maxDepth) {
1326
1342
  let best = { path: [start], edges: [] };
1327
1343
  const visited = /* @__PURE__ */ new Set([start]);
1328
- function step(node, path82, edges) {
1329
- if (path82.length > best.path.length) {
1330
- best = { path: [...path82], edges: [...edges] };
1344
+ function step(node, path84, edges) {
1345
+ if (path84.length > best.path.length) {
1346
+ best = { path: [...path84], edges: [...edges] };
1331
1347
  }
1332
- if (path82.length - 1 >= maxDepth) return;
1348
+ if (path84.length - 1 >= maxDepth) return;
1333
1349
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1334
1350
  for (const [srcId, edge] of incoming) {
1335
1351
  if (visited.has(srcId)) continue;
1336
1352
  visited.add(srcId);
1337
- path82.push(srcId);
1353
+ path84.push(srcId);
1338
1354
  edges.push(edge);
1339
- step(srcId, path82, edges);
1340
- path82.pop();
1355
+ step(srcId, path84, edges);
1356
+ path84.pop();
1341
1357
  edges.pop();
1342
1358
  visited.delete(srcId);
1343
1359
  }
@@ -1544,26 +1560,26 @@ function dominantFailingCall(graph, serviceId9, visited) {
1544
1560
  return best;
1545
1561
  }
1546
1562
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1547
- const path82 = [originServiceId];
1563
+ const path84 = [originServiceId];
1548
1564
  const edges = [];
1549
1565
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1550
1566
  let current = originServiceId;
1551
1567
  for (let depth = 0; depth < maxDepth; depth++) {
1552
1568
  const hop = dominantFailingCall(graph, current, visited);
1553
1569
  if (!hop) break;
1554
- path82.push(hop.nextService);
1570
+ path84.push(hop.nextService);
1555
1571
  edges.push(hop.edge);
1556
1572
  visited.add(hop.nextService);
1557
1573
  current = hop.nextService;
1558
1574
  }
1559
1575
  if (edges.length === 0) return null;
1560
- return { path: path82, edges, culprit: current };
1576
+ return { path: path84, edges, culprit: current };
1561
1577
  }
1562
1578
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1563
1579
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1564
1580
  if (!chain) return null;
1565
1581
  const culprit = chain.culprit;
1566
- const path82 = [...chain.path];
1582
+ const path84 = [...chain.path];
1567
1583
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1568
1584
  const baseConfidence = confidenceFromMix(chain.edges);
1569
1585
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1571,14 +1587,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1571
1587
  if (loc) {
1572
1588
  let rootCauseNode = culprit;
1573
1589
  if (loc.fileNode) {
1574
- path82.push(loc.fileNode);
1590
+ path84.push(loc.fileNode);
1575
1591
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1576
1592
  rootCauseNode = loc.fileNode;
1577
1593
  }
1578
1594
  return import_types.RootCauseResultSchema.parse({
1579
1595
  rootCauseNode,
1580
1596
  rootCauseReason: loc.rootCauseReason,
1581
- traversalPath: path82,
1597
+ traversalPath: path84,
1582
1598
  edgeProvenances,
1583
1599
  confidence,
1584
1600
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1590,7 +1606,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1590
1606
  return import_types.RootCauseResultSchema.parse({
1591
1607
  rootCauseNode: culprit,
1592
1608
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1593
- traversalPath: path82,
1609
+ traversalPath: path84,
1594
1610
  edgeProvenances,
1595
1611
  confidence,
1596
1612
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -2258,6 +2274,7 @@ var import_yaml = require("yaml");
2258
2274
  var import_types3 = require("@neat.is/types");
2259
2275
  var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
2260
2276
  var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
2277
+ var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
2261
2278
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
2262
2279
  "node_modules",
2263
2280
  ".git",
@@ -2297,6 +2314,7 @@ async function isPythonVenvDir(dir) {
2297
2314
  function isConfigFile(name) {
2298
2315
  const ext = import_node_path4.default.extname(name);
2299
2316
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2317
+ if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
2300
2318
  if (name === ".env" || name.startsWith(".env.")) {
2301
2319
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2302
2320
  if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
@@ -3125,8 +3143,8 @@ function chiRoutesFromSource(source, parser) {
3125
3143
  chiWalk(tree.rootNode, "", out);
3126
3144
  return out;
3127
3145
  }
3128
- function stripChiRegex(path82) {
3129
- return path82.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
3146
+ function stripChiRegex(path84) {
3147
+ return path84.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
3130
3148
  }
3131
3149
  function chiWalk(node, prefix, out) {
3132
3150
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -3798,9 +3816,9 @@ function rubyRocketRoute(args) {
3798
3816
  if (!pair || pair.type !== "pair") continue;
3799
3817
  const k = pair.childForFieldName("key");
3800
3818
  if (k?.type !== "string") continue;
3801
- const path82 = rubyLiteral(k);
3802
- if (path82 === null) continue;
3803
- return { path: path82, target: rubyLiteral(pair.childForFieldName("value")) };
3819
+ const path84 = rubyLiteral(k);
3820
+ if (path84 === null) continue;
3821
+ return { path: path84, target: rubyLiteral(pair.childForFieldName("value")) };
3804
3822
  }
3805
3823
  return null;
3806
3824
  }
@@ -4420,7 +4438,7 @@ async function expressMountPrefixes(files, serviceDir, tsPaths) {
4420
4438
  };
4421
4439
  const filePrefix = /* @__PURE__ */ new Map();
4422
4440
  const conflicted = /* @__PURE__ */ new Set();
4423
- const apply4 = (file, prefix) => {
4441
+ const apply6 = (file, prefix) => {
4424
4442
  if (conflicted.has(file)) return;
4425
4443
  const existing = filePrefix.get(file);
4426
4444
  if (existing === void 0) filePrefix.set(file, prefix);
@@ -4437,7 +4455,7 @@ async function expressMountPrefixes(files, serviceDir, tsPaths) {
4437
4455
  const info = fileInfo.get(file);
4438
4456
  const rv = info?.routerVars.get(name);
4439
4457
  if (!info || !rv) return;
4440
- if (rv.declares && info.appVars.size === 0) apply4(file, accPrefix);
4458
+ if (rv.declares && info.appVars.size === 0) apply6(file, accPrefix);
4441
4459
  for (const m of rv.mounts) {
4442
4460
  if (!m.target) continue;
4443
4461
  const t = resolveTarget(m.target, file);
@@ -4885,10 +4903,15 @@ function resolveDistToSrc(absFilepath, line) {
4885
4903
  }
4886
4904
  if (!entry2) return null;
4887
4905
  try {
4888
- const pos = entry2.consumer.originalPositionFor({
4889
- line: line !== void 0 && Number.isFinite(line) ? line : 1,
4890
- column: 0
4891
- });
4906
+ const queryLine = line !== void 0 && Number.isFinite(line) ? line : 1;
4907
+ let pos = entry2.consumer.originalPositionFor({ line: queryLine, column: 0 });
4908
+ if (!pos || !pos.source) {
4909
+ pos = entry2.consumer.originalPositionFor({
4910
+ line: queryLine,
4911
+ column: 0,
4912
+ bias: sourceMapJs.SourceMapConsumer.LEAST_UPPER_BOUND
4913
+ });
4914
+ }
4892
4915
  if (!pos || !pos.source) return null;
4893
4916
  const root = entry2.consumer.sourceRoot ?? "";
4894
4917
  const resolved = import_node_path9.default.resolve(entry2.dir, root, pos.source);
@@ -4897,6 +4920,9 @@ function resolveDistToSrc(absFilepath, line) {
4897
4920
  return null;
4898
4921
  }
4899
4922
  }
4923
+ function hasAdjacentSourceMap(absFilepath) {
4924
+ return sourceMapCache.get(absFilepath) != null;
4925
+ }
4900
4926
  function callSiteFromSpan(span, serviceNode, scanPath) {
4901
4927
  const filepath = codeFilepathOf(span.attributes);
4902
4928
  if (filepath === void 0) return null;
@@ -4912,7 +4938,7 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
4912
4938
  }
4913
4939
  const relPath = relPathForRuntimeFile(effectivePath, serviceNode, scanPath);
4914
4940
  if (!relPath) return null;
4915
- if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && serviceNode?.name) {
4941
+ if (!resolved && abs.endsWith(".js") && relPath.startsWith("dist/") && !hasAdjacentSourceMap(abs) && serviceNode?.name) {
4916
4942
  warnNoSourceMaps(serviceNode.name);
4917
4943
  }
4918
4944
  const fn = codeFunctionOf(span.attributes);
@@ -5188,7 +5214,7 @@ function resolveServiceId(graph, host, env) {
5188
5214
  function frontierIdFor(host) {
5189
5215
  return (0, import_types8.frontierId)(host);
5190
5216
  }
5191
- function ensureServiceNode(graph, serviceName, env) {
5217
+ function resolveFusedServiceId(graph, serviceName, env) {
5192
5218
  const id = (0, import_types8.serviceId)(serviceName, env);
5193
5219
  if (graph.hasNode(id)) return id;
5194
5220
  const wanted = serviceName.toLowerCase();
@@ -5198,17 +5224,21 @@ function ensureServiceNode(graph, serviceName, env) {
5198
5224
  if (svc.discoveredVia === "otel") return false;
5199
5225
  return typeof svc.name === "string" && svc.name.toLowerCase() === wanted;
5200
5226
  });
5201
- if (extractedId) return extractedId;
5227
+ return extractedId ?? id;
5228
+ }
5229
+ function ensureServiceNode(graph, serviceName, env) {
5230
+ const resolved = resolveFusedServiceId(graph, serviceName, env);
5231
+ if (graph.hasNode(resolved)) return resolved;
5202
5232
  const node = {
5203
- id,
5233
+ id: resolved,
5204
5234
  type: import_types8.NodeType.ServiceNode,
5205
5235
  name: serviceName,
5206
5236
  language: "unknown",
5207
5237
  discoveredVia: "otel",
5208
5238
  ...env !== "unknown" ? { env } : {}
5209
5239
  };
5210
- graph.addNode(id, node);
5211
- return id;
5240
+ graph.addNode(resolved, node);
5241
+ return resolved;
5212
5242
  }
5213
5243
  function ensureInfraNode(graph, kind, name, provider) {
5214
5244
  const id = (0, import_types8.infraId)(kind, name);
@@ -5401,8 +5431,23 @@ async function appendErrorEvent(ctx, ev) {
5401
5431
  await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(ctx.errorsPath), { recursive: true });
5402
5432
  await import_node_fs8.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
5403
5433
  }
5434
+ async function appendConnectorIncident(errorsPath, input) {
5435
+ const ev = {
5436
+ id: input.id,
5437
+ timestamp: input.timestamp,
5438
+ service: input.service,
5439
+ traceId: input.id,
5440
+ spanId: input.id,
5441
+ errorType: input.errorType,
5442
+ errorMessage: input.errorMessage,
5443
+ ...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
5444
+ affectedNode: input.affectedNode
5445
+ };
5446
+ await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(errorsPath), { recursive: true });
5447
+ await import_node_fs8.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
5448
+ }
5404
5449
  function incidentAffectedNode(span, graph, scanPath) {
5405
- const sid = (0, import_types8.serviceId)(span.service, span.env);
5450
+ const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
5406
5451
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
5407
5452
  const callSite = callSiteFromSpan(span, serviceNode, scanPath);
5408
5453
  if (callSite) {
@@ -6804,7 +6849,7 @@ function disambiguate(defs) {
6804
6849
  }
6805
6850
  async function addSymbols(graph, services) {
6806
6851
  const parsers = /* @__PURE__ */ new Map();
6807
- const parserForExt4 = (ext) => {
6852
+ const parserForExt5 = (ext) => {
6808
6853
  const grammar = GRAMMAR_BY_EXT[ext];
6809
6854
  if (!grammar) return null;
6810
6855
  let parser = parsers.get(ext);
@@ -6820,7 +6865,7 @@ async function addSymbols(graph, services) {
6820
6865
  for (const service of services) {
6821
6866
  const files = await loadSourceFiles(service.dir);
6822
6867
  for (const file of files) {
6823
- const parser = parserForExt4(import_node_path18.default.extname(file.path));
6868
+ const parser = parserForExt5(import_node_path18.default.extname(file.path));
6824
6869
  if (!parser) continue;
6825
6870
  const relPath = toPosix(import_node_path18.default.relative(service.dir, file.path));
6826
6871
  let defs;
@@ -6972,7 +7017,7 @@ function stringInner(node) {
6972
7017
  }
6973
7018
  async function addSymbolEdges(graph, services) {
6974
7019
  const parsers = /* @__PURE__ */ new Map();
6975
- const parserForExt4 = (ext) => {
7020
+ const parserForExt5 = (ext) => {
6976
7021
  const grammar = GRAMMAR_BY_EXT[ext];
6977
7022
  if (!grammar) return null;
6978
7023
  let parser = parsers.get(ext);
@@ -6989,7 +7034,7 @@ async function addSymbolEdges(graph, services) {
6989
7034
  const tsPaths = await loadTsPathConfig(service.dir);
6990
7035
  const files = await loadSourceFiles(service.dir);
6991
7036
  for (const file of files) {
6992
- const parser = parserForExt4(import_node_path19.default.extname(file.path));
7037
+ const parser = parserForExt5(import_node_path19.default.extname(file.path));
6993
7038
  if (!parser) continue;
6994
7039
  const relPath = toPosix(import_node_path19.default.relative(service.dir, file.path));
6995
7040
  const fileDir = import_node_path19.default.dirname(file.path);
@@ -7251,7 +7296,7 @@ function firstReferenceLines(root, wanted) {
7251
7296
  }
7252
7297
  async function addServerActions(graph, services) {
7253
7298
  const parsers = /* @__PURE__ */ new Map();
7254
- const parserForExt4 = (ext) => {
7299
+ const parserForExt5 = (ext) => {
7255
7300
  const grammar = GRAMMAR_BY_EXT[ext];
7256
7301
  if (!grammar) return null;
7257
7302
  let parser = parsers.get(ext);
@@ -7274,7 +7319,7 @@ async function addServerActions(graph, services) {
7274
7319
  const files = await loadSourceFiles(service.dir);
7275
7320
  for (const file of files) {
7276
7321
  if (isTestPath(file.path)) continue;
7277
- const parser = parserForExt4(import_node_path20.default.extname(file.path));
7322
+ const parser = parserForExt5(import_node_path20.default.extname(file.path));
7278
7323
  if (!parser) continue;
7279
7324
  const relPath = toPosix(import_node_path20.default.relative(service.dir, file.path));
7280
7325
  let root;
@@ -7337,7 +7382,7 @@ async function addServerActions(graph, services) {
7337
7382
  }
7338
7383
  for (const file of files) {
7339
7384
  if (isTestPath(file.path)) continue;
7340
- const parser = parserForExt4(import_node_path20.default.extname(file.path));
7385
+ const parser = parserForExt5(import_node_path20.default.extname(file.path));
7341
7386
  if (!parser) continue;
7342
7387
  const relPath = toPosix(import_node_path20.default.relative(service.dir, file.path));
7343
7388
  const fileDir = import_node_path20.default.dirname(file.path);
@@ -8301,6 +8346,7 @@ init_cjs_shims();
8301
8346
  var import_node_path31 = __toESM(require("path"), 1);
8302
8347
  var import_tree_sitter6 = __toESM(require("tree-sitter"), 1);
8303
8348
  var import_tree_sitter_javascript4 = __toESM(require("tree-sitter-javascript"), 1);
8349
+ var import_tree_sitter_typescript2 = __toESM(require("tree-sitter-typescript"), 1);
8304
8350
  var import_tree_sitter_python3 = __toESM(require("tree-sitter-python"), 1);
8305
8351
  var import_types20 = require("@neat.is/types");
8306
8352
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -8351,19 +8397,27 @@ function callsFromSource(source, parser, knownHosts) {
8351
8397
  }
8352
8398
  return out;
8353
8399
  }
8354
- function makeJsParser3() {
8355
- const p = new import_tree_sitter6.default();
8356
- p.setLanguage(import_tree_sitter_javascript4.default);
8357
- return p;
8358
- }
8359
- function makePyParser3() {
8360
- const p = new import_tree_sitter6.default();
8361
- p.setLanguage(import_tree_sitter_python3.default);
8362
- return p;
8400
+ var GRAMMAR_BY_EXT2 = {
8401
+ ".ts": import_tree_sitter_typescript2.default.typescript,
8402
+ ".tsx": import_tree_sitter_typescript2.default.tsx,
8403
+ ".js": import_tree_sitter_javascript4.default,
8404
+ ".jsx": import_tree_sitter_javascript4.default,
8405
+ ".mjs": import_tree_sitter_javascript4.default,
8406
+ ".cjs": import_tree_sitter_javascript4.default,
8407
+ ".py": import_tree_sitter_python3.default
8408
+ };
8409
+ function parserForExt(ext, cache) {
8410
+ const grammar = GRAMMAR_BY_EXT2[ext] ?? import_tree_sitter_javascript4.default;
8411
+ let parser = cache.get(grammar);
8412
+ if (!parser) {
8413
+ parser = new import_tree_sitter6.default();
8414
+ parser.setLanguage(grammar);
8415
+ cache.set(grammar, parser);
8416
+ }
8417
+ return parser;
8363
8418
  }
8364
8419
  async function addHttpCallEdges(graph, services) {
8365
- const jsParser = makeJsParser3();
8366
- const pyParser = makePyParser3();
8420
+ const parserCache = /* @__PURE__ */ new Map();
8367
8421
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
8368
8422
  let nodesAdded = 0;
8369
8423
  let edgesAdded = 0;
@@ -8372,7 +8426,7 @@ async function addHttpCallEdges(graph, services) {
8372
8426
  const seen = /* @__PURE__ */ new Set();
8373
8427
  for (const file of files) {
8374
8428
  if (isTestPath(file.path)) continue;
8375
- const parser = import_node_path31.default.extname(file.path) === ".py" ? pyParser : jsParser;
8429
+ const parser = parserForExt(import_node_path31.default.extname(file.path), parserCache);
8376
8430
  let sites;
8377
8431
  try {
8378
8432
  sites = callsFromSource(file.content, parser, knownHosts);
@@ -8445,7 +8499,7 @@ function parseSource5(parser, source) {
8445
8499
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK5)
8446
8500
  );
8447
8501
  }
8448
- function makeJsParser4() {
8502
+ function makeJsParser3() {
8449
8503
  const p = new import_tree_sitter7.default();
8450
8504
  p.setLanguage(import_tree_sitter_javascript5.default);
8451
8505
  return p;
@@ -8623,7 +8677,7 @@ function findRoute(entries, method, normalizedPath) {
8623
8677
  );
8624
8678
  }
8625
8679
  async function addRouteCallEdges(graph, services) {
8626
- const jsParser = makeJsParser4();
8680
+ const jsParser = makeJsParser3();
8627
8681
  const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
8628
8682
  const routeIndex = buildRouteIndex(graph);
8629
8683
  if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
@@ -9015,7 +9069,7 @@ var import_tree_sitter_javascript6 = __toESM(require("tree-sitter-javascript"),
9015
9069
  var import_types27 = require("@neat.is/types");
9016
9070
  var FIRESTORE_CLIENT_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])firebase\/firestore['"`]/;
9017
9071
  var FIRESTORE_ADMIN_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])firebase-admin(?:\/firestore)?['"`]/;
9018
- function parserForExt(ext) {
9072
+ function parserForExt2(ext) {
9019
9073
  const p = new import_tree_sitter8.default();
9020
9074
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript6.default);
9021
9075
  return p;
@@ -9180,7 +9234,7 @@ function firestoreEndpointsFromFile(file, serviceDir) {
9180
9234
  const hasAdmin = FIRESTORE_ADMIN_IMPORT_RE.test(file.content);
9181
9235
  if (!hasClient && !hasAdmin) return [];
9182
9236
  const fileSdk = hasClient && !hasAdmin ? "client" : hasAdmin && !hasClient ? "admin" : null;
9183
- const tree = parseSource3(parserForExt(import_node_path38.default.extname(file.path)), file.content);
9237
+ const tree = parseSource3(parserForExt2(import_node_path38.default.extname(file.path)), file.content);
9184
9238
  const clientVars = firestoreClientVars(tree.rootNode);
9185
9239
  const collLine = /* @__PURE__ */ new Map();
9186
9240
  const writes = /* @__PURE__ */ new Map();
@@ -9597,7 +9651,7 @@ var import_tree_sitter_python4 = __toESM(require("tree-sitter-python"), 1);
9597
9651
  var import_types29 = require("@neat.is/types");
9598
9652
  var SQLALCHEMY_IMPORT_RE = /(?:from|import)\s+(?:flask_sqlalchemy|sqlalchemy)\b/;
9599
9653
  var PARSE_CHUNK6 = 16384;
9600
- function makePyParser4() {
9654
+ function makePyParser3() {
9601
9655
  const p = new import_tree_sitter9.default();
9602
9656
  p.setLanguage(import_tree_sitter_python4.default);
9603
9657
  return p;
@@ -9704,7 +9758,7 @@ function foreignKeyParentTable(call) {
9704
9758
  }
9705
9759
  function sqlalchemyForeignKeys(file, serviceDir) {
9706
9760
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
9707
- const tree = parseSource6(makePyParser4(), file.content);
9761
+ const tree = parseSource6(makePyParser3(), file.content);
9708
9762
  const out = [];
9709
9763
  const seen = /* @__PURE__ */ new Set();
9710
9764
  walk3(tree.rootNode, (node) => {
@@ -9741,7 +9795,7 @@ function sqlalchemyForeignKeys(file, serviceDir) {
9741
9795
  }
9742
9796
  function sqlalchemyEndpointsFromFile(file, serviceDir) {
9743
9797
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) return [];
9744
- const tree = parseSource6(makePyParser4(), file.content);
9798
+ const tree = parseSource6(makePyParser3(), file.content);
9745
9799
  const out = [];
9746
9800
  const seen = /* @__PURE__ */ new Set();
9747
9801
  const push = (name, line, columns) => {
@@ -9793,7 +9847,7 @@ function sqlalchemyEndpointsFromFile(file, serviceDir) {
9793
9847
  function buildSqlalchemyModelRegistry(files) {
9794
9848
  const table = /* @__PURE__ */ new Map();
9795
9849
  const ambiguous = /* @__PURE__ */ new Set();
9796
- const parser = makePyParser4();
9850
+ const parser = makePyParser3();
9797
9851
  for (const file of files) {
9798
9852
  if (!SQLALCHEMY_IMPORT_RE.test(file.content)) continue;
9799
9853
  const tree = parseSource6(parser, file.content);
@@ -9842,7 +9896,7 @@ function importsModelName(content, name) {
9842
9896
  function pythonOrmCrossFileEndpoints(files, serviceDir) {
9843
9897
  const registry = buildSqlalchemyModelRegistry(files);
9844
9898
  if (registry.size === 0) return [];
9845
- const parser = makePyParser4();
9899
+ const parser = makePyParser3();
9846
9900
  const out = [];
9847
9901
  const seen = /* @__PURE__ */ new Set();
9848
9902
  for (const file of files) {
@@ -9880,7 +9934,7 @@ var import_tree_sitter_python5 = __toESM(require("tree-sitter-python"), 1);
9880
9934
  var import_types30 = require("@neat.is/types");
9881
9935
  var DJANGO_IMPORT_RE = /(?:from|import)\s+django\b/;
9882
9936
  var PARSE_CHUNK7 = 16384;
9883
- function makePyParser5() {
9937
+ function makePyParser4() {
9884
9938
  const p = new import_tree_sitter10.default();
9885
9939
  p.setLanguage(import_tree_sitter_python5.default);
9886
9940
  return p;
@@ -9941,7 +9995,7 @@ function readMeta(body) {
9941
9995
  }
9942
9996
  function djangoOrmEndpointsFromFile(file, serviceDir) {
9943
9997
  if (!DJANGO_IMPORT_RE.test(file.content)) return [];
9944
- const tree = parseSource7(makePyParser5(), file.content);
9998
+ const tree = parseSource7(makePyParser4(), file.content);
9945
9999
  const out = [];
9946
10000
  const seen = /* @__PURE__ */ new Set();
9947
10001
  const defaultAppLabel = import_node_path41.default.basename(import_node_path41.default.dirname(file.path));
@@ -9976,7 +10030,7 @@ var import_tree_sitter_javascript7 = __toESM(require("tree-sitter-javascript"),
9976
10030
  var import_types31 = require("@neat.is/types");
9977
10031
  var DRIZZLE_IMPORT_RE = /drizzle-orm/;
9978
10032
  var TABLE_BUILDERS = /* @__PURE__ */ new Set(["pgTable", "mysqlTable", "sqliteTable"]);
9979
- function parserForExt2(ext) {
10033
+ function parserForExt3(ext) {
9980
10034
  const p = new import_tree_sitter11.default();
9981
10035
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript7.default);
9982
10036
  return p;
@@ -10048,7 +10102,7 @@ function columnsFromObject(obj) {
10048
10102
  }
10049
10103
  function drizzleEndpointsFromFile(file, serviceDir) {
10050
10104
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
10051
- const tree = parseSource3(parserForExt2(import_node_path42.default.extname(file.path)), file.content);
10105
+ const tree = parseSource3(parserForExt3(import_node_path42.default.extname(file.path)), file.content);
10052
10106
  const out = [];
10053
10107
  const seen = /* @__PURE__ */ new Set();
10054
10108
  const walk9 = (node) => {
@@ -10137,7 +10191,7 @@ function referencesTargetVar(call) {
10137
10191
  }
10138
10192
  function drizzleForeignKeys(file, serviceDir) {
10139
10193
  if (!DRIZZLE_IMPORT_RE.test(file.content)) return [];
10140
- const tree = parseSource3(parserForExt2(import_node_path42.default.extname(file.path)), file.content);
10194
+ const tree = parseSource3(parserForExt3(import_node_path42.default.extname(file.path)), file.content);
10141
10195
  const { tables, varToTable } = collectDrizzleTables(tree.rootNode);
10142
10196
  const out = [];
10143
10197
  const seen = /* @__PURE__ */ new Set();
@@ -11205,8 +11259,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
11205
11259
  var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
11206
11260
  var import_types35 = require("@neat.is/types");
11207
11261
  init_otel();
11208
- var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
11209
11262
  var PARSE_CHUNK10 = 16384;
11263
+ var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
11264
+ "Query",
11265
+ "QueryContext",
11266
+ "QueryRow",
11267
+ "QueryRowContext",
11268
+ "Exec",
11269
+ "ExecContext",
11270
+ "Prepare",
11271
+ "PrepareContext"
11272
+ ]);
11273
+ var SQLX_METHODS = /* @__PURE__ */ new Set([
11274
+ "Get",
11275
+ "Select",
11276
+ "Queryx",
11277
+ "QueryRowx",
11278
+ "NamedExec",
11279
+ "NamedQuery",
11280
+ "MustExec",
11281
+ "Preparex",
11282
+ "GetContext",
11283
+ "SelectContext"
11284
+ ]);
11285
+ var DATABASE_SQL_IMPORT = "database/sql";
11286
+ var SQLX_IMPORT = "github.com/jmoiron/sqlx";
11287
+ function makeGoParser3() {
11288
+ const p = new import_tree_sitter14.default();
11289
+ p.setLanguage(import_tree_sitter_go3.default);
11290
+ return p;
11291
+ }
11292
+ function parseSource10(parser, source) {
11293
+ return parser.parse(
11294
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
11295
+ );
11296
+ }
11210
11297
  function walk7(node, visit) {
11211
11298
  visit(node);
11212
11299
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -11214,25 +11301,54 @@ function walk7(node, visit) {
11214
11301
  if (child) walk7(child, visit);
11215
11302
  }
11216
11303
  }
11304
+ function goStringLiteralValue(node) {
11305
+ if (!node) return null;
11306
+ if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
11307
+ const t = node.text;
11308
+ return t.length >= 2 ? t.slice(1, -1) : "";
11309
+ }
11310
+ return null;
11311
+ }
11312
+ function goImportsAny(root, names) {
11313
+ let found = false;
11314
+ walk7(root, (node) => {
11315
+ if (found || node.type !== "import_spec") return;
11316
+ for (let i = 0; i < node.namedChildCount; i++) {
11317
+ const value = goStringLiteralValue(node.namedChild(i));
11318
+ if (value !== null && names.has(value)) found = true;
11319
+ }
11320
+ });
11321
+ return found;
11322
+ }
11323
+ function firstStringLiteralArg(argsNode) {
11324
+ if (!argsNode) return null;
11325
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
11326
+ const value = goStringLiteralValue(argsNode.namedChild(i));
11327
+ if (value !== null) return value;
11328
+ }
11329
+ return null;
11330
+ }
11217
11331
  function goSqlEndpointsFromFile(file, serviceDir) {
11218
11332
  if (import_node_path48.default.extname(file.path) !== ".go") return [];
11219
- const parser = new import_tree_sitter14.default();
11220
- parser.setLanguage(import_tree_sitter_go3.default);
11221
- const tree = parser.parse(
11222
- (index) => index >= file.content.length ? "" : file.content.slice(index, index + PARSE_CHUNK10)
11223
- );
11333
+ if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
11334
+ const tree = parseSource10(makeGoParser3(), file.content);
11335
+ const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
11336
+ const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
11337
+ if (!importsDatabaseSql && !importsSqlx) return [];
11224
11338
  const out = [];
11225
11339
  walk7(tree.rootNode, (node) => {
11226
11340
  if (node.type !== "call_expression") return;
11227
11341
  const fn = node.childForFieldName("function");
11228
11342
  if (fn?.type !== "selector_expression") return;
11229
11343
  const method = fn.childForFieldName("field")?.text;
11230
- if (!method || !SQL_METHODS.has(method)) return;
11231
- const arg = node.childForFieldName("arguments")?.namedChild(0);
11232
- if (!arg || arg.type !== "interpreted_string_literal" && arg.type !== "raw_string_literal") return;
11233
- const sql = arg.text.slice(1, -1);
11344
+ if (!method) return;
11345
+ const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
11346
+ if (!recognized) return;
11347
+ const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
11348
+ if (sql === null) return;
11234
11349
  const table = tableFromSqlStatement(sql);
11235
11350
  if (!table) return;
11351
+ const columns = columnsFromSqlStatement(sql);
11236
11352
  const line = node.startPosition.row + 1;
11237
11353
  out.push({
11238
11354
  infraId: (0, import_types35.infraId)("sql-table", table),
@@ -11240,7 +11356,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
11240
11356
  kind: "sql-table",
11241
11357
  edgeType: "CALLS",
11242
11358
  confidenceKind: "verified-call-site",
11243
- evidence: { file: toPosix(import_node_path48.default.relative(serviceDir, file.path)), line, snippet: snippet(file.content, line) }
11359
+ ...columns.length > 0 ? { columns } : {},
11360
+ evidence: {
11361
+ file: toPosix(import_node_path48.default.relative(serviceDir, file.path)),
11362
+ line,
11363
+ snippet: snippet(file.content, line)
11364
+ }
11244
11365
  });
11245
11366
  });
11246
11367
  return out;
@@ -11254,12 +11375,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
11254
11375
  var import_types36 = require("@neat.is/types");
11255
11376
  var GORM_IMPORT_RE = /gorm\.io\/gorm/;
11256
11377
  var PARSE_CHUNK11 = 16384;
11257
- function makeGoParser3() {
11378
+ function makeGoParser4() {
11258
11379
  const p = new import_tree_sitter15.default();
11259
11380
  p.setLanguage(import_tree_sitter_go4.default);
11260
11381
  return p;
11261
11382
  }
11262
- function parseSource10(parser, source) {
11383
+ function parseSource11(parser, source) {
11263
11384
  return parser.parse(
11264
11385
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
11265
11386
  );
@@ -11683,7 +11804,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
11683
11804
  function gormEndpointsFromFile(file, serviceDir) {
11684
11805
  if (import_node_path49.default.extname(file.path) !== ".go") return [];
11685
11806
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11686
- const tree = parseSource10(makeGoParser3(), file.content);
11807
+ const tree = parseSource11(makeGoParser4(), file.content);
11687
11808
  const { structs, models, tableFor } = analyze(tree);
11688
11809
  const out = [];
11689
11810
  const seenTables = /* @__PURE__ */ new Set();
@@ -11714,7 +11835,7 @@ function gormEndpointsFromFile(file, serviceDir) {
11714
11835
  function gormForeignKeys(file, serviceDir) {
11715
11836
  if (import_node_path49.default.extname(file.path) !== ".go") return [];
11716
11837
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11717
- const tree = parseSource10(makeGoParser3(), file.content);
11838
+ const tree = parseSource11(makeGoParser4(), file.content);
11718
11839
  const { structs, models, tableFor } = analyze(tree);
11719
11840
  const out = [];
11720
11841
  const seen = /* @__PURE__ */ new Set();
@@ -12844,7 +12965,7 @@ var import_tree_sitter_javascript8 = __toESM(require("tree-sitter-javascript"),
12844
12965
  var import_types47 = require("@neat.is/types");
12845
12966
  var ZOD_IMPORT_RE = /\bzod\b/;
12846
12967
  var ZOD_OBJECTS = /* @__PURE__ */ new Set(["z", "zod"]);
12847
- function parserForExt3(ext) {
12968
+ function parserForExt4(ext) {
12848
12969
  const p = new import_tree_sitter16.default();
12849
12970
  p.setLanguage(GRAMMAR_BY_EXT[ext] ?? import_tree_sitter_javascript8.default);
12850
12971
  return p;
@@ -12933,7 +13054,7 @@ function topLevelSchemas(root) {
12933
13054
  }
12934
13055
  function zodShapesFromFile(file, serviceDir) {
12935
13056
  if (!ZOD_IMPORT_RE.test(file.content)) return [];
12936
- const tree = parseSource3(parserForExt3(import_node_path58.default.extname(file.path)), file.content);
13057
+ const tree = parseSource3(parserForExt4(import_node_path58.default.extname(file.path)), file.content);
12937
13058
  const out = [];
12938
13059
  const seen = /* @__PURE__ */ new Set();
12939
13060
  for (const { name, call } of topLevelSchemas(tree.rootNode)) {
@@ -13941,7 +14062,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
13941
14062
  init_cjs_shims();
13942
14063
  var import_fastify2 = __toESM(require("fastify"), 1);
13943
14064
  var import_cors = __toESM(require("@fastify/cors"), 1);
13944
- var import_types81 = require("@neat.is/types");
14065
+ var import_types86 = require("@neat.is/types");
13945
14066
 
13946
14067
  // src/extend/index.ts
13947
14068
  init_cjs_shims();
@@ -15270,6 +15391,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
15270
15391
  unresolved++;
15271
15392
  continue;
15272
15393
  }
15394
+ if (signal.incident) {
15395
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
15396
+ if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
15397
+ unresolved++;
15398
+ continue;
15399
+ }
15400
+ await appendConnectorIncident(ctx.errorsPath, {
15401
+ id: signal.incident.id,
15402
+ timestamp: signal.incident.timestamp,
15403
+ service: signal.incident.service,
15404
+ errorType: signal.incident.errorType,
15405
+ errorMessage: signal.incident.errorMessage,
15406
+ ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
15407
+ affectedNode: resolved.targetNodeId
15408
+ });
15409
+ continue;
15410
+ }
15273
15411
  if (resolved.ensureInfraNode) {
15274
15412
  const { kind, name, provider } = resolved.ensureInfraNode;
15275
15413
  ensureInfraNode(graph, kind, name, provider);
@@ -15735,10 +15873,10 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
15735
15873
  // src/connectors/supabase/map.ts
15736
15874
  var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
15737
15875
  var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
15738
- function targetFromRestPath(path82) {
15739
- const rpcMatch = REST_RPC_PATH_RE.exec(path82);
15876
+ function targetFromRestPath(path84) {
15877
+ const rpcMatch = REST_RPC_PATH_RE.exec(path84);
15740
15878
  if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
15741
- const tableMatch = REST_TABLE_PATH_RE.exec(path82);
15879
+ const tableMatch = REST_TABLE_PATH_RE.exec(path84);
15742
15880
  if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
15743
15881
  return null;
15744
15882
  }
@@ -16342,9 +16480,9 @@ function parseFirebaseTargetName(targetName) {
16342
16480
  const secondSep = rest.indexOf(FIELD_SEP);
16343
16481
  if (secondSep === -1) return null;
16344
16482
  const method = rest.slice(0, secondSep);
16345
- const path82 = rest.slice(secondSep + 1);
16346
- if (!resourceName || !method || !path82) return null;
16347
- return { resourceName, method, path: path82 };
16483
+ const path84 = rest.slice(secondSep + 1);
16484
+ if (!resourceName || !method || !path84) return null;
16485
+ return { resourceName, method, path: path84 };
16348
16486
  }
16349
16487
  function resourceNameFor(type, labels) {
16350
16488
  if (!labels) return null;
@@ -16382,14 +16520,14 @@ function mapLogEntryToSignal(entry2) {
16382
16520
  if (!req) return null;
16383
16521
  if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
16384
16522
  const method = req.requestMethod.toUpperCase();
16385
- const path82 = pathFromRequestUrl(req.requestUrl);
16386
- if (path82 === null) return null;
16523
+ const path84 = pathFromRequestUrl(req.requestUrl);
16524
+ if (path84 === null) return null;
16387
16525
  const timestamp = entry2.timestamp;
16388
16526
  if (typeof timestamp !== "string" || timestamp.length === 0) return null;
16389
16527
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
16390
16528
  return {
16391
16529
  targetKind: resourceType,
16392
- targetName: packFirebaseTargetName({ resourceName, method, path: path82 }),
16530
+ targetName: packFirebaseTargetName({ resourceName, method, path: path84 }),
16393
16531
  callCount: 1,
16394
16532
  errorCount: isError ? 1 : 0,
16395
16533
  lastObservedIso: timestamp
@@ -16596,7 +16734,7 @@ function mapEventToSignal(event) {
16596
16734
  if (Number.isNaN(observedAt.getTime())) return null;
16597
16735
  const statusCode = metadata?.statusCode;
16598
16736
  const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
16599
- const path82 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
16737
+ const path84 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
16600
16738
  return {
16601
16739
  targetKind: CLOUDFLARE_TARGET_KIND,
16602
16740
  targetName: scriptName,
@@ -16604,7 +16742,7 @@ function mapEventToSignal(event) {
16604
16742
  errorCount: isError ? 1 : 0,
16605
16743
  lastObservedIso: observedAt.toISOString(),
16606
16744
  method,
16607
- ...path82 ? { path: path82 } : {},
16745
+ ...path84 ? { path: path84 } : {},
16608
16746
  ...typeof statusCode === "number" ? { statusCode } : {},
16609
16747
  ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
16610
16748
  };
@@ -16650,8 +16788,8 @@ function findTaggedWorkerFileNode(graph, workerName) {
16650
16788
  });
16651
16789
  return found;
16652
16790
  }
16653
- function findMatchingRouteNode(graph, serviceName, method, path82) {
16654
- const normalizedPath = normalizePathTemplate(path82);
16791
+ function findMatchingRouteNode(graph, serviceName, method, path84) {
16792
+ const normalizedPath = normalizePathTemplate(path84);
16655
16793
  let found = null;
16656
16794
  graph.forEachNode((id, attrs) => {
16657
16795
  if (found) return;
@@ -16668,10 +16806,10 @@ function createCloudflareResolveTarget(config, graph) {
16668
16806
  return (signal) => {
16669
16807
  if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
16670
16808
  const scriptName = signal.targetName;
16671
- const { method, path: path82 } = signal;
16809
+ const { method, path: path84 } = signal;
16672
16810
  const resolveRouteGrain = (serviceName, wholeFileId) => {
16673
- if (!method || !path82) return wholeFileId;
16674
- return findMatchingRouteNode(graph, serviceName, method, path82) ?? wholeFileId;
16811
+ if (!method || !path84) return wholeFileId;
16812
+ return findMatchingRouteNode(graph, serviceName, method, path84) ?? wholeFileId;
16675
16813
  };
16676
16814
  const mapping = config.workers?.[scriptName];
16677
16815
  if (mapping) {
@@ -17023,9 +17161,9 @@ function parseCloudRunTargetName(targetName) {
17023
17161
  const secondSep = rest.indexOf(FIELD_SEP2);
17024
17162
  if (secondSep === -1) return null;
17025
17163
  const method = rest.slice(0, secondSep);
17026
- const path82 = rest.slice(secondSep + 1);
17027
- if (!serviceName || !method || !path82) return null;
17028
- return { serviceName, method, path: path82 };
17164
+ const path84 = rest.slice(secondSep + 1);
17165
+ if (!serviceName || !method || !path84) return null;
17166
+ return { serviceName, method, path: path84 };
17029
17167
  }
17030
17168
 
17031
17169
  // src/connectors/cloud-run/map.ts
@@ -17054,14 +17192,14 @@ function mapLogEntryToSignal2(entry2) {
17054
17192
  if (!req) return null;
17055
17193
  if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
17056
17194
  const method = req.requestMethod.toUpperCase();
17057
- const path82 = pathFromRequestUrl2(req.requestUrl);
17058
- if (path82 === null) return null;
17195
+ const path84 = pathFromRequestUrl2(req.requestUrl);
17196
+ if (path84 === null) return null;
17059
17197
  const timestamp = entry2.timestamp;
17060
17198
  if (typeof timestamp !== "string" || timestamp.length === 0) return null;
17061
17199
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD4;
17062
17200
  return {
17063
17201
  targetKind: CLOUD_RUN_TARGET_KIND,
17064
- targetName: packCloudRunTargetName({ serviceName, method, path: path82 }),
17202
+ targetName: packCloudRunTargetName({ serviceName, method, path: path84 }),
17065
17203
  callCount: 1,
17066
17204
  errorCount: isError ? 1 : 0,
17067
17205
  lastObservedIso: timestamp
@@ -17100,14 +17238,14 @@ function createCloudRunResolveTarget(graph, config) {
17100
17238
  if (signal.targetKind !== CLOUD_RUN_TARGET_KIND) return null;
17101
17239
  const identity = parseCloudRunTargetName(signal.targetName);
17102
17240
  if (!identity) return null;
17103
- const { serviceName: gcpServiceName, method, path: path82 } = identity;
17241
+ const { serviceName: gcpServiceName, method, path: path84 } = identity;
17104
17242
  const mappedService = config.serviceMap?.[gcpServiceName];
17105
17243
  if (mappedService) {
17106
17244
  const routeNodeId = findMatchingRouteNode2(
17107
17245
  graph,
17108
17246
  mappedService,
17109
17247
  method,
17110
- normalizePathTemplate(path82)
17248
+ normalizePathTemplate(path84)
17111
17249
  );
17112
17250
  if (routeNodeId) {
17113
17251
  return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types71.EdgeType.CALLS };
@@ -17504,6 +17642,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
17504
17642
  };
17505
17643
  }
17506
17644
 
17645
+ // src/connectors/eas/index.ts
17646
+ init_cjs_shims();
17647
+
17648
+ // src/connectors/eas/client.ts
17649
+ init_cjs_shims();
17650
+
17651
+ // src/connectors/eas/types.ts
17652
+ init_cjs_shims();
17653
+ function readEasCredentials(raw) {
17654
+ const token = raw["token"];
17655
+ if (typeof token !== "string" || token.length === 0) {
17656
+ throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
17657
+ }
17658
+ return { token };
17659
+ }
17660
+ var EAS_STATUS_ERRORED = "ERRORED";
17661
+ var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
17662
+ "SPIN_UP_BUILDER",
17663
+ "PREPARE_CREDENTIALS",
17664
+ "RESTORE_CACHE",
17665
+ "UPLOAD_APPLICATION_ARCHIVE"
17666
+ ]);
17667
+ var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
17668
+ function isTransientFailure(err) {
17669
+ if (!err) return false;
17670
+ const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
17671
+ if (phase) {
17672
+ if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
17673
+ if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
17674
+ }
17675
+ const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
17676
+ if (code && INTERNAL_ERROR_CODE.test(code)) return true;
17677
+ return false;
17678
+ }
17679
+ var FIELD_SEP3 = "\0";
17680
+ var EAS_TARGET_KIND = "eas-build";
17681
+ function packEasTargetName(identity) {
17682
+ return [identity.serviceName, identity.phase].join(FIELD_SEP3);
17683
+ }
17684
+ function parseEasTargetName(targetName) {
17685
+ const sep = targetName.indexOf(FIELD_SEP3);
17686
+ if (sep === -1) return null;
17687
+ const serviceName = targetName.slice(0, sep);
17688
+ const phase = targetName.slice(sep + 1);
17689
+ if (!serviceName) return null;
17690
+ return { serviceName, phase };
17691
+ }
17692
+
17693
+ // src/connectors/eas/client.ts
17694
+ var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
17695
+ var DEFAULT_PAGE_SIZE = 50;
17696
+ var DEFAULT_MAX_PAGES = 10;
17697
+ var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
17698
+ var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
17699
+ var BUILDS_QUERY = `
17700
+ query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
17701
+ app {
17702
+ byId(appId: $appId) {
17703
+ builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
17704
+ id
17705
+ status
17706
+ platform
17707
+ buildProfile
17708
+ gitCommitHash
17709
+ gitCommitMessage
17710
+ gitRef
17711
+ isGitWorkingTreeDirty
17712
+ createdAt
17713
+ completedAt
17714
+ error {
17715
+ buildPhase
17716
+ errorCode
17717
+ message
17718
+ docsUrl
17719
+ }
17720
+ logFileUrls
17721
+ }
17722
+ }
17723
+ }
17724
+ }
17725
+ `;
17726
+ async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
17727
+ const res = await junctionFetch(
17728
+ apiUrl,
17729
+ {
17730
+ method: "POST",
17731
+ headers: {
17732
+ "Content-Type": "application/json",
17733
+ ...bearerAuthHeader(token)
17734
+ },
17735
+ body: JSON.stringify({ query, variables })
17736
+ },
17737
+ // accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
17738
+ // bucket (ADR-131), the closest thing this connector carries to "one account".
17739
+ { provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
17740
+ );
17741
+ if (!res.ok) {
17742
+ throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
17743
+ }
17744
+ const body = await res.json();
17745
+ if (body.errors && body.errors.length > 0) {
17746
+ throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
17747
+ }
17748
+ if (!body.data) throw new Error("Expo GraphQL response carried no data");
17749
+ return body.data;
17750
+ }
17751
+ async function fetchErroredBuilds(token, config, fetchImpl) {
17752
+ const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
17753
+ const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
17754
+ const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
17755
+ const out = [];
17756
+ const seen = /* @__PURE__ */ new Set();
17757
+ for (let page = 0; page < maxPages; page++) {
17758
+ const data = await easGraphQL(
17759
+ apiUrl,
17760
+ token,
17761
+ BUILDS_QUERY,
17762
+ { appId: config.appId, offset: page * pageSize, limit: pageSize },
17763
+ config.appId,
17764
+ fetchImpl
17765
+ );
17766
+ const builds = data.app?.byId?.builds;
17767
+ if (!Array.isArray(builds)) break;
17768
+ let added = 0;
17769
+ for (const b of builds) {
17770
+ if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
17771
+ if (b.status !== EAS_STATUS_ERRORED) continue;
17772
+ if (seen.has(b.id)) continue;
17773
+ seen.add(b.id);
17774
+ out.push(b);
17775
+ added++;
17776
+ }
17777
+ if (builds.length < pageSize) break;
17778
+ if (added === 0) break;
17779
+ }
17780
+ return out;
17781
+ }
17782
+ async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
17783
+ if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
17784
+ const doFetch = fetchImpl ?? fetch;
17785
+ const chunks = [];
17786
+ for (const url of logFileUrls) {
17787
+ if (typeof url !== "string" || url.length === 0) continue;
17788
+ try {
17789
+ const res = await doFetch(url);
17790
+ if (!res.ok) continue;
17791
+ chunks.push(await res.text());
17792
+ } catch {
17793
+ }
17794
+ }
17795
+ const joined = chunks.join("\n");
17796
+ return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
17797
+ }
17798
+
17799
+ // src/connectors/eas/map.ts
17800
+ init_cjs_shims();
17801
+ function buildEventTime(build) {
17802
+ if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
17803
+ if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
17804
+ return (/* @__PURE__ */ new Date()).toISOString();
17805
+ }
17806
+ function incidentMessage2(build) {
17807
+ const err = build.error ?? {};
17808
+ const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
17809
+ const detail = typeof err.message === "string" && err.message.trim().length > 0 && err.message.trim() || typeof err.errorCode === "string" && err.errorCode.length > 0 && err.errorCode || "no error detail reported";
17810
+ let msg = `EAS build failed${phase}: ${detail}`;
17811
+ if (build.isGitWorkingTreeDirty === true) {
17812
+ msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
17813
+ }
17814
+ return msg;
17815
+ }
17816
+ function incidentAttributes(build) {
17817
+ const attrs = {};
17818
+ const err = build.error ?? {};
17819
+ const put = (k, v) => {
17820
+ if (typeof v === "string" && v.length === 0) return;
17821
+ if (v !== void 0 && v !== null) attrs[k] = v;
17822
+ };
17823
+ put("eas.buildId", build.id);
17824
+ put("eas.platform", build.platform ?? void 0);
17825
+ put("eas.buildProfile", build.buildProfile ?? void 0);
17826
+ put("eas.buildPhase", err.buildPhase ?? void 0);
17827
+ put("eas.errorCode", err.errorCode ?? void 0);
17828
+ put("eas.docsUrl", err.docsUrl ?? void 0);
17829
+ put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
17830
+ put("eas.gitRef", build.gitRef ?? void 0);
17831
+ put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
17832
+ if (typeof build.isGitWorkingTreeDirty === "boolean") {
17833
+ attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
17834
+ if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
17835
+ }
17836
+ put("eas.createdAt", build.createdAt ?? void 0);
17837
+ put("eas.completedAt", build.completedAt ?? void 0);
17838
+ if (typeof build.logsText === "string" && build.logsText.length > 0) {
17839
+ attrs["eas.logs"] = build.logsText;
17840
+ }
17841
+ return attrs;
17842
+ }
17843
+ function mapBuildToSignal(build, serviceName) {
17844
+ if (!build || typeof build !== "object") return null;
17845
+ if (build.status !== EAS_STATUS_ERRORED) return null;
17846
+ if (!build.error) return null;
17847
+ if (isTransientFailure(build.error)) return null;
17848
+ const timestamp = buildEventTime(build);
17849
+ const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
17850
+ return {
17851
+ targetKind: EAS_TARGET_KIND,
17852
+ targetName: packEasTargetName({ serviceName, phase }),
17853
+ // Incident-only — no edge, so no call/error count to replay.
17854
+ callCount: 0,
17855
+ errorCount: 0,
17856
+ lastObservedIso: timestamp,
17857
+ incident: {
17858
+ id: `eas:build:${build.id}`,
17859
+ timestamp,
17860
+ service: serviceName,
17861
+ errorType: "eas-build-failure",
17862
+ errorMessage: incidentMessage2(build),
17863
+ attributes: incidentAttributes(build)
17864
+ }
17865
+ };
17866
+ }
17867
+ function mapBuildsToSignals(builds, serviceName) {
17868
+ const out = [];
17869
+ for (const build of builds) {
17870
+ const signal = mapBuildToSignal(build, serviceName);
17871
+ if (signal) out.push(signal);
17872
+ }
17873
+ return out;
17874
+ }
17875
+
17876
+ // src/connectors/eas/resolve.ts
17877
+ init_cjs_shims();
17878
+ var import_types83 = require("@neat.is/types");
17879
+ var NO_ENV2 = "unknown";
17880
+ var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
17881
+ var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
17882
+ "READ_APP_CONFIG",
17883
+ "CONFIGURE_EXPO_UPDATES",
17884
+ "CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
17885
+ ]);
17886
+ function configBasenamesForPhase(phase) {
17887
+ if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
17888
+ if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
17889
+ return [];
17890
+ }
17891
+ function configNodeService(graph, configNodeId) {
17892
+ for (const edgeId of graph.inboundEdges(configNodeId)) {
17893
+ const edge = graph.getEdgeAttributes(edgeId);
17894
+ if (edge.type !== import_types83.EdgeType.CONFIGURED_BY) continue;
17895
+ const parsed = (0, import_types83.parseFileId)(edge.source);
17896
+ if (parsed) return parsed.service;
17897
+ }
17898
+ return null;
17899
+ }
17900
+ function findConfigNode(graph, basenames, serviceName) {
17901
+ let scoped = null;
17902
+ let anyMatch = null;
17903
+ graph.forEachNode((id, attrs) => {
17904
+ if (scoped) return;
17905
+ const node = attrs;
17906
+ if (node.type !== import_types83.NodeType.ConfigNode) return;
17907
+ if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
17908
+ if (anyMatch === null) anyMatch = id;
17909
+ if (configNodeService(graph, id) === serviceName) scoped = id;
17910
+ });
17911
+ return scoped ?? anyMatch;
17912
+ }
17913
+ function createEasResolveTarget(graph) {
17914
+ return (signal) => {
17915
+ if (signal.targetKind !== EAS_TARGET_KIND) return null;
17916
+ const identity = parseEasTargetName(signal.targetName);
17917
+ if (!identity) return null;
17918
+ const { serviceName, phase } = identity;
17919
+ const basenames = configBasenamesForPhase(phase);
17920
+ if (basenames.length > 0) {
17921
+ const configNodeId = findConfigNode(graph, basenames, serviceName);
17922
+ if (configNodeId) {
17923
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types83.EdgeType.CALLS };
17924
+ }
17925
+ }
17926
+ return {
17927
+ targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
17928
+ serviceName,
17929
+ edgeType: import_types83.EdgeType.CALLS
17930
+ };
17931
+ };
17932
+ }
17933
+
17934
+ // src/connectors/eas/index.ts
17935
+ function isBuildSince(build, sinceIso) {
17936
+ const t = Date.parse(buildEventTime(build));
17937
+ const s = Date.parse(sinceIso);
17938
+ if (Number.isNaN(t) || Number.isNaN(s)) return true;
17939
+ return t > s;
17940
+ }
17941
+ function boundedSinceIso2(since, now, maxLookbackMs) {
17942
+ const floor = new Date(now.getTime() - maxLookbackMs);
17943
+ if (!since) return floor.toISOString();
17944
+ const sinceMs = new Date(since).getTime();
17945
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
17946
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
17947
+ }
17948
+ var EasConnector = class {
17949
+ constructor(config, fetchImpl) {
17950
+ this.config = config;
17951
+ this.fetchImpl = fetchImpl;
17952
+ }
17953
+ config;
17954
+ fetchImpl;
17955
+ provider = "eas";
17956
+ async poll(ctx) {
17957
+ const creds = readEasCredentials(ctx.credentials);
17958
+ const serviceName = this.config.serviceName ?? this.config.appId;
17959
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
17960
+ const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
17961
+ const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
17962
+ const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
17963
+ const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
17964
+ for (const build of fresh) {
17965
+ build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
17966
+ }
17967
+ return mapBuildsToSignals(fresh, serviceName);
17968
+ }
17969
+ };
17970
+ function createEasConnector(graph, config, fetchImpl) {
17971
+ return {
17972
+ connector: new EasConnector(config, fetchImpl),
17973
+ resolveTarget: createEasResolveTarget(graph)
17974
+ };
17975
+ }
17976
+
17507
17977
  // src/connectors/registry.ts
17508
17978
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
17509
17979
  async function authProbe(input) {
@@ -17791,6 +18261,41 @@ var PROVIDER_DISPATCH = {
17791
18261
  ...fetchImpl ? { fetchImpl } : {}
17792
18262
  });
17793
18263
  }
18264
+ },
18265
+ eas: {
18266
+ provider: "eas",
18267
+ // The secret is a single robot-user EXPO_TOKEN; a single-string credential
18268
+ // maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
18269
+ primaryCredentialKey: "token",
18270
+ requiredCredentialFields: ["token"],
18271
+ requiredOptionFields: ["appId"],
18272
+ build(graph, options) {
18273
+ return createEasConnector(graph, options);
18274
+ },
18275
+ // Runs the connector's real `builds` query at limit 1 — the exact read poll()
18276
+ // performs, minus the pages — so the probe checks both that the EXPO_TOKEN
18277
+ // authenticates and that this app id is reachable, the same probe-the-real-
18278
+ // query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
18279
+ // A bad or wrong-scoped token comes back as an Expo GraphQL error, which
18280
+ // `fetchErroredBuilds` throws on, so it fails honestly here rather than
18281
+ // silently at the first poll.
18282
+ async validate({ credentials, options, fetchImpl }) {
18283
+ const cfg = options;
18284
+ const appId = String(cfg.appId ?? "");
18285
+ if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
18286
+ const probeConfig = {
18287
+ appId,
18288
+ pageSize: 1,
18289
+ maxPages: 1,
18290
+ ...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
18291
+ };
18292
+ try {
18293
+ await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
18294
+ return { ok: true };
18295
+ } catch (err) {
18296
+ return { ok: false, reason: `eas auth check failed: ${err.message}` };
18297
+ }
18298
+ }
17794
18299
  }
17795
18300
  };
17796
18301
  function vercelCredsFrom(credentials) {
@@ -18003,7 +18508,11 @@ async function startConnectorPolling(input) {
18003
18508
  const stopFns = all.map(
18004
18509
  (registration) => startConnectorPollLoop(
18005
18510
  registration.connector,
18006
- { projectDir: input.projectDir, credentials: registration.credentials },
18511
+ {
18512
+ projectDir: input.projectDir,
18513
+ credentials: registration.credentials,
18514
+ ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
18515
+ },
18007
18516
  input.graph,
18008
18517
  registration.resolveTarget,
18009
18518
  { intervalMs: registration.intervalMs, connectorId: registration.id }
@@ -18221,11 +18730,11 @@ function registerRoutes(scope, ctx) {
18221
18730
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
18222
18731
  const parsed = [];
18223
18732
  for (const c of candidates) {
18224
- const r = import_types81.DivergenceTypeSchema.safeParse(c);
18733
+ const r = import_types86.DivergenceTypeSchema.safeParse(c);
18225
18734
  if (!r.success) {
18226
18735
  return reply.code(400).send({
18227
18736
  error: `unknown divergence type "${c}"`,
18228
- allowed: import_types81.DivergenceTypeSchema.options
18737
+ allowed: import_types86.DivergenceTypeSchema.options
18229
18738
  });
18230
18739
  }
18231
18740
  parsed.push(r.data);
@@ -18332,10 +18841,15 @@ function registerRoutes(scope, ctx) {
18332
18841
  }
18333
18842
  const reg = built.registration;
18334
18843
  const at = (/* @__PURE__ */ new Date()).toISOString();
18844
+ const incidentsPath = errorsPathFor(proj);
18335
18845
  try {
18336
18846
  const result = await ctx.runPoll(
18337
18847
  reg.connector,
18338
- { projectDir: proj.scanPath ?? "", credentials: reg.credentials },
18848
+ {
18849
+ projectDir: proj.scanPath ?? "",
18850
+ credentials: reg.credentials,
18851
+ ...incidentsPath ? { errorsPath: incidentsPath } : {}
18852
+ },
18339
18853
  proj.graph,
18340
18854
  reg.resolveTarget
18341
18855
  );
@@ -18534,7 +19048,7 @@ function registerRoutes(scope, ctx) {
18534
19048
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
18535
19049
  let violations = await log.readAll();
18536
19050
  if (req.query.severity) {
18537
- const sev = import_types81.PolicySeveritySchema.safeParse(req.query.severity);
19051
+ const sev = import_types86.PolicySeveritySchema.safeParse(req.query.severity);
18538
19052
  if (!sev.success) {
18539
19053
  return reply.code(400).send({
18540
19054
  error: "invalid severity",
@@ -18573,7 +19087,7 @@ function registerRoutes(scope, ctx) {
18573
19087
  scope.post("/policies/check", async (req, reply) => {
18574
19088
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
18575
19089
  if (!proj) return;
18576
- const parsed = import_types81.PoliciesCheckBodySchema.safeParse(req.body ?? {});
19090
+ const parsed = import_types86.PoliciesCheckBodySchema.safeParse(req.body ?? {});
18577
19091
  if (!parsed.success) {
18578
19092
  return reply.code(400).send({
18579
19093
  error: "invalid /policies/check body",
@@ -18906,7 +19420,7 @@ var import_node_fs34 = require("fs");
18906
19420
  var import_node_path68 = __toESM(require("path"), 1);
18907
19421
 
18908
19422
  // src/daemon.ts
18909
- var import_types82 = require("@neat.is/types");
19423
+ var import_types87 = require("@neat.is/types");
18910
19424
  function daemonJsonPath(scanPath) {
18911
19425
  return import_node_path69.default.join(scanPath, "neat-out", "daemon.json");
18912
19426
  }
@@ -19275,12 +19789,46 @@ var SubstringIndex = class {
19275
19789
  this.graph = graph;
19276
19790
  }
19277
19791
  };
19792
+ var DEFAULT_SEARCH_INIT_TIMEOUT_MS = 3e4;
19793
+ function searchInitTimeoutMs() {
19794
+ const env = process.env.NEAT_SEARCH_INIT_TIMEOUT_MS;
19795
+ if (env !== void 0 && env.length > 0) {
19796
+ const n = Number.parseInt(env, 10);
19797
+ if (Number.isFinite(n) && n >= 0) return n;
19798
+ }
19799
+ return DEFAULT_SEARCH_INIT_TIMEOUT_MS;
19800
+ }
19801
+ async function resolveEmbedderBounded(factory, timeoutMs) {
19802
+ if (timeoutMs <= 0) return factory();
19803
+ let timer;
19804
+ const TIMED_OUT = /* @__PURE__ */ Symbol("embedder-init-timeout");
19805
+ const timeout = new Promise((resolve) => {
19806
+ timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
19807
+ timer.unref?.();
19808
+ });
19809
+ try {
19810
+ const result = await Promise.race([factory(), timeout]);
19811
+ if (result === TIMED_OUT) {
19812
+ console.warn(
19813
+ `semantic_search: embedder init exceeded ${timeoutMs}ms; falling back to substring search. Set NEAT_SEARCH_INIT_TIMEOUT_MS to raise the bound (or 0 to wait indefinitely).`
19814
+ );
19815
+ return null;
19816
+ }
19817
+ return result;
19818
+ } finally {
19819
+ if (timer) clearTimeout(timer);
19820
+ }
19821
+ }
19278
19822
  async function buildSearchIndex(graph, options = {}) {
19279
19823
  let embedder = null;
19280
19824
  if (options.embedder) {
19281
19825
  embedder = options.embedder;
19282
19826
  } else if (options.forceProvider !== "substring") {
19283
- embedder = await pickEmbedder();
19827
+ const factory = options.embedderFactory ?? pickEmbedder;
19828
+ embedder = await resolveEmbedderBounded(
19829
+ factory,
19830
+ options.initTimeoutMs ?? searchInitTimeoutMs()
19831
+ );
19284
19832
  if (options.forceProvider === "ollama" && embedder?.provider !== "ollama") {
19285
19833
  embedder = null;
19286
19834
  }
@@ -19530,6 +20078,9 @@ async function startWatch(graph, opts) {
19530
20078
  project: projectName,
19531
20079
  graph,
19532
20080
  projectDir: opts.scanPath,
20081
+ // Incident ledger for an incident-emitting connector (ADR-185), same path
20082
+ // `neat watch`'s own error-span writer already uses.
20083
+ errorsPath: opts.errorsPath,
19533
20084
  ...opts.neatHome ? { home: opts.neatHome } : {},
19534
20085
  onSkip: (skipped, reason) => console.warn(
19535
20086
  `neat watch: connector "${skipped.id}" (${skipped.provider}) skipped for project "${projectName}" \u2014 ${reason}`
@@ -19903,7 +20454,8 @@ var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
19903
20454
  try {
19904
20455
  const __rec = JSON.parse(__neatFs.readFileSync(__neatPath.join(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
19905
20456
  if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
19906
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
20457
+ const __neatProj = (typeof __rec.project === 'string' && __rec.project) || '__PROJECT__'
20458
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/projects/' + __neatProj + '/v1/traces'
19907
20459
  break
19908
20460
  }
19909
20461
  } catch (_e) {}
@@ -19912,7 +20464,7 @@ var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
19912
20464
  __neatDir = __parent
19913
20465
  }
19914
20466
  } catch (_e) {}
19915
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
20467
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
19916
20468
  })()`;
19917
20469
  var OTEL_ESM_NODE_IMPORTS = "import { readFileSync as __neatReadFileSync } from 'node:fs'\nimport { join as __neatJoin, dirname as __neatDirname } from 'node:path'";
19918
20470
  var OTEL_ENDPOINT_RESOLVER_ESM = `;(function () {
@@ -19923,7 +20475,8 @@ var OTEL_ENDPOINT_RESOLVER_ESM = `;(function () {
19923
20475
  try {
19924
20476
  const __rec = JSON.parse(__neatReadFileSync(__neatJoin(__neatDir, 'neat-out', 'daemon.json'), 'utf8'))
19925
20477
  if (__rec && __rec.ports && typeof __rec.ports.otlp === 'number') {
19926
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/v1/traces'
20478
+ const __neatProj = (typeof __rec.project === 'string' && __rec.project) || '__PROJECT__'
20479
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT = 'http://localhost:' + __rec.ports.otlp + '/projects/' + __neatProj + '/v1/traces'
19927
20480
  break
19928
20481
  }
19929
20482
  } catch (_e) {}
@@ -19932,7 +20485,7 @@ var OTEL_ENDPOINT_RESOLVER_ESM = `;(function () {
19932
20485
  __neatDir = __parent
19933
20486
  }
19934
20487
  } catch (_e) {}
19935
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
20488
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
19936
20489
  })()`;
19937
20490
  function neatCaptureSource(ts) {
19938
20491
  const spanT = ts ? ": any" : "";
@@ -20334,14 +20887,15 @@ ${registrations.join("\n")}
20334
20887
  `;
20335
20888
  return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
20336
20889
  }
20337
- function renderEnvNeat(serviceName, _projectName) {
20890
+ function renderEnvNeat(serviceName, projectName) {
20338
20891
  return [
20339
20892
  "# Generated by `neat init --apply` (ADR-069).",
20340
20893
  `OTEL_SERVICE_NAME=${serviceName}`,
20341
20894
  "# Advisory only \u2014 the generated otel-init resolves the live endpoint from",
20342
- "# <project>/neat-out/daemon.json (ports.otlp) at boot (ADR-096). This is the",
20343
- "# canonical default the daemon takes when its first-choice OTLP port is free.",
20344
- "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces",
20895
+ "# <project>/neat-out/daemon.json (ports.otlp + project) at boot (ADR-096).",
20896
+ "# This is the canonical default the daemon takes when its first-choice OTLP",
20897
+ "# port is free; the project scope routes the span without service.name guessing (#879).",
20898
+ `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/projects/${projectName}/v1/traces`,
20345
20899
  "OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
20346
20900
  "# Set NEAT_OTEL_TOKEN to the daemon's OTLP secret to authenticate exported spans (#410).",
20347
20901
  "# NEAT_OTEL_TOKEN=",
@@ -20407,7 +20961,7 @@ var NEXT_INSTRUMENTATION_EDGE_TS = `${NEXT_INSTRUMENTATION_EDGE_HEADER}
20407
20961
  import { registerOTel } from '@vercel/otel'
20408
20962
 
20409
20963
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
20410
- process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/v1/traces'
20964
+ process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
20411
20965
  ${OTEL_OTLP_PROTOCOL_JS}
20412
20966
  ${OTEL_OTLP_HEADERS_JS}
20413
20967
 
@@ -22155,14 +22709,519 @@ async function apply3(installPlan) {
22155
22709
  await import_node_fs41.promises.writeFile(generated.file, generated.contents, "utf8");
22156
22710
  writtenFiles.push(generated.file);
22157
22711
  }
22158
- return { serviceDir: installPlan.serviceDir, outcome: writtenFiles.length ? "instrumented" : "already-instrumented", writtenFiles };
22712
+ const wroteManifest = writtenFiles.some((f) => f.split(/[\\/]/).pop() === "go.mod");
22713
+ return {
22714
+ serviceDir: installPlan.serviceDir,
22715
+ outcome: writtenFiles.length ? "instrumented" : "already-instrumented",
22716
+ writtenFiles,
22717
+ ...wroteManifest ? { followUpInstall: "go mod download" } : {}
22718
+ };
22159
22719
  }
22160
22720
  var goInstaller = { name: "go", detect: detect3, plan: plan3, apply: apply3 };
22161
22721
 
22722
+ // src/installers/ruby.ts
22723
+ init_cjs_shims();
22724
+ var import_node_fs42 = require("fs");
22725
+ var import_node_path76 = __toESM(require("path"), 1);
22726
+ var RUBY_MARKERS = [
22727
+ "Gemfile",
22728
+ "Gemfile.lock"
22729
+ ];
22730
+ var RUBY_GEMS = [
22731
+ { name: "opentelemetry-sdk", version: "~> 1.5" },
22732
+ { name: "opentelemetry-exporter-otlp", version: "~> 0.29" },
22733
+ { name: "opentelemetry-instrumentation-all", version: "~> 0.62" }
22734
+ ];
22735
+ var NEAT_OTEL_STAMP2 = "neat-otel-init v1";
22736
+ var INITIALIZER_REL = import_node_path76.default.join("config", "initializers", "neat_otel.rb");
22737
+ function neatOtelRb(opts = {}) {
22738
+ const service = opts.project ?? "ruby-service";
22739
+ const endpoint2 = opts.project ? `http://localhost:4318/projects/${opts.project}/v1/traces` : "http://localhost:4318/v1/traces";
22740
+ return `# ${NEAT_OTEL_STAMP2} \u2014 generated by NEAT. Safe to re-generate; do not edit.
22741
+ # Rails auto-loads this at boot (config/initializers/*). It points the
22742
+ # OpenTelemetry SDK at your NEAT daemon, enables the Ruby auto-instrumentation
22743
+ # set, and installs a span processor that stamps code.file.path /
22744
+ # code.line.number / code.function.name on the CLIENT/PRODUCER spans your app
22745
+ # issues, so NEAT fuses each runtime span onto the source file that made the
22746
+ # call (docs/contracts/file-awareness.md). Absolute paths are emitted here;
22747
+ # ingest anchors them against the service root. If the OpenTelemetry gems are
22748
+ # not installed this file degrades to a no-op rather than breaking boot.
22749
+
22750
+ begin
22751
+ require 'opentelemetry/sdk'
22752
+ require 'opentelemetry/exporter/otlp'
22753
+ require 'opentelemetry/instrumentation/all'
22754
+ _neat_otel_loaded = true
22755
+ rescue LoadError
22756
+ _neat_otel_loaded = false
22757
+ end
22758
+
22759
+ if _neat_otel_loaded && ENV['NEAT_CALLSITE_DISABLED'] != '1'
22760
+ # Walk the Ruby call stack to the first application frame and stamp the stable
22761
+ # OTel source attributes on CLIENT/PRODUCER spans. SERVER spans are created
22762
+ # before the handler runs, so they stay route/service-grained, honestly.
22763
+ class NeatCallSiteSpanProcessor
22764
+ def initialize(root)
22765
+ @root = root.to_s.end_with?(File::SEPARATOR) ? root.to_s : root.to_s + File::SEPARATOR
22766
+ end
22767
+
22768
+ def on_start(span, _parent_context)
22769
+ kind = span.kind
22770
+ return unless kind == OpenTelemetry::Trace::SpanKind::CLIENT ||
22771
+ kind == OpenTelemetry::Trace::SpanKind::PRODUCER
22772
+ caller_locations(1).each do |loc|
22773
+ file = loc.absolute_path || loc.path
22774
+ next if file.nil?
22775
+ next unless file.start_with?(@root)
22776
+ next if file.include?('/vendor/') || file.include?('/.bundle/')
22777
+ next if file.end_with?('neat_otel.rb')
22778
+ span.set_attribute('code.file.path', file)
22779
+ span.set_attribute('code.line.number', loc.lineno)
22780
+ span.set_attribute('code.function.name', loc.label.to_s)
22781
+ break
22782
+ end
22783
+ rescue StandardError
22784
+ # never break the host application
22785
+ end
22786
+
22787
+ def on_finish(_span); end
22788
+
22789
+ def force_flush(timeout: nil)
22790
+ OpenTelemetry::SDK::Trace::Export::SUCCESS
22791
+ end
22792
+
22793
+ def shutdown(timeout: nil)
22794
+ OpenTelemetry::SDK::Trace::Export::SUCCESS
22795
+ end
22796
+ end
22797
+
22798
+ _neat_root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
22799
+ _neat_service = ENV.fetch('OTEL_SERVICE_NAME', '${service}')
22800
+ _neat_endpoint = ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', '${endpoint2}')
22801
+
22802
+ OpenTelemetry::SDK.configure do |c|
22803
+ c.service_name = _neat_service
22804
+ c.use_all
22805
+ c.add_span_processor(
22806
+ OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
22807
+ OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: _neat_endpoint)
22808
+ )
22809
+ )
22810
+ c.add_span_processor(NeatCallSiteSpanProcessor.new(_neat_root))
22811
+ end
22812
+ end
22813
+ `;
22814
+ }
22815
+ async function exists6(p) {
22816
+ try {
22817
+ await import_node_fs42.promises.stat(p);
22818
+ return true;
22819
+ } catch {
22820
+ return false;
22821
+ }
22822
+ }
22823
+ async function detect4(serviceDir) {
22824
+ for (const marker of RUBY_MARKERS) {
22825
+ if (await exists6(import_node_path76.default.join(serviceDir, marker))) return true;
22826
+ }
22827
+ return false;
22828
+ }
22829
+ async function isRailsApp(serviceDir, gemfile) {
22830
+ if (gemfile && /^\s*gem\s+['"]rails['"]/m.test(gemfile)) return true;
22831
+ for (const marker of ["config/application.rb", "config/environment.rb", "bin/rails"]) {
22832
+ if (await exists6(import_node_path76.default.join(serviceDir, marker))) return true;
22833
+ }
22834
+ return false;
22835
+ }
22836
+ function gemPresent(gemfile, name) {
22837
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
22838
+ return new RegExp(`^\\s*gem\\s+['"]${escaped}['"]`, "m").test(gemfile);
22839
+ }
22840
+ async function readGemfile(serviceDir) {
22841
+ const file = import_node_path76.default.join(serviceDir, "Gemfile");
22842
+ if (!await exists6(file)) return null;
22843
+ return { file, body: await import_node_fs42.promises.readFile(file, "utf8") };
22844
+ }
22845
+ async function plan4(serviceDir, opts) {
22846
+ const empty = {
22847
+ language: "ruby",
22848
+ serviceDir,
22849
+ dependencyEdits: [],
22850
+ entrypointEdits: [],
22851
+ envEdits: []
22852
+ };
22853
+ const gemfile = await readGemfile(serviceDir);
22854
+ const dependencyEdits = [];
22855
+ if (gemfile) {
22856
+ for (const gem of RUBY_GEMS) {
22857
+ if (!gemPresent(gemfile.body, gem.name)) {
22858
+ dependencyEdits.push({ file: gemfile.file, kind: "add", name: gem.name, version: gem.version });
22859
+ }
22860
+ }
22861
+ }
22862
+ const rails = await isRailsApp(serviceDir, gemfile?.body ?? null);
22863
+ const initializer = import_node_path76.default.join(serviceDir, INITIALIZER_REL);
22864
+ const generatedFiles = [];
22865
+ if (rails && !await exists6(initializer)) {
22866
+ generatedFiles.push({
22867
+ file: initializer,
22868
+ contents: neatOtelRb({ project: opts?.project }),
22869
+ skipIfExists: true
22870
+ });
22871
+ }
22872
+ if (dependencyEdits.length === 0 && generatedFiles.length === 0) {
22873
+ return empty;
22874
+ }
22875
+ const envEdits = [
22876
+ { file: null, key: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://localhost:4318" }
22877
+ ];
22878
+ return {
22879
+ language: "ruby",
22880
+ serviceDir,
22881
+ dependencyEdits,
22882
+ entrypointEdits: [],
22883
+ envEdits,
22884
+ ...generatedFiles.length > 0 ? { generatedFiles } : {}
22885
+ };
22886
+ }
22887
+ async function writeFileAtomic2(file, contents) {
22888
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
22889
+ await import_node_fs42.promises.writeFile(tmp, contents, "utf8");
22890
+ await import_node_fs42.promises.rename(tmp, file);
22891
+ }
22892
+ async function applyGemfile(file, edits, original) {
22893
+ const lines = edits.filter((e) => e.kind === "add").map((e) => `gem '${e.name}', '${e.version}'`);
22894
+ const banner = `
22895
+ # ${NEAT_OTEL_STAMP2} \u2014 OpenTelemetry gems added by NEAT
22896
+ `;
22897
+ const trailing = original.endsWith("\n") ? "" : "\n";
22898
+ await writeFileAtomic2(file, `${original}${trailing}${banner}${lines.join("\n")}
22899
+ `);
22900
+ }
22901
+ async function rollback3(serviceDir, language, originals, created) {
22902
+ const restored = [];
22903
+ for (const [file, raw] of originals.entries()) {
22904
+ try {
22905
+ await import_node_fs42.promises.writeFile(file, raw, "utf8");
22906
+ restored.push(file);
22907
+ } catch {
22908
+ }
22909
+ }
22910
+ const removed = [];
22911
+ for (const file of created) {
22912
+ try {
22913
+ await import_node_fs42.promises.rm(file, { force: true });
22914
+ removed.push(file);
22915
+ } catch {
22916
+ }
22917
+ }
22918
+ const body = [
22919
+ "# neat-rollback.patch",
22920
+ "",
22921
+ `# Generated after a partial apply failure in the ${language} installer.`,
22922
+ "# Files listed below were restored to their pre-apply contents.",
22923
+ "",
22924
+ ...restored.map((f) => `restored: ${f}`),
22925
+ ...removed.map((f) => `removed: ${f}`),
22926
+ ""
22927
+ ];
22928
+ await import_node_fs42.promises.writeFile(import_node_path76.default.join(serviceDir, "neat-rollback.patch"), body.join("\n"), "utf8");
22929
+ }
22930
+ async function apply4(installPlan) {
22931
+ const { serviceDir } = installPlan;
22932
+ const generatedFiles = installPlan.generatedFiles ?? [];
22933
+ const manifests = new Set(installPlan.dependencyEdits.map((e) => e.file));
22934
+ if (manifests.size === 0 && generatedFiles.length === 0) {
22935
+ return { serviceDir, outcome: "already-instrumented", writtenFiles: [] };
22936
+ }
22937
+ const originals = /* @__PURE__ */ new Map();
22938
+ for (const file of manifests) {
22939
+ try {
22940
+ originals.set(file, await import_node_fs42.promises.readFile(file, "utf8"));
22941
+ } catch {
22942
+ }
22943
+ }
22944
+ const writtenFiles = [];
22945
+ const created = [];
22946
+ try {
22947
+ for (const gf of generatedFiles) {
22948
+ if (await exists6(gf.file)) continue;
22949
+ await import_node_fs42.promises.mkdir(import_node_path76.default.dirname(gf.file), { recursive: true });
22950
+ await writeFileAtomic2(gf.file, gf.contents);
22951
+ writtenFiles.push(gf.file);
22952
+ created.push(gf.file);
22953
+ }
22954
+ for (const file of manifests) {
22955
+ const raw = originals.get(file);
22956
+ if (raw === void 0) throw new Error(`ruby installer: cannot read ${file} during apply`);
22957
+ const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
22958
+ if (edits.length > 0) {
22959
+ await applyGemfile(file, edits, raw);
22960
+ writtenFiles.push(file);
22961
+ }
22962
+ }
22963
+ } catch (err) {
22964
+ await rollback3(serviceDir, installPlan.language, originals, created);
22965
+ throw err;
22966
+ }
22967
+ const wroteManifest = writtenFiles.some((f) => import_node_path76.default.basename(f) === "Gemfile");
22968
+ return {
22969
+ serviceDir,
22970
+ outcome: writtenFiles.length > 0 ? "instrumented" : "already-instrumented",
22971
+ writtenFiles,
22972
+ ...wroteManifest ? { followUpInstall: "bundle install" } : {}
22973
+ };
22974
+ }
22975
+ var rubyInstaller = { name: "ruby", detect: detect4, plan: plan4, apply: apply4 };
22976
+
22977
+ // src/installers/php.ts
22978
+ init_cjs_shims();
22979
+ var import_node_fs43 = require("fs");
22980
+ var import_node_path77 = __toESM(require("path"), 1);
22981
+ var PHP_MARKERS = [
22982
+ "composer.json",
22983
+ "composer.lock"
22984
+ ];
22985
+ var NEAT_OTEL_FILENAME2 = "neat_otel.php";
22986
+ var NEAT_OTEL_STAMP3 = "neat-otel-init v1";
22987
+ var LARAVEL_PACKAGE = "open-telemetry/opentelemetry-auto-laravel";
22988
+ var PHP_PACKAGES = [
22989
+ { name: "open-telemetry/sdk", version: "^1.0" },
22990
+ { name: "open-telemetry/exporter-otlp", version: "^1.0" },
22991
+ { name: "php-http/guzzle7-adapter", version: "^1.0" },
22992
+ { name: LARAVEL_PACKAGE, version: "^0.1" }
22993
+ ];
22994
+ var PHP_PECL_CAVEAT = "PHP auto-instrumentation requires the `opentelemetry` PECL extension (`pecl install opentelemetry`, then `extension=opentelemetry.so` in php.ini). NEAT cannot install a PECL extension via composer \u2014 until it is loaded no spans are produced. See neat_otel.php and ADR-186.";
22995
+ function neatOtelPhp(opts = {}) {
22996
+ const service = opts.project ?? "php-service";
22997
+ const endpoint2 = opts.project ? `http://localhost:4318/projects/${opts.project}/v1/traces` : "http://localhost:4318/v1/traces";
22998
+ return `<?php
22999
+ // ${NEAT_OTEL_STAMP3} \u2014 generated by NEAT. Safe to re-generate; do not edit.
23000
+ //
23001
+ // REQUIRED SYSTEM STEP \u2014 NEAT CANNOT DO THIS FOR YOU:
23002
+ // PHP OpenTelemetry auto-instrumentation needs the \`opentelemetry\` PECL
23003
+ // extension, a system-level install composer cannot provide:
23004
+ // pecl install opentelemetry
23005
+ // then enable it in your php.ini:
23006
+ // extension=opentelemetry.so
23007
+ // Verify with \`php -m | grep opentelemetry\`. Until the extension is loaded
23008
+ // the Laravel auto-instrumentation hooks never fire and no spans are emitted.
23009
+ //
23010
+ // Wire this file so it runs before the framework boots \u2014 either set
23011
+ // auto_prepend_file = /absolute/path/to/neat_otel.php
23012
+ // in php.ini / .user.ini, or require it at the very top of public/index.php and
23013
+ // artisan. It points the exporter at your NEAT daemon and turns on the SDK
23014
+ // autoloader; the auto-laravel instrumentation then produces route, DB, cache,
23015
+ // and queue spans that fuse onto your extracted routes and Eloquent tables.
23016
+ //
23017
+ // FILE-GRAIN (code.file.path call-site attribution) is a documented follow-up
23018
+ // for PHP \u2014 see ADR-186. Route, table, and service grain land now.
23019
+
23020
+ declare(strict_types=1);
23021
+
23022
+ // Degrade to a no-op when the extension isn't present, so a bare app still
23023
+ // boots (never break the host application).
23024
+ if (!extension_loaded('opentelemetry')) {
23025
+ return;
23026
+ }
23027
+
23028
+ // Point the exporter at NEAT unless the operator already set these. The
23029
+ // endpoint is NEAT's project-scoped traces path (ADR-183).
23030
+ $neat_defaults = [
23031
+ 'OTEL_PHP_AUTOLOAD_ENABLED' => 'true',
23032
+ 'OTEL_SERVICE_NAME' => '${service}',
23033
+ 'OTEL_TRACES_EXPORTER' => 'otlp',
23034
+ 'OTEL_EXPORTER_OTLP_PROTOCOL' => 'http/json',
23035
+ 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT' => '${endpoint2}',
23036
+ 'OTEL_PROPAGATORS' => 'baggage,tracecontext',
23037
+ ];
23038
+ foreach ($neat_defaults as $neat_key => $neat_value) {
23039
+ if (getenv($neat_key) === false && !isset($_SERVER[$neat_key]) && !isset($_ENV[$neat_key])) {
23040
+ putenv($neat_key . '=' . $neat_value);
23041
+ $_SERVER[$neat_key] = $neat_value;
23042
+ $_ENV[$neat_key] = $neat_value;
23043
+ }
23044
+ }
23045
+ `;
23046
+ }
23047
+ async function exists7(p) {
23048
+ try {
23049
+ await import_node_fs43.promises.stat(p);
23050
+ return true;
23051
+ } catch {
23052
+ return false;
23053
+ }
23054
+ }
23055
+ async function detect5(serviceDir) {
23056
+ for (const marker of PHP_MARKERS) {
23057
+ if (await exists7(import_node_path77.default.join(serviceDir, marker))) return true;
23058
+ }
23059
+ return false;
23060
+ }
23061
+ function readComposerObject(body) {
23062
+ let parsed = null;
23063
+ try {
23064
+ parsed = JSON.parse(body);
23065
+ } catch {
23066
+ parsed = null;
23067
+ }
23068
+ const obj = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
23069
+ const require2 = obj.require && typeof obj.require === "object" && !Array.isArray(obj.require) ? obj.require : {};
23070
+ const requireDev = obj["require-dev"] && typeof obj["require-dev"] === "object" && !Array.isArray(obj["require-dev"]) ? obj["require-dev"] : {};
23071
+ return { require: require2, requireDev };
23072
+ }
23073
+ async function plan5(serviceDir, opts) {
23074
+ const empty = {
23075
+ language: "php",
23076
+ serviceDir,
23077
+ dependencyEdits: [],
23078
+ entrypointEdits: [],
23079
+ envEdits: []
23080
+ };
23081
+ const composerPath = import_node_path77.default.join(serviceDir, "composer.json");
23082
+ const hasComposer = await exists7(composerPath);
23083
+ const dependencyEdits = [];
23084
+ if (hasComposer) {
23085
+ const body = await import_node_fs43.promises.readFile(composerPath, "utf8");
23086
+ const { require: require2, requireDev } = readComposerObject(body);
23087
+ const laravel = "laravel/framework" in require2 || "laravel/framework" in requireDev || await exists7(import_node_path77.default.join(serviceDir, "artisan"));
23088
+ const wanted = laravel ? PHP_PACKAGES : PHP_PACKAGES.filter((p) => p.name !== LARAVEL_PACKAGE);
23089
+ for (const pkg of wanted) {
23090
+ if (!(pkg.name in require2)) {
23091
+ dependencyEdits.push({ file: composerPath, kind: "add", name: pkg.name, version: pkg.version });
23092
+ }
23093
+ }
23094
+ }
23095
+ const bootstrap = import_node_path77.default.join(serviceDir, NEAT_OTEL_FILENAME2);
23096
+ const generatedFiles = [];
23097
+ if (hasComposer && !await exists7(bootstrap)) {
23098
+ generatedFiles.push({ file: bootstrap, contents: neatOtelPhp({ project: opts?.project }), skipIfExists: true });
23099
+ }
23100
+ if (dependencyEdits.length === 0 && generatedFiles.length === 0) {
23101
+ return empty;
23102
+ }
23103
+ const envEdits = [
23104
+ { file: null, key: "OTEL_PHP_AUTOLOAD_ENABLED", value: "true" },
23105
+ { file: null, key: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://localhost:4318" }
23106
+ ];
23107
+ return {
23108
+ language: "php",
23109
+ serviceDir,
23110
+ dependencyEdits,
23111
+ entrypointEdits: [],
23112
+ envEdits,
23113
+ ...generatedFiles.length > 0 ? { generatedFiles } : {}
23114
+ };
23115
+ }
23116
+ async function writeFileAtomic3(file, contents) {
23117
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
23118
+ await import_node_fs43.promises.writeFile(tmp, contents, "utf8");
23119
+ await import_node_fs43.promises.rename(tmp, file);
23120
+ }
23121
+ async function applyComposerJson(file, edits, original) {
23122
+ let parsed;
23123
+ try {
23124
+ parsed = JSON.parse(original);
23125
+ } catch {
23126
+ throw new Error(`php installer: composer.json at ${file} is not valid JSON`);
23127
+ }
23128
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
23129
+ throw new Error(`php installer: composer.json at ${file} is not a JSON object`);
23130
+ }
23131
+ const obj = parsed;
23132
+ const require2 = obj.require && typeof obj.require === "object" && !Array.isArray(obj.require) ? obj.require : {};
23133
+ for (const e of edits) {
23134
+ if (e.kind !== "add") continue;
23135
+ if (!(e.name in require2)) require2[e.name] = e.version;
23136
+ }
23137
+ obj.require = require2;
23138
+ await writeFileAtomic3(file, JSON.stringify(obj, null, 2) + "\n");
23139
+ }
23140
+ async function rollback4(serviceDir, language, originals, created) {
23141
+ const restored = [];
23142
+ for (const [file, raw] of originals.entries()) {
23143
+ try {
23144
+ await import_node_fs43.promises.writeFile(file, raw, "utf8");
23145
+ restored.push(file);
23146
+ } catch {
23147
+ }
23148
+ }
23149
+ const removed = [];
23150
+ for (const file of created) {
23151
+ try {
23152
+ await import_node_fs43.promises.rm(file, { force: true });
23153
+ removed.push(file);
23154
+ } catch {
23155
+ }
23156
+ }
23157
+ const body = [
23158
+ "# neat-rollback.patch",
23159
+ "",
23160
+ `# Generated after a partial apply failure in the ${language} installer.`,
23161
+ "# Files listed below were restored to their pre-apply contents.",
23162
+ "",
23163
+ ...restored.map((f) => `restored: ${f}`),
23164
+ ...removed.map((f) => `removed: ${f}`),
23165
+ ""
23166
+ ];
23167
+ await import_node_fs43.promises.writeFile(import_node_path77.default.join(serviceDir, "neat-rollback.patch"), body.join("\n"), "utf8");
23168
+ }
23169
+ async function apply5(installPlan) {
23170
+ const { serviceDir } = installPlan;
23171
+ const generatedFiles = installPlan.generatedFiles ?? [];
23172
+ const manifests = new Set(installPlan.dependencyEdits.map((e) => e.file));
23173
+ if (manifests.size === 0 && generatedFiles.length === 0) {
23174
+ return { serviceDir, outcome: "already-instrumented", writtenFiles: [], reason: PHP_PECL_CAVEAT };
23175
+ }
23176
+ const originals = /* @__PURE__ */ new Map();
23177
+ for (const file of manifests) {
23178
+ try {
23179
+ originals.set(file, await import_node_fs43.promises.readFile(file, "utf8"));
23180
+ } catch {
23181
+ }
23182
+ }
23183
+ const writtenFiles = [];
23184
+ const created = [];
23185
+ try {
23186
+ for (const gf of generatedFiles) {
23187
+ if (await exists7(gf.file)) continue;
23188
+ await import_node_fs43.promises.mkdir(import_node_path77.default.dirname(gf.file), { recursive: true });
23189
+ await writeFileAtomic3(gf.file, gf.contents);
23190
+ writtenFiles.push(gf.file);
23191
+ created.push(gf.file);
23192
+ }
23193
+ for (const file of manifests) {
23194
+ const raw = originals.get(file);
23195
+ if (raw === void 0) throw new Error(`php installer: cannot read ${file} during apply`);
23196
+ const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
23197
+ if (edits.length > 0) {
23198
+ await applyComposerJson(file, edits, raw);
23199
+ writtenFiles.push(file);
23200
+ }
23201
+ }
23202
+ } catch (err) {
23203
+ await rollback4(serviceDir, installPlan.language, originals, created);
23204
+ throw err;
23205
+ }
23206
+ if (writtenFiles.length > 0) {
23207
+ console.warn(`neat: PHP instrumentation staged in ${import_node_path77.default.basename(serviceDir)}, but a system step remains:
23208
+ ${PHP_PECL_CAVEAT}`);
23209
+ }
23210
+ const wroteManifest = writtenFiles.some((f) => import_node_path77.default.basename(f) === "composer.json");
23211
+ return {
23212
+ serviceDir,
23213
+ outcome: writtenFiles.length > 0 ? "instrumented" : "already-instrumented",
23214
+ writtenFiles,
23215
+ reason: PHP_PECL_CAVEAT,
23216
+ ...wroteManifest ? { followUpInstall: "composer install" } : {}
23217
+ };
23218
+ }
23219
+ var phpInstaller = { name: "php", detect: detect5, plan: plan5, apply: apply5 };
23220
+
22162
23221
  // src/installers/shared.ts
22163
23222
  init_cjs_shims();
22164
- function isEmptyPlan(plan4) {
22165
- return plan4.dependencyEdits.length === 0 && plan4.entrypointEdits.length === 0 && plan4.envEdits.length === 0 && (plan4.generatedFiles?.length ?? 0) === 0 && plan4.nextConfigEdit === void 0;
23223
+ function isEmptyPlan(plan6) {
23224
+ return plan6.dependencyEdits.length === 0 && plan6.entrypointEdits.length === 0 && plan6.envEdits.length === 0 && (plan6.generatedFiles?.length ?? 0) === 0 && plan6.nextConfigEdit === void 0;
22166
23225
  }
22167
23226
 
22168
23227
  // src/installers/index.ts
@@ -22174,9 +23233,16 @@ var FORBIDDEN_LOCKFILES = /* @__PURE__ */ new Set([
22174
23233
  "Pipfile.lock",
22175
23234
  "Gemfile.lock",
22176
23235
  "Cargo.lock",
22177
- "go.sum"
23236
+ "go.sum",
23237
+ "composer.lock"
22178
23238
  ]);
22179
- var INSTALLERS = [javascriptInstaller, pythonInstaller, goInstaller];
23239
+ var INSTALLERS = [
23240
+ javascriptInstaller,
23241
+ pythonInstaller,
23242
+ goInstaller,
23243
+ rubyInstaller,
23244
+ phpInstaller
23245
+ ];
22180
23246
  async function pickInstaller(serviceDir) {
22181
23247
  for (const inst of INSTALLERS) {
22182
23248
  if (await inst.detect(serviceDir)) return inst;
@@ -22191,7 +23257,7 @@ function renderPatch(sections) {
22191
23257
  "No SDK installers matched the discovered services. Two reasons this",
22192
23258
  "normally happens:",
22193
23259
  " - the project uses a language NEAT does not yet instrument",
22194
- " (Java / Ruby / .NET / Rust are out of MVP scope per ADR-047);",
23260
+ " (Java / .NET / Rust are out of scope per ADR-047);",
22195
23261
  " - the SDK is already installed, so the installer returned an empty",
22196
23262
  " plan.",
22197
23263
  "",
@@ -22201,22 +23267,22 @@ function renderPatch(sections) {
22201
23267
  }
22202
23268
  const lines = ["# neat install plan", ""];
22203
23269
  for (const section of sections) {
22204
- const { installer, plan: plan4 } = section;
22205
- lines.push(`## ${installer} (${plan4.language}) \u2014 ${plan4.serviceDir}`);
23270
+ const { installer, plan: plan6 } = section;
23271
+ lines.push(`## ${installer} (${plan6.language}) \u2014 ${plan6.serviceDir}`);
22206
23272
  lines.push("");
22207
- if (plan4.libOnly) {
23273
+ if (plan6.libOnly) {
22208
23274
  lines.push("### skipped \u2014 no resolvable entry point (lib-only)");
22209
23275
  lines.push("");
22210
23276
  continue;
22211
23277
  }
22212
- if (plan4.entryFile) {
22213
- lines.push(`entry: ${plan4.entryFile}`);
23278
+ if (plan6.entryFile) {
23279
+ lines.push(`entry: ${plan6.entryFile}`);
22214
23280
  lines.push("");
22215
23281
  }
22216
- if (plan4.dependencyEdits.length > 0) {
23282
+ if (plan6.dependencyEdits.length > 0) {
22217
23283
  lines.push("### dependencies");
22218
23284
  const byFile = /* @__PURE__ */ new Map();
22219
- for (const dep of plan4.dependencyEdits) {
23285
+ for (const dep of plan6.dependencyEdits) {
22220
23286
  const base = dep.file.split(/[\\/]/).pop() ?? dep.file;
22221
23287
  if (FORBIDDEN_LOCKFILES.has(base)) {
22222
23288
  throw new Error(
@@ -22235,9 +23301,9 @@ function renderPatch(sections) {
22235
23301
  }
22236
23302
  lines.push("");
22237
23303
  }
22238
- if (plan4.generatedFiles && plan4.generatedFiles.length > 0) {
23304
+ if (plan6.generatedFiles && plan6.generatedFiles.length > 0) {
22239
23305
  lines.push("### generated files");
22240
- for (const gen of plan4.generatedFiles) {
23306
+ for (const gen of plan6.generatedFiles) {
22241
23307
  lines.push(`--- (new file) ${gen.file}`);
22242
23308
  for (const ln of gen.contents.split(/\r?\n/)) {
22243
23309
  lines.push(`+ ${ln}`);
@@ -22245,26 +23311,26 @@ function renderPatch(sections) {
22245
23311
  }
22246
23312
  lines.push("");
22247
23313
  }
22248
- if (plan4.entrypointEdits.length > 0) {
23314
+ if (plan6.entrypointEdits.length > 0) {
22249
23315
  lines.push("### entry-point injection");
22250
- for (const e of plan4.entrypointEdits) {
23316
+ for (const e of plan6.entrypointEdits) {
22251
23317
  lines.push(`--- ${e.file}`);
22252
23318
  lines.push(`+ ${e.after}`);
22253
23319
  lines.push(` ${e.before}`);
22254
23320
  }
22255
23321
  lines.push("");
22256
23322
  }
22257
- if (plan4.envEdits.length > 0) {
23323
+ if (plan6.envEdits.length > 0) {
22258
23324
  lines.push("### env (written to <package-dir>/.env.neat)");
22259
- for (const env of plan4.envEdits) {
23325
+ for (const env of plan6.envEdits) {
22260
23326
  lines.push(`- ${env.key}=${env.value}`);
22261
23327
  }
22262
23328
  lines.push("");
22263
23329
  }
22264
- if (plan4.nextConfigEdit) {
23330
+ if (plan6.nextConfigEdit) {
22265
23331
  lines.push("### next.config (framework flag)");
22266
- lines.push(`--- ${plan4.nextConfigEdit.file}`);
22267
- lines.push(`+ experimental: { instrumentationHook: true }, // ${plan4.nextConfigEdit.reason}`);
23332
+ lines.push(`--- ${plan6.nextConfigEdit.file}`);
23333
+ lines.push(`+ experimental: { instrumentationHook: true }, // ${plan6.nextConfigEdit.reason}`);
22268
23334
  lines.push("");
22269
23335
  }
22270
23336
  }
@@ -22273,10 +23339,10 @@ function renderPatch(sections) {
22273
23339
 
22274
23340
  // src/orchestrator.ts
22275
23341
  init_cjs_shims();
22276
- var import_node_fs42 = require("fs");
23342
+ var import_node_fs44 = require("fs");
22277
23343
  var import_node_http = __toESM(require("http"), 1);
22278
23344
  var import_node_net = __toESM(require("net"), 1);
22279
- var import_node_path76 = __toESM(require("path"), 1);
23345
+ var import_node_path78 = __toESM(require("path"), 1);
22280
23346
  var import_node_url4 = require("url");
22281
23347
  var import_node_child_process3 = require("child_process");
22282
23348
  var import_node_readline = __toESM(require("readline"), 1);
@@ -22286,7 +23352,7 @@ async function extractAndPersist(opts) {
22286
23352
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
22287
23353
  resetGraph(graphKey);
22288
23354
  const graph = getGraph(graphKey);
22289
- const projectPaths = pathsForProject(graphKey, import_node_path76.default.join(opts.scanPath, "neat-out"));
23355
+ const projectPaths = pathsForProject(graphKey, import_node_path78.default.join(opts.scanPath, "neat-out"));
22290
23356
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
22291
23357
  errorsPath: projectPaths.errorsPath
22292
23358
  });
@@ -22317,28 +23383,34 @@ async function applyInstallersOver(services, project, options = {}) {
22317
23383
  let cloudflareWorkers = 0;
22318
23384
  let electron = 0;
22319
23385
  const installPlans = /* @__PURE__ */ new Map();
23386
+ const dependencyInstructions = /* @__PURE__ */ new Map();
22320
23387
  for (const svc of services) {
22321
23388
  const installer = await pickInstaller(svc.dir);
22322
23389
  if (!installer) continue;
22323
- const plan4 = await installer.plan(svc.dir, { project });
22324
- if (isEmptyPlan(plan4) && !plan4.libOnly && plan4.runtimeKind === void 0) {
23390
+ const plan6 = await installer.plan(svc.dir, { project });
23391
+ if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) {
22325
23392
  already++;
22326
23393
  continue;
22327
23394
  }
22328
- const outcome = await installer.apply(plan4);
23395
+ const outcome = await installer.apply(plan6);
22329
23396
  if (outcome.outcome === "instrumented") {
22330
23397
  instrumented++;
22331
- if (plan4.dependencyEdits.length > 0) {
22332
- const cmd = await resolveManager(svc.dir);
22333
- const key = `${cmd.pm}:${cmd.cwd}`;
22334
- if (!installPlans.has(key)) installPlans.set(key, cmd);
23398
+ if (plan6.dependencyEdits.length > 0) {
23399
+ const manifest = import_node_path78.default.basename(plan6.dependencyEdits[0].file);
23400
+ if (manifest === "package.json") {
23401
+ const cmd = await resolveManager(svc.dir);
23402
+ const key = `${cmd.pm}:${cmd.cwd}`;
23403
+ if (!installPlans.has(key)) installPlans.set(key, cmd);
23404
+ } else if (outcome.followUpInstall) {
23405
+ dependencyInstructions.set(svc.dir, outcome.followUpInstall);
23406
+ }
22335
23407
  }
22336
23408
  } else if (outcome.outcome === "already-instrumented") already++;
22337
23409
  else if (outcome.outcome === "lib-only") {
22338
23410
  libOnly++;
22339
23411
  const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
22340
23412
  if (appDeps.length > 0) {
22341
- const svcName = import_node_path76.default.basename(svc.dir);
23413
+ const svcName = import_node_path78.default.basename(svc.dir);
22342
23414
  const list = appDeps.join(", ");
22343
23415
  console.warn(
22344
23416
  `neat: runtime layer won't engage for ${svcName}: no entry point found.
@@ -22351,7 +23423,7 @@ async function applyInstallersOver(services, project, options = {}) {
22351
23423
  console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
22352
23424
  } else if (outcome.outcome === "react-native") {
22353
23425
  reactNative++;
22354
- const svcName = import_node_path76.default.basename(svc.dir);
23426
+ const svcName = import_node_path78.default.basename(svc.dir);
22355
23427
  console.log(
22356
23428
  `neat: ${svc.dir} detected as React Native / Expo
22357
23429
  The installer doesn't cover this runtime deterministically.
@@ -22362,7 +23434,7 @@ async function applyInstallersOver(services, project, options = {}) {
22362
23434
  );
22363
23435
  } else if (outcome.outcome === "bun") {
22364
23436
  bun++;
22365
- const svcName = import_node_path76.default.basename(svc.dir);
23437
+ const svcName = import_node_path78.default.basename(svc.dir);
22366
23438
  console.log(
22367
23439
  `neat: ${svc.dir} detected as Bun
22368
23440
  The installer doesn't cover this runtime deterministically.
@@ -22373,7 +23445,7 @@ async function applyInstallersOver(services, project, options = {}) {
22373
23445
  );
22374
23446
  } else if (outcome.outcome === "deno") {
22375
23447
  deno++;
22376
- const svcName = import_node_path76.default.basename(svc.dir);
23448
+ const svcName = import_node_path78.default.basename(svc.dir);
22377
23449
  console.log(
22378
23450
  `neat: ${svc.dir} detected as Deno
22379
23451
  The installer doesn't cover this runtime deterministically.
@@ -22384,7 +23456,7 @@ async function applyInstallersOver(services, project, options = {}) {
22384
23456
  );
22385
23457
  } else if (outcome.outcome === "cloudflare-workers") {
22386
23458
  cloudflareWorkers++;
22387
- const svcName = import_node_path76.default.basename(svc.dir);
23459
+ const svcName = import_node_path78.default.basename(svc.dir);
22388
23460
  console.log(
22389
23461
  `neat: ${svc.dir} detected as Cloudflare Workers
22390
23462
  The installer doesn't cover this runtime deterministically.
@@ -22395,7 +23467,7 @@ async function applyInstallersOver(services, project, options = {}) {
22395
23467
  );
22396
23468
  } else if (outcome.outcome === "electron") {
22397
23469
  electron++;
22398
- const svcName = import_node_path76.default.basename(svc.dir);
23470
+ const svcName = import_node_path78.default.basename(svc.dir);
22399
23471
  console.log(
22400
23472
  `neat: ${svc.dir} detected as Electron
22401
23473
  The installer doesn't cover this runtime deterministically.
@@ -22408,7 +23480,7 @@ async function applyInstallersOver(services, project, options = {}) {
22408
23480
  if (svc.pkg && (outcome.outcome === "instrumented" || outcome.outcome === "already-instrumented")) {
22409
23481
  const gaps = uninstrumentedLibraries(svc.pkg);
22410
23482
  if (gaps.length > 0) {
22411
- const svcName = import_node_path76.default.basename(svc.dir);
23483
+ const svcName = import_node_path78.default.basename(svc.dir);
22412
23484
  const list = gaps.join(", ");
22413
23485
  const subject = gaps.length === 1 ? "this library" : "these libraries";
22414
23486
  const aux = gaps.length === 1 ? "isn't" : "aren't";
@@ -22436,6 +23508,11 @@ async function applyInstallersOver(services, project, options = {}) {
22436
23508
  }
22437
23509
  }
22438
23510
  }
23511
+ for (const [dir, command] of dependencyInstructions) {
23512
+ console.log(
23513
+ `neat: dependencies staged in ${dir}; run \`${command}\` to install them \u2014 NEAT does not run it for you.`
23514
+ );
23515
+ }
22439
23516
  return {
22440
23517
  instrumented,
22441
23518
  alreadyInstrumented: already,
@@ -22446,7 +23523,8 @@ async function applyInstallersOver(services, project, options = {}) {
22446
23523
  deno,
22447
23524
  cloudflareWorkers,
22448
23525
  electron,
22449
- packageManagerInstalls
23526
+ packageManagerInstalls,
23527
+ dependencyInstructions: [...dependencyInstructions].map(([dir, command]) => ({ dir, command }))
22450
23528
  };
22451
23529
  }
22452
23530
  async function promptYesNo(question) {
@@ -22614,24 +23692,24 @@ async function persistedPortsFor(scanPath) {
22614
23692
  return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
22615
23693
  }
22616
23694
  async function acquireSpawnLock(scanPath) {
22617
- const lockPath = import_node_path76.default.join(scanPath, "neat-out", "daemon.spawn.lock");
22618
- await import_node_fs42.promises.mkdir(import_node_path76.default.dirname(lockPath), { recursive: true });
23695
+ const lockPath = import_node_path78.default.join(scanPath, "neat-out", "daemon.spawn.lock");
23696
+ await import_node_fs44.promises.mkdir(import_node_path78.default.dirname(lockPath), { recursive: true });
22619
23697
  const STALE_LOCK_MS = 6e4;
22620
23698
  try {
22621
- const fd = await import_node_fs42.promises.open(lockPath, "wx");
23699
+ const fd = await import_node_fs44.promises.open(lockPath, "wx");
22622
23700
  await fd.writeFile(`${process.pid}
22623
23701
  `, "utf8");
22624
23702
  await fd.close();
22625
23703
  return async () => {
22626
- await import_node_fs42.promises.unlink(lockPath).catch(() => {
23704
+ await import_node_fs44.promises.unlink(lockPath).catch(() => {
22627
23705
  });
22628
23706
  };
22629
23707
  } catch (err) {
22630
23708
  if (err.code !== "EEXIST") return null;
22631
23709
  try {
22632
- const stat = await import_node_fs42.promises.stat(lockPath);
23710
+ const stat = await import_node_fs44.promises.stat(lockPath);
22633
23711
  if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
22634
- await import_node_fs42.promises.unlink(lockPath).catch(() => {
23712
+ await import_node_fs44.promises.unlink(lockPath).catch(() => {
22635
23713
  });
22636
23714
  return acquireSpawnLock(scanPath);
22637
23715
  }
@@ -22660,13 +23738,13 @@ async function healthIsForProject(restPort, project) {
22660
23738
  return false;
22661
23739
  }
22662
23740
  function daemonLogPath(projectPath3) {
22663
- return import_node_path76.default.join(projectPath3, "neat-out", "daemon.log");
23741
+ return import_node_path78.default.join(projectPath3, "neat-out", "daemon.log");
22664
23742
  }
22665
23743
  function spawnDaemonDetached(spec) {
22666
- const here = import_node_path76.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
23744
+ const here = import_node_path78.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
22667
23745
  const candidates = [
22668
- import_node_path76.default.join(here, "neatd.cjs"),
22669
- import_node_path76.default.join(here, "neatd.js")
23746
+ import_node_path78.default.join(here, "neatd.cjs"),
23747
+ import_node_path78.default.join(here, "neatd.js")
22670
23748
  ];
22671
23749
  let entry2 = null;
22672
23750
  const fsSync = require("fs");
@@ -22696,7 +23774,7 @@ function spawnDaemonDetached(spec) {
22696
23774
  let logFd = null;
22697
23775
  if (spec) {
22698
23776
  const logPath = daemonLogPath(spec.projectPath);
22699
- fsSync.mkdirSync(import_node_path76.default.dirname(logPath), { recursive: true });
23777
+ fsSync.mkdirSync(import_node_path78.default.dirname(logPath), { recursive: true });
22700
23778
  logFd = fsSync.openSync(logPath, "a");
22701
23779
  }
22702
23780
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entry2, "start"], {
@@ -22735,7 +23813,7 @@ async function runOrchestrator(opts) {
22735
23813
  browser: "skipped"
22736
23814
  }
22737
23815
  };
22738
- const stat = await import_node_fs42.promises.stat(opts.scanPath).catch(() => null);
23816
+ const stat = await import_node_fs44.promises.stat(opts.scanPath).catch(() => null);
22739
23817
  if (!stat || !stat.isDirectory()) {
22740
23818
  console.error(`neat: ${opts.scanPath} is not a directory`);
22741
23819
  result.exitCode = 2;
@@ -22895,7 +23973,7 @@ async function runOrchestrator(opts) {
22895
23973
  result.steps.browser = openBrowser(dashboardUrl);
22896
23974
  }
22897
23975
  const daemonRunning = result.steps.daemon === "spawned" || result.steps.daemon === "already-running";
22898
- const daemonLog = daemonRunning ? import_node_path76.default.relative(opts.scanPath, daemonLogPath(opts.scanPath)) : null;
23976
+ const daemonLog = daemonRunning ? import_node_path78.default.relative(opts.scanPath, daemonLogPath(opts.scanPath)) : null;
22899
23977
  printSummary(result, graph, dashboardUrl, daemonLog);
22900
23978
  return result;
22901
23979
  }
@@ -23354,27 +24432,27 @@ async function runConnectorCommand(rawArgs, deps = {}) {
23354
24432
 
23355
24433
  // src/hooks-cli.ts
23356
24434
  init_cjs_shims();
23357
- var import_node_path77 = __toESM(require("path"), 1);
24435
+ var import_node_path79 = __toESM(require("path"), 1);
23358
24436
  var import_node_os5 = __toESM(require("os"), 1);
23359
- var import_node_fs43 = require("fs");
24437
+ var import_node_fs45 = require("fs");
23360
24438
  var import_node_url5 = require("url");
23361
24439
  var HOOK_FILENAME = "neat-search-nudge.mjs";
23362
24440
  var GUIDE_FILENAME = "GRAPH_FIRST.md";
23363
24441
  var GUIDE_INSTALL_NAME = "neat-graph-first.md";
23364
24442
  var HOOK_MATCHER = "Grep|Glob|Bash";
23365
24443
  function moduleDir() {
23366
- return typeof __dirname !== "undefined" ? __dirname : import_node_path77.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
24444
+ return typeof __dirname !== "undefined" ? __dirname : import_node_path79.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
23367
24445
  }
23368
24446
  async function readSkillAsset(rel) {
23369
24447
  const here = moduleDir();
23370
24448
  const candidates = [
23371
- import_node_path77.default.resolve(here, "../../claude-skill", rel),
23372
- import_node_path77.default.resolve(here, "../../../claude-skill", rel),
23373
- import_node_path77.default.resolve(here, "../claude-skill", rel)
24449
+ import_node_path79.default.resolve(here, "../../claude-skill", rel),
24450
+ import_node_path79.default.resolve(here, "../../../claude-skill", rel),
24451
+ import_node_path79.default.resolve(here, "../claude-skill", rel)
23374
24452
  ];
23375
24453
  for (const candidate of candidates) {
23376
24454
  try {
23377
- return await import_node_fs43.promises.readFile(candidate, "utf8");
24455
+ return await import_node_fs45.promises.readFile(candidate, "utf8");
23378
24456
  } catch {
23379
24457
  }
23380
24458
  }
@@ -23384,17 +24462,17 @@ async function readSkillAsset(rel) {
23384
24462
  }
23385
24463
  function neatHome3() {
23386
24464
  const override = process.env.NEAT_HOME;
23387
- if (override && override.length > 0) return import_node_path77.default.resolve(override);
23388
- return import_node_path77.default.join(import_node_os5.default.homedir(), ".neat");
24465
+ if (override && override.length > 0) return import_node_path79.default.resolve(override);
24466
+ return import_node_path79.default.join(import_node_os5.default.homedir(), ".neat");
23389
24467
  }
23390
24468
  function claudeSettingsPath() {
23391
24469
  const override = process.env.NEAT_CLAUDE_SETTINGS;
23392
- if (override && override.length > 0) return import_node_path77.default.resolve(override);
24470
+ if (override && override.length > 0) return import_node_path79.default.resolve(override);
23393
24471
  const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os5.default.homedir();
23394
- return import_node_path77.default.join(home, ".claude", "settings.json");
24472
+ return import_node_path79.default.join(home, ".claude", "settings.json");
23395
24473
  }
23396
24474
  function installedHookPath() {
23397
- return import_node_path77.default.join(neatHome3(), "hooks", HOOK_FILENAME);
24475
+ return import_node_path79.default.join(neatHome3(), "hooks", HOOK_FILENAME);
23398
24476
  }
23399
24477
  function isNeatSearchEntry(entry2) {
23400
24478
  return (entry2.hooks ?? []).some(
@@ -23427,14 +24505,14 @@ async function runHooks(opts) {
23427
24505
  const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
23428
24506
  const guide = await readSkillAsset(GUIDE_FILENAME);
23429
24507
  const scriptPath = installedHookPath();
23430
- await import_node_fs43.promises.mkdir(import_node_path77.default.dirname(scriptPath), { recursive: true });
23431
- await import_node_fs43.promises.writeFile(scriptPath, hookScript, { mode: 493 });
23432
- const guidePath = import_node_path77.default.join(neatHome3(), GUIDE_INSTALL_NAME);
23433
- await import_node_fs43.promises.writeFile(guidePath, guide, "utf8");
24508
+ await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(scriptPath), { recursive: true });
24509
+ await import_node_fs45.promises.writeFile(scriptPath, hookScript, { mode: 493 });
24510
+ const guidePath = import_node_path79.default.join(neatHome3(), GUIDE_INSTALL_NAME);
24511
+ await import_node_fs45.promises.writeFile(guidePath, guide, "utf8");
23434
24512
  const settingsFile = claudeSettingsPath();
23435
24513
  let settings = {};
23436
24514
  try {
23437
- settings = JSON.parse(await import_node_fs43.promises.readFile(settingsFile, "utf8"));
24515
+ settings = JSON.parse(await import_node_fs45.promises.readFile(settingsFile, "utf8"));
23438
24516
  } catch (err) {
23439
24517
  if (err.code !== "ENOENT") {
23440
24518
  console.error(
@@ -23456,8 +24534,8 @@ async function runHooks(opts) {
23456
24534
  ...settings,
23457
24535
  hooks: { ...hooks, PreToolUse: preToolUse }
23458
24536
  };
23459
- await import_node_fs43.promises.mkdir(import_node_path77.default.dirname(settingsFile), { recursive: true });
23460
- await import_node_fs43.promises.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
24537
+ await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(settingsFile), { recursive: true });
24538
+ await import_node_fs45.promises.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
23461
24539
  console.log(`neat hooks: installed the search-nudge hook`);
23462
24540
  console.log(` script: ${scriptPath}`);
23463
24541
  console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
@@ -23528,9 +24606,9 @@ async function runHooksCommand(args) {
23528
24606
 
23529
24607
  // src/codex-cli.ts
23530
24608
  init_cjs_shims();
23531
- var import_node_path78 = __toESM(require("path"), 1);
24609
+ var import_node_path80 = __toESM(require("path"), 1);
23532
24610
  var import_node_os6 = __toESM(require("os"), 1);
23533
- var import_node_fs44 = require("fs");
24611
+ var import_node_fs46 = require("fs");
23534
24612
  var import_node_util = require("util");
23535
24613
  var import_smol_toml5 = require("smol-toml");
23536
24614
  var CODEX_MCP_SERVER = {
@@ -23548,14 +24626,14 @@ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
23548
24626
  var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
23549
24627
  function codexConfigPath() {
23550
24628
  const override = process.env.NEAT_CODEX_CONFIG;
23551
- if (override && override.length > 0) return import_node_path78.default.resolve(override);
24629
+ if (override && override.length > 0) return import_node_path80.default.resolve(override);
23552
24630
  const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os6.default.homedir();
23553
- return import_node_path78.default.join(home, ".codex", "config.toml");
24631
+ return import_node_path80.default.join(home, ".codex", "config.toml");
23554
24632
  }
23555
24633
  function agentsFilePath() {
23556
24634
  const override = process.env.NEAT_CODEX_AGENTS;
23557
- if (override && override.length > 0) return import_node_path78.default.resolve(override);
23558
- return import_node_path78.default.join(process.cwd(), "AGENTS.md");
24635
+ if (override && override.length > 0) return import_node_path80.default.resolve(override);
24636
+ return import_node_path80.default.join(process.cwd(), "AGENTS.md");
23559
24637
  }
23560
24638
  function isTableHeader(line) {
23561
24639
  return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
@@ -23689,7 +24767,7 @@ async function runCodex(opts) {
23689
24767
  const agentsPath = agentsFilePath();
23690
24768
  let configRaw = "";
23691
24769
  try {
23692
- configRaw = await import_node_fs44.promises.readFile(configPath, "utf8");
24770
+ configRaw = await import_node_fs46.promises.readFile(configPath, "utf8");
23693
24771
  } catch (err) {
23694
24772
  if (err.code !== "ENOENT") {
23695
24773
  console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
@@ -23698,7 +24776,7 @@ async function runCodex(opts) {
23698
24776
  }
23699
24777
  let agentsRaw = "";
23700
24778
  try {
23701
- agentsRaw = await import_node_fs44.promises.readFile(agentsPath, "utf8");
24779
+ agentsRaw = await import_node_fs46.promises.readFile(agentsPath, "utf8");
23702
24780
  } catch (err) {
23703
24781
  if (err.code !== "ENOENT") {
23704
24782
  console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
@@ -23738,15 +24816,15 @@ async function runCodex(opts) {
23738
24816
  return { exitCode: 0 };
23739
24817
  }
23740
24818
  if (config.changed) {
23741
- await import_node_fs44.promises.mkdir(import_node_path78.default.dirname(configPath), { recursive: true });
23742
- await import_node_fs44.promises.writeFile(configPath, config.text, "utf8");
24819
+ await import_node_fs46.promises.mkdir(import_node_path80.default.dirname(configPath), { recursive: true });
24820
+ await import_node_fs46.promises.writeFile(configPath, config.text, "utf8");
23743
24821
  console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
23744
24822
  } else {
23745
24823
  console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
23746
24824
  }
23747
24825
  if (agents.changed) {
23748
- await import_node_fs44.promises.mkdir(import_node_path78.default.dirname(agentsPath), { recursive: true });
23749
- await import_node_fs44.promises.writeFile(agentsPath, agents.text, "utf8");
24826
+ await import_node_fs46.promises.mkdir(import_node_path80.default.dirname(agentsPath), { recursive: true });
24827
+ await import_node_fs46.promises.writeFile(agentsPath, agents.text, "utf8");
23750
24828
  console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
23751
24829
  } else {
23752
24830
  console.log(`neat codex: ${agentsPath} already has the graph-first block`);
@@ -23803,9 +24881,9 @@ async function runCodexCommand(args) {
23803
24881
 
23804
24882
  // src/editors-cli.ts
23805
24883
  init_cjs_shims();
23806
- var import_node_path79 = __toESM(require("path"), 1);
24884
+ var import_node_path81 = __toESM(require("path"), 1);
23807
24885
  var import_node_os7 = __toESM(require("os"), 1);
23808
- var import_node_fs45 = require("fs");
24886
+ var import_node_fs47 = require("fs");
23809
24887
  var import_node_util2 = require("util");
23810
24888
  var jsonc = __toESM(require("jsonc-parser"), 1);
23811
24889
  var NEAT_MCP_SERVER = {
@@ -23829,17 +24907,17 @@ function homeDir() {
23829
24907
  }
23830
24908
  function xdgConfigDir() {
23831
24909
  const xdg = process.env.XDG_CONFIG_HOME;
23832
- return xdg && xdg.length > 0 ? import_node_path79.default.resolve(xdg) : import_node_path79.default.join(homeDir(), ".config");
24910
+ return xdg && xdg.length > 0 ? import_node_path81.default.resolve(xdg) : import_node_path81.default.join(homeDir(), ".config");
23833
24911
  }
23834
24912
  function envOverride(name) {
23835
24913
  const v = process.env[name];
23836
- return v && v.length > 0 ? import_node_path79.default.resolve(v) : void 0;
24914
+ return v && v.length > 0 ? import_node_path81.default.resolve(v) : void 0;
23837
24915
  }
23838
24916
  var CURSOR_CLIENT = {
23839
24917
  id: "cursor",
23840
24918
  label: "Cursor",
23841
24919
  docsUrl: "https://docs.cursor.com/context/mcp",
23842
- mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? import_node_path79.default.join(homeDir(), ".cursor", "mcp.json"),
24920
+ mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? import_node_path81.default.join(homeDir(), ".cursor", "mcp.json"),
23843
24921
  mcpContainerKey: "mcpServers",
23844
24922
  format: "json",
23845
24923
  // Cursor still reads a single `.cursorrules` at the project root (the modern
@@ -23851,7 +24929,7 @@ var DEVIN_CLIENT = {
23851
24929
  id: "devin",
23852
24930
  label: "Devin Desktop (Cascade)",
23853
24931
  docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
23854
- mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? import_node_path79.default.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
24932
+ mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? import_node_path81.default.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
23855
24933
  mcpContainerKey: "mcpServers",
23856
24934
  format: "json",
23857
24935
  rulesFileName: ".windsurfrules"
@@ -23860,7 +24938,7 @@ var GEMINI_CLIENT = {
23860
24938
  id: "gemini",
23861
24939
  label: "Gemini CLI",
23862
24940
  docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
23863
- mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? import_node_path79.default.join(homeDir(), ".gemini", "settings.json"),
24941
+ mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? import_node_path81.default.join(homeDir(), ".gemini", "settings.json"),
23864
24942
  mcpContainerKey: "mcpServers",
23865
24943
  format: "json",
23866
24944
  rulesFileName: "GEMINI.md"
@@ -23869,7 +24947,7 @@ var QWEN_CLIENT = {
23869
24947
  id: "qwen",
23870
24948
  label: "Qwen Code",
23871
24949
  docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
23872
- mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? import_node_path79.default.join(homeDir(), ".qwen", "settings.json"),
24950
+ mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? import_node_path81.default.join(homeDir(), ".qwen", "settings.json"),
23873
24951
  mcpContainerKey: "mcpServers",
23874
24952
  format: "json",
23875
24953
  rulesFileName: "QWEN.md"
@@ -23878,7 +24956,7 @@ var AMAZONQ_CLIENT = {
23878
24956
  id: "amazonq",
23879
24957
  label: "Amazon Q Developer CLI",
23880
24958
  docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
23881
- mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? import_node_path79.default.join(homeDir(), ".aws", "amazonq", "mcp.json"),
24959
+ mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? import_node_path81.default.join(homeDir(), ".aws", "amazonq", "mcp.json"),
23882
24960
  mcpContainerKey: "mcpServers",
23883
24961
  format: "json"
23884
24962
  };
@@ -23886,7 +24964,7 @@ var ROOCODE_CLIENT = {
23886
24964
  id: "roocode",
23887
24965
  label: "Roo Code",
23888
24966
  docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
23889
- mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? import_node_path79.default.join(process.cwd(), ".roo", "mcp.json"),
24967
+ mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? import_node_path81.default.join(process.cwd(), ".roo", "mcp.json"),
23890
24968
  mcpContainerKey: "mcpServers",
23891
24969
  format: "json"
23892
24970
  };
@@ -23899,9 +24977,9 @@ var ZED_CLIENT = {
23899
24977
  if (override) return override;
23900
24978
  if (process.platform === "win32") {
23901
24979
  const appData = process.env.APPDATA;
23902
- if (appData && appData.length > 0) return import_node_path79.default.join(appData, "Zed", "settings.json");
24980
+ if (appData && appData.length > 0) return import_node_path81.default.join(appData, "Zed", "settings.json");
23903
24981
  }
23904
- return import_node_path79.default.join(homeDir(), ".config", "zed", "settings.json");
24982
+ return import_node_path81.default.join(homeDir(), ".config", "zed", "settings.json");
23905
24983
  },
23906
24984
  mcpContainerKey: "context_servers",
23907
24985
  format: "jsonc",
@@ -23911,7 +24989,7 @@ var OPENCODE_CLIENT = {
23911
24989
  id: "opencode",
23912
24990
  label: "OpenCode",
23913
24991
  docsUrl: "https://opencode.ai/docs/mcp-servers/",
23914
- mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? import_node_path79.default.join(xdgConfigDir(), "opencode", "opencode.json"),
24992
+ mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? import_node_path81.default.join(xdgConfigDir(), "opencode", "opencode.json"),
23915
24993
  mcpContainerKey: "mcp",
23916
24994
  format: "json",
23917
24995
  serverEntry: NEAT_OPENCODE_SERVER,
@@ -23921,7 +24999,7 @@ var CRUSH_CLIENT = {
23921
24999
  id: "crush",
23922
25000
  label: "Crush",
23923
25001
  docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
23924
- mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? import_node_path79.default.join(xdgConfigDir(), "crush", "crush.json"),
25002
+ mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? import_node_path81.default.join(xdgConfigDir(), "crush", "crush.json"),
23925
25003
  mcpContainerKey: "mcp",
23926
25004
  format: "json",
23927
25005
  serverEntry: NEAT_CRUSH_SERVER,
@@ -23984,7 +25062,7 @@ async function planMcp(client, mcpPath) {
23984
25062
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
23985
25063
  let raw = "";
23986
25064
  try {
23987
- raw = await import_node_fs45.promises.readFile(mcpPath, "utf8");
25065
+ raw = await import_node_fs47.promises.readFile(mcpPath, "utf8");
23988
25066
  } catch (err) {
23989
25067
  const e = err;
23990
25068
  if (e.code === "ENOENT") {
@@ -24026,7 +25104,7 @@ async function runEditorInstall(client, opts) {
24026
25104
  const mcpPath = client.mcpConfigPath();
24027
25105
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
24028
25106
  const hasRules = typeof client.rulesFileName === "string";
24029
- const rulesPath = hasRules ? import_node_path79.default.join(opts.projectDir, client.rulesFileName) : "";
25107
+ const rulesPath = hasRules ? import_node_path81.default.join(opts.projectDir, client.rulesFileName) : "";
24030
25108
  const mcp = await planMcp(client, mcpPath);
24031
25109
  if (mcp === null) return { exitCode: 1 };
24032
25110
  let existingRules = "";
@@ -24035,7 +25113,7 @@ async function runEditorInstall(client, opts) {
24035
25113
  let block = "";
24036
25114
  if (hasRules) {
24037
25115
  try {
24038
- existingRules = await import_node_fs45.promises.readFile(rulesPath, "utf8");
25116
+ existingRules = await import_node_fs47.promises.readFile(rulesPath, "utf8");
24039
25117
  } catch (err) {
24040
25118
  if (err.code !== "ENOENT") {
24041
25119
  console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
@@ -24069,11 +25147,11 @@ async function runEditorInstall(client, opts) {
24069
25147
  );
24070
25148
  return { exitCode: 0 };
24071
25149
  }
24072
- await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(mcpPath), { recursive: true });
24073
- await import_node_fs45.promises.writeFile(mcpPath, mcp.text, "utf8");
25150
+ await import_node_fs47.promises.mkdir(import_node_path81.default.dirname(mcpPath), { recursive: true });
25151
+ await import_node_fs47.promises.writeFile(mcpPath, mcp.text, "utf8");
24074
25152
  if (hasRules) {
24075
- await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(rulesPath), { recursive: true });
24076
- await import_node_fs45.promises.writeFile(rulesPath, newRules, "utf8");
25153
+ await import_node_fs47.promises.mkdir(import_node_path81.default.dirname(rulesPath), { recursive: true });
25154
+ await import_node_fs47.promises.writeFile(rulesPath, newRules, "utf8");
24077
25155
  }
24078
25156
  console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
24079
25157
  console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
@@ -24109,11 +25187,11 @@ function usage3(client) {
24109
25187
  }
24110
25188
  async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
24111
25189
  const client = CLIENTS[clientId];
24112
- let apply4 = false;
25190
+ let apply6 = false;
24113
25191
  for (const arg of args) {
24114
25192
  switch (arg) {
24115
25193
  case "--apply":
24116
- apply4 = true;
25194
+ apply6 = true;
24117
25195
  break;
24118
25196
  case "-h":
24119
25197
  case "--help":
@@ -24126,7 +25204,7 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
24126
25204
  }
24127
25205
  }
24128
25206
  try {
24129
- const { exitCode } = await runEditorInstall(client, { apply: apply4, projectDir });
25207
+ const { exitCode } = await runEditorInstall(client, { apply: apply6, projectDir });
24130
25208
  return exitCode;
24131
25209
  } catch (err) {
24132
25210
  console.error(err.message);
@@ -24136,11 +25214,11 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
24136
25214
 
24137
25215
  // src/monitor.ts
24138
25216
  init_cjs_shims();
24139
- var import_types84 = require("@neat.is/types");
25217
+ var import_types89 = require("@neat.is/types");
24140
25218
 
24141
25219
  // src/cli-client.ts
24142
25220
  init_cjs_shims();
24143
- var import_types83 = require("@neat.is/types");
25221
+ var import_types88 = require("@neat.is/types");
24144
25222
  var HttpError = class extends Error {
24145
25223
  constructor(status2, message, responseBody = "") {
24146
25224
  super(message);
@@ -24165,10 +25243,10 @@ function createHttpClient(baseUrl, bearerToken) {
24165
25243
  const root = baseUrl.replace(/\/$/, "");
24166
25244
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
24167
25245
  return {
24168
- async get(path82) {
25246
+ async get(path84) {
24169
25247
  let res;
24170
25248
  try {
24171
- res = await fetch(`${root}${path82}`, {
25249
+ res = await fetch(`${root}${path84}`, {
24172
25250
  headers: { ...authHeader }
24173
25251
  });
24174
25252
  } catch (err) {
@@ -24180,16 +25258,16 @@ function createHttpClient(baseUrl, bearerToken) {
24180
25258
  const body = await res.text().catch(() => "");
24181
25259
  throw new HttpError(
24182
25260
  res.status,
24183
- `${res.status} ${res.statusText} on GET ${path82}: ${body}`,
25261
+ `${res.status} ${res.statusText} on GET ${path84}: ${body}`,
24184
25262
  body
24185
25263
  );
24186
25264
  }
24187
25265
  return await res.json();
24188
25266
  },
24189
- async post(path82, body) {
25267
+ async post(path84, body) {
24190
25268
  let res;
24191
25269
  try {
24192
- res = await fetch(`${root}${path82}`, {
25270
+ res = await fetch(`${root}${path84}`, {
24193
25271
  method: "POST",
24194
25272
  headers: { "content-type": "application/json", ...authHeader },
24195
25273
  body: JSON.stringify(body)
@@ -24203,7 +25281,7 @@ function createHttpClient(baseUrl, bearerToken) {
24203
25281
  const text = await res.text().catch(() => "");
24204
25282
  throw new HttpError(
24205
25283
  res.status,
24206
- `${res.status} ${res.statusText} on POST ${path82}: ${text}`,
25284
+ `${res.status} ${res.statusText} on POST ${path84}: ${text}`,
24207
25285
  text
24208
25286
  );
24209
25287
  }
@@ -24217,12 +25295,12 @@ function projectPath(project, suffix) {
24217
25295
  }
24218
25296
  async function runRootCause(client, input) {
24219
25297
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
24220
- const path82 = projectPath(
25298
+ const path84 = projectPath(
24221
25299
  input.project,
24222
25300
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
24223
25301
  );
24224
25302
  try {
24225
- const result = await client.get(path82);
25303
+ const result = await client.get(path84);
24226
25304
  const arrowPath = result.traversalPath.join(" \u2190 ");
24227
25305
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
24228
25306
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -24248,12 +25326,12 @@ async function runRootCause(client, input) {
24248
25326
  }
24249
25327
  async function runBlastRadius(client, input) {
24250
25328
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
24251
- const path82 = projectPath(
25329
+ const path84 = projectPath(
24252
25330
  input.project,
24253
25331
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
24254
25332
  );
24255
25333
  try {
24256
- const result = await client.get(path82);
25334
+ const result = await client.get(path84);
24257
25335
  if (result.totalAffected === 0) {
24258
25336
  return {
24259
25337
  summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
@@ -24282,17 +25360,17 @@ async function runBlastRadius(client, input) {
24282
25360
  }
24283
25361
  }
24284
25362
  function formatBlastEntry(n) {
24285
- const tag = n.edgeProvenance === import_types83.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
25363
+ const tag = n.edgeProvenance === import_types88.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
24286
25364
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
24287
25365
  }
24288
25366
  async function runDependencies(client, input) {
24289
25367
  const depth = input.depth ?? 3;
24290
- const path82 = projectPath(
25368
+ const path84 = projectPath(
24291
25369
  input.project,
24292
25370
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
24293
25371
  );
24294
25372
  try {
24295
- const result = await client.get(path82);
25373
+ const result = await client.get(path84);
24296
25374
  if (result.total === 0) {
24297
25375
  return {
24298
25376
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -24339,7 +25417,7 @@ async function runObservedDependencies(client, input) {
24339
25417
  if (result.observed) {
24340
25418
  return {
24341
25419
  summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
24342
- provenance: import_types83.Provenance.OBSERVED
25420
+ provenance: import_types88.Provenance.OBSERVED
24343
25421
  };
24344
25422
  }
24345
25423
  const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
@@ -24349,7 +25427,7 @@ async function runObservedDependencies(client, input) {
24349
25427
  return {
24350
25428
  summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
24351
25429
  block: blockLines.join("\n"),
24352
- provenance: import_types83.Provenance.OBSERVED
25430
+ provenance: import_types88.Provenance.OBSERVED
24353
25431
  };
24354
25432
  } catch (err) {
24355
25433
  if (err instanceof HttpError && err.status === 404) {
@@ -24384,9 +25462,9 @@ function formatDuration(ms) {
24384
25462
  return `${Math.round(h / 24)}d`;
24385
25463
  }
24386
25464
  async function runIncidents(client, input) {
24387
- const path82 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
25465
+ const path84 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
24388
25466
  try {
24389
- const body = await client.get(path82);
25467
+ const body = await client.get(path84);
24390
25468
  const events = body.events;
24391
25469
  if (events.length === 0) {
24392
25470
  return {
@@ -24403,7 +25481,7 @@ async function runIncidents(client, input) {
24403
25481
  return {
24404
25482
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
24405
25483
  block: blockLines.join("\n"),
24406
- provenance: import_types83.Provenance.OBSERVED
25484
+ provenance: import_types88.Provenance.OBSERVED
24407
25485
  };
24408
25486
  } catch (err) {
24409
25487
  if (err instanceof HttpError && err.status === 404) {
@@ -24512,7 +25590,7 @@ async function runStaleEdges(client, input) {
24512
25590
  return {
24513
25591
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
24514
25592
  block: blockLines.join("\n"),
24515
- provenance: import_types83.Provenance.STALE
25593
+ provenance: import_types88.Provenance.STALE
24516
25594
  };
24517
25595
  }
24518
25596
  async function runPolicies(client, input) {
@@ -24671,10 +25749,10 @@ async function pushSnapshotToRemote(input) {
24671
25749
 
24672
25750
  // src/monitor.ts
24673
25751
  var OBSERVED_DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
24674
- import_types84.EdgeType.CALLS,
24675
- import_types84.EdgeType.CONNECTS_TO,
24676
- import_types84.EdgeType.PUBLISHES_TO,
24677
- import_types84.EdgeType.CONSUMES_FROM
25752
+ import_types89.EdgeType.CALLS,
25753
+ import_types89.EdgeType.CONNECTS_TO,
25754
+ import_types89.EdgeType.PUBLISHES_TO,
25755
+ import_types89.EdgeType.CONSUMES_FROM
24678
25756
  ]);
24679
25757
  function divergenceKey(d) {
24680
25758
  const column = "column" in d && d.column ? d.column : "";
@@ -24719,7 +25797,7 @@ function formatDivergenceLine2(d) {
24719
25797
  }
24720
25798
  }
24721
25799
  function formatStaleLine(edgeId) {
24722
- const parsed = (0, import_types84.parseEdgeId)(edgeId);
25800
+ const parsed = (0, import_types89.parseEdgeId)(edgeId);
24723
25801
  if (parsed) {
24724
25802
  return `\u22EF stale ${parsed.source} \u2192 ${parsed.target} (observed edge went quiet)`;
24725
25803
  }
@@ -24732,7 +25810,7 @@ function divergenceJson(d) {
24732
25810
  return JSON.stringify({ kind: "divergence", ...d });
24733
25811
  }
24734
25812
  function staleJson(edgeId) {
24735
- const parsed = (0, import_types84.parseEdgeId)(edgeId);
25813
+ const parsed = (0, import_types89.parseEdgeId)(edgeId);
24736
25814
  return JSON.stringify({
24737
25815
  kind: "stale",
24738
25816
  edgeId,
@@ -24802,7 +25880,7 @@ var MonitorEmitter = class {
24802
25880
  // ignores non-OBSERVED edges and non-dependency edge types (structural
24803
25881
  // ownership), so only real runtime dependencies reach stdout.
24804
25882
  emitObservedEdge(edge) {
24805
- if (edge.provenance !== import_types84.Provenance.OBSERVED) return false;
25883
+ if (edge.provenance !== import_types89.Provenance.OBSERVED) return false;
24806
25884
  if (!OBSERVED_DEP_EDGE_TYPES.has(edge.type)) return false;
24807
25885
  const key = `edge|${edge.id}`;
24808
25886
  if (this.seen.has(key)) return false;
@@ -24960,7 +26038,7 @@ async function runMonitor(opts) {
24960
26038
  case "edge-added": {
24961
26039
  const payload = safeParse(frame.data);
24962
26040
  const edge = payload?.edge;
24963
- if (edge && edge.provenance === import_types84.Provenance.OBSERVED) {
26041
+ if (edge && edge.provenance === import_types89.Provenance.OBSERVED) {
24964
26042
  emitter.emitObservedEdge(edge);
24965
26043
  divergences.schedule();
24966
26044
  }
@@ -25040,7 +26118,7 @@ function sleep(ms, signal) {
25040
26118
 
25041
26119
  // src/cli-verbs.ts
25042
26120
  init_cjs_shims();
25043
- var import_node_path80 = __toESM(require("path"), 1);
26121
+ var import_node_path82 = __toESM(require("path"), 1);
25044
26122
  async function resolveProjectEntry(opts) {
25045
26123
  const entries = await listProjects();
25046
26124
  if (opts.project) {
@@ -25050,7 +26128,7 @@ async function resolveProjectEntry(opts) {
25050
26128
  const cwd = opts.cwd ?? process.cwd();
25051
26129
  const resolvedCwd = await normalizeProjectPath(cwd);
25052
26130
  for (const entry2 of entries) {
25053
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path80.default.sep}`)) {
26131
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path82.default.sep}`)) {
25054
26132
  return entry2;
25055
26133
  }
25056
26134
  }
@@ -25203,7 +26281,7 @@ async function runSync(opts) {
25203
26281
  }
25204
26282
 
25205
26283
  // src/cli.ts
25206
- var import_types85 = require("@neat.is/types");
26284
+ var import_types90 = require("@neat.is/types");
25207
26285
  function isNpxInvocation() {
25208
26286
  if (process.env.npm_command === "exec") return true;
25209
26287
  const execpath = process.env.npm_execpath ?? "";
@@ -25557,15 +26635,15 @@ async function buildPatchSections(services, project) {
25557
26635
  for (const svc of services) {
25558
26636
  const installer = await pickInstaller(svc.dir);
25559
26637
  if (!installer) continue;
25560
- const plan4 = await installer.plan(svc.dir, { project });
25561
- if (isEmptyPlan(plan4) && !plan4.libOnly && plan4.runtimeKind === void 0) continue;
25562
- sections.push({ installer: installer.name, plan: plan4 });
26638
+ const plan6 = await installer.plan(svc.dir, { project });
26639
+ if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) continue;
26640
+ sections.push({ installer: installer.name, plan: plan6 });
25563
26641
  }
25564
26642
  return sections;
25565
26643
  }
25566
26644
  async function runInit(opts) {
25567
26645
  const written = [];
25568
- const stat = await import_node_fs46.promises.stat(opts.scanPath).catch(() => null);
26646
+ const stat = await import_node_fs48.promises.stat(opts.scanPath).catch(() => null);
25569
26647
  if (!stat || !stat.isDirectory()) {
25570
26648
  console.error(`neat init: ${opts.scanPath} is not a directory`);
25571
26649
  return { exitCode: 2, writtenFiles: written };
@@ -25574,13 +26652,13 @@ async function runInit(opts) {
25574
26652
  printDiscoveryReport(opts, services);
25575
26653
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
25576
26654
  const patch = renderPatch(sections);
25577
- const patchPath = import_node_path81.default.join(opts.scanPath, "neat.patch");
26655
+ const patchPath = import_node_path83.default.join(opts.scanPath, "neat.patch");
25578
26656
  if (opts.dryRun) {
25579
- await import_node_fs46.promises.writeFile(patchPath, patch, "utf8");
26657
+ await import_node_fs48.promises.writeFile(patchPath, patch, "utf8");
25580
26658
  written.push(patchPath);
25581
26659
  console.log(`dry-run: patch written to ${patchPath}`);
25582
- const gitignorePath = import_node_path81.default.join(opts.scanPath, ".gitignore");
25583
- const gitignoreExists = await import_node_fs46.promises.stat(gitignorePath).then(() => true).catch(() => false);
26660
+ const gitignorePath = import_node_path83.default.join(opts.scanPath, ".gitignore");
26661
+ const gitignoreExists = await import_node_fs48.promises.stat(gitignorePath).then(() => true).catch(() => false);
25584
26662
  const verb = gitignoreExists ? "append" : "create";
25585
26663
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
25586
26664
  console.log("rerun without --dry-run to register and snapshot.");
@@ -25591,9 +26669,9 @@ async function runInit(opts) {
25591
26669
  const graph = getGraph(graphKey);
25592
26670
  const projectPaths = pathsForProject(
25593
26671
  graphKey,
25594
- import_node_path81.default.join(opts.scanPath, "neat-out")
26672
+ import_node_path83.default.join(opts.scanPath, "neat-out")
25595
26673
  );
25596
- const errorsPath = import_node_path81.default.join(import_node_path81.default.dirname(opts.outPath), import_node_path81.default.basename(projectPaths.errorsPath));
26674
+ const errorsPath = import_node_path83.default.join(import_node_path83.default.dirname(opts.outPath), import_node_path83.default.basename(projectPaths.errorsPath));
25597
26675
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
25598
26676
  await saveGraphToDisk(graph, opts.outPath);
25599
26677
  written.push(opts.outPath);
@@ -25672,7 +26750,7 @@ async function runInit(opts) {
25672
26750
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
25673
26751
  }
25674
26752
  } else {
25675
- await import_node_fs46.promises.writeFile(patchPath, patch, "utf8");
26753
+ await import_node_fs48.promises.writeFile(patchPath, patch, "utf8");
25676
26754
  written.push(patchPath);
25677
26755
  }
25678
26756
  }
@@ -25712,9 +26790,9 @@ var CLAUDE_SKILL_CONFIG = {
25712
26790
  };
25713
26791
  function claudeConfigPath() {
25714
26792
  const override = process.env.NEAT_CLAUDE_CONFIG;
25715
- if (override && override.length > 0) return import_node_path81.default.resolve(override);
26793
+ if (override && override.length > 0) return import_node_path83.default.resolve(override);
25716
26794
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
25717
- return import_node_path81.default.join(home, ".claude.json");
26795
+ return import_node_path83.default.join(home, ".claude.json");
25718
26796
  }
25719
26797
  async function runSkill(opts) {
25720
26798
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -25726,7 +26804,7 @@ async function runSkill(opts) {
25726
26804
  const target = claudeConfigPath();
25727
26805
  let existing = {};
25728
26806
  try {
25729
- existing = JSON.parse(await import_node_fs46.promises.readFile(target, "utf8"));
26807
+ existing = JSON.parse(await import_node_fs48.promises.readFile(target, "utf8"));
25730
26808
  } catch (err) {
25731
26809
  if (err.code !== "ENOENT") {
25732
26810
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -25738,8 +26816,8 @@ async function runSkill(opts) {
25738
26816
  ...existing,
25739
26817
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
25740
26818
  };
25741
- await import_node_fs46.promises.mkdir(import_node_path81.default.dirname(target), { recursive: true });
25742
- await import_node_fs46.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
26819
+ await import_node_fs48.promises.mkdir(import_node_path83.default.dirname(target), { recursive: true });
26820
+ await import_node_fs48.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
25743
26821
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
25744
26822
  console.log("restart Claude Code to pick up the new MCP server.");
25745
26823
  console.log("");
@@ -25812,7 +26890,7 @@ async function main() {
25812
26890
  }
25813
26891
  const cmd = argvParsed.positional[0];
25814
26892
  const parsed = { ...argvParsed, positional: argvParsed.positional.slice(1) };
25815
- const { positional, apply: apply4, dryRun, noInstall } = parsed;
26893
+ const { positional, apply: apply6, dryRun, noInstall } = parsed;
25816
26894
  const project = parsed.project ?? DEFAULT_PROJECT;
25817
26895
  if (cmd === "init") {
25818
26896
  const target = positional[0];
@@ -25821,22 +26899,22 @@ async function main() {
25821
26899
  usage4();
25822
26900
  process.exit(2);
25823
26901
  }
25824
- if (apply4 && dryRun) {
26902
+ if (apply6 && dryRun) {
25825
26903
  console.error("neat init: --apply and --dry-run are mutually exclusive");
25826
26904
  process.exit(2);
25827
26905
  }
25828
- const scanPath = import_node_path81.default.resolve(target);
26906
+ const scanPath = import_node_path83.default.resolve(target);
25829
26907
  const projectExplicit = parsed.project !== null;
25830
- const projectName = projectExplicit ? project : import_node_path81.default.basename(scanPath);
26908
+ const projectName = projectExplicit ? project : import_node_path83.default.basename(scanPath);
25831
26909
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
25832
- const fallback = pathsForProject(projectKey, import_node_path81.default.join(scanPath, "neat-out")).snapshotPath;
25833
- const outPath = import_node_path81.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
26910
+ const fallback = pathsForProject(projectKey, import_node_path83.default.join(scanPath, "neat-out")).snapshotPath;
26911
+ const outPath = import_node_path83.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
25834
26912
  const result = await runInit({
25835
26913
  scanPath,
25836
26914
  outPath,
25837
26915
  project: projectName,
25838
26916
  projectExplicit,
25839
- apply: apply4,
26917
+ apply: apply6,
25840
26918
  dryRun,
25841
26919
  noInstall,
25842
26920
  verbose: parsed.verbose
@@ -25851,21 +26929,21 @@ async function main() {
25851
26929
  usage4();
25852
26930
  process.exit(2);
25853
26931
  }
25854
- const scanPath = import_node_path81.default.resolve(target);
25855
- const stat = await import_node_fs46.promises.stat(scanPath).catch(() => null);
26932
+ const scanPath = import_node_path83.default.resolve(target);
26933
+ const stat = await import_node_fs48.promises.stat(scanPath).catch(() => null);
25856
26934
  if (!stat || !stat.isDirectory()) {
25857
26935
  console.error(`neat watch: ${scanPath} is not a directory`);
25858
26936
  process.exit(2);
25859
26937
  }
25860
- const projectPaths = pathsForProject(project, import_node_path81.default.join(scanPath, "neat-out"));
25861
- const outPath = import_node_path81.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
25862
- const errorsPath = import_node_path81.default.resolve(
25863
- process.env.NEAT_ERRORS_PATH ?? import_node_path81.default.join(import_node_path81.default.dirname(outPath), import_node_path81.default.basename(projectPaths.errorsPath))
26938
+ const projectPaths = pathsForProject(project, import_node_path83.default.join(scanPath, "neat-out"));
26939
+ const outPath = import_node_path83.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
26940
+ const errorsPath = import_node_path83.default.resolve(
26941
+ process.env.NEAT_ERRORS_PATH ?? import_node_path83.default.join(import_node_path83.default.dirname(outPath), import_node_path83.default.basename(projectPaths.errorsPath))
25864
26942
  );
25865
- const staleEventsPath = import_node_path81.default.resolve(
25866
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path81.default.join(import_node_path81.default.dirname(outPath), import_node_path81.default.basename(projectPaths.staleEventsPath))
26943
+ const staleEventsPath = import_node_path83.default.resolve(
26944
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path83.default.join(import_node_path83.default.dirname(outPath), import_node_path83.default.basename(projectPaths.staleEventsPath))
25867
26945
  );
25868
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path81.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
26946
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path83.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
25869
26947
  const handle = await startWatch(getGraph(project), {
25870
26948
  scanPath,
25871
26949
  outPath,
@@ -25874,7 +26952,7 @@ async function main() {
25874
26952
  project,
25875
26953
  // Resolve NEAT_HOME so a `neat watch` picks up connectors added to
25876
26954
  // ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
25877
- neatHome: process.env.NEAT_HOME ? import_node_path81.default.resolve(process.env.NEAT_HOME) : import_node_path81.default.join(import_node_os8.default.homedir(), ".neat"),
26955
+ neatHome: process.env.NEAT_HOME ? import_node_path83.default.resolve(process.env.NEAT_HOME) : import_node_path83.default.join(import_node_os8.default.homedir(), ".neat"),
25878
26956
  ...embeddingsCachePath ? { embeddingsCachePath } : {},
25879
26957
  host: process.env.HOST ?? "0.0.0.0",
25880
26958
  port: Number(process.env.PORT ?? 8080),
@@ -26056,11 +27134,11 @@ async function main() {
26056
27134
  process.exit(1);
26057
27135
  }
26058
27136
  async function tryOrchestrator(cmd, parsed) {
26059
- const scanPath = import_node_path81.default.resolve(cmd);
26060
- const stat = await import_node_fs46.promises.stat(scanPath).catch(() => null);
27137
+ const scanPath = import_node_path83.default.resolve(cmd);
27138
+ const stat = await import_node_fs48.promises.stat(scanPath).catch(() => null);
26061
27139
  if (!stat || !stat.isDirectory()) return null;
26062
27140
  const projectExplicit = parsed.project !== null;
26063
- const projectName = projectExplicit ? parsed.project : import_node_path81.default.basename(scanPath);
27141
+ const projectName = projectExplicit ? parsed.project : import_node_path83.default.basename(scanPath);
26064
27142
  const result = await runOrchestrator({
26065
27143
  scanPath,
26066
27144
  project: projectName,
@@ -26249,10 +27327,10 @@ async function runQueryVerb(cmd, parsed) {
26249
27327
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
26250
27328
  const out = [];
26251
27329
  for (const p of parts) {
26252
- const r = import_types85.DivergenceTypeSchema.safeParse(p);
27330
+ const r = import_types90.DivergenceTypeSchema.safeParse(p);
26253
27331
  if (!r.success) {
26254
27332
  console.error(
26255
- `neat divergences: unknown --type "${p}". allowed: ${import_types85.DivergenceTypeSchema.options.join(", ")}`
27333
+ `neat divergences: unknown --type "${p}". allowed: ${import_types90.DivergenceTypeSchema.options.join(", ")}`
26256
27334
  );
26257
27335
  return 2;
26258
27336
  }