@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/cli.cjs CHANGED
@@ -101,8 +101,8 @@ function reshapeGrpcRequest(req) {
101
101
  };
102
102
  }
103
103
  function resolveProtoRoot() {
104
- const here = import_node_path29.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
105
- return import_node_path29.default.resolve(here, "..", "proto");
104
+ const here = import_node_path30.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
105
+ return import_node_path30.default.resolve(here, "..", "proto");
106
106
  }
107
107
  function loadTraceService() {
108
108
  const protoRoot = resolveProtoRoot();
@@ -157,13 +157,13 @@ async function startOtelGrpcReceiver(opts) {
157
157
  })
158
158
  };
159
159
  }
160
- var import_node_url, import_node_path29, grpc, protoLoader;
160
+ var import_node_url, import_node_path30, grpc, protoLoader;
161
161
  var init_otel_grpc = __esm({
162
162
  "src/otel-grpc.ts"() {
163
163
  "use strict";
164
164
  init_cjs_shims();
165
165
  import_node_url = require("url");
166
- import_node_path29 = __toESM(require("path"), 1);
166
+ import_node_path30 = __toESM(require("path"), 1);
167
167
  grpc = __toESM(require("@grpc/grpc-js"), 1);
168
168
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
169
169
  init_otel();
@@ -260,10 +260,10 @@ function parseOtlpRequest(body) {
260
260
  }
261
261
  function loadProtobufDecoder() {
262
262
  if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
263
- const here = import_node_path30.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
264
- const protoRoot = import_node_path30.default.resolve(here, "..", "proto");
263
+ const here = import_node_path31.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
264
+ const protoRoot = import_node_path31.default.resolve(here, "..", "proto");
265
265
  const root = new import_protobufjs.default.Root();
266
- root.resolvePath = (_origin, target) => import_node_path30.default.resolve(protoRoot, target);
266
+ root.resolvePath = (_origin, target) => import_node_path31.default.resolve(protoRoot, target);
267
267
  root.loadSync(
268
268
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
269
269
  { keepCase: true }
@@ -355,12 +355,12 @@ async function buildOtelReceiver(opts) {
355
355
  };
356
356
  return decorated;
357
357
  }
358
- var import_node_path30, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
358
+ var import_node_path31, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
359
359
  var init_otel = __esm({
360
360
  "src/otel.ts"() {
361
361
  "use strict";
362
362
  init_cjs_shims();
363
- import_node_path30 = __toESM(require("path"), 1);
363
+ import_node_path31 = __toESM(require("path"), 1);
364
364
  import_node_url2 = require("url");
365
365
  import_fastify2 = __toESM(require("fastify"), 1);
366
366
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -372,7 +372,10 @@ var init_otel = __esm({
372
372
  var cli_exports = {};
373
373
  __export(cli_exports, {
374
374
  CLAUDE_SKILL_CONFIG: () => CLAUDE_SKILL_CONFIG,
375
+ QUERY_VERBS: () => QUERY_VERBS,
376
+ parseArgs: () => parseArgs,
375
377
  runInit: () => runInit,
378
+ runQueryVerb: () => runQueryVerb,
376
379
  runSkill: () => runSkill
377
380
  });
378
381
  module.exports = __toCommonJS(cli_exports);
@@ -735,6 +738,68 @@ function deprecatedApis() {
735
738
  return currentMatrix().deprecatedApis ?? [];
736
739
  }
737
740
 
741
+ // src/events.ts
742
+ init_cjs_shims();
743
+ var import_node_events = require("events");
744
+ var EVENT_BUS_CHANNEL = "event";
745
+ var NeatEventBus = class extends import_node_events.EventEmitter {
746
+ };
747
+ var eventBus = new NeatEventBus();
748
+ eventBus.setMaxListeners(0);
749
+ function emitNeatEvent(envelope) {
750
+ eventBus.emit(EVENT_BUS_CHANNEL, envelope);
751
+ }
752
+ function attachGraphToEventBus(graph, opts) {
753
+ const { project } = opts;
754
+ const onNodeAdded = (payload) => {
755
+ emitNeatEvent({
756
+ type: "node-added",
757
+ project,
758
+ payload: { node: payload.attributes }
759
+ });
760
+ };
761
+ const onNodeDropped = (payload) => {
762
+ emitNeatEvent({
763
+ type: "node-removed",
764
+ project,
765
+ payload: { id: payload.key }
766
+ });
767
+ };
768
+ const onEdgeAdded = (payload) => {
769
+ emitNeatEvent({
770
+ type: "edge-added",
771
+ project,
772
+ payload: { edge: payload.attributes }
773
+ });
774
+ };
775
+ const onEdgeDropped = (payload) => {
776
+ emitNeatEvent({
777
+ type: "edge-removed",
778
+ project,
779
+ payload: { id: payload.key }
780
+ });
781
+ };
782
+ const onNodeAttrsUpdated = (payload) => {
783
+ emitNeatEvent({
784
+ type: "node-updated",
785
+ project,
786
+ payload: { id: payload.key, changes: payload.attributes }
787
+ });
788
+ };
789
+ graph.on("nodeAdded", onNodeAdded);
790
+ graph.on("nodeDropped", onNodeDropped);
791
+ graph.on("edgeAdded", onEdgeAdded);
792
+ graph.on("edgeDropped", onEdgeDropped);
793
+ graph.on("nodeAttributesUpdated", onNodeAttrsUpdated);
794
+ return () => {
795
+ graph.off("nodeAdded", onNodeAdded);
796
+ graph.off("nodeDropped", onNodeDropped);
797
+ graph.off("edgeAdded", onEdgeAdded);
798
+ graph.off("edgeDropped", onEdgeDropped);
799
+ graph.off("nodeAttributesUpdated", onNodeAttrsUpdated);
800
+ };
801
+ }
802
+
738
803
  // src/traverse.ts
739
804
  init_cjs_shims();
740
805
  var import_types = require("@neat.is/types");
@@ -1298,9 +1363,11 @@ async function loadPolicyFile(policyPath) {
1298
1363
  }
1299
1364
  var PolicyViolationsLog = class {
1300
1365
  path;
1366
+ project;
1301
1367
  seen = null;
1302
- constructor(logPath) {
1368
+ constructor(logPath, project = DEFAULT_PROJECT) {
1303
1369
  this.path = logPath;
1370
+ this.project = project;
1304
1371
  }
1305
1372
  async append(v) {
1306
1373
  if (!this.seen) await this.hydrate();
@@ -1308,6 +1375,11 @@ var PolicyViolationsLog = class {
1308
1375
  this.seen.add(v.id);
1309
1376
  await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(this.path), { recursive: true });
1310
1377
  await import_node_fs2.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
1378
+ emitNeatEvent({
1379
+ type: "policy-violation",
1380
+ project: this.project,
1381
+ payload: { violation: v }
1382
+ });
1311
1383
  return true;
1312
1384
  }
1313
1385
  async readAll() {
@@ -1777,6 +1849,7 @@ async function markStaleEdges(graph, options = {}) {
1777
1849
  const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
1778
1850
  const now = options.now ?? Date.now();
1779
1851
  const events = [];
1852
+ const project = options.project ?? DEFAULT_PROJECT;
1780
1853
  graph.forEachEdge((id, attrs) => {
1781
1854
  const e = attrs;
1782
1855
  if (e.provenance !== import_types3.Provenance.OBSERVED) return;
@@ -1796,6 +1869,15 @@ async function markStaleEdges(graph, options = {}) {
1796
1869
  lastObserved: e.lastObserved,
1797
1870
  transitionedAt: new Date(now).toISOString()
1798
1871
  });
1872
+ emitNeatEvent({
1873
+ type: "stale-transition",
1874
+ project,
1875
+ payload: {
1876
+ edgeId: id,
1877
+ from: import_types3.Provenance.OBSERVED,
1878
+ to: import_types3.Provenance.STALE
1879
+ }
1880
+ });
1799
1881
  }
1800
1882
  });
1801
1883
  if (options.staleEventsPath && events.length > 0) {
@@ -1826,7 +1908,8 @@ function startStalenessLoop(graph, options = {}) {
1826
1908
  try {
1827
1909
  await markStaleEdges(graph, {
1828
1910
  thresholds: options.thresholds,
1829
- staleEventsPath: options.staleEventsPath
1911
+ staleEventsPath: options.staleEventsPath,
1912
+ project: options.project
1830
1913
  });
1831
1914
  if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
1832
1915
  } catch (err) {
@@ -3056,7 +3139,15 @@ async function addHttpCallEdges(graph, services) {
3056
3139
  const seenTargets = /* @__PURE__ */ new Map();
3057
3140
  for (const file of files) {
3058
3141
  const parser = import_node_path18.default.extname(file.path) === ".py" ? pyParser : jsParser;
3059
- const targets = callsFromSource(file.content, parser, knownHosts);
3142
+ let targets;
3143
+ try {
3144
+ targets = callsFromSource(file.content, parser, knownHosts);
3145
+ } catch (err) {
3146
+ console.warn(
3147
+ `[neat] http call extraction skipped ${file.path}: ${err.message}`
3148
+ );
3149
+ continue;
3150
+ }
3060
3151
  for (const t of targets) {
3061
3152
  const targetId = hostToNodeId.get(t);
3062
3153
  if (!targetId || targetId === service.node.id) continue;
@@ -3585,11 +3676,22 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3585
3676
  const phase5 = await addInfra(graph, scanPath, services);
3586
3677
  const frontiersPromoted = promoteFrontierNodes(graph);
3587
3678
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
3588
- return {
3679
+ const result = {
3589
3680
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3590
3681
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3591
3682
  frontiersPromoted
3592
3683
  };
3684
+ emitNeatEvent({
3685
+ type: "extraction-complete",
3686
+ project: opts.project ?? DEFAULT_PROJECT,
3687
+ payload: {
3688
+ project: opts.project ?? DEFAULT_PROJECT,
3689
+ fileCount: services.length,
3690
+ nodesAdded: result.nodesAdded,
3691
+ edgesAdded: result.edgesAdded
3692
+ }
3693
+ });
3694
+ return result;
3593
3695
  }
3594
3696
 
3595
3697
  // src/persist.ts
@@ -3678,14 +3780,14 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3678
3780
 
3679
3781
  // src/watch.ts
3680
3782
  init_cjs_shims();
3681
- var import_node_path32 = __toESM(require("path"), 1);
3783
+ var import_node_path33 = __toESM(require("path"), 1);
3682
3784
  var import_chokidar = __toESM(require("chokidar"), 1);
3683
3785
 
3684
3786
  // src/api.ts
3685
3787
  init_cjs_shims();
3686
3788
  var import_fastify = __toESM(require("fastify"), 1);
3687
3789
  var import_cors = __toESM(require("@fastify/cors"), 1);
3688
- var import_types18 = require("@neat.is/types");
3790
+ var import_types19 = require("@neat.is/types");
3689
3791
 
3690
3792
  // src/diff.ts
3691
3793
  init_cjs_shims();
@@ -3816,6 +3918,216 @@ var Projects = class {
3816
3918
  }
3817
3919
  };
3818
3920
 
3921
+ // src/registry.ts
3922
+ init_cjs_shims();
3923
+ var import_node_fs17 = require("fs");
3924
+ var import_node_os2 = __toESM(require("os"), 1);
3925
+ var import_node_path29 = __toESM(require("path"), 1);
3926
+ var import_types18 = require("@neat.is/types");
3927
+ var LOCK_TIMEOUT_MS = 5e3;
3928
+ var LOCK_RETRY_MS = 50;
3929
+ function neatHome() {
3930
+ const override = process.env.NEAT_HOME;
3931
+ if (override && override.length > 0) return import_node_path29.default.resolve(override);
3932
+ return import_node_path29.default.join(import_node_os2.default.homedir(), ".neat");
3933
+ }
3934
+ function registryPath() {
3935
+ return import_node_path29.default.join(neatHome(), "projects.json");
3936
+ }
3937
+ function registryLockPath() {
3938
+ return import_node_path29.default.join(neatHome(), "projects.json.lock");
3939
+ }
3940
+ async function normalizeProjectPath(input) {
3941
+ const resolved = import_node_path29.default.resolve(input);
3942
+ try {
3943
+ return await import_node_fs17.promises.realpath(resolved);
3944
+ } catch {
3945
+ return resolved;
3946
+ }
3947
+ }
3948
+ async function writeAtomically(target, contents) {
3949
+ await import_node_fs17.promises.mkdir(import_node_path29.default.dirname(target), { recursive: true });
3950
+ const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
3951
+ const fd = await import_node_fs17.promises.open(tmp, "w");
3952
+ try {
3953
+ await fd.writeFile(contents, "utf8");
3954
+ await fd.sync();
3955
+ } finally {
3956
+ await fd.close();
3957
+ }
3958
+ await import_node_fs17.promises.rename(tmp, target);
3959
+ }
3960
+ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
3961
+ const deadline = Date.now() + timeoutMs;
3962
+ await import_node_fs17.promises.mkdir(import_node_path29.default.dirname(lockPath), { recursive: true });
3963
+ while (true) {
3964
+ try {
3965
+ const fd = await import_node_fs17.promises.open(lockPath, "wx");
3966
+ await fd.close();
3967
+ return;
3968
+ } catch (err) {
3969
+ const code = err.code;
3970
+ if (code !== "EEXIST") throw err;
3971
+ if (Date.now() >= deadline) {
3972
+ throw new Error(
3973
+ `neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
3974
+ );
3975
+ }
3976
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
3977
+ }
3978
+ }
3979
+ }
3980
+ async function releaseLock(lockPath) {
3981
+ await import_node_fs17.promises.unlink(lockPath).catch(() => {
3982
+ });
3983
+ }
3984
+ async function withLock(fn) {
3985
+ const lock = registryLockPath();
3986
+ await acquireLock(lock);
3987
+ try {
3988
+ return await fn();
3989
+ } finally {
3990
+ await releaseLock(lock);
3991
+ }
3992
+ }
3993
+ async function readRegistry() {
3994
+ const file = registryPath();
3995
+ let raw;
3996
+ try {
3997
+ raw = await import_node_fs17.promises.readFile(file, "utf8");
3998
+ } catch (err) {
3999
+ if (err.code === "ENOENT") {
4000
+ return { version: 1, projects: [] };
4001
+ }
4002
+ throw err;
4003
+ }
4004
+ const parsed = JSON.parse(raw);
4005
+ return import_types18.RegistryFileSchema.parse(parsed);
4006
+ }
4007
+ async function writeRegistry(reg) {
4008
+ const validated = import_types18.RegistryFileSchema.parse(reg);
4009
+ await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4010
+ }
4011
+ var ProjectNameCollisionError = class extends Error {
4012
+ projectName;
4013
+ constructor(name) {
4014
+ super(`neat registry: a project named "${name}" is already registered`);
4015
+ this.name = "ProjectNameCollisionError";
4016
+ this.projectName = name;
4017
+ }
4018
+ };
4019
+ async function addProject(opts) {
4020
+ const resolvedPath = await normalizeProjectPath(opts.path);
4021
+ return withLock(async () => {
4022
+ const reg = await readRegistry();
4023
+ const byName = reg.projects.find((p) => p.name === opts.name);
4024
+ const byPath = reg.projects.find((p) => p.path === resolvedPath);
4025
+ if (byName && byName.path !== resolvedPath) {
4026
+ throw new ProjectNameCollisionError(opts.name);
4027
+ }
4028
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4029
+ if (byName && byName.path === resolvedPath) {
4030
+ byName.lastSeenAt = now;
4031
+ if (opts.languages) byName.languages = opts.languages;
4032
+ if (opts.status) byName.status = opts.status;
4033
+ await writeRegistry(reg);
4034
+ return byName;
4035
+ }
4036
+ if (byPath && byPath.name !== opts.name) {
4037
+ throw new ProjectNameCollisionError(byPath.name);
4038
+ }
4039
+ const entry2 = {
4040
+ name: opts.name,
4041
+ path: resolvedPath,
4042
+ registeredAt: now,
4043
+ languages: opts.languages ?? [],
4044
+ status: opts.status ?? "active"
4045
+ };
4046
+ reg.projects.push(entry2);
4047
+ await writeRegistry(reg);
4048
+ return entry2;
4049
+ });
4050
+ }
4051
+ async function listProjects() {
4052
+ const reg = await readRegistry();
4053
+ return reg.projects;
4054
+ }
4055
+ async function setStatus(name, status2) {
4056
+ return withLock(async () => {
4057
+ const reg = await readRegistry();
4058
+ const entry2 = reg.projects.find((p) => p.name === name);
4059
+ if (!entry2) throw new Error(`neat registry: no project named "${name}"`);
4060
+ entry2.status = status2;
4061
+ await writeRegistry(reg);
4062
+ return entry2;
4063
+ });
4064
+ }
4065
+ async function removeProject(name) {
4066
+ return withLock(async () => {
4067
+ const reg = await readRegistry();
4068
+ const idx = reg.projects.findIndex((p) => p.name === name);
4069
+ if (idx < 0) return void 0;
4070
+ const [removed] = reg.projects.splice(idx, 1);
4071
+ await writeRegistry(reg);
4072
+ return removed;
4073
+ });
4074
+ }
4075
+
4076
+ // src/streaming.ts
4077
+ init_cjs_shims();
4078
+ var SSE_HEARTBEAT_MS = 3e4;
4079
+ var SSE_BACKPRESSURE_CAP = 1e3;
4080
+ function handleSse(req, reply, opts) {
4081
+ const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
4082
+ const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
4083
+ reply.raw.setHeader("Content-Type", "text/event-stream");
4084
+ reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
4085
+ reply.raw.setHeader("Connection", "keep-alive");
4086
+ reply.raw.setHeader("X-Accel-Buffering", "no");
4087
+ reply.raw.flushHeaders?.();
4088
+ let pending = 0;
4089
+ let dropped = false;
4090
+ const closeConnection = () => {
4091
+ if (dropped) return;
4092
+ dropped = true;
4093
+ eventBus.off(EVENT_BUS_CHANNEL, listener);
4094
+ clearInterval(heartbeat);
4095
+ if (!reply.raw.writableEnded) reply.raw.end();
4096
+ };
4097
+ const writeFrame = (frame) => {
4098
+ if (dropped) return;
4099
+ if (pending >= backpressureCap) {
4100
+ const errFrame = `event: error
4101
+ data: ${JSON.stringify({ reason: "backpressure" })}
4102
+
4103
+ `;
4104
+ reply.raw.write(errFrame);
4105
+ closeConnection();
4106
+ return;
4107
+ }
4108
+ pending++;
4109
+ reply.raw.write(frame, () => {
4110
+ pending = Math.max(0, pending - 1);
4111
+ });
4112
+ };
4113
+ const listener = (envelope) => {
4114
+ if (envelope.project !== opts.project) return;
4115
+ writeFrame(`event: ${envelope.type}
4116
+ data: ${JSON.stringify(envelope.payload)}
4117
+
4118
+ `);
4119
+ };
4120
+ eventBus.on(EVENT_BUS_CHANNEL, listener);
4121
+ const heartbeat = setInterval(() => {
4122
+ if (dropped) return;
4123
+ reply.raw.write(":heartbeat\n\n");
4124
+ }, heartbeatMs);
4125
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
4126
+ req.raw.on("close", closeConnection);
4127
+ reply.raw.on("close", closeConnection);
4128
+ reply.raw.on("error", closeConnection);
4129
+ }
4130
+
3819
4131
  // src/api.ts
3820
4132
  function serializeGraph(graph) {
3821
4133
  const nodes = [];
@@ -3864,6 +4176,11 @@ function buildLegacyRegistry(opts) {
3864
4176
  }
3865
4177
  function registerRoutes(scope, ctx) {
3866
4178
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
4179
+ scope.get("/events", (req, reply) => {
4180
+ const proj = resolveProject(registry, req, reply);
4181
+ if (!proj) return;
4182
+ handleSse(req, reply, { project: proj.name });
4183
+ });
3867
4184
  scope.get("/health", async (req, reply) => {
3868
4185
  const proj = resolveProject(registry, req, reply);
3869
4186
  if (!proj) return;
@@ -4074,7 +4391,7 @@ function registerRoutes(scope, ctx) {
4074
4391
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4075
4392
  let violations = await log.readAll();
4076
4393
  if (req.query.severity) {
4077
- const sev = import_types18.PolicySeveritySchema.safeParse(req.query.severity);
4394
+ const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
4078
4395
  if (!sev.success) {
4079
4396
  return reply.code(400).send({
4080
4397
  error: "invalid severity",
@@ -4091,7 +4408,7 @@ function registerRoutes(scope, ctx) {
4091
4408
  scope.post("/policies/check", async (req, reply) => {
4092
4409
  const proj = resolveProject(registry, req, reply);
4093
4410
  if (!proj) return;
4094
- const parsed = import_types18.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4411
+ const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4095
4412
  if (!parsed.success) {
4096
4413
  return reply.code(400).send({
4097
4414
  error: "invalid /policies/check body",
@@ -4155,17 +4472,16 @@ async function buildApi(opts) {
4155
4472
  staleEventsPathFor,
4156
4473
  policyFilePathFor
4157
4474
  };
4158
- app.get("/projects", async () => ({
4159
- projects: registry.list().map((name) => {
4160
- const proj = registry.get(name);
4161
- return {
4162
- name,
4163
- nodeCount: proj.graph.order,
4164
- edgeCount: proj.graph.size,
4165
- scanPath: proj.scanPath
4166
- };
4167
- })
4168
- }));
4475
+ app.get("/projects", async (_req, reply) => {
4476
+ try {
4477
+ return await listProjects();
4478
+ } catch (err) {
4479
+ return reply.code(500).send({
4480
+ error: "failed to read project registry",
4481
+ details: err.message
4482
+ });
4483
+ }
4484
+ });
4169
4485
  registerRoutes(app, routeCtx);
4170
4486
  await app.register(
4171
4487
  async (scope) => {
@@ -4178,13 +4494,13 @@ async function buildApi(opts) {
4178
4494
 
4179
4495
  // src/extract/retire.ts
4180
4496
  init_cjs_shims();
4181
- var import_types19 = require("@neat.is/types");
4497
+ var import_types20 = require("@neat.is/types");
4182
4498
  function retireEdgesByFile(graph, file) {
4183
4499
  const normalized = file.split("\\").join("/");
4184
4500
  const toDrop = [];
4185
4501
  graph.forEachEdge((id, attrs) => {
4186
4502
  const edge = attrs;
4187
- if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4503
+ if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
4188
4504
  if (!edge.evidence?.file) return;
4189
4505
  if (edge.evidence.file === normalized) toDrop.push(id);
4190
4506
  });
@@ -4198,8 +4514,8 @@ init_otel_grpc();
4198
4514
 
4199
4515
  // src/search.ts
4200
4516
  init_cjs_shims();
4201
- var import_node_fs17 = require("fs");
4202
- var import_node_path31 = __toESM(require("path"), 1);
4517
+ var import_node_fs18 = require("fs");
4518
+ var import_node_path32 = __toESM(require("path"), 1);
4203
4519
  var import_node_crypto = require("crypto");
4204
4520
  var DEFAULT_LIMIT = 10;
4205
4521
  var NOMIC_DIM = 768;
@@ -4329,7 +4645,7 @@ async function pickEmbedder() {
4329
4645
  }
4330
4646
  async function readCache(cachePath) {
4331
4647
  try {
4332
- const raw = await import_node_fs17.promises.readFile(cachePath, "utf8");
4648
+ const raw = await import_node_fs18.promises.readFile(cachePath, "utf8");
4333
4649
  const parsed = JSON.parse(raw);
4334
4650
  if (parsed.version !== 1) return null;
4335
4651
  return parsed;
@@ -4338,8 +4654,8 @@ async function readCache(cachePath) {
4338
4654
  }
4339
4655
  }
4340
4656
  async function writeCache(cachePath, cache) {
4341
- await import_node_fs17.promises.mkdir(import_node_path31.default.dirname(cachePath), { recursive: true });
4342
- await import_node_fs17.promises.writeFile(cachePath, JSON.stringify(cache));
4657
+ await import_node_fs18.promises.mkdir(import_node_path32.default.dirname(cachePath), { recursive: true });
4658
+ await import_node_fs18.promises.writeFile(cachePath, JSON.stringify(cache));
4343
4659
  }
4344
4660
  var VectorIndex = class {
4345
4661
  constructor(embedder, cachePath) {
@@ -4494,8 +4810,8 @@ var ALL_PHASES = [
4494
4810
  ];
4495
4811
  function classifyChange(relPath) {
4496
4812
  const phases = /* @__PURE__ */ new Set();
4497
- const base = import_node_path32.default.basename(relPath).toLowerCase();
4498
- const segments = relPath.split(import_node_path32.default.sep).map((s) => s.toLowerCase());
4813
+ const base = import_node_path33.default.basename(relPath).toLowerCase();
4814
+ const segments = relPath.split(import_node_path33.default.sep).map((s) => s.toLowerCase());
4499
4815
  if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
4500
4816
  phases.add("services");
4501
4817
  phases.add("aliases");
@@ -4518,7 +4834,8 @@ function classifyChange(relPath) {
4518
4834
  }
4519
4835
  return phases;
4520
4836
  }
4521
- async function runExtractPhases(graph, scanPath, phases) {
4837
+ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJECT) {
4838
+ void project;
4522
4839
  const started = Date.now();
4523
4840
  await ensureCompatLoaded();
4524
4841
  const services = await discoverServices(scanPath);
@@ -4574,9 +4891,11 @@ function shouldIgnore(absPath) {
4574
4891
  }
4575
4892
  async function startWatch(graph, opts) {
4576
4893
  const debounceMs = opts.debounceMs ?? 1e3;
4894
+ const projectName = opts.project ?? DEFAULT_PROJECT;
4577
4895
  await loadGraphFromDisk(graph, opts.outPath);
4578
- const policyFilePath = import_node_path32.default.join(opts.scanPath, "policy.json");
4579
- const policyViolationsPath = import_node_path32.default.join(import_node_path32.default.dirname(opts.outPath), "policy-violations.ndjson");
4896
+ const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
4897
+ const policyFilePath = import_node_path33.default.join(opts.scanPath, "policy.json");
4898
+ const policyViolationsPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.outPath), "policy-violations.ndjson");
4580
4899
  let policies = [];
4581
4900
  try {
4582
4901
  policies = await loadPolicyFile(policyFilePath);
@@ -4586,7 +4905,7 @@ async function startWatch(graph, opts) {
4586
4905
  } catch (err) {
4587
4906
  console.warn(`policies: failed to load ${policyFilePath} \u2014 ${err.message}`);
4588
4907
  }
4589
- const policyLog = new PolicyViolationsLog(policyViolationsPath);
4908
+ const policyLog = new PolicyViolationsLog(policyViolationsPath, projectName);
4590
4909
  const onPolicyTrigger = async (g) => {
4591
4910
  if (policies.length === 0) return;
4592
4911
  try {
@@ -4596,20 +4915,36 @@ async function startWatch(graph, opts) {
4596
4915
  console.warn(`policies: evaluation failed \u2014 ${err.message}`);
4597
4916
  }
4598
4917
  };
4599
- const initial = await runExtractPhases(graph, opts.scanPath, new Set(ALL_PHASES));
4918
+ const initial = await runExtractPhases(
4919
+ graph,
4920
+ opts.scanPath,
4921
+ new Set(ALL_PHASES),
4922
+ projectName
4923
+ );
4600
4924
  console.log(
4601
4925
  `extract: ${initial.nodesAdded} new nodes, ${initial.edgesAdded} new edges (graph total ${graph.order}/${graph.size})`
4602
4926
  );
4927
+ emitNeatEvent({
4928
+ type: "extraction-complete",
4929
+ project: projectName,
4930
+ payload: {
4931
+ project: projectName,
4932
+ fileCount: 0,
4933
+ nodesAdded: initial.nodesAdded,
4934
+ edgesAdded: initial.edgesAdded
4935
+ }
4936
+ });
4603
4937
  await onPolicyTrigger(graph);
4604
4938
  const stopPersist = startPersistLoop(graph, opts.outPath);
4605
4939
  const stopStaleness = startStalenessLoop(graph, {
4606
4940
  staleEventsPath: opts.staleEventsPath,
4941
+ project: projectName,
4607
4942
  onPolicyTrigger
4608
4943
  });
4609
4944
  const host = opts.host ?? "0.0.0.0";
4610
4945
  const port = opts.port ?? 8080;
4611
4946
  const otelPort = opts.otelPort ?? 4318;
4612
- const cachePath = opts.embeddingsCachePath ?? import_node_path32.default.join(import_node_path32.default.dirname(opts.outPath), "embeddings.json");
4947
+ const cachePath = opts.embeddingsCachePath ?? import_node_path33.default.join(import_node_path33.default.dirname(opts.outPath), "embeddings.json");
4613
4948
  let searchIndex;
4614
4949
  try {
4615
4950
  searchIndex = await buildSearchIndex(graph, { cachePath });
@@ -4619,7 +4954,6 @@ async function startWatch(graph, opts) {
4619
4954
  `semantic_search: index build failed (${err.message}); falling back to inline substring`
4620
4955
  );
4621
4956
  }
4622
- const projectName = opts.project ?? DEFAULT_PROJECT;
4623
4957
  const registry = new Projects();
4624
4958
  registry.set(projectName, {
4625
4959
  graph,
@@ -4628,7 +4962,7 @@ async function startWatch(graph, opts) {
4628
4962
  // Paths are derived from the explicit options the watch caller passes
4629
4963
  // — pathsForProject is only used to fill in the embeddings/snapshot
4630
4964
  // fields so the registry shape is complete.
4631
- ...pathsForProject(projectName, import_node_path32.default.dirname(opts.outPath)),
4965
+ ...pathsForProject(projectName, import_node_path33.default.dirname(opts.outPath)),
4632
4966
  snapshotPath: opts.outPath,
4633
4967
  errorsPath: opts.errorsPath,
4634
4968
  staleEventsPath: opts.staleEventsPath
@@ -4644,6 +4978,7 @@ async function startWatch(graph, opts) {
4644
4978
  const onSpan = makeSpanHandler({
4645
4979
  graph,
4646
4980
  errorsPath: opts.errorsPath,
4981
+ project: projectName,
4647
4982
  writeErrorEventInline: false,
4648
4983
  onPolicyTrigger
4649
4984
  });
@@ -4657,6 +4992,7 @@ async function startWatch(graph, opts) {
4657
4992
  const onSpanGrpc = makeSpanHandler({
4658
4993
  graph,
4659
4994
  errorsPath: opts.errorsPath,
4995
+ project: projectName,
4660
4996
  onPolicyTrigger
4661
4997
  });
4662
4998
  const r = await startOtelGrpcReceiver({ onSpan: onSpanGrpc, host, port: grpcPort });
@@ -4676,10 +5012,20 @@ async function startWatch(graph, opts) {
4676
5012
  try {
4677
5013
  let retired = 0;
4678
5014
  for (const p of paths) retired += retireEdgesByFile(graph, p);
4679
- const result = await runExtractPhases(graph, opts.scanPath, phases);
5015
+ const result = await runExtractPhases(graph, opts.scanPath, phases, projectName);
4680
5016
  console.log(
4681
5017
  `[watch] re-extract phases=${result.phases.join(",")} retired=${retired} +${result.nodesAdded}n/+${result.edgesAdded}e in ${result.durationMs}ms`
4682
5018
  );
5019
+ emitNeatEvent({
5020
+ type: "extraction-complete",
5021
+ project: projectName,
5022
+ payload: {
5023
+ project: projectName,
5024
+ fileCount: paths.size,
5025
+ nodesAdded: result.nodesAdded,
5026
+ edgesAdded: result.edgesAdded
5027
+ }
5028
+ });
4683
5029
  if (searchIndex) {
4684
5030
  try {
4685
5031
  await searchIndex.refresh(graph);
@@ -4701,9 +5047,9 @@ async function startWatch(graph, opts) {
4701
5047
  };
4702
5048
  const onPath = (absPath) => {
4703
5049
  if (shouldIgnore(absPath)) return;
4704
- const rel = import_node_path32.default.relative(opts.scanPath, absPath);
5050
+ const rel = import_node_path33.default.relative(opts.scanPath, absPath);
4705
5051
  if (!rel || rel.startsWith("..")) return;
4706
- pendingPaths.add(rel.split(import_node_path32.default.sep).join("/"));
5052
+ pendingPaths.add(rel.split(import_node_path33.default.sep).join("/"));
4707
5053
  const phases = classifyChange(rel);
4708
5054
  if (phases.size === 0) {
4709
5055
  for (const p of ALL_PHASES) pending.add(p);
@@ -4738,6 +5084,7 @@ async function startWatch(graph, opts) {
4738
5084
  await watcher.close();
4739
5085
  stopStaleness();
4740
5086
  stopPersist();
5087
+ detachEventBus();
4741
5088
  await api.close();
4742
5089
  await otelHttp.close();
4743
5090
  if (grpcReceiver) await grpcReceiver.stop();
@@ -4745,182 +5092,27 @@ async function startWatch(graph, opts) {
4745
5092
  return { api, stop };
4746
5093
  }
4747
5094
 
4748
- // src/registry.ts
5095
+ // src/installers/index.ts
4749
5096
  init_cjs_shims();
4750
- var import_node_fs18 = require("fs");
4751
- var import_node_os2 = __toESM(require("os"), 1);
4752
- var import_node_path33 = __toESM(require("path"), 1);
4753
- var import_types20 = require("@neat.is/types");
4754
- var LOCK_TIMEOUT_MS = 5e3;
4755
- var LOCK_RETRY_MS = 50;
4756
- function neatHome() {
4757
- const override = process.env.NEAT_HOME;
4758
- if (override && override.length > 0) return import_node_path33.default.resolve(override);
4759
- return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
4760
- }
4761
- function registryPath() {
4762
- return import_node_path33.default.join(neatHome(), "projects.json");
4763
- }
4764
- function registryLockPath() {
4765
- return import_node_path33.default.join(neatHome(), "projects.json.lock");
4766
- }
4767
- async function normalizeProjectPath(input) {
4768
- const resolved = import_node_path33.default.resolve(input);
4769
- try {
4770
- return await import_node_fs18.promises.realpath(resolved);
4771
- } catch {
4772
- return resolved;
4773
- }
4774
- }
4775
- async function writeAtomically(target, contents) {
4776
- await import_node_fs18.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
4777
- const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4778
- const fd = await import_node_fs18.promises.open(tmp, "w");
4779
- try {
4780
- await fd.writeFile(contents, "utf8");
4781
- await fd.sync();
4782
- } finally {
4783
- await fd.close();
4784
- }
4785
- await import_node_fs18.promises.rename(tmp, target);
4786
- }
4787
- async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4788
- const deadline = Date.now() + timeoutMs;
4789
- await import_node_fs18.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
4790
- while (true) {
4791
- try {
4792
- const fd = await import_node_fs18.promises.open(lockPath, "wx");
4793
- await fd.close();
4794
- return;
4795
- } catch (err) {
4796
- const code = err.code;
4797
- if (code !== "EEXIST") throw err;
4798
- if (Date.now() >= deadline) {
4799
- throw new Error(
4800
- `neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
4801
- );
4802
- }
4803
- await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
4804
- }
4805
- }
4806
- }
4807
- async function releaseLock(lockPath) {
4808
- await import_node_fs18.promises.unlink(lockPath).catch(() => {
4809
- });
4810
- }
4811
- async function withLock(fn) {
4812
- const lock = registryLockPath();
4813
- await acquireLock(lock);
4814
- try {
4815
- return await fn();
4816
- } finally {
4817
- await releaseLock(lock);
4818
- }
4819
- }
4820
- async function readRegistry() {
4821
- const file = registryPath();
4822
- let raw;
4823
- try {
4824
- raw = await import_node_fs18.promises.readFile(file, "utf8");
4825
- } catch (err) {
4826
- if (err.code === "ENOENT") {
4827
- return { version: 1, projects: [] };
4828
- }
4829
- throw err;
4830
- }
4831
- const parsed = JSON.parse(raw);
4832
- return import_types20.RegistryFileSchema.parse(parsed);
4833
- }
4834
- async function writeRegistry(reg) {
4835
- const validated = import_types20.RegistryFileSchema.parse(reg);
4836
- await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4837
- }
4838
- var ProjectNameCollisionError = class extends Error {
4839
- projectName;
4840
- constructor(name) {
4841
- super(`neat registry: a project named "${name}" is already registered`);
4842
- this.name = "ProjectNameCollisionError";
4843
- this.projectName = name;
4844
- }
4845
- };
4846
- async function addProject(opts) {
4847
- const resolvedPath = await normalizeProjectPath(opts.path);
4848
- return withLock(async () => {
4849
- const reg = await readRegistry();
4850
- const byName = reg.projects.find((p) => p.name === opts.name);
4851
- const byPath = reg.projects.find((p) => p.path === resolvedPath);
4852
- if (byName && byName.path !== resolvedPath) {
4853
- throw new ProjectNameCollisionError(opts.name);
4854
- }
4855
- const now = (/* @__PURE__ */ new Date()).toISOString();
4856
- if (byName && byName.path === resolvedPath) {
4857
- byName.lastSeenAt = now;
4858
- if (opts.languages) byName.languages = opts.languages;
4859
- if (opts.status) byName.status = opts.status;
4860
- await writeRegistry(reg);
4861
- return byName;
4862
- }
4863
- if (byPath && byPath.name !== opts.name) {
4864
- throw new ProjectNameCollisionError(byPath.name);
4865
- }
4866
- const entry2 = {
4867
- name: opts.name,
4868
- path: resolvedPath,
4869
- registeredAt: now,
4870
- languages: opts.languages ?? [],
4871
- status: opts.status ?? "active"
4872
- };
4873
- reg.projects.push(entry2);
4874
- await writeRegistry(reg);
4875
- return entry2;
4876
- });
4877
- }
4878
- async function listProjects() {
4879
- const reg = await readRegistry();
4880
- return reg.projects;
4881
- }
4882
- async function setStatus(name, status2) {
4883
- return withLock(async () => {
4884
- const reg = await readRegistry();
4885
- const entry2 = reg.projects.find((p) => p.name === name);
4886
- if (!entry2) throw new Error(`neat registry: no project named "${name}"`);
4887
- entry2.status = status2;
4888
- await writeRegistry(reg);
4889
- return entry2;
4890
- });
4891
- }
4892
- async function removeProject(name) {
4893
- return withLock(async () => {
4894
- const reg = await readRegistry();
4895
- const idx = reg.projects.findIndex((p) => p.name === name);
4896
- if (idx < 0) return void 0;
4897
- const [removed] = reg.projects.splice(idx, 1);
4898
- await writeRegistry(reg);
4899
- return removed;
4900
- });
4901
- }
4902
-
4903
- // src/installers/index.ts
4904
- init_cjs_shims();
4905
-
4906
- // src/installers/javascript.ts
4907
- init_cjs_shims();
4908
- var import_node_fs19 = require("fs");
4909
- var import_node_path34 = __toESM(require("path"), 1);
4910
- var SDK_PACKAGES = [
4911
- { name: "@opentelemetry/api", version: "^1.9.0" },
4912
- { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
4913
- { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
4914
- ];
4915
- var AUTO_INSTRUMENT_REQUIRE = "--require @opentelemetry/auto-instrumentations-node/register";
4916
- var OTEL_ENV = {
4917
- // null target — NEAT does not write `.env` itself; the user sets the env
4918
- // var in their orchestration layer.
4919
- file: null,
4920
- key: "OTEL_EXPORTER_OTLP_ENDPOINT",
4921
- value: "http://localhost:4318"
4922
- };
4923
- async function readPackageJson(serviceDir) {
5097
+
5098
+ // src/installers/javascript.ts
5099
+ init_cjs_shims();
5100
+ var import_node_fs19 = require("fs");
5101
+ var import_node_path34 = __toESM(require("path"), 1);
5102
+ var SDK_PACKAGES = [
5103
+ { name: "@opentelemetry/api", version: "^1.9.0" },
5104
+ { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
5105
+ { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
5106
+ ];
5107
+ var AUTO_INSTRUMENT_REQUIRE = "--require @opentelemetry/auto-instrumentations-node/register";
5108
+ var OTEL_ENV = {
5109
+ // null target — NEAT does not write `.env` itself; the user sets the env
5110
+ // var in their orchestration layer.
5111
+ file: null,
5112
+ key: "OTEL_EXPORTER_OTLP_ENDPOINT",
5113
+ value: "http://localhost:4318"
5114
+ };
5115
+ async function readPackageJson(serviceDir) {
4924
5116
  try {
4925
5117
  const raw = await import_node_fs19.promises.readFile(import_node_path34.default.join(serviceDir, "package.json"), "utf8");
4926
5118
  return JSON.parse(raw);
@@ -5300,11 +5492,457 @@ function renderPatch(sections) {
5300
5492
  return lines.join("\n");
5301
5493
  }
5302
5494
 
5495
+ // src/cli-client.ts
5496
+ init_cjs_shims();
5497
+ var import_types21 = require("@neat.is/types");
5498
+ var HttpError = class extends Error {
5499
+ constructor(status2, message, responseBody = "") {
5500
+ super(message);
5501
+ this.status = status2;
5502
+ this.responseBody = responseBody;
5503
+ this.name = "HttpError";
5504
+ }
5505
+ status;
5506
+ responseBody;
5507
+ };
5508
+ var TransportError = class extends Error {
5509
+ constructor(message) {
5510
+ super(message);
5511
+ this.name = "TransportError";
5512
+ }
5513
+ };
5514
+ function createHttpClient(baseUrl) {
5515
+ const root = baseUrl.replace(/\/$/, "");
5516
+ return {
5517
+ async get(path37) {
5518
+ let res;
5519
+ try {
5520
+ res = await fetch(`${root}${path37}`);
5521
+ } catch (err) {
5522
+ throw new TransportError(
5523
+ `cannot reach neat-core at ${root}: ${err.message}`
5524
+ );
5525
+ }
5526
+ if (!res.ok) {
5527
+ const body = await res.text().catch(() => "");
5528
+ throw new HttpError(
5529
+ res.status,
5530
+ `${res.status} ${res.statusText} on GET ${path37}: ${body}`,
5531
+ body
5532
+ );
5533
+ }
5534
+ return await res.json();
5535
+ },
5536
+ async post(path37, body) {
5537
+ let res;
5538
+ try {
5539
+ res = await fetch(`${root}${path37}`, {
5540
+ method: "POST",
5541
+ headers: { "content-type": "application/json" },
5542
+ body: JSON.stringify(body)
5543
+ });
5544
+ } catch (err) {
5545
+ throw new TransportError(
5546
+ `cannot reach neat-core at ${root}: ${err.message}`
5547
+ );
5548
+ }
5549
+ if (!res.ok) {
5550
+ const text = await res.text().catch(() => "");
5551
+ throw new HttpError(
5552
+ res.status,
5553
+ `${res.status} ${res.statusText} on POST ${path37}: ${text}`,
5554
+ text
5555
+ );
5556
+ }
5557
+ return await res.json();
5558
+ }
5559
+ };
5560
+ }
5561
+ function projectPath(project, suffix) {
5562
+ if (!project) return suffix;
5563
+ return `/projects/${encodeURIComponent(project)}${suffix}`;
5564
+ }
5565
+ async function runRootCause(client, input) {
5566
+ const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
5567
+ const path37 = projectPath(
5568
+ input.project,
5569
+ `/traverse/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
5570
+ );
5571
+ try {
5572
+ const result = await client.get(path37);
5573
+ const arrowPath = result.traversalPath.join(" \u2190 ");
5574
+ const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
5575
+ const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
5576
+ const blockLines = [
5577
+ `Traversal path: ${arrowPath}`,
5578
+ `Edge provenances: ${provenances}`
5579
+ ];
5580
+ if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
5581
+ return {
5582
+ summary,
5583
+ block: blockLines.join("\n"),
5584
+ confidence: result.confidence,
5585
+ provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
5586
+ };
5587
+ } catch (err) {
5588
+ if (err instanceof HttpError && err.status === 404) {
5589
+ return {
5590
+ summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
5591
+ };
5592
+ }
5593
+ throw err;
5594
+ }
5595
+ }
5596
+ async function runBlastRadius(client, input) {
5597
+ const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
5598
+ const path37 = projectPath(
5599
+ input.project,
5600
+ `/traverse/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
5601
+ );
5602
+ try {
5603
+ const result = await client.get(path37);
5604
+ if (result.totalAffected === 0) {
5605
+ return {
5606
+ summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
5607
+ };
5608
+ }
5609
+ const sorted = [...result.affectedNodes].sort(
5610
+ (a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
5611
+ );
5612
+ const blockLines = sorted.map(formatBlastEntry);
5613
+ const minConfidence = sorted.reduce(
5614
+ (m, n) => Math.min(m, n.confidence),
5615
+ Number.POSITIVE_INFINITY
5616
+ );
5617
+ const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
5618
+ return {
5619
+ summary: `Blast radius for ${result.origin}: ${result.totalAffected} affected node${result.totalAffected === 1 ? "" : "s"} reachable downstream.`,
5620
+ block: blockLines.join("\n"),
5621
+ confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
5622
+ provenance: provenances.length ? provenances : void 0
5623
+ };
5624
+ } catch (err) {
5625
+ if (err instanceof HttpError && err.status === 404) {
5626
+ return { summary: `Node ${input.nodeId} not found in the graph.` };
5627
+ }
5628
+ throw err;
5629
+ }
5630
+ }
5631
+ function formatBlastEntry(n) {
5632
+ const tag = n.edgeProvenance === import_types21.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
5633
+ return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
5634
+ }
5635
+ async function runDependencies(client, input) {
5636
+ const depth = input.depth ?? 3;
5637
+ const path37 = projectPath(
5638
+ input.project,
5639
+ `/graph/node/${encodeURIComponent(input.nodeId)}/dependencies?depth=${depth}`
5640
+ );
5641
+ try {
5642
+ const result = await client.get(path37);
5643
+ if (result.total === 0) {
5644
+ return {
5645
+ summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
5646
+ };
5647
+ }
5648
+ const byDistance = /* @__PURE__ */ new Map();
5649
+ for (const dep of result.dependencies) {
5650
+ const ring = byDistance.get(dep.distance) ?? [];
5651
+ ring.push(dep);
5652
+ byDistance.set(dep.distance, ring);
5653
+ }
5654
+ const blockLines = [];
5655
+ for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
5656
+ const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
5657
+ blockLines.push(`${label}:`);
5658
+ for (const dep of byDistance.get(distance)) {
5659
+ blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
5660
+ }
5661
+ }
5662
+ const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
5663
+ const directCount = byDistance.get(1)?.length ?? 0;
5664
+ 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).`;
5665
+ return { summary, block: blockLines.join("\n"), provenance: provenances };
5666
+ } catch (err) {
5667
+ if (err instanceof HttpError && err.status === 404) {
5668
+ return { summary: `Node ${input.nodeId} not found in the graph.` };
5669
+ }
5670
+ throw err;
5671
+ }
5672
+ }
5673
+ async function runObservedDependencies(client, input) {
5674
+ try {
5675
+ const edges = await client.get(
5676
+ projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
5677
+ );
5678
+ const observed = edges.outbound.filter((e) => e.provenance === import_types21.Provenance.OBSERVED);
5679
+ if (observed.length === 0) {
5680
+ const hasExtracted = edges.outbound.some((e) => e.provenance === import_types21.Provenance.EXTRACTED);
5681
+ const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
5682
+ return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
5683
+ }
5684
+ const blockLines = observed.map((e) => ` \u2022 ${e.target} \u2014 ${e.type}${edgeMeta(e)}`);
5685
+ return {
5686
+ summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
5687
+ block: blockLines.join("\n"),
5688
+ provenance: import_types21.Provenance.OBSERVED
5689
+ };
5690
+ } catch (err) {
5691
+ if (err instanceof HttpError && err.status === 404) {
5692
+ return { summary: `Node ${input.nodeId} not found in the graph.` };
5693
+ }
5694
+ throw err;
5695
+ }
5696
+ }
5697
+ function edgeMeta(e) {
5698
+ const bits = [];
5699
+ if (e.signal) {
5700
+ bits.push(`spans=${e.signal.spanCount}`);
5701
+ if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
5702
+ if (e.signal.lastObservedAgeMs !== void 0) {
5703
+ bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
5704
+ }
5705
+ } else if (e.callCount !== void 0) {
5706
+ bits.push(`callCount=${e.callCount}`);
5707
+ }
5708
+ if (e.lastObserved) bits.push(`lastObserved=${e.lastObserved}`);
5709
+ if (e.confidence !== void 0) bits.push(`confidence=${e.confidence}`);
5710
+ return bits.length ? ` [${bits.join(", ")}]` : "";
5711
+ }
5712
+ function formatDuration(ms) {
5713
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
5714
+ const s = Math.round(ms / 1e3);
5715
+ if (s < 60) return `${s}s`;
5716
+ const m = Math.round(s / 60);
5717
+ if (m < 60) return `${m}m`;
5718
+ const h = Math.round(m / 60);
5719
+ if (h < 48) return `${h}h`;
5720
+ return `${Math.round(h / 24)}d`;
5721
+ }
5722
+ async function runIncidents(client, input) {
5723
+ const path37 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
5724
+ try {
5725
+ const events = await client.get(path37);
5726
+ if (events.length === 0) {
5727
+ return {
5728
+ summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
5729
+ };
5730
+ }
5731
+ const ordered = [...events].reverse().slice(0, input.limit ?? 20);
5732
+ const blockLines = [];
5733
+ for (const ev of ordered) {
5734
+ blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
5735
+ blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
5736
+ }
5737
+ const target = input.nodeId ?? "the project";
5738
+ return {
5739
+ summary: `${target} has ${events.length} recorded incident${events.length === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
5740
+ block: blockLines.join("\n"),
5741
+ provenance: import_types21.Provenance.OBSERVED
5742
+ };
5743
+ } catch (err) {
5744
+ if (err instanceof HttpError && err.status === 404) {
5745
+ return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
5746
+ }
5747
+ throw err;
5748
+ }
5749
+ }
5750
+ async function runSearch(client, input) {
5751
+ const result = await client.get(
5752
+ projectPath(input.project, `/search?q=${encodeURIComponent(input.query)}`)
5753
+ );
5754
+ if (result.matches.length === 0) {
5755
+ return { summary: `No matches for "${input.query}".` };
5756
+ }
5757
+ const provider = result.provider ?? "substring";
5758
+ const blockLines = [];
5759
+ let topScore;
5760
+ for (const n of result.matches) {
5761
+ const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
5762
+ const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
5763
+ if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
5764
+ blockLines.push(
5765
+ ` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
5766
+ );
5767
+ }
5768
+ return {
5769
+ summary: `Found ${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${input.query}" via ${provider} provider.`,
5770
+ block: blockLines.join("\n"),
5771
+ confidence: topScore
5772
+ };
5773
+ }
5774
+ async function runDiff(client, input) {
5775
+ const result = await client.get(
5776
+ projectPath(
5777
+ input.project,
5778
+ `/graph/diff?against=${encodeURIComponent(input.againstSnapshot)}`
5779
+ )
5780
+ );
5781
+ 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;
5782
+ const baseLabel = result.base.exportedAt ?? "unknown";
5783
+ if (total === 0) {
5784
+ return {
5785
+ summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
5786
+ };
5787
+ }
5788
+ const blockLines = [
5789
+ ` base exportedAt: ${baseLabel}`,
5790
+ ` current exportedAt: ${result.current.exportedAt}`,
5791
+ ""
5792
+ ];
5793
+ if (result.added.nodes.length || result.added.edges.length) {
5794
+ blockLines.push("Added:");
5795
+ for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
5796
+ for (const e of result.added.edges)
5797
+ blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
5798
+ blockLines.push("");
5799
+ }
5800
+ if (result.removed.nodes.length || result.removed.edges.length) {
5801
+ blockLines.push("Removed:");
5802
+ for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
5803
+ for (const e of result.removed.edges)
5804
+ blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
5805
+ blockLines.push("");
5806
+ }
5807
+ if (result.changed.nodes.length || result.changed.edges.length) {
5808
+ blockLines.push("Changed:");
5809
+ for (const c of result.changed.nodes) {
5810
+ blockLines.push(` ~ node ${c.id} \u2014 ${summariseAttrDiff(c.before, c.after)}`);
5811
+ }
5812
+ for (const c of result.changed.edges) {
5813
+ const provBit = c.before.provenance !== c.after.provenance ? `provenance ${c.before.provenance} \u2192 ${c.after.provenance}` : summariseAttrDiff(c.before, c.after);
5814
+ blockLines.push(` ~ edge ${c.id} \u2014 ${provBit}`);
5815
+ }
5816
+ }
5817
+ return {
5818
+ summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
5819
+ block: blockLines.join("\n").trimEnd()
5820
+ };
5821
+ }
5822
+ function summariseAttrDiff(before, after) {
5823
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
5824
+ const changed = [];
5825
+ for (const k of keys) {
5826
+ if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.push(k);
5827
+ }
5828
+ return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
5829
+ }
5830
+ async function runStaleEdges(client, input) {
5831
+ const params = new URLSearchParams();
5832
+ if (input.limit !== void 0) params.set("limit", String(input.limit));
5833
+ if (input.edgeType) params.set("edgeType", input.edgeType);
5834
+ const qs = params.size > 0 ? `?${params.toString()}` : "";
5835
+ const events = await client.get(
5836
+ projectPath(input.project, `/incidents/stale${qs}`)
5837
+ );
5838
+ if (events.length === 0) {
5839
+ return {
5840
+ summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
5841
+ };
5842
+ }
5843
+ const blockLines = events.map(
5844
+ (e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
5845
+ );
5846
+ return {
5847
+ summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
5848
+ block: blockLines.join("\n"),
5849
+ provenance: import_types21.Provenance.STALE
5850
+ };
5851
+ }
5852
+ async function runPolicies(client, input) {
5853
+ let violations;
5854
+ let allowed = true;
5855
+ let hypothetical;
5856
+ if (input.hypotheticalAction) {
5857
+ if (typeof client.post !== "function") {
5858
+ throw new Error("HttpClient does not support POST \u2014 required for policies dry-run");
5859
+ }
5860
+ const body = await client.post(
5861
+ projectPath(input.project, "/policies/check"),
5862
+ { hypotheticalAction: input.hypotheticalAction }
5863
+ );
5864
+ violations = body.violations;
5865
+ allowed = body.allowed;
5866
+ hypothetical = body.hypotheticalAction;
5867
+ } else {
5868
+ const params = new URLSearchParams();
5869
+ if (input.policyId) params.set("policyId", input.policyId);
5870
+ const qs = params.size > 0 ? `?${params.toString()}` : "";
5871
+ violations = await client.get(
5872
+ projectPath(input.project, `/policies/violations${qs}`)
5873
+ );
5874
+ allowed = violations.every((v) => v.onViolation !== "block");
5875
+ }
5876
+ if (input.nodeId) {
5877
+ violations = violations.filter(
5878
+ (v) => v.subject.nodeId === input.nodeId || v.subject.path?.includes(input.nodeId)
5879
+ );
5880
+ }
5881
+ if (violations.length === 0) {
5882
+ return {
5883
+ summary: hypothetical ? `No violations would result from the hypothetical action (${hypothetical.kind}).` : "No policy violations recorded."
5884
+ };
5885
+ }
5886
+ const blockCount = violations.filter((v) => v.onViolation === "block").length;
5887
+ const summaryParts = [];
5888
+ if (hypothetical) {
5889
+ summaryParts.push(
5890
+ `Hypothetical ${hypothetical.kind} would surface ${violations.length} violation${violations.length === 1 ? "" : "s"}`
5891
+ );
5892
+ } else {
5893
+ summaryParts.push(
5894
+ `${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
5895
+ );
5896
+ }
5897
+ if (blockCount > 0) summaryParts.push(`${blockCount} of which block`);
5898
+ if (!allowed && hypothetical) summaryParts.push("action denied");
5899
+ const summary = summaryParts.join("; ") + ".";
5900
+ const blockLines = violations.map((v) => {
5901
+ const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
5902
+ return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
5903
+ });
5904
+ const severities = [...new Set(violations.map((v) => v.severity))];
5905
+ return {
5906
+ summary,
5907
+ block: blockLines.join("\n"),
5908
+ confidence: hypothetical ? 0.7 : 1,
5909
+ provenance: severities.join(" ")
5910
+ };
5911
+ }
5912
+ function formatFooter(confidence, provenance) {
5913
+ const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
5914
+ const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
5915
+ return `confidence: ${c} \xB7 provenance: ${p}`;
5916
+ }
5917
+ function formatHuman(result) {
5918
+ const sections = [result.summary.trim()];
5919
+ if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
5920
+ sections.push(formatFooter(result.confidence, result.provenance));
5921
+ return sections.join("\n\n");
5922
+ }
5923
+ function formatJson(result) {
5924
+ return JSON.stringify(
5925
+ {
5926
+ summary: result.summary,
5927
+ block: result.block ?? "",
5928
+ confidence: result.confidence ?? null,
5929
+ provenance: result.provenance ?? null
5930
+ },
5931
+ null,
5932
+ 2
5933
+ );
5934
+ }
5935
+ function exitCodeForError(err) {
5936
+ if (err instanceof TransportError) return 3;
5937
+ if (err instanceof HttpError) return 1;
5938
+ return 1;
5939
+ }
5940
+
5303
5941
  // src/cli.ts
5304
5942
  function usage() {
5305
5943
  console.log("usage: neat <command> [args] [--project <name>]");
5306
5944
  console.log("");
5307
- console.log("commands:");
5945
+ console.log("lifecycle commands:");
5308
5946
  console.log(" init <path> One-time install: discover, extract, register, plan SDK install.");
5309
5947
  console.log(" Snapshot lands in <path>/neat-out/graph.json by default");
5310
5948
  console.log(" (or <path>/neat-out/<project>.json for non-default).");
@@ -5326,51 +5964,133 @@ function usage() {
5326
5964
  console.log(" --print-config print the JSON snippet to stdout");
5327
5965
  console.log(" --apply merge mcpServers.neat into ~/.claude.json");
5328
5966
  console.log("");
5967
+ console.log("query commands (mirror the MCP tools, ADR-050):");
5968
+ console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
5969
+ console.log(" example: neat root-cause service:<name>");
5970
+ console.log(" blast-radius <node-id> BFS outbound \u2014 what would break if this dies.");
5971
+ console.log(" example: neat blast-radius database:<host>");
5972
+ console.log(" dependencies <node-id> Transitive outbound dependencies.");
5973
+ console.log(" Flags: --depth N (default 3, max 10)");
5974
+ console.log(" example: neat dependencies service:<name> --depth 2");
5975
+ console.log(" observed-dependencies <node-id> OBSERVED-only outbound edges (runtime traffic).");
5976
+ console.log(" example: neat observed-dependencies service:<name>");
5977
+ console.log(" incidents [<node-id>] Recent error events; per-node when an id is given.");
5978
+ console.log(" Flags: --limit N (default 20)");
5979
+ console.log(" example: neat incidents service:<name> --limit 5");
5980
+ console.log(" search <query> Semantic (or substring) match on node names/ids.");
5981
+ console.log(' example: neat search "checkout"');
5982
+ console.log(" diff --against <snapshot> Compare the live graph to a saved snapshot.");
5983
+ console.log(" example: neat diff --against ./snapshots/baseline.json");
5984
+ console.log(" stale-edges Recent OBSERVED \u2192 STALE transitions.");
5985
+ console.log(" Flags: --limit N, --edge-type CALLS|CONNECTS_TO|...");
5986
+ console.log(" example: neat stale-edges --edge-type CALLS");
5987
+ console.log(" policies Current policy violations.");
5988
+ console.log(" Flags: --node <id>, --hypothetical-action <json>");
5989
+ console.log(" example: neat policies --node service:<name> --json");
5990
+ console.log("");
5329
5991
  console.log("flags:");
5330
5992
  console.log(' --project <name> Name the project this command targets. Default: "default".');
5331
- }
5993
+ console.log(" --json Emit machine-readable JSON instead of human text. Query verbs only.");
5994
+ console.log("");
5995
+ console.log("exit codes:");
5996
+ console.log(" 0 success");
5997
+ console.log(" 1 server error (4xx/5xx body printed to stderr)");
5998
+ console.log(" 2 misuse (missing args, bad flags) \u2014 handled before any network call");
5999
+ console.log(" 3 daemon not reachable (connection refused / timeout)");
6000
+ console.log("");
6001
+ console.log("environment:");
6002
+ console.log(" NEAT_API_URL base URL for the core REST API (default http://localhost:8080)");
6003
+ console.log(" NEAT_PROJECT project name when --project isn't passed");
6004
+ }
6005
+ var STRING_FLAGS = [
6006
+ ["--project", "project"],
6007
+ ["--depth", "depth"],
6008
+ ["--limit", "limit"],
6009
+ ["--edge-type", "edgeType"],
6010
+ ["--node", "node"],
6011
+ ["--since", "since"],
6012
+ ["--against", "against"],
6013
+ ["--error-id", "errorId"],
6014
+ ["--hypothetical-action", "hypotheticalAction"]
6015
+ ];
5332
6016
  function parseArgs(rest) {
5333
6017
  const positional = [];
5334
- let project = null;
5335
- let apply3 = false;
5336
- let dryRun = false;
5337
- let noInstall = false;
5338
- let printConfig = false;
6018
+ const out = {
6019
+ project: null,
6020
+ apply: false,
6021
+ dryRun: false,
6022
+ noInstall: false,
6023
+ printConfig: false,
6024
+ json: false,
6025
+ depth: null,
6026
+ limit: null,
6027
+ edgeType: null,
6028
+ node: null,
6029
+ since: null,
6030
+ against: null,
6031
+ errorId: null,
6032
+ hypotheticalAction: null,
6033
+ positional: []
6034
+ };
5339
6035
  for (let i = 0; i < rest.length; i++) {
5340
6036
  const arg = rest[i];
5341
- if (arg === "--project") {
5342
- const next = rest[i + 1];
5343
- if (!next) {
5344
- console.error("neat: --project requires a value");
5345
- process.exit(2);
5346
- }
5347
- project = next;
5348
- i++;
5349
- continue;
5350
- }
5351
- if (arg.startsWith("--project=")) {
5352
- project = arg.slice("--project=".length);
5353
- continue;
5354
- }
5355
6037
  if (arg === "--apply") {
5356
- apply3 = true;
6038
+ out.apply = true;
5357
6039
  continue;
5358
6040
  }
5359
6041
  if (arg === "--dry-run") {
5360
- dryRun = true;
6042
+ out.dryRun = true;
5361
6043
  continue;
5362
6044
  }
5363
6045
  if (arg === "--no-install") {
5364
- noInstall = true;
6046
+ out.noInstall = true;
5365
6047
  continue;
5366
6048
  }
5367
6049
  if (arg === "--print-config") {
5368
- printConfig = true;
6050
+ out.printConfig = true;
6051
+ continue;
6052
+ }
6053
+ if (arg === "--json") {
6054
+ out.json = true;
5369
6055
  continue;
5370
6056
  }
6057
+ let matched = false;
6058
+ for (const [flag, field] of STRING_FLAGS) {
6059
+ if (arg === flag) {
6060
+ const next = rest[i + 1];
6061
+ if (next === void 0) {
6062
+ console.error(`neat: ${flag} requires a value`);
6063
+ process.exit(2);
6064
+ }
6065
+ assignFlag(out, field, next);
6066
+ i++;
6067
+ matched = true;
6068
+ break;
6069
+ }
6070
+ if (arg.startsWith(`${flag}=`)) {
6071
+ assignFlag(out, field, arg.slice(flag.length + 1));
6072
+ matched = true;
6073
+ break;
6074
+ }
6075
+ }
6076
+ if (matched) continue;
5371
6077
  positional.push(arg);
5372
6078
  }
5373
- return { project, apply: apply3, dryRun, noInstall, printConfig, positional };
6079
+ out.positional = positional;
6080
+ return out;
6081
+ }
6082
+ function assignFlag(out, field, value) {
6083
+ if (field === "depth" || field === "limit") {
6084
+ const n = Number(value);
6085
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
6086
+ console.error(`neat: --${field === "depth" ? "depth" : "limit"} must be a positive integer`);
6087
+ process.exit(2);
6088
+ }
6089
+ out[field] = n;
6090
+ return;
6091
+ }
6092
+ ;
6093
+ out[field] = value;
5374
6094
  }
5375
6095
  function summarise(nodes, edges) {
5376
6096
  const byNode = /* @__PURE__ */ new Map();
@@ -5734,10 +6454,167 @@ async function main() {
5734
6454
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
5735
6455
  return;
5736
6456
  }
6457
+ if (QUERY_VERBS.has(cmd)) {
6458
+ const code = await runQueryVerb(cmd, parsed);
6459
+ if (code !== 0) process.exit(code);
6460
+ return;
6461
+ }
5737
6462
  console.error(`neat: unknown command "${cmd}"`);
5738
6463
  usage();
5739
6464
  process.exit(1);
5740
6465
  }
6466
+ var QUERY_VERBS = /* @__PURE__ */ new Set([
6467
+ "root-cause",
6468
+ "blast-radius",
6469
+ "dependencies",
6470
+ "observed-dependencies",
6471
+ "incidents",
6472
+ "search",
6473
+ "diff",
6474
+ "stale-edges",
6475
+ "policies"
6476
+ ]);
6477
+ function resolveProjectFlag(parsed) {
6478
+ if (parsed.project) return parsed.project;
6479
+ const env = process.env.NEAT_PROJECT;
6480
+ if (env && env.length > 0 && env !== DEFAULT_PROJECT) return env;
6481
+ return void 0;
6482
+ }
6483
+ async function runQueryVerb(cmd, parsed) {
6484
+ const baseUrl = process.env.NEAT_API_URL ?? "http://localhost:8080";
6485
+ const client = createHttpClient(baseUrl);
6486
+ const project = resolveProjectFlag(parsed);
6487
+ const positional = parsed.positional;
6488
+ let work;
6489
+ switch (cmd) {
6490
+ case "root-cause": {
6491
+ const node = positional[0];
6492
+ if (!node) {
6493
+ console.error("neat root-cause: missing <node-id>");
6494
+ return 2;
6495
+ }
6496
+ work = runRootCause(client, {
6497
+ errorNode: node,
6498
+ ...parsed.errorId ? { errorId: parsed.errorId } : {},
6499
+ ...project ? { project } : {}
6500
+ });
6501
+ break;
6502
+ }
6503
+ case "blast-radius": {
6504
+ const node = positional[0];
6505
+ if (!node) {
6506
+ console.error("neat blast-radius: missing <node-id>");
6507
+ return 2;
6508
+ }
6509
+ work = runBlastRadius(client, {
6510
+ nodeId: node,
6511
+ ...parsed.depth !== null ? { depth: parsed.depth } : {},
6512
+ ...project ? { project } : {}
6513
+ });
6514
+ break;
6515
+ }
6516
+ case "dependencies": {
6517
+ const node = positional[0];
6518
+ if (!node) {
6519
+ console.error("neat dependencies: missing <node-id>");
6520
+ return 2;
6521
+ }
6522
+ work = runDependencies(client, {
6523
+ nodeId: node,
6524
+ ...parsed.depth !== null ? { depth: parsed.depth } : {},
6525
+ ...project ? { project } : {}
6526
+ });
6527
+ break;
6528
+ }
6529
+ case "observed-dependencies": {
6530
+ const node = positional[0];
6531
+ if (!node) {
6532
+ console.error("neat observed-dependencies: missing <node-id>");
6533
+ return 2;
6534
+ }
6535
+ work = runObservedDependencies(client, {
6536
+ nodeId: node,
6537
+ ...project ? { project } : {}
6538
+ });
6539
+ break;
6540
+ }
6541
+ case "incidents": {
6542
+ work = runIncidents(client, {
6543
+ ...positional[0] ? { nodeId: positional[0] } : {},
6544
+ ...parsed.limit !== null ? { limit: parsed.limit } : {},
6545
+ ...project ? { project } : {}
6546
+ });
6547
+ break;
6548
+ }
6549
+ case "search": {
6550
+ const q = positional.join(" ").trim();
6551
+ if (!q) {
6552
+ console.error("neat search: missing <query>");
6553
+ return 2;
6554
+ }
6555
+ work = runSearch(client, { query: q, ...project ? { project } : {} });
6556
+ break;
6557
+ }
6558
+ case "diff": {
6559
+ const against = parsed.against ?? parsed.since;
6560
+ if (!against) {
6561
+ console.error("neat diff: --against <snapshot-path> is required");
6562
+ return 2;
6563
+ }
6564
+ work = runDiff(client, {
6565
+ againstSnapshot: against,
6566
+ ...project ? { project } : {}
6567
+ });
6568
+ break;
6569
+ }
6570
+ case "stale-edges": {
6571
+ work = runStaleEdges(client, {
6572
+ ...parsed.limit !== null ? { limit: parsed.limit } : {},
6573
+ ...parsed.edgeType ? { edgeType: parsed.edgeType } : {},
6574
+ ...project ? { project } : {}
6575
+ });
6576
+ break;
6577
+ }
6578
+ case "policies": {
6579
+ let hypothetical;
6580
+ if (parsed.hypotheticalAction) {
6581
+ try {
6582
+ hypothetical = JSON.parse(parsed.hypotheticalAction);
6583
+ } catch (err) {
6584
+ console.error(
6585
+ `neat policies: --hypothetical-action must be valid JSON: ${err.message}`
6586
+ );
6587
+ return 2;
6588
+ }
6589
+ }
6590
+ work = runPolicies(client, {
6591
+ ...parsed.node ? { nodeId: parsed.node } : {},
6592
+ ...hypothetical ? { hypotheticalAction: hypothetical } : {},
6593
+ ...project ? { project } : {}
6594
+ });
6595
+ break;
6596
+ }
6597
+ default:
6598
+ console.error(`neat: unknown query verb "${cmd}"`);
6599
+ return 2;
6600
+ }
6601
+ try {
6602
+ const result = await work;
6603
+ if (parsed.json) process.stdout.write(formatJson(result) + "\n");
6604
+ else process.stdout.write(formatHuman(result) + "\n");
6605
+ return 0;
6606
+ } catch (err) {
6607
+ if (err instanceof HttpError) {
6608
+ const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
6609
+ console.error(`neat ${cmd}: ${detail.trim()}`);
6610
+ } else if (err instanceof TransportError) {
6611
+ console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${process.env.NEAT_API_URL ?? "http://localhost:8080"})`);
6612
+ } else {
6613
+ console.error(`neat ${cmd}: ${err.message}`);
6614
+ }
6615
+ return exitCodeForError(err);
6616
+ }
6617
+ }
5741
6618
  var entry = process.argv[1] ?? "";
5742
6619
  if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat")) {
5743
6620
  main().catch((err) => {
@@ -5748,7 +6625,10 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
5748
6625
  // Annotate the CommonJS export names for ESM import in node:
5749
6626
  0 && (module.exports = {
5750
6627
  CLAUDE_SKILL_CONFIG,
6628
+ QUERY_VERBS,
6629
+ parseArgs,
5751
6630
  runInit,
6631
+ runQueryVerb,
5752
6632
  runSkill
5753
6633
  });
5754
6634
  //# sourceMappingURL=cli.cjs.map