@neat.is/core 0.9.5-dev.20260824 → 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/{chunk-TGCWMMF6.js → chunk-4SLKQNG7.js} +2 -2
- package/dist/{chunk-IVVF37OU.js → chunk-DGAI4VOE.js} +264 -19
- package/dist/chunk-DGAI4VOE.js.map +1 -0
- package/dist/cli.cjs +1873 -1466
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1534 -1382
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +280 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +280 -26
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +278 -24
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-IVVF37OU.js.map +0 -1
- /package/dist/{chunk-TGCWMMF6.js.map → chunk-4SLKQNG7.js.map} +0 -0
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
|
|
65
|
-
if (exactUnauthPaths.has(
|
|
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
|
|
419
|
-
if (
|
|
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
|
|
806
|
+
var import_node_path93 = __toESM(require("path"), 1);
|
|
807
807
|
var import_node_os8 = __toESM(require("os"), 1);
|
|
808
|
-
var
|
|
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,
|
|
1377
|
-
if (
|
|
1378
|
-
best = { path: [...
|
|
1376
|
+
function step(node, path94, edges) {
|
|
1377
|
+
if (path94.length > best.path.length) {
|
|
1378
|
+
best = { path: [...path94], edges: [...edges] };
|
|
1379
1379
|
}
|
|
1380
|
-
if (
|
|
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
|
-
|
|
1385
|
+
path94.push(srcId);
|
|
1386
1386
|
edges.push(edge);
|
|
1387
|
-
step(srcId,
|
|
1388
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
2204
|
-
if (
|
|
2205
|
-
traversalPath =
|
|
2206
|
-
edgeProvenances =
|
|
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(
|
|
3655
|
-
return
|
|
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
|
|
4328
|
-
if (
|
|
4329
|
-
return { path:
|
|
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
|
|
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(
|
|
19139
|
-
const rpcMatch = REST_RPC_PATH_RE.exec(
|
|
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(
|
|
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
|
|
19746
|
-
if (!resourceName || !method || !
|
|
19747
|
-
return { resourceName, method, path:
|
|
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
|
|
19786
|
-
if (
|
|
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:
|
|
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
|
|
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
|
-
...
|
|
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,
|
|
20054
|
-
const normalizedPath = normalizePathTemplate(
|
|
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:
|
|
20071
|
+
const { method, path: path94 } = signal;
|
|
20072
20072
|
const resolveRouteGrain = (serviceName, wholeFileId) => {
|
|
20073
|
-
if (!method || !
|
|
20074
|
-
return findMatchingRouteNode(graph, serviceName, method,
|
|
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
|
|
20427
|
-
if (!serviceName || !method || !
|
|
20428
|
-
return { serviceName, method, path:
|
|
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
|
|
20458
|
-
if (
|
|
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:
|
|
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:
|
|
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(
|
|
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
|
|
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 !==
|
|
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:
|
|
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
|
|
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,
|
|
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:
|
|
21089
|
+
return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types90.EdgeType.CALLS };
|
|
20870
21090
|
}
|
|
20871
|
-
const providerId = (0,
|
|
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:
|
|
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
|
|
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(
|
|
21164
|
+
return [identity.serviceName, identity.phase].join(FIELD_SEP4);
|
|
20945
21165
|
}
|
|
20946
21166
|
function parseEasTargetName(targetName) {
|
|
20947
|
-
const sep = targetName.indexOf(
|
|
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
|
|
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 !==
|
|
21157
|
-
const parsed = (0,
|
|
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 !==
|
|
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:
|
|
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:
|
|
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
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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
|
|
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/
|
|
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
|
|
27751
|
-
|
|
27752
|
-
|
|
27753
|
-
|
|
27754
|
-
var
|
|
27755
|
-
|
|
27756
|
-
|
|
27757
|
-
|
|
27758
|
-
|
|
27759
|
-
|
|
27760
|
-
|
|
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
|
-
|
|
27813
|
-
|
|
27814
|
-
|
|
27815
|
-
|
|
27816
|
-
|
|
27817
|
-
|
|
28014
|
+
status;
|
|
28015
|
+
responseBody;
|
|
28016
|
+
};
|
|
28017
|
+
var TransportError = class extends Error {
|
|
28018
|
+
constructor(message) {
|
|
28019
|
+
super(message);
|
|
28020
|
+
this.name = "TransportError";
|
|
27818
28021
|
}
|
|
27819
|
-
|
|
27820
|
-
|
|
27821
|
-
|
|
27822
|
-
|
|
27823
|
-
|
|
27824
|
-
|
|
27825
|
-
|
|
27826
|
-
|
|
27827
|
-
|
|
27828
|
-
|
|
27829
|
-
|
|
27830
|
-
|
|
27831
|
-
|
|
27832
|
-
|
|
27833
|
-
|
|
27834
|
-
|
|
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
|
-
|
|
27840
|
-
|
|
27841
|
-
|
|
27842
|
-
|
|
27843
|
-
|
|
27844
|
-
|
|
27845
|
-
|
|
27846
|
-
|
|
27847
|
-
|
|
27848
|
-
|
|
27849
|
-
|
|
27850
|
-
|
|
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
|
-
|
|
27853
|
-
|
|
27854
|
-
|
|
27855
|
-
|
|
27856
|
-
|
|
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
|
-
|
|
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
|
|
27904
|
-
const
|
|
27905
|
-
|
|
27906
|
-
|
|
27907
|
-
|
|
27908
|
-
|
|
27909
|
-
|
|
27910
|
-
|
|
27911
|
-
|
|
27912
|
-
|
|
27913
|
-
|
|
27914
|
-
|
|
27915
|
-
|
|
27916
|
-
|
|
27917
|
-
|
|
27918
|
-
|
|
27919
|
-
|
|
27920
|
-
|
|
27921
|
-
|
|
27922
|
-
|
|
27923
|
-
|
|
27924
|
-
|
|
27925
|
-
|
|
27926
|
-
|
|
27927
|
-
|
|
27928
|
-
|
|
27929
|
-
|
|
27930
|
-
|
|
27931
|
-
|
|
27932
|
-
|
|
27933
|
-
|
|
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
|
|
27940
|
-
|
|
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
|
-
|
|
27943
|
-
|
|
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
|
-
|
|
27948
|
-
|
|
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
|
|
27988
|
-
|
|
27989
|
-
|
|
27990
|
-
|
|
27991
|
-
|
|
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
|
-
|
|
27994
|
-
|
|
27995
|
-
|
|
27996
|
-
|
|
27997
|
-
|
|
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
|
-
|
|
28001
|
-
|
|
28002
|
-
return
|
|
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
|
|
28005
|
-
|
|
28006
|
-
|
|
28007
|
-
|
|
28008
|
-
|
|
28009
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
28020
|
-
const
|
|
28021
|
-
|
|
28022
|
-
|
|
28023
|
-
|
|
28024
|
-
|
|
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
|
|
28040
|
-
|
|
28041
|
-
|
|
28042
|
-
|
|
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
|
-
|
|
28045
|
-
|
|
28046
|
-
|
|
28047
|
-
|
|
28048
|
-
|
|
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
|
|
28062
|
-
const
|
|
28063
|
-
|
|
28064
|
-
|
|
28065
|
-
|
|
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
|
-
|
|
28068
|
-
|
|
28069
|
-
|
|
28070
|
-
|
|
28071
|
-
|
|
28072
|
-
|
|
28073
|
-
|
|
28074
|
-
|
|
28075
|
-
|
|
28076
|
-
|
|
28077
|
-
|
|
28078
|
-
|
|
28079
|
-
|
|
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
|
-
|
|
28088
|
-
|
|
28089
|
-
|
|
28090
|
-
|
|
28091
|
-
|
|
28092
|
-
|
|
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
|
|
28128
|
-
|
|
28129
|
-
|
|
28130
|
-
|
|
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
|
|
28133
|
-
|
|
28134
|
-
|
|
28135
|
-
|
|
28136
|
-
|
|
28137
|
-
|
|
28138
|
-
|
|
28139
|
-
|
|
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
|
|
28142
|
-
|
|
28143
|
-
|
|
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
|
-
|
|
28149
|
-
|
|
28150
|
-
|
|
28151
|
-
|
|
28152
|
-
|
|
28153
|
-
|
|
28154
|
-
|
|
28155
|
-
|
|
28156
|
-
|
|
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
|
-
|
|
28163
|
-
|
|
28164
|
-
|
|
28165
|
-
|
|
28166
|
-
|
|
28167
|
-
|
|
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
|
|
28185
|
-
|
|
28186
|
-
|
|
28187
|
-
|
|
28188
|
-
|
|
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
|
-
|
|
28192
|
-
const
|
|
28193
|
-
|
|
28194
|
-
|
|
28195
|
-
|
|
28196
|
-
|
|
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
|
-
|
|
28199
|
-
|
|
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
|
-
|
|
28202
|
-
|
|
28203
|
-
|
|
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
|
|
28206
|
-
|
|
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
|
|
28209
|
-
|
|
28210
|
-
|
|
28211
|
-
|
|
28212
|
-
|
|
28213
|
-
|
|
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
|
|
28217
|
-
const
|
|
28218
|
-
|
|
28219
|
-
|
|
28220
|
-
|
|
28221
|
-
|
|
28222
|
-
|
|
28223
|
-
|
|
28224
|
-
|
|
28225
|
-
|
|
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
|
-
|
|
28228
|
-
|
|
28561
|
+
return client.post(
|
|
28562
|
+
`/projects/${encodeURIComponent(input.project)}/snapshot`,
|
|
28563
|
+
{ snapshot: input.snapshot }
|
|
28564
|
+
);
|
|
28565
|
+
}
|
|
28229
28566
|
|
|
28230
|
-
|
|
28231
|
-
|
|
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
|
-
|
|
28234
|
-
|
|
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
|
|
28237
|
-
|
|
28238
|
-
|
|
28239
|
-
|
|
28240
|
-
|
|
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
|
-
|
|
28243
|
-
|
|
28244
|
-
|
|
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
|
-
|
|
28247
|
-
|
|
28248
|
-
|
|
28249
|
-
|
|
28250
|
-
|
|
28251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28260
|
-
|
|
28261
|
-
|
|
28262
|
-
|
|
28263
|
-
|
|
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
|
-
|
|
28267
|
-
|
|
28268
|
-
|
|
28269
|
-
|
|
28270
|
-
|
|
28271
|
-
|
|
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
|
-
|
|
28289
|
-
|
|
28290
|
-
|
|
28291
|
-
|
|
28292
|
-
|
|
28293
|
-
|
|
28294
|
-
|
|
28295
|
-
|
|
28296
|
-
|
|
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
|
|
28331
|
-
const
|
|
28332
|
-
|
|
28333
|
-
|
|
28334
|
-
|
|
28335
|
-
|
|
28336
|
-
|
|
28337
|
-
|
|
28338
|
-
|
|
28339
|
-
|
|
28340
|
-
|
|
28341
|
-
|
|
28342
|
-
|
|
28343
|
-
|
|
28344
|
-
|
|
28345
|
-
|
|
28346
|
-
|
|
28347
|
-
|
|
28348
|
-
|
|
28349
|
-
|
|
28350
|
-
|
|
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
|
-
|
|
28354
|
-
|
|
28355
|
-
|
|
28356
|
-
|
|
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/
|
|
28709
|
+
// src/hooks-cli.ts
|
|
28363
28710
|
init_cjs_shims();
|
|
28364
|
-
var
|
|
28365
|
-
var
|
|
28366
|
-
var
|
|
28367
|
-
var
|
|
28368
|
-
var
|
|
28369
|
-
var
|
|
28370
|
-
|
|
28371
|
-
|
|
28372
|
-
|
|
28373
|
-
|
|
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
|
|
28389
|
-
const
|
|
28390
|
-
|
|
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
|
|
28393
|
-
const
|
|
28394
|
-
|
|
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
|
-
|
|
28397
|
-
|
|
28398
|
-
|
|
28399
|
-
|
|
28400
|
-
|
|
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
|
|
28523
|
-
return
|
|
28750
|
+
function installedHookPath() {
|
|
28751
|
+
return import_node_path88.default.join(neatHome3(), "hooks", HOOK_FILENAME);
|
|
28524
28752
|
}
|
|
28525
|
-
function
|
|
28526
|
-
return
|
|
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
|
|
28532
|
-
|
|
28533
|
-
|
|
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
|
-
|
|
28542
|
-
|
|
28543
|
-
|
|
28544
|
-
|
|
28545
|
-
|
|
28546
|
-
|
|
28547
|
-
|
|
28548
|
-
|
|
28549
|
-
|
|
28550
|
-
|
|
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 (
|
|
28556
|
-
|
|
28557
|
-
|
|
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
|
-
|
|
28570
|
-
|
|
28571
|
-
|
|
28572
|
-
|
|
28573
|
-
|
|
28574
|
-
|
|
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
|
-
|
|
28581
|
-
|
|
28582
|
-
|
|
28583
|
-
|
|
28584
|
-
|
|
28585
|
-
|
|
28586
|
-
|
|
28587
|
-
|
|
28588
|
-
|
|
28589
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
28604
|
-
|
|
28605
|
-
|
|
28606
|
-
|
|
28607
|
-
|
|
28608
|
-
|
|
28609
|
-
|
|
28610
|
-
|
|
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
|
-
|
|
28626
|
-
|
|
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
|
-
|
|
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
|
|
28645
|
-
|
|
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("
|
|
28658
|
-
console.log(
|
|
28659
|
-
|
|
28660
|
-
|
|
28661
|
-
|
|
28662
|
-
|
|
28663
|
-
|
|
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(
|
|
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
|
|
28669
|
-
const
|
|
28670
|
-
|
|
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
|
-
|
|
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
|
-
|
|
28894
|
+
usage();
|
|
28679
28895
|
return 0;
|
|
28680
28896
|
default:
|
|
28681
|
-
console.error(`neat
|
|
28682
|
-
|
|
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
|
|
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/
|
|
28911
|
+
// src/claude-cli.ts
|
|
28696
28912
|
init_cjs_shims();
|
|
28697
|
-
var
|
|
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
|
-
|
|
28700
|
-
|
|
28701
|
-
|
|
28702
|
-
|
|
28703
|
-
|
|
28704
|
-
|
|
28705
|
-
|
|
28706
|
-
|
|
28707
|
-
|
|
28708
|
-
|
|
28709
|
-
|
|
28710
|
-
|
|
28711
|
-
|
|
28712
|
-
|
|
28713
|
-
|
|
28714
|
-
|
|
28715
|
-
|
|
28716
|
-
|
|
28717
|
-
|
|
28718
|
-
|
|
28719
|
-
|
|
28720
|
-
return
|
|
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
|
|
28723
|
-
const
|
|
28724
|
-
|
|
28725
|
-
return
|
|
28726
|
-
|
|
28727
|
-
|
|
28728
|
-
|
|
28729
|
-
|
|
28730
|
-
|
|
28731
|
-
|
|
28732
|
-
|
|
28733
|
-
|
|
28734
|
-
|
|
28735
|
-
|
|
28736
|
-
|
|
28737
|
-
|
|
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
|
|
28773
|
-
|
|
28774
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
28808
|
-
const
|
|
28809
|
-
const
|
|
28810
|
-
|
|
28811
|
-
|
|
28812
|
-
);
|
|
28813
|
-
|
|
28814
|
-
|
|
28815
|
-
|
|
28816
|
-
|
|
28817
|
-
|
|
28818
|
-
|
|
28819
|
-
|
|
28820
|
-
|
|
28821
|
-
|
|
28822
|
-
|
|
28823
|
-
|
|
28824
|
-
|
|
28825
|
-
|
|
28826
|
-
|
|
28827
|
-
|
|
28828
|
-
|
|
28829
|
-
|
|
28830
|
-
|
|
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
|
|
28843
|
-
|
|
28844
|
-
|
|
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
|
|
28847
|
-
const
|
|
28848
|
-
|
|
28849
|
-
|
|
28850
|
-
|
|
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
|
-
|
|
28854
|
-
|
|
28855
|
-
|
|
28856
|
-
|
|
28857
|
-
|
|
28858
|
-
|
|
28859
|
-
|
|
28860
|
-
|
|
28861
|
-
|
|
28862
|
-
|
|
28863
|
-
|
|
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
|
-
|
|
28879
|
-
|
|
28880
|
-
}
|
|
28881
|
-
throw err;
|
|
29046
|
+
console.error(`neat claude: ${err.message}`);
|
|
29047
|
+
return 1;
|
|
28882
29048
|
}
|
|
28883
29049
|
}
|
|
28884
|
-
|
|
28885
|
-
|
|
28886
|
-
|
|
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
|
-
|
|
28889
|
-
|
|
28890
|
-
|
|
28891
|
-
|
|
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
|
|
28920
|
-
|
|
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
|
|
28935
|
-
|
|
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
|
-
|
|
28945
|
-
|
|
28946
|
-
|
|
28947
|
-
|
|
28948
|
-
|
|
28949
|
-
|
|
28950
|
-
|
|
28951
|
-
|
|
28952
|
-
|
|
28953
|
-
|
|
28954
|
-
|
|
28955
|
-
|
|
28956
|
-
|
|
28957
|
-
|
|
28958
|
-
|
|
28959
|
-
|
|
28960
|
-
|
|
28961
|
-
|
|
28962
|
-
|
|
28963
|
-
|
|
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
|
-
|
|
28967
|
-
if (
|
|
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
|
-
|
|
28974
|
-
const
|
|
28975
|
-
|
|
28976
|
-
);
|
|
28977
|
-
if (
|
|
28978
|
-
|
|
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
|
-
|
|
28981
|
-
|
|
28982
|
-
|
|
28983
|
-
|
|
28984
|
-
|
|
28985
|
-
|
|
28986
|
-
|
|
28987
|
-
|
|
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
|
-
|
|
28992
|
-
|
|
28993
|
-
|
|
28994
|
-
|
|
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
|
-
|
|
28998
|
-
|
|
28999
|
-
|
|
29000
|
-
|
|
29001
|
-
|
|
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
|
|
29012
|
-
|
|
29013
|
-
|
|
29014
|
-
|
|
29015
|
-
|
|
29016
|
-
|
|
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
|
-
|
|
29024
|
-
|
|
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
|
-
|
|
29031
|
-
|
|
29032
|
-
|
|
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
|
-
|
|
29041
|
-
|
|
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
|
|
29172
|
+
return true;
|
|
29052
29173
|
}
|
|
29053
|
-
|
|
29054
|
-
|
|
29055
|
-
|
|
29056
|
-
|
|
29057
|
-
|
|
29058
|
-
|
|
29059
|
-
|
|
29060
|
-
);
|
|
29061
|
-
|
|
29062
|
-
|
|
29063
|
-
|
|
29064
|
-
|
|
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
|
|
29068
|
-
|
|
29069
|
-
|
|
29070
|
-
|
|
29071
|
-
|
|
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
|
|
29077
|
-
|
|
29078
|
-
|
|
29079
|
-
|
|
29080
|
-
if (
|
|
29081
|
-
|
|
29082
|
-
|
|
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
|
-
|
|
29085
|
-
|
|
29086
|
-
|
|
29087
|
-
);
|
|
29088
|
-
|
|
29089
|
-
|
|
29090
|
-
|
|
29091
|
-
|
|
29092
|
-
|
|
29093
|
-
|
|
29094
|
-
|
|
29095
|
-
|
|
29096
|
-
|
|
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
|
-
|
|
29099
|
-
|
|
29237
|
+
console.error("neat codex: fix the file and re-run; nothing was written.");
|
|
29238
|
+
return { exitCode: 1 };
|
|
29100
29239
|
}
|
|
29101
|
-
|
|
29102
|
-
|
|
29103
|
-
|
|
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 (
|
|
29107
|
-
|
|
29108
|
-
|
|
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
|
-
|
|
29112
|
-
|
|
29113
|
-
|
|
29114
|
-
|
|
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
|
-
|
|
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
|
-
|
|
29123
|
-
|
|
29124
|
-
|
|
29125
|
-
|
|
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
|
|
29138
|
-
|
|
29139
|
-
|
|
29140
|
-
|
|
29141
|
-
|
|
29142
|
-
|
|
29143
|
-
|
|
29144
|
-
|
|
29145
|
-
|
|
29146
|
-
|
|
29147
|
-
|
|
29148
|
-
|
|
29149
|
-
|
|
29150
|
-
|
|
29151
|
-
|
|
29152
|
-
|
|
29153
|
-
|
|
29154
|
-
|
|
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
|
-
|
|
29159
|
-
|
|
29160
|
-
|
|
29161
|
-
|
|
29162
|
-
|
|
29163
|
-
|
|
29164
|
-
|
|
29165
|
-
|
|
29166
|
-
|
|
29167
|
-
|
|
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 (
|
|
29170
|
-
|
|
29171
|
-
|
|
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
|
-
|
|
29175
|
-
|
|
29176
|
-
|
|
29177
|
-
|
|
29178
|
-
|
|
29179
|
-
|
|
29180
|
-
|
|
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
|
-
|
|
29183
|
-
|
|
29184
|
-
|
|
29185
|
-
|
|
29186
|
-
|
|
29187
|
-
|
|
29188
|
-
|
|
29189
|
-
|
|
29190
|
-
|
|
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
|
|
29194
|
-
const
|
|
29195
|
-
|
|
29196
|
-
|
|
29197
|
-
const
|
|
29198
|
-
|
|
29199
|
-
|
|
29200
|
-
|
|
29201
|
-
|
|
29202
|
-
|
|
29203
|
-
|
|
29204
|
-
|
|
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
|
-
|
|
29207
|
-
|
|
29208
|
-
|
|
29209
|
-
|
|
29210
|
-
|
|
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
|
-
|
|
29214
|
-
|
|
29215
|
-
|
|
29216
|
-
|
|
29217
|
-
|
|
29218
|
-
}
|
|
29219
|
-
}
|
|
29220
|
-
|
|
29221
|
-
|
|
29222
|
-
|
|
29223
|
-
|
|
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
|
|
29226
|
-
|
|
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
|
|
29232
|
-
|
|
29233
|
-
|
|
29234
|
-
|
|
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
|
|
29244
|
-
|
|
29245
|
-
|
|
29246
|
-
|
|
29247
|
-
|
|
29248
|
-
|
|
29249
|
-
|
|
29250
|
-
|
|
29251
|
-
|
|
29252
|
-
|
|
29253
|
-
|
|
29254
|
-
|
|
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
|
-
|
|
29265
|
-
|
|
29266
|
-
|
|
29267
|
-
|
|
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,
|
|
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,
|
|
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 !==
|
|
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 ===
|
|
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
|
|
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}${
|
|
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
|
|
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
|
|
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 =
|
|
30586
|
+
const patchPath = import_node_path93.default.join(opts.scanPath, "neat.patch");
|
|
30185
30587
|
if (opts.dryRun) {
|
|
30186
|
-
await
|
|
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 =
|
|
30190
|
-
const gitignoreExists = await
|
|
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
|
-
|
|
30603
|
+
import_node_path93.default.join(opts.scanPath, "neat-out")
|
|
30202
30604
|
);
|
|
30203
|
-
const 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
30350
|
-
await
|
|
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 =
|
|
30848
|
+
const scanPath = import_node_path93.default.resolve(target);
|
|
30442
30849
|
const projectExplicit = parsed.project !== null;
|
|
30443
|
-
const projectName = projectExplicit ? project :
|
|
30850
|
+
const projectName = projectExplicit ? project : import_node_path93.default.basename(scanPath);
|
|
30444
30851
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
30445
|
-
const fallback = pathsForProject(projectKey,
|
|
30446
|
-
const outPath =
|
|
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 =
|
|
30468
|
-
const stat = await
|
|
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,
|
|
30474
|
-
const outPath =
|
|
30475
|
-
const errorsPath =
|
|
30476
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
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 =
|
|
30479
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
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 ?
|
|
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 ?
|
|
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 =
|
|
30673
|
-
const stat = await
|
|
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 :
|
|
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 =
|
|
31284
|
+
const r = import_types102.DivergenceTypeSchema.safeParse(p);
|
|
30878
31285
|
if (!r.success) {
|
|
30879
31286
|
console.error(
|
|
30880
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
31287
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types102.DivergenceTypeSchema.options.join(", ")}`
|
|
30881
31288
|
);
|
|
30882
31289
|
return 2;
|
|
30883
31290
|
}
|