@neat.is/core 0.9.5-dev.20260825 → 0.9.5

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 path93 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
- if (exactUnauthPaths.has(path93) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path93)) {
64
+ const path94 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
+ if (exactUnauthPaths.has(path94) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path94)) {
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 path93 = q === -1 ? v : v.slice(0, q);
419
- if (path93.length > 0) return path93;
418
+ const path94 = q === -1 ? v : v.slice(0, q);
419
+ if (path94.length > 0) return path94;
420
420
  }
421
421
  }
422
422
  return void 0;
@@ -803,9 +803,9 @@ __export(cli_exports, {
803
803
  });
804
804
  module.exports = __toCommonJS(cli_exports);
805
805
  init_cjs_shims();
806
- var import_node_path92 = __toESM(require("path"), 1);
806
+ var import_node_path93 = __toESM(require("path"), 1);
807
807
  var import_node_os8 = __toESM(require("os"), 1);
808
- var import_node_fs56 = require("fs");
808
+ var import_node_fs57 = require("fs");
809
809
 
810
810
  // src/banner.ts
811
811
  init_cjs_shims();
@@ -1373,19 +1373,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1373
1373
  function longestIncomingWalk(graph, start, maxDepth) {
1374
1374
  let best = { path: [start], edges: [] };
1375
1375
  const visited = /* @__PURE__ */ new Set([start]);
1376
- function step(node, path93, edges) {
1377
- if (path93.length > best.path.length) {
1378
- best = { path: [...path93], edges: [...edges] };
1376
+ function step(node, path94, edges) {
1377
+ if (path94.length > best.path.length) {
1378
+ best = { path: [...path94], edges: [...edges] };
1379
1379
  }
1380
- if (path93.length - 1 >= maxDepth) return;
1380
+ if (path94.length - 1 >= maxDepth) return;
1381
1381
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1382
1382
  for (const [srcId, edge] of incoming) {
1383
1383
  if (visited.has(srcId)) continue;
1384
1384
  visited.add(srcId);
1385
- path93.push(srcId);
1385
+ path94.push(srcId);
1386
1386
  edges.push(edge);
1387
- step(srcId, path93, edges);
1388
- path93.pop();
1387
+ step(srcId, path94, edges);
1388
+ path94.pop();
1389
1389
  edges.pop();
1390
1390
  visited.delete(srcId);
1391
1391
  }
@@ -1600,20 +1600,20 @@ function dominantFailingCall(graph, serviceId16, visited) {
1600
1600
  return best;
1601
1601
  }
1602
1602
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1603
- const path93 = [originServiceId];
1603
+ const path94 = [originServiceId];
1604
1604
  const edges = [];
1605
1605
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1606
1606
  let current = originServiceId;
1607
1607
  for (let depth = 0; depth < maxDepth; depth++) {
1608
1608
  const hop = dominantFailingCall(graph, current, visited);
1609
1609
  if (!hop) break;
1610
- path93.push(hop.nextService);
1610
+ path94.push(hop.nextService);
1611
1611
  edges.push(hop.edge);
1612
1612
  visited.add(hop.nextService);
1613
1613
  current = hop.nextService;
1614
1614
  }
1615
1615
  if (edges.length === 0) return null;
1616
- return { path: path93, edges, culprit: current };
1616
+ return { path: path94, edges, culprit: current };
1617
1617
  }
1618
1618
  function isStaleCallEdge(e) {
1619
1619
  return e.type === import_types.EdgeType.CALLS && e.provenance === import_types.Provenance.STALE;
@@ -1649,26 +1649,26 @@ function dominantStaleCall(graph, serviceId16, visited) {
1649
1649
  return best;
1650
1650
  }
1651
1651
  function followStaleCallChain(graph, originServiceId, maxDepth) {
1652
- const path93 = [originServiceId];
1652
+ const path94 = [originServiceId];
1653
1653
  const edges = [];
1654
1654
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1655
1655
  let current = originServiceId;
1656
1656
  for (let depth = 0; depth < maxDepth; depth++) {
1657
1657
  const hop = dominantStaleCall(graph, current, visited);
1658
1658
  if (!hop) break;
1659
- path93.push(hop.nextService);
1659
+ path94.push(hop.nextService);
1660
1660
  edges.push(hop.edge);
1661
1661
  visited.add(hop.nextService);
1662
1662
  current = hop.nextService;
1663
1663
  }
1664
1664
  if (edges.length === 0) return null;
1665
- return { path: path93, edges, culprit: current };
1665
+ return { path: path94, edges, culprit: current };
1666
1666
  }
1667
1667
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1668
1668
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1669
1669
  if (!chain) return null;
1670
1670
  const culprit = chain.culprit;
1671
- const path93 = [...chain.path];
1671
+ const path94 = [...chain.path];
1672
1672
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1673
1673
  const baseConfidence = confidenceFromMix(chain.edges);
1674
1674
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1676,14 +1676,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1676
1676
  if (loc) {
1677
1677
  let rootCauseNode = culprit;
1678
1678
  if (loc.fileNode) {
1679
- path93.push(loc.fileNode);
1679
+ path94.push(loc.fileNode);
1680
1680
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1681
1681
  rootCauseNode = loc.fileNode;
1682
1682
  }
1683
1683
  return import_types.RootCauseResultSchema.parse({
1684
1684
  rootCauseNode,
1685
1685
  rootCauseReason: loc.rootCauseReason,
1686
- traversalPath: path93,
1686
+ traversalPath: path94,
1687
1687
  edgeProvenances,
1688
1688
  confidence,
1689
1689
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1695,7 +1695,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1695
1695
  return import_types.RootCauseResultSchema.parse({
1696
1696
  rootCauseNode: culprit,
1697
1697
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1698
- traversalPath: path93,
1698
+ traversalPath: path94,
1699
1699
  edgeProvenances,
1700
1700
  confidence,
1701
1701
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -2200,10 +2200,10 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2200
2200
  traversalPath = staleChain.path;
2201
2201
  edgeProvenances = staleChain.edges.map((e) => e.provenance);
2202
2202
  } else if (top.node !== seedNode) {
2203
- const path93 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
2204
- if (path93) {
2205
- traversalPath = path93.nodes;
2206
- edgeProvenances = path93.edges.map((e) => e.provenance);
2203
+ const path94 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
2204
+ if (path94) {
2205
+ traversalPath = path94.nodes;
2206
+ edgeProvenances = path94.edges.map((e) => e.provenance);
2207
2207
  } else {
2208
2208
  traversalPath = [errorNodeId, top.node];
2209
2209
  edgeProvenances = [top.provenance ?? import_types.Provenance.OBSERVED];
@@ -3651,8 +3651,8 @@ function chiRoutesFromSource(source, parser) {
3651
3651
  chiWalk(tree.rootNode, "", out);
3652
3652
  return out;
3653
3653
  }
3654
- function stripChiRegex(path93) {
3655
- return path93.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
3654
+ function stripChiRegex(path94) {
3655
+ return path94.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
3656
3656
  }
3657
3657
  function chiWalk(node, prefix, out) {
3658
3658
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -4324,9 +4324,9 @@ function rubyRocketRoute(args) {
4324
4324
  if (!pair || pair.type !== "pair") continue;
4325
4325
  const k = pair.childForFieldName("key");
4326
4326
  if (k?.type !== "string") continue;
4327
- const path93 = rubyLiteral(k);
4328
- if (path93 === null) continue;
4329
- return { path: path93, target: rubyLiteral(pair.childForFieldName("value")) };
4327
+ const path94 = rubyLiteral(k);
4328
+ if (path94 === null) continue;
4329
+ return { path: path94, target: rubyLiteral(pair.childForFieldName("value")) };
4330
4330
  }
4331
4331
  return null;
4332
4332
  }
@@ -16686,7 +16686,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
16686
16686
  init_cjs_shims();
16687
16687
  var import_fastify2 = __toESM(require("fastify"), 1);
16688
16688
  var import_cors = __toESM(require("@fastify/cors"), 1);
16689
- var import_types94 = require("@neat.is/types");
16689
+ var import_types98 = require("@neat.is/types");
16690
16690
 
16691
16691
  // src/extend/index.ts
16692
16692
  init_cjs_shims();
@@ -19135,10 +19135,10 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
19135
19135
  // src/connectors/supabase/map.ts
19136
19136
  var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
19137
19137
  var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
19138
- function targetFromRestPath(path93) {
19139
- const rpcMatch = REST_RPC_PATH_RE.exec(path93);
19138
+ function targetFromRestPath(path94) {
19139
+ const rpcMatch = REST_RPC_PATH_RE.exec(path94);
19140
19140
  if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
19141
- const tableMatch = REST_TABLE_PATH_RE.exec(path93);
19141
+ const tableMatch = REST_TABLE_PATH_RE.exec(path94);
19142
19142
  if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
19143
19143
  return null;
19144
19144
  }
@@ -19742,9 +19742,9 @@ function parseFirebaseTargetName(targetName) {
19742
19742
  const secondSep = rest.indexOf(FIELD_SEP);
19743
19743
  if (secondSep === -1) return null;
19744
19744
  const method = rest.slice(0, secondSep);
19745
- const path93 = rest.slice(secondSep + 1);
19746
- if (!resourceName || !method || !path93) return null;
19747
- return { resourceName, method, path: path93 };
19745
+ const path94 = rest.slice(secondSep + 1);
19746
+ if (!resourceName || !method || !path94) return null;
19747
+ return { resourceName, method, path: path94 };
19748
19748
  }
19749
19749
  function resourceNameFor(type, labels) {
19750
19750
  if (!labels) return null;
@@ -19782,14 +19782,14 @@ function mapLogEntryToSignal(entry2) {
19782
19782
  if (!req) return null;
19783
19783
  if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
19784
19784
  const method = req.requestMethod.toUpperCase();
19785
- const path93 = pathFromRequestUrl(req.requestUrl);
19786
- if (path93 === null) return null;
19785
+ const path94 = pathFromRequestUrl(req.requestUrl);
19786
+ if (path94 === null) return null;
19787
19787
  const timestamp = entry2.timestamp;
19788
19788
  if (typeof timestamp !== "string" || timestamp.length === 0) return null;
19789
19789
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
19790
19790
  return {
19791
19791
  targetKind: resourceType,
19792
- targetName: packFirebaseTargetName({ resourceName, method, path: path93 }),
19792
+ targetName: packFirebaseTargetName({ resourceName, method, path: path94 }),
19793
19793
  callCount: 1,
19794
19794
  errorCount: isError ? 1 : 0,
19795
19795
  lastObservedIso: timestamp
@@ -19996,7 +19996,7 @@ function mapEventToSignal(event) {
19996
19996
  if (Number.isNaN(observedAt.getTime())) return null;
19997
19997
  const statusCode = metadata?.statusCode;
19998
19998
  const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
19999
- const path93 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
19999
+ const path94 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
20000
20000
  return {
20001
20001
  targetKind: CLOUDFLARE_TARGET_KIND,
20002
20002
  targetName: scriptName,
@@ -20004,7 +20004,7 @@ function mapEventToSignal(event) {
20004
20004
  errorCount: isError ? 1 : 0,
20005
20005
  lastObservedIso: observedAt.toISOString(),
20006
20006
  method,
20007
- ...path93 ? { path: path93 } : {},
20007
+ ...path94 ? { path: path94 } : {},
20008
20008
  ...typeof statusCode === "number" ? { statusCode } : {},
20009
20009
  ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
20010
20010
  };
@@ -20050,8 +20050,8 @@ function findTaggedWorkerFileNode(graph, workerName) {
20050
20050
  });
20051
20051
  return found;
20052
20052
  }
20053
- function findMatchingRouteNode(graph, serviceName, method, path93) {
20054
- const normalizedPath = normalizePathTemplate(path93);
20053
+ function findMatchingRouteNode(graph, serviceName, method, path94) {
20054
+ const normalizedPath = normalizePathTemplate(path94);
20055
20055
  let found = null;
20056
20056
  graph.forEachNode((id, attrs) => {
20057
20057
  if (found) return;
@@ -20068,10 +20068,10 @@ function createCloudflareResolveTarget(config, graph) {
20068
20068
  return (signal) => {
20069
20069
  if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
20070
20070
  const scriptName = signal.targetName;
20071
- const { method, path: path93 } = signal;
20071
+ const { method, path: path94 } = signal;
20072
20072
  const resolveRouteGrain = (serviceName, wholeFileId) => {
20073
- if (!method || !path93) return wholeFileId;
20074
- return findMatchingRouteNode(graph, serviceName, method, path93) ?? wholeFileId;
20073
+ if (!method || !path94) return wholeFileId;
20074
+ return findMatchingRouteNode(graph, serviceName, method, path94) ?? wholeFileId;
20075
20075
  };
20076
20076
  const mapping = config.workers?.[scriptName];
20077
20077
  if (mapping) {
@@ -20423,9 +20423,9 @@ function parseCloudRunTargetName(targetName) {
20423
20423
  const secondSep = rest.indexOf(FIELD_SEP2);
20424
20424
  if (secondSep === -1) return null;
20425
20425
  const method = rest.slice(0, secondSep);
20426
- const path93 = rest.slice(secondSep + 1);
20427
- if (!serviceName || !method || !path93) return null;
20428
- return { serviceName, method, path: path93 };
20426
+ const path94 = rest.slice(secondSep + 1);
20427
+ if (!serviceName || !method || !path94) return null;
20428
+ return { serviceName, method, path: path94 };
20429
20429
  }
20430
20430
 
20431
20431
  // src/connectors/cloud-run/map.ts
@@ -20454,14 +20454,14 @@ function mapLogEntryToSignal2(entry2) {
20454
20454
  if (!req) return null;
20455
20455
  if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
20456
20456
  const method = req.requestMethod.toUpperCase();
20457
- const path93 = pathFromRequestUrl2(req.requestUrl);
20458
- if (path93 === null) return null;
20457
+ const path94 = pathFromRequestUrl2(req.requestUrl);
20458
+ if (path94 === null) return null;
20459
20459
  const timestamp = entry2.timestamp;
20460
20460
  if (typeof timestamp !== "string" || timestamp.length === 0) return null;
20461
20461
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD4;
20462
20462
  return {
20463
20463
  targetKind: CLOUD_RUN_TARGET_KIND,
20464
- targetName: packCloudRunTargetName({ serviceName, method, path: path93 }),
20464
+ targetName: packCloudRunTargetName({ serviceName, method, path: path94 }),
20465
20465
  callCount: 1,
20466
20466
  errorCount: isError ? 1 : 0,
20467
20467
  lastObservedIso: timestamp
@@ -20500,14 +20500,14 @@ function createCloudRunResolveTarget(graph, config) {
20500
20500
  if (signal.targetKind !== CLOUD_RUN_TARGET_KIND) return null;
20501
20501
  const identity = parseCloudRunTargetName(signal.targetName);
20502
20502
  if (!identity) return null;
20503
- const { serviceName: gcpServiceName, method, path: path93 } = identity;
20503
+ const { serviceName: gcpServiceName, method, path: path94 } = identity;
20504
20504
  const mappedService = config.serviceMap?.[gcpServiceName];
20505
20505
  if (mappedService) {
20506
20506
  const routeNodeId = findMatchingRouteNode2(
20507
20507
  graph,
20508
20508
  mappedService,
20509
20509
  method,
20510
- normalizePathTemplate(path93)
20510
+ normalizePathTemplate(path94)
20511
20511
  );
20512
20512
  if (routeNodeId) {
20513
20513
  return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types79.EdgeType.CALLS };
@@ -20555,9 +20555,229 @@ function createCloudRunConnector(graph, config = {}) {
20555
20555
  };
20556
20556
  }
20557
20557
 
20558
+ // src/connectors/gcp-lb/index.ts
20559
+ init_cjs_shims();
20560
+
20561
+ // src/connectors/gcp-lb/client.ts
20562
+ init_cjs_shims();
20563
+ function gcpLbRequestLogName(projectId) {
20564
+ return `projects/${projectId}/logs/requests`;
20565
+ }
20566
+ function buildGcpLbEntriesFilter(projectId, sinceIso) {
20567
+ return [
20568
+ `logName = "${gcpLbRequestLogName(projectId)}"`,
20569
+ `resource.type = "${GCP_LB_RESOURCE_TYPE}"`,
20570
+ 'httpRequest.requestMethod != ""',
20571
+ `timestamp >= "${sinceIso}"`
20572
+ ].join(" AND ");
20573
+ }
20574
+ var GCP_LB_RESOURCE_TYPE = "http_load_balancer";
20575
+ var DEFAULT_LOOKBACK_MS3 = 24 * 60 * 60 * 1e3;
20576
+ var ENTRIES_LIST_URL3 = "https://logging.googleapis.com/v2/entries:list";
20577
+ var PAGE_SIZE3 = 1e3;
20578
+ var MAX_PAGES3 = 20;
20579
+ async function fetchGcpLbRequestLogEntries(creds, sinceIso, apiUrl = ENTRIES_LIST_URL3) {
20580
+ const filter = buildGcpLbEntriesFilter(creds.projectId, sinceIso);
20581
+ const out = [];
20582
+ let pageToken;
20583
+ for (let page = 0; page < MAX_PAGES3; page++) {
20584
+ const body = {
20585
+ resourceNames: [`projects/${creds.projectId}`],
20586
+ filter,
20587
+ orderBy: "timestamp asc",
20588
+ pageSize: PAGE_SIZE3,
20589
+ ...pageToken ? { pageToken } : {}
20590
+ };
20591
+ const res = await junctionFetch(
20592
+ apiUrl,
20593
+ {
20594
+ method: "POST",
20595
+ headers: {
20596
+ ...bearerAuthHeader(creds.accessToken),
20597
+ "Content-Type": "application/json"
20598
+ },
20599
+ body: JSON.stringify(body)
20600
+ },
20601
+ // accountKey: the GCP project id — one customer's Cloud Logging quota is
20602
+ // scoped per GCP project (ADR-131's per-(provider, accountKey) rate-limit
20603
+ // bucket), the same key Cloud Run's and Firebase's connectors use.
20604
+ { provider: "gcp-lb", accountKey: creds.projectId }
20605
+ );
20606
+ if (!res.ok) {
20607
+ throw new Error(`Cloud Logging entries.list failed: ${res.status} ${res.statusText}`);
20608
+ }
20609
+ const json = await res.json();
20610
+ if (Array.isArray(json.entries)) out.push(...json.entries);
20611
+ if (!json.nextPageToken) break;
20612
+ pageToken = json.nextPageToken;
20613
+ }
20614
+ return out;
20615
+ }
20616
+
20617
+ // src/connectors/gcp-lb/map.ts
20618
+ init_cjs_shims();
20619
+
20620
+ // src/connectors/gcp-lb/types.ts
20621
+ init_cjs_shims();
20622
+ function readGcpLbCredentials(raw) {
20623
+ const projectId = raw["projectId"];
20624
+ const accessToken = raw["accessToken"];
20625
+ if (typeof projectId !== "string" || projectId.length === 0) {
20626
+ throw new Error("gcp-lb connector: credentials.projectId must be a non-empty string");
20627
+ }
20628
+ if (typeof accessToken !== "string" || accessToken.length === 0) {
20629
+ throw new Error("gcp-lb connector: credentials.accessToken must be a non-empty string");
20630
+ }
20631
+ return { projectId, accessToken };
20632
+ }
20633
+ var GCP_LB_TARGET_KIND = "http_load_balancer";
20634
+ var FIELD_SEP3 = "\0";
20635
+ function packGcpLbTargetName(identity) {
20636
+ return [identity.backendServiceName, identity.method, identity.path].join(FIELD_SEP3);
20637
+ }
20638
+ function parseGcpLbTargetName(targetName) {
20639
+ const firstSep = targetName.indexOf(FIELD_SEP3);
20640
+ if (firstSep === -1) return null;
20641
+ const backendServiceName = targetName.slice(0, firstSep);
20642
+ const rest = targetName.slice(firstSep + 1);
20643
+ const secondSep = rest.indexOf(FIELD_SEP3);
20644
+ if (secondSep === -1) return null;
20645
+ const method = rest.slice(0, secondSep);
20646
+ const path94 = rest.slice(secondSep + 1);
20647
+ if (!backendServiceName || !method || !path94) return null;
20648
+ return { backendServiceName, method, path: path94 };
20649
+ }
20650
+
20651
+ // src/connectors/gcp-lb/map.ts
20652
+ var GCP_LB_RESOURCE_TYPE2 = "http_load_balancer";
20653
+ function pathFromRequestUrl3(requestUrl) {
20654
+ if (typeof requestUrl !== "string" || requestUrl.length === 0) return null;
20655
+ if (requestUrl.startsWith("/")) {
20656
+ const withoutQuery = requestUrl.split("?")[0];
20657
+ return withoutQuery && withoutQuery.length > 0 ? withoutQuery : "/";
20658
+ }
20659
+ try {
20660
+ const candidate = requestUrl.startsWith("//") ? `https:${requestUrl}` : requestUrl;
20661
+ const parsed = new URL(candidate);
20662
+ return parsed.pathname || "/";
20663
+ } catch {
20664
+ return null;
20665
+ }
20666
+ }
20667
+ var ERROR_STATUS_THRESHOLD5 = 500;
20668
+ function mapLogEntryToSignal3(entry2) {
20669
+ if (!entry2 || typeof entry2 !== "object") return null;
20670
+ if (entry2.resource?.type !== GCP_LB_RESOURCE_TYPE2) return null;
20671
+ const backendServiceName = entry2.resource?.labels?.["backend_service_name"];
20672
+ if (typeof backendServiceName !== "string" || backendServiceName.length === 0) return null;
20673
+ const req = entry2.httpRequest;
20674
+ if (!req) return null;
20675
+ if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
20676
+ const method = req.requestMethod.toUpperCase();
20677
+ const path94 = pathFromRequestUrl3(req.requestUrl);
20678
+ if (path94 === null) return null;
20679
+ const timestamp = entry2.timestamp;
20680
+ if (typeof timestamp !== "string" || timestamp.length === 0) return null;
20681
+ const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD5;
20682
+ return {
20683
+ targetKind: GCP_LB_TARGET_KIND,
20684
+ targetName: packGcpLbTargetName({ backendServiceName, method, path: path94 }),
20685
+ callCount: 1,
20686
+ errorCount: isError ? 1 : 0,
20687
+ lastObservedIso: timestamp
20688
+ };
20689
+ }
20690
+ function mapLogEntriesToSignals3(entries) {
20691
+ const out = [];
20692
+ for (const entry2 of entries) {
20693
+ const signal = mapLogEntryToSignal3(entry2);
20694
+ if (signal) out.push(signal);
20695
+ }
20696
+ return out;
20697
+ }
20698
+
20699
+ // src/connectors/gcp-lb/resolve.ts
20700
+ init_cjs_shims();
20701
+ var import_types83 = require("@neat.is/types");
20702
+ var GCP_LB_BACKEND_INFRA_KIND = "gcp-lb-backend";
20703
+ function findMatchingRouteNode3(graph, serviceName, method, normalizedPath) {
20704
+ let found = null;
20705
+ graph.forEachNode((_id, attrs) => {
20706
+ if (found) return;
20707
+ const node = attrs;
20708
+ if (node.type !== import_types83.NodeType.RouteNode) return;
20709
+ const route = attrs;
20710
+ if (route.service !== serviceName || !route.pathTemplate) return;
20711
+ if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
20712
+ const routeMethod = route.method.toUpperCase();
20713
+ if (routeMethod !== "ALL" && routeMethod !== method) return;
20714
+ found = route.id;
20715
+ });
20716
+ return found;
20717
+ }
20718
+ function createGcpLbResolveTarget(graph, config) {
20719
+ return (signal) => {
20720
+ if (signal.targetKind !== GCP_LB_TARGET_KIND) return null;
20721
+ const identity = parseGcpLbTargetName(signal.targetName);
20722
+ if (!identity) return null;
20723
+ const { backendServiceName, method, path: path94 } = identity;
20724
+ const mappedService = config.backendServiceMap?.[backendServiceName];
20725
+ if (mappedService) {
20726
+ const routeNodeId = findMatchingRouteNode3(
20727
+ graph,
20728
+ mappedService,
20729
+ method,
20730
+ normalizePathTemplate(path94)
20731
+ );
20732
+ if (routeNodeId) {
20733
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types83.EdgeType.CALLS };
20734
+ }
20735
+ }
20736
+ return {
20737
+ targetNodeId: (0, import_types83.infraId)(GCP_LB_BACKEND_INFRA_KIND, backendServiceName),
20738
+ serviceName: mappedService ?? backendServiceName,
20739
+ edgeType: import_types83.EdgeType.CALLS,
20740
+ ensureInfraNode: {
20741
+ kind: GCP_LB_BACKEND_INFRA_KIND,
20742
+ name: backendServiceName,
20743
+ provider: "gcp-lb"
20744
+ }
20745
+ };
20746
+ };
20747
+ }
20748
+
20749
+ // src/connectors/gcp-lb/index.ts
20750
+ var GcpLbConnector = class {
20751
+ constructor(config = {}) {
20752
+ this.config = config;
20753
+ }
20754
+ config;
20755
+ provider = "gcp-lb";
20756
+ async poll(ctx) {
20757
+ const creds = readGcpLbCredentials(ctx.credentials);
20758
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_LOOKBACK_MS3;
20759
+ const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
20760
+ const entries = await fetchGcpLbRequestLogEntries(creds, sinceIso, this.config.apiUrl);
20761
+ return mapLogEntriesToSignals3(entries);
20762
+ }
20763
+ };
20764
+ function boundedSinceIso2(since, now, maxLookbackMs) {
20765
+ const floor = new Date(now.getTime() - maxLookbackMs);
20766
+ if (!since) return floor.toISOString();
20767
+ const sinceMs = new Date(since).getTime();
20768
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
20769
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
20770
+ }
20771
+ function createGcpLbConnector(graph, config = {}) {
20772
+ return {
20773
+ connector: new GcpLbConnector(config),
20774
+ resolveTarget: createGcpLbResolveTarget(graph, config)
20775
+ };
20776
+ }
20777
+
20558
20778
  // src/connectors/render/index.ts
20559
20779
  init_cjs_shims();
20560
- var import_types82 = require("@neat.is/types");
20780
+ var import_types86 = require("@neat.is/types");
20561
20781
 
20562
20782
  // src/connectors/render/types.ts
20563
20783
  init_cjs_shims();
@@ -20635,7 +20855,7 @@ function buildRenderRouteIndex(graph, serviceName) {
20635
20855
  const out = [];
20636
20856
  graph.forEachNode((_id, attrs) => {
20637
20857
  const node = attrs;
20638
- if (node.type !== import_types82.NodeType.RouteNode) return;
20858
+ if (node.type !== import_types86.NodeType.RouteNode) return;
20639
20859
  const route = attrs;
20640
20860
  if (route.service !== serviceName) return;
20641
20861
  out.push({
@@ -20720,7 +20940,7 @@ function mapRenderRequestLogsToSignals(entries, routeIndex) {
20720
20940
  function createRenderResolveTarget(config) {
20721
20941
  return (signal) => {
20722
20942
  if (signal.targetKind === ROUTE_TARGET_KIND2) {
20723
- return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types82.EdgeType.CALLS };
20943
+ return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types86.EdgeType.CALLS };
20724
20944
  }
20725
20945
  return null;
20726
20946
  };
@@ -20858,21 +21078,21 @@ function mapInsightsToSignals(rows, observedAtIso) {
20858
21078
 
20859
21079
  // src/connectors/planetscale/resolve.ts
20860
21080
  init_cjs_shims();
20861
- var import_types86 = require("@neat.is/types");
21081
+ var import_types90 = require("@neat.is/types");
20862
21082
  var PLANETSCALE_DATABASE_KIND = "planetscale-database";
20863
21083
  function createPlanetscaleResolveTarget(graph, config) {
20864
21084
  const databaseName = `${config.organization}/${config.database}`;
20865
21085
  return (signal, _ctx) => {
20866
21086
  if (signal.targetKind !== PLANETSCALE_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
20867
- const tableId = (0, import_types86.infraId)("sql-table", signal.targetName);
21087
+ const tableId = (0, import_types90.infraId)("sql-table", signal.targetName);
20868
21088
  if (graph.hasNode(tableId)) {
20869
- return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types86.EdgeType.CALLS };
21089
+ return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types90.EdgeType.CALLS };
20870
21090
  }
20871
- const providerId = (0, import_types86.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
21091
+ const providerId = (0, import_types90.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
20872
21092
  return {
20873
21093
  targetNodeId: providerId,
20874
21094
  serviceName: config.serviceName,
20875
- edgeType: import_types86.EdgeType.CALLS,
21095
+ edgeType: import_types90.EdgeType.CALLS,
20876
21096
  ensureInfraNode: { kind: PLANETSCALE_DATABASE_KIND, name: databaseName, provider: "planetscale" }
20877
21097
  };
20878
21098
  };
@@ -20938,13 +21158,13 @@ function isTransientFailure(err) {
20938
21158
  if (code && INTERNAL_ERROR_CODE.test(code)) return true;
20939
21159
  return false;
20940
21160
  }
20941
- var FIELD_SEP3 = "\0";
21161
+ var FIELD_SEP4 = "\0";
20942
21162
  var EAS_TARGET_KIND = "eas-build";
20943
21163
  function packEasTargetName(identity) {
20944
- return [identity.serviceName, identity.phase].join(FIELD_SEP3);
21164
+ return [identity.serviceName, identity.phase].join(FIELD_SEP4);
20945
21165
  }
20946
21166
  function parseEasTargetName(targetName) {
20947
- const sep = targetName.indexOf(FIELD_SEP3);
21167
+ const sep = targetName.indexOf(FIELD_SEP4);
20948
21168
  if (sep === -1) return null;
20949
21169
  const serviceName = targetName.slice(0, sep);
20950
21170
  const phase = targetName.slice(sep + 1);
@@ -21137,7 +21357,7 @@ function mapBuildsToSignals(builds, serviceName) {
21137
21357
 
21138
21358
  // src/connectors/eas/resolve.ts
21139
21359
  init_cjs_shims();
21140
- var import_types91 = require("@neat.is/types");
21360
+ var import_types95 = require("@neat.is/types");
21141
21361
  var NO_ENV2 = "unknown";
21142
21362
  var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
21143
21363
  var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
@@ -21153,8 +21373,8 @@ function configBasenamesForPhase(phase) {
21153
21373
  function configNodeService(graph, configNodeId) {
21154
21374
  for (const edgeId of graph.inboundEdges(configNodeId)) {
21155
21375
  const edge = graph.getEdgeAttributes(edgeId);
21156
- if (edge.type !== import_types91.EdgeType.CONFIGURED_BY) continue;
21157
- const parsed = (0, import_types91.parseFileId)(edge.source);
21376
+ if (edge.type !== import_types95.EdgeType.CONFIGURED_BY) continue;
21377
+ const parsed = (0, import_types95.parseFileId)(edge.source);
21158
21378
  if (parsed) return parsed.service;
21159
21379
  }
21160
21380
  return null;
@@ -21165,7 +21385,7 @@ function findConfigNode(graph, basenames, serviceName) {
21165
21385
  graph.forEachNode((id, attrs) => {
21166
21386
  if (scoped) return;
21167
21387
  const node = attrs;
21168
- if (node.type !== import_types91.NodeType.ConfigNode) return;
21388
+ if (node.type !== import_types95.NodeType.ConfigNode) return;
21169
21389
  if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
21170
21390
  if (anyMatch === null) anyMatch = id;
21171
21391
  if (configNodeService(graph, id) === serviceName) scoped = id;
@@ -21182,13 +21402,13 @@ function createEasResolveTarget(graph) {
21182
21402
  if (basenames.length > 0) {
21183
21403
  const configNodeId = findConfigNode(graph, basenames, serviceName);
21184
21404
  if (configNodeId) {
21185
- return { targetNodeId: configNodeId, serviceName, edgeType: import_types91.EdgeType.CALLS };
21405
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types95.EdgeType.CALLS };
21186
21406
  }
21187
21407
  }
21188
21408
  return {
21189
21409
  targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
21190
21410
  serviceName,
21191
- edgeType: import_types91.EdgeType.CALLS
21411
+ edgeType: import_types95.EdgeType.CALLS
21192
21412
  };
21193
21413
  };
21194
21414
  }
@@ -21200,7 +21420,7 @@ function isBuildSince(build, sinceIso) {
21200
21420
  if (Number.isNaN(t) || Number.isNaN(s)) return true;
21201
21421
  return t > s;
21202
21422
  }
21203
- function boundedSinceIso2(since, now, maxLookbackMs) {
21423
+ function boundedSinceIso3(since, now, maxLookbackMs) {
21204
21424
  const floor = new Date(now.getTime() - maxLookbackMs);
21205
21425
  if (!since) return floor.toISOString();
21206
21426
  const sinceMs = new Date(since).getTime();
@@ -21219,7 +21439,7 @@ var EasConnector = class {
21219
21439
  const creds = readEasCredentials(ctx.credentials);
21220
21440
  const serviceName = this.config.serviceName ?? this.config.appId;
21221
21441
  const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
21222
- const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
21442
+ const sinceIso = boundedSinceIso3(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
21223
21443
  const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
21224
21444
  const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
21225
21445
  const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
@@ -21450,6 +21670,40 @@ var PROVIDER_DISPATCH = {
21450
21670
  });
21451
21671
  }
21452
21672
  },
21673
+ "gcp-lb": {
21674
+ provider: "gcp-lb",
21675
+ // Like cloud-run, gcp-lb reads both projectId and accessToken from the
21676
+ // credential; the single-string form maps to the secret (the token), and the
21677
+ // required-fields check below catches a projectId that was never supplied.
21678
+ primaryCredentialKey: "accessToken",
21679
+ requiredCredentialFields: ["projectId", "accessToken"],
21680
+ requiredOptionFields: [],
21681
+ build(graph, options) {
21682
+ return createGcpLbConnector(graph, options);
21683
+ },
21684
+ // POST entries:list with pageSize 1 — the exact surface poll() reads, so the
21685
+ // probe checks the actual `logging.logEntries.list` permission the connector
21686
+ // needs. This is the same Cloud Logging read-verdict cloud-run's validate
21687
+ // uses (a lighter GET on logs.list would instead check `logging.logs.list`,
21688
+ // falsely rejecting a correctly-scoped custom role carrying only
21689
+ // `logging.logEntries.list`). A 2xx means the token can list log entries;
21690
+ // 401/403 means the provider rejected it.
21691
+ validate({ credentials, fetchImpl }) {
21692
+ const projectId = String(credentials.projectId ?? "");
21693
+ return authProbe({
21694
+ provider: "gcp-lb",
21695
+ accountKey: projectId || "validate",
21696
+ url: "https://logging.googleapis.com/v2/entries:list",
21697
+ token: String(credentials.accessToken ?? ""),
21698
+ init: {
21699
+ method: "POST",
21700
+ headers: { "Content-Type": "application/json" },
21701
+ body: JSON.stringify({ resourceNames: [`projects/${projectId}`], pageSize: 1 })
21702
+ },
21703
+ ...fetchImpl ? { fetchImpl } : {}
21704
+ });
21705
+ }
21706
+ },
21453
21707
  render: {
21454
21708
  provider: "render",
21455
21709
  primaryCredentialKey: "token",
@@ -21994,11 +22248,11 @@ function registerRoutes(scope, ctx) {
21994
22248
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
21995
22249
  const parsed = [];
21996
22250
  for (const c of candidates) {
21997
- const r = import_types94.DivergenceTypeSchema.safeParse(c);
22251
+ const r = import_types98.DivergenceTypeSchema.safeParse(c);
21998
22252
  if (!r.success) {
21999
22253
  return reply.code(400).send({
22000
22254
  error: `unknown divergence type "${c}"`,
22001
- allowed: import_types94.DivergenceTypeSchema.options
22255
+ allowed: import_types98.DivergenceTypeSchema.options
22002
22256
  });
22003
22257
  }
22004
22258
  parsed.push(r.data);
@@ -22360,7 +22614,7 @@ function registerRoutes(scope, ctx) {
22360
22614
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
22361
22615
  let violations = await log.readAll();
22362
22616
  if (req.query.severity) {
22363
- const sev = import_types94.PolicySeveritySchema.safeParse(req.query.severity);
22617
+ const sev = import_types98.PolicySeveritySchema.safeParse(req.query.severity);
22364
22618
  if (!sev.success) {
22365
22619
  return reply.code(400).send({
22366
22620
  error: "invalid severity",
@@ -22399,7 +22653,7 @@ function registerRoutes(scope, ctx) {
22399
22653
  scope.post("/policies/check", async (req, reply) => {
22400
22654
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
22401
22655
  if (!proj) return;
22402
- const parsed = import_types94.PoliciesCheckBodySchema.safeParse(req.body ?? {});
22656
+ const parsed = import_types98.PoliciesCheckBodySchema.safeParse(req.body ?? {});
22403
22657
  if (!parsed.success) {
22404
22658
  return reply.code(400).send({
22405
22659
  error: "invalid /policies/check body",
@@ -22732,7 +22986,7 @@ var import_node_fs41 = require("fs");
22732
22986
  var import_node_path76 = __toESM(require("path"), 1);
22733
22987
 
22734
22988
  // src/daemon.ts
22735
- var import_types95 = require("@neat.is/types");
22989
+ var import_types99 = require("@neat.is/types");
22736
22990
  function daemonJsonPath(scanPath) {
22737
22991
  return import_node_path77.default.join(scanPath, "neat-out", "daemon.json");
22738
22992
  }
@@ -27742,949 +27996,911 @@ async function runConnectorCommand(rawArgs, deps = {}) {
27742
27996
  }
27743
27997
  }
27744
27998
 
27745
- // src/hooks-cli.ts
27999
+ // src/doctor-cli.ts
27746
28000
  init_cjs_shims();
27747
- var import_node_path87 = __toESM(require("path"), 1);
27748
- var import_node_os5 = __toESM(require("os"), 1);
27749
28001
  var import_node_fs52 = require("fs");
27750
- var import_node_url5 = require("url");
27751
- var HOOK_FILENAME = "neat-search-nudge.mjs";
27752
- var GUIDE_FILENAME = "GRAPH_FIRST.md";
27753
- var GUIDE_INSTALL_NAME = "neat-graph-first.md";
27754
- var HOOK_MATCHER = "Grep|Glob|Bash";
27755
- function moduleDir() {
27756
- return typeof __dirname !== "undefined" ? __dirname : import_node_path87.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
27757
- }
27758
- async function readSkillAsset(rel) {
27759
- const here = moduleDir();
27760
- const candidates = [
27761
- import_node_path87.default.resolve(here, "../../claude-skill", rel),
27762
- import_node_path87.default.resolve(here, "../../../claude-skill", rel),
27763
- import_node_path87.default.resolve(here, "../claude-skill", rel)
27764
- ];
27765
- for (const candidate of candidates) {
27766
- try {
27767
- return await import_node_fs52.promises.readFile(candidate, "utf8");
27768
- } catch {
27769
- }
27770
- }
27771
- throw new Error(
27772
- `neat hooks: could not find @neat.is/claude-skill/${rel} \u2014 is the package installed?`
27773
- );
27774
- }
27775
- function neatHome3() {
27776
- const override = process.env.NEAT_HOME;
27777
- if (override && override.length > 0) return import_node_path87.default.resolve(override);
27778
- return import_node_path87.default.join(import_node_os5.default.homedir(), ".neat");
27779
- }
27780
- function claudeSettingsPath() {
27781
- const override = process.env.NEAT_CLAUDE_SETTINGS;
27782
- if (override && override.length > 0) return import_node_path87.default.resolve(override);
27783
- const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os5.default.homedir();
27784
- return import_node_path87.default.join(home, ".claude", "settings.json");
27785
- }
27786
- function installedHookPath() {
27787
- return import_node_path87.default.join(neatHome3(), "hooks", HOOK_FILENAME);
27788
- }
27789
- function gateFlagPath() {
27790
- return import_node_path87.default.join(neatHome3(), "hooks", "gate-enabled");
27791
- }
27792
- function isNeatSearchEntry(entry2) {
27793
- return (entry2.hooks ?? []).some(
27794
- (h) => typeof h.command === "string" && h.command.includes(HOOK_FILENAME)
27795
- );
27796
- }
27797
- function neatHookEntry(command) {
27798
- return { matcher: HOOK_MATCHER, hooks: [{ type: "command", command }] };
27799
- }
27800
- function hookCommand(scriptPath) {
27801
- return `node "${scriptPath}"`;
27802
- }
27803
- async function runHooks(opts) {
27804
- if (opts.printHook) {
27805
- process.stdout.write(await readSkillAsset(`hooks/${HOOK_FILENAME}`));
27806
- return { exitCode: 0 };
27807
- }
27808
- if (opts.printGuide) {
27809
- process.stdout.write(await readSkillAsset(GUIDE_FILENAME));
27810
- return { exitCode: 0 };
28002
+ var import_node_path87 = __toESM(require("path"), 1);
28003
+
28004
+ // src/cli-client.ts
28005
+ init_cjs_shims();
28006
+ var import_types100 = require("@neat.is/types");
28007
+ var HttpError = class extends Error {
28008
+ constructor(status2, message, responseBody = "") {
28009
+ super(message);
28010
+ this.status = status2;
28011
+ this.responseBody = responseBody;
28012
+ this.name = "HttpError";
27811
28013
  }
27812
- if (opts.printSettings) {
27813
- const block = {
27814
- hooks: { PreToolUse: [neatHookEntry(hookCommand(installedHookPath()))] }
27815
- };
27816
- process.stdout.write(JSON.stringify(block, null, 2) + "\n");
27817
- return { exitCode: 0 };
28014
+ status;
28015
+ responseBody;
28016
+ };
28017
+ var TransportError = class extends Error {
28018
+ constructor(message) {
28019
+ super(message);
28020
+ this.name = "TransportError";
27818
28021
  }
27819
- if (opts.apply) {
27820
- const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
27821
- const guide = await readSkillAsset(GUIDE_FILENAME);
27822
- const scriptPath = installedHookPath();
27823
- await import_node_fs52.promises.mkdir(import_node_path87.default.dirname(scriptPath), { recursive: true });
27824
- await import_node_fs52.promises.writeFile(scriptPath, hookScript, { mode: 493 });
27825
- const guidePath = import_node_path87.default.join(neatHome3(), GUIDE_INSTALL_NAME);
27826
- await import_node_fs52.promises.writeFile(guidePath, guide, "utf8");
27827
- const settingsFile = claudeSettingsPath();
27828
- let settings = {};
27829
- try {
27830
- settings = JSON.parse(await import_node_fs52.promises.readFile(settingsFile, "utf8"));
27831
- } catch (err) {
27832
- if (err.code !== "ENOENT") {
27833
- console.error(
27834
- `neat hooks: failed to read ${settingsFile} \u2014 ${err.message}`
28022
+ };
28023
+ function resolveAuthToken(env = process.env) {
28024
+ const t = env.NEAT_AUTH_TOKEN;
28025
+ return t && t.length > 0 ? t : void 0;
28026
+ }
28027
+ function createHttpClient(baseUrl, bearerToken) {
28028
+ const root = baseUrl.replace(/\/$/, "");
28029
+ const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
28030
+ return {
28031
+ async get(path94) {
28032
+ let res;
28033
+ try {
28034
+ res = await fetch(`${root}${path94}`, {
28035
+ headers: { ...authHeader }
28036
+ });
28037
+ } catch (err) {
28038
+ throw new TransportError(
28039
+ `cannot reach neat-core at ${root}: ${err.message}`
27835
28040
  );
27836
- return { exitCode: 1 };
27837
28041
  }
28042
+ if (!res.ok) {
28043
+ const body = await res.text().catch(() => "");
28044
+ throw new HttpError(
28045
+ res.status,
28046
+ `${res.status} ${res.statusText} on GET ${path94}: ${body}`,
28047
+ body
28048
+ );
28049
+ }
28050
+ return await res.json();
28051
+ },
28052
+ async post(path94, body) {
28053
+ let res;
28054
+ try {
28055
+ res = await fetch(`${root}${path94}`, {
28056
+ method: "POST",
28057
+ headers: { "content-type": "application/json", ...authHeader },
28058
+ body: JSON.stringify(body)
28059
+ });
28060
+ } catch (err) {
28061
+ throw new TransportError(
28062
+ `cannot reach neat-core at ${root}: ${err.message}`
28063
+ );
28064
+ }
28065
+ if (!res.ok) {
28066
+ const text = await res.text().catch(() => "");
28067
+ throw new HttpError(
28068
+ res.status,
28069
+ `${res.status} ${res.statusText} on POST ${path94}: ${text}`,
28070
+ text
28071
+ );
28072
+ }
28073
+ return await res.json();
27838
28074
  }
27839
- const hooks = settings.hooks ?? {};
27840
- const preToolUse = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
27841
- const command = hookCommand(scriptPath);
27842
- const existingIdx = preToolUse.findIndex(isNeatSearchEntry);
27843
- if (existingIdx >= 0) {
27844
- preToolUse[existingIdx] = neatHookEntry(command);
27845
- } else {
27846
- preToolUse.push(neatHookEntry(command));
27847
- }
27848
- const merged = {
27849
- ...settings,
27850
- hooks: { ...hooks, PreToolUse: preToolUse }
28075
+ };
28076
+ }
28077
+ function projectPath(project, suffix) {
28078
+ if (!project) return suffix;
28079
+ return `/projects/${encodeURIComponent(project)}${suffix}`;
28080
+ }
28081
+ async function runRootCause(client, input) {
28082
+ const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
28083
+ const path94 = projectPath(
28084
+ input.project,
28085
+ `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
28086
+ );
28087
+ try {
28088
+ const result = await client.get(path94);
28089
+ const arrowPath = result.traversalPath.join(" \u2190 ");
28090
+ const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
28091
+ const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
28092
+ const blockLines = [
28093
+ `Traversal path: ${arrowPath}`,
28094
+ `Edge provenances: ${provenances}`
28095
+ ];
28096
+ if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
28097
+ return {
28098
+ summary,
28099
+ block: blockLines.join("\n"),
28100
+ confidence: result.confidence,
28101
+ provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
27851
28102
  };
27852
- await import_node_fs52.promises.mkdir(import_node_path87.default.dirname(settingsFile), { recursive: true });
27853
- await import_node_fs52.promises.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
27854
- const flag = gateFlagPath();
27855
- if (opts.gate) {
27856
- await import_node_fs52.promises.mkdir(import_node_path87.default.dirname(flag), { recursive: true });
27857
- await import_node_fs52.promises.writeFile(flag, "1\n", "utf8");
27858
- } else {
27859
- await import_node_fs52.promises.rm(flag, { force: true });
27860
- }
27861
- const mode = opts.gate ? "GATE (deny search until you ask the graph)" : "nudge (search still runs)";
27862
- console.log(`neat hooks: installed the search hook in ${opts.gate ? "gate" : "nudge"} mode`);
27863
- console.log(` script: ${scriptPath}`);
27864
- console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
27865
- console.log(` guidance: ${guidePath}`);
27866
- console.log(` mode: ${mode}`);
27867
- console.log("");
27868
- if (opts.gate) {
27869
- console.log("restart Claude Code to load the hook. A Grep/Glob or Bash grep is now DENIED");
27870
- console.log('until you run `neat ask "<question>"` (or the ask MCP tool) once this session;');
27871
- console.log("after that, search is allowed as a fallback. Set NEAT_SEARCH_GATE=0 to fall");
27872
- console.log("back to nudge-only without re-running.");
27873
- } else {
27874
- console.log("restart Claude Code to load the hook. On a Grep/Glob or a Bash grep,");
27875
- console.log("your agent will now be nudged to query NEAT first (the search still runs).");
27876
- console.log("Re-run with --gate to hard-force the graph-first orientation.");
28103
+ } catch (err) {
28104
+ if (err instanceof HttpError && err.status === 404) {
28105
+ return {
28106
+ summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
28107
+ };
27877
28108
  }
27878
- console.log("");
27879
- console.log("The hook is Claude-Code-specific. For agents on other harnesses, paste");
27880
- console.log(`the guidance above into your project instructions (CLAUDE.md / AGENTS.md).`);
27881
- return { exitCode: 0 };
28109
+ throw err;
27882
28110
  }
27883
- usage();
27884
- return { exitCode: 0 };
27885
- }
27886
- function usage() {
27887
- console.log("neat hooks \u2014 wire NEAT into your agent so it queries the graph before grepping");
27888
- console.log("");
27889
- console.log(" --apply install the Claude Code search hook and write the");
27890
- console.log(" graph-first guidance to ~/.neat/, merging into");
27891
- console.log(" ~/.claude/settings.json without touching your other hooks");
27892
- console.log(" --gate with --apply, enable hard-gate mode: DENY Grep/Glob/grep-Bash");
27893
- console.log(" until `neat ask` has run this session (default is nudge-only).");
27894
- console.log(" Toggle off at run time with NEAT_SEARCH_GATE=0.");
27895
- console.log(" --print-hook print the hook script to stdout");
27896
- console.log(" --print-guide print the agent-agnostic graph-first guidance to stdout");
27897
- console.log(" --print-settings print the settings.json PreToolUse block --apply would add");
27898
- console.log("");
27899
- console.log("By default the hook is a gentle, non-blocking nudge \u2014 searches still run.");
27900
- console.log("--gate turns it into a hard forcing mechanism. It is Claude-Code-specific;");
27901
- console.log("other harnesses get the same steer from the graph-first guidance.");
27902
28111
  }
27903
- async function runHooksCommand(args) {
27904
- const opts = {
27905
- apply: false,
27906
- printHook: false,
27907
- printGuide: false,
27908
- printSettings: false,
27909
- gate: false
27910
- };
27911
- for (const arg of args) {
27912
- switch (arg) {
27913
- case "--apply":
27914
- opts.apply = true;
27915
- break;
27916
- case "--gate":
27917
- opts.gate = true;
27918
- break;
27919
- case "--print-hook":
27920
- opts.printHook = true;
27921
- break;
27922
- case "--print-guide":
27923
- opts.printGuide = true;
27924
- break;
27925
- case "--print-settings":
27926
- opts.printSettings = true;
27927
- break;
27928
- case "-h":
27929
- case "--help":
27930
- usage();
27931
- return 0;
27932
- default:
27933
- console.error(`neat hooks: unknown flag "${arg}"`);
27934
- usage();
27935
- return 2;
28112
+ async function runBlastRadius(client, input) {
28113
+ const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
28114
+ const path94 = projectPath(
28115
+ input.project,
28116
+ `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
28117
+ );
28118
+ try {
28119
+ const result = await client.get(path94);
28120
+ if (result.totalAffected === 0) {
28121
+ return {
28122
+ summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
28123
+ };
28124
+ }
28125
+ const sorted = [...result.affectedNodes].sort(
28126
+ (a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
28127
+ );
28128
+ const blockLines = sorted.map(formatBlastEntry);
28129
+ const minConfidence = sorted.reduce(
28130
+ (m, n) => Math.min(m, n.confidence),
28131
+ Number.POSITIVE_INFINITY
28132
+ );
28133
+ const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
28134
+ return {
28135
+ summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
28136
+ block: blockLines.join("\n"),
28137
+ confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
28138
+ provenance: provenances.length ? provenances : void 0
28139
+ };
28140
+ } catch (err) {
28141
+ if (err instanceof HttpError && err.status === 404) {
28142
+ return { summary: `Node ${input.nodeId} not found in the graph.` };
27936
28143
  }
28144
+ throw err;
27937
28145
  }
28146
+ }
28147
+ function formatBlastEntry(n) {
28148
+ const tag = n.edgeProvenance === import_types100.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
28149
+ return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
28150
+ }
28151
+ async function runDependencies(client, input) {
28152
+ const depth = input.depth ?? 3;
28153
+ const path94 = projectPath(
28154
+ input.project,
28155
+ `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
28156
+ );
27938
28157
  try {
27939
- const { exitCode } = await runHooks(opts);
27940
- return exitCode;
28158
+ const result = await client.get(path94);
28159
+ if (result.total === 0) {
28160
+ return {
28161
+ summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
28162
+ };
28163
+ }
28164
+ const byDistance = /* @__PURE__ */ new Map();
28165
+ for (const dep of result.dependencies) {
28166
+ const ring = byDistance.get(dep.distance) ?? [];
28167
+ ring.push(dep);
28168
+ byDistance.set(dep.distance, ring);
28169
+ }
28170
+ const blockLines = [];
28171
+ for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
28172
+ const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
28173
+ blockLines.push(`${label}:`);
28174
+ for (const dep of byDistance.get(distance)) {
28175
+ blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
28176
+ }
28177
+ }
28178
+ const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
28179
+ const directCount = byDistance.get(1)?.length ?? 0;
28180
+ const summary = depth === 1 ? `${input.nodeId} has ${directCount} direct dependenc${directCount === 1 ? "y" : "ies"}.` : `${input.nodeId} has ${result.total} dependenc${result.total === 1 ? "y" : "ies"} reachable to depth ${depth} (${directCount} direct).`;
28181
+ return { summary, block: blockLines.join("\n"), provenance: provenances };
27941
28182
  } catch (err) {
27942
- console.error(err.message);
27943
- return 1;
28183
+ if (err instanceof HttpError && err.status === 404) {
28184
+ return { summary: `Node ${input.nodeId} not found in the graph.` };
28185
+ }
28186
+ throw err;
27944
28187
  }
27945
28188
  }
27946
-
27947
- // src/claude-cli.ts
27948
- init_cjs_shims();
27949
- var import_node_path88 = __toESM(require("path"), 1);
27950
- var import_node_fs53 = require("fs");
27951
- var NEAT_SECTION_HEADING = "## neat";
27952
- var NEAT_DIRECTIVE_BODY = `This project has NEAT wired in: a live, fused semantic graph of the system \u2014
27953
- code and runtime behaviour (OpenTelemetry) in one model, every fact tagged with
27954
- its provenance (EXTRACTED / OBSERVED / INFERRED / STALE) and a confidence.
27955
-
27956
- For ANY question about this system's behaviour, dependencies, failures, root
27957
- cause, or blast radius, call \`neat ask\` FIRST \u2014 before Read, Grep, Glob, or Bash:
27958
-
27959
- neat ask "why is checkout failing?"
27960
- neat ask "what breaks if I change the orders table?"
27961
- neat ask "what does the payments service depend on at runtime?"
27962
-
27963
- \`ask\` resolves your question to graph nodes and routes it to the right traversal
27964
- \u2014 root cause, dependencies, observed runtime calls, incidents, divergences,
27965
- blast radius \u2014 and returns one compact, provenance-tagged answer. The same door
27966
- is the \`ask\` MCP tool (\`mcp__neat__ask\`) when NEAT's MCP server is wired in.
27967
-
27968
- The graph is live and fused: it is faster and more accurate than scanning files,
27969
- and it can tell you what the system actually does at runtime, not only what the
27970
- source declares. Fall back to Read/Grep only when the graph does not have what
27971
- you need \u2014 comments, string literals, config minutiae. Ask the graph first,
27972
- then scan.
27973
-
27974
- If \`neat ask\` errors, the daemon may not be running (\`neat list\`) \u2014 start it
27975
- with \`neat <path>\`, then re-ask.`;
27976
- function neatSection() {
27977
- return `${NEAT_SECTION_HEADING}
27978
-
27979
- ${NEAT_DIRECTIVE_BODY}
27980
- `;
27981
- }
27982
- function claudeMdPath() {
27983
- const override = process.env.NEAT_CLAUDE_MD;
27984
- if (override && override.length > 0) return import_node_path88.default.resolve(override);
27985
- return import_node_path88.default.join(process.cwd(), "CLAUDE.md");
28189
+ function observedDepLine(nodeId, e) {
28190
+ const via = e.source !== nodeId ? ` (via ${e.source})` : "";
28191
+ return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
27986
28192
  }
27987
- function splitAroundSection(raw) {
27988
- const lines = raw.split("\n");
27989
- const startIdx = lines.findIndex((l) => l.replace(/\s+$/, "") === NEAT_SECTION_HEADING);
27990
- if (startIdx === -1) {
27991
- return { before: raw.replace(/\n*$/, ""), after: "", found: false };
28193
+ async function runObservedDependencies(client, input) {
28194
+ try {
28195
+ const result = await client.get(
28196
+ projectPath(
28197
+ input.project,
28198
+ `/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
28199
+ )
28200
+ );
28201
+ if (result.dependencies.length === 0) {
28202
+ if (result.observed) {
28203
+ return {
28204
+ 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.`,
28205
+ provenance: import_types100.Provenance.OBSERVED
28206
+ };
28207
+ }
28208
+ const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
28209
+ return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
28210
+ }
28211
+ const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
28212
+ return {
28213
+ summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
28214
+ block: blockLines.join("\n"),
28215
+ provenance: import_types100.Provenance.OBSERVED
28216
+ };
28217
+ } catch (err) {
28218
+ if (err instanceof HttpError && err.status === 404) {
28219
+ return { summary: `Node ${input.nodeId} not found in the graph.` };
28220
+ }
28221
+ throw err;
27992
28222
  }
27993
- let endIdx = lines.length;
27994
- for (let i = startIdx + 1; i < lines.length; i++) {
27995
- if (/^#{1,2}\s+/.test(lines[i] ?? "")) {
27996
- endIdx = i;
27997
- break;
28223
+ }
28224
+ function edgeMeta(e) {
28225
+ const bits = [];
28226
+ if (e.signal) {
28227
+ bits.push(`spans=${e.signal.spanCount}`);
28228
+ if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
28229
+ if (e.signal.lastObservedAgeMs !== void 0) {
28230
+ bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
27998
28231
  }
28232
+ } else if (e.callCount !== void 0) {
28233
+ bits.push(`callCount=${e.callCount}`);
27999
28234
  }
28000
- const before = lines.slice(0, startIdx).join("\n").replace(/\n*$/, "");
28001
- const after = lines.slice(endIdx).join("\n").replace(/^\n*/, "");
28002
- return { before, after, found: true };
28235
+ if (e.lastObserved) bits.push(`lastObserved=${e.lastObserved}`);
28236
+ if (e.confidence !== void 0) bits.push(`confidence=${e.confidence}`);
28237
+ return bits.length ? ` [${bits.join(", ")}]` : "";
28003
28238
  }
28004
- function compose(before, after) {
28005
- const parts = [];
28006
- if (before.length > 0) parts.push(before);
28007
- parts.push(neatSection().replace(/\n+$/, ""));
28008
- if (after.length > 0) parts.push(after);
28009
- return parts.join("\n\n").replace(/\n*$/, "") + "\n";
28239
+ function formatDuration(ms) {
28240
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
28241
+ const s = Math.round(ms / 1e3);
28242
+ if (s < 60) return `${s}s`;
28243
+ const m = Math.round(s / 60);
28244
+ if (m < 60) return `${m}m`;
28245
+ const h = Math.round(m / 60);
28246
+ if (h < 48) return `${h}h`;
28247
+ return `${Math.round(h / 24)}d`;
28010
28248
  }
28011
- async function readIfExists2(file) {
28249
+ async function runIncidents(client, input) {
28250
+ const path94 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
28012
28251
  try {
28013
- return await import_node_fs53.promises.readFile(file, "utf8");
28252
+ const body = await client.get(path94);
28253
+ const events = body.events;
28254
+ if (events.length === 0) {
28255
+ return {
28256
+ summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
28257
+ };
28258
+ }
28259
+ const ordered = [...events].reverse().slice(0, input.limit ?? 20);
28260
+ const blockLines = [];
28261
+ for (const ev of ordered) {
28262
+ blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
28263
+ blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
28264
+ }
28265
+ const target = input.nodeId ?? "the project";
28266
+ return {
28267
+ summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
28268
+ block: blockLines.join("\n"),
28269
+ provenance: import_types100.Provenance.OBSERVED
28270
+ };
28014
28271
  } catch (err) {
28015
- if (err.code === "ENOENT") return null;
28272
+ if (err instanceof HttpError && err.status === 404) {
28273
+ return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
28274
+ }
28016
28275
  throw err;
28017
28276
  }
28018
28277
  }
28019
- async function runInstall() {
28020
- const file = claudeMdPath();
28021
- const raw = await readIfExists2(file) ?? "";
28022
- const { before, after, found } = splitAroundSection(raw);
28023
- const next = compose(before, after);
28024
- await import_node_fs53.promises.mkdir(import_node_path88.default.dirname(file), { recursive: true });
28025
- await import_node_fs53.promises.writeFile(file, next, "utf8");
28026
- const verb = raw.length === 0 ? "created" : found ? "refreshed" : "added";
28027
- console.log(`neat claude: ${verb} the \`${NEAT_SECTION_HEADING}\` section in ${file}`);
28028
- console.log("Your agent will now reach for `neat ask` before Read/Grep/Bash. Restart the");
28029
- console.log("session (or reload CLAUDE.md) to pick it up.");
28030
- return { exitCode: 0 };
28031
- }
28032
- async function runUninstall() {
28033
- const file = claudeMdPath();
28034
- const raw = await readIfExists2(file);
28035
- if (raw === null) {
28036
- console.log(`neat claude: no CLAUDE.md at ${file} \u2014 nothing to remove.`);
28037
- return { exitCode: 0 };
28278
+ async function runSearch(client, input) {
28279
+ const result = await client.get(
28280
+ projectPath(input.project, `/search?q=${encodeURIComponent(input.query)}`)
28281
+ );
28282
+ if (result.matches.length === 0) {
28283
+ return { summary: `No matches for "${input.query}".` };
28038
28284
  }
28039
- const { before, after, found } = splitAroundSection(raw);
28040
- if (!found) {
28041
- console.log(`neat claude: no \`${NEAT_SECTION_HEADING}\` section in ${file} \u2014 nothing to remove.`);
28042
- return { exitCode: 0 };
28285
+ const provider = result.provider ?? "substring";
28286
+ const blockLines = [];
28287
+ let topScore;
28288
+ for (const n of result.matches) {
28289
+ const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
28290
+ const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
28291
+ if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
28292
+ blockLines.push(
28293
+ ` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
28294
+ );
28043
28295
  }
28044
- const remaining = [before, after].filter((s) => s.length > 0).join("\n\n");
28045
- const next = remaining.length > 0 ? remaining.replace(/\n*$/, "") + "\n" : "";
28046
- await import_node_fs53.promises.writeFile(file, next, "utf8");
28047
- console.log(`neat claude: removed the \`${NEAT_SECTION_HEADING}\` section from ${file}.`);
28048
- return { exitCode: 0 };
28049
- }
28050
- function usage2() {
28051
- console.log("neat claude \u2014 make the query-first directive always-on in Claude Code");
28052
- console.log("");
28053
- console.log(" install write (or refresh) a `## neat` section in ./CLAUDE.md so your");
28054
- console.log(" agent reaches for `neat ask` before Read/Grep/Bash");
28055
- console.log(" uninstall remove the `## neat` section from ./CLAUDE.md");
28056
- console.log(" print print the directive block to stdout (for a manual paste)");
28057
- console.log("");
28058
- console.log("Idempotent: re-running install replaces its own section, never duplicates it.");
28059
- console.log("Target file overridable via NEAT_CLAUDE_MD.");
28296
+ return {
28297
+ summary: `Found ${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${input.query}" via ${provider} provider.`,
28298
+ block: blockLines.join("\n"),
28299
+ confidence: topScore
28300
+ };
28060
28301
  }
28061
- async function runClaudeCommand(args) {
28062
- const sub = args[0];
28063
- if (sub === "-h" || sub === "--help" || sub === void 0) {
28064
- usage2();
28065
- return sub === void 0 ? 2 : 0;
28302
+ async function runDiff(client, input) {
28303
+ const result = await client.get(
28304
+ projectPath(
28305
+ input.project,
28306
+ `/graph/diff?against=${encodeURIComponent(input.againstSnapshot)}`
28307
+ )
28308
+ );
28309
+ const total = result.added.nodes.length + result.added.edges.length + result.removed.nodes.length + result.removed.edges.length + result.changed.nodes.length + result.changed.edges.length;
28310
+ const baseLabel = result.base.exportedAt ?? "unknown";
28311
+ if (total === 0) {
28312
+ return {
28313
+ summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
28314
+ };
28066
28315
  }
28067
- try {
28068
- switch (sub) {
28069
- case "install":
28070
- return (await runInstall()).exitCode;
28071
- case "uninstall":
28072
- return (await runUninstall()).exitCode;
28073
- case "print":
28074
- process.stdout.write(neatSection());
28075
- return 0;
28076
- default:
28077
- console.error(`neat claude: unknown subcommand "${sub}"`);
28078
- usage2();
28079
- return 2;
28316
+ const blockLines = [
28317
+ ` base exportedAt: ${baseLabel}`,
28318
+ ` current exportedAt: ${result.current.exportedAt}`,
28319
+ ""
28320
+ ];
28321
+ if (result.added.nodes.length || result.added.edges.length) {
28322
+ blockLines.push("Added:");
28323
+ for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
28324
+ for (const e of result.added.edges)
28325
+ blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
28326
+ blockLines.push("");
28327
+ }
28328
+ if (result.removed.nodes.length || result.removed.edges.length) {
28329
+ blockLines.push("Removed:");
28330
+ for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
28331
+ for (const e of result.removed.edges)
28332
+ blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
28333
+ blockLines.push("");
28334
+ }
28335
+ if (result.changed.nodes.length || result.changed.edges.length) {
28336
+ blockLines.push("Changed:");
28337
+ for (const c of result.changed.nodes) {
28338
+ blockLines.push(` ~ node ${c.id} \u2014 ${summariseAttrDiff(c.before, c.after)}`);
28339
+ }
28340
+ for (const c of result.changed.edges) {
28341
+ const provBit = c.before.provenance !== c.after.provenance ? `provenance ${c.before.provenance} \u2192 ${c.after.provenance}` : summariseAttrDiff(c.before, c.after);
28342
+ blockLines.push(` ~ edge ${c.id} \u2014 ${provBit}`);
28080
28343
  }
28081
- } catch (err) {
28082
- console.error(`neat claude: ${err.message}`);
28083
- return 1;
28084
28344
  }
28345
+ return {
28346
+ summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
28347
+ block: blockLines.join("\n").trimEnd()
28348
+ };
28085
28349
  }
28086
-
28087
- // src/codex-cli.ts
28088
- init_cjs_shims();
28089
- var import_node_path89 = __toESM(require("path"), 1);
28090
- var import_node_os6 = __toESM(require("os"), 1);
28091
- var import_node_fs54 = require("fs");
28092
- var import_node_util = require("util");
28093
- var import_smol_toml6 = require("smol-toml");
28094
- var CODEX_MCP_SERVER = {
28095
- command: "npx",
28096
- args: ["-y", "@neat.is/mcp"],
28097
- env: { NEAT_CORE_URL: "http://localhost:8080" }
28098
- };
28099
- var CODEX_NEAT_BLOCK = [
28100
- "[mcp_servers.neat]",
28101
- 'command = "npx"',
28102
- 'args = ["-y", "@neat.is/mcp"]',
28103
- 'env = { NEAT_CORE_URL = "http://localhost:8080" }'
28104
- ].join("\n");
28105
- var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
28106
- var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
28107
- function codexConfigPath() {
28108
- const override = process.env.NEAT_CODEX_CONFIG;
28109
- if (override && override.length > 0) return import_node_path89.default.resolve(override);
28110
- const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os6.default.homedir();
28111
- return import_node_path89.default.join(home, ".codex", "config.toml");
28112
- }
28113
- function agentsFilePath() {
28114
- const override = process.env.NEAT_CODEX_AGENTS;
28115
- if (override && override.length > 0) return import_node_path89.default.resolve(override);
28116
- return import_node_path89.default.join(process.cwd(), "AGENTS.md");
28117
- }
28118
- function isTableHeader(line) {
28119
- return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
28120
- }
28121
- function tableName(line) {
28122
- return line.trim().replace(/^\[\[?/, "").replace(/\]\]?$/, "").trim();
28123
- }
28124
- function isNeatHeader(line) {
28125
- return isTableHeader(line) && tableName(line) === "mcp_servers.neat";
28350
+ function summariseAttrDiff(before, after) {
28351
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
28352
+ const changed = [];
28353
+ for (const k of keys) {
28354
+ if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.push(k);
28355
+ }
28356
+ return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
28126
28357
  }
28127
- function isNeatChildHeader(line) {
28128
- if (!isTableHeader(line)) return false;
28129
- const name = tableName(line);
28130
- return name === "mcp_servers.neat" || name.startsWith("mcp_servers.neat.");
28358
+ async function runStaleEdges(client, input) {
28359
+ const params = new URLSearchParams();
28360
+ if (input.limit !== void 0) params.set("limit", String(input.limit));
28361
+ if (input.edgeType) params.set("edgeType", input.edgeType);
28362
+ const qs = params.size > 0 ? `?${params.toString()}` : "";
28363
+ const body = await client.get(
28364
+ projectPath(input.project, `/stale-events${qs}`)
28365
+ );
28366
+ const events = body.events;
28367
+ if (events.length === 0) {
28368
+ return {
28369
+ summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
28370
+ };
28371
+ }
28372
+ const blockLines = events.map(
28373
+ (e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
28374
+ );
28375
+ return {
28376
+ summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
28377
+ block: blockLines.join("\n"),
28378
+ provenance: import_types100.Provenance.STALE
28379
+ };
28131
28380
  }
28132
- function upsertCodexConfig(raw) {
28133
- const trimmed = raw.trim();
28134
- const parsed = trimmed.length > 0 ? (0, import_smol_toml6.parse)(raw) : {};
28135
- const existingServers = parsed.mcp_servers ?? {};
28136
- const spliced = spliceNeatBlock(raw);
28137
- let text;
28138
- if (verifyPreserved(raw, spliced, parsed)) {
28139
- text = spliced;
28381
+ async function runPolicies(client, input) {
28382
+ let violations;
28383
+ let allowed = true;
28384
+ let hypothetical;
28385
+ if (input.hypotheticalAction) {
28386
+ if (typeof client.post !== "function") {
28387
+ throw new Error("HttpClient does not support POST \u2014 required for policies dry-run");
28388
+ }
28389
+ const body = await client.post(
28390
+ projectPath(input.project, "/policies/check"),
28391
+ { hypotheticalAction: input.hypotheticalAction }
28392
+ );
28393
+ violations = body.violations;
28394
+ allowed = body.allowed;
28395
+ hypothetical = body.hypotheticalAction;
28140
28396
  } else {
28141
- const merged = {
28142
- ...parsed,
28143
- mcp_servers: { ...existingServers, neat: CODEX_MCP_SERVER }
28397
+ const params = new URLSearchParams();
28398
+ if (input.policyId) params.set("policyId", input.policyId);
28399
+ const qs = params.size > 0 ? `?${params.toString()}` : "";
28400
+ const body = await client.get(
28401
+ projectPath(input.project, `/policies/violations${qs}`)
28402
+ );
28403
+ violations = body.violations;
28404
+ allowed = violations.every((v) => v.onViolation !== "block");
28405
+ }
28406
+ if (input.nodeId) {
28407
+ violations = violations.filter(
28408
+ (v) => v.subject.nodeId === input.nodeId || v.subject.path?.includes(input.nodeId)
28409
+ );
28410
+ }
28411
+ if (violations.length === 0) {
28412
+ return {
28413
+ summary: hypothetical ? `No violations would result from the hypothetical action (${hypothetical.kind}).` : "No policy violations recorded."
28144
28414
  };
28145
- text = (0, import_smol_toml6.stringify)(merged);
28146
- if (!text.endsWith("\n")) text += "\n";
28147
28415
  }
28148
- return { text, changed: text !== raw };
28149
- }
28150
- function spliceNeatBlock(raw) {
28151
- const block = CODEX_NEAT_BLOCK;
28152
- const lines = raw.length > 0 ? raw.split("\n") : [];
28153
- const start = lines.findIndex(isNeatHeader);
28154
- if (start === -1) {
28155
- const base = raw.replace(/\n+$/, "");
28156
- return base.length > 0 ? `${base}
28157
-
28158
- ${block}
28159
- ` : `${block}
28160
- `;
28416
+ const blockCount = violations.filter((v) => v.onViolation === "block").length;
28417
+ const summaryParts = [];
28418
+ if (hypothetical) {
28419
+ summaryParts.push(
28420
+ `Hypothetical ${hypothetical.kind} would surface ${violations.length} violation${violations.length === 1 ? "" : "s"}`
28421
+ );
28422
+ } else {
28423
+ summaryParts.push(
28424
+ `${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
28425
+ );
28161
28426
  }
28162
- let end = start + 1;
28163
- for (; ; ) {
28164
- while (end < lines.length && !isTableHeader(lines[end])) end++;
28165
- if (end < lines.length && isNeatChildHeader(lines[end])) {
28166
- end++;
28167
- continue;
28427
+ if (blockCount > 0) summaryParts.push(`${blockCount} of which block`);
28428
+ if (!allowed && hypothetical) summaryParts.push("action denied");
28429
+ const summary = summaryParts.join("; ") + ".";
28430
+ const blockLines = violations.map((v) => {
28431
+ const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
28432
+ return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
28433
+ });
28434
+ const severities = [...new Set(violations.map((v) => v.severity))];
28435
+ return {
28436
+ summary,
28437
+ block: blockLines.join("\n"),
28438
+ confidence: hypothetical ? 0.7 : 1,
28439
+ provenance: severities.join(" ")
28440
+ };
28441
+ }
28442
+ function formatDivergenceLine(d) {
28443
+ switch (d.type) {
28444
+ case "missing-observed":
28445
+ case "missing-extracted":
28446
+ if (d.column) {
28447
+ return ` \u2022 [${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 confidence ${d.confidence.toFixed(2)}`;
28448
+ }
28449
+ return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014 confidence ${d.confidence.toFixed(2)}`;
28450
+ case "version-mismatch":
28451
+ return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared ${d.extractedVersion}, observed engine ${d.observedVersion} (${d.compatibility})`;
28452
+ case "host-mismatch":
28453
+ return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
28454
+ case "compat-violation":
28455
+ return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
28456
+ case "observed-symbol-mismatch": {
28457
+ const at = d.location ? ` at ${d.location}` : "";
28458
+ const member = d.symbol ? ` ${d.symbol}` : "";
28459
+ return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
28168
28460
  }
28169
- break;
28170
28461
  }
28171
- const before = lines.slice(0, start).join("\n").replace(/\n*$/, "");
28172
- const after = lines.slice(end).join("\n").replace(/^\n*/, "");
28173
- let text = "";
28174
- if (before.length > 0) text += `${before}
28175
-
28176
- `;
28177
- text += `${block}
28178
- `;
28179
- if (after.length > 0) text += `
28180
- ${after}`;
28181
- return `${text.replace(/\n*$/, "")}
28182
- `;
28183
28462
  }
28184
- function verifyPreserved(raw, spliced, original) {
28185
- let next;
28186
- try {
28187
- next = (0, import_smol_toml6.parse)(spliced);
28188
- } catch {
28189
- return false;
28463
+ async function runDivergences(client, input) {
28464
+ const params = new URLSearchParams();
28465
+ if (input.type && input.type.length > 0) params.set("type", input.type.join(","));
28466
+ if (input.minConfidence !== void 0) {
28467
+ params.set("minConfidence", String(input.minConfidence));
28190
28468
  }
28191
- const origServers = original.mcp_servers ?? {};
28192
- const nextServers = next.mcp_servers ?? {};
28193
- if (!(0, import_node_util.isDeepStrictEqual)(nextServers.neat, CODEX_MCP_SERVER)) return false;
28194
- for (const name of Object.keys(origServers)) {
28195
- if (name === "neat") continue;
28196
- if (!(0, import_node_util.isDeepStrictEqual)(nextServers[name], origServers[name])) return false;
28469
+ if (input.node) params.set("node", input.node);
28470
+ const qs = params.size > 0 ? `?${params.toString()}` : "";
28471
+ const result = await client.get(
28472
+ projectPath(input.project, `/graph/divergences${qs}`)
28473
+ );
28474
+ if (result.totalAffected === 0) {
28475
+ return {
28476
+ summary: "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
28477
+ };
28197
28478
  }
28198
- for (const name of Object.keys(nextServers)) {
28199
- if (name !== "neat" && !(name in origServers)) return false;
28479
+ const headline = result.divergences[0];
28480
+ const summary = `Found ${result.totalAffected} divergence${result.totalAffected === 1 ? "" : "s"} between code and production. Highest-confidence: ${headline.type} on ${headline.source} \u2192 ${headline.target}. ${headline.reason}`;
28481
+ const blockLines = [];
28482
+ for (const d of result.divergences) {
28483
+ blockLines.push(formatDivergenceLine(d));
28484
+ blockLines.push(` reason: ${d.reason}`);
28485
+ blockLines.push(` recommendation: ${d.recommendation}`);
28200
28486
  }
28201
- for (const key of Object.keys(original)) {
28202
- if (key === "mcp_servers") continue;
28203
- if (!(0, import_node_util.isDeepStrictEqual)(next[key], original[key])) return false;
28487
+ const maxConfidence = result.divergences.reduce(
28488
+ (m, d) => Math.max(m, d.confidence),
28489
+ 0
28490
+ );
28491
+ return {
28492
+ summary,
28493
+ block: blockLines.join("\n"),
28494
+ confidence: maxConfidence,
28495
+ provenance: "composite (EXTRACTED + OBSERVED)"
28496
+ };
28497
+ }
28498
+ async function runAsk(client, input) {
28499
+ const result = await client.get(
28500
+ projectPath(input.project, `/graph/ask?q=${encodeURIComponent(input.question)}`)
28501
+ );
28502
+ const blockLines = [];
28503
+ if (result.matched.length > 0) {
28504
+ blockLines.push(
28505
+ `Matched: ${result.matched.map((m) => `${m.nodeId} [${m.via} ${m.score.toFixed(2)}]`).join(", ")}`
28506
+ );
28507
+ blockLines.push(`Intent: ${result.intent}`);
28508
+ } else if (result.scope === "global") {
28509
+ blockLines.push(`Graph-wide answer (${result.intent}) \u2014 no entity named.`);
28204
28510
  }
28205
- for (const key of Object.keys(next)) {
28206
- if (key !== "mcp_servers" && !(key in original)) return false;
28511
+ for (const section of result.sections) {
28512
+ blockLines.push("", section.heading + ":");
28513
+ for (const fact of section.facts) {
28514
+ const tag = fact.provenance ? ` [${fact.provenance}${fact.confidence !== void 0 ? ` ${fact.confidence.toFixed(2)}` : ""}]` : fact.confidence !== void 0 ? ` [confidence ${fact.confidence.toFixed(2)}]` : "";
28515
+ blockLines.push(` \u2022 ${fact.text}${tag}`);
28516
+ }
28207
28517
  }
28208
- return true;
28209
- }
28210
- function agentsBlock(guide) {
28211
- return `${NEAT_GRAPH_FIRST_START}
28212
- ${guide.replace(/\s+$/, "")}
28213
- ${NEAT_GRAPH_FIRST_END}
28214
- `;
28518
+ return {
28519
+ summary: result.answer,
28520
+ block: blockLines.join("\n").trim(),
28521
+ ...result.confidence !== void 0 ? { confidence: result.confidence } : {},
28522
+ ...result.provenance.length > 0 ? { provenance: result.provenance } : {}
28523
+ };
28215
28524
  }
28216
- function upsertAgents(raw, guide) {
28217
- const block = agentsBlock(guide);
28218
- if (raw.length === 0) return { text: block, changed: true };
28219
- const startIdx = raw.indexOf(NEAT_GRAPH_FIRST_START);
28220
- const endIdx = raw.indexOf(NEAT_GRAPH_FIRST_END);
28221
- if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
28222
- const before = raw.slice(0, startIdx);
28223
- const after = raw.slice(endIdx + NEAT_GRAPH_FIRST_END.length);
28224
- const text2 = `${before}${block.replace(/\n+$/, "")}${after}`;
28225
- return { text: text2, changed: text2 !== raw };
28525
+ function formatFooter(confidence, provenance) {
28526
+ const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
28527
+ const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
28528
+ return `confidence: ${c} \xB7 provenance: ${p}`;
28529
+ }
28530
+ function formatHuman(result) {
28531
+ const sections = [result.summary.trim()];
28532
+ if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
28533
+ sections.push(formatFooter(result.confidence, result.provenance));
28534
+ return sections.join("\n\n");
28535
+ }
28536
+ function formatJson(result) {
28537
+ return JSON.stringify(
28538
+ {
28539
+ summary: result.summary,
28540
+ block: result.block ?? "",
28541
+ confidence: result.confidence ?? null,
28542
+ provenance: result.provenance ?? null
28543
+ },
28544
+ null,
28545
+ 2
28546
+ );
28547
+ }
28548
+ function exitCodeForError(err) {
28549
+ if (err instanceof TransportError) return 3;
28550
+ if (err instanceof HttpError) return 1;
28551
+ return 1;
28552
+ }
28553
+ function createSnapshotPushClient(baseUrl, token) {
28554
+ return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
28555
+ }
28556
+ async function pushSnapshotToRemote(input) {
28557
+ const client = createSnapshotPushClient(input.baseUrl, input.token);
28558
+ if (typeof client.post !== "function") {
28559
+ throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
28226
28560
  }
28227
- const base = raw.replace(/\n+$/, "");
28228
- const text = base.length > 0 ? `${base}
28561
+ return client.post(
28562
+ `/projects/${encodeURIComponent(input.project)}/snapshot`,
28563
+ { snapshot: input.snapshot }
28564
+ );
28565
+ }
28229
28566
 
28230
- ${block}` : block;
28231
- return { text, changed: text !== raw };
28567
+ // src/doctor-cli.ts
28568
+ function resolveDeps2(deps) {
28569
+ return {
28570
+ cwd: deps.cwd ?? process.cwd(),
28571
+ env: deps.env ?? process.env,
28572
+ nodeVersion: deps.nodeVersion ?? process.versions.node,
28573
+ fetchImpl: deps.fetchImpl ?? fetch,
28574
+ readRecord: deps.readRecord ?? readDaemonRecord,
28575
+ out: deps.out ?? ((line) => console.log(line))
28576
+ };
28232
28577
  }
28233
- async function readGuide() {
28234
- return readSkillAsset(GUIDE_FILENAME);
28578
+ var NODE_FLOOR = 20;
28579
+ var HEALTH_TIMEOUT_MS = 3e3;
28580
+ function checkNode(nodeVersion) {
28581
+ const major = Number.parseInt(nodeVersion.split(".")[0] ?? "", 10);
28582
+ const ok = Number.isFinite(major) && major >= NODE_FLOOR;
28583
+ return ok ? { name: "node", ok, detail: `v${nodeVersion} (>= ${NODE_FLOOR} required)` } : {
28584
+ name: "node",
28585
+ ok,
28586
+ detail: `v${nodeVersion} \u2014 NEAT needs Node ${NODE_FLOOR} or newer`,
28587
+ fix: `install Node ${NODE_FLOOR}.x (e.g. \`nvm install ${NODE_FLOOR}\`) and re-run`
28588
+ };
28235
28589
  }
28236
- async function runCodex(opts) {
28237
- if (opts.printConfig) {
28238
- process.stdout.write(`${CODEX_NEAT_BLOCK}
28239
- `);
28240
- return { exitCode: 0 };
28590
+ async function neatOutExists(cwd) {
28591
+ try {
28592
+ const st = await import_node_fs52.promises.stat(import_node_path87.default.join(cwd, "neat-out"));
28593
+ return st.isDirectory();
28594
+ } catch {
28595
+ return false;
28241
28596
  }
28242
- if (opts.printGuide) {
28243
- process.stdout.write(agentsBlock(await readGuide()));
28244
- return { exitCode: 0 };
28597
+ }
28598
+ async function checkProject(cwd, record) {
28599
+ if (record) {
28600
+ return {
28601
+ name: "project",
28602
+ ok: true,
28603
+ detail: `"${record.project}" \u2014 set up in this directory (daemon record on REST ${record.ports.rest})`
28604
+ };
28245
28605
  }
28246
- const configPath = codexConfigPath();
28247
- const agentsPath = agentsFilePath();
28248
- let configRaw = "";
28249
- try {
28250
- configRaw = await import_node_fs54.promises.readFile(configPath, "utf8");
28251
- } catch (err) {
28252
- if (err.code !== "ENOENT") {
28253
- console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
28254
- return { exitCode: 1 };
28255
- }
28606
+ if (await neatOutExists(cwd)) {
28607
+ return {
28608
+ name: "project",
28609
+ ok: true,
28610
+ detail: "set up in this directory (no live daemon record \u2014 it may be stopped)"
28611
+ };
28256
28612
  }
28257
- let agentsRaw = "";
28613
+ return {
28614
+ name: "project",
28615
+ ok: false,
28616
+ detail: "no NEAT project in this directory",
28617
+ fix: "set one up: `neat .`"
28618
+ };
28619
+ }
28620
+ function resolveHealthUrl(env, record) {
28621
+ const explicit = env.NEAT_API_URL ?? env.NEAT_CORE_URL;
28622
+ if (explicit && explicit.length > 0) return explicit.replace(/\/$/, "");
28623
+ if (record) return `http://localhost:${record.ports.rest}`;
28624
+ return "http://localhost:8080";
28625
+ }
28626
+ function summariseHealth(url, body) {
28627
+ const projects = body.projects ?? [];
28628
+ const nodes = projects.reduce((n, p) => n + (p.nodeCount ?? 0), 0);
28629
+ const edges = projects.reduce((n, p) => n + (p.edgeCount ?? 0), 0);
28630
+ const proj = body.project ?? projects[0]?.name;
28631
+ const graph = projects.length > 0 ? ` \u2014 ${nodes} nodes / ${edges} edges` : "";
28632
+ return `up at ${url}${proj ? ` (project "${proj}"${graph})` : ""}`;
28633
+ }
28634
+ async function checkDaemon(deps, record) {
28635
+ const url = resolveHealthUrl(deps.env, record);
28636
+ const token = resolveAuthToken(deps.env);
28637
+ const headers = token ? { authorization: `Bearer ${token}` } : {};
28258
28638
  try {
28259
- agentsRaw = await import_node_fs54.promises.readFile(agentsPath, "utf8");
28260
- } catch (err) {
28261
- if (err.code !== "ENOENT") {
28262
- console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
28263
- return { exitCode: 1 };
28639
+ const res = await deps.fetchImpl(`${url}/health`, {
28640
+ headers,
28641
+ signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
28642
+ });
28643
+ if (res.status === 401 || res.status === 403) {
28644
+ return {
28645
+ name: "daemon",
28646
+ ok: false,
28647
+ detail: `up at ${url}, but rejected the request (${res.status})`,
28648
+ fix: "set NEAT_AUTH_TOKEN to the daemon's token"
28649
+ };
28264
28650
  }
28265
- }
28266
- let config;
28267
- try {
28268
- config = upsertCodexConfig(configRaw);
28269
- } catch (err) {
28270
- console.error(
28271
- `neat codex: ${configPath} is not valid TOML \u2014 ${err.message}`
28272
- );
28273
- console.error("neat codex: fix the file and re-run; nothing was written.");
28274
- return { exitCode: 1 };
28275
- }
28276
- const guide = await readGuide();
28277
- const agents = upsertAgents(agentsRaw, guide);
28278
- if (!opts.apply) {
28279
- console.log("neat codex \u2014 plan (nothing written; re-run with --apply to write)");
28280
- console.log("");
28281
- console.log(` Codex MCP config: ${configPath}`);
28282
- console.log(
28283
- config.changed ? ` ${configRaw ? "update" : "create"} the [mcp_servers.neat] table:` : " already up to date \u2014 [mcp_servers.neat] matches"
28284
- );
28285
- if (config.changed) {
28286
- for (const line of CODEX_NEAT_BLOCK.split("\n")) console.log(` ${line}`);
28651
+ if (!res.ok) {
28652
+ return {
28653
+ name: "daemon",
28654
+ ok: false,
28655
+ detail: `reachable at ${url} but /health returned ${res.status}`,
28656
+ fix: "check the daemon logs"
28657
+ };
28287
28658
  }
28288
- console.log("");
28289
- console.log(` Project instructions: ${agentsPath}`);
28290
- console.log(
28291
- agents.changed ? ` ${agentsRaw ? "update" : "create"} the graph-first block (between ${NEAT_GRAPH_FIRST_START} markers)` : " already up to date \u2014 graph-first block matches"
28292
- );
28293
- console.log("");
28294
- console.log("The MCP server reads NEAT_CORE_URL for the daemon URL \u2014 edit that value in");
28295
- console.log("the generated table to point Codex at a non-default daemon.");
28296
- return { exitCode: 0 };
28297
- }
28298
- if (config.changed) {
28299
- await import_node_fs54.promises.mkdir(import_node_path89.default.dirname(configPath), { recursive: true });
28300
- await import_node_fs54.promises.writeFile(configPath, config.text, "utf8");
28301
- console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
28302
- } else {
28303
- console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
28304
- }
28305
- if (agents.changed) {
28306
- await import_node_fs54.promises.mkdir(import_node_path89.default.dirname(agentsPath), { recursive: true });
28307
- await import_node_fs54.promises.writeFile(agentsPath, agents.text, "utf8");
28308
- console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
28309
- } else {
28310
- console.log(`neat codex: ${agentsPath} already has the graph-first block`);
28659
+ const body = await res.json().catch(() => ({}));
28660
+ return { name: "daemon", ok: true, detail: summariseHealth(url, body) };
28661
+ } catch {
28662
+ return {
28663
+ name: "daemon",
28664
+ ok: false,
28665
+ detail: `down \u2014 nothing answering at ${url}`,
28666
+ fix: "start it: `neat .` (or `neat watch`)"
28667
+ };
28311
28668
  }
28312
- console.log("");
28313
- console.log("restart Codex to pick up the new MCP server. NEAT_CORE_URL in the table");
28314
- console.log("points the server at the local daemon \u2014 edit it for a non-default one.");
28315
- return { exitCode: 0 };
28316
- }
28317
- function usage3() {
28318
- console.log("neat codex \u2014 install NEAT into the OpenAI Codex CLI (MCP server + AGENTS.md)");
28319
- console.log("");
28320
- console.log(" (no flag) plan: print what would change, write nothing");
28321
- console.log(" --apply add [mcp_servers.neat] to ~/.codex/config.toml and write");
28322
- console.log(" the graph-first block into ./AGENTS.md, merging into both");
28323
- console.log(" without touching your other servers or instructions");
28324
- console.log(" --print-config print the [mcp_servers.neat] TOML block to stdout");
28325
- console.log(" --print-guide print the AGENTS.md graph-first block to stdout");
28326
- console.log("");
28327
- console.log("Existing config is preserved and a re-run is a no-op. A malformed");
28328
- console.log("config.toml is a clear error with no partial write.");
28329
28669
  }
28330
- async function runCodexCommand(args) {
28331
- const opts = { apply: false, printConfig: false, printGuide: false };
28332
- for (const arg of args) {
28333
- switch (arg) {
28334
- case "--apply":
28335
- opts.apply = true;
28336
- break;
28337
- case "--print-config":
28338
- opts.printConfig = true;
28339
- break;
28340
- case "--print-guide":
28341
- opts.printGuide = true;
28342
- break;
28343
- case "-h":
28344
- case "--help":
28345
- usage3();
28346
- return 0;
28347
- default:
28348
- console.error(`neat codex: unknown flag "${arg}"`);
28349
- usage3();
28350
- return 2;
28670
+ async function runDoctorChecks(deps = {}) {
28671
+ const d = resolveDeps2(deps);
28672
+ const record = await d.readRecord(d.cwd).catch(() => null);
28673
+ return [checkNode(d.nodeVersion), await checkProject(d.cwd, record), await checkDaemon(d, record)];
28674
+ }
28675
+ var NAME_COL = "project".length;
28676
+ function renderHuman(checks, out) {
28677
+ out("neat doctor \u2014 checking this project's setup");
28678
+ out("");
28679
+ for (const c of checks) {
28680
+ const mark = c.ok ? "\u2713" : "\u2717";
28681
+ out(` ${mark} ${c.name.padEnd(NAME_COL)} ${c.detail}`);
28682
+ if (!c.ok && c.fix) out(` ${" ".repeat(NAME_COL + 3)}fix: ${c.fix}`);
28683
+ }
28684
+ out("");
28685
+ const failed = checks.filter((c) => !c.ok).length;
28686
+ out(failed === 0 ? "all good." : `${failed} check${failed === 1 ? "" : "s"} failed.`);
28687
+ }
28688
+ async function runDoctorCommand(argv, deps = {}) {
28689
+ const out = deps.out ?? ((line) => console.log(line));
28690
+ let json = false;
28691
+ for (const arg of argv) {
28692
+ if (arg === "--json") json = true;
28693
+ else if (arg === "-h" || arg === "--help") {
28694
+ out("usage: neat doctor [--json]");
28695
+ out(" Probe this directory's NEAT setup: Node version, project, daemon.");
28696
+ out(" Exit 0 when every check passes, 1 when any fails.");
28697
+ return 0;
28698
+ } else {
28699
+ out(`neat doctor: unknown argument "${arg}"`);
28700
+ return 2;
28351
28701
  }
28352
28702
  }
28353
- try {
28354
- const { exitCode } = await runCodex(opts);
28355
- return exitCode;
28356
- } catch (err) {
28357
- console.error(err.message);
28358
- return 1;
28359
- }
28703
+ const checks = await runDoctorChecks(deps);
28704
+ if (json) out(JSON.stringify({ ok: checks.every((c) => c.ok), checks }, null, 2));
28705
+ else renderHuman(checks, out);
28706
+ return checks.every((c) => c.ok) ? 0 : 1;
28360
28707
  }
28361
28708
 
28362
- // src/editors-cli.ts
28709
+ // src/hooks-cli.ts
28363
28710
  init_cjs_shims();
28364
- var import_node_path90 = __toESM(require("path"), 1);
28365
- var import_node_os7 = __toESM(require("os"), 1);
28366
- var import_node_fs55 = require("fs");
28367
- var import_node_util2 = require("util");
28368
- var jsonc = __toESM(require("jsonc-parser"), 1);
28369
- var NEAT_MCP_SERVER = {
28370
- command: "npx",
28371
- args: ["-y", "@neat.is/mcp"]
28372
- };
28373
- var NEAT_OPENCODE_SERVER = {
28374
- type: "local",
28375
- command: ["npx", "-y", "@neat.is/mcp"],
28376
- enabled: true
28377
- };
28378
- var NEAT_CRUSH_SERVER = {
28379
- type: "stdio",
28380
- command: "npx",
28381
- args: ["-y", "@neat.is/mcp"]
28382
- };
28383
- var GRAPH_FIRST_MARKER_OPEN = "<!-- neat:graph-first -->";
28384
- var GRAPH_FIRST_MARKER_CLOSE = "<!-- /neat:graph-first -->";
28385
- function homeDir() {
28386
- return process.env.HOME ?? process.env.USERPROFILE ?? import_node_os7.default.homedir();
28711
+ var import_node_path88 = __toESM(require("path"), 1);
28712
+ var import_node_os5 = __toESM(require("os"), 1);
28713
+ var import_node_fs53 = require("fs");
28714
+ var import_node_url5 = require("url");
28715
+ var HOOK_FILENAME = "neat-search-nudge.mjs";
28716
+ var GUIDE_FILENAME = "GRAPH_FIRST.md";
28717
+ var GUIDE_INSTALL_NAME = "neat-graph-first.md";
28718
+ var HOOK_MATCHER = "Grep|Glob|Bash";
28719
+ function moduleDir() {
28720
+ return typeof __dirname !== "undefined" ? __dirname : import_node_path88.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
28387
28721
  }
28388
- function xdgConfigDir() {
28389
- const xdg = process.env.XDG_CONFIG_HOME;
28390
- return xdg && xdg.length > 0 ? import_node_path90.default.resolve(xdg) : import_node_path90.default.join(homeDir(), ".config");
28722
+ async function readSkillAsset(rel) {
28723
+ const here = moduleDir();
28724
+ const candidates = [
28725
+ import_node_path88.default.resolve(here, "../../claude-skill", rel),
28726
+ import_node_path88.default.resolve(here, "../../../claude-skill", rel),
28727
+ import_node_path88.default.resolve(here, "../claude-skill", rel)
28728
+ ];
28729
+ for (const candidate of candidates) {
28730
+ try {
28731
+ return await import_node_fs53.promises.readFile(candidate, "utf8");
28732
+ } catch {
28733
+ }
28734
+ }
28735
+ throw new Error(
28736
+ `neat hooks: could not find @neat.is/claude-skill/${rel} \u2014 is the package installed?`
28737
+ );
28391
28738
  }
28392
- function envOverride(name) {
28393
- const v = process.env[name];
28394
- return v && v.length > 0 ? import_node_path90.default.resolve(v) : void 0;
28739
+ function neatHome3() {
28740
+ const override = process.env.NEAT_HOME;
28741
+ if (override && override.length > 0) return import_node_path88.default.resolve(override);
28742
+ return import_node_path88.default.join(import_node_os5.default.homedir(), ".neat");
28395
28743
  }
28396
- var CURSOR_CLIENT = {
28397
- id: "cursor",
28398
- label: "Cursor",
28399
- docsUrl: "https://docs.cursor.com/context/mcp",
28400
- mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? import_node_path90.default.join(homeDir(), ".cursor", "mcp.json"),
28401
- mcpContainerKey: "mcpServers",
28402
- format: "json",
28403
- // Cursor still reads a single `.cursorrules` at the project root (the modern
28404
- // `.cursor/rules/*.mdc` split is one-rule-per-file with frontmatter — a worse
28405
- // fit for a marker-fenced block). GRAPH_FIRST.md names this file directly.
28406
- rulesFileName: ".cursorrules"
28407
- };
28408
- var DEVIN_CLIENT = {
28409
- id: "devin",
28410
- label: "Devin Desktop (Cascade)",
28411
- docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
28412
- mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? import_node_path90.default.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
28413
- mcpContainerKey: "mcpServers",
28414
- format: "json",
28415
- rulesFileName: ".windsurfrules"
28416
- };
28417
- var GEMINI_CLIENT = {
28418
- id: "gemini",
28419
- label: "Gemini CLI",
28420
- docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
28421
- mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? import_node_path90.default.join(homeDir(), ".gemini", "settings.json"),
28422
- mcpContainerKey: "mcpServers",
28423
- format: "json",
28424
- rulesFileName: "GEMINI.md"
28425
- };
28426
- var QWEN_CLIENT = {
28427
- id: "qwen",
28428
- label: "Qwen Code",
28429
- docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
28430
- mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? import_node_path90.default.join(homeDir(), ".qwen", "settings.json"),
28431
- mcpContainerKey: "mcpServers",
28432
- format: "json",
28433
- rulesFileName: "QWEN.md"
28434
- };
28435
- var AMAZONQ_CLIENT = {
28436
- id: "amazonq",
28437
- label: "Amazon Q Developer CLI",
28438
- docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
28439
- mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? import_node_path90.default.join(homeDir(), ".aws", "amazonq", "mcp.json"),
28440
- mcpContainerKey: "mcpServers",
28441
- format: "json"
28442
- };
28443
- var ROOCODE_CLIENT = {
28444
- id: "roocode",
28445
- label: "Roo Code",
28446
- docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
28447
- mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? import_node_path90.default.join(process.cwd(), ".roo", "mcp.json"),
28448
- mcpContainerKey: "mcpServers",
28449
- format: "json"
28450
- };
28451
- var ZED_CLIENT = {
28452
- id: "zed",
28453
- label: "Zed",
28454
- docsUrl: "https://zed.dev/docs/ai/mcp",
28455
- mcpConfigPath: () => {
28456
- const override = envOverride("NEAT_ZED_CONFIG");
28457
- if (override) return override;
28458
- if (process.platform === "win32") {
28459
- const appData = process.env.APPDATA;
28460
- if (appData && appData.length > 0) return import_node_path90.default.join(appData, "Zed", "settings.json");
28461
- }
28462
- return import_node_path90.default.join(homeDir(), ".config", "zed", "settings.json");
28463
- },
28464
- mcpContainerKey: "context_servers",
28465
- format: "jsonc",
28466
- rulesFileName: ".rules"
28467
- };
28468
- var OPENCODE_CLIENT = {
28469
- id: "opencode",
28470
- label: "OpenCode",
28471
- docsUrl: "https://opencode.ai/docs/mcp-servers/",
28472
- mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? import_node_path90.default.join(xdgConfigDir(), "opencode", "opencode.json"),
28473
- mcpContainerKey: "mcp",
28474
- format: "json",
28475
- serverEntry: NEAT_OPENCODE_SERVER,
28476
- rulesFileName: "AGENTS.md"
28477
- };
28478
- var CRUSH_CLIENT = {
28479
- id: "crush",
28480
- label: "Crush",
28481
- docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
28482
- mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? import_node_path90.default.join(xdgConfigDir(), "crush", "crush.json"),
28483
- mcpContainerKey: "mcp",
28484
- format: "json",
28485
- serverEntry: NEAT_CRUSH_SERVER,
28486
- rulesFileName: "AGENTS.md"
28487
- };
28488
- var CLIENTS = {
28489
- cursor: CURSOR_CLIENT,
28490
- devin: DEVIN_CLIENT,
28491
- gemini: GEMINI_CLIENT,
28492
- qwen: QWEN_CLIENT,
28493
- amazonq: AMAZONQ_CLIENT,
28494
- roocode: ROOCODE_CLIENT,
28495
- zed: ZED_CLIENT,
28496
- opencode: OPENCODE_CLIENT,
28497
- crush: CRUSH_CLIENT
28498
- };
28499
- function mergeJsonMcp(existing, containerKey, serverEntry) {
28500
- const servers = existing[containerKey] ?? {};
28501
- const already = (0, import_node_util2.isDeepStrictEqual)(servers.neat, serverEntry);
28502
- const merged = {
28503
- ...existing,
28504
- [containerKey]: { ...servers, neat: serverEntry }
28505
- };
28506
- return { merged, changed: !already };
28507
- }
28508
- function mergeJsoncMcp(raw, containerKey, serverEntry) {
28509
- const base = raw.trim().length > 0 ? raw : "{}";
28510
- const parsed = jsonc.parse(base) ?? {};
28511
- const servers = parsed[containerKey] ?? {};
28512
- if ((0, import_node_util2.isDeepStrictEqual)(servers.neat, serverEntry)) {
28513
- return { text: raw, changed: false };
28514
- }
28515
- const edits = jsonc.modify(base, [containerKey, "neat"], serverEntry, {
28516
- formattingOptions: { tabSize: 2, insertSpaces: true }
28517
- });
28518
- let text = jsonc.applyEdits(base, edits);
28519
- if (!text.endsWith("\n")) text += "\n";
28520
- return { text, changed: text !== raw };
28744
+ function claudeSettingsPath() {
28745
+ const override = process.env.NEAT_CLAUDE_SETTINGS;
28746
+ if (override && override.length > 0) return import_node_path88.default.resolve(override);
28747
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os5.default.homedir();
28748
+ return import_node_path88.default.join(home, ".claude", "settings.json");
28521
28749
  }
28522
- function escapeRegExp(s) {
28523
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
28750
+ function installedHookPath() {
28751
+ return import_node_path88.default.join(neatHome3(), "hooks", HOOK_FILENAME);
28524
28752
  }
28525
- function buildGuidanceBlock(guide) {
28526
- return `${GRAPH_FIRST_MARKER_OPEN}
28527
- ${guide.trim()}
28528
- ${GRAPH_FIRST_MARKER_CLOSE}
28529
- `;
28753
+ function gateFlagPath() {
28754
+ return import_node_path88.default.join(neatHome3(), "hooks", "gate-enabled");
28530
28755
  }
28531
- function mergeRulesFile(existing, block) {
28532
- const region = new RegExp(
28533
- `${escapeRegExp(GRAPH_FIRST_MARKER_OPEN)}[\\s\\S]*?${escapeRegExp(GRAPH_FIRST_MARKER_CLOSE)}\\n?`
28756
+ function isNeatSearchEntry(entry2) {
28757
+ return (entry2.hooks ?? []).some(
28758
+ (h) => typeof h.command === "string" && h.command.includes(HOOK_FILENAME)
28534
28759
  );
28535
- if (region.test(existing)) return existing.replace(region, block);
28536
- if (existing.trim().length === 0) return block;
28537
- return `${existing.replace(/\s+$/, "")}
28538
-
28539
- ${block}`;
28540
28760
  }
28541
- async function planMcp(client, mcpPath) {
28542
- const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
28543
- let raw = "";
28544
- try {
28545
- raw = await import_node_fs55.promises.readFile(mcpPath, "utf8");
28546
- } catch (err) {
28547
- const e = err;
28548
- if (e.code === "ENOENT") {
28549
- raw = "";
28550
- } else {
28551
- console.error(`neat ${client.id}: failed to read ${mcpPath} \u2014 ${e.message}`);
28552
- return null;
28553
- }
28761
+ function neatHookEntry(command) {
28762
+ return { matcher: HOOK_MATCHER, hooks: [{ type: "command", command }] };
28763
+ }
28764
+ function hookCommand(scriptPath) {
28765
+ return `node "${scriptPath}"`;
28766
+ }
28767
+ async function runHooks(opts) {
28768
+ if (opts.printHook) {
28769
+ process.stdout.write(await readSkillAsset(`hooks/${HOOK_FILENAME}`));
28770
+ return { exitCode: 0 };
28554
28771
  }
28555
- if (client.format === "jsonc") {
28556
- if (raw.trim().length > 0) {
28557
- const errors = [];
28558
- jsonc.parse(raw, errors, { allowTrailingComma: true });
28559
- if (errors.length > 0) {
28560
- const first = errors[0];
28561
- console.error(
28562
- `neat ${client.id}: ${mcpPath} is not valid JSONC \u2014 ${jsonc.printParseErrorCode(first.error)} at offset ${first.offset}. Fix it (or move it aside) and re-run; nothing was written.`
28563
- );
28564
- return null;
28565
- }
28566
- }
28567
- return mergeJsoncMcp(raw, client.mcpContainerKey, serverEntry);
28772
+ if (opts.printGuide) {
28773
+ process.stdout.write(await readSkillAsset(GUIDE_FILENAME));
28774
+ return { exitCode: 0 };
28568
28775
  }
28569
- let existing = {};
28570
- if (raw.trim().length > 0) {
28571
- try {
28572
- existing = JSON.parse(raw);
28573
- } catch (err) {
28574
- console.error(
28575
- `neat ${client.id}: ${mcpPath} is not valid JSON \u2014 ${err.message}. Fix it (or move it aside) and re-run; nothing was written.`
28576
- );
28577
- return null;
28578
- }
28776
+ if (opts.printSettings) {
28777
+ const block = {
28778
+ hooks: { PreToolUse: [neatHookEntry(hookCommand(installedHookPath()))] }
28779
+ };
28780
+ process.stdout.write(JSON.stringify(block, null, 2) + "\n");
28781
+ return { exitCode: 0 };
28579
28782
  }
28580
- const { merged, changed } = mergeJsonMcp(existing, client.mcpContainerKey, serverEntry);
28581
- return { text: JSON.stringify(merged, null, 2) + "\n", changed };
28582
- }
28583
- async function runEditorInstall(client, opts) {
28584
- const mcpPath = client.mcpConfigPath();
28585
- const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
28586
- const hasRules = typeof client.rulesFileName === "string";
28587
- const rulesPath = hasRules ? import_node_path90.default.join(opts.projectDir, client.rulesFileName) : "";
28588
- const mcp = await planMcp(client, mcpPath);
28589
- if (mcp === null) return { exitCode: 1 };
28590
- let existingRules = "";
28591
- let newRules = "";
28592
- let rulesChanged = false;
28593
- let block = "";
28594
- if (hasRules) {
28783
+ if (opts.apply) {
28784
+ const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
28785
+ const guide = await readSkillAsset(GUIDE_FILENAME);
28786
+ const scriptPath = installedHookPath();
28787
+ await import_node_fs53.promises.mkdir(import_node_path88.default.dirname(scriptPath), { recursive: true });
28788
+ await import_node_fs53.promises.writeFile(scriptPath, hookScript, { mode: 493 });
28789
+ const guidePath = import_node_path88.default.join(neatHome3(), GUIDE_INSTALL_NAME);
28790
+ await import_node_fs53.promises.writeFile(guidePath, guide, "utf8");
28791
+ const settingsFile = claudeSettingsPath();
28792
+ let settings = {};
28595
28793
  try {
28596
- existingRules = await import_node_fs55.promises.readFile(rulesPath, "utf8");
28794
+ settings = JSON.parse(await import_node_fs53.promises.readFile(settingsFile, "utf8"));
28597
28795
  } catch (err) {
28598
28796
  if (err.code !== "ENOENT") {
28599
- console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
28797
+ console.error(
28798
+ `neat hooks: failed to read ${settingsFile} \u2014 ${err.message}`
28799
+ );
28600
28800
  return { exitCode: 1 };
28601
28801
  }
28602
28802
  }
28603
- const guide = await readSkillAsset(GUIDE_FILENAME);
28604
- block = buildGuidanceBlock(guide);
28605
- newRules = mergeRulesFile(existingRules, block);
28606
- rulesChanged = newRules !== existingRules;
28607
- }
28608
- if (!opts.apply) {
28609
- console.log(`neat ${client.id} \u2014 wire NEAT into ${client.label} (plan; nothing written)`);
28610
- console.log("");
28611
- console.log(`MCP server \u2192 ${mcpPath}`);
28612
- console.log(
28613
- mcp.changed ? ` would add ${client.mcpContainerKey}.neat:` : ` ${client.mcpContainerKey}.neat already present and current \u2014 no change:`
28614
- );
28615
- console.log(indent(JSON.stringify({ neat: serverEntry }, null, 2)));
28616
- if (hasRules) {
28617
- console.log("");
28618
- console.log(`Graph-first guidance \u2192 ${rulesPath}`);
28619
- console.log(
28620
- rulesChanged ? existingRules.includes(GRAPH_FIRST_MARKER_OPEN) ? " would refresh the neat:graph-first block:" : " would add the neat:graph-first block:" : " neat:graph-first block already present and current \u2014 no change."
28621
- );
28622
- if (rulesChanged) console.log(indent(block.trimEnd()));
28803
+ const hooks = settings.hooks ?? {};
28804
+ const preToolUse = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
28805
+ const command = hookCommand(scriptPath);
28806
+ const existingIdx = preToolUse.findIndex(isNeatSearchEntry);
28807
+ if (existingIdx >= 0) {
28808
+ preToolUse[existingIdx] = neatHookEntry(command);
28809
+ } else {
28810
+ preToolUse.push(neatHookEntry(command));
28623
28811
  }
28812
+ const merged = {
28813
+ ...settings,
28814
+ hooks: { ...hooks, PreToolUse: preToolUse }
28815
+ };
28816
+ await import_node_fs53.promises.mkdir(import_node_path88.default.dirname(settingsFile), { recursive: true });
28817
+ await import_node_fs53.promises.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
28818
+ const flag = gateFlagPath();
28819
+ if (opts.gate) {
28820
+ await import_node_fs53.promises.mkdir(import_node_path88.default.dirname(flag), { recursive: true });
28821
+ await import_node_fs53.promises.writeFile(flag, "1\n", "utf8");
28822
+ } else {
28823
+ await import_node_fs53.promises.rm(flag, { force: true });
28824
+ }
28825
+ const mode = opts.gate ? "GATE (deny search until you ask the graph)" : "nudge (search still runs)";
28826
+ console.log(`neat hooks: installed the search hook in ${opts.gate ? "gate" : "nudge"} mode`);
28827
+ console.log(` script: ${scriptPath}`);
28828
+ console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
28829
+ console.log(` guidance: ${guidePath}`);
28830
+ console.log(` mode: ${mode}`);
28624
28831
  console.log("");
28625
- console.log(
28626
- hasRules ? `Re-run with --apply to write both files. Existing servers and rules are kept.` : `Re-run with --apply to write the config. Existing servers are kept.`
28627
- );
28832
+ if (opts.gate) {
28833
+ console.log("restart Claude Code to load the hook. A Grep/Glob or Bash grep is now DENIED");
28834
+ console.log('until you run `neat ask "<question>"` (or the ask MCP tool) once this session;');
28835
+ console.log("after that, search is allowed as a fallback. Set NEAT_SEARCH_GATE=0 to fall");
28836
+ console.log("back to nudge-only without re-running.");
28837
+ } else {
28838
+ console.log("restart Claude Code to load the hook. On a Grep/Glob or a Bash grep,");
28839
+ console.log("your agent will now be nudged to query NEAT first (the search still runs).");
28840
+ console.log("Re-run with --gate to hard-force the graph-first orientation.");
28841
+ }
28842
+ console.log("");
28843
+ console.log("The hook is Claude-Code-specific. For agents on other harnesses, paste");
28844
+ console.log(`the guidance above into your project instructions (CLAUDE.md / AGENTS.md).`);
28628
28845
  return { exitCode: 0 };
28629
28846
  }
28630
- await import_node_fs55.promises.mkdir(import_node_path90.default.dirname(mcpPath), { recursive: true });
28631
- await import_node_fs55.promises.writeFile(mcpPath, mcp.text, "utf8");
28632
- if (hasRules) {
28633
- await import_node_fs55.promises.mkdir(import_node_path90.default.dirname(rulesPath), { recursive: true });
28634
- await import_node_fs55.promises.writeFile(rulesPath, newRules, "utf8");
28635
- }
28636
- console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
28637
- console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
28638
- if (hasRules) console.log(` guidance: ${rulesPath} (neat:graph-first block)`);
28639
- console.log("");
28640
- console.log(`restart ${client.label} to pick up the MCP server. Point it at a non-default`);
28641
- console.log(`daemon by setting NEAT_CORE_URL in the neat server's env in that config.`);
28847
+ usage();
28642
28848
  return { exitCode: 0 };
28643
28849
  }
28644
- function indent(text) {
28645
- return text.split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n");
28646
- }
28647
- function usage4(client) {
28648
- const hasRules = typeof client.rulesFileName === "string";
28649
- console.log(
28650
- hasRules ? `neat ${client.id} \u2014 install NEAT's MCP server + graph-first guidance into ${client.label}` : `neat ${client.id} \u2014 install NEAT's MCP server into ${client.label}`
28651
- );
28652
- console.log("");
28653
- console.log(
28654
- hasRules ? " --apply write the MCP config and the rules file (default: plan only)" : " --apply write the MCP config (default: plan only)"
28655
- );
28850
+ function usage() {
28851
+ console.log("neat hooks \u2014 wire NEAT into your agent so it queries the graph before grepping");
28656
28852
  console.log("");
28657
- console.log("Writes NEAT's stdio MCP server (npx -y @neat.is/mcp) into");
28658
- console.log(` ${client.mcpConfigPath()}`);
28659
- if (hasRules) {
28660
- console.log(`and the graph-first guidance block into ./${client.rulesFileName}, both`);
28661
- console.log("additively \u2014 existing servers and rules are preserved, a re-run is a no-op.");
28662
- } else {
28663
- console.log("additively \u2014 existing servers are preserved, a re-run is a no-op.");
28664
- }
28853
+ console.log(" --apply install the Claude Code search hook and write the");
28854
+ console.log(" graph-first guidance to ~/.neat/, merging into");
28855
+ console.log(" ~/.claude/settings.json without touching your other hooks");
28856
+ console.log(" --gate with --apply, enable hard-gate mode: DENY Grep/Glob/grep-Bash");
28857
+ console.log(" until `neat ask` has run this session (default is nudge-only).");
28858
+ console.log(" Toggle off at run time with NEAT_SEARCH_GATE=0.");
28859
+ console.log(" --print-hook print the hook script to stdout");
28860
+ console.log(" --print-guide print the agent-agnostic graph-first guidance to stdout");
28861
+ console.log(" --print-settings print the settings.json PreToolUse block --apply would add");
28665
28862
  console.log("");
28666
- console.log(`See ${client.docsUrl} for ${client.label}'s MCP config format.`);
28863
+ console.log("By default the hook is a gentle, non-blocking nudge \u2014 searches still run.");
28864
+ console.log("--gate turns it into a hard forcing mechanism. It is Claude-Code-specific;");
28865
+ console.log("other harnesses get the same steer from the graph-first guidance.");
28667
28866
  }
28668
- async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
28669
- const client = CLIENTS[clientId];
28670
- let apply6 = false;
28867
+ async function runHooksCommand(args) {
28868
+ const opts = {
28869
+ apply: false,
28870
+ printHook: false,
28871
+ printGuide: false,
28872
+ printSettings: false,
28873
+ gate: false
28874
+ };
28671
28875
  for (const arg of args) {
28672
28876
  switch (arg) {
28673
28877
  case "--apply":
28674
- apply6 = true;
28878
+ opts.apply = true;
28879
+ break;
28880
+ case "--gate":
28881
+ opts.gate = true;
28882
+ break;
28883
+ case "--print-hook":
28884
+ opts.printHook = true;
28885
+ break;
28886
+ case "--print-guide":
28887
+ opts.printGuide = true;
28888
+ break;
28889
+ case "--print-settings":
28890
+ opts.printSettings = true;
28675
28891
  break;
28676
28892
  case "-h":
28677
28893
  case "--help":
28678
- usage4(client);
28894
+ usage();
28679
28895
  return 0;
28680
28896
  default:
28681
- console.error(`neat ${client.id}: unknown flag "${arg}"`);
28682
- usage4(client);
28897
+ console.error(`neat hooks: unknown flag "${arg}"`);
28898
+ usage();
28683
28899
  return 2;
28684
28900
  }
28685
28901
  }
28686
28902
  try {
28687
- const { exitCode } = await runEditorInstall(client, { apply: apply6, projectDir });
28903
+ const { exitCode } = await runHooks(opts);
28688
28904
  return exitCode;
28689
28905
  } catch (err) {
28690
28906
  console.error(err.message);
@@ -28692,579 +28908,762 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
28692
28908
  }
28693
28909
  }
28694
28910
 
28695
- // src/monitor.ts
28911
+ // src/claude-cli.ts
28696
28912
  init_cjs_shims();
28697
- var import_types97 = require("@neat.is/types");
28913
+ var import_node_path89 = __toESM(require("path"), 1);
28914
+ var import_node_fs54 = require("fs");
28915
+ var NEAT_SECTION_HEADING = "## neat";
28916
+ var NEAT_DIRECTIVE_BODY = `This project has NEAT wired in: a live, fused semantic graph of the system \u2014
28917
+ code and runtime behaviour (OpenTelemetry) in one model, every fact tagged with
28918
+ its provenance (EXTRACTED / OBSERVED / INFERRED / STALE) and a confidence.
28698
28919
 
28699
- // src/cli-client.ts
28700
- init_cjs_shims();
28701
- var import_types96 = require("@neat.is/types");
28702
- var HttpError = class extends Error {
28703
- constructor(status2, message, responseBody = "") {
28704
- super(message);
28705
- this.status = status2;
28706
- this.responseBody = responseBody;
28707
- this.name = "HttpError";
28708
- }
28709
- status;
28710
- responseBody;
28711
- };
28712
- var TransportError = class extends Error {
28713
- constructor(message) {
28714
- super(message);
28715
- this.name = "TransportError";
28716
- }
28717
- };
28718
- function resolveAuthToken(env = process.env) {
28719
- const t = env.NEAT_AUTH_TOKEN;
28720
- return t && t.length > 0 ? t : void 0;
28920
+ For ANY question about this system's behaviour, dependencies, failures, root
28921
+ cause, or blast radius, call \`neat ask\` FIRST \u2014 before Read, Grep, Glob, or Bash:
28922
+
28923
+ neat ask "why is checkout failing?"
28924
+ neat ask "what breaks if I change the orders table?"
28925
+ neat ask "what does the payments service depend on at runtime?"
28926
+
28927
+ \`ask\` resolves your question to graph nodes and routes it to the right traversal
28928
+ \u2014 root cause, dependencies, observed runtime calls, incidents, divergences,
28929
+ blast radius \u2014 and returns one compact, provenance-tagged answer. The same door
28930
+ is the \`ask\` MCP tool (\`mcp__neat__ask\`) when NEAT's MCP server is wired in.
28931
+
28932
+ The graph is live and fused: it is faster and more accurate than scanning files,
28933
+ and it can tell you what the system actually does at runtime, not only what the
28934
+ source declares. Fall back to Read/Grep only when the graph does not have what
28935
+ you need \u2014 comments, string literals, config minutiae. Ask the graph first,
28936
+ then scan.
28937
+
28938
+ If \`neat ask\` errors, the daemon may not be running (\`neat list\`) \u2014 start it
28939
+ with \`neat <path>\`, then re-ask.`;
28940
+ function neatSection() {
28941
+ return `${NEAT_SECTION_HEADING}
28942
+
28943
+ ${NEAT_DIRECTIVE_BODY}
28944
+ `;
28721
28945
  }
28722
- function createHttpClient(baseUrl, bearerToken) {
28723
- const root = baseUrl.replace(/\/$/, "");
28724
- const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
28725
- return {
28726
- async get(path93) {
28727
- let res;
28728
- try {
28729
- res = await fetch(`${root}${path93}`, {
28730
- headers: { ...authHeader }
28731
- });
28732
- } catch (err) {
28733
- throw new TransportError(
28734
- `cannot reach neat-core at ${root}: ${err.message}`
28735
- );
28736
- }
28737
- if (!res.ok) {
28738
- const body = await res.text().catch(() => "");
28739
- throw new HttpError(
28740
- res.status,
28741
- `${res.status} ${res.statusText} on GET ${path93}: ${body}`,
28742
- body
28743
- );
28744
- }
28745
- return await res.json();
28746
- },
28747
- async post(path93, body) {
28748
- let res;
28749
- try {
28750
- res = await fetch(`${root}${path93}`, {
28751
- method: "POST",
28752
- headers: { "content-type": "application/json", ...authHeader },
28753
- body: JSON.stringify(body)
28754
- });
28755
- } catch (err) {
28756
- throw new TransportError(
28757
- `cannot reach neat-core at ${root}: ${err.message}`
28758
- );
28759
- }
28760
- if (!res.ok) {
28761
- const text = await res.text().catch(() => "");
28762
- throw new HttpError(
28763
- res.status,
28764
- `${res.status} ${res.statusText} on POST ${path93}: ${text}`,
28765
- text
28766
- );
28767
- }
28768
- return await res.json();
28946
+ function claudeMdPath() {
28947
+ const override = process.env.NEAT_CLAUDE_MD;
28948
+ if (override && override.length > 0) return import_node_path89.default.resolve(override);
28949
+ return import_node_path89.default.join(process.cwd(), "CLAUDE.md");
28950
+ }
28951
+ function splitAroundSection(raw) {
28952
+ const lines = raw.split("\n");
28953
+ const startIdx = lines.findIndex((l) => l.replace(/\s+$/, "") === NEAT_SECTION_HEADING);
28954
+ if (startIdx === -1) {
28955
+ return { before: raw.replace(/\n*$/, ""), after: "", found: false };
28956
+ }
28957
+ let endIdx = lines.length;
28958
+ for (let i = startIdx + 1; i < lines.length; i++) {
28959
+ if (/^#{1,2}\s+/.test(lines[i] ?? "")) {
28960
+ endIdx = i;
28961
+ break;
28769
28962
  }
28770
- };
28963
+ }
28964
+ const before = lines.slice(0, startIdx).join("\n").replace(/\n*$/, "");
28965
+ const after = lines.slice(endIdx).join("\n").replace(/^\n*/, "");
28966
+ return { before, after, found: true };
28771
28967
  }
28772
- function projectPath(project, suffix) {
28773
- if (!project) return suffix;
28774
- return `/projects/${encodeURIComponent(project)}${suffix}`;
28968
+ function compose(before, after) {
28969
+ const parts = [];
28970
+ if (before.length > 0) parts.push(before);
28971
+ parts.push(neatSection().replace(/\n+$/, ""));
28972
+ if (after.length > 0) parts.push(after);
28973
+ return parts.join("\n\n").replace(/\n*$/, "") + "\n";
28775
28974
  }
28776
- async function runRootCause(client, input) {
28777
- const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
28778
- const path93 = projectPath(
28779
- input.project,
28780
- `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
28781
- );
28975
+ async function readIfExists2(file) {
28782
28976
  try {
28783
- const result = await client.get(path93);
28784
- const arrowPath = result.traversalPath.join(" \u2190 ");
28785
- const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
28786
- const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
28787
- const blockLines = [
28788
- `Traversal path: ${arrowPath}`,
28789
- `Edge provenances: ${provenances}`
28790
- ];
28791
- if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
28792
- return {
28793
- summary,
28794
- block: blockLines.join("\n"),
28795
- confidence: result.confidence,
28796
- provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
28797
- };
28977
+ return await import_node_fs54.promises.readFile(file, "utf8");
28798
28978
  } catch (err) {
28799
- if (err instanceof HttpError && err.status === 404) {
28800
- return {
28801
- summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
28802
- };
28803
- }
28979
+ if (err.code === "ENOENT") return null;
28804
28980
  throw err;
28805
28981
  }
28806
28982
  }
28807
- async function runBlastRadius(client, input) {
28808
- const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
28809
- const path93 = projectPath(
28810
- input.project,
28811
- `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
28812
- );
28813
- try {
28814
- const result = await client.get(path93);
28815
- if (result.totalAffected === 0) {
28816
- return {
28817
- summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
28818
- };
28819
- }
28820
- const sorted = [...result.affectedNodes].sort(
28821
- (a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
28822
- );
28823
- const blockLines = sorted.map(formatBlastEntry);
28824
- const minConfidence = sorted.reduce(
28825
- (m, n) => Math.min(m, n.confidence),
28826
- Number.POSITIVE_INFINITY
28827
- );
28828
- const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
28829
- return {
28830
- summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
28831
- block: blockLines.join("\n"),
28832
- confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
28833
- provenance: provenances.length ? provenances : void 0
28834
- };
28835
- } catch (err) {
28836
- if (err instanceof HttpError && err.status === 404) {
28837
- return { summary: `Node ${input.nodeId} not found in the graph.` };
28838
- }
28839
- throw err;
28983
+ async function runInstall() {
28984
+ const file = claudeMdPath();
28985
+ const raw = await readIfExists2(file) ?? "";
28986
+ const { before, after, found } = splitAroundSection(raw);
28987
+ const next = compose(before, after);
28988
+ await import_node_fs54.promises.mkdir(import_node_path89.default.dirname(file), { recursive: true });
28989
+ await import_node_fs54.promises.writeFile(file, next, "utf8");
28990
+ const verb = raw.length === 0 ? "created" : found ? "refreshed" : "added";
28991
+ console.log(`neat claude: ${verb} the \`${NEAT_SECTION_HEADING}\` section in ${file}`);
28992
+ console.log("Your agent will now reach for `neat ask` before Read/Grep/Bash. Restart the");
28993
+ console.log("session (or reload CLAUDE.md) to pick it up.");
28994
+ return { exitCode: 0 };
28995
+ }
28996
+ async function runUninstall() {
28997
+ const file = claudeMdPath();
28998
+ const raw = await readIfExists2(file);
28999
+ if (raw === null) {
29000
+ console.log(`neat claude: no CLAUDE.md at ${file} \u2014 nothing to remove.`);
29001
+ return { exitCode: 0 };
29002
+ }
29003
+ const { before, after, found } = splitAroundSection(raw);
29004
+ if (!found) {
29005
+ console.log(`neat claude: no \`${NEAT_SECTION_HEADING}\` section in ${file} \u2014 nothing to remove.`);
29006
+ return { exitCode: 0 };
28840
29007
  }
29008
+ const remaining = [before, after].filter((s) => s.length > 0).join("\n\n");
29009
+ const next = remaining.length > 0 ? remaining.replace(/\n*$/, "") + "\n" : "";
29010
+ await import_node_fs54.promises.writeFile(file, next, "utf8");
29011
+ console.log(`neat claude: removed the \`${NEAT_SECTION_HEADING}\` section from ${file}.`);
29012
+ return { exitCode: 0 };
28841
29013
  }
28842
- function formatBlastEntry(n) {
28843
- const tag = n.edgeProvenance === import_types96.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
28844
- return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
29014
+ function usage2() {
29015
+ console.log("neat claude \u2014 make the query-first directive always-on in Claude Code");
29016
+ console.log("");
29017
+ console.log(" install write (or refresh) a `## neat` section in ./CLAUDE.md so your");
29018
+ console.log(" agent reaches for `neat ask` before Read/Grep/Bash");
29019
+ console.log(" uninstall remove the `## neat` section from ./CLAUDE.md");
29020
+ console.log(" print print the directive block to stdout (for a manual paste)");
29021
+ console.log("");
29022
+ console.log("Idempotent: re-running install replaces its own section, never duplicates it.");
29023
+ console.log("Target file overridable via NEAT_CLAUDE_MD.");
28845
29024
  }
28846
- async function runDependencies(client, input) {
28847
- const depth = input.depth ?? 3;
28848
- const path93 = projectPath(
28849
- input.project,
28850
- `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
28851
- );
29025
+ async function runClaudeCommand(args) {
29026
+ const sub = args[0];
29027
+ if (sub === "-h" || sub === "--help" || sub === void 0) {
29028
+ usage2();
29029
+ return sub === void 0 ? 2 : 0;
29030
+ }
28852
29031
  try {
28853
- const result = await client.get(path93);
28854
- if (result.total === 0) {
28855
- return {
28856
- summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
28857
- };
28858
- }
28859
- const byDistance = /* @__PURE__ */ new Map();
28860
- for (const dep of result.dependencies) {
28861
- const ring = byDistance.get(dep.distance) ?? [];
28862
- ring.push(dep);
28863
- byDistance.set(dep.distance, ring);
28864
- }
28865
- const blockLines = [];
28866
- for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
28867
- const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
28868
- blockLines.push(`${label}:`);
28869
- for (const dep of byDistance.get(distance)) {
28870
- blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
28871
- }
29032
+ switch (sub) {
29033
+ case "install":
29034
+ return (await runInstall()).exitCode;
29035
+ case "uninstall":
29036
+ return (await runUninstall()).exitCode;
29037
+ case "print":
29038
+ process.stdout.write(neatSection());
29039
+ return 0;
29040
+ default:
29041
+ console.error(`neat claude: unknown subcommand "${sub}"`);
29042
+ usage2();
29043
+ return 2;
28872
29044
  }
28873
- const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
28874
- const directCount = byDistance.get(1)?.length ?? 0;
28875
- const summary = depth === 1 ? `${input.nodeId} has ${directCount} direct dependenc${directCount === 1 ? "y" : "ies"}.` : `${input.nodeId} has ${result.total} dependenc${result.total === 1 ? "y" : "ies"} reachable to depth ${depth} (${directCount} direct).`;
28876
- return { summary, block: blockLines.join("\n"), provenance: provenances };
28877
29045
  } catch (err) {
28878
- if (err instanceof HttpError && err.status === 404) {
28879
- return { summary: `Node ${input.nodeId} not found in the graph.` };
28880
- }
28881
- throw err;
29046
+ console.error(`neat claude: ${err.message}`);
29047
+ return 1;
28882
29048
  }
28883
29049
  }
28884
- function observedDepLine(nodeId, e) {
28885
- const via = e.source !== nodeId ? ` (via ${e.source})` : "";
28886
- return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
29050
+
29051
+ // src/codex-cli.ts
29052
+ init_cjs_shims();
29053
+ var import_node_path90 = __toESM(require("path"), 1);
29054
+ var import_node_os6 = __toESM(require("os"), 1);
29055
+ var import_node_fs55 = require("fs");
29056
+ var import_node_util = require("util");
29057
+ var import_smol_toml6 = require("smol-toml");
29058
+ var CODEX_MCP_SERVER = {
29059
+ command: "npx",
29060
+ args: ["-y", "@neat.is/mcp"],
29061
+ env: { NEAT_CORE_URL: "http://localhost:8080" }
29062
+ };
29063
+ var CODEX_NEAT_BLOCK = [
29064
+ "[mcp_servers.neat]",
29065
+ 'command = "npx"',
29066
+ 'args = ["-y", "@neat.is/mcp"]',
29067
+ 'env = { NEAT_CORE_URL = "http://localhost:8080" }'
29068
+ ].join("\n");
29069
+ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
29070
+ var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
29071
+ function codexConfigPath() {
29072
+ const override = process.env.NEAT_CODEX_CONFIG;
29073
+ if (override && override.length > 0) return import_node_path90.default.resolve(override);
29074
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os6.default.homedir();
29075
+ return import_node_path90.default.join(home, ".codex", "config.toml");
28887
29076
  }
28888
- async function runObservedDependencies(client, input) {
28889
- try {
28890
- const result = await client.get(
28891
- projectPath(
28892
- input.project,
28893
- `/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
28894
- )
28895
- );
28896
- if (result.dependencies.length === 0) {
28897
- if (result.observed) {
28898
- return {
28899
- 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.`,
28900
- provenance: import_types96.Provenance.OBSERVED
28901
- };
28902
- }
28903
- const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
28904
- return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
28905
- }
28906
- const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
28907
- return {
28908
- summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
28909
- block: blockLines.join("\n"),
28910
- provenance: import_types96.Provenance.OBSERVED
28911
- };
28912
- } catch (err) {
28913
- if (err instanceof HttpError && err.status === 404) {
28914
- return { summary: `Node ${input.nodeId} not found in the graph.` };
28915
- }
28916
- throw err;
28917
- }
29077
+ function agentsFilePath() {
29078
+ const override = process.env.NEAT_CODEX_AGENTS;
29079
+ if (override && override.length > 0) return import_node_path90.default.resolve(override);
29080
+ return import_node_path90.default.join(process.cwd(), "AGENTS.md");
28918
29081
  }
28919
- function edgeMeta(e) {
28920
- const bits = [];
28921
- if (e.signal) {
28922
- bits.push(`spans=${e.signal.spanCount}`);
28923
- if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
28924
- if (e.signal.lastObservedAgeMs !== void 0) {
28925
- bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
28926
- }
28927
- } else if (e.callCount !== void 0) {
28928
- bits.push(`callCount=${e.callCount}`);
28929
- }
28930
- if (e.lastObserved) bits.push(`lastObserved=${e.lastObserved}`);
28931
- if (e.confidence !== void 0) bits.push(`confidence=${e.confidence}`);
28932
- return bits.length ? ` [${bits.join(", ")}]` : "";
29082
+ function isTableHeader(line) {
29083
+ return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
28933
29084
  }
28934
- function formatDuration(ms) {
28935
- if (ms < 1e3) return `${Math.round(ms)}ms`;
28936
- const s = Math.round(ms / 1e3);
28937
- if (s < 60) return `${s}s`;
28938
- const m = Math.round(s / 60);
28939
- if (m < 60) return `${m}m`;
28940
- const h = Math.round(m / 60);
28941
- if (h < 48) return `${h}h`;
28942
- return `${Math.round(h / 24)}d`;
29085
+ function tableName(line) {
29086
+ return line.trim().replace(/^\[\[?/, "").replace(/\]\]?$/, "").trim();
28943
29087
  }
28944
- async function runIncidents(client, input) {
28945
- const path93 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
28946
- try {
28947
- const body = await client.get(path93);
28948
- const events = body.events;
28949
- if (events.length === 0) {
28950
- return {
28951
- summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
28952
- };
28953
- }
28954
- const ordered = [...events].reverse().slice(0, input.limit ?? 20);
28955
- const blockLines = [];
28956
- for (const ev of ordered) {
28957
- blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
28958
- blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
28959
- }
28960
- const target = input.nodeId ?? "the project";
28961
- return {
28962
- summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
28963
- block: blockLines.join("\n"),
28964
- provenance: import_types96.Provenance.OBSERVED
29088
+ function isNeatHeader(line) {
29089
+ return isTableHeader(line) && tableName(line) === "mcp_servers.neat";
29090
+ }
29091
+ function isNeatChildHeader(line) {
29092
+ if (!isTableHeader(line)) return false;
29093
+ const name = tableName(line);
29094
+ return name === "mcp_servers.neat" || name.startsWith("mcp_servers.neat.");
29095
+ }
29096
+ function upsertCodexConfig(raw) {
29097
+ const trimmed = raw.trim();
29098
+ const parsed = trimmed.length > 0 ? (0, import_smol_toml6.parse)(raw) : {};
29099
+ const existingServers = parsed.mcp_servers ?? {};
29100
+ const spliced = spliceNeatBlock(raw);
29101
+ let text;
29102
+ if (verifyPreserved(raw, spliced, parsed)) {
29103
+ text = spliced;
29104
+ } else {
29105
+ const merged = {
29106
+ ...parsed,
29107
+ mcp_servers: { ...existingServers, neat: CODEX_MCP_SERVER }
28965
29108
  };
28966
- } catch (err) {
28967
- if (err instanceof HttpError && err.status === 404) {
28968
- return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
28969
- }
28970
- throw err;
29109
+ text = (0, import_smol_toml6.stringify)(merged);
29110
+ if (!text.endsWith("\n")) text += "\n";
28971
29111
  }
29112
+ return { text, changed: text !== raw };
28972
29113
  }
28973
- async function runSearch(client, input) {
28974
- const result = await client.get(
28975
- projectPath(input.project, `/search?q=${encodeURIComponent(input.query)}`)
28976
- );
28977
- if (result.matches.length === 0) {
28978
- return { summary: `No matches for "${input.query}".` };
29114
+ function spliceNeatBlock(raw) {
29115
+ const block = CODEX_NEAT_BLOCK;
29116
+ const lines = raw.length > 0 ? raw.split("\n") : [];
29117
+ const start = lines.findIndex(isNeatHeader);
29118
+ if (start === -1) {
29119
+ const base = raw.replace(/\n+$/, "");
29120
+ return base.length > 0 ? `${base}
29121
+
29122
+ ${block}
29123
+ ` : `${block}
29124
+ `;
28979
29125
  }
28980
- const provider = result.provider ?? "substring";
28981
- const blockLines = [];
28982
- let topScore;
28983
- for (const n of result.matches) {
28984
- const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
28985
- const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
28986
- if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
28987
- blockLines.push(
28988
- ` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
28989
- );
29126
+ let end = start + 1;
29127
+ for (; ; ) {
29128
+ while (end < lines.length && !isTableHeader(lines[end])) end++;
29129
+ if (end < lines.length && isNeatChildHeader(lines[end])) {
29130
+ end++;
29131
+ continue;
29132
+ }
29133
+ break;
28990
29134
  }
28991
- return {
28992
- summary: `Found ${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${input.query}" via ${provider} provider.`,
28993
- block: blockLines.join("\n"),
28994
- confidence: topScore
28995
- };
29135
+ const before = lines.slice(0, start).join("\n").replace(/\n*$/, "");
29136
+ const after = lines.slice(end).join("\n").replace(/^\n*/, "");
29137
+ let text = "";
29138
+ if (before.length > 0) text += `${before}
29139
+
29140
+ `;
29141
+ text += `${block}
29142
+ `;
29143
+ if (after.length > 0) text += `
29144
+ ${after}`;
29145
+ return `${text.replace(/\n*$/, "")}
29146
+ `;
28996
29147
  }
28997
- async function runDiff(client, input) {
28998
- const result = await client.get(
28999
- projectPath(
29000
- input.project,
29001
- `/graph/diff?against=${encodeURIComponent(input.againstSnapshot)}`
29002
- )
29003
- );
29004
- const total = result.added.nodes.length + result.added.edges.length + result.removed.nodes.length + result.removed.edges.length + result.changed.nodes.length + result.changed.edges.length;
29005
- const baseLabel = result.base.exportedAt ?? "unknown";
29006
- if (total === 0) {
29007
- return {
29008
- summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
29009
- };
29148
+ function verifyPreserved(raw, spliced, original) {
29149
+ let next;
29150
+ try {
29151
+ next = (0, import_smol_toml6.parse)(spliced);
29152
+ } catch {
29153
+ return false;
29010
29154
  }
29011
- const blockLines = [
29012
- ` base exportedAt: ${baseLabel}`,
29013
- ` current exportedAt: ${result.current.exportedAt}`,
29014
- ""
29015
- ];
29016
- if (result.added.nodes.length || result.added.edges.length) {
29017
- blockLines.push("Added:");
29018
- for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
29019
- for (const e of result.added.edges)
29020
- blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
29021
- blockLines.push("");
29155
+ const origServers = original.mcp_servers ?? {};
29156
+ const nextServers = next.mcp_servers ?? {};
29157
+ if (!(0, import_node_util.isDeepStrictEqual)(nextServers.neat, CODEX_MCP_SERVER)) return false;
29158
+ for (const name of Object.keys(origServers)) {
29159
+ if (name === "neat") continue;
29160
+ if (!(0, import_node_util.isDeepStrictEqual)(nextServers[name], origServers[name])) return false;
29022
29161
  }
29023
- if (result.removed.nodes.length || result.removed.edges.length) {
29024
- blockLines.push("Removed:");
29025
- for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
29026
- for (const e of result.removed.edges)
29027
- blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
29028
- blockLines.push("");
29162
+ for (const name of Object.keys(nextServers)) {
29163
+ if (name !== "neat" && !(name in origServers)) return false;
29029
29164
  }
29030
- if (result.changed.nodes.length || result.changed.edges.length) {
29031
- blockLines.push("Changed:");
29032
- for (const c of result.changed.nodes) {
29033
- blockLines.push(` ~ node ${c.id} \u2014 ${summariseAttrDiff(c.before, c.after)}`);
29034
- }
29035
- for (const c of result.changed.edges) {
29036
- const provBit = c.before.provenance !== c.after.provenance ? `provenance ${c.before.provenance} \u2192 ${c.after.provenance}` : summariseAttrDiff(c.before, c.after);
29037
- blockLines.push(` ~ edge ${c.id} \u2014 ${provBit}`);
29038
- }
29165
+ for (const key of Object.keys(original)) {
29166
+ if (key === "mcp_servers") continue;
29167
+ if (!(0, import_node_util.isDeepStrictEqual)(next[key], original[key])) return false;
29039
29168
  }
29040
- return {
29041
- summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
29042
- block: blockLines.join("\n").trimEnd()
29043
- };
29044
- }
29045
- function summariseAttrDiff(before, after) {
29046
- const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
29047
- const changed = [];
29048
- for (const k of keys) {
29049
- if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.push(k);
29169
+ for (const key of Object.keys(next)) {
29170
+ if (key !== "mcp_servers" && !(key in original)) return false;
29050
29171
  }
29051
- return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
29172
+ return true;
29052
29173
  }
29053
- async function runStaleEdges(client, input) {
29054
- const params = new URLSearchParams();
29055
- if (input.limit !== void 0) params.set("limit", String(input.limit));
29056
- if (input.edgeType) params.set("edgeType", input.edgeType);
29057
- const qs = params.size > 0 ? `?${params.toString()}` : "";
29058
- const body = await client.get(
29059
- projectPath(input.project, `/stale-events${qs}`)
29060
- );
29061
- const events = body.events;
29062
- if (events.length === 0) {
29063
- return {
29064
- summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
29065
- };
29174
+ function agentsBlock(guide) {
29175
+ return `${NEAT_GRAPH_FIRST_START}
29176
+ ${guide.replace(/\s+$/, "")}
29177
+ ${NEAT_GRAPH_FIRST_END}
29178
+ `;
29179
+ }
29180
+ function upsertAgents(raw, guide) {
29181
+ const block = agentsBlock(guide);
29182
+ if (raw.length === 0) return { text: block, changed: true };
29183
+ const startIdx = raw.indexOf(NEAT_GRAPH_FIRST_START);
29184
+ const endIdx = raw.indexOf(NEAT_GRAPH_FIRST_END);
29185
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
29186
+ const before = raw.slice(0, startIdx);
29187
+ const after = raw.slice(endIdx + NEAT_GRAPH_FIRST_END.length);
29188
+ const text2 = `${before}${block.replace(/\n+$/, "")}${after}`;
29189
+ return { text: text2, changed: text2 !== raw };
29066
29190
  }
29067
- const blockLines = events.map(
29068
- (e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
29069
- );
29070
- return {
29071
- summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
29072
- block: blockLines.join("\n"),
29073
- provenance: import_types96.Provenance.STALE
29074
- };
29191
+ const base = raw.replace(/\n+$/, "");
29192
+ const text = base.length > 0 ? `${base}
29193
+
29194
+ ${block}` : block;
29195
+ return { text, changed: text !== raw };
29075
29196
  }
29076
- async function runPolicies(client, input) {
29077
- let violations;
29078
- let allowed = true;
29079
- let hypothetical;
29080
- if (input.hypotheticalAction) {
29081
- if (typeof client.post !== "function") {
29082
- throw new Error("HttpClient does not support POST \u2014 required for policies dry-run");
29197
+ async function readGuide() {
29198
+ return readSkillAsset(GUIDE_FILENAME);
29199
+ }
29200
+ async function runCodex(opts) {
29201
+ if (opts.printConfig) {
29202
+ process.stdout.write(`${CODEX_NEAT_BLOCK}
29203
+ `);
29204
+ return { exitCode: 0 };
29205
+ }
29206
+ if (opts.printGuide) {
29207
+ process.stdout.write(agentsBlock(await readGuide()));
29208
+ return { exitCode: 0 };
29209
+ }
29210
+ const configPath = codexConfigPath();
29211
+ const agentsPath = agentsFilePath();
29212
+ let configRaw = "";
29213
+ try {
29214
+ configRaw = await import_node_fs55.promises.readFile(configPath, "utf8");
29215
+ } catch (err) {
29216
+ if (err.code !== "ENOENT") {
29217
+ console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
29218
+ return { exitCode: 1 };
29083
29219
  }
29084
- const body = await client.post(
29085
- projectPath(input.project, "/policies/check"),
29086
- { hypotheticalAction: input.hypotheticalAction }
29087
- );
29088
- violations = body.violations;
29089
- allowed = body.allowed;
29090
- hypothetical = body.hypotheticalAction;
29091
- } else {
29092
- const params = new URLSearchParams();
29093
- if (input.policyId) params.set("policyId", input.policyId);
29094
- const qs = params.size > 0 ? `?${params.toString()}` : "";
29095
- const body = await client.get(
29096
- projectPath(input.project, `/policies/violations${qs}`)
29220
+ }
29221
+ let agentsRaw = "";
29222
+ try {
29223
+ agentsRaw = await import_node_fs55.promises.readFile(agentsPath, "utf8");
29224
+ } catch (err) {
29225
+ if (err.code !== "ENOENT") {
29226
+ console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
29227
+ return { exitCode: 1 };
29228
+ }
29229
+ }
29230
+ let config;
29231
+ try {
29232
+ config = upsertCodexConfig(configRaw);
29233
+ } catch (err) {
29234
+ console.error(
29235
+ `neat codex: ${configPath} is not valid TOML \u2014 ${err.message}`
29097
29236
  );
29098
- violations = body.violations;
29099
- allowed = violations.every((v) => v.onViolation !== "block");
29237
+ console.error("neat codex: fix the file and re-run; nothing was written.");
29238
+ return { exitCode: 1 };
29100
29239
  }
29101
- if (input.nodeId) {
29102
- violations = violations.filter(
29103
- (v) => v.subject.nodeId === input.nodeId || v.subject.path?.includes(input.nodeId)
29240
+ const guide = await readGuide();
29241
+ const agents = upsertAgents(agentsRaw, guide);
29242
+ if (!opts.apply) {
29243
+ console.log("neat codex \u2014 plan (nothing written; re-run with --apply to write)");
29244
+ console.log("");
29245
+ console.log(` Codex MCP config: ${configPath}`);
29246
+ console.log(
29247
+ config.changed ? ` ${configRaw ? "update" : "create"} the [mcp_servers.neat] table:` : " already up to date \u2014 [mcp_servers.neat] matches"
29104
29248
  );
29249
+ if (config.changed) {
29250
+ for (const line of CODEX_NEAT_BLOCK.split("\n")) console.log(` ${line}`);
29251
+ }
29252
+ console.log("");
29253
+ console.log(` Project instructions: ${agentsPath}`);
29254
+ console.log(
29255
+ agents.changed ? ` ${agentsRaw ? "update" : "create"} the graph-first block (between ${NEAT_GRAPH_FIRST_START} markers)` : " already up to date \u2014 graph-first block matches"
29256
+ );
29257
+ console.log("");
29258
+ console.log("The MCP server reads NEAT_CORE_URL for the daemon URL \u2014 edit that value in");
29259
+ console.log("the generated table to point Codex at a non-default daemon.");
29260
+ return { exitCode: 0 };
29105
29261
  }
29106
- if (violations.length === 0) {
29107
- return {
29108
- summary: hypothetical ? `No violations would result from the hypothetical action (${hypothetical.kind}).` : "No policy violations recorded."
29109
- };
29262
+ if (config.changed) {
29263
+ await import_node_fs55.promises.mkdir(import_node_path90.default.dirname(configPath), { recursive: true });
29264
+ await import_node_fs55.promises.writeFile(configPath, config.text, "utf8");
29265
+ console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
29266
+ } else {
29267
+ console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
29110
29268
  }
29111
- const blockCount = violations.filter((v) => v.onViolation === "block").length;
29112
- const summaryParts = [];
29113
- if (hypothetical) {
29114
- summaryParts.push(
29115
- `Hypothetical ${hypothetical.kind} would surface ${violations.length} violation${violations.length === 1 ? "" : "s"}`
29116
- );
29269
+ if (agents.changed) {
29270
+ await import_node_fs55.promises.mkdir(import_node_path90.default.dirname(agentsPath), { recursive: true });
29271
+ await import_node_fs55.promises.writeFile(agentsPath, agents.text, "utf8");
29272
+ console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
29117
29273
  } else {
29118
- summaryParts.push(
29119
- `${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
29120
- );
29274
+ console.log(`neat codex: ${agentsPath} already has the graph-first block`);
29121
29275
  }
29122
- if (blockCount > 0) summaryParts.push(`${blockCount} of which block`);
29123
- if (!allowed && hypothetical) summaryParts.push("action denied");
29124
- const summary = summaryParts.join("; ") + ".";
29125
- const blockLines = violations.map((v) => {
29126
- const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
29127
- return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
29128
- });
29129
- const severities = [...new Set(violations.map((v) => v.severity))];
29130
- return {
29131
- summary,
29132
- block: blockLines.join("\n"),
29133
- confidence: hypothetical ? 0.7 : 1,
29134
- provenance: severities.join(" ")
29135
- };
29276
+ console.log("");
29277
+ console.log("restart Codex to pick up the new MCP server. NEAT_CORE_URL in the table");
29278
+ console.log("points the server at the local daemon \u2014 edit it for a non-default one.");
29279
+ return { exitCode: 0 };
29136
29280
  }
29137
- function formatDivergenceLine(d) {
29138
- switch (d.type) {
29139
- case "missing-observed":
29140
- case "missing-extracted":
29141
- if (d.column) {
29142
- return ` \u2022 [${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 confidence ${d.confidence.toFixed(2)}`;
29143
- }
29144
- return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014 confidence ${d.confidence.toFixed(2)}`;
29145
- case "version-mismatch":
29146
- return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared ${d.extractedVersion}, observed engine ${d.observedVersion} (${d.compatibility})`;
29147
- case "host-mismatch":
29148
- return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
29149
- case "compat-violation":
29150
- return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
29151
- case "observed-symbol-mismatch": {
29152
- const at = d.location ? ` at ${d.location}` : "";
29153
- const member = d.symbol ? ` ${d.symbol}` : "";
29154
- return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
29281
+ function usage3() {
29282
+ console.log("neat codex \u2014 install NEAT into the OpenAI Codex CLI (MCP server + AGENTS.md)");
29283
+ console.log("");
29284
+ console.log(" (no flag) plan: print what would change, write nothing");
29285
+ console.log(" --apply add [mcp_servers.neat] to ~/.codex/config.toml and write");
29286
+ console.log(" the graph-first block into ./AGENTS.md, merging into both");
29287
+ console.log(" without touching your other servers or instructions");
29288
+ console.log(" --print-config print the [mcp_servers.neat] TOML block to stdout");
29289
+ console.log(" --print-guide print the AGENTS.md graph-first block to stdout");
29290
+ console.log("");
29291
+ console.log("Existing config is preserved and a re-run is a no-op. A malformed");
29292
+ console.log("config.toml is a clear error with no partial write.");
29293
+ }
29294
+ async function runCodexCommand(args) {
29295
+ const opts = { apply: false, printConfig: false, printGuide: false };
29296
+ for (const arg of args) {
29297
+ switch (arg) {
29298
+ case "--apply":
29299
+ opts.apply = true;
29300
+ break;
29301
+ case "--print-config":
29302
+ opts.printConfig = true;
29303
+ break;
29304
+ case "--print-guide":
29305
+ opts.printGuide = true;
29306
+ break;
29307
+ case "-h":
29308
+ case "--help":
29309
+ usage3();
29310
+ return 0;
29311
+ default:
29312
+ console.error(`neat codex: unknown flag "${arg}"`);
29313
+ usage3();
29314
+ return 2;
29315
+ }
29316
+ }
29317
+ try {
29318
+ const { exitCode } = await runCodex(opts);
29319
+ return exitCode;
29320
+ } catch (err) {
29321
+ console.error(err.message);
29322
+ return 1;
29323
+ }
29324
+ }
29325
+
29326
+ // src/editors-cli.ts
29327
+ init_cjs_shims();
29328
+ var import_node_path91 = __toESM(require("path"), 1);
29329
+ var import_node_os7 = __toESM(require("os"), 1);
29330
+ var import_node_fs56 = require("fs");
29331
+ var import_node_util2 = require("util");
29332
+ var jsonc = __toESM(require("jsonc-parser"), 1);
29333
+ var NEAT_MCP_SERVER = {
29334
+ command: "npx",
29335
+ args: ["-y", "@neat.is/mcp"]
29336
+ };
29337
+ var NEAT_OPENCODE_SERVER = {
29338
+ type: "local",
29339
+ command: ["npx", "-y", "@neat.is/mcp"],
29340
+ enabled: true
29341
+ };
29342
+ var NEAT_CRUSH_SERVER = {
29343
+ type: "stdio",
29344
+ command: "npx",
29345
+ args: ["-y", "@neat.is/mcp"]
29346
+ };
29347
+ var GRAPH_FIRST_MARKER_OPEN = "<!-- neat:graph-first -->";
29348
+ var GRAPH_FIRST_MARKER_CLOSE = "<!-- /neat:graph-first -->";
29349
+ function homeDir() {
29350
+ return process.env.HOME ?? process.env.USERPROFILE ?? import_node_os7.default.homedir();
29351
+ }
29352
+ function xdgConfigDir() {
29353
+ const xdg = process.env.XDG_CONFIG_HOME;
29354
+ return xdg && xdg.length > 0 ? import_node_path91.default.resolve(xdg) : import_node_path91.default.join(homeDir(), ".config");
29355
+ }
29356
+ function envOverride(name) {
29357
+ const v = process.env[name];
29358
+ return v && v.length > 0 ? import_node_path91.default.resolve(v) : void 0;
29359
+ }
29360
+ var CURSOR_CLIENT = {
29361
+ id: "cursor",
29362
+ label: "Cursor",
29363
+ docsUrl: "https://docs.cursor.com/context/mcp",
29364
+ mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? import_node_path91.default.join(homeDir(), ".cursor", "mcp.json"),
29365
+ mcpContainerKey: "mcpServers",
29366
+ format: "json",
29367
+ // Cursor still reads a single `.cursorrules` at the project root (the modern
29368
+ // `.cursor/rules/*.mdc` split is one-rule-per-file with frontmatter — a worse
29369
+ // fit for a marker-fenced block). GRAPH_FIRST.md names this file directly.
29370
+ rulesFileName: ".cursorrules"
29371
+ };
29372
+ var DEVIN_CLIENT = {
29373
+ id: "devin",
29374
+ label: "Devin Desktop (Cascade)",
29375
+ docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
29376
+ mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? import_node_path91.default.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
29377
+ mcpContainerKey: "mcpServers",
29378
+ format: "json",
29379
+ rulesFileName: ".windsurfrules"
29380
+ };
29381
+ var GEMINI_CLIENT = {
29382
+ id: "gemini",
29383
+ label: "Gemini CLI",
29384
+ docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
29385
+ mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? import_node_path91.default.join(homeDir(), ".gemini", "settings.json"),
29386
+ mcpContainerKey: "mcpServers",
29387
+ format: "json",
29388
+ rulesFileName: "GEMINI.md"
29389
+ };
29390
+ var QWEN_CLIENT = {
29391
+ id: "qwen",
29392
+ label: "Qwen Code",
29393
+ docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
29394
+ mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? import_node_path91.default.join(homeDir(), ".qwen", "settings.json"),
29395
+ mcpContainerKey: "mcpServers",
29396
+ format: "json",
29397
+ rulesFileName: "QWEN.md"
29398
+ };
29399
+ var AMAZONQ_CLIENT = {
29400
+ id: "amazonq",
29401
+ label: "Amazon Q Developer CLI",
29402
+ docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
29403
+ mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? import_node_path91.default.join(homeDir(), ".aws", "amazonq", "mcp.json"),
29404
+ mcpContainerKey: "mcpServers",
29405
+ format: "json"
29406
+ };
29407
+ var ROOCODE_CLIENT = {
29408
+ id: "roocode",
29409
+ label: "Roo Code",
29410
+ docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
29411
+ mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? import_node_path91.default.join(process.cwd(), ".roo", "mcp.json"),
29412
+ mcpContainerKey: "mcpServers",
29413
+ format: "json"
29414
+ };
29415
+ var ZED_CLIENT = {
29416
+ id: "zed",
29417
+ label: "Zed",
29418
+ docsUrl: "https://zed.dev/docs/ai/mcp",
29419
+ mcpConfigPath: () => {
29420
+ const override = envOverride("NEAT_ZED_CONFIG");
29421
+ if (override) return override;
29422
+ if (process.platform === "win32") {
29423
+ const appData = process.env.APPDATA;
29424
+ if (appData && appData.length > 0) return import_node_path91.default.join(appData, "Zed", "settings.json");
29155
29425
  }
29426
+ return import_node_path91.default.join(homeDir(), ".config", "zed", "settings.json");
29427
+ },
29428
+ mcpContainerKey: "context_servers",
29429
+ format: "jsonc",
29430
+ rulesFileName: ".rules"
29431
+ };
29432
+ var OPENCODE_CLIENT = {
29433
+ id: "opencode",
29434
+ label: "OpenCode",
29435
+ docsUrl: "https://opencode.ai/docs/mcp-servers/",
29436
+ mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? import_node_path91.default.join(xdgConfigDir(), "opencode", "opencode.json"),
29437
+ mcpContainerKey: "mcp",
29438
+ format: "json",
29439
+ serverEntry: NEAT_OPENCODE_SERVER,
29440
+ rulesFileName: "AGENTS.md"
29441
+ };
29442
+ var CRUSH_CLIENT = {
29443
+ id: "crush",
29444
+ label: "Crush",
29445
+ docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
29446
+ mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? import_node_path91.default.join(xdgConfigDir(), "crush", "crush.json"),
29447
+ mcpContainerKey: "mcp",
29448
+ format: "json",
29449
+ serverEntry: NEAT_CRUSH_SERVER,
29450
+ rulesFileName: "AGENTS.md"
29451
+ };
29452
+ var CLIENTS = {
29453
+ cursor: CURSOR_CLIENT,
29454
+ devin: DEVIN_CLIENT,
29455
+ gemini: GEMINI_CLIENT,
29456
+ qwen: QWEN_CLIENT,
29457
+ amazonq: AMAZONQ_CLIENT,
29458
+ roocode: ROOCODE_CLIENT,
29459
+ zed: ZED_CLIENT,
29460
+ opencode: OPENCODE_CLIENT,
29461
+ crush: CRUSH_CLIENT
29462
+ };
29463
+ function mergeJsonMcp(existing, containerKey, serverEntry) {
29464
+ const servers = existing[containerKey] ?? {};
29465
+ const already = (0, import_node_util2.isDeepStrictEqual)(servers.neat, serverEntry);
29466
+ const merged = {
29467
+ ...existing,
29468
+ [containerKey]: { ...servers, neat: serverEntry }
29469
+ };
29470
+ return { merged, changed: !already };
29471
+ }
29472
+ function mergeJsoncMcp(raw, containerKey, serverEntry) {
29473
+ const base = raw.trim().length > 0 ? raw : "{}";
29474
+ const parsed = jsonc.parse(base) ?? {};
29475
+ const servers = parsed[containerKey] ?? {};
29476
+ if ((0, import_node_util2.isDeepStrictEqual)(servers.neat, serverEntry)) {
29477
+ return { text: raw, changed: false };
29156
29478
  }
29479
+ const edits = jsonc.modify(base, [containerKey, "neat"], serverEntry, {
29480
+ formattingOptions: { tabSize: 2, insertSpaces: true }
29481
+ });
29482
+ let text = jsonc.applyEdits(base, edits);
29483
+ if (!text.endsWith("\n")) text += "\n";
29484
+ return { text, changed: text !== raw };
29157
29485
  }
29158
- async function runDivergences(client, input) {
29159
- const params = new URLSearchParams();
29160
- if (input.type && input.type.length > 0) params.set("type", input.type.join(","));
29161
- if (input.minConfidence !== void 0) {
29162
- params.set("minConfidence", String(input.minConfidence));
29163
- }
29164
- if (input.node) params.set("node", input.node);
29165
- const qs = params.size > 0 ? `?${params.toString()}` : "";
29166
- const result = await client.get(
29167
- projectPath(input.project, `/graph/divergences${qs}`)
29486
+ function escapeRegExp(s) {
29487
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
29488
+ }
29489
+ function buildGuidanceBlock(guide) {
29490
+ return `${GRAPH_FIRST_MARKER_OPEN}
29491
+ ${guide.trim()}
29492
+ ${GRAPH_FIRST_MARKER_CLOSE}
29493
+ `;
29494
+ }
29495
+ function mergeRulesFile(existing, block) {
29496
+ const region = new RegExp(
29497
+ `${escapeRegExp(GRAPH_FIRST_MARKER_OPEN)}[\\s\\S]*?${escapeRegExp(GRAPH_FIRST_MARKER_CLOSE)}\\n?`
29168
29498
  );
29169
- if (result.totalAffected === 0) {
29170
- return {
29171
- summary: "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
29172
- };
29499
+ if (region.test(existing)) return existing.replace(region, block);
29500
+ if (existing.trim().length === 0) return block;
29501
+ return `${existing.replace(/\s+$/, "")}
29502
+
29503
+ ${block}`;
29504
+ }
29505
+ async function planMcp(client, mcpPath) {
29506
+ const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
29507
+ let raw = "";
29508
+ try {
29509
+ raw = await import_node_fs56.promises.readFile(mcpPath, "utf8");
29510
+ } catch (err) {
29511
+ const e = err;
29512
+ if (e.code === "ENOENT") {
29513
+ raw = "";
29514
+ } else {
29515
+ console.error(`neat ${client.id}: failed to read ${mcpPath} \u2014 ${e.message}`);
29516
+ return null;
29517
+ }
29173
29518
  }
29174
- const headline = result.divergences[0];
29175
- const summary = `Found ${result.totalAffected} divergence${result.totalAffected === 1 ? "" : "s"} between code and production. Highest-confidence: ${headline.type} on ${headline.source} \u2192 ${headline.target}. ${headline.reason}`;
29176
- const blockLines = [];
29177
- for (const d of result.divergences) {
29178
- blockLines.push(formatDivergenceLine(d));
29179
- blockLines.push(` reason: ${d.reason}`);
29180
- blockLines.push(` recommendation: ${d.recommendation}`);
29519
+ if (client.format === "jsonc") {
29520
+ if (raw.trim().length > 0) {
29521
+ const errors = [];
29522
+ jsonc.parse(raw, errors, { allowTrailingComma: true });
29523
+ if (errors.length > 0) {
29524
+ const first = errors[0];
29525
+ console.error(
29526
+ `neat ${client.id}: ${mcpPath} is not valid JSONC \u2014 ${jsonc.printParseErrorCode(first.error)} at offset ${first.offset}. Fix it (or move it aside) and re-run; nothing was written.`
29527
+ );
29528
+ return null;
29529
+ }
29530
+ }
29531
+ return mergeJsoncMcp(raw, client.mcpContainerKey, serverEntry);
29181
29532
  }
29182
- const maxConfidence = result.divergences.reduce(
29183
- (m, d) => Math.max(m, d.confidence),
29184
- 0
29185
- );
29186
- return {
29187
- summary,
29188
- block: blockLines.join("\n"),
29189
- confidence: maxConfidence,
29190
- provenance: "composite (EXTRACTED + OBSERVED)"
29191
- };
29533
+ let existing = {};
29534
+ if (raw.trim().length > 0) {
29535
+ try {
29536
+ existing = JSON.parse(raw);
29537
+ } catch (err) {
29538
+ console.error(
29539
+ `neat ${client.id}: ${mcpPath} is not valid JSON \u2014 ${err.message}. Fix it (or move it aside) and re-run; nothing was written.`
29540
+ );
29541
+ return null;
29542
+ }
29543
+ }
29544
+ const { merged, changed } = mergeJsonMcp(existing, client.mcpContainerKey, serverEntry);
29545
+ return { text: JSON.stringify(merged, null, 2) + "\n", changed };
29192
29546
  }
29193
- async function runAsk(client, input) {
29194
- const result = await client.get(
29195
- projectPath(input.project, `/graph/ask?q=${encodeURIComponent(input.question)}`)
29196
- );
29197
- const blockLines = [];
29198
- if (result.matched.length > 0) {
29199
- blockLines.push(
29200
- `Matched: ${result.matched.map((m) => `${m.nodeId} [${m.via} ${m.score.toFixed(2)}]`).join(", ")}`
29201
- );
29202
- blockLines.push(`Intent: ${result.intent}`);
29203
- } else if (result.scope === "global") {
29204
- blockLines.push(`Graph-wide answer (${result.intent}) \u2014 no entity named.`);
29547
+ async function runEditorInstall(client, opts) {
29548
+ const mcpPath = client.mcpConfigPath();
29549
+ const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
29550
+ const hasRules = typeof client.rulesFileName === "string";
29551
+ const rulesPath = hasRules ? import_node_path91.default.join(opts.projectDir, client.rulesFileName) : "";
29552
+ const mcp = await planMcp(client, mcpPath);
29553
+ if (mcp === null) return { exitCode: 1 };
29554
+ let existingRules = "";
29555
+ let newRules = "";
29556
+ let rulesChanged = false;
29557
+ let block = "";
29558
+ if (hasRules) {
29559
+ try {
29560
+ existingRules = await import_node_fs56.promises.readFile(rulesPath, "utf8");
29561
+ } catch (err) {
29562
+ if (err.code !== "ENOENT") {
29563
+ console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
29564
+ return { exitCode: 1 };
29565
+ }
29566
+ }
29567
+ const guide = await readSkillAsset(GUIDE_FILENAME);
29568
+ block = buildGuidanceBlock(guide);
29569
+ newRules = mergeRulesFile(existingRules, block);
29570
+ rulesChanged = newRules !== existingRules;
29205
29571
  }
29206
- for (const section of result.sections) {
29207
- blockLines.push("", section.heading + ":");
29208
- for (const fact of section.facts) {
29209
- const tag = fact.provenance ? ` [${fact.provenance}${fact.confidence !== void 0 ? ` ${fact.confidence.toFixed(2)}` : ""}]` : fact.confidence !== void 0 ? ` [confidence ${fact.confidence.toFixed(2)}]` : "";
29210
- blockLines.push(` \u2022 ${fact.text}${tag}`);
29572
+ if (!opts.apply) {
29573
+ console.log(`neat ${client.id} \u2014 wire NEAT into ${client.label} (plan; nothing written)`);
29574
+ console.log("");
29575
+ console.log(`MCP server \u2192 ${mcpPath}`);
29576
+ console.log(
29577
+ mcp.changed ? ` would add ${client.mcpContainerKey}.neat:` : ` ${client.mcpContainerKey}.neat already present and current \u2014 no change:`
29578
+ );
29579
+ console.log(indent(JSON.stringify({ neat: serverEntry }, null, 2)));
29580
+ if (hasRules) {
29581
+ console.log("");
29582
+ console.log(`Graph-first guidance \u2192 ${rulesPath}`);
29583
+ console.log(
29584
+ rulesChanged ? existingRules.includes(GRAPH_FIRST_MARKER_OPEN) ? " would refresh the neat:graph-first block:" : " would add the neat:graph-first block:" : " neat:graph-first block already present and current \u2014 no change."
29585
+ );
29586
+ if (rulesChanged) console.log(indent(block.trimEnd()));
29211
29587
  }
29588
+ console.log("");
29589
+ console.log(
29590
+ hasRules ? `Re-run with --apply to write both files. Existing servers and rules are kept.` : `Re-run with --apply to write the config. Existing servers are kept.`
29591
+ );
29592
+ return { exitCode: 0 };
29212
29593
  }
29213
- return {
29214
- summary: result.answer,
29215
- block: blockLines.join("\n").trim(),
29216
- ...result.confidence !== void 0 ? { confidence: result.confidence } : {},
29217
- ...result.provenance.length > 0 ? { provenance: result.provenance } : {}
29218
- };
29219
- }
29220
- function formatFooter(confidence, provenance) {
29221
- const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
29222
- const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
29223
- return `confidence: ${c} \xB7 provenance: ${p}`;
29594
+ await import_node_fs56.promises.mkdir(import_node_path91.default.dirname(mcpPath), { recursive: true });
29595
+ await import_node_fs56.promises.writeFile(mcpPath, mcp.text, "utf8");
29596
+ if (hasRules) {
29597
+ await import_node_fs56.promises.mkdir(import_node_path91.default.dirname(rulesPath), { recursive: true });
29598
+ await import_node_fs56.promises.writeFile(rulesPath, newRules, "utf8");
29599
+ }
29600
+ console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
29601
+ console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
29602
+ if (hasRules) console.log(` guidance: ${rulesPath} (neat:graph-first block)`);
29603
+ console.log("");
29604
+ console.log(`restart ${client.label} to pick up the MCP server. Point it at a non-default`);
29605
+ console.log(`daemon by setting NEAT_CORE_URL in the neat server's env in that config.`);
29606
+ return { exitCode: 0 };
29224
29607
  }
29225
- function formatHuman(result) {
29226
- const sections = [result.summary.trim()];
29227
- if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
29228
- sections.push(formatFooter(result.confidence, result.provenance));
29229
- return sections.join("\n\n");
29608
+ function indent(text) {
29609
+ return text.split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n");
29230
29610
  }
29231
- function formatJson(result) {
29232
- return JSON.stringify(
29233
- {
29234
- summary: result.summary,
29235
- block: result.block ?? "",
29236
- confidence: result.confidence ?? null,
29237
- provenance: result.provenance ?? null
29238
- },
29239
- null,
29240
- 2
29611
+ function usage4(client) {
29612
+ const hasRules = typeof client.rulesFileName === "string";
29613
+ console.log(
29614
+ hasRules ? `neat ${client.id} \u2014 install NEAT's MCP server + graph-first guidance into ${client.label}` : `neat ${client.id} \u2014 install NEAT's MCP server into ${client.label}`
29241
29615
  );
29616
+ console.log("");
29617
+ console.log(
29618
+ hasRules ? " --apply write the MCP config and the rules file (default: plan only)" : " --apply write the MCP config (default: plan only)"
29619
+ );
29620
+ console.log("");
29621
+ console.log("Writes NEAT's stdio MCP server (npx -y @neat.is/mcp) into");
29622
+ console.log(` ${client.mcpConfigPath()}`);
29623
+ if (hasRules) {
29624
+ console.log(`and the graph-first guidance block into ./${client.rulesFileName}, both`);
29625
+ console.log("additively \u2014 existing servers and rules are preserved, a re-run is a no-op.");
29626
+ } else {
29627
+ console.log("additively \u2014 existing servers are preserved, a re-run is a no-op.");
29628
+ }
29629
+ console.log("");
29630
+ console.log(`See ${client.docsUrl} for ${client.label}'s MCP config format.`);
29242
29631
  }
29243
- function exitCodeForError(err) {
29244
- if (err instanceof TransportError) return 3;
29245
- if (err instanceof HttpError) return 1;
29246
- return 1;
29247
- }
29248
- function createSnapshotPushClient(baseUrl, token) {
29249
- return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
29250
- }
29251
- async function pushSnapshotToRemote(input) {
29252
- const client = createSnapshotPushClient(input.baseUrl, input.token);
29253
- if (typeof client.post !== "function") {
29254
- throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
29632
+ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
29633
+ const client = CLIENTS[clientId];
29634
+ let apply6 = false;
29635
+ for (const arg of args) {
29636
+ switch (arg) {
29637
+ case "--apply":
29638
+ apply6 = true;
29639
+ break;
29640
+ case "-h":
29641
+ case "--help":
29642
+ usage4(client);
29643
+ return 0;
29644
+ default:
29645
+ console.error(`neat ${client.id}: unknown flag "${arg}"`);
29646
+ usage4(client);
29647
+ return 2;
29648
+ }
29649
+ }
29650
+ try {
29651
+ const { exitCode } = await runEditorInstall(client, { apply: apply6, projectDir });
29652
+ return exitCode;
29653
+ } catch (err) {
29654
+ console.error(err.message);
29655
+ return 1;
29255
29656
  }
29256
- return client.post(
29257
- `/projects/${encodeURIComponent(input.project)}/snapshot`,
29258
- { snapshot: input.snapshot }
29259
- );
29260
29657
  }
29261
29658
 
29262
29659
  // src/monitor.ts
29660
+ init_cjs_shims();
29661
+ var import_types101 = require("@neat.is/types");
29263
29662
  var OBSERVED_DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
29264
- import_types97.EdgeType.CALLS,
29265
- import_types97.EdgeType.CONNECTS_TO,
29266
- import_types97.EdgeType.PUBLISHES_TO,
29267
- import_types97.EdgeType.CONSUMES_FROM
29663
+ import_types101.EdgeType.CALLS,
29664
+ import_types101.EdgeType.CONNECTS_TO,
29665
+ import_types101.EdgeType.PUBLISHES_TO,
29666
+ import_types101.EdgeType.CONSUMES_FROM
29268
29667
  ]);
29269
29668
  function divergenceKey(d) {
29270
29669
  const column = "column" in d && d.column ? d.column : "";
@@ -29314,7 +29713,7 @@ function formatDivergenceLine2(d) {
29314
29713
  }
29315
29714
  }
29316
29715
  function formatStaleLine(edgeId) {
29317
- const parsed = (0, import_types97.parseEdgeId)(edgeId);
29716
+ const parsed = (0, import_types101.parseEdgeId)(edgeId);
29318
29717
  if (parsed) {
29319
29718
  return `\u22EF stale ${parsed.source} \u2192 ${parsed.target} (observed edge went quiet)`;
29320
29719
  }
@@ -29327,7 +29726,7 @@ function divergenceJson(d) {
29327
29726
  return JSON.stringify({ kind: "divergence", ...d });
29328
29727
  }
29329
29728
  function staleJson(edgeId) {
29330
- const parsed = (0, import_types97.parseEdgeId)(edgeId);
29729
+ const parsed = (0, import_types101.parseEdgeId)(edgeId);
29331
29730
  return JSON.stringify({
29332
29731
  kind: "stale",
29333
29732
  edgeId,
@@ -29397,7 +29796,7 @@ var MonitorEmitter = class {
29397
29796
  // ignores non-OBSERVED edges and non-dependency edge types (structural
29398
29797
  // ownership), so only real runtime dependencies reach stdout.
29399
29798
  emitObservedEdge(edge) {
29400
- if (edge.provenance !== import_types97.Provenance.OBSERVED) return false;
29799
+ if (edge.provenance !== import_types101.Provenance.OBSERVED) return false;
29401
29800
  if (!OBSERVED_DEP_EDGE_TYPES.has(edge.type)) return false;
29402
29801
  const key = `edge|${edge.id}`;
29403
29802
  if (this.seen.has(key)) return false;
@@ -29555,7 +29954,7 @@ async function runMonitor(opts) {
29555
29954
  case "edge-added": {
29556
29955
  const payload = safeParse(frame.data);
29557
29956
  const edge = payload?.edge;
29558
- if (edge && edge.provenance === import_types97.Provenance.OBSERVED) {
29957
+ if (edge && edge.provenance === import_types101.Provenance.OBSERVED) {
29559
29958
  emitter.emitObservedEdge(edge);
29560
29959
  divergences.schedule();
29561
29960
  }
@@ -29635,7 +30034,7 @@ function sleep(ms, signal) {
29635
30034
 
29636
30035
  // src/cli-verbs.ts
29637
30036
  init_cjs_shims();
29638
- var import_node_path91 = __toESM(require("path"), 1);
30037
+ var import_node_path92 = __toESM(require("path"), 1);
29639
30038
  async function resolveProjectEntry(opts) {
29640
30039
  const entries = await listProjects();
29641
30040
  if (opts.project) {
@@ -29645,7 +30044,7 @@ async function resolveProjectEntry(opts) {
29645
30044
  const cwd = opts.cwd ?? process.cwd();
29646
30045
  const resolvedCwd = await normalizeProjectPath(cwd);
29647
30046
  for (const entry2 of entries) {
29648
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path91.default.sep}`)) {
30047
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path92.default.sep}`)) {
29649
30048
  return entry2;
29650
30049
  }
29651
30050
  }
@@ -29798,7 +30197,7 @@ async function runSync(opts) {
29798
30197
  }
29799
30198
 
29800
30199
  // src/cli.ts
29801
- var import_types98 = require("@neat.is/types");
30200
+ var import_types102 = require("@neat.is/types");
29802
30201
  function isNpxInvocation() {
29803
30202
  if (process.env.npm_command === "exec") return true;
29804
30203
  const execpath = process.env.npm_execpath ?? "";
@@ -29947,6 +30346,9 @@ function usage5() {
29947
30346
  console.log(" test <id> re-check an existing connector's credential");
29948
30347
  console.log(" Credentials default to an env-var reference ($VAR) resolved at");
29949
30348
  console.log(" run time; the config file is written owner-only (0600).");
30349
+ console.log(" doctor Preflight this directory's setup \u2014 Node version, project,");
30350
+ console.log(" and daemon reachability \u2014 and print a fix for anything down.");
30351
+ console.log(" Flags: --json. Exits 0 when all pass, 1 when a check fails.");
29950
30352
  console.log("");
29951
30353
  console.log("query commands (mirror the MCP tools, ADR-050):");
29952
30354
  console.log(" ask <question> Plain-language door: resolves the question to");
@@ -30172,7 +30574,7 @@ async function buildPatchSections(services, project) {
30172
30574
  }
30173
30575
  async function runInit(opts) {
30174
30576
  const written = [];
30175
- const stat = await import_node_fs56.promises.stat(opts.scanPath).catch(() => null);
30577
+ const stat = await import_node_fs57.promises.stat(opts.scanPath).catch(() => null);
30176
30578
  if (!stat || !stat.isDirectory()) {
30177
30579
  console.error(`neat init: ${opts.scanPath} is not a directory`);
30178
30580
  return { exitCode: 2, writtenFiles: written };
@@ -30181,13 +30583,13 @@ async function runInit(opts) {
30181
30583
  printDiscoveryReport(opts, services);
30182
30584
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
30183
30585
  const patch = renderPatch(sections);
30184
- const patchPath = import_node_path92.default.join(opts.scanPath, "neat.patch");
30586
+ const patchPath = import_node_path93.default.join(opts.scanPath, "neat.patch");
30185
30587
  if (opts.dryRun) {
30186
- await import_node_fs56.promises.writeFile(patchPath, patch, "utf8");
30588
+ await import_node_fs57.promises.writeFile(patchPath, patch, "utf8");
30187
30589
  written.push(patchPath);
30188
30590
  console.log(`dry-run: patch written to ${patchPath}`);
30189
- const gitignorePath = import_node_path92.default.join(opts.scanPath, ".gitignore");
30190
- const gitignoreExists = await import_node_fs56.promises.stat(gitignorePath).then(() => true).catch(() => false);
30591
+ const gitignorePath = import_node_path93.default.join(opts.scanPath, ".gitignore");
30592
+ const gitignoreExists = await import_node_fs57.promises.stat(gitignorePath).then(() => true).catch(() => false);
30191
30593
  const verb = gitignoreExists ? "append" : "create";
30192
30594
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
30193
30595
  console.log("rerun without --dry-run to register and snapshot.");
@@ -30198,9 +30600,9 @@ async function runInit(opts) {
30198
30600
  const graph = getGraph(graphKey);
30199
30601
  const projectPaths = pathsForProject(
30200
30602
  graphKey,
30201
- import_node_path92.default.join(opts.scanPath, "neat-out")
30603
+ import_node_path93.default.join(opts.scanPath, "neat-out")
30202
30604
  );
30203
- const errorsPath = import_node_path92.default.join(import_node_path92.default.dirname(opts.outPath), import_node_path92.default.basename(projectPaths.errorsPath));
30605
+ const errorsPath = import_node_path93.default.join(import_node_path93.default.dirname(opts.outPath), import_node_path93.default.basename(projectPaths.errorsPath));
30204
30606
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
30205
30607
  await saveGraphToDisk(graph, opts.outPath);
30206
30608
  written.push(opts.outPath);
@@ -30279,7 +30681,7 @@ async function runInit(opts) {
30279
30681
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
30280
30682
  }
30281
30683
  } else {
30282
- await import_node_fs56.promises.writeFile(patchPath, patch, "utf8");
30684
+ await import_node_fs57.promises.writeFile(patchPath, patch, "utf8");
30283
30685
  written.push(patchPath);
30284
30686
  }
30285
30687
  }
@@ -30320,9 +30722,9 @@ var CLAUDE_SKILL_CONFIG = {
30320
30722
  };
30321
30723
  function claudeConfigPath() {
30322
30724
  const override = process.env.NEAT_CLAUDE_CONFIG;
30323
- if (override && override.length > 0) return import_node_path92.default.resolve(override);
30725
+ if (override && override.length > 0) return import_node_path93.default.resolve(override);
30324
30726
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
30325
- return import_node_path92.default.join(home, ".claude.json");
30727
+ return import_node_path93.default.join(home, ".claude.json");
30326
30728
  }
30327
30729
  async function runSkill(opts) {
30328
30730
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -30334,7 +30736,7 @@ async function runSkill(opts) {
30334
30736
  const target = claudeConfigPath();
30335
30737
  let existing = {};
30336
30738
  try {
30337
- existing = JSON.parse(await import_node_fs56.promises.readFile(target, "utf8"));
30739
+ existing = JSON.parse(await import_node_fs57.promises.readFile(target, "utf8"));
30338
30740
  } catch (err) {
30339
30741
  if (err.code !== "ENOENT") {
30340
30742
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -30346,8 +30748,8 @@ async function runSkill(opts) {
30346
30748
  ...existing,
30347
30749
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
30348
30750
  };
30349
- await import_node_fs56.promises.mkdir(import_node_path92.default.dirname(target), { recursive: true });
30350
- await import_node_fs56.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
30751
+ await import_node_fs57.promises.mkdir(import_node_path93.default.dirname(target), { recursive: true });
30752
+ await import_node_fs57.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
30351
30753
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
30352
30754
  console.log("restart Claude Code to pick up the new MCP server.");
30353
30755
  console.log("");
@@ -30386,6 +30788,11 @@ async function main() {
30386
30788
  if (code !== 0) process.exit(code);
30387
30789
  return;
30388
30790
  }
30791
+ if (cmd0 === "doctor") {
30792
+ const code = await runDoctorCommand(argv.slice(1));
30793
+ if (code !== 0) process.exit(code);
30794
+ return;
30795
+ }
30389
30796
  if (cmd0 === "hooks") {
30390
30797
  const code = await runHooksCommand(argv.slice(1));
30391
30798
  if (code !== 0) process.exit(code);
@@ -30438,12 +30845,12 @@ async function main() {
30438
30845
  console.error("neat init: --apply and --dry-run are mutually exclusive");
30439
30846
  process.exit(2);
30440
30847
  }
30441
- const scanPath = import_node_path92.default.resolve(target);
30848
+ const scanPath = import_node_path93.default.resolve(target);
30442
30849
  const projectExplicit = parsed.project !== null;
30443
- const projectName = projectExplicit ? project : import_node_path92.default.basename(scanPath);
30850
+ const projectName = projectExplicit ? project : import_node_path93.default.basename(scanPath);
30444
30851
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
30445
- const fallback = pathsForProject(projectKey, import_node_path92.default.join(scanPath, "neat-out")).snapshotPath;
30446
- const outPath = import_node_path92.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
30852
+ const fallback = pathsForProject(projectKey, import_node_path93.default.join(scanPath, "neat-out")).snapshotPath;
30853
+ const outPath = import_node_path93.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
30447
30854
  const result = await runInit({
30448
30855
  scanPath,
30449
30856
  outPath,
@@ -30464,21 +30871,21 @@ async function main() {
30464
30871
  usage5();
30465
30872
  process.exit(2);
30466
30873
  }
30467
- const scanPath = import_node_path92.default.resolve(target);
30468
- const stat = await import_node_fs56.promises.stat(scanPath).catch(() => null);
30874
+ const scanPath = import_node_path93.default.resolve(target);
30875
+ const stat = await import_node_fs57.promises.stat(scanPath).catch(() => null);
30469
30876
  if (!stat || !stat.isDirectory()) {
30470
30877
  console.error(`neat watch: ${scanPath} is not a directory`);
30471
30878
  process.exit(2);
30472
30879
  }
30473
- const projectPaths = pathsForProject(project, import_node_path92.default.join(scanPath, "neat-out"));
30474
- const outPath = import_node_path92.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
30475
- const errorsPath = import_node_path92.default.resolve(
30476
- process.env.NEAT_ERRORS_PATH ?? import_node_path92.default.join(import_node_path92.default.dirname(outPath), import_node_path92.default.basename(projectPaths.errorsPath))
30880
+ const projectPaths = pathsForProject(project, import_node_path93.default.join(scanPath, "neat-out"));
30881
+ const outPath = import_node_path93.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
30882
+ const errorsPath = import_node_path93.default.resolve(
30883
+ process.env.NEAT_ERRORS_PATH ?? import_node_path93.default.join(import_node_path93.default.dirname(outPath), import_node_path93.default.basename(projectPaths.errorsPath))
30477
30884
  );
30478
- const staleEventsPath = import_node_path92.default.resolve(
30479
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path92.default.join(import_node_path92.default.dirname(outPath), import_node_path92.default.basename(projectPaths.staleEventsPath))
30885
+ const staleEventsPath = import_node_path93.default.resolve(
30886
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path93.default.join(import_node_path93.default.dirname(outPath), import_node_path93.default.basename(projectPaths.staleEventsPath))
30480
30887
  );
30481
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path92.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
30888
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path93.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
30482
30889
  const handle = await startWatch(getGraph(project), {
30483
30890
  scanPath,
30484
30891
  outPath,
@@ -30487,7 +30894,7 @@ async function main() {
30487
30894
  project,
30488
30895
  // Resolve NEAT_HOME so a `neat watch` picks up connectors added to
30489
30896
  // ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
30490
- neatHome: process.env.NEAT_HOME ? import_node_path92.default.resolve(process.env.NEAT_HOME) : import_node_path92.default.join(import_node_os8.default.homedir(), ".neat"),
30897
+ neatHome: process.env.NEAT_HOME ? import_node_path93.default.resolve(process.env.NEAT_HOME) : import_node_path93.default.join(import_node_os8.default.homedir(), ".neat"),
30491
30898
  ...embeddingsCachePath ? { embeddingsCachePath } : {},
30492
30899
  host: process.env.HOST ?? "0.0.0.0",
30493
30900
  port: Number(process.env.PORT ?? 8080),
@@ -30669,11 +31076,11 @@ async function main() {
30669
31076
  process.exit(1);
30670
31077
  }
30671
31078
  async function tryOrchestrator(cmd, parsed) {
30672
- const scanPath = import_node_path92.default.resolve(cmd);
30673
- const stat = await import_node_fs56.promises.stat(scanPath).catch(() => null);
31079
+ const scanPath = import_node_path93.default.resolve(cmd);
31080
+ const stat = await import_node_fs57.promises.stat(scanPath).catch(() => null);
30674
31081
  if (!stat || !stat.isDirectory()) return null;
30675
31082
  const projectExplicit = parsed.project !== null;
30676
- const projectName = projectExplicit ? parsed.project : import_node_path92.default.basename(scanPath);
31083
+ const projectName = projectExplicit ? parsed.project : import_node_path93.default.basename(scanPath);
30677
31084
  const result = await runOrchestrator({
30678
31085
  scanPath,
30679
31086
  project: projectName,
@@ -30874,10 +31281,10 @@ async function runQueryVerb(cmd, parsed) {
30874
31281
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
30875
31282
  const out = [];
30876
31283
  for (const p of parts) {
30877
- const r = import_types98.DivergenceTypeSchema.safeParse(p);
31284
+ const r = import_types102.DivergenceTypeSchema.safeParse(p);
30878
31285
  if (!r.success) {
30879
31286
  console.error(
30880
- `neat divergences: unknown --type "${p}". allowed: ${import_types98.DivergenceTypeSchema.options.join(", ")}`
31287
+ `neat divergences: unknown --type "${p}". allowed: ${import_types102.DivergenceTypeSchema.options.join(", ")}`
30881
31288
  );
30882
31289
  return 2;
30883
31290
  }