@neat.is/core 0.2.6 → 0.2.8
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-T2U4U256.js → chunk-FTKDVKBC.js} +76 -13
- package/dist/chunk-FTKDVKBC.js.map +1 -0
- package/dist/{chunk-6SFEITLJ.js → chunk-GAYTAGEH.js} +298 -13
- package/dist/chunk-GAYTAGEH.js.map +1 -0
- package/dist/{chunk-6JT6L2OV.js → chunk-HWO746IM.js} +8 -10
- package/dist/{chunk-6JT6L2OV.js.map → chunk-HWO746IM.js.map} +1 -1
- package/dist/cli.cjs +1129 -249
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +22 -1
- package/dist/cli.d.ts +22 -1
- package/dist/cli.js +757 -39
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +303 -197
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +16 -18
- package/dist/neatd.cjs +32 -2
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -3
- package/dist/neatd.js.map +1 -1
- package/dist/server.cjs +190 -49
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +2 -2
- package/package.json +5 -3
- package/dist/chunk-6SFEITLJ.js.map +0 -1
- package/dist/chunk-T2U4U256.js.map +0 -1
- package/dist/chunk-WX55TLUT.js +0 -184
- package/dist/chunk-WX55TLUT.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,43 +1,43 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
ProjectNameCollisionError,
|
|
4
|
-
addProject,
|
|
5
|
-
listProjects,
|
|
6
|
-
removeProject,
|
|
7
|
-
setStatus
|
|
8
|
-
} from "./chunk-WX55TLUT.js";
|
|
9
2
|
import {
|
|
10
3
|
buildSearchIndex
|
|
11
4
|
} from "./chunk-XOOCA5T7.js";
|
|
12
5
|
import {
|
|
13
6
|
buildApi
|
|
14
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-FTKDVKBC.js";
|
|
15
8
|
import {
|
|
16
9
|
DEFAULT_PROJECT,
|
|
17
10
|
PolicyViolationsLog,
|
|
11
|
+
ProjectNameCollisionError,
|
|
18
12
|
Projects,
|
|
19
13
|
addCallEdges,
|
|
20
14
|
addConfigNodes,
|
|
21
15
|
addDatabasesAndCompat,
|
|
22
16
|
addInfra,
|
|
17
|
+
addProject,
|
|
23
18
|
addServiceAliases,
|
|
24
19
|
addServiceNodes,
|
|
20
|
+
attachGraphToEventBus,
|
|
25
21
|
discoverServices,
|
|
22
|
+
emitNeatEvent,
|
|
26
23
|
ensureCompatLoaded,
|
|
27
24
|
evaluateAllPolicies,
|
|
28
25
|
extractFromDirectory,
|
|
29
26
|
getGraph,
|
|
27
|
+
listProjects,
|
|
30
28
|
loadGraphFromDisk,
|
|
31
29
|
loadPolicyFile,
|
|
32
30
|
makeErrorSpanWriter,
|
|
33
31
|
makeSpanHandler,
|
|
34
32
|
pathsForProject,
|
|
35
33
|
promoteFrontierNodes,
|
|
34
|
+
removeProject,
|
|
36
35
|
resetGraph,
|
|
37
36
|
saveGraphToDisk,
|
|
37
|
+
setStatus,
|
|
38
38
|
startPersistLoop,
|
|
39
39
|
startStalenessLoop
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-GAYTAGEH.js";
|
|
41
41
|
import {
|
|
42
42
|
buildOtelReceiver,
|
|
43
43
|
startOtelGrpcReceiver
|
|
@@ -101,7 +101,8 @@ function classifyChange(relPath) {
|
|
|
101
101
|
}
|
|
102
102
|
return phases;
|
|
103
103
|
}
|
|
104
|
-
async function runExtractPhases(graph, scanPath, phases) {
|
|
104
|
+
async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJECT) {
|
|
105
|
+
void project;
|
|
105
106
|
const started = Date.now();
|
|
106
107
|
await ensureCompatLoaded();
|
|
107
108
|
const services = await discoverServices(scanPath);
|
|
@@ -157,7 +158,9 @@ function shouldIgnore(absPath) {
|
|
|
157
158
|
}
|
|
158
159
|
async function startWatch(graph, opts) {
|
|
159
160
|
const debounceMs = opts.debounceMs ?? 1e3;
|
|
161
|
+
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
160
162
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
163
|
+
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
161
164
|
const policyFilePath = path.join(opts.scanPath, "policy.json");
|
|
162
165
|
const policyViolationsPath = path.join(path.dirname(opts.outPath), "policy-violations.ndjson");
|
|
163
166
|
let policies = [];
|
|
@@ -169,7 +172,7 @@ async function startWatch(graph, opts) {
|
|
|
169
172
|
} catch (err) {
|
|
170
173
|
console.warn(`policies: failed to load ${policyFilePath} \u2014 ${err.message}`);
|
|
171
174
|
}
|
|
172
|
-
const policyLog = new PolicyViolationsLog(policyViolationsPath);
|
|
175
|
+
const policyLog = new PolicyViolationsLog(policyViolationsPath, projectName);
|
|
173
176
|
const onPolicyTrigger = async (g) => {
|
|
174
177
|
if (policies.length === 0) return;
|
|
175
178
|
try {
|
|
@@ -179,14 +182,30 @@ async function startWatch(graph, opts) {
|
|
|
179
182
|
console.warn(`policies: evaluation failed \u2014 ${err.message}`);
|
|
180
183
|
}
|
|
181
184
|
};
|
|
182
|
-
const initial = await runExtractPhases(
|
|
185
|
+
const initial = await runExtractPhases(
|
|
186
|
+
graph,
|
|
187
|
+
opts.scanPath,
|
|
188
|
+
new Set(ALL_PHASES),
|
|
189
|
+
projectName
|
|
190
|
+
);
|
|
183
191
|
console.log(
|
|
184
192
|
`extract: ${initial.nodesAdded} new nodes, ${initial.edgesAdded} new edges (graph total ${graph.order}/${graph.size})`
|
|
185
193
|
);
|
|
194
|
+
emitNeatEvent({
|
|
195
|
+
type: "extraction-complete",
|
|
196
|
+
project: projectName,
|
|
197
|
+
payload: {
|
|
198
|
+
project: projectName,
|
|
199
|
+
fileCount: 0,
|
|
200
|
+
nodesAdded: initial.nodesAdded,
|
|
201
|
+
edgesAdded: initial.edgesAdded
|
|
202
|
+
}
|
|
203
|
+
});
|
|
186
204
|
await onPolicyTrigger(graph);
|
|
187
205
|
const stopPersist = startPersistLoop(graph, opts.outPath);
|
|
188
206
|
const stopStaleness = startStalenessLoop(graph, {
|
|
189
207
|
staleEventsPath: opts.staleEventsPath,
|
|
208
|
+
project: projectName,
|
|
190
209
|
onPolicyTrigger
|
|
191
210
|
});
|
|
192
211
|
const host = opts.host ?? "0.0.0.0";
|
|
@@ -202,7 +221,6 @@ async function startWatch(graph, opts) {
|
|
|
202
221
|
`semantic_search: index build failed (${err.message}); falling back to inline substring`
|
|
203
222
|
);
|
|
204
223
|
}
|
|
205
|
-
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
206
224
|
const registry = new Projects();
|
|
207
225
|
registry.set(projectName, {
|
|
208
226
|
graph,
|
|
@@ -227,6 +245,7 @@ async function startWatch(graph, opts) {
|
|
|
227
245
|
const onSpan = makeSpanHandler({
|
|
228
246
|
graph,
|
|
229
247
|
errorsPath: opts.errorsPath,
|
|
248
|
+
project: projectName,
|
|
230
249
|
writeErrorEventInline: false,
|
|
231
250
|
onPolicyTrigger
|
|
232
251
|
});
|
|
@@ -240,6 +259,7 @@ async function startWatch(graph, opts) {
|
|
|
240
259
|
const onSpanGrpc = makeSpanHandler({
|
|
241
260
|
graph,
|
|
242
261
|
errorsPath: opts.errorsPath,
|
|
262
|
+
project: projectName,
|
|
243
263
|
onPolicyTrigger
|
|
244
264
|
});
|
|
245
265
|
const r = await startOtelGrpcReceiver({ onSpan: onSpanGrpc, host, port: grpcPort });
|
|
@@ -259,10 +279,20 @@ async function startWatch(graph, opts) {
|
|
|
259
279
|
try {
|
|
260
280
|
let retired = 0;
|
|
261
281
|
for (const p of paths) retired += retireEdgesByFile(graph, p);
|
|
262
|
-
const result = await runExtractPhases(graph, opts.scanPath, phases);
|
|
282
|
+
const result = await runExtractPhases(graph, opts.scanPath, phases, projectName);
|
|
263
283
|
console.log(
|
|
264
284
|
`[watch] re-extract phases=${result.phases.join(",")} retired=${retired} +${result.nodesAdded}n/+${result.edgesAdded}e in ${result.durationMs}ms`
|
|
265
285
|
);
|
|
286
|
+
emitNeatEvent({
|
|
287
|
+
type: "extraction-complete",
|
|
288
|
+
project: projectName,
|
|
289
|
+
payload: {
|
|
290
|
+
project: projectName,
|
|
291
|
+
fileCount: paths.size,
|
|
292
|
+
nodesAdded: result.nodesAdded,
|
|
293
|
+
edgesAdded: result.edgesAdded
|
|
294
|
+
}
|
|
295
|
+
});
|
|
266
296
|
if (searchIndex) {
|
|
267
297
|
try {
|
|
268
298
|
await searchIndex.refresh(graph);
|
|
@@ -321,6 +351,7 @@ async function startWatch(graph, opts) {
|
|
|
321
351
|
await watcher.close();
|
|
322
352
|
stopStaleness();
|
|
323
353
|
stopPersist();
|
|
354
|
+
detachEventBus();
|
|
324
355
|
await api.close();
|
|
325
356
|
await otelHttp.close();
|
|
326
357
|
if (grpcReceiver) await grpcReceiver.stop();
|
|
@@ -722,11 +753,456 @@ function renderPatch(sections) {
|
|
|
722
753
|
return lines.join("\n");
|
|
723
754
|
}
|
|
724
755
|
|
|
756
|
+
// src/cli-client.ts
|
|
757
|
+
import { Provenance as Provenance2 } from "@neat.is/types";
|
|
758
|
+
var HttpError = class extends Error {
|
|
759
|
+
constructor(status, message, responseBody = "") {
|
|
760
|
+
super(message);
|
|
761
|
+
this.status = status;
|
|
762
|
+
this.responseBody = responseBody;
|
|
763
|
+
this.name = "HttpError";
|
|
764
|
+
}
|
|
765
|
+
status;
|
|
766
|
+
responseBody;
|
|
767
|
+
};
|
|
768
|
+
var TransportError = class extends Error {
|
|
769
|
+
constructor(message) {
|
|
770
|
+
super(message);
|
|
771
|
+
this.name = "TransportError";
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
function createHttpClient(baseUrl) {
|
|
775
|
+
const root = baseUrl.replace(/\/$/, "");
|
|
776
|
+
return {
|
|
777
|
+
async get(path5) {
|
|
778
|
+
let res;
|
|
779
|
+
try {
|
|
780
|
+
res = await fetch(`${root}${path5}`);
|
|
781
|
+
} catch (err) {
|
|
782
|
+
throw new TransportError(
|
|
783
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
if (!res.ok) {
|
|
787
|
+
const body = await res.text().catch(() => "");
|
|
788
|
+
throw new HttpError(
|
|
789
|
+
res.status,
|
|
790
|
+
`${res.status} ${res.statusText} on GET ${path5}: ${body}`,
|
|
791
|
+
body
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
return await res.json();
|
|
795
|
+
},
|
|
796
|
+
async post(path5, body) {
|
|
797
|
+
let res;
|
|
798
|
+
try {
|
|
799
|
+
res = await fetch(`${root}${path5}`, {
|
|
800
|
+
method: "POST",
|
|
801
|
+
headers: { "content-type": "application/json" },
|
|
802
|
+
body: JSON.stringify(body)
|
|
803
|
+
});
|
|
804
|
+
} catch (err) {
|
|
805
|
+
throw new TransportError(
|
|
806
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
807
|
+
);
|
|
808
|
+
}
|
|
809
|
+
if (!res.ok) {
|
|
810
|
+
const text = await res.text().catch(() => "");
|
|
811
|
+
throw new HttpError(
|
|
812
|
+
res.status,
|
|
813
|
+
`${res.status} ${res.statusText} on POST ${path5}: ${text}`,
|
|
814
|
+
text
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
return await res.json();
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function projectPath(project, suffix) {
|
|
822
|
+
if (!project) return suffix;
|
|
823
|
+
return `/projects/${encodeURIComponent(project)}${suffix}`;
|
|
824
|
+
}
|
|
825
|
+
async function runRootCause(client, input) {
|
|
826
|
+
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
827
|
+
const path5 = projectPath(
|
|
828
|
+
input.project,
|
|
829
|
+
`/traverse/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
830
|
+
);
|
|
831
|
+
try {
|
|
832
|
+
const result = await client.get(path5);
|
|
833
|
+
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
834
|
+
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
835
|
+
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
836
|
+
const blockLines = [
|
|
837
|
+
`Traversal path: ${arrowPath}`,
|
|
838
|
+
`Edge provenances: ${provenances}`
|
|
839
|
+
];
|
|
840
|
+
if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
|
|
841
|
+
return {
|
|
842
|
+
summary,
|
|
843
|
+
block: blockLines.join("\n"),
|
|
844
|
+
confidence: result.confidence,
|
|
845
|
+
provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
|
|
846
|
+
};
|
|
847
|
+
} catch (err) {
|
|
848
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
849
|
+
return {
|
|
850
|
+
summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
throw err;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
async function runBlastRadius(client, input) {
|
|
857
|
+
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
858
|
+
const path5 = projectPath(
|
|
859
|
+
input.project,
|
|
860
|
+
`/traverse/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
861
|
+
);
|
|
862
|
+
try {
|
|
863
|
+
const result = await client.get(path5);
|
|
864
|
+
if (result.totalAffected === 0) {
|
|
865
|
+
return {
|
|
866
|
+
summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
const sorted = [...result.affectedNodes].sort(
|
|
870
|
+
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
871
|
+
);
|
|
872
|
+
const blockLines = sorted.map(formatBlastEntry);
|
|
873
|
+
const minConfidence = sorted.reduce(
|
|
874
|
+
(m, n) => Math.min(m, n.confidence),
|
|
875
|
+
Number.POSITIVE_INFINITY
|
|
876
|
+
);
|
|
877
|
+
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
878
|
+
return {
|
|
879
|
+
summary: `Blast radius for ${result.origin}: ${result.totalAffected} affected node${result.totalAffected === 1 ? "" : "s"} reachable downstream.`,
|
|
880
|
+
block: blockLines.join("\n"),
|
|
881
|
+
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
882
|
+
provenance: provenances.length ? provenances : void 0
|
|
883
|
+
};
|
|
884
|
+
} catch (err) {
|
|
885
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
886
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
887
|
+
}
|
|
888
|
+
throw err;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
function formatBlastEntry(n) {
|
|
892
|
+
const tag = n.edgeProvenance === Provenance2.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
893
|
+
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
894
|
+
}
|
|
895
|
+
async function runDependencies(client, input) {
|
|
896
|
+
const depth = input.depth ?? 3;
|
|
897
|
+
const path5 = projectPath(
|
|
898
|
+
input.project,
|
|
899
|
+
`/graph/node/${encodeURIComponent(input.nodeId)}/dependencies?depth=${depth}`
|
|
900
|
+
);
|
|
901
|
+
try {
|
|
902
|
+
const result = await client.get(path5);
|
|
903
|
+
if (result.total === 0) {
|
|
904
|
+
return {
|
|
905
|
+
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
const byDistance = /* @__PURE__ */ new Map();
|
|
909
|
+
for (const dep of result.dependencies) {
|
|
910
|
+
const ring = byDistance.get(dep.distance) ?? [];
|
|
911
|
+
ring.push(dep);
|
|
912
|
+
byDistance.set(dep.distance, ring);
|
|
913
|
+
}
|
|
914
|
+
const blockLines = [];
|
|
915
|
+
for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
|
|
916
|
+
const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
|
|
917
|
+
blockLines.push(`${label}:`);
|
|
918
|
+
for (const dep of byDistance.get(distance)) {
|
|
919
|
+
blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
|
|
923
|
+
const directCount = byDistance.get(1)?.length ?? 0;
|
|
924
|
+
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).`;
|
|
925
|
+
return { summary, block: blockLines.join("\n"), provenance: provenances };
|
|
926
|
+
} catch (err) {
|
|
927
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
928
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
929
|
+
}
|
|
930
|
+
throw err;
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
async function runObservedDependencies(client, input) {
|
|
934
|
+
try {
|
|
935
|
+
const edges = await client.get(
|
|
936
|
+
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
937
|
+
);
|
|
938
|
+
const observed = edges.outbound.filter((e) => e.provenance === Provenance2.OBSERVED);
|
|
939
|
+
if (observed.length === 0) {
|
|
940
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === Provenance2.EXTRACTED);
|
|
941
|
+
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
942
|
+
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
943
|
+
}
|
|
944
|
+
const blockLines = observed.map((e) => ` \u2022 ${e.target} \u2014 ${e.type}${edgeMeta(e)}`);
|
|
945
|
+
return {
|
|
946
|
+
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
947
|
+
block: blockLines.join("\n"),
|
|
948
|
+
provenance: Provenance2.OBSERVED
|
|
949
|
+
};
|
|
950
|
+
} catch (err) {
|
|
951
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
952
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
953
|
+
}
|
|
954
|
+
throw err;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
function edgeMeta(e) {
|
|
958
|
+
const bits = [];
|
|
959
|
+
if (e.signal) {
|
|
960
|
+
bits.push(`spans=${e.signal.spanCount}`);
|
|
961
|
+
if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
|
|
962
|
+
if (e.signal.lastObservedAgeMs !== void 0) {
|
|
963
|
+
bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
|
|
964
|
+
}
|
|
965
|
+
} else if (e.callCount !== void 0) {
|
|
966
|
+
bits.push(`callCount=${e.callCount}`);
|
|
967
|
+
}
|
|
968
|
+
if (e.lastObserved) bits.push(`lastObserved=${e.lastObserved}`);
|
|
969
|
+
if (e.confidence !== void 0) bits.push(`confidence=${e.confidence}`);
|
|
970
|
+
return bits.length ? ` [${bits.join(", ")}]` : "";
|
|
971
|
+
}
|
|
972
|
+
function formatDuration(ms) {
|
|
973
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
974
|
+
const s = Math.round(ms / 1e3);
|
|
975
|
+
if (s < 60) return `${s}s`;
|
|
976
|
+
const m = Math.round(s / 60);
|
|
977
|
+
if (m < 60) return `${m}m`;
|
|
978
|
+
const h = Math.round(m / 60);
|
|
979
|
+
if (h < 48) return `${h}h`;
|
|
980
|
+
return `${Math.round(h / 24)}d`;
|
|
981
|
+
}
|
|
982
|
+
async function runIncidents(client, input) {
|
|
983
|
+
const path5 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
984
|
+
try {
|
|
985
|
+
const events = await client.get(path5);
|
|
986
|
+
if (events.length === 0) {
|
|
987
|
+
return {
|
|
988
|
+
summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
const ordered = [...events].reverse().slice(0, input.limit ?? 20);
|
|
992
|
+
const blockLines = [];
|
|
993
|
+
for (const ev of ordered) {
|
|
994
|
+
blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
|
|
995
|
+
blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
|
|
996
|
+
}
|
|
997
|
+
const target = input.nodeId ?? "the project";
|
|
998
|
+
return {
|
|
999
|
+
summary: `${target} has ${events.length} recorded incident${events.length === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
1000
|
+
block: blockLines.join("\n"),
|
|
1001
|
+
provenance: Provenance2.OBSERVED
|
|
1002
|
+
};
|
|
1003
|
+
} catch (err) {
|
|
1004
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
1005
|
+
return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
|
|
1006
|
+
}
|
|
1007
|
+
throw err;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
async function runSearch(client, input) {
|
|
1011
|
+
const result = await client.get(
|
|
1012
|
+
projectPath(input.project, `/search?q=${encodeURIComponent(input.query)}`)
|
|
1013
|
+
);
|
|
1014
|
+
if (result.matches.length === 0) {
|
|
1015
|
+
return { summary: `No matches for "${input.query}".` };
|
|
1016
|
+
}
|
|
1017
|
+
const provider = result.provider ?? "substring";
|
|
1018
|
+
const blockLines = [];
|
|
1019
|
+
let topScore;
|
|
1020
|
+
for (const n of result.matches) {
|
|
1021
|
+
const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
|
|
1022
|
+
const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
|
|
1023
|
+
if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
|
|
1024
|
+
blockLines.push(
|
|
1025
|
+
` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
|
|
1026
|
+
);
|
|
1027
|
+
}
|
|
1028
|
+
return {
|
|
1029
|
+
summary: `Found ${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${input.query}" via ${provider} provider.`,
|
|
1030
|
+
block: blockLines.join("\n"),
|
|
1031
|
+
confidence: topScore
|
|
1032
|
+
};
|
|
1033
|
+
}
|
|
1034
|
+
async function runDiff(client, input) {
|
|
1035
|
+
const result = await client.get(
|
|
1036
|
+
projectPath(
|
|
1037
|
+
input.project,
|
|
1038
|
+
`/graph/diff?against=${encodeURIComponent(input.againstSnapshot)}`
|
|
1039
|
+
)
|
|
1040
|
+
);
|
|
1041
|
+
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;
|
|
1042
|
+
const baseLabel = result.base.exportedAt ?? "unknown";
|
|
1043
|
+
if (total === 0) {
|
|
1044
|
+
return {
|
|
1045
|
+
summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
const blockLines = [
|
|
1049
|
+
` base exportedAt: ${baseLabel}`,
|
|
1050
|
+
` current exportedAt: ${result.current.exportedAt}`,
|
|
1051
|
+
""
|
|
1052
|
+
];
|
|
1053
|
+
if (result.added.nodes.length || result.added.edges.length) {
|
|
1054
|
+
blockLines.push("Added:");
|
|
1055
|
+
for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
|
|
1056
|
+
for (const e of result.added.edges)
|
|
1057
|
+
blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
1058
|
+
blockLines.push("");
|
|
1059
|
+
}
|
|
1060
|
+
if (result.removed.nodes.length || result.removed.edges.length) {
|
|
1061
|
+
blockLines.push("Removed:");
|
|
1062
|
+
for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
|
|
1063
|
+
for (const e of result.removed.edges)
|
|
1064
|
+
blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
1065
|
+
blockLines.push("");
|
|
1066
|
+
}
|
|
1067
|
+
if (result.changed.nodes.length || result.changed.edges.length) {
|
|
1068
|
+
blockLines.push("Changed:");
|
|
1069
|
+
for (const c of result.changed.nodes) {
|
|
1070
|
+
blockLines.push(` ~ node ${c.id} \u2014 ${summariseAttrDiff(c.before, c.after)}`);
|
|
1071
|
+
}
|
|
1072
|
+
for (const c of result.changed.edges) {
|
|
1073
|
+
const provBit = c.before.provenance !== c.after.provenance ? `provenance ${c.before.provenance} \u2192 ${c.after.provenance}` : summariseAttrDiff(c.before, c.after);
|
|
1074
|
+
blockLines.push(` ~ edge ${c.id} \u2014 ${provBit}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
return {
|
|
1078
|
+
summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
|
|
1079
|
+
block: blockLines.join("\n").trimEnd()
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
1082
|
+
function summariseAttrDiff(before, after) {
|
|
1083
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
1084
|
+
const changed = [];
|
|
1085
|
+
for (const k of keys) {
|
|
1086
|
+
if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.push(k);
|
|
1087
|
+
}
|
|
1088
|
+
return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
|
|
1089
|
+
}
|
|
1090
|
+
async function runStaleEdges(client, input) {
|
|
1091
|
+
const params = new URLSearchParams();
|
|
1092
|
+
if (input.limit !== void 0) params.set("limit", String(input.limit));
|
|
1093
|
+
if (input.edgeType) params.set("edgeType", input.edgeType);
|
|
1094
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
1095
|
+
const events = await client.get(
|
|
1096
|
+
projectPath(input.project, `/incidents/stale${qs}`)
|
|
1097
|
+
);
|
|
1098
|
+
if (events.length === 0) {
|
|
1099
|
+
return {
|
|
1100
|
+
summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
const blockLines = events.map(
|
|
1104
|
+
(e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
|
|
1105
|
+
);
|
|
1106
|
+
return {
|
|
1107
|
+
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
1108
|
+
block: blockLines.join("\n"),
|
|
1109
|
+
provenance: Provenance2.STALE
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
async function runPolicies(client, input) {
|
|
1113
|
+
let violations;
|
|
1114
|
+
let allowed = true;
|
|
1115
|
+
let hypothetical;
|
|
1116
|
+
if (input.hypotheticalAction) {
|
|
1117
|
+
if (typeof client.post !== "function") {
|
|
1118
|
+
throw new Error("HttpClient does not support POST \u2014 required for policies dry-run");
|
|
1119
|
+
}
|
|
1120
|
+
const body = await client.post(
|
|
1121
|
+
projectPath(input.project, "/policies/check"),
|
|
1122
|
+
{ hypotheticalAction: input.hypotheticalAction }
|
|
1123
|
+
);
|
|
1124
|
+
violations = body.violations;
|
|
1125
|
+
allowed = body.allowed;
|
|
1126
|
+
hypothetical = body.hypotheticalAction;
|
|
1127
|
+
} else {
|
|
1128
|
+
const params = new URLSearchParams();
|
|
1129
|
+
if (input.policyId) params.set("policyId", input.policyId);
|
|
1130
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
1131
|
+
violations = await client.get(
|
|
1132
|
+
projectPath(input.project, `/policies/violations${qs}`)
|
|
1133
|
+
);
|
|
1134
|
+
allowed = violations.every((v) => v.onViolation !== "block");
|
|
1135
|
+
}
|
|
1136
|
+
if (input.nodeId) {
|
|
1137
|
+
violations = violations.filter(
|
|
1138
|
+
(v) => v.subject.nodeId === input.nodeId || v.subject.path?.includes(input.nodeId)
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
if (violations.length === 0) {
|
|
1142
|
+
return {
|
|
1143
|
+
summary: hypothetical ? `No violations would result from the hypothetical action (${hypothetical.kind}).` : "No policy violations recorded."
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
const blockCount = violations.filter((v) => v.onViolation === "block").length;
|
|
1147
|
+
const summaryParts = [];
|
|
1148
|
+
if (hypothetical) {
|
|
1149
|
+
summaryParts.push(
|
|
1150
|
+
`Hypothetical ${hypothetical.kind} would surface ${violations.length} violation${violations.length === 1 ? "" : "s"}`
|
|
1151
|
+
);
|
|
1152
|
+
} else {
|
|
1153
|
+
summaryParts.push(
|
|
1154
|
+
`${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
if (blockCount > 0) summaryParts.push(`${blockCount} of which block`);
|
|
1158
|
+
if (!allowed && hypothetical) summaryParts.push("action denied");
|
|
1159
|
+
const summary = summaryParts.join("; ") + ".";
|
|
1160
|
+
const blockLines = violations.map((v) => {
|
|
1161
|
+
const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
|
|
1162
|
+
return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
|
|
1163
|
+
});
|
|
1164
|
+
const severities = [...new Set(violations.map((v) => v.severity))];
|
|
1165
|
+
return {
|
|
1166
|
+
summary,
|
|
1167
|
+
block: blockLines.join("\n"),
|
|
1168
|
+
confidence: hypothetical ? 0.7 : 1,
|
|
1169
|
+
provenance: severities.join(" ")
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
function formatFooter(confidence, provenance) {
|
|
1173
|
+
const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
|
|
1174
|
+
const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
|
|
1175
|
+
return `confidence: ${c} \xB7 provenance: ${p}`;
|
|
1176
|
+
}
|
|
1177
|
+
function formatHuman(result) {
|
|
1178
|
+
const sections = [result.summary.trim()];
|
|
1179
|
+
if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
|
|
1180
|
+
sections.push(formatFooter(result.confidence, result.provenance));
|
|
1181
|
+
return sections.join("\n\n");
|
|
1182
|
+
}
|
|
1183
|
+
function formatJson(result) {
|
|
1184
|
+
return JSON.stringify(
|
|
1185
|
+
{
|
|
1186
|
+
summary: result.summary,
|
|
1187
|
+
block: result.block ?? "",
|
|
1188
|
+
confidence: result.confidence ?? null,
|
|
1189
|
+
provenance: result.provenance ?? null
|
|
1190
|
+
},
|
|
1191
|
+
null,
|
|
1192
|
+
2
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
function exitCodeForError(err) {
|
|
1196
|
+
if (err instanceof TransportError) return 3;
|
|
1197
|
+
if (err instanceof HttpError) return 1;
|
|
1198
|
+
return 1;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
725
1201
|
// src/cli.ts
|
|
726
1202
|
function usage() {
|
|
727
1203
|
console.log("usage: neat <command> [args] [--project <name>]");
|
|
728
1204
|
console.log("");
|
|
729
|
-
console.log("commands:");
|
|
1205
|
+
console.log("lifecycle commands:");
|
|
730
1206
|
console.log(" init <path> One-time install: discover, extract, register, plan SDK install.");
|
|
731
1207
|
console.log(" Snapshot lands in <path>/neat-out/graph.json by default");
|
|
732
1208
|
console.log(" (or <path>/neat-out/<project>.json for non-default).");
|
|
@@ -748,51 +1224,133 @@ function usage() {
|
|
|
748
1224
|
console.log(" --print-config print the JSON snippet to stdout");
|
|
749
1225
|
console.log(" --apply merge mcpServers.neat into ~/.claude.json");
|
|
750
1226
|
console.log("");
|
|
1227
|
+
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
1228
|
+
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
1229
|
+
console.log(" example: neat root-cause service:<name>");
|
|
1230
|
+
console.log(" blast-radius <node-id> BFS outbound \u2014 what would break if this dies.");
|
|
1231
|
+
console.log(" example: neat blast-radius database:<host>");
|
|
1232
|
+
console.log(" dependencies <node-id> Transitive outbound dependencies.");
|
|
1233
|
+
console.log(" Flags: --depth N (default 3, max 10)");
|
|
1234
|
+
console.log(" example: neat dependencies service:<name> --depth 2");
|
|
1235
|
+
console.log(" observed-dependencies <node-id> OBSERVED-only outbound edges (runtime traffic).");
|
|
1236
|
+
console.log(" example: neat observed-dependencies service:<name>");
|
|
1237
|
+
console.log(" incidents [<node-id>] Recent error events; per-node when an id is given.");
|
|
1238
|
+
console.log(" Flags: --limit N (default 20)");
|
|
1239
|
+
console.log(" example: neat incidents service:<name> --limit 5");
|
|
1240
|
+
console.log(" search <query> Semantic (or substring) match on node names/ids.");
|
|
1241
|
+
console.log(' example: neat search "checkout"');
|
|
1242
|
+
console.log(" diff --against <snapshot> Compare the live graph to a saved snapshot.");
|
|
1243
|
+
console.log(" example: neat diff --against ./snapshots/baseline.json");
|
|
1244
|
+
console.log(" stale-edges Recent OBSERVED \u2192 STALE transitions.");
|
|
1245
|
+
console.log(" Flags: --limit N, --edge-type CALLS|CONNECTS_TO|...");
|
|
1246
|
+
console.log(" example: neat stale-edges --edge-type CALLS");
|
|
1247
|
+
console.log(" policies Current policy violations.");
|
|
1248
|
+
console.log(" Flags: --node <id>, --hypothetical-action <json>");
|
|
1249
|
+
console.log(" example: neat policies --node service:<name> --json");
|
|
1250
|
+
console.log("");
|
|
751
1251
|
console.log("flags:");
|
|
752
1252
|
console.log(' --project <name> Name the project this command targets. Default: "default".');
|
|
1253
|
+
console.log(" --json Emit machine-readable JSON instead of human text. Query verbs only.");
|
|
1254
|
+
console.log("");
|
|
1255
|
+
console.log("exit codes:");
|
|
1256
|
+
console.log(" 0 success");
|
|
1257
|
+
console.log(" 1 server error (4xx/5xx body printed to stderr)");
|
|
1258
|
+
console.log(" 2 misuse (missing args, bad flags) \u2014 handled before any network call");
|
|
1259
|
+
console.log(" 3 daemon not reachable (connection refused / timeout)");
|
|
1260
|
+
console.log("");
|
|
1261
|
+
console.log("environment:");
|
|
1262
|
+
console.log(" NEAT_API_URL base URL for the core REST API (default http://localhost:8080)");
|
|
1263
|
+
console.log(" NEAT_PROJECT project name when --project isn't passed");
|
|
753
1264
|
}
|
|
1265
|
+
var STRING_FLAGS = [
|
|
1266
|
+
["--project", "project"],
|
|
1267
|
+
["--depth", "depth"],
|
|
1268
|
+
["--limit", "limit"],
|
|
1269
|
+
["--edge-type", "edgeType"],
|
|
1270
|
+
["--node", "node"],
|
|
1271
|
+
["--since", "since"],
|
|
1272
|
+
["--against", "against"],
|
|
1273
|
+
["--error-id", "errorId"],
|
|
1274
|
+
["--hypothetical-action", "hypotheticalAction"]
|
|
1275
|
+
];
|
|
754
1276
|
function parseArgs(rest) {
|
|
755
1277
|
const positional = [];
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
1278
|
+
const out = {
|
|
1279
|
+
project: null,
|
|
1280
|
+
apply: false,
|
|
1281
|
+
dryRun: false,
|
|
1282
|
+
noInstall: false,
|
|
1283
|
+
printConfig: false,
|
|
1284
|
+
json: false,
|
|
1285
|
+
depth: null,
|
|
1286
|
+
limit: null,
|
|
1287
|
+
edgeType: null,
|
|
1288
|
+
node: null,
|
|
1289
|
+
since: null,
|
|
1290
|
+
against: null,
|
|
1291
|
+
errorId: null,
|
|
1292
|
+
hypotheticalAction: null,
|
|
1293
|
+
positional: []
|
|
1294
|
+
};
|
|
761
1295
|
for (let i = 0; i < rest.length; i++) {
|
|
762
1296
|
const arg = rest[i];
|
|
763
|
-
if (arg === "--project") {
|
|
764
|
-
const next = rest[i + 1];
|
|
765
|
-
if (!next) {
|
|
766
|
-
console.error("neat: --project requires a value");
|
|
767
|
-
process.exit(2);
|
|
768
|
-
}
|
|
769
|
-
project = next;
|
|
770
|
-
i++;
|
|
771
|
-
continue;
|
|
772
|
-
}
|
|
773
|
-
if (arg.startsWith("--project=")) {
|
|
774
|
-
project = arg.slice("--project=".length);
|
|
775
|
-
continue;
|
|
776
|
-
}
|
|
777
1297
|
if (arg === "--apply") {
|
|
778
|
-
|
|
1298
|
+
out.apply = true;
|
|
779
1299
|
continue;
|
|
780
1300
|
}
|
|
781
1301
|
if (arg === "--dry-run") {
|
|
782
|
-
dryRun = true;
|
|
1302
|
+
out.dryRun = true;
|
|
783
1303
|
continue;
|
|
784
1304
|
}
|
|
785
1305
|
if (arg === "--no-install") {
|
|
786
|
-
noInstall = true;
|
|
1306
|
+
out.noInstall = true;
|
|
787
1307
|
continue;
|
|
788
1308
|
}
|
|
789
1309
|
if (arg === "--print-config") {
|
|
790
|
-
printConfig = true;
|
|
1310
|
+
out.printConfig = true;
|
|
791
1311
|
continue;
|
|
792
1312
|
}
|
|
1313
|
+
if (arg === "--json") {
|
|
1314
|
+
out.json = true;
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
let matched = false;
|
|
1318
|
+
for (const [flag, field] of STRING_FLAGS) {
|
|
1319
|
+
if (arg === flag) {
|
|
1320
|
+
const next = rest[i + 1];
|
|
1321
|
+
if (next === void 0) {
|
|
1322
|
+
console.error(`neat: ${flag} requires a value`);
|
|
1323
|
+
process.exit(2);
|
|
1324
|
+
}
|
|
1325
|
+
assignFlag(out, field, next);
|
|
1326
|
+
i++;
|
|
1327
|
+
matched = true;
|
|
1328
|
+
break;
|
|
1329
|
+
}
|
|
1330
|
+
if (arg.startsWith(`${flag}=`)) {
|
|
1331
|
+
assignFlag(out, field, arg.slice(flag.length + 1));
|
|
1332
|
+
matched = true;
|
|
1333
|
+
break;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
if (matched) continue;
|
|
793
1337
|
positional.push(arg);
|
|
794
1338
|
}
|
|
795
|
-
|
|
1339
|
+
out.positional = positional;
|
|
1340
|
+
return out;
|
|
1341
|
+
}
|
|
1342
|
+
function assignFlag(out, field, value) {
|
|
1343
|
+
if (field === "depth" || field === "limit") {
|
|
1344
|
+
const n = Number(value);
|
|
1345
|
+
if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
|
|
1346
|
+
console.error(`neat: --${field === "depth" ? "depth" : "limit"} must be a positive integer`);
|
|
1347
|
+
process.exit(2);
|
|
1348
|
+
}
|
|
1349
|
+
out[field] = n;
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
;
|
|
1353
|
+
out[field] = value;
|
|
796
1354
|
}
|
|
797
1355
|
function summarise(nodes, edges) {
|
|
798
1356
|
const byNode = /* @__PURE__ */ new Map();
|
|
@@ -1156,10 +1714,167 @@ async function main() {
|
|
|
1156
1714
|
console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
|
|
1157
1715
|
return;
|
|
1158
1716
|
}
|
|
1717
|
+
if (QUERY_VERBS.has(cmd)) {
|
|
1718
|
+
const code = await runQueryVerb(cmd, parsed);
|
|
1719
|
+
if (code !== 0) process.exit(code);
|
|
1720
|
+
return;
|
|
1721
|
+
}
|
|
1159
1722
|
console.error(`neat: unknown command "${cmd}"`);
|
|
1160
1723
|
usage();
|
|
1161
1724
|
process.exit(1);
|
|
1162
1725
|
}
|
|
1726
|
+
var QUERY_VERBS = /* @__PURE__ */ new Set([
|
|
1727
|
+
"root-cause",
|
|
1728
|
+
"blast-radius",
|
|
1729
|
+
"dependencies",
|
|
1730
|
+
"observed-dependencies",
|
|
1731
|
+
"incidents",
|
|
1732
|
+
"search",
|
|
1733
|
+
"diff",
|
|
1734
|
+
"stale-edges",
|
|
1735
|
+
"policies"
|
|
1736
|
+
]);
|
|
1737
|
+
function resolveProjectFlag(parsed) {
|
|
1738
|
+
if (parsed.project) return parsed.project;
|
|
1739
|
+
const env = process.env.NEAT_PROJECT;
|
|
1740
|
+
if (env && env.length > 0 && env !== DEFAULT_PROJECT) return env;
|
|
1741
|
+
return void 0;
|
|
1742
|
+
}
|
|
1743
|
+
async function runQueryVerb(cmd, parsed) {
|
|
1744
|
+
const baseUrl = process.env.NEAT_API_URL ?? "http://localhost:8080";
|
|
1745
|
+
const client = createHttpClient(baseUrl);
|
|
1746
|
+
const project = resolveProjectFlag(parsed);
|
|
1747
|
+
const positional = parsed.positional;
|
|
1748
|
+
let work;
|
|
1749
|
+
switch (cmd) {
|
|
1750
|
+
case "root-cause": {
|
|
1751
|
+
const node = positional[0];
|
|
1752
|
+
if (!node) {
|
|
1753
|
+
console.error("neat root-cause: missing <node-id>");
|
|
1754
|
+
return 2;
|
|
1755
|
+
}
|
|
1756
|
+
work = runRootCause(client, {
|
|
1757
|
+
errorNode: node,
|
|
1758
|
+
...parsed.errorId ? { errorId: parsed.errorId } : {},
|
|
1759
|
+
...project ? { project } : {}
|
|
1760
|
+
});
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
case "blast-radius": {
|
|
1764
|
+
const node = positional[0];
|
|
1765
|
+
if (!node) {
|
|
1766
|
+
console.error("neat blast-radius: missing <node-id>");
|
|
1767
|
+
return 2;
|
|
1768
|
+
}
|
|
1769
|
+
work = runBlastRadius(client, {
|
|
1770
|
+
nodeId: node,
|
|
1771
|
+
...parsed.depth !== null ? { depth: parsed.depth } : {},
|
|
1772
|
+
...project ? { project } : {}
|
|
1773
|
+
});
|
|
1774
|
+
break;
|
|
1775
|
+
}
|
|
1776
|
+
case "dependencies": {
|
|
1777
|
+
const node = positional[0];
|
|
1778
|
+
if (!node) {
|
|
1779
|
+
console.error("neat dependencies: missing <node-id>");
|
|
1780
|
+
return 2;
|
|
1781
|
+
}
|
|
1782
|
+
work = runDependencies(client, {
|
|
1783
|
+
nodeId: node,
|
|
1784
|
+
...parsed.depth !== null ? { depth: parsed.depth } : {},
|
|
1785
|
+
...project ? { project } : {}
|
|
1786
|
+
});
|
|
1787
|
+
break;
|
|
1788
|
+
}
|
|
1789
|
+
case "observed-dependencies": {
|
|
1790
|
+
const node = positional[0];
|
|
1791
|
+
if (!node) {
|
|
1792
|
+
console.error("neat observed-dependencies: missing <node-id>");
|
|
1793
|
+
return 2;
|
|
1794
|
+
}
|
|
1795
|
+
work = runObservedDependencies(client, {
|
|
1796
|
+
nodeId: node,
|
|
1797
|
+
...project ? { project } : {}
|
|
1798
|
+
});
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
case "incidents": {
|
|
1802
|
+
work = runIncidents(client, {
|
|
1803
|
+
...positional[0] ? { nodeId: positional[0] } : {},
|
|
1804
|
+
...parsed.limit !== null ? { limit: parsed.limit } : {},
|
|
1805
|
+
...project ? { project } : {}
|
|
1806
|
+
});
|
|
1807
|
+
break;
|
|
1808
|
+
}
|
|
1809
|
+
case "search": {
|
|
1810
|
+
const q = positional.join(" ").trim();
|
|
1811
|
+
if (!q) {
|
|
1812
|
+
console.error("neat search: missing <query>");
|
|
1813
|
+
return 2;
|
|
1814
|
+
}
|
|
1815
|
+
work = runSearch(client, { query: q, ...project ? { project } : {} });
|
|
1816
|
+
break;
|
|
1817
|
+
}
|
|
1818
|
+
case "diff": {
|
|
1819
|
+
const against = parsed.against ?? parsed.since;
|
|
1820
|
+
if (!against) {
|
|
1821
|
+
console.error("neat diff: --against <snapshot-path> is required");
|
|
1822
|
+
return 2;
|
|
1823
|
+
}
|
|
1824
|
+
work = runDiff(client, {
|
|
1825
|
+
againstSnapshot: against,
|
|
1826
|
+
...project ? { project } : {}
|
|
1827
|
+
});
|
|
1828
|
+
break;
|
|
1829
|
+
}
|
|
1830
|
+
case "stale-edges": {
|
|
1831
|
+
work = runStaleEdges(client, {
|
|
1832
|
+
...parsed.limit !== null ? { limit: parsed.limit } : {},
|
|
1833
|
+
...parsed.edgeType ? { edgeType: parsed.edgeType } : {},
|
|
1834
|
+
...project ? { project } : {}
|
|
1835
|
+
});
|
|
1836
|
+
break;
|
|
1837
|
+
}
|
|
1838
|
+
case "policies": {
|
|
1839
|
+
let hypothetical;
|
|
1840
|
+
if (parsed.hypotheticalAction) {
|
|
1841
|
+
try {
|
|
1842
|
+
hypothetical = JSON.parse(parsed.hypotheticalAction);
|
|
1843
|
+
} catch (err) {
|
|
1844
|
+
console.error(
|
|
1845
|
+
`neat policies: --hypothetical-action must be valid JSON: ${err.message}`
|
|
1846
|
+
);
|
|
1847
|
+
return 2;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
work = runPolicies(client, {
|
|
1851
|
+
...parsed.node ? { nodeId: parsed.node } : {},
|
|
1852
|
+
...hypothetical ? { hypotheticalAction: hypothetical } : {},
|
|
1853
|
+
...project ? { project } : {}
|
|
1854
|
+
});
|
|
1855
|
+
break;
|
|
1856
|
+
}
|
|
1857
|
+
default:
|
|
1858
|
+
console.error(`neat: unknown query verb "${cmd}"`);
|
|
1859
|
+
return 2;
|
|
1860
|
+
}
|
|
1861
|
+
try {
|
|
1862
|
+
const result = await work;
|
|
1863
|
+
if (parsed.json) process.stdout.write(formatJson(result) + "\n");
|
|
1864
|
+
else process.stdout.write(formatHuman(result) + "\n");
|
|
1865
|
+
return 0;
|
|
1866
|
+
} catch (err) {
|
|
1867
|
+
if (err instanceof HttpError) {
|
|
1868
|
+
const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
|
|
1869
|
+
console.error(`neat ${cmd}: ${detail.trim()}`);
|
|
1870
|
+
} else if (err instanceof TransportError) {
|
|
1871
|
+
console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${process.env.NEAT_API_URL ?? "http://localhost:8080"})`);
|
|
1872
|
+
} else {
|
|
1873
|
+
console.error(`neat ${cmd}: ${err.message}`);
|
|
1874
|
+
}
|
|
1875
|
+
return exitCodeForError(err);
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1163
1878
|
var entry = process.argv[1] ?? "";
|
|
1164
1879
|
if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat")) {
|
|
1165
1880
|
main().catch((err) => {
|
|
@@ -1169,7 +1884,10 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
|
|
|
1169
1884
|
}
|
|
1170
1885
|
export {
|
|
1171
1886
|
CLAUDE_SKILL_CONFIG,
|
|
1887
|
+
QUERY_VERBS,
|
|
1888
|
+
parseArgs,
|
|
1172
1889
|
runInit,
|
|
1890
|
+
runQueryVerb,
|
|
1173
1891
|
runSkill
|
|
1174
1892
|
};
|
|
1175
1893
|
//# sourceMappingURL=cli.js.map
|