@neat.is/core 0.5.1 → 0.5.3

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 path61 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
- if (exactUnauthPaths.has(path61) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path61)) {
64
+ const path62 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
+ if (exactUnauthPaths.has(path62) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path62)) {
66
66
  done();
67
67
  return;
68
68
  }
@@ -362,8 +362,8 @@ function websocketChannelPathOf(attrs) {
362
362
  const v = attrs[key];
363
363
  if (typeof v === "string" && v.length > 0) {
364
364
  const q = v.indexOf("?");
365
- const path61 = q === -1 ? v : v.slice(0, q);
366
- if (path61.length > 0) return path61;
365
+ const path62 = q === -1 ? v : v.slice(0, q);
366
+ if (path62.length > 0) return path62;
367
367
  }
368
368
  }
369
369
  return void 0;
@@ -686,12 +686,12 @@ __export(cli_exports, {
686
686
  runInit: () => runInit,
687
687
  runQueryVerb: () => runQueryVerb,
688
688
  runSkill: () => runSkill,
689
- usage: () => usage
689
+ usage: () => usage2
690
690
  });
691
691
  module.exports = __toCommonJS(cli_exports);
692
692
  init_cjs_shims();
693
- var import_node_path60 = __toESM(require("path"), 1);
694
- var import_node_fs40 = require("fs");
693
+ var import_node_path61 = __toESM(require("path"), 1);
694
+ var import_node_fs41 = require("fs");
695
695
 
696
696
  // src/banner.ts
697
697
  init_cjs_shims();
@@ -1256,19 +1256,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1256
1256
  function longestIncomingWalk(graph, start, maxDepth) {
1257
1257
  let best = { path: [start], edges: [] };
1258
1258
  const visited = /* @__PURE__ */ new Set([start]);
1259
- function step(node, path61, edges) {
1260
- if (path61.length > best.path.length) {
1261
- best = { path: [...path61], edges: [...edges] };
1259
+ function step(node, path62, edges) {
1260
+ if (path62.length > best.path.length) {
1261
+ best = { path: [...path62], edges: [...edges] };
1262
1262
  }
1263
- if (path61.length - 1 >= maxDepth) return;
1263
+ if (path62.length - 1 >= maxDepth) return;
1264
1264
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1265
1265
  for (const [srcId, edge] of incoming) {
1266
1266
  if (visited.has(srcId)) continue;
1267
1267
  visited.add(srcId);
1268
- path61.push(srcId);
1268
+ path62.push(srcId);
1269
1269
  edges.push(edge);
1270
- step(srcId, path61, edges);
1271
- path61.pop();
1270
+ step(srcId, path62, edges);
1271
+ path62.pop();
1272
1272
  edges.pop();
1273
1273
  visited.delete(srcId);
1274
1274
  }
@@ -1462,26 +1462,26 @@ function dominantFailingCall(graph, serviceId6, visited) {
1462
1462
  return best;
1463
1463
  }
1464
1464
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1465
- const path61 = [originServiceId];
1465
+ const path62 = [originServiceId];
1466
1466
  const edges = [];
1467
1467
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1468
1468
  let current = originServiceId;
1469
1469
  for (let depth = 0; depth < maxDepth; depth++) {
1470
1470
  const hop = dominantFailingCall(graph, current, visited);
1471
1471
  if (!hop) break;
1472
- path61.push(hop.nextService);
1472
+ path62.push(hop.nextService);
1473
1473
  edges.push(hop.edge);
1474
1474
  visited.add(hop.nextService);
1475
1475
  current = hop.nextService;
1476
1476
  }
1477
1477
  if (edges.length === 0) return null;
1478
- return { path: path61, edges, culprit: current };
1478
+ return { path: path62, edges, culprit: current };
1479
1479
  }
1480
1480
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1481
1481
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1482
1482
  if (!chain) return null;
1483
1483
  const culprit = chain.culprit;
1484
- const path61 = [...chain.path];
1484
+ const path62 = [...chain.path];
1485
1485
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1486
1486
  const baseConfidence = confidenceFromMix(chain.edges);
1487
1487
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1489,14 +1489,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1489
1489
  if (loc) {
1490
1490
  let rootCauseNode = culprit;
1491
1491
  if (loc.fileNode) {
1492
- path61.push(loc.fileNode);
1492
+ path62.push(loc.fileNode);
1493
1493
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1494
1494
  rootCauseNode = loc.fileNode;
1495
1495
  }
1496
1496
  return import_types.RootCauseResultSchema.parse({
1497
1497
  rootCauseNode,
1498
1498
  rootCauseReason: loc.rootCauseReason,
1499
- traversalPath: path61,
1499
+ traversalPath: path62,
1500
1500
  edgeProvenances,
1501
1501
  confidence,
1502
1502
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1508,7 +1508,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1508
1508
  return import_types.RootCauseResultSchema.parse({
1509
1509
  rootCauseNode: culprit,
1510
1510
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1511
- traversalPath: path61,
1511
+ traversalPath: path62,
1512
1512
  edgeProvenances,
1513
1513
  confidence,
1514
1514
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -4678,14 +4678,21 @@ function engineFromImage(image) {
4678
4678
  // src/extract/databases/dotenv.ts
4679
4679
  var CONNECTION_KEYS = /* @__PURE__ */ new Set([
4680
4680
  "DATABASE_URL",
4681
+ "DATABASE_URI",
4681
4682
  "DB_URL",
4683
+ "DB_URI",
4682
4684
  "POSTGRES_URL",
4685
+ "POSTGRES_URI",
4683
4686
  "POSTGRESQL_URL",
4687
+ "POSTGRESQL_URI",
4684
4688
  "MYSQL_URL",
4689
+ "MYSQL_URI",
4690
+ "MONGODB_URL",
4685
4691
  "MONGODB_URI",
4686
4692
  "MONGO_URL",
4687
4693
  "MONGO_URI",
4688
- "REDIS_URL"
4694
+ "REDIS_URL",
4695
+ "REDIS_URI"
4689
4696
  ]);
4690
4697
  function parseDotenvLine(line) {
4691
4698
  const trimmed = line.trim();
@@ -8198,11 +8205,42 @@ async function readPackageJson(scanPath) {
8198
8205
  const raw = await import_node_fs28.promises.readFile(pkgPath, "utf8");
8199
8206
  return JSON.parse(raw);
8200
8207
  }
8208
+ var HOOK_WALK_SKIP_DIRS = /* @__PURE__ */ new Set([
8209
+ "node_modules",
8210
+ "dist",
8211
+ "build",
8212
+ "out",
8213
+ "coverage",
8214
+ "neat-out"
8215
+ ]);
8201
8216
  async function findHookFiles(scanPath) {
8202
- const entries = await import_node_fs28.promises.readdir(scanPath);
8203
- return entries.filter(
8204
- (e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
8205
- ).sort();
8217
+ const found = [];
8218
+ const walk3 = async (dir) => {
8219
+ const entries = await import_node_fs28.promises.readdir(dir, { withFileTypes: true }).catch(() => []);
8220
+ for (const entry2 of entries) {
8221
+ if (entry2.isDirectory()) {
8222
+ if (entry2.name.startsWith(".") || HOOK_WALK_SKIP_DIRS.has(entry2.name)) continue;
8223
+ await walk3(import_node_path45.default.join(dir, entry2.name));
8224
+ } else if (entry2.isFile()) {
8225
+ if ((entry2.name.startsWith("instrumentation") || entry2.name.startsWith("otel-init")) && /\.(ts|js|cjs|mjs)$/.test(entry2.name)) {
8226
+ const rel = import_node_path45.default.relative(scanPath, import_node_path45.default.join(dir, entry2.name));
8227
+ found.push(rel.split(import_node_path45.default.sep).join("/"));
8228
+ }
8229
+ }
8230
+ }
8231
+ };
8232
+ await walk3(scanPath);
8233
+ return found.sort();
8234
+ }
8235
+ async function pickPrimaryHookFile(scanPath, hookFiles, snippet2) {
8236
+ let fallback = null;
8237
+ for (const file of hookFiles) {
8238
+ const content = await import_node_fs28.promises.readFile(import_node_path45.default.join(scanPath, file), "utf8");
8239
+ const patched = splicedContent(content, snippet2);
8240
+ if (patched !== null) return { file, content, patched };
8241
+ if (fallback === null) fallback = { file, content };
8242
+ }
8243
+ return { file: fallback.file, content: fallback.content, patched: null };
8206
8244
  }
8207
8245
  function extendLogPath() {
8208
8246
  return process.env.NEAT_EXTEND_LOG ?? import_node_path45.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
@@ -8295,7 +8333,13 @@ async function applyExtension(ctx, args, options) {
8295
8333
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
8296
8334
  }
8297
8335
  }
8298
- const primaryFile = hookFiles[0];
8336
+ const primary = await pickPrimaryHookFile(ctx.scanPath, hookFiles, args.registration_snippet);
8337
+ if (primary.patched === null) {
8338
+ throw new Error(
8339
+ `Could not find instrumentation insertion point in ${hookFiles.join(", ")}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
8340
+ );
8341
+ }
8342
+ const primaryFile = primary.file;
8299
8343
  const primaryPath = import_node_path45.default.join(ctx.scanPath, primaryFile);
8300
8344
  const filesTouched = [];
8301
8345
  const depsAdded = [];
@@ -8307,14 +8351,7 @@ async function applyExtension(ctx, args, options) {
8307
8351
  filesTouched.push("package.json");
8308
8352
  depsAdded.push(`${args.instrumentation_package}@${args.version}`);
8309
8353
  }
8310
- const hookContent = await import_node_fs28.promises.readFile(primaryPath, "utf8");
8311
- const patched = splicedContent(hookContent, args.registration_snippet);
8312
- if (!patched) {
8313
- throw new Error(
8314
- `Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
8315
- );
8316
- }
8317
- await import_node_fs28.promises.writeFile(primaryPath, patched, "utf8");
8354
+ await import_node_fs28.promises.writeFile(primaryPath, primary.patched, "utf8");
8318
8355
  filesTouched.push(primaryFile);
8319
8356
  const cmd = await detectPackageManager(ctx.scanPath);
8320
8357
  const installer = options?.runInstall ?? runPackageManagerInstall;
@@ -8356,7 +8393,7 @@ async function dryRunExtension(ctx, args) {
8356
8393
  };
8357
8394
  }
8358
8395
  }
8359
- const primaryFile = hookFiles[0];
8396
+ const primary = await pickPrimaryHookFile(ctx.scanPath, hookFiles, args.registration_snippet);
8360
8397
  const filesTouched = [];
8361
8398
  const depsToAdd = [];
8362
8399
  let packageJsonPatch = {};
@@ -8367,10 +8404,8 @@ async function dryRunExtension(ctx, args) {
8367
8404
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
8368
8405
  filesTouched.push("package.json");
8369
8406
  }
8370
- const hookContent = await import_node_fs28.promises.readFile(import_node_path45.default.join(ctx.scanPath, primaryFile), "utf8");
8371
- const patched = splicedContent(hookContent, args.registration_snippet);
8372
- if (patched) {
8373
- filesTouched.push(primaryFile);
8407
+ if (primary.patched !== null) {
8408
+ filesTouched.push(primary.file);
8374
8409
  templatePatch = `+ ${args.registration_snippet}`;
8375
8410
  } else {
8376
8411
  templatePatch = "Could not find insertion point in hook file.";
@@ -9943,7 +9978,7 @@ function registerRoutes(scope, ctx) {
9943
9978
  });
9944
9979
  }
9945
9980
  async function buildApi(opts) {
9946
- const app = (0, import_fastify.default)({ logger: false });
9981
+ const app = (0, import_fastify.default)({ logger: false, routerOptions: { maxParamLength: 1024 } });
9947
9982
  await app.register(import_cors.default, { origin: true });
9948
9983
  const env = readAuthEnv();
9949
9984
  const authToken = opts.authToken ?? env.authToken;
@@ -10451,17 +10486,20 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
10451
10486
  // src/connectors/supabase/map.ts
10452
10487
  var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
10453
10488
  var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
10454
- function targetFromRestPath(path61) {
10455
- const rpcMatch = REST_RPC_PATH_RE.exec(path61);
10489
+ function targetFromRestPath(path62) {
10490
+ const rpcMatch = REST_RPC_PATH_RE.exec(path62);
10456
10491
  if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
10457
- const tableMatch = REST_TABLE_PATH_RE.exec(path61);
10492
+ const tableMatch = REST_TABLE_PATH_RE.exec(path62);
10458
10493
  if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
10459
10494
  return null;
10460
10495
  }
10461
10496
  var ERROR_STATUS_THRESHOLD = 500;
10462
10497
  function mapEdgeLogRowsToSignals(rows) {
10463
10498
  const buckets2 = /* @__PURE__ */ new Map();
10499
+ if (!Array.isArray(rows)) return [];
10464
10500
  for (const row of rows) {
10501
+ if (!row || typeof row !== "object") continue;
10502
+ if (typeof row.path !== "string" || typeof row.timestamp !== "string") continue;
10465
10503
  const target = targetFromRestPath(row.path);
10466
10504
  if (!target) continue;
10467
10505
  const key = `${target.targetKind}:${target.name}`;
@@ -10502,9 +10540,12 @@ function tableNameFromQueryText(query) {
10502
10540
  function diffPgStatStatementsToSignals(rows, previous, nowIso2) {
10503
10541
  const signals = [];
10504
10542
  const seen = /* @__PURE__ */ new Set();
10543
+ if (!Array.isArray(rows)) return signals;
10505
10544
  for (const row of rows) {
10506
- seen.add(row.queryid);
10545
+ if (!row || typeof row !== "object" || typeof row.queryid !== "string") continue;
10507
10546
  const calls = Number(row.calls);
10547
+ if (!Number.isFinite(calls)) continue;
10548
+ seen.add(row.queryid);
10508
10549
  const prior = previous.get(row.queryid);
10509
10550
  previous.set(row.queryid, { calls });
10510
10551
  if (!prior || calls < prior.calls) continue;
@@ -10828,7 +10869,12 @@ function upsertBucket(buckets2, key, isError, timestamp, build) {
10828
10869
  }
10829
10870
  function mapRailwayHttpLogsToSignals(entries, routeIndex) {
10830
10871
  const buckets2 = /* @__PURE__ */ new Map();
10872
+ if (!Array.isArray(entries)) return [];
10831
10873
  for (const entry2 of entries) {
10874
+ if (!entry2 || typeof entry2 !== "object") continue;
10875
+ if (typeof entry2.method !== "string" || typeof entry2.path !== "string" || typeof entry2.timestamp !== "string") {
10876
+ continue;
10877
+ }
10832
10878
  const method = entry2.method.toUpperCase();
10833
10879
  const normalizedPath = normalizePathTemplate(entry2.path);
10834
10880
  const match = findRailwayRoute(routeIndex, method, normalizedPath);
@@ -10867,8 +10913,11 @@ function mapRailwayHttpLogsToSignals(entries, routeIndex) {
10867
10913
  }
10868
10914
  function mapRailwayNetworkFlowLogsToSignals(entries) {
10869
10915
  const buckets2 = /* @__PURE__ */ new Map();
10916
+ if (!Array.isArray(entries)) return [];
10870
10917
  for (const entry2 of entries) {
10871
- if (!entry2.peerServiceId) continue;
10918
+ if (!entry2 || typeof entry2 !== "object") continue;
10919
+ if (typeof entry2.peerServiceId !== "string" || entry2.peerServiceId.length === 0) continue;
10920
+ if (typeof entry2.timestamp !== "string") continue;
10872
10921
  const isError = entry2.dropCause !== null && entry2.dropCause !== "";
10873
10922
  upsertBucket(buckets2, entry2.peerServiceId, isError, entry2.timestamp, () => ({
10874
10923
  targetKind: PEER_SERVICE_TARGET_KIND,
@@ -10996,7 +11045,7 @@ async function fetchHttpRequestLogEntries(creds, sinceIso) {
10996
11045
  throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`);
10997
11046
  }
10998
11047
  const json = await res.json();
10999
- out.push(...json.entries ?? []);
11048
+ if (Array.isArray(json.entries)) out.push(...json.entries);
11000
11049
  if (!json.nextPageToken) break;
11001
11050
  pageToken = json.nextPageToken;
11002
11051
  }
@@ -11017,9 +11066,9 @@ function parseFirebaseTargetName(targetName) {
11017
11066
  const secondSep = rest.indexOf(FIELD_SEP);
11018
11067
  if (secondSep === -1) return null;
11019
11068
  const method = rest.slice(0, secondSep);
11020
- const path61 = rest.slice(secondSep + 1);
11021
- if (!resourceName || !method || !path61) return null;
11022
- return { resourceName, method, path: path61 };
11069
+ const path62 = rest.slice(secondSep + 1);
11070
+ if (!resourceName || !method || !path62) return null;
11071
+ return { resourceName, method, path: path62 };
11023
11072
  }
11024
11073
  function resourceNameFor(type, labels) {
11025
11074
  if (!labels) return null;
@@ -11033,7 +11082,7 @@ function resourceNameFor(type, labels) {
11033
11082
  }
11034
11083
  }
11035
11084
  function pathFromRequestUrl(requestUrl) {
11036
- if (!requestUrl) return null;
11085
+ if (typeof requestUrl !== "string" || requestUrl.length === 0) return null;
11037
11086
  if (requestUrl.startsWith("/")) {
11038
11087
  const withoutQuery = requestUrl.split("?")[0];
11039
11088
  return withoutQuery && withoutQuery.length > 0 ? withoutQuery : "/";
@@ -11048,22 +11097,23 @@ function pathFromRequestUrl(requestUrl) {
11048
11097
  }
11049
11098
  var ERROR_STATUS_THRESHOLD2 = 500;
11050
11099
  function mapLogEntryToSignal(entry2) {
11100
+ if (!entry2 || typeof entry2 !== "object") return null;
11051
11101
  const resourceType = entry2.resource?.type;
11052
11102
  if (!resourceType || !isFirebaseResourceType(resourceType)) return null;
11053
11103
  const resourceName = resourceNameFor(resourceType, entry2.resource?.labels);
11054
11104
  if (!resourceName) return null;
11055
11105
  const req = entry2.httpRequest;
11056
11106
  if (!req) return null;
11057
- if (!req.requestMethod) return null;
11107
+ if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
11058
11108
  const method = req.requestMethod.toUpperCase();
11059
- const path61 = pathFromRequestUrl(req.requestUrl);
11060
- if (path61 === null) return null;
11109
+ const path62 = pathFromRequestUrl(req.requestUrl);
11110
+ if (path62 === null) return null;
11061
11111
  const timestamp = entry2.timestamp;
11062
- if (!timestamp) return null;
11112
+ if (typeof timestamp !== "string" || timestamp.length === 0) return null;
11063
11113
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
11064
11114
  return {
11065
11115
  targetKind: resourceType,
11066
- targetName: packFirebaseTargetName({ resourceName, method, path: path61 }),
11116
+ targetName: packFirebaseTargetName({ resourceName, method, path: path62 }),
11067
11117
  callCount: 1,
11068
11118
  errorCount: isError ? 1 : 0,
11069
11119
  lastObservedIso: timestamp
@@ -11211,7 +11261,7 @@ async function queryWorkerInvocations(ctx, config, window, fetchImpl = fetch) {
11211
11261
  throw new Error(`cloudflare connector: telemetry query returned an error (${message})`);
11212
11262
  }
11213
11263
  const events = payload.result?.events?.events;
11214
- if (events === void 0) {
11264
+ if (!Array.isArray(events)) {
11215
11265
  console.warn(
11216
11266
  "[neat connector] cloudflare: telemetry query returned success:true but no result.events.events array \u2014 the response shape may have changed; treating as zero events this tick"
11217
11267
  );
@@ -11241,7 +11291,7 @@ var HTTP_METHODS = /* @__PURE__ */ new Set([
11241
11291
  ]);
11242
11292
  var LEADING_TOKEN_RE = /^(\S+)\s+\S/;
11243
11293
  function parseHttpMethodFromTrigger(trigger) {
11244
- if (!trigger) return null;
11294
+ if (typeof trigger !== "string") return null;
11245
11295
  const match = LEADING_TOKEN_RE.exec(trigger.trim());
11246
11296
  const token = match?.[1];
11247
11297
  if (!token) return null;
@@ -11257,25 +11307,28 @@ function parsePathFromTrigger(trigger) {
11257
11307
  }
11258
11308
  var ERROR_STATUS_THRESHOLD3 = 500;
11259
11309
  function mapEventToSignal(event) {
11310
+ if (!event || typeof event !== "object") return null;
11260
11311
  const metadata = event.$metadata;
11261
11312
  const workers = event.$workers;
11262
11313
  const method = parseHttpMethodFromTrigger(metadata?.trigger);
11263
11314
  if (!method) return null;
11264
11315
  const scriptName = workers?.scriptName ?? metadata?.service;
11265
- if (!scriptName) return null;
11316
+ if (typeof scriptName !== "string" || scriptName.length === 0) return null;
11266
11317
  const timestampMs = event.timestamp ?? metadata?.startTime;
11267
11318
  if (typeof timestampMs !== "number" || !Number.isFinite(timestampMs)) return null;
11319
+ const observedAt = new Date(timestampMs);
11320
+ if (Number.isNaN(observedAt.getTime())) return null;
11268
11321
  const statusCode = metadata?.statusCode;
11269
11322
  const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
11270
- const path61 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
11323
+ const path62 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
11271
11324
  return {
11272
11325
  targetKind: CLOUDFLARE_TARGET_KIND,
11273
11326
  targetName: scriptName,
11274
11327
  callCount: 1,
11275
11328
  errorCount: isError ? 1 : 0,
11276
- lastObservedIso: new Date(timestampMs).toISOString(),
11329
+ lastObservedIso: observedAt.toISOString(),
11277
11330
  method,
11278
- ...path61 ? { path: path61 } : {},
11331
+ ...path62 ? { path: path62 } : {},
11279
11332
  ...typeof statusCode === "number" ? { statusCode } : {},
11280
11333
  ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
11281
11334
  };
@@ -11321,8 +11374,8 @@ function findTaggedWorkerFileNode(graph, workerName) {
11321
11374
  });
11322
11375
  return found;
11323
11376
  }
11324
- function findMatchingRouteNode(graph, serviceName, method, path61) {
11325
- const normalizedPath = normalizePathTemplate(path61);
11377
+ function findMatchingRouteNode(graph, serviceName, method, path62) {
11378
+ const normalizedPath = normalizePathTemplate(path62);
11326
11379
  let found = null;
11327
11380
  graph.forEachNode((id, attrs) => {
11328
11381
  if (found) return;
@@ -11339,10 +11392,10 @@ function createCloudflareResolveTarget(config, graph) {
11339
11392
  return (signal) => {
11340
11393
  if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
11341
11394
  const scriptName = signal.targetName;
11342
- const { method, path: path61 } = signal;
11395
+ const { method, path: path62 } = signal;
11343
11396
  const resolveRouteGrain = (serviceName, wholeFileId) => {
11344
- if (!method || !path61) return wholeFileId;
11345
- return findMatchingRouteNode(graph, serviceName, method, path61) ?? wholeFileId;
11397
+ if (!method || !path62) return wholeFileId;
11398
+ return findMatchingRouteNode(graph, serviceName, method, path62) ?? wholeFileId;
11346
11399
  };
11347
11400
  const mapping = config.workers?.[scriptName];
11348
11401
  if (mapping) {
@@ -12539,7 +12592,7 @@ var import_semver2 = __toESM(require("semver"), 1);
12539
12592
  // src/installers/templates.ts
12540
12593
  init_cjs_shims();
12541
12594
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
12542
- var OTEL_INIT_STAMP = "// neat-template-version: 6 \u2014 daemon.json endpoint resolution (ADR-096) + layered file-first capture (ADR-090).";
12595
+ var OTEL_INIT_STAMP = "// neat-template-version: 7 \u2014 OTel init degrades to no-OBSERVED instead of crashing the host app when @opentelemetry deps are absent (#820); daemon.json endpoint resolution (ADR-096) + layered file-first capture (ADR-090).";
12543
12596
  var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer ' + process.env.NEAT_OTEL_TOKEN";
12544
12597
  var OTEL_OTLP_PROTOCOL_JS = "process.env.OTEL_EXPORTER_OTLP_PROTOCOL ||= 'http/json'";
12545
12598
  var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
@@ -12875,6 +12928,14 @@ ${OTEL_ENDPOINT_RESOLVER_CJS}
12875
12928
  ${OTEL_OTLP_PROTOCOL_JS}
12876
12929
  ${OTEL_OTLP_HEADERS_JS}
12877
12930
 
12931
+ // Instrumentation is ambient \u2014 it must never break the host app. If the
12932
+ // @opentelemetry packages aren't installed (a package-manager install that
12933
+ // failed or hasn't run yet), degrade to running WITHOUT OBSERVED rather than
12934
+ // crashing the process on a missing module (#820). The require-based flavor can
12935
+ // guard this at runtime; do so around the whole SDK setup, not just the
12936
+ // requires, so a broken instrumentation registration can't take the app down
12937
+ // either.
12938
+ try {
12878
12939
  const { NodeSDK } = require('@opentelemetry/sdk-node')
12879
12940
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
12880
12941
  const { trace, context } = require('@opentelemetry/api')
@@ -12886,6 +12947,14 @@ __INSTRUMENTATION_BLOCK__
12886
12947
  const sdk = new NodeSDK({ instrumentations })
12887
12948
  sdk.start()
12888
12949
  ${neatWireCaptureSource(false)}
12950
+ } catch (__neatOtelErr) {
12951
+ const __neatMsg = String((__neatOtelErr && __neatOtelErr.message) || __neatOtelErr)
12952
+ if (/Cannot find module|MODULE_NOT_FOUND/.test(__neatMsg)) {
12953
+ console.warn('[neat] OpenTelemetry is not active: its packages are not installed, so this app is running without OBSERVED tracing. Run your package manager install and restart to enable it.')
12954
+ } else {
12955
+ console.warn('[neat] OpenTelemetry failed to start; the app is running without OBSERVED tracing: ' + __neatMsg)
12956
+ }
12957
+ }
12889
12958
  `;
12890
12959
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
12891
12960
  ${OTEL_INIT_STAMP}
@@ -15569,9 +15638,183 @@ async function runConnectorCommand(rawArgs, deps = {}) {
15569
15638
  }
15570
15639
  }
15571
15640
 
15572
- // src/cli-verbs.ts
15641
+ // src/hooks-cli.ts
15573
15642
  init_cjs_shims();
15574
15643
  var import_node_path59 = __toESM(require("path"), 1);
15644
+ var import_node_os5 = __toESM(require("os"), 1);
15645
+ var import_node_fs40 = require("fs");
15646
+ var import_node_url5 = require("url");
15647
+ var HOOK_FILENAME = "neat-search-nudge.mjs";
15648
+ var GUIDE_FILENAME = "GRAPH_FIRST.md";
15649
+ var GUIDE_INSTALL_NAME = "neat-graph-first.md";
15650
+ var HOOK_MATCHER = "Grep|Glob|Bash";
15651
+ function moduleDir() {
15652
+ return typeof __dirname !== "undefined" ? __dirname : import_node_path59.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
15653
+ }
15654
+ async function readSkillAsset(rel) {
15655
+ const here = moduleDir();
15656
+ const candidates = [
15657
+ import_node_path59.default.resolve(here, "../../claude-skill", rel),
15658
+ import_node_path59.default.resolve(here, "../../../claude-skill", rel),
15659
+ import_node_path59.default.resolve(here, "../claude-skill", rel)
15660
+ ];
15661
+ for (const candidate of candidates) {
15662
+ try {
15663
+ return await import_node_fs40.promises.readFile(candidate, "utf8");
15664
+ } catch {
15665
+ }
15666
+ }
15667
+ throw new Error(
15668
+ `neat hooks: could not find @neat.is/claude-skill/${rel} \u2014 is the package installed?`
15669
+ );
15670
+ }
15671
+ function neatHome3() {
15672
+ const override = process.env.NEAT_HOME;
15673
+ if (override && override.length > 0) return import_node_path59.default.resolve(override);
15674
+ return import_node_path59.default.join(import_node_os5.default.homedir(), ".neat");
15675
+ }
15676
+ function claudeSettingsPath() {
15677
+ const override = process.env.NEAT_CLAUDE_SETTINGS;
15678
+ if (override && override.length > 0) return import_node_path59.default.resolve(override);
15679
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os5.default.homedir();
15680
+ return import_node_path59.default.join(home, ".claude", "settings.json");
15681
+ }
15682
+ function installedHookPath() {
15683
+ return import_node_path59.default.join(neatHome3(), "hooks", HOOK_FILENAME);
15684
+ }
15685
+ function isNeatSearchEntry(entry2) {
15686
+ return (entry2.hooks ?? []).some(
15687
+ (h) => typeof h.command === "string" && h.command.includes(HOOK_FILENAME)
15688
+ );
15689
+ }
15690
+ function neatHookEntry(command) {
15691
+ return { matcher: HOOK_MATCHER, hooks: [{ type: "command", command }] };
15692
+ }
15693
+ function hookCommand(scriptPath) {
15694
+ return `node "${scriptPath}"`;
15695
+ }
15696
+ async function runHooks(opts) {
15697
+ if (opts.printHook) {
15698
+ process.stdout.write(await readSkillAsset(`hooks/${HOOK_FILENAME}`));
15699
+ return { exitCode: 0 };
15700
+ }
15701
+ if (opts.printGuide) {
15702
+ process.stdout.write(await readSkillAsset(GUIDE_FILENAME));
15703
+ return { exitCode: 0 };
15704
+ }
15705
+ if (opts.printSettings) {
15706
+ const block = {
15707
+ hooks: { PreToolUse: [neatHookEntry(hookCommand(installedHookPath()))] }
15708
+ };
15709
+ process.stdout.write(JSON.stringify(block, null, 2) + "\n");
15710
+ return { exitCode: 0 };
15711
+ }
15712
+ if (opts.apply) {
15713
+ const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
15714
+ const guide = await readSkillAsset(GUIDE_FILENAME);
15715
+ const scriptPath = installedHookPath();
15716
+ await import_node_fs40.promises.mkdir(import_node_path59.default.dirname(scriptPath), { recursive: true });
15717
+ await import_node_fs40.promises.writeFile(scriptPath, hookScript, { mode: 493 });
15718
+ const guidePath = import_node_path59.default.join(neatHome3(), GUIDE_INSTALL_NAME);
15719
+ await import_node_fs40.promises.writeFile(guidePath, guide, "utf8");
15720
+ const settingsFile = claudeSettingsPath();
15721
+ let settings = {};
15722
+ try {
15723
+ settings = JSON.parse(await import_node_fs40.promises.readFile(settingsFile, "utf8"));
15724
+ } catch (err) {
15725
+ if (err.code !== "ENOENT") {
15726
+ console.error(
15727
+ `neat hooks: failed to read ${settingsFile} \u2014 ${err.message}`
15728
+ );
15729
+ return { exitCode: 1 };
15730
+ }
15731
+ }
15732
+ const hooks = settings.hooks ?? {};
15733
+ const preToolUse = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
15734
+ const command = hookCommand(scriptPath);
15735
+ const existingIdx = preToolUse.findIndex(isNeatSearchEntry);
15736
+ if (existingIdx >= 0) {
15737
+ preToolUse[existingIdx] = neatHookEntry(command);
15738
+ } else {
15739
+ preToolUse.push(neatHookEntry(command));
15740
+ }
15741
+ const merged = {
15742
+ ...settings,
15743
+ hooks: { ...hooks, PreToolUse: preToolUse }
15744
+ };
15745
+ await import_node_fs40.promises.mkdir(import_node_path59.default.dirname(settingsFile), { recursive: true });
15746
+ await import_node_fs40.promises.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
15747
+ console.log(`neat hooks: installed the search-nudge hook`);
15748
+ console.log(` script: ${scriptPath}`);
15749
+ console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
15750
+ console.log(` guidance: ${guidePath}`);
15751
+ console.log("");
15752
+ console.log("restart Claude Code to load the hook. On a Grep/Glob or a Bash grep,");
15753
+ console.log("your agent will now be nudged to query NEAT first.");
15754
+ console.log("");
15755
+ console.log("The hook is Claude-Code-specific. For agents on other harnesses, paste");
15756
+ console.log(`the guidance above into your project instructions (CLAUDE.md / AGENTS.md).`);
15757
+ return { exitCode: 0 };
15758
+ }
15759
+ usage();
15760
+ return { exitCode: 0 };
15761
+ }
15762
+ function usage() {
15763
+ console.log("neat hooks \u2014 wire NEAT into your agent so it queries the graph before grepping");
15764
+ console.log("");
15765
+ console.log(" --apply install the Claude Code search-nudge hook and write the");
15766
+ console.log(" graph-first guidance to ~/.neat/, merging into");
15767
+ console.log(" ~/.claude/settings.json without touching your other hooks");
15768
+ console.log(" --print-hook print the hook script to stdout");
15769
+ console.log(" --print-guide print the agent-agnostic graph-first guidance to stdout");
15770
+ console.log(" --print-settings print the settings.json PreToolUse block --apply would add");
15771
+ console.log("");
15772
+ console.log("The hook is a gentle, non-blocking nudge \u2014 searches still run. It is");
15773
+ console.log("Claude-Code-specific; other harnesses get the same steer from the guidance.");
15774
+ }
15775
+ async function runHooksCommand(args) {
15776
+ const opts = {
15777
+ apply: false,
15778
+ printHook: false,
15779
+ printGuide: false,
15780
+ printSettings: false
15781
+ };
15782
+ for (const arg of args) {
15783
+ switch (arg) {
15784
+ case "--apply":
15785
+ opts.apply = true;
15786
+ break;
15787
+ case "--print-hook":
15788
+ opts.printHook = true;
15789
+ break;
15790
+ case "--print-guide":
15791
+ opts.printGuide = true;
15792
+ break;
15793
+ case "--print-settings":
15794
+ opts.printSettings = true;
15795
+ break;
15796
+ case "-h":
15797
+ case "--help":
15798
+ usage();
15799
+ return 0;
15800
+ default:
15801
+ console.error(`neat hooks: unknown flag "${arg}"`);
15802
+ usage();
15803
+ return 2;
15804
+ }
15805
+ }
15806
+ try {
15807
+ const { exitCode } = await runHooks(opts);
15808
+ return exitCode;
15809
+ } catch (err) {
15810
+ console.error(err.message);
15811
+ return 1;
15812
+ }
15813
+ }
15814
+
15815
+ // src/cli-verbs.ts
15816
+ init_cjs_shims();
15817
+ var import_node_path60 = __toESM(require("path"), 1);
15575
15818
 
15576
15819
  // src/cli-client.ts
15577
15820
  init_cjs_shims();
@@ -15600,10 +15843,10 @@ function createHttpClient(baseUrl, bearerToken) {
15600
15843
  const root = baseUrl.replace(/\/$/, "");
15601
15844
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
15602
15845
  return {
15603
- async get(path61) {
15846
+ async get(path62) {
15604
15847
  let res;
15605
15848
  try {
15606
- res = await fetch(`${root}${path61}`, {
15849
+ res = await fetch(`${root}${path62}`, {
15607
15850
  headers: { ...authHeader }
15608
15851
  });
15609
15852
  } catch (err) {
@@ -15615,16 +15858,16 @@ function createHttpClient(baseUrl, bearerToken) {
15615
15858
  const body = await res.text().catch(() => "");
15616
15859
  throw new HttpError(
15617
15860
  res.status,
15618
- `${res.status} ${res.statusText} on GET ${path61}: ${body}`,
15861
+ `${res.status} ${res.statusText} on GET ${path62}: ${body}`,
15619
15862
  body
15620
15863
  );
15621
15864
  }
15622
15865
  return await res.json();
15623
15866
  },
15624
- async post(path61, body) {
15867
+ async post(path62, body) {
15625
15868
  let res;
15626
15869
  try {
15627
- res = await fetch(`${root}${path61}`, {
15870
+ res = await fetch(`${root}${path62}`, {
15628
15871
  method: "POST",
15629
15872
  headers: { "content-type": "application/json", ...authHeader },
15630
15873
  body: JSON.stringify(body)
@@ -15638,7 +15881,7 @@ function createHttpClient(baseUrl, bearerToken) {
15638
15881
  const text = await res.text().catch(() => "");
15639
15882
  throw new HttpError(
15640
15883
  res.status,
15641
- `${res.status} ${res.statusText} on POST ${path61}: ${text}`,
15884
+ `${res.status} ${res.statusText} on POST ${path62}: ${text}`,
15642
15885
  text
15643
15886
  );
15644
15887
  }
@@ -15652,12 +15895,12 @@ function projectPath(project, suffix) {
15652
15895
  }
15653
15896
  async function runRootCause(client, input) {
15654
15897
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
15655
- const path61 = projectPath(
15898
+ const path62 = projectPath(
15656
15899
  input.project,
15657
15900
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
15658
15901
  );
15659
15902
  try {
15660
- const result = await client.get(path61);
15903
+ const result = await client.get(path62);
15661
15904
  const arrowPath = result.traversalPath.join(" \u2190 ");
15662
15905
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
15663
15906
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -15683,12 +15926,12 @@ async function runRootCause(client, input) {
15683
15926
  }
15684
15927
  async function runBlastRadius(client, input) {
15685
15928
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
15686
- const path61 = projectPath(
15929
+ const path62 = projectPath(
15687
15930
  input.project,
15688
15931
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
15689
15932
  );
15690
15933
  try {
15691
- const result = await client.get(path61);
15934
+ const result = await client.get(path62);
15692
15935
  if (result.totalAffected === 0) {
15693
15936
  return {
15694
15937
  summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
@@ -15722,12 +15965,12 @@ function formatBlastEntry(n) {
15722
15965
  }
15723
15966
  async function runDependencies(client, input) {
15724
15967
  const depth = input.depth ?? 3;
15725
- const path61 = projectPath(
15968
+ const path62 = projectPath(
15726
15969
  input.project,
15727
15970
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
15728
15971
  );
15729
15972
  try {
15730
- const result = await client.get(path61);
15973
+ const result = await client.get(path62);
15731
15974
  if (result.total === 0) {
15732
15975
  return {
15733
15976
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -15819,9 +16062,9 @@ function formatDuration(ms) {
15819
16062
  return `${Math.round(h / 24)}d`;
15820
16063
  }
15821
16064
  async function runIncidents(client, input) {
15822
- const path61 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
16065
+ const path62 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
15823
16066
  try {
15824
- const body = await client.get(path61);
16067
+ const body = await client.get(path62);
15825
16068
  const events = body.events;
15826
16069
  if (events.length === 0) {
15827
16070
  return {
@@ -16111,7 +16354,7 @@ async function resolveProjectEntry(opts) {
16111
16354
  const cwd = opts.cwd ?? process.cwd();
16112
16355
  const resolvedCwd = await normalizeProjectPath(cwd);
16113
16356
  for (const entry2 of entries) {
16114
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path59.default.sep}`)) {
16357
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path60.default.sep}`)) {
16115
16358
  return entry2;
16116
16359
  }
16117
16360
  }
@@ -16276,7 +16519,7 @@ function isNpxInvocation() {
16276
16519
  function commandPrefix() {
16277
16520
  return isNpxInvocation() ? "npx neat.is" : "neat";
16278
16521
  }
16279
- function usage() {
16522
+ function usage2() {
16280
16523
  const neat = commandPrefix();
16281
16524
  console.log("Installed via npx? Prefix commands with `npx neat.is`, or install once: `npm i -g neat.is`.");
16282
16525
  console.log("");
@@ -16313,6 +16556,14 @@ function usage() {
16313
16556
  console.log(" Flags:");
16314
16557
  console.log(" --print-config print the JSON snippet to stdout");
16315
16558
  console.log(" --apply merge mcpServers.neat into ~/.claude.json");
16559
+ console.log(" hooks Wire NEAT into your agent so it queries the graph before");
16560
+ console.log(" grepping. Installs a gentle Claude Code search-nudge hook and");
16561
+ console.log(" writes agent-agnostic graph-first guidance for other harnesses.");
16562
+ console.log(" Flags:");
16563
+ console.log(" --apply install the hook + guidance");
16564
+ console.log(" --print-hook print the hook script");
16565
+ console.log(" --print-guide print the graph-first guidance");
16566
+ console.log(" --print-settings print the settings.json block --apply adds");
16316
16567
  console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
16317
16568
  console.log(" emit a docker-compose / systemd / docker run artifact, and");
16318
16569
  console.log(" print the OTel env-vars block to paste into your platform.");
@@ -16557,7 +16808,7 @@ async function buildPatchSections(services, project) {
16557
16808
  }
16558
16809
  async function runInit(opts) {
16559
16810
  const written = [];
16560
- const stat = await import_node_fs40.promises.stat(opts.scanPath).catch(() => null);
16811
+ const stat = await import_node_fs41.promises.stat(opts.scanPath).catch(() => null);
16561
16812
  if (!stat || !stat.isDirectory()) {
16562
16813
  console.error(`neat init: ${opts.scanPath} is not a directory`);
16563
16814
  return { exitCode: 2, writtenFiles: written };
@@ -16566,13 +16817,13 @@ async function runInit(opts) {
16566
16817
  printDiscoveryReport(opts, services);
16567
16818
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
16568
16819
  const patch = renderPatch(sections);
16569
- const patchPath = import_node_path60.default.join(opts.scanPath, "neat.patch");
16820
+ const patchPath = import_node_path61.default.join(opts.scanPath, "neat.patch");
16570
16821
  if (opts.dryRun) {
16571
- await import_node_fs40.promises.writeFile(patchPath, patch, "utf8");
16822
+ await import_node_fs41.promises.writeFile(patchPath, patch, "utf8");
16572
16823
  written.push(patchPath);
16573
16824
  console.log(`dry-run: patch written to ${patchPath}`);
16574
- const gitignorePath = import_node_path60.default.join(opts.scanPath, ".gitignore");
16575
- const gitignoreExists = await import_node_fs40.promises.stat(gitignorePath).then(() => true).catch(() => false);
16825
+ const gitignorePath = import_node_path61.default.join(opts.scanPath, ".gitignore");
16826
+ const gitignoreExists = await import_node_fs41.promises.stat(gitignorePath).then(() => true).catch(() => false);
16576
16827
  const verb = gitignoreExists ? "append" : "create";
16577
16828
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
16578
16829
  console.log("rerun without --dry-run to register and snapshot.");
@@ -16583,9 +16834,9 @@ async function runInit(opts) {
16583
16834
  const graph = getGraph(graphKey);
16584
16835
  const projectPaths = pathsForProject(
16585
16836
  graphKey,
16586
- import_node_path60.default.join(opts.scanPath, "neat-out")
16837
+ import_node_path61.default.join(opts.scanPath, "neat-out")
16587
16838
  );
16588
- const errorsPath = import_node_path60.default.join(import_node_path60.default.dirname(opts.outPath), import_node_path60.default.basename(projectPaths.errorsPath));
16839
+ const errorsPath = import_node_path61.default.join(import_node_path61.default.dirname(opts.outPath), import_node_path61.default.basename(projectPaths.errorsPath));
16589
16840
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
16590
16841
  await saveGraphToDisk(graph, opts.outPath);
16591
16842
  written.push(opts.outPath);
@@ -16664,7 +16915,7 @@ async function runInit(opts) {
16664
16915
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
16665
16916
  }
16666
16917
  } else {
16667
- await import_node_fs40.promises.writeFile(patchPath, patch, "utf8");
16918
+ await import_node_fs41.promises.writeFile(patchPath, patch, "utf8");
16668
16919
  written.push(patchPath);
16669
16920
  }
16670
16921
  }
@@ -16704,9 +16955,9 @@ var CLAUDE_SKILL_CONFIG = {
16704
16955
  };
16705
16956
  function claudeConfigPath() {
16706
16957
  const override = process.env.NEAT_CLAUDE_CONFIG;
16707
- if (override && override.length > 0) return import_node_path60.default.resolve(override);
16958
+ if (override && override.length > 0) return import_node_path61.default.resolve(override);
16708
16959
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
16709
- return import_node_path60.default.join(home, ".claude.json");
16960
+ return import_node_path61.default.join(home, ".claude.json");
16710
16961
  }
16711
16962
  async function runSkill(opts) {
16712
16963
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -16718,7 +16969,7 @@ async function runSkill(opts) {
16718
16969
  const target = claudeConfigPath();
16719
16970
  let existing = {};
16720
16971
  try {
16721
- existing = JSON.parse(await import_node_fs40.promises.readFile(target, "utf8"));
16972
+ existing = JSON.parse(await import_node_fs41.promises.readFile(target, "utf8"));
16722
16973
  } catch (err) {
16723
16974
  if (err.code !== "ENOENT") {
16724
16975
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -16730,10 +16981,13 @@ async function runSkill(opts) {
16730
16981
  ...existing,
16731
16982
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
16732
16983
  };
16733
- await import_node_fs40.promises.mkdir(import_node_path60.default.dirname(target), { recursive: true });
16734
- await import_node_fs40.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
16984
+ await import_node_fs41.promises.mkdir(import_node_path61.default.dirname(target), { recursive: true });
16985
+ await import_node_fs41.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
16735
16986
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
16736
16987
  console.log("restart Claude Code to pick up the new MCP server.");
16988
+ console.log("");
16989
+ console.log("Tip: run `neat hooks --apply` to also install the search-nudge hook, so");
16990
+ console.log("your agent reaches for the graph before it grep-scans the repo.");
16737
16991
  return { exitCode: 0 };
16738
16992
  }
16739
16993
  console.log("neat skill \u2014 Claude Code MCP drop-in for NEAT");
@@ -16746,13 +17000,16 @@ async function runSkill(opts) {
16746
17000
  console.log("");
16747
17001
  console.log("The MCP server reads NEAT_CORE_URL for the daemon URL \u2014 point it at a");
16748
17002
  console.log("non-default daemon by editing that value in the generated config.");
17003
+ console.log("");
17004
+ console.log("See also `neat hooks --apply` \u2014 the search-nudge hook + graph-first guidance");
17005
+ console.log("that steer an agent to query the graph before falling back to text search.");
16749
17006
  return { exitCode: 0 };
16750
17007
  }
16751
17008
  async function main() {
16752
17009
  const argv = process.argv.slice(2);
16753
17010
  const cmd0 = argv[0];
16754
17011
  if (cmd0 === "-h" || cmd0 === "--help") {
16755
- usage();
17012
+ usage2();
16756
17013
  process.exit(0);
16757
17014
  }
16758
17015
  if (cmd0 === "--version" || cmd0 === "-v" || cmd0 === "version") {
@@ -16764,6 +17021,11 @@ async function main() {
16764
17021
  if (code !== 0) process.exit(code);
16765
17022
  return;
16766
17023
  }
17024
+ if (cmd0 === "hooks") {
17025
+ const code = await runHooksCommand(argv.slice(1));
17026
+ if (code !== 0) process.exit(code);
17027
+ return;
17028
+ }
16767
17029
  const argvParsed = parseArgs(argv);
16768
17030
  if (argvParsed.positional.length === 0) {
16769
17031
  const orchestratorCode2 = await tryOrchestrator(process.cwd(), argvParsed);
@@ -16778,19 +17040,19 @@ async function main() {
16778
17040
  const target = positional[0];
16779
17041
  if (!target) {
16780
17042
  console.error("neat init: missing <path>");
16781
- usage();
17043
+ usage2();
16782
17044
  process.exit(2);
16783
17045
  }
16784
17046
  if (apply3 && dryRun) {
16785
17047
  console.error("neat init: --apply and --dry-run are mutually exclusive");
16786
17048
  process.exit(2);
16787
17049
  }
16788
- const scanPath = import_node_path60.default.resolve(target);
17050
+ const scanPath = import_node_path61.default.resolve(target);
16789
17051
  const projectExplicit = parsed.project !== null;
16790
- const projectName = projectExplicit ? project : import_node_path60.default.basename(scanPath);
17052
+ const projectName = projectExplicit ? project : import_node_path61.default.basename(scanPath);
16791
17053
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
16792
- const fallback = pathsForProject(projectKey, import_node_path60.default.join(scanPath, "neat-out")).snapshotPath;
16793
- const outPath = import_node_path60.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
17054
+ const fallback = pathsForProject(projectKey, import_node_path61.default.join(scanPath, "neat-out")).snapshotPath;
17055
+ const outPath = import_node_path61.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
16794
17056
  const result = await runInit({
16795
17057
  scanPath,
16796
17058
  outPath,
@@ -16808,24 +17070,24 @@ async function main() {
16808
17070
  const target = positional[0];
16809
17071
  if (!target) {
16810
17072
  console.error("neat watch: missing <path>");
16811
- usage();
17073
+ usage2();
16812
17074
  process.exit(2);
16813
17075
  }
16814
- const scanPath = import_node_path60.default.resolve(target);
16815
- const stat = await import_node_fs40.promises.stat(scanPath).catch(() => null);
17076
+ const scanPath = import_node_path61.default.resolve(target);
17077
+ const stat = await import_node_fs41.promises.stat(scanPath).catch(() => null);
16816
17078
  if (!stat || !stat.isDirectory()) {
16817
17079
  console.error(`neat watch: ${scanPath} is not a directory`);
16818
17080
  process.exit(2);
16819
17081
  }
16820
- const projectPaths = pathsForProject(project, import_node_path60.default.join(scanPath, "neat-out"));
16821
- const outPath = import_node_path60.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
16822
- const errorsPath = import_node_path60.default.resolve(
16823
- process.env.NEAT_ERRORS_PATH ?? import_node_path60.default.join(import_node_path60.default.dirname(outPath), import_node_path60.default.basename(projectPaths.errorsPath))
17082
+ const projectPaths = pathsForProject(project, import_node_path61.default.join(scanPath, "neat-out"));
17083
+ const outPath = import_node_path61.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
17084
+ const errorsPath = import_node_path61.default.resolve(
17085
+ process.env.NEAT_ERRORS_PATH ?? import_node_path61.default.join(import_node_path61.default.dirname(outPath), import_node_path61.default.basename(projectPaths.errorsPath))
16824
17086
  );
16825
- const staleEventsPath = import_node_path60.default.resolve(
16826
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path60.default.join(import_node_path60.default.dirname(outPath), import_node_path60.default.basename(projectPaths.staleEventsPath))
17087
+ const staleEventsPath = import_node_path61.default.resolve(
17088
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path61.default.join(import_node_path61.default.dirname(outPath), import_node_path61.default.basename(projectPaths.staleEventsPath))
16827
17089
  );
16828
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path60.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
17090
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path61.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
16829
17091
  const handle = await startWatch(getGraph(project), {
16830
17092
  scanPath,
16831
17093
  outPath,
@@ -16867,7 +17129,7 @@ async function main() {
16867
17129
  const name = positional[0];
16868
17130
  if (!name) {
16869
17131
  console.error("neat pause: missing <name>");
16870
- usage();
17132
+ usage2();
16871
17133
  process.exit(2);
16872
17134
  }
16873
17135
  const daemon = await findDaemonByProject(name);
@@ -16892,7 +17154,7 @@ async function main() {
16892
17154
  const name = positional[0];
16893
17155
  if (!name) {
16894
17156
  console.error("neat resume: missing <name>");
16895
- usage();
17157
+ usage2();
16896
17158
  process.exit(2);
16897
17159
  }
16898
17160
  const daemon = await findDaemonByProject(name);
@@ -16922,7 +17184,7 @@ async function main() {
16922
17184
  const name = positional[0];
16923
17185
  if (!name) {
16924
17186
  console.error("neat uninstall: missing <name>");
16925
- usage();
17187
+ usage2();
16926
17188
  process.exit(2);
16927
17189
  }
16928
17190
  const daemon = await findDaemonByProject(name);
@@ -17004,15 +17266,15 @@ async function main() {
17004
17266
  return;
17005
17267
  }
17006
17268
  console.error(`neat: unknown command "${cmd}"`);
17007
- usage();
17269
+ usage2();
17008
17270
  process.exit(1);
17009
17271
  }
17010
17272
  async function tryOrchestrator(cmd, parsed) {
17011
- const scanPath = import_node_path60.default.resolve(cmd);
17012
- const stat = await import_node_fs40.promises.stat(scanPath).catch(() => null);
17273
+ const scanPath = import_node_path61.default.resolve(cmd);
17274
+ const stat = await import_node_fs41.promises.stat(scanPath).catch(() => null);
17013
17275
  if (!stat || !stat.isDirectory()) return null;
17014
17276
  const projectExplicit = parsed.project !== null;
17015
- const projectName = projectExplicit ? parsed.project : import_node_path60.default.basename(scanPath);
17277
+ const projectName = projectExplicit ? parsed.project : import_node_path61.default.basename(scanPath);
17016
17278
  const result = await runOrchestrator({
17017
17279
  scanPath,
17018
17280
  project: projectName,