@neat.is/core 0.2.7 → 0.2.9

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_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
105
+ return import_node_path31.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_path31, 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_path31 = __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_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
264
+ const protoRoot = import_node_path32.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_path32.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_path32, 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_path32 = __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,13 +372,16 @@ 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);
379
382
  init_cjs_shims();
380
- var import_node_path36 = __toESM(require("path"), 1);
381
- var import_node_fs21 = require("fs");
383
+ var import_node_path37 = __toESM(require("path"), 1);
384
+ var import_node_fs22 = require("fs");
382
385
 
383
386
  // src/graph.ts
384
387
  init_cjs_shims();
@@ -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");
@@ -822,19 +887,19 @@ function confidenceFromMix(edges, now = Date.now()) {
822
887
  function longestIncomingWalk(graph, start, maxDepth) {
823
888
  let best = { path: [start], edges: [] };
824
889
  const visited = /* @__PURE__ */ new Set([start]);
825
- function step(node, path37, edges) {
826
- if (path37.length > best.path.length) {
827
- best = { path: [...path37], edges: [...edges] };
890
+ function step(node, path38, edges) {
891
+ if (path38.length > best.path.length) {
892
+ best = { path: [...path38], edges: [...edges] };
828
893
  }
829
- if (path37.length - 1 >= maxDepth) return;
894
+ if (path38.length - 1 >= maxDepth) return;
830
895
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
831
896
  for (const [srcId, edge] of incoming) {
832
897
  if (visited.has(srcId)) continue;
833
898
  visited.add(srcId);
834
- path37.push(srcId);
899
+ path38.push(srcId);
835
900
  edges.push(edge);
836
- step(srcId, path37, edges);
837
- path37.pop();
901
+ step(srcId, path38, edges);
902
+ path38.pop();
838
903
  edges.pop();
839
904
  visited.delete(srcId);
840
905
  }
@@ -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) {
@@ -1853,10 +1936,10 @@ async function readErrorEvents(errorsPath) {
1853
1936
 
1854
1937
  // src/extract/services.ts
1855
1938
  init_cjs_shims();
1856
- var import_node_fs6 = require("fs");
1857
- var import_node_path6 = __toESM(require("path"), 1);
1939
+ var import_node_fs7 = require("fs");
1940
+ var import_node_path7 = __toESM(require("path"), 1);
1858
1941
  var import_ignore = __toESM(require("ignore"), 1);
1859
- var import_minimatch = require("minimatch");
1942
+ var import_minimatch2 = require("minimatch");
1860
1943
  var import_types5 = require("@neat.is/types");
1861
1944
 
1862
1945
  // src/extract/shared.ts
@@ -1969,6 +2052,73 @@ function pythonToPackage(service) {
1969
2052
  };
1970
2053
  }
1971
2054
 
2055
+ // src/extract/owners.ts
2056
+ init_cjs_shims();
2057
+ var import_node_fs6 = require("fs");
2058
+ var import_node_path6 = __toESM(require("path"), 1);
2059
+ var import_minimatch = require("minimatch");
2060
+ async function loadCodeowners(scanPath) {
2061
+ const candidates = [
2062
+ import_node_path6.default.join(scanPath, "CODEOWNERS"),
2063
+ import_node_path6.default.join(scanPath, ".github", "CODEOWNERS")
2064
+ ];
2065
+ for (const file of candidates) {
2066
+ if (await exists(file)) {
2067
+ const raw = await import_node_fs6.promises.readFile(file, "utf8");
2068
+ return parseCodeowners(raw);
2069
+ }
2070
+ }
2071
+ return null;
2072
+ }
2073
+ function parseCodeowners(raw) {
2074
+ const rules = [];
2075
+ for (const line of raw.split("\n")) {
2076
+ const trimmed = line.trim();
2077
+ if (!trimmed || trimmed.startsWith("#")) continue;
2078
+ const match = /^(\S+)\s+(.+)$/.exec(trimmed);
2079
+ if (!match) continue;
2080
+ rules.push({ pattern: match[1], owners: match[2].trim() });
2081
+ }
2082
+ return { rules };
2083
+ }
2084
+ function matchOwner(file, repoPath) {
2085
+ const normalized = repoPath.split(import_node_path6.default.sep).join("/");
2086
+ for (const rule of file.rules) {
2087
+ if (matchesPattern(rule.pattern, normalized)) return rule.owners;
2088
+ }
2089
+ return null;
2090
+ }
2091
+ function matchesPattern(rawPattern, repoPath) {
2092
+ let pattern = rawPattern.startsWith("/") ? rawPattern.slice(1) : rawPattern;
2093
+ if (pattern === "*") return !repoPath.includes("/");
2094
+ if (pattern === "**" || pattern === "") return true;
2095
+ if (pattern.endsWith("/")) pattern = pattern + "**";
2096
+ if ((0, import_minimatch.minimatch)(repoPath, pattern, { dot: true })) return true;
2097
+ if (!pattern.includes("*") && (0, import_minimatch.minimatch)(repoPath, pattern + "/**", { dot: true })) return true;
2098
+ return false;
2099
+ }
2100
+ async function readPackageJsonAuthor(serviceDir) {
2101
+ const pkgPath = import_node_path6.default.join(serviceDir, "package.json");
2102
+ if (!await exists(pkgPath)) return null;
2103
+ try {
2104
+ const pkg = await readJson(pkgPath);
2105
+ if (!pkg.author) return null;
2106
+ if (typeof pkg.author === "string") return pkg.author;
2107
+ if (typeof pkg.author === "object" && typeof pkg.author.name === "string") return pkg.author.name;
2108
+ return null;
2109
+ } catch {
2110
+ return null;
2111
+ }
2112
+ }
2113
+ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
2114
+ if (codeowners && repoPath !== void 0) {
2115
+ const owner = matchOwner(codeowners, repoPath);
2116
+ if (owner) return owner;
2117
+ }
2118
+ const author = await readPackageJsonAuthor(serviceDir);
2119
+ return author ?? void 0;
2120
+ }
2121
+
1972
2122
  // src/extract/services.ts
1973
2123
  var DEFAULT_SCAN_DEPTH = 5;
1974
2124
  function parseScanDepth() {
@@ -1985,21 +2135,21 @@ function workspaceGlobs(pkg) {
1985
2135
  return null;
1986
2136
  }
1987
2137
  async function loadGitignore(scanPath) {
1988
- const gitignorePath = import_node_path6.default.join(scanPath, ".gitignore");
2138
+ const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
1989
2139
  if (!await exists(gitignorePath)) return null;
1990
- const raw = await import_node_fs6.promises.readFile(gitignorePath, "utf8");
2140
+ const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
1991
2141
  return (0, import_ignore.default)().add(raw);
1992
2142
  }
1993
2143
  async function walkDirs(start, scanPath, options, visit) {
1994
2144
  async function recurse(current, depth) {
1995
2145
  if (depth > options.maxDepth) return;
1996
- const entries = await import_node_fs6.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2146
+ const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
1997
2147
  for (const entry2 of entries) {
1998
2148
  if (!entry2.isDirectory()) continue;
1999
2149
  if (IGNORED_DIRS.has(entry2.name)) continue;
2000
- const child = import_node_path6.default.join(current, entry2.name);
2150
+ const child = import_node_path7.default.join(current, entry2.name);
2001
2151
  if (options.ig) {
2002
- const rel = import_node_path6.default.relative(scanPath, child).split(import_node_path6.default.sep).join("/");
2152
+ const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
2003
2153
  if (rel && options.ig.ignores(rel + "/")) continue;
2004
2154
  }
2005
2155
  await visit(child);
@@ -2014,8 +2164,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2014
2164
  for (const raw of globs) {
2015
2165
  const pattern = raw.replace(/^\.\//, "");
2016
2166
  if (!pattern.includes("*")) {
2017
- const candidate = import_node_path6.default.join(scanPath, pattern);
2018
- if (await exists(import_node_path6.default.join(candidate, "package.json"))) found.add(candidate);
2167
+ const candidate = import_node_path7.default.join(scanPath, pattern);
2168
+ if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
2019
2169
  continue;
2020
2170
  }
2021
2171
  const segments = pattern.split("/");
@@ -2024,13 +2174,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2024
2174
  if (seg.includes("*")) break;
2025
2175
  staticSegments.push(seg);
2026
2176
  }
2027
- const start = import_node_path6.default.join(scanPath, ...staticSegments);
2177
+ const start = import_node_path7.default.join(scanPath, ...staticSegments);
2028
2178
  if (!await exists(start)) continue;
2029
2179
  const hasDoubleStar = pattern.includes("**");
2030
2180
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2031
2181
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
2032
- const rel = import_node_path6.default.relative(scanPath, dir).split(import_node_path6.default.sep).join("/");
2033
- if ((0, import_minimatch.minimatch)(rel, pattern) && await exists(import_node_path6.default.join(dir, "package.json"))) {
2182
+ const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
2183
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
2034
2184
  found.add(dir);
2035
2185
  }
2036
2186
  });
@@ -2038,9 +2188,17 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2038
2188
  return [...found];
2039
2189
  }
2040
2190
  async function discoverNodeService(scanPath, dir) {
2041
- const pkgPath = import_node_path6.default.join(dir, "package.json");
2191
+ const pkgPath = import_node_path7.default.join(dir, "package.json");
2042
2192
  if (!await exists(pkgPath)) return null;
2043
- const pkg = await readJson(pkgPath);
2193
+ let pkg;
2194
+ try {
2195
+ pkg = await readJson(pkgPath);
2196
+ } catch (err) {
2197
+ console.warn(
2198
+ `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
2199
+ );
2200
+ return null;
2201
+ }
2044
2202
  if (!pkg.name) return null;
2045
2203
  const node = {
2046
2204
  id: (0, import_types5.serviceId)(pkg.name),
@@ -2049,7 +2207,7 @@ async function discoverNodeService(scanPath, dir) {
2049
2207
  language: "javascript",
2050
2208
  version: pkg.version,
2051
2209
  dependencies: pkg.dependencies ?? {},
2052
- repoPath: import_node_path6.default.relative(scanPath, dir),
2210
+ repoPath: import_node_path7.default.relative(scanPath, dir),
2053
2211
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2054
2212
  };
2055
2213
  return { pkg, dir, node };
@@ -2065,13 +2223,22 @@ async function discoverPyService(scanPath, dir) {
2065
2223
  language: "python",
2066
2224
  version: py.version,
2067
2225
  dependencies: py.dependencies,
2068
- repoPath: import_node_path6.default.relative(scanPath, dir)
2226
+ repoPath: import_node_path7.default.relative(scanPath, dir)
2069
2227
  };
2070
2228
  return { pkg, dir, node };
2071
2229
  }
2072
2230
  async function discoverServices(scanPath) {
2073
- const rootPkgPath = import_node_path6.default.join(scanPath, "package.json");
2074
- const rootPkg = await exists(rootPkgPath) ? await readJson(rootPkgPath) : null;
2231
+ const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
2232
+ let rootPkg = null;
2233
+ if (await exists(rootPkgPath)) {
2234
+ try {
2235
+ rootPkg = await readJson(rootPkgPath);
2236
+ } catch (err) {
2237
+ console.warn(
2238
+ `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
2239
+ );
2240
+ }
2241
+ }
2075
2242
  const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
2076
2243
  const candidateDirs = [];
2077
2244
  if (wsGlobs) {
@@ -2084,9 +2251,9 @@ async function discoverServices(scanPath) {
2084
2251
  scanPath,
2085
2252
  { maxDepth: parseScanDepth(), ig },
2086
2253
  async (dir) => {
2087
- if (await exists(import_node_path6.default.join(dir, "package.json"))) {
2254
+ if (await exists(import_node_path7.default.join(dir, "package.json"))) {
2088
2255
  candidateDirs.push(dir);
2089
- } else if (await exists(import_node_path6.default.join(dir, "pyproject.toml")) || await exists(import_node_path6.default.join(dir, "requirements.txt")) || await exists(import_node_path6.default.join(dir, "setup.py"))) {
2256
+ } else if (await exists(import_node_path7.default.join(dir, "pyproject.toml")) || await exists(import_node_path7.default.join(dir, "requirements.txt")) || await exists(import_node_path7.default.join(dir, "setup.py"))) {
2090
2257
  candidateDirs.push(dir);
2091
2258
  }
2092
2259
  }
@@ -2100,8 +2267,8 @@ async function discoverServices(scanPath) {
2100
2267
  if (!service) continue;
2101
2268
  const existingDir = seen.get(service.node.name);
2102
2269
  if (existingDir !== void 0) {
2103
- const a = import_node_path6.default.relative(scanPath, existingDir) || ".";
2104
- const b = import_node_path6.default.relative(scanPath, dir) || ".";
2270
+ const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
2271
+ const b = import_node_path7.default.relative(scanPath, dir) || ".";
2105
2272
  console.warn(
2106
2273
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
2107
2274
  );
@@ -2110,6 +2277,11 @@ async function discoverServices(scanPath) {
2110
2277
  seen.set(service.node.name, dir);
2111
2278
  out.push(service);
2112
2279
  }
2280
+ const codeowners = await loadCodeowners(scanPath);
2281
+ for (const service of out) {
2282
+ const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
2283
+ if (owner !== void 0) service.node.owner = owner;
2284
+ }
2113
2285
  return out;
2114
2286
  }
2115
2287
  function addServiceNodes(graph, services) {
@@ -2133,8 +2305,8 @@ function addServiceNodes(graph, services) {
2133
2305
 
2134
2306
  // src/extract/aliases.ts
2135
2307
  init_cjs_shims();
2136
- var import_node_path7 = __toESM(require("path"), 1);
2137
- var import_node_fs7 = require("fs");
2308
+ var import_node_path8 = __toESM(require("path"), 1);
2309
+ var import_node_fs8 = require("fs");
2138
2310
  var import_yaml2 = require("yaml");
2139
2311
  var import_types6 = require("@neat.is/types");
2140
2312
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -2161,21 +2333,29 @@ function indexServicesByName(services) {
2161
2333
  const map = /* @__PURE__ */ new Map();
2162
2334
  for (const s of services) {
2163
2335
  map.set(s.node.name, s.node.id);
2164
- map.set(import_node_path7.default.basename(s.dir), s.node.id);
2336
+ map.set(import_node_path8.default.basename(s.dir), s.node.id);
2165
2337
  }
2166
2338
  return map;
2167
2339
  }
2168
2340
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
2169
2341
  let composePath = null;
2170
2342
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2171
- const abs = import_node_path7.default.join(scanPath, name);
2343
+ const abs = import_node_path8.default.join(scanPath, name);
2172
2344
  if (await exists(abs)) {
2173
2345
  composePath = abs;
2174
2346
  break;
2175
2347
  }
2176
2348
  }
2177
2349
  if (!composePath) return;
2178
- const compose = await readYaml(composePath);
2350
+ let compose;
2351
+ try {
2352
+ compose = await readYaml(composePath);
2353
+ } catch (err) {
2354
+ console.warn(
2355
+ `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
2356
+ );
2357
+ return;
2358
+ }
2179
2359
  if (!compose?.services) return;
2180
2360
  for (const [composeName, svc] of Object.entries(compose.services)) {
2181
2361
  const serviceId3 = serviceIndex.get(composeName);
@@ -2214,9 +2394,17 @@ function parseDockerfileLabels(content) {
2214
2394
  }
2215
2395
  async function collectDockerfileAliases(graph, services) {
2216
2396
  for (const service of services) {
2217
- const dockerfilePath = import_node_path7.default.join(service.dir, "Dockerfile");
2397
+ const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
2218
2398
  if (!await exists(dockerfilePath)) continue;
2219
- const content = await import_node_fs7.promises.readFile(dockerfilePath, "utf8");
2399
+ let content;
2400
+ try {
2401
+ content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
2402
+ } catch (err) {
2403
+ console.warn(
2404
+ `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2405
+ );
2406
+ continue;
2407
+ }
2220
2408
  const aliases = parseDockerfileLabels(content);
2221
2409
  if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
2222
2410
  }
@@ -2224,13 +2412,13 @@ async function collectDockerfileAliases(graph, services) {
2224
2412
  async function walkYamlFiles(start, depth = 0, max = 5) {
2225
2413
  if (depth > max) return [];
2226
2414
  const out = [];
2227
- const entries = await import_node_fs7.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2415
+ const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2228
2416
  for (const entry2 of entries) {
2229
2417
  if (entry2.isDirectory()) {
2230
2418
  if (IGNORED_DIRS.has(entry2.name)) continue;
2231
- out.push(...await walkYamlFiles(import_node_path7.default.join(start, entry2.name), depth + 1, max));
2232
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path7.default.extname(entry2.name))) {
2233
- out.push(import_node_path7.default.join(start, entry2.name));
2419
+ out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry2.name), depth + 1, max));
2420
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry2.name))) {
2421
+ out.push(import_node_path8.default.join(start, entry2.name));
2234
2422
  }
2235
2423
  }
2236
2424
  return out;
@@ -2257,7 +2445,7 @@ function k8sServiceTarget(doc, byName) {
2257
2445
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
2258
2446
  const files = await walkYamlFiles(scanPath);
2259
2447
  for (const file of files) {
2260
- const content = await import_node_fs7.promises.readFile(file, "utf8");
2448
+ const content = await import_node_fs8.promises.readFile(file, "utf8");
2261
2449
  let docs;
2262
2450
  try {
2263
2451
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2282,14 +2470,14 @@ async function addServiceAliases(graph, scanPath, services) {
2282
2470
 
2283
2471
  // src/extract/databases/index.ts
2284
2472
  init_cjs_shims();
2285
- var import_node_path15 = __toESM(require("path"), 1);
2473
+ var import_node_path16 = __toESM(require("path"), 1);
2286
2474
  var import_types7 = require("@neat.is/types");
2287
2475
 
2288
2476
  // src/extract/databases/db-config-yaml.ts
2289
2477
  init_cjs_shims();
2290
- var import_node_path8 = __toESM(require("path"), 1);
2478
+ var import_node_path9 = __toESM(require("path"), 1);
2291
2479
  async function parse(serviceDir) {
2292
- const yamlPath = import_node_path8.default.join(serviceDir, "db-config.yaml");
2480
+ const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
2293
2481
  if (!await exists(yamlPath)) return [];
2294
2482
  const raw = await readYaml(yamlPath);
2295
2483
  return [
@@ -2307,13 +2495,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2307
2495
 
2308
2496
  // src/extract/databases/dotenv.ts
2309
2497
  init_cjs_shims();
2310
- var import_node_fs9 = require("fs");
2311
- var import_node_path10 = __toESM(require("path"), 1);
2498
+ var import_node_fs10 = require("fs");
2499
+ var import_node_path11 = __toESM(require("path"), 1);
2312
2500
 
2313
2501
  // src/extract/databases/shared.ts
2314
2502
  init_cjs_shims();
2315
- var import_node_fs8 = require("fs");
2316
- var import_node_path9 = __toESM(require("path"), 1);
2503
+ var import_node_fs9 = require("fs");
2504
+ var import_node_path10 = __toESM(require("path"), 1);
2317
2505
  function schemeToEngine(scheme) {
2318
2506
  const s = scheme.toLowerCase().split("+")[0];
2319
2507
  switch (s) {
@@ -2352,14 +2540,14 @@ function parseConnectionString(url) {
2352
2540
  }
2353
2541
  async function readIfExists(filePath) {
2354
2542
  try {
2355
- return await import_node_fs8.promises.readFile(filePath, "utf8");
2543
+ return await import_node_fs9.promises.readFile(filePath, "utf8");
2356
2544
  } catch {
2357
2545
  return null;
2358
2546
  }
2359
2547
  }
2360
2548
  async function findFirst(serviceDir, candidates) {
2361
2549
  for (const rel of candidates) {
2362
- const abs = import_node_path9.default.join(serviceDir, rel);
2550
+ const abs = import_node_path10.default.join(serviceDir, rel);
2363
2551
  const content = await readIfExists(abs);
2364
2552
  if (content !== null) return abs;
2365
2553
  }
@@ -2410,15 +2598,15 @@ function parseDotenvLine(line) {
2410
2598
  return { key, value };
2411
2599
  }
2412
2600
  async function parse2(serviceDir) {
2413
- const entries = await import_node_fs9.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2601
+ const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2414
2602
  const configs = [];
2415
2603
  const seen = /* @__PURE__ */ new Set();
2416
2604
  for (const entry2 of entries) {
2417
2605
  if (!entry2.isFile()) continue;
2418
2606
  const match = isConfigFile(entry2.name);
2419
2607
  if (!match.match || match.fileType !== "env") continue;
2420
- const filePath = import_node_path10.default.join(serviceDir, entry2.name);
2421
- const content = await import_node_fs9.promises.readFile(filePath, "utf8");
2608
+ const filePath = import_node_path11.default.join(serviceDir, entry2.name);
2609
+ const content = await import_node_fs10.promises.readFile(filePath, "utf8");
2422
2610
  for (const line of content.split("\n")) {
2423
2611
  const parsed = parseDotenvLine(line);
2424
2612
  if (!parsed) continue;
@@ -2437,9 +2625,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
2437
2625
 
2438
2626
  // src/extract/databases/prisma.ts
2439
2627
  init_cjs_shims();
2440
- var import_node_path11 = __toESM(require("path"), 1);
2628
+ var import_node_path12 = __toESM(require("path"), 1);
2441
2629
  async function parse3(serviceDir) {
2442
- const schemaPath = import_node_path11.default.join(serviceDir, "prisma", "schema.prisma");
2630
+ const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
2443
2631
  const content = await readIfExists(schemaPath);
2444
2632
  if (!content) return [];
2445
2633
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2571,10 +2759,10 @@ var knexParser = { name: "knex", parse: parse5 };
2571
2759
 
2572
2760
  // src/extract/databases/ormconfig.ts
2573
2761
  init_cjs_shims();
2574
- var import_node_path12 = __toESM(require("path"), 1);
2762
+ var import_node_path13 = __toESM(require("path"), 1);
2575
2763
  async function parse6(serviceDir) {
2576
2764
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2577
- const abs = import_node_path12.default.join(serviceDir, candidate);
2765
+ const abs = import_node_path13.default.join(serviceDir, candidate);
2578
2766
  if (!await exists(abs)) continue;
2579
2767
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2580
2768
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2634,9 +2822,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
2634
2822
 
2635
2823
  // src/extract/databases/sequelize.ts
2636
2824
  init_cjs_shims();
2637
- var import_node_path13 = __toESM(require("path"), 1);
2825
+ var import_node_path14 = __toESM(require("path"), 1);
2638
2826
  async function parse8(serviceDir) {
2639
- const configPath = import_node_path13.default.join(serviceDir, "config", "config.json");
2827
+ const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
2640
2828
  if (!await exists(configPath)) return [];
2641
2829
  const raw = await readJson(configPath);
2642
2830
  const out = [];
@@ -2663,7 +2851,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
2663
2851
 
2664
2852
  // src/extract/databases/docker-compose.ts
2665
2853
  init_cjs_shims();
2666
- var import_node_path14 = __toESM(require("path"), 1);
2854
+ var import_node_path15 = __toESM(require("path"), 1);
2667
2855
  function portFromService(svc) {
2668
2856
  for (const raw of svc.ports ?? []) {
2669
2857
  const str = String(raw);
@@ -2690,7 +2878,7 @@ function databaseFromEnv(svc) {
2690
2878
  }
2691
2879
  async function parse9(serviceDir) {
2692
2880
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2693
- const abs = import_node_path14.default.join(serviceDir, name);
2881
+ const abs = import_node_path15.default.join(serviceDir, name);
2694
2882
  if (!await exists(abs)) continue;
2695
2883
  const raw = await readYaml(abs);
2696
2884
  if (!raw?.services) return [];
@@ -2870,7 +3058,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2870
3058
  provenance: import_types7.Provenance.EXTRACTED,
2871
3059
  ...config.sourceFile ? {
2872
3060
  evidence: {
2873
- file: import_node_path15.default.relative(scanPath, config.sourceFile).split(import_node_path15.default.sep).join("/")
3061
+ file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
2874
3062
  }
2875
3063
  } : {}
2876
3064
  };
@@ -2898,15 +3086,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2898
3086
 
2899
3087
  // src/extract/configs.ts
2900
3088
  init_cjs_shims();
2901
- var import_node_fs10 = require("fs");
2902
- var import_node_path16 = __toESM(require("path"), 1);
3089
+ var import_node_fs11 = require("fs");
3090
+ var import_node_path17 = __toESM(require("path"), 1);
2903
3091
  var import_types8 = require("@neat.is/types");
2904
3092
  async function walkConfigFiles(dir) {
2905
3093
  const out = [];
2906
3094
  async function walk(current) {
2907
- const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true });
3095
+ const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
2908
3096
  for (const entry2 of entries) {
2909
- const full = import_node_path16.default.join(current, entry2.name);
3097
+ const full = import_node_path17.default.join(current, entry2.name);
2910
3098
  if (entry2.isDirectory()) {
2911
3099
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
2912
3100
  } else if (entry2.isFile() && isConfigFile(entry2.name).match) {
@@ -2923,13 +3111,13 @@ async function addConfigNodes(graph, services, scanPath) {
2923
3111
  for (const service of services) {
2924
3112
  const configFiles = await walkConfigFiles(service.dir);
2925
3113
  for (const file of configFiles) {
2926
- const relPath = import_node_path16.default.relative(scanPath, file);
3114
+ const relPath = import_node_path17.default.relative(scanPath, file);
2927
3115
  const node = {
2928
3116
  id: (0, import_types8.configId)(relPath),
2929
3117
  type: import_types8.NodeType.ConfigNode,
2930
- name: import_node_path16.default.basename(file),
3118
+ name: import_node_path17.default.basename(file),
2931
3119
  path: relPath,
2932
- fileType: isConfigFile(import_node_path16.default.basename(file)).fileType
3120
+ fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
2933
3121
  };
2934
3122
  if (!graph.hasNode(node.id)) {
2935
3123
  graph.addNode(node.id, node);
@@ -2941,7 +3129,7 @@ async function addConfigNodes(graph, services, scanPath) {
2941
3129
  target: node.id,
2942
3130
  type: import_types8.EdgeType.CONFIGURED_BY,
2943
3131
  provenance: import_types8.Provenance.EXTRACTED,
2944
- evidence: { file: relPath.split(import_node_path16.default.sep).join("/") }
3132
+ evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
2945
3133
  };
2946
3134
  if (!graph.hasEdge(edge.id)) {
2947
3135
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -2958,7 +3146,7 @@ var import_types14 = require("@neat.is/types");
2958
3146
 
2959
3147
  // src/extract/calls/http.ts
2960
3148
  init_cjs_shims();
2961
- var import_node_path18 = __toESM(require("path"), 1);
3149
+ var import_node_path19 = __toESM(require("path"), 1);
2962
3150
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2963
3151
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
2964
3152
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
@@ -2966,17 +3154,17 @@ var import_types9 = require("@neat.is/types");
2966
3154
 
2967
3155
  // src/extract/calls/shared.ts
2968
3156
  init_cjs_shims();
2969
- var import_node_fs11 = require("fs");
2970
- var import_node_path17 = __toESM(require("path"), 1);
3157
+ var import_node_fs12 = require("fs");
3158
+ var import_node_path18 = __toESM(require("path"), 1);
2971
3159
  async function walkSourceFiles(dir) {
2972
3160
  const out = [];
2973
3161
  async function walk(current) {
2974
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3162
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2975
3163
  for (const entry2 of entries) {
2976
- const full = import_node_path17.default.join(current, entry2.name);
3164
+ const full = import_node_path18.default.join(current, entry2.name);
2977
3165
  if (entry2.isDirectory()) {
2978
3166
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
2979
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path17.default.extname(entry2.name))) {
3167
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry2.name))) {
2980
3168
  out.push(full);
2981
3169
  }
2982
3170
  }
@@ -2989,7 +3177,7 @@ async function loadSourceFiles(dir) {
2989
3177
  const out = [];
2990
3178
  for (const p of paths) {
2991
3179
  try {
2992
- const content = await import_node_fs11.promises.readFile(p, "utf8");
3180
+ const content = await import_node_fs12.promises.readFile(p, "utf8");
2993
3181
  out.push({ path: p, content });
2994
3182
  } catch {
2995
3183
  }
@@ -3045,9 +3233,9 @@ async function addHttpCallEdges(graph, services) {
3045
3233
  const knownHosts = /* @__PURE__ */ new Set();
3046
3234
  const hostToNodeId = /* @__PURE__ */ new Map();
3047
3235
  for (const service of services) {
3048
- knownHosts.add(import_node_path18.default.basename(service.dir));
3236
+ knownHosts.add(import_node_path19.default.basename(service.dir));
3049
3237
  knownHosts.add(service.pkg.name);
3050
- hostToNodeId.set(import_node_path18.default.basename(service.dir), service.node.id);
3238
+ hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
3051
3239
  hostToNodeId.set(service.pkg.name, service.node.id);
3052
3240
  }
3053
3241
  let edgesAdded = 0;
@@ -3055,8 +3243,16 @@ async function addHttpCallEdges(graph, services) {
3055
3243
  const files = await loadSourceFiles(service.dir);
3056
3244
  const seenTargets = /* @__PURE__ */ new Map();
3057
3245
  for (const file of files) {
3058
- const parser = import_node_path18.default.extname(file.path) === ".py" ? pyParser : jsParser;
3059
- const targets = callsFromSource(file.content, parser, knownHosts);
3246
+ const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
3247
+ let targets;
3248
+ try {
3249
+ targets = callsFromSource(file.content, parser, knownHosts);
3250
+ } catch (err) {
3251
+ console.warn(
3252
+ `[neat] http call extraction skipped ${file.path}: ${err.message}`
3253
+ );
3254
+ continue;
3255
+ }
3060
3256
  for (const t of targets) {
3061
3257
  const targetId = hostToNodeId.get(t);
3062
3258
  if (!targetId || targetId === service.node.id) continue;
@@ -3075,7 +3271,7 @@ async function addHttpCallEdges(graph, services) {
3075
3271
  type: import_types9.EdgeType.CALLS,
3076
3272
  provenance: import_types9.Provenance.EXTRACTED,
3077
3273
  evidence: {
3078
- file: import_node_path18.default.relative(service.dir, evidenceFile.file),
3274
+ file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3079
3275
  line,
3080
3276
  snippet: snippet(fileContent, line)
3081
3277
  }
@@ -3091,7 +3287,7 @@ async function addHttpCallEdges(graph, services) {
3091
3287
 
3092
3288
  // src/extract/calls/kafka.ts
3093
3289
  init_cjs_shims();
3094
- var import_node_path19 = __toESM(require("path"), 1);
3290
+ var import_node_path20 = __toESM(require("path"), 1);
3095
3291
  var import_types10 = require("@neat.is/types");
3096
3292
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3097
3293
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -3118,7 +3314,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3118
3314
  kind: "kafka-topic",
3119
3315
  edgeType,
3120
3316
  evidence: {
3121
- file: import_node_path19.default.relative(serviceDir, file.path),
3317
+ file: import_node_path20.default.relative(serviceDir, file.path),
3122
3318
  line,
3123
3319
  snippet: snippet(file.content, line)
3124
3320
  }
@@ -3131,7 +3327,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3131
3327
 
3132
3328
  // src/extract/calls/redis.ts
3133
3329
  init_cjs_shims();
3134
- var import_node_path20 = __toESM(require("path"), 1);
3330
+ var import_node_path21 = __toESM(require("path"), 1);
3135
3331
  var import_types11 = require("@neat.is/types");
3136
3332
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3137
3333
  function redisEndpointsFromFile(file, serviceDir) {
@@ -3150,7 +3346,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3150
3346
  kind: "redis",
3151
3347
  edgeType: "CALLS",
3152
3348
  evidence: {
3153
- file: import_node_path20.default.relative(serviceDir, file.path),
3349
+ file: import_node_path21.default.relative(serviceDir, file.path),
3154
3350
  line,
3155
3351
  snippet: snippet(file.content, line)
3156
3352
  }
@@ -3161,7 +3357,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3161
3357
 
3162
3358
  // src/extract/calls/aws.ts
3163
3359
  init_cjs_shims();
3164
- var import_node_path21 = __toESM(require("path"), 1);
3360
+ var import_node_path22 = __toESM(require("path"), 1);
3165
3361
  var import_types12 = require("@neat.is/types");
3166
3362
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3167
3363
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -3191,7 +3387,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3191
3387
  kind,
3192
3388
  edgeType: "CALLS",
3193
3389
  evidence: {
3194
- file: import_node_path21.default.relative(serviceDir, file.path),
3390
+ file: import_node_path22.default.relative(serviceDir, file.path),
3195
3391
  line,
3196
3392
  snippet: snippet(file.content, line)
3197
3393
  }
@@ -3216,7 +3412,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3216
3412
 
3217
3413
  // src/extract/calls/grpc.ts
3218
3414
  init_cjs_shims();
3219
- var import_node_path22 = __toESM(require("path"), 1);
3415
+ var import_node_path23 = __toESM(require("path"), 1);
3220
3416
  var import_types13 = require("@neat.is/types");
3221
3417
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3222
3418
  function isLikelyAddress(value) {
@@ -3241,7 +3437,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
3241
3437
  kind: "grpc-service",
3242
3438
  edgeType: "CALLS",
3243
3439
  evidence: {
3244
- file: import_node_path22.default.relative(serviceDir, file.path),
3440
+ file: import_node_path23.default.relative(serviceDir, file.path),
3245
3441
  line,
3246
3442
  snippet: snippet(file.content, line)
3247
3443
  }
@@ -3321,7 +3517,7 @@ init_cjs_shims();
3321
3517
 
3322
3518
  // src/extract/infra/docker-compose.ts
3323
3519
  init_cjs_shims();
3324
- var import_node_path23 = __toESM(require("path"), 1);
3520
+ var import_node_path24 = __toESM(require("path"), 1);
3325
3521
  var import_types16 = require("@neat.is/types");
3326
3522
 
3327
3523
  // src/extract/infra/shared.ts
@@ -3359,7 +3555,7 @@ function dependsOnList(value) {
3359
3555
  }
3360
3556
  function serviceNameToServiceNode(name, services) {
3361
3557
  for (const s of services) {
3362
- if (s.node.name === name || import_node_path23.default.basename(s.dir) === name) return s.node.id;
3558
+ if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
3363
3559
  }
3364
3560
  return null;
3365
3561
  }
@@ -3368,16 +3564,24 @@ async function addComposeInfra(graph, scanPath, services) {
3368
3564
  let edgesAdded = 0;
3369
3565
  let composePath = null;
3370
3566
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3371
- const abs = import_node_path23.default.join(scanPath, name);
3567
+ const abs = import_node_path24.default.join(scanPath, name);
3372
3568
  if (await exists(abs)) {
3373
3569
  composePath = abs;
3374
3570
  break;
3375
3571
  }
3376
3572
  }
3377
3573
  if (!composePath) return { nodesAdded, edgesAdded };
3378
- const compose = await readYaml(composePath);
3574
+ let compose;
3575
+ try {
3576
+ compose = await readYaml(composePath);
3577
+ } catch (err) {
3578
+ console.warn(
3579
+ `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
3580
+ );
3581
+ return { nodesAdded, edgesAdded };
3582
+ }
3379
3583
  if (!compose?.services) return { nodesAdded, edgesAdded };
3380
- const evidenceFile = import_node_path23.default.relative(scanPath, composePath).split(import_node_path23.default.sep).join("/");
3584
+ const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
3381
3585
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3382
3586
  for (const [composeName, svc] of Object.entries(compose.services)) {
3383
3587
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3418,8 +3622,8 @@ async function addComposeInfra(graph, scanPath, services) {
3418
3622
 
3419
3623
  // src/extract/infra/dockerfile.ts
3420
3624
  init_cjs_shims();
3421
- var import_node_path24 = __toESM(require("path"), 1);
3422
- var import_node_fs12 = require("fs");
3625
+ var import_node_path25 = __toESM(require("path"), 1);
3626
+ var import_node_fs13 = require("fs");
3423
3627
  var import_types17 = require("@neat.is/types");
3424
3628
  function runtimeImage(content) {
3425
3629
  const lines = content.split("\n");
@@ -3439,9 +3643,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3439
3643
  let nodesAdded = 0;
3440
3644
  let edgesAdded = 0;
3441
3645
  for (const service of services) {
3442
- const dockerfilePath = import_node_path24.default.join(service.dir, "Dockerfile");
3646
+ const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
3443
3647
  if (!await exists(dockerfilePath)) continue;
3444
- const content = await import_node_fs12.promises.readFile(dockerfilePath, "utf8");
3648
+ let content;
3649
+ try {
3650
+ content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
3651
+ } catch (err) {
3652
+ console.warn(
3653
+ `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
3654
+ );
3655
+ continue;
3656
+ }
3445
3657
  const image = runtimeImage(content);
3446
3658
  if (!image) continue;
3447
3659
  const node = makeInfraNode("container-image", image);
@@ -3458,7 +3670,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3458
3670
  type: import_types17.EdgeType.RUNS_ON,
3459
3671
  provenance: import_types17.Provenance.EXTRACTED,
3460
3672
  evidence: {
3461
- file: import_node_path24.default.relative(scanPath, dockerfilePath).split(import_node_path24.default.sep).join("/")
3673
+ file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
3462
3674
  }
3463
3675
  };
3464
3676
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3470,19 +3682,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3470
3682
 
3471
3683
  // src/extract/infra/terraform.ts
3472
3684
  init_cjs_shims();
3473
- var import_node_fs13 = require("fs");
3474
- var import_node_path25 = __toESM(require("path"), 1);
3685
+ var import_node_fs14 = require("fs");
3686
+ var import_node_path26 = __toESM(require("path"), 1);
3475
3687
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3476
3688
  async function walkTfFiles(start, depth = 0, max = 5) {
3477
3689
  if (depth > max) return [];
3478
3690
  const out = [];
3479
- const entries = await import_node_fs13.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3691
+ const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3480
3692
  for (const entry2 of entries) {
3481
3693
  if (entry2.isDirectory()) {
3482
3694
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
3483
- out.push(...await walkTfFiles(import_node_path25.default.join(start, entry2.name), depth + 1, max));
3695
+ out.push(...await walkTfFiles(import_node_path26.default.join(start, entry2.name), depth + 1, max));
3484
3696
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
3485
- out.push(import_node_path25.default.join(start, entry2.name));
3697
+ out.push(import_node_path26.default.join(start, entry2.name));
3486
3698
  }
3487
3699
  }
3488
3700
  return out;
@@ -3491,7 +3703,7 @@ async function addTerraformResources(graph, scanPath) {
3491
3703
  let nodesAdded = 0;
3492
3704
  const files = await walkTfFiles(scanPath);
3493
3705
  for (const file of files) {
3494
- const content = await import_node_fs13.promises.readFile(file, "utf8");
3706
+ const content = await import_node_fs14.promises.readFile(file, "utf8");
3495
3707
  RESOURCE_RE.lastIndex = 0;
3496
3708
  let m;
3497
3709
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3509,8 +3721,8 @@ async function addTerraformResources(graph, scanPath) {
3509
3721
 
3510
3722
  // src/extract/infra/k8s.ts
3511
3723
  init_cjs_shims();
3512
- var import_node_fs14 = require("fs");
3513
- var import_node_path26 = __toESM(require("path"), 1);
3724
+ var import_node_fs15 = require("fs");
3725
+ var import_node_path27 = __toESM(require("path"), 1);
3514
3726
  var import_yaml3 = require("yaml");
3515
3727
  var K8S_KIND_TO_INFRA_KIND = {
3516
3728
  Service: "k8s-service",
@@ -3524,13 +3736,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3524
3736
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3525
3737
  if (depth > max) return [];
3526
3738
  const out = [];
3527
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3739
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3528
3740
  for (const entry2 of entries) {
3529
3741
  if (entry2.isDirectory()) {
3530
3742
  if (IGNORED_DIRS.has(entry2.name)) continue;
3531
- out.push(...await walkYamlFiles2(import_node_path26.default.join(start, entry2.name), depth + 1, max));
3532
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path26.default.extname(entry2.name))) {
3533
- out.push(import_node_path26.default.join(start, entry2.name));
3743
+ out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry2.name), depth + 1, max));
3744
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry2.name))) {
3745
+ out.push(import_node_path27.default.join(start, entry2.name));
3534
3746
  }
3535
3747
  }
3536
3748
  return out;
@@ -3539,7 +3751,7 @@ async function addK8sResources(graph, scanPath) {
3539
3751
  let nodesAdded = 0;
3540
3752
  const files = await walkYamlFiles2(scanPath);
3541
3753
  for (const file of files) {
3542
- const content = await import_node_fs14.promises.readFile(file, "utf8");
3754
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
3543
3755
  let docs;
3544
3756
  try {
3545
3757
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3585,17 +3797,28 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3585
3797
  const phase5 = await addInfra(graph, scanPath, services);
3586
3798
  const frontiersPromoted = promoteFrontierNodes(graph);
3587
3799
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
3588
- return {
3800
+ const result = {
3589
3801
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3590
3802
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3591
3803
  frontiersPromoted
3592
3804
  };
3805
+ emitNeatEvent({
3806
+ type: "extraction-complete",
3807
+ project: opts.project ?? DEFAULT_PROJECT,
3808
+ payload: {
3809
+ project: opts.project ?? DEFAULT_PROJECT,
3810
+ fileCount: services.length,
3811
+ nodesAdded: result.nodesAdded,
3812
+ edgesAdded: result.edgesAdded
3813
+ }
3814
+ });
3815
+ return result;
3593
3816
  }
3594
3817
 
3595
3818
  // src/persist.ts
3596
3819
  init_cjs_shims();
3597
- var import_node_fs15 = require("fs");
3598
- var import_node_path27 = __toESM(require("path"), 1);
3820
+ var import_node_fs16 = require("fs");
3821
+ var import_node_path28 = __toESM(require("path"), 1);
3599
3822
  var SCHEMA_VERSION = 2;
3600
3823
  function migrateV1ToV2(payload) {
3601
3824
  const nodes = payload.graph.nodes;
@@ -3609,7 +3832,7 @@ function migrateV1ToV2(payload) {
3609
3832
  return { ...payload, schemaVersion: 2 };
3610
3833
  }
3611
3834
  async function ensureDir(filePath) {
3612
- await import_node_fs15.promises.mkdir(import_node_path27.default.dirname(filePath), { recursive: true });
3835
+ await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
3613
3836
  }
3614
3837
  async function saveGraphToDisk(graph, outPath) {
3615
3838
  await ensureDir(outPath);
@@ -3619,13 +3842,13 @@ async function saveGraphToDisk(graph, outPath) {
3619
3842
  graph: graph.export()
3620
3843
  };
3621
3844
  const tmp = `${outPath}.tmp`;
3622
- await import_node_fs15.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
3623
- await import_node_fs15.promises.rename(tmp, outPath);
3845
+ await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
3846
+ await import_node_fs16.promises.rename(tmp, outPath);
3624
3847
  }
3625
3848
  async function loadGraphFromDisk(graph, outPath) {
3626
3849
  let raw;
3627
3850
  try {
3628
- raw = await import_node_fs15.promises.readFile(outPath, "utf8");
3851
+ raw = await import_node_fs16.promises.readFile(outPath, "utf8");
3629
3852
  } catch (err) {
3630
3853
  if (err.code === "ENOENT") return;
3631
3854
  throw err;
@@ -3678,18 +3901,18 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3678
3901
 
3679
3902
  // src/watch.ts
3680
3903
  init_cjs_shims();
3681
- var import_node_path32 = __toESM(require("path"), 1);
3904
+ var import_node_path34 = __toESM(require("path"), 1);
3682
3905
  var import_chokidar = __toESM(require("chokidar"), 1);
3683
3906
 
3684
3907
  // src/api.ts
3685
3908
  init_cjs_shims();
3686
3909
  var import_fastify = __toESM(require("fastify"), 1);
3687
3910
  var import_cors = __toESM(require("@fastify/cors"), 1);
3688
- var import_types18 = require("@neat.is/types");
3911
+ var import_types19 = require("@neat.is/types");
3689
3912
 
3690
3913
  // src/diff.ts
3691
3914
  init_cjs_shims();
3692
- var import_node_fs16 = require("fs");
3915
+ var import_node_fs17 = require("fs");
3693
3916
  async function loadSnapshotForDiff(target) {
3694
3917
  if (/^https?:\/\//i.test(target)) {
3695
3918
  const res = await fetch(target);
@@ -3698,7 +3921,7 @@ async function loadSnapshotForDiff(target) {
3698
3921
  }
3699
3922
  return await res.json();
3700
3923
  }
3701
- const raw = await import_node_fs16.promises.readFile(target, "utf8");
3924
+ const raw = await import_node_fs17.promises.readFile(target, "utf8");
3702
3925
  return JSON.parse(raw);
3703
3926
  }
3704
3927
  function indexEntries(entries) {
@@ -3766,23 +3989,23 @@ function canonicalJson(value) {
3766
3989
 
3767
3990
  // src/projects.ts
3768
3991
  init_cjs_shims();
3769
- var import_node_path28 = __toESM(require("path"), 1);
3992
+ var import_node_path29 = __toESM(require("path"), 1);
3770
3993
  function pathsForProject(project, baseDir) {
3771
3994
  if (project === DEFAULT_PROJECT) {
3772
3995
  return {
3773
- snapshotPath: import_node_path28.default.join(baseDir, "graph.json"),
3774
- errorsPath: import_node_path28.default.join(baseDir, "errors.ndjson"),
3775
- staleEventsPath: import_node_path28.default.join(baseDir, "stale-events.ndjson"),
3776
- embeddingsCachePath: import_node_path28.default.join(baseDir, "embeddings.json"),
3777
- policyViolationsPath: import_node_path28.default.join(baseDir, "policy-violations.ndjson")
3996
+ snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
3997
+ errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
3998
+ staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
3999
+ embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
4000
+ policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
3778
4001
  };
3779
4002
  }
3780
4003
  return {
3781
- snapshotPath: import_node_path28.default.join(baseDir, `${project}.json`),
3782
- errorsPath: import_node_path28.default.join(baseDir, `errors.${project}.ndjson`),
3783
- staleEventsPath: import_node_path28.default.join(baseDir, `stale-events.${project}.ndjson`),
3784
- embeddingsCachePath: import_node_path28.default.join(baseDir, `embeddings.${project}.json`),
3785
- policyViolationsPath: import_node_path28.default.join(baseDir, `policy-violations.${project}.ndjson`)
4004
+ snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
4005
+ errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
4006
+ staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
4007
+ embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
4008
+ policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
3786
4009
  };
3787
4010
  }
3788
4011
  var Projects = class {
@@ -3816,56 +4039,271 @@ var Projects = class {
3816
4039
  }
3817
4040
  };
3818
4041
 
3819
- // src/api.ts
3820
- function serializeGraph(graph) {
3821
- const nodes = [];
3822
- graph.forEachNode((_id, attrs) => {
3823
- nodes.push(attrs);
3824
- });
3825
- const edges = [];
3826
- graph.forEachEdge((_id, attrs) => {
3827
- edges.push(attrs);
3828
- });
3829
- return { nodes, edges };
4042
+ // src/registry.ts
4043
+ init_cjs_shims();
4044
+ var import_node_fs18 = require("fs");
4045
+ var import_node_os2 = __toESM(require("os"), 1);
4046
+ var import_node_path30 = __toESM(require("path"), 1);
4047
+ var import_types18 = require("@neat.is/types");
4048
+ var LOCK_TIMEOUT_MS = 5e3;
4049
+ var LOCK_RETRY_MS = 50;
4050
+ function neatHome() {
4051
+ const override = process.env.NEAT_HOME;
4052
+ if (override && override.length > 0) return import_node_path30.default.resolve(override);
4053
+ return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
3830
4054
  }
3831
- function projectFromReq(req) {
3832
- const params = req.params;
3833
- return params.project ?? DEFAULT_PROJECT;
4055
+ function registryPath() {
4056
+ return import_node_path30.default.join(neatHome(), "projects.json");
3834
4057
  }
3835
- function resolveProject(registry, req, reply) {
3836
- const name = projectFromReq(req);
3837
- const ctx = registry.get(name);
3838
- if (!ctx) {
3839
- void reply.code(404).send({ error: "project not found", project: name });
3840
- return null;
4058
+ function registryLockPath() {
4059
+ return import_node_path30.default.join(neatHome(), "projects.json.lock");
4060
+ }
4061
+ async function normalizeProjectPath(input) {
4062
+ const resolved = import_node_path30.default.resolve(input);
4063
+ try {
4064
+ return await import_node_fs18.promises.realpath(resolved);
4065
+ } catch {
4066
+ return resolved;
3841
4067
  }
3842
- return ctx;
3843
4068
  }
3844
- function buildLegacyRegistry(opts) {
3845
- if (opts.projects) return opts.projects;
3846
- if (!opts.graph) {
3847
- throw new Error("buildApi: either `projects` or `graph` must be provided");
4069
+ async function writeAtomically(target, contents) {
4070
+ await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
4071
+ const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4072
+ const fd = await import_node_fs18.promises.open(tmp, "w");
4073
+ try {
4074
+ await fd.writeFile(contents, "utf8");
4075
+ await fd.sync();
4076
+ } finally {
4077
+ await fd.close();
3848
4078
  }
3849
- const registry = new Projects();
3850
- const paths = pathsForProject(DEFAULT_PROJECT, "");
3851
- registry.set(DEFAULT_PROJECT, {
3852
- graph: opts.graph,
3853
- scanPath: opts.scanPath,
3854
- paths: {
3855
- snapshotPath: paths.snapshotPath,
3856
- errorsPath: opts.errorsPath ?? paths.errorsPath,
3857
- staleEventsPath: opts.staleEventsPath ?? paths.staleEventsPath,
3858
- embeddingsCachePath: paths.embeddingsCachePath,
3859
- policyViolationsPath: paths.policyViolationsPath
3860
- },
3861
- searchIndex: opts.searchIndex
3862
- });
3863
- return registry;
4079
+ await import_node_fs18.promises.rename(tmp, target);
3864
4080
  }
3865
- function registerRoutes(scope, ctx) {
3866
- const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
3867
- scope.get("/health", async (req, reply) => {
3868
- const proj = resolveProject(registry, req, reply);
4081
+ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4082
+ const deadline = Date.now() + timeoutMs;
4083
+ await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
4084
+ while (true) {
4085
+ try {
4086
+ const fd = await import_node_fs18.promises.open(lockPath, "wx");
4087
+ await fd.close();
4088
+ return;
4089
+ } catch (err) {
4090
+ const code = err.code;
4091
+ if (code !== "EEXIST") throw err;
4092
+ if (Date.now() >= deadline) {
4093
+ throw new Error(
4094
+ `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.`
4095
+ );
4096
+ }
4097
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
4098
+ }
4099
+ }
4100
+ }
4101
+ async function releaseLock(lockPath) {
4102
+ await import_node_fs18.promises.unlink(lockPath).catch(() => {
4103
+ });
4104
+ }
4105
+ async function withLock(fn) {
4106
+ const lock = registryLockPath();
4107
+ await acquireLock(lock);
4108
+ try {
4109
+ return await fn();
4110
+ } finally {
4111
+ await releaseLock(lock);
4112
+ }
4113
+ }
4114
+ async function readRegistry() {
4115
+ const file = registryPath();
4116
+ let raw;
4117
+ try {
4118
+ raw = await import_node_fs18.promises.readFile(file, "utf8");
4119
+ } catch (err) {
4120
+ if (err.code === "ENOENT") {
4121
+ return { version: 1, projects: [] };
4122
+ }
4123
+ throw err;
4124
+ }
4125
+ const parsed = JSON.parse(raw);
4126
+ return import_types18.RegistryFileSchema.parse(parsed);
4127
+ }
4128
+ async function writeRegistry(reg) {
4129
+ const validated = import_types18.RegistryFileSchema.parse(reg);
4130
+ await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4131
+ }
4132
+ var ProjectNameCollisionError = class extends Error {
4133
+ projectName;
4134
+ constructor(name) {
4135
+ super(`neat registry: a project named "${name}" is already registered`);
4136
+ this.name = "ProjectNameCollisionError";
4137
+ this.projectName = name;
4138
+ }
4139
+ };
4140
+ async function addProject(opts) {
4141
+ const resolvedPath = await normalizeProjectPath(opts.path);
4142
+ return withLock(async () => {
4143
+ const reg = await readRegistry();
4144
+ const byName = reg.projects.find((p) => p.name === opts.name);
4145
+ const byPath = reg.projects.find((p) => p.path === resolvedPath);
4146
+ if (byName && byName.path !== resolvedPath) {
4147
+ throw new ProjectNameCollisionError(opts.name);
4148
+ }
4149
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4150
+ if (byName && byName.path === resolvedPath) {
4151
+ byName.lastSeenAt = now;
4152
+ if (opts.languages) byName.languages = opts.languages;
4153
+ if (opts.status) byName.status = opts.status;
4154
+ await writeRegistry(reg);
4155
+ return byName;
4156
+ }
4157
+ if (byPath && byPath.name !== opts.name) {
4158
+ throw new ProjectNameCollisionError(byPath.name);
4159
+ }
4160
+ const entry2 = {
4161
+ name: opts.name,
4162
+ path: resolvedPath,
4163
+ registeredAt: now,
4164
+ languages: opts.languages ?? [],
4165
+ status: opts.status ?? "active"
4166
+ };
4167
+ reg.projects.push(entry2);
4168
+ await writeRegistry(reg);
4169
+ return entry2;
4170
+ });
4171
+ }
4172
+ async function listProjects() {
4173
+ const reg = await readRegistry();
4174
+ return reg.projects;
4175
+ }
4176
+ async function setStatus(name, status2) {
4177
+ return withLock(async () => {
4178
+ const reg = await readRegistry();
4179
+ const entry2 = reg.projects.find((p) => p.name === name);
4180
+ if (!entry2) throw new Error(`neat registry: no project named "${name}"`);
4181
+ entry2.status = status2;
4182
+ await writeRegistry(reg);
4183
+ return entry2;
4184
+ });
4185
+ }
4186
+ async function removeProject(name) {
4187
+ return withLock(async () => {
4188
+ const reg = await readRegistry();
4189
+ const idx = reg.projects.findIndex((p) => p.name === name);
4190
+ if (idx < 0) return void 0;
4191
+ const [removed] = reg.projects.splice(idx, 1);
4192
+ await writeRegistry(reg);
4193
+ return removed;
4194
+ });
4195
+ }
4196
+
4197
+ // src/streaming.ts
4198
+ init_cjs_shims();
4199
+ var SSE_HEARTBEAT_MS = 3e4;
4200
+ var SSE_BACKPRESSURE_CAP = 1e3;
4201
+ function handleSse(req, reply, opts) {
4202
+ const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
4203
+ const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
4204
+ reply.raw.setHeader("Content-Type", "text/event-stream");
4205
+ reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
4206
+ reply.raw.setHeader("Connection", "keep-alive");
4207
+ reply.raw.setHeader("X-Accel-Buffering", "no");
4208
+ reply.raw.flushHeaders?.();
4209
+ let pending = 0;
4210
+ let dropped = false;
4211
+ const closeConnection = () => {
4212
+ if (dropped) return;
4213
+ dropped = true;
4214
+ eventBus.off(EVENT_BUS_CHANNEL, listener);
4215
+ clearInterval(heartbeat);
4216
+ if (!reply.raw.writableEnded) reply.raw.end();
4217
+ };
4218
+ const writeFrame = (frame) => {
4219
+ if (dropped) return;
4220
+ if (pending >= backpressureCap) {
4221
+ const errFrame = `event: error
4222
+ data: ${JSON.stringify({ reason: "backpressure" })}
4223
+
4224
+ `;
4225
+ reply.raw.write(errFrame);
4226
+ closeConnection();
4227
+ return;
4228
+ }
4229
+ pending++;
4230
+ reply.raw.write(frame, () => {
4231
+ pending = Math.max(0, pending - 1);
4232
+ });
4233
+ };
4234
+ const listener = (envelope) => {
4235
+ if (envelope.project !== opts.project) return;
4236
+ writeFrame(`event: ${envelope.type}
4237
+ data: ${JSON.stringify(envelope.payload)}
4238
+
4239
+ `);
4240
+ };
4241
+ eventBus.on(EVENT_BUS_CHANNEL, listener);
4242
+ const heartbeat = setInterval(() => {
4243
+ if (dropped) return;
4244
+ reply.raw.write(":heartbeat\n\n");
4245
+ }, heartbeatMs);
4246
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
4247
+ req.raw.on("close", closeConnection);
4248
+ reply.raw.on("close", closeConnection);
4249
+ reply.raw.on("error", closeConnection);
4250
+ }
4251
+
4252
+ // src/api.ts
4253
+ function serializeGraph(graph) {
4254
+ const nodes = [];
4255
+ graph.forEachNode((_id, attrs) => {
4256
+ nodes.push(attrs);
4257
+ });
4258
+ const edges = [];
4259
+ graph.forEachEdge((_id, attrs) => {
4260
+ edges.push(attrs);
4261
+ });
4262
+ return { nodes, edges };
4263
+ }
4264
+ function projectFromReq(req) {
4265
+ const params = req.params;
4266
+ return params.project ?? DEFAULT_PROJECT;
4267
+ }
4268
+ function resolveProject(registry, req, reply) {
4269
+ const name = projectFromReq(req);
4270
+ const ctx = registry.get(name);
4271
+ if (!ctx) {
4272
+ void reply.code(404).send({ error: "project not found", project: name });
4273
+ return null;
4274
+ }
4275
+ return ctx;
4276
+ }
4277
+ function buildLegacyRegistry(opts) {
4278
+ if (opts.projects) return opts.projects;
4279
+ if (!opts.graph) {
4280
+ throw new Error("buildApi: either `projects` or `graph` must be provided");
4281
+ }
4282
+ const registry = new Projects();
4283
+ const paths = pathsForProject(DEFAULT_PROJECT, "");
4284
+ registry.set(DEFAULT_PROJECT, {
4285
+ graph: opts.graph,
4286
+ scanPath: opts.scanPath,
4287
+ paths: {
4288
+ snapshotPath: paths.snapshotPath,
4289
+ errorsPath: opts.errorsPath ?? paths.errorsPath,
4290
+ staleEventsPath: opts.staleEventsPath ?? paths.staleEventsPath,
4291
+ embeddingsCachePath: paths.embeddingsCachePath,
4292
+ policyViolationsPath: paths.policyViolationsPath
4293
+ },
4294
+ searchIndex: opts.searchIndex
4295
+ });
4296
+ return registry;
4297
+ }
4298
+ function registerRoutes(scope, ctx) {
4299
+ const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
4300
+ scope.get("/events", (req, reply) => {
4301
+ const proj = resolveProject(registry, req, reply);
4302
+ if (!proj) return;
4303
+ handleSse(req, reply, { project: proj.name });
4304
+ });
4305
+ scope.get("/health", async (req, reply) => {
4306
+ const proj = resolveProject(registry, req, reply);
3869
4307
  if (!proj) return;
3870
4308
  return {
3871
4309
  uptime: Math.floor((Date.now() - startedAt) / 1e3),
@@ -4074,7 +4512,7 @@ function registerRoutes(scope, ctx) {
4074
4512
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4075
4513
  let violations = await log.readAll();
4076
4514
  if (req.query.severity) {
4077
- const sev = import_types18.PolicySeveritySchema.safeParse(req.query.severity);
4515
+ const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
4078
4516
  if (!sev.success) {
4079
4517
  return reply.code(400).send({
4080
4518
  error: "invalid severity",
@@ -4091,7 +4529,7 @@ function registerRoutes(scope, ctx) {
4091
4529
  scope.post("/policies/check", async (req, reply) => {
4092
4530
  const proj = resolveProject(registry, req, reply);
4093
4531
  if (!proj) return;
4094
- const parsed = import_types18.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4532
+ const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4095
4533
  if (!parsed.success) {
4096
4534
  return reply.code(400).send({
4097
4535
  error: "invalid /policies/check body",
@@ -4155,17 +4593,16 @@ async function buildApi(opts) {
4155
4593
  staleEventsPathFor,
4156
4594
  policyFilePathFor
4157
4595
  };
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
- }));
4596
+ app.get("/projects", async (_req, reply) => {
4597
+ try {
4598
+ return await listProjects();
4599
+ } catch (err) {
4600
+ return reply.code(500).send({
4601
+ error: "failed to read project registry",
4602
+ details: err.message
4603
+ });
4604
+ }
4605
+ });
4169
4606
  registerRoutes(app, routeCtx);
4170
4607
  await app.register(
4171
4608
  async (scope) => {
@@ -4178,13 +4615,13 @@ async function buildApi(opts) {
4178
4615
 
4179
4616
  // src/extract/retire.ts
4180
4617
  init_cjs_shims();
4181
- var import_types19 = require("@neat.is/types");
4618
+ var import_types20 = require("@neat.is/types");
4182
4619
  function retireEdgesByFile(graph, file) {
4183
4620
  const normalized = file.split("\\").join("/");
4184
4621
  const toDrop = [];
4185
4622
  graph.forEachEdge((id, attrs) => {
4186
4623
  const edge = attrs;
4187
- if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4624
+ if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
4188
4625
  if (!edge.evidence?.file) return;
4189
4626
  if (edge.evidence.file === normalized) toDrop.push(id);
4190
4627
  });
@@ -4198,8 +4635,8 @@ init_otel_grpc();
4198
4635
 
4199
4636
  // src/search.ts
4200
4637
  init_cjs_shims();
4201
- var import_node_fs17 = require("fs");
4202
- var import_node_path31 = __toESM(require("path"), 1);
4638
+ var import_node_fs19 = require("fs");
4639
+ var import_node_path33 = __toESM(require("path"), 1);
4203
4640
  var import_node_crypto = require("crypto");
4204
4641
  var DEFAULT_LIMIT = 10;
4205
4642
  var NOMIC_DIM = 768;
@@ -4329,7 +4766,7 @@ async function pickEmbedder() {
4329
4766
  }
4330
4767
  async function readCache(cachePath) {
4331
4768
  try {
4332
- const raw = await import_node_fs17.promises.readFile(cachePath, "utf8");
4769
+ const raw = await import_node_fs19.promises.readFile(cachePath, "utf8");
4333
4770
  const parsed = JSON.parse(raw);
4334
4771
  if (parsed.version !== 1) return null;
4335
4772
  return parsed;
@@ -4338,8 +4775,8 @@ async function readCache(cachePath) {
4338
4775
  }
4339
4776
  }
4340
4777
  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));
4778
+ await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(cachePath), { recursive: true });
4779
+ await import_node_fs19.promises.writeFile(cachePath, JSON.stringify(cache));
4343
4780
  }
4344
4781
  var VectorIndex = class {
4345
4782
  constructor(embedder, cachePath) {
@@ -4494,8 +4931,8 @@ var ALL_PHASES = [
4494
4931
  ];
4495
4932
  function classifyChange(relPath) {
4496
4933
  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());
4934
+ const base = import_node_path34.default.basename(relPath).toLowerCase();
4935
+ const segments = relPath.split(import_node_path34.default.sep).map((s) => s.toLowerCase());
4499
4936
  if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
4500
4937
  phases.add("services");
4501
4938
  phases.add("aliases");
@@ -4518,7 +4955,8 @@ function classifyChange(relPath) {
4518
4955
  }
4519
4956
  return phases;
4520
4957
  }
4521
- async function runExtractPhases(graph, scanPath, phases) {
4958
+ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJECT) {
4959
+ void project;
4522
4960
  const started = Date.now();
4523
4961
  await ensureCompatLoaded();
4524
4962
  const services = await discoverServices(scanPath);
@@ -4574,9 +5012,11 @@ function shouldIgnore(absPath) {
4574
5012
  }
4575
5013
  async function startWatch(graph, opts) {
4576
5014
  const debounceMs = opts.debounceMs ?? 1e3;
5015
+ const projectName = opts.project ?? DEFAULT_PROJECT;
4577
5016
  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");
5017
+ const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
5018
+ const policyFilePath = import_node_path34.default.join(opts.scanPath, "policy.json");
5019
+ const policyViolationsPath = import_node_path34.default.join(import_node_path34.default.dirname(opts.outPath), "policy-violations.ndjson");
4580
5020
  let policies = [];
4581
5021
  try {
4582
5022
  policies = await loadPolicyFile(policyFilePath);
@@ -4586,7 +5026,7 @@ async function startWatch(graph, opts) {
4586
5026
  } catch (err) {
4587
5027
  console.warn(`policies: failed to load ${policyFilePath} \u2014 ${err.message}`);
4588
5028
  }
4589
- const policyLog = new PolicyViolationsLog(policyViolationsPath);
5029
+ const policyLog = new PolicyViolationsLog(policyViolationsPath, projectName);
4590
5030
  const onPolicyTrigger = async (g) => {
4591
5031
  if (policies.length === 0) return;
4592
5032
  try {
@@ -4596,20 +5036,36 @@ async function startWatch(graph, opts) {
4596
5036
  console.warn(`policies: evaluation failed \u2014 ${err.message}`);
4597
5037
  }
4598
5038
  };
4599
- const initial = await runExtractPhases(graph, opts.scanPath, new Set(ALL_PHASES));
5039
+ const initial = await runExtractPhases(
5040
+ graph,
5041
+ opts.scanPath,
5042
+ new Set(ALL_PHASES),
5043
+ projectName
5044
+ );
4600
5045
  console.log(
4601
5046
  `extract: ${initial.nodesAdded} new nodes, ${initial.edgesAdded} new edges (graph total ${graph.order}/${graph.size})`
4602
5047
  );
5048
+ emitNeatEvent({
5049
+ type: "extraction-complete",
5050
+ project: projectName,
5051
+ payload: {
5052
+ project: projectName,
5053
+ fileCount: 0,
5054
+ nodesAdded: initial.nodesAdded,
5055
+ edgesAdded: initial.edgesAdded
5056
+ }
5057
+ });
4603
5058
  await onPolicyTrigger(graph);
4604
5059
  const stopPersist = startPersistLoop(graph, opts.outPath);
4605
5060
  const stopStaleness = startStalenessLoop(graph, {
4606
5061
  staleEventsPath: opts.staleEventsPath,
5062
+ project: projectName,
4607
5063
  onPolicyTrigger
4608
5064
  });
4609
5065
  const host = opts.host ?? "0.0.0.0";
4610
5066
  const port = opts.port ?? 8080;
4611
5067
  const otelPort = opts.otelPort ?? 4318;
4612
- const cachePath = opts.embeddingsCachePath ?? import_node_path32.default.join(import_node_path32.default.dirname(opts.outPath), "embeddings.json");
5068
+ const cachePath = opts.embeddingsCachePath ?? import_node_path34.default.join(import_node_path34.default.dirname(opts.outPath), "embeddings.json");
4613
5069
  let searchIndex;
4614
5070
  try {
4615
5071
  searchIndex = await buildSearchIndex(graph, { cachePath });
@@ -4619,7 +5075,6 @@ async function startWatch(graph, opts) {
4619
5075
  `semantic_search: index build failed (${err.message}); falling back to inline substring`
4620
5076
  );
4621
5077
  }
4622
- const projectName = opts.project ?? DEFAULT_PROJECT;
4623
5078
  const registry = new Projects();
4624
5079
  registry.set(projectName, {
4625
5080
  graph,
@@ -4628,7 +5083,7 @@ async function startWatch(graph, opts) {
4628
5083
  // Paths are derived from the explicit options the watch caller passes
4629
5084
  // — pathsForProject is only used to fill in the embeddings/snapshot
4630
5085
  // fields so the registry shape is complete.
4631
- ...pathsForProject(projectName, import_node_path32.default.dirname(opts.outPath)),
5086
+ ...pathsForProject(projectName, import_node_path34.default.dirname(opts.outPath)),
4632
5087
  snapshotPath: opts.outPath,
4633
5088
  errorsPath: opts.errorsPath,
4634
5089
  staleEventsPath: opts.staleEventsPath
@@ -4644,6 +5099,7 @@ async function startWatch(graph, opts) {
4644
5099
  const onSpan = makeSpanHandler({
4645
5100
  graph,
4646
5101
  errorsPath: opts.errorsPath,
5102
+ project: projectName,
4647
5103
  writeErrorEventInline: false,
4648
5104
  onPolicyTrigger
4649
5105
  });
@@ -4657,6 +5113,7 @@ async function startWatch(graph, opts) {
4657
5113
  const onSpanGrpc = makeSpanHandler({
4658
5114
  graph,
4659
5115
  errorsPath: opts.errorsPath,
5116
+ project: projectName,
4660
5117
  onPolicyTrigger
4661
5118
  });
4662
5119
  const r = await startOtelGrpcReceiver({ onSpan: onSpanGrpc, host, port: grpcPort });
@@ -4676,10 +5133,20 @@ async function startWatch(graph, opts) {
4676
5133
  try {
4677
5134
  let retired = 0;
4678
5135
  for (const p of paths) retired += retireEdgesByFile(graph, p);
4679
- const result = await runExtractPhases(graph, opts.scanPath, phases);
5136
+ const result = await runExtractPhases(graph, opts.scanPath, phases, projectName);
4680
5137
  console.log(
4681
5138
  `[watch] re-extract phases=${result.phases.join(",")} retired=${retired} +${result.nodesAdded}n/+${result.edgesAdded}e in ${result.durationMs}ms`
4682
5139
  );
5140
+ emitNeatEvent({
5141
+ type: "extraction-complete",
5142
+ project: projectName,
5143
+ payload: {
5144
+ project: projectName,
5145
+ fileCount: paths.size,
5146
+ nodesAdded: result.nodesAdded,
5147
+ edgesAdded: result.edgesAdded
5148
+ }
5149
+ });
4683
5150
  if (searchIndex) {
4684
5151
  try {
4685
5152
  await searchIndex.refresh(graph);
@@ -4701,9 +5168,9 @@ async function startWatch(graph, opts) {
4701
5168
  };
4702
5169
  const onPath = (absPath) => {
4703
5170
  if (shouldIgnore(absPath)) return;
4704
- const rel = import_node_path32.default.relative(opts.scanPath, absPath);
5171
+ const rel = import_node_path34.default.relative(opts.scanPath, absPath);
4705
5172
  if (!rel || rel.startsWith("..")) return;
4706
- pendingPaths.add(rel.split(import_node_path32.default.sep).join("/"));
5173
+ pendingPaths.add(rel.split(import_node_path34.default.sep).join("/"));
4707
5174
  const phases = classifyChange(rel);
4708
5175
  if (phases.size === 0) {
4709
5176
  for (const p of ALL_PHASES) pending.add(p);
@@ -4738,6 +5205,7 @@ async function startWatch(graph, opts) {
4738
5205
  await watcher.close();
4739
5206
  stopStaleness();
4740
5207
  stopPersist();
5208
+ detachEventBus();
4741
5209
  await api.close();
4742
5210
  await otelHttp.close();
4743
5211
  if (grpcReceiver) await grpcReceiver.stop();
@@ -4745,192 +5213,37 @@ async function startWatch(graph, opts) {
4745
5213
  return { api, stop };
4746
5214
  }
4747
5215
 
4748
- // src/registry.ts
5216
+ // src/installers/index.ts
4749
5217
  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);
5218
+
5219
+ // src/installers/javascript.ts
5220
+ init_cjs_shims();
5221
+ var import_node_fs20 = require("fs");
5222
+ var import_node_path35 = __toESM(require("path"), 1);
5223
+ var SDK_PACKAGES = [
5224
+ { name: "@opentelemetry/api", version: "^1.9.0" },
5225
+ { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
5226
+ { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
5227
+ ];
5228
+ var AUTO_INSTRUMENT_REQUIRE = "--require @opentelemetry/auto-instrumentations-node/register";
5229
+ var OTEL_ENV = {
5230
+ // null target — NEAT does not write `.env` itself; the user sets the env
5231
+ // var in their orchestration layer.
5232
+ file: null,
5233
+ key: "OTEL_EXPORTER_OTLP_ENDPOINT",
5234
+ value: "http://localhost:4318"
5235
+ };
5236
+ async function readPackageJson(serviceDir) {
4769
5237
  try {
4770
- return await import_node_fs18.promises.realpath(resolved);
5238
+ const raw = await import_node_fs20.promises.readFile(import_node_path35.default.join(serviceDir, "package.json"), "utf8");
5239
+ return JSON.parse(raw);
4771
5240
  } catch {
4772
- return resolved;
5241
+ return null;
4773
5242
  }
4774
5243
  }
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) {
4924
- try {
4925
- const raw = await import_node_fs19.promises.readFile(import_node_path34.default.join(serviceDir, "package.json"), "utf8");
4926
- return JSON.parse(raw);
4927
- } catch {
4928
- return null;
4929
- }
4930
- }
4931
- async function detect(serviceDir) {
4932
- const pkg = await readPackageJson(serviceDir);
4933
- return pkg !== null && typeof pkg.name === "string";
5244
+ async function detect(serviceDir) {
5245
+ const pkg = await readPackageJson(serviceDir);
5246
+ return pkg !== null && typeof pkg.name === "string";
4934
5247
  }
4935
5248
  function rewriteStartScript(start) {
4936
5249
  if (start.includes(AUTO_INSTRUMENT_REQUIRE)) return start;
@@ -4941,7 +5254,7 @@ function rewriteStartScript(start) {
4941
5254
  }
4942
5255
  async function plan(serviceDir) {
4943
5256
  const pkg = await readPackageJson(serviceDir);
4944
- const manifestPath = import_node_path34.default.join(serviceDir, "package.json");
5257
+ const manifestPath = import_node_path35.default.join(serviceDir, "package.json");
4945
5258
  const empty = {
4946
5259
  language: "javascript",
4947
5260
  serviceDir,
@@ -4988,7 +5301,7 @@ async function apply(installPlan) {
4988
5301
  const originals = /* @__PURE__ */ new Map();
4989
5302
  for (const file of touched) {
4990
5303
  try {
4991
- originals.set(file, await import_node_fs19.promises.readFile(file, "utf8"));
5304
+ originals.set(file, await import_node_fs20.promises.readFile(file, "utf8"));
4992
5305
  } catch {
4993
5306
  }
4994
5307
  }
@@ -5014,8 +5327,8 @@ async function apply(installPlan) {
5014
5327
  }
5015
5328
  const newRaw = JSON.stringify(pkg, null, 2) + "\n";
5016
5329
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
5017
- await import_node_fs19.promises.writeFile(tmp, newRaw, "utf8");
5018
- await import_node_fs19.promises.rename(tmp, file);
5330
+ await import_node_fs20.promises.writeFile(tmp, newRaw, "utf8");
5331
+ await import_node_fs20.promises.rename(tmp, file);
5019
5332
  }
5020
5333
  } catch (err) {
5021
5334
  await rollback(installPlan, originals);
@@ -5026,7 +5339,7 @@ async function rollback(installPlan, originals) {
5026
5339
  const restored = [];
5027
5340
  for (const [file, raw] of originals.entries()) {
5028
5341
  try {
5029
- await import_node_fs19.promises.writeFile(file, raw, "utf8");
5342
+ await import_node_fs20.promises.writeFile(file, raw, "utf8");
5030
5343
  restored.push(file);
5031
5344
  } catch {
5032
5345
  }
@@ -5040,8 +5353,8 @@ async function rollback(installPlan, originals) {
5040
5353
  ...restored.map((f) => `restored: ${f}`),
5041
5354
  ""
5042
5355
  ];
5043
- const rollbackPath = import_node_path34.default.join(installPlan.serviceDir, "neat-rollback.patch");
5044
- await import_node_fs19.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
5356
+ const rollbackPath = import_node_path35.default.join(installPlan.serviceDir, "neat-rollback.patch");
5357
+ await import_node_fs20.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
5045
5358
  }
5046
5359
  var javascriptInstaller = {
5047
5360
  name: "javascript",
@@ -5052,8 +5365,8 @@ var javascriptInstaller = {
5052
5365
 
5053
5366
  // src/installers/python.ts
5054
5367
  init_cjs_shims();
5055
- var import_node_fs20 = require("fs");
5056
- var import_node_path35 = __toESM(require("path"), 1);
5368
+ var import_node_fs21 = require("fs");
5369
+ var import_node_path36 = __toESM(require("path"), 1);
5057
5370
  var SDK_PACKAGES2 = [
5058
5371
  { name: "opentelemetry-distro", version: ">=0.49b0" },
5059
5372
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
@@ -5065,7 +5378,7 @@ var OTEL_ENV2 = {
5065
5378
  };
5066
5379
  async function exists2(p) {
5067
5380
  try {
5068
- await import_node_fs20.promises.stat(p);
5381
+ await import_node_fs21.promises.stat(p);
5069
5382
  return true;
5070
5383
  } catch {
5071
5384
  return false;
@@ -5074,7 +5387,7 @@ async function exists2(p) {
5074
5387
  async function detect2(serviceDir) {
5075
5388
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
5076
5389
  for (const m of markers) {
5077
- if (await exists2(import_node_path35.default.join(serviceDir, m))) return true;
5390
+ if (await exists2(import_node_path36.default.join(serviceDir, m))) return true;
5078
5391
  }
5079
5392
  return false;
5080
5393
  }
@@ -5084,9 +5397,9 @@ function reqPackageName(line) {
5084
5397
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
5085
5398
  }
5086
5399
  async function planRequirementsTxtEdits(serviceDir) {
5087
- const file = import_node_path35.default.join(serviceDir, "requirements.txt");
5400
+ const file = import_node_path36.default.join(serviceDir, "requirements.txt");
5088
5401
  if (!await exists2(file)) return null;
5089
- const raw = await import_node_fs20.promises.readFile(file, "utf8");
5402
+ const raw = await import_node_fs21.promises.readFile(file, "utf8");
5090
5403
  const presentNames = new Set(
5091
5404
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
5092
5405
  );
@@ -5094,9 +5407,9 @@ async function planRequirementsTxtEdits(serviceDir) {
5094
5407
  return { manifest: file, missing: [...missing] };
5095
5408
  }
5096
5409
  async function planProcfileEdits(serviceDir) {
5097
- const procfile = import_node_path35.default.join(serviceDir, "Procfile");
5410
+ const procfile = import_node_path36.default.join(serviceDir, "Procfile");
5098
5411
  if (!await exists2(procfile)) return [];
5099
- const raw = await import_node_fs20.promises.readFile(procfile, "utf8");
5412
+ const raw = await import_node_fs21.promises.readFile(procfile, "utf8");
5100
5413
  const edits = [];
5101
5414
  for (const line of raw.split(/\r?\n/)) {
5102
5415
  if (line.length === 0) continue;
@@ -5148,8 +5461,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
5148
5461
  const next = `${original}${trailing}${newlines.join("\n")}
5149
5462
  `;
5150
5463
  const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
5151
- await import_node_fs20.promises.writeFile(tmp, next, "utf8");
5152
- await import_node_fs20.promises.rename(tmp, manifest);
5464
+ await import_node_fs21.promises.writeFile(tmp, next, "utf8");
5465
+ await import_node_fs21.promises.rename(tmp, manifest);
5153
5466
  }
5154
5467
  async function applyProcfile(procfile, edits, original) {
5155
5468
  let next = original;
@@ -5158,8 +5471,8 @@ async function applyProcfile(procfile, edits, original) {
5158
5471
  next = next.replace(e.before, e.after);
5159
5472
  }
5160
5473
  const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
5161
- await import_node_fs20.promises.writeFile(tmp, next, "utf8");
5162
- await import_node_fs20.promises.rename(tmp, procfile);
5474
+ await import_node_fs21.promises.writeFile(tmp, next, "utf8");
5475
+ await import_node_fs21.promises.rename(tmp, procfile);
5163
5476
  }
5164
5477
  async function apply2(installPlan) {
5165
5478
  const touched = /* @__PURE__ */ new Set();
@@ -5169,7 +5482,7 @@ async function apply2(installPlan) {
5169
5482
  const originals = /* @__PURE__ */ new Map();
5170
5483
  for (const file of touched) {
5171
5484
  try {
5172
- originals.set(file, await import_node_fs20.promises.readFile(file, "utf8"));
5485
+ originals.set(file, await import_node_fs21.promises.readFile(file, "utf8"));
5173
5486
  } catch {
5174
5487
  }
5175
5488
  }
@@ -5179,7 +5492,7 @@ async function apply2(installPlan) {
5179
5492
  if (raw === void 0) {
5180
5493
  throw new Error(`python installer: cannot read ${file} during apply`);
5181
5494
  }
5182
- const base = import_node_path35.default.basename(file);
5495
+ const base = import_node_path36.default.basename(file);
5183
5496
  if (base === "requirements.txt") {
5184
5497
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
5185
5498
  if (edits.length > 0) await applyRequirementsTxt(file, edits, raw);
@@ -5197,7 +5510,7 @@ async function rollback2(installPlan, originals) {
5197
5510
  const restored = [];
5198
5511
  for (const [file, raw] of originals.entries()) {
5199
5512
  try {
5200
- await import_node_fs20.promises.writeFile(file, raw, "utf8");
5513
+ await import_node_fs21.promises.writeFile(file, raw, "utf8");
5201
5514
  restored.push(file);
5202
5515
  } catch {
5203
5516
  }
@@ -5211,8 +5524,8 @@ async function rollback2(installPlan, originals) {
5211
5524
  ...restored.map((f) => `restored: ${f}`),
5212
5525
  ""
5213
5526
  ];
5214
- const rollbackPath = import_node_path35.default.join(installPlan.serviceDir, "neat-rollback.patch");
5215
- await import_node_fs20.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
5527
+ const rollbackPath = import_node_path36.default.join(installPlan.serviceDir, "neat-rollback.patch");
5528
+ await import_node_fs21.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
5216
5529
  }
5217
5530
  var pythonInstaller = {
5218
5531
  name: "python",
@@ -5300,11 +5613,457 @@ function renderPatch(sections) {
5300
5613
  return lines.join("\n");
5301
5614
  }
5302
5615
 
5616
+ // src/cli-client.ts
5617
+ init_cjs_shims();
5618
+ var import_types21 = require("@neat.is/types");
5619
+ var HttpError = class extends Error {
5620
+ constructor(status2, message, responseBody = "") {
5621
+ super(message);
5622
+ this.status = status2;
5623
+ this.responseBody = responseBody;
5624
+ this.name = "HttpError";
5625
+ }
5626
+ status;
5627
+ responseBody;
5628
+ };
5629
+ var TransportError = class extends Error {
5630
+ constructor(message) {
5631
+ super(message);
5632
+ this.name = "TransportError";
5633
+ }
5634
+ };
5635
+ function createHttpClient(baseUrl) {
5636
+ const root = baseUrl.replace(/\/$/, "");
5637
+ return {
5638
+ async get(path38) {
5639
+ let res;
5640
+ try {
5641
+ res = await fetch(`${root}${path38}`);
5642
+ } catch (err) {
5643
+ throw new TransportError(
5644
+ `cannot reach neat-core at ${root}: ${err.message}`
5645
+ );
5646
+ }
5647
+ if (!res.ok) {
5648
+ const body = await res.text().catch(() => "");
5649
+ throw new HttpError(
5650
+ res.status,
5651
+ `${res.status} ${res.statusText} on GET ${path38}: ${body}`,
5652
+ body
5653
+ );
5654
+ }
5655
+ return await res.json();
5656
+ },
5657
+ async post(path38, body) {
5658
+ let res;
5659
+ try {
5660
+ res = await fetch(`${root}${path38}`, {
5661
+ method: "POST",
5662
+ headers: { "content-type": "application/json" },
5663
+ body: JSON.stringify(body)
5664
+ });
5665
+ } catch (err) {
5666
+ throw new TransportError(
5667
+ `cannot reach neat-core at ${root}: ${err.message}`
5668
+ );
5669
+ }
5670
+ if (!res.ok) {
5671
+ const text = await res.text().catch(() => "");
5672
+ throw new HttpError(
5673
+ res.status,
5674
+ `${res.status} ${res.statusText} on POST ${path38}: ${text}`,
5675
+ text
5676
+ );
5677
+ }
5678
+ return await res.json();
5679
+ }
5680
+ };
5681
+ }
5682
+ function projectPath(project, suffix) {
5683
+ if (!project) return suffix;
5684
+ return `/projects/${encodeURIComponent(project)}${suffix}`;
5685
+ }
5686
+ async function runRootCause(client, input) {
5687
+ const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
5688
+ const path38 = projectPath(
5689
+ input.project,
5690
+ `/traverse/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
5691
+ );
5692
+ try {
5693
+ const result = await client.get(path38);
5694
+ const arrowPath = result.traversalPath.join(" \u2190 ");
5695
+ const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
5696
+ const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
5697
+ const blockLines = [
5698
+ `Traversal path: ${arrowPath}`,
5699
+ `Edge provenances: ${provenances}`
5700
+ ];
5701
+ if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
5702
+ return {
5703
+ summary,
5704
+ block: blockLines.join("\n"),
5705
+ confidence: result.confidence,
5706
+ provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
5707
+ };
5708
+ } catch (err) {
5709
+ if (err instanceof HttpError && err.status === 404) {
5710
+ return {
5711
+ summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
5712
+ };
5713
+ }
5714
+ throw err;
5715
+ }
5716
+ }
5717
+ async function runBlastRadius(client, input) {
5718
+ const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
5719
+ const path38 = projectPath(
5720
+ input.project,
5721
+ `/traverse/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
5722
+ );
5723
+ try {
5724
+ const result = await client.get(path38);
5725
+ if (result.totalAffected === 0) {
5726
+ return {
5727
+ summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
5728
+ };
5729
+ }
5730
+ const sorted = [...result.affectedNodes].sort(
5731
+ (a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
5732
+ );
5733
+ const blockLines = sorted.map(formatBlastEntry);
5734
+ const minConfidence = sorted.reduce(
5735
+ (m, n) => Math.min(m, n.confidence),
5736
+ Number.POSITIVE_INFINITY
5737
+ );
5738
+ const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
5739
+ return {
5740
+ summary: `Blast radius for ${result.origin}: ${result.totalAffected} affected node${result.totalAffected === 1 ? "" : "s"} reachable downstream.`,
5741
+ block: blockLines.join("\n"),
5742
+ confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
5743
+ provenance: provenances.length ? provenances : void 0
5744
+ };
5745
+ } catch (err) {
5746
+ if (err instanceof HttpError && err.status === 404) {
5747
+ return { summary: `Node ${input.nodeId} not found in the graph.` };
5748
+ }
5749
+ throw err;
5750
+ }
5751
+ }
5752
+ function formatBlastEntry(n) {
5753
+ const tag = n.edgeProvenance === import_types21.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
5754
+ return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
5755
+ }
5756
+ async function runDependencies(client, input) {
5757
+ const depth = input.depth ?? 3;
5758
+ const path38 = projectPath(
5759
+ input.project,
5760
+ `/graph/node/${encodeURIComponent(input.nodeId)}/dependencies?depth=${depth}`
5761
+ );
5762
+ try {
5763
+ const result = await client.get(path38);
5764
+ if (result.total === 0) {
5765
+ return {
5766
+ summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
5767
+ };
5768
+ }
5769
+ const byDistance = /* @__PURE__ */ new Map();
5770
+ for (const dep of result.dependencies) {
5771
+ const ring = byDistance.get(dep.distance) ?? [];
5772
+ ring.push(dep);
5773
+ byDistance.set(dep.distance, ring);
5774
+ }
5775
+ const blockLines = [];
5776
+ for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
5777
+ const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
5778
+ blockLines.push(`${label}:`);
5779
+ for (const dep of byDistance.get(distance)) {
5780
+ blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
5781
+ }
5782
+ }
5783
+ const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
5784
+ const directCount = byDistance.get(1)?.length ?? 0;
5785
+ 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).`;
5786
+ return { summary, block: blockLines.join("\n"), provenance: provenances };
5787
+ } catch (err) {
5788
+ if (err instanceof HttpError && err.status === 404) {
5789
+ return { summary: `Node ${input.nodeId} not found in the graph.` };
5790
+ }
5791
+ throw err;
5792
+ }
5793
+ }
5794
+ async function runObservedDependencies(client, input) {
5795
+ try {
5796
+ const edges = await client.get(
5797
+ projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
5798
+ );
5799
+ const observed = edges.outbound.filter((e) => e.provenance === import_types21.Provenance.OBSERVED);
5800
+ if (observed.length === 0) {
5801
+ const hasExtracted = edges.outbound.some((e) => e.provenance === import_types21.Provenance.EXTRACTED);
5802
+ const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
5803
+ return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
5804
+ }
5805
+ const blockLines = observed.map((e) => ` \u2022 ${e.target} \u2014 ${e.type}${edgeMeta(e)}`);
5806
+ return {
5807
+ summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
5808
+ block: blockLines.join("\n"),
5809
+ provenance: import_types21.Provenance.OBSERVED
5810
+ };
5811
+ } catch (err) {
5812
+ if (err instanceof HttpError && err.status === 404) {
5813
+ return { summary: `Node ${input.nodeId} not found in the graph.` };
5814
+ }
5815
+ throw err;
5816
+ }
5817
+ }
5818
+ function edgeMeta(e) {
5819
+ const bits = [];
5820
+ if (e.signal) {
5821
+ bits.push(`spans=${e.signal.spanCount}`);
5822
+ if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
5823
+ if (e.signal.lastObservedAgeMs !== void 0) {
5824
+ bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
5825
+ }
5826
+ } else if (e.callCount !== void 0) {
5827
+ bits.push(`callCount=${e.callCount}`);
5828
+ }
5829
+ if (e.lastObserved) bits.push(`lastObserved=${e.lastObserved}`);
5830
+ if (e.confidence !== void 0) bits.push(`confidence=${e.confidence}`);
5831
+ return bits.length ? ` [${bits.join(", ")}]` : "";
5832
+ }
5833
+ function formatDuration(ms) {
5834
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
5835
+ const s = Math.round(ms / 1e3);
5836
+ if (s < 60) return `${s}s`;
5837
+ const m = Math.round(s / 60);
5838
+ if (m < 60) return `${m}m`;
5839
+ const h = Math.round(m / 60);
5840
+ if (h < 48) return `${h}h`;
5841
+ return `${Math.round(h / 24)}d`;
5842
+ }
5843
+ async function runIncidents(client, input) {
5844
+ const path38 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
5845
+ try {
5846
+ const events = await client.get(path38);
5847
+ if (events.length === 0) {
5848
+ return {
5849
+ summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
5850
+ };
5851
+ }
5852
+ const ordered = [...events].reverse().slice(0, input.limit ?? 20);
5853
+ const blockLines = [];
5854
+ for (const ev of ordered) {
5855
+ blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
5856
+ blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
5857
+ }
5858
+ const target = input.nodeId ?? "the project";
5859
+ return {
5860
+ summary: `${target} has ${events.length} recorded incident${events.length === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
5861
+ block: blockLines.join("\n"),
5862
+ provenance: import_types21.Provenance.OBSERVED
5863
+ };
5864
+ } catch (err) {
5865
+ if (err instanceof HttpError && err.status === 404) {
5866
+ return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
5867
+ }
5868
+ throw err;
5869
+ }
5870
+ }
5871
+ async function runSearch(client, input) {
5872
+ const result = await client.get(
5873
+ projectPath(input.project, `/search?q=${encodeURIComponent(input.query)}`)
5874
+ );
5875
+ if (result.matches.length === 0) {
5876
+ return { summary: `No matches for "${input.query}".` };
5877
+ }
5878
+ const provider = result.provider ?? "substring";
5879
+ const blockLines = [];
5880
+ let topScore;
5881
+ for (const n of result.matches) {
5882
+ const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
5883
+ const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
5884
+ if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
5885
+ blockLines.push(
5886
+ ` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
5887
+ );
5888
+ }
5889
+ return {
5890
+ summary: `Found ${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${input.query}" via ${provider} provider.`,
5891
+ block: blockLines.join("\n"),
5892
+ confidence: topScore
5893
+ };
5894
+ }
5895
+ async function runDiff(client, input) {
5896
+ const result = await client.get(
5897
+ projectPath(
5898
+ input.project,
5899
+ `/graph/diff?against=${encodeURIComponent(input.againstSnapshot)}`
5900
+ )
5901
+ );
5902
+ 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;
5903
+ const baseLabel = result.base.exportedAt ?? "unknown";
5904
+ if (total === 0) {
5905
+ return {
5906
+ summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
5907
+ };
5908
+ }
5909
+ const blockLines = [
5910
+ ` base exportedAt: ${baseLabel}`,
5911
+ ` current exportedAt: ${result.current.exportedAt}`,
5912
+ ""
5913
+ ];
5914
+ if (result.added.nodes.length || result.added.edges.length) {
5915
+ blockLines.push("Added:");
5916
+ for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
5917
+ for (const e of result.added.edges)
5918
+ blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
5919
+ blockLines.push("");
5920
+ }
5921
+ if (result.removed.nodes.length || result.removed.edges.length) {
5922
+ blockLines.push("Removed:");
5923
+ for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
5924
+ for (const e of result.removed.edges)
5925
+ blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
5926
+ blockLines.push("");
5927
+ }
5928
+ if (result.changed.nodes.length || result.changed.edges.length) {
5929
+ blockLines.push("Changed:");
5930
+ for (const c of result.changed.nodes) {
5931
+ blockLines.push(` ~ node ${c.id} \u2014 ${summariseAttrDiff(c.before, c.after)}`);
5932
+ }
5933
+ for (const c of result.changed.edges) {
5934
+ const provBit = c.before.provenance !== c.after.provenance ? `provenance ${c.before.provenance} \u2192 ${c.after.provenance}` : summariseAttrDiff(c.before, c.after);
5935
+ blockLines.push(` ~ edge ${c.id} \u2014 ${provBit}`);
5936
+ }
5937
+ }
5938
+ return {
5939
+ summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
5940
+ block: blockLines.join("\n").trimEnd()
5941
+ };
5942
+ }
5943
+ function summariseAttrDiff(before, after) {
5944
+ const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
5945
+ const changed = [];
5946
+ for (const k of keys) {
5947
+ if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.push(k);
5948
+ }
5949
+ return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
5950
+ }
5951
+ async function runStaleEdges(client, input) {
5952
+ const params = new URLSearchParams();
5953
+ if (input.limit !== void 0) params.set("limit", String(input.limit));
5954
+ if (input.edgeType) params.set("edgeType", input.edgeType);
5955
+ const qs = params.size > 0 ? `?${params.toString()}` : "";
5956
+ const events = await client.get(
5957
+ projectPath(input.project, `/incidents/stale${qs}`)
5958
+ );
5959
+ if (events.length === 0) {
5960
+ return {
5961
+ summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
5962
+ };
5963
+ }
5964
+ const blockLines = events.map(
5965
+ (e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
5966
+ );
5967
+ return {
5968
+ summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
5969
+ block: blockLines.join("\n"),
5970
+ provenance: import_types21.Provenance.STALE
5971
+ };
5972
+ }
5973
+ async function runPolicies(client, input) {
5974
+ let violations;
5975
+ let allowed = true;
5976
+ let hypothetical;
5977
+ if (input.hypotheticalAction) {
5978
+ if (typeof client.post !== "function") {
5979
+ throw new Error("HttpClient does not support POST \u2014 required for policies dry-run");
5980
+ }
5981
+ const body = await client.post(
5982
+ projectPath(input.project, "/policies/check"),
5983
+ { hypotheticalAction: input.hypotheticalAction }
5984
+ );
5985
+ violations = body.violations;
5986
+ allowed = body.allowed;
5987
+ hypothetical = body.hypotheticalAction;
5988
+ } else {
5989
+ const params = new URLSearchParams();
5990
+ if (input.policyId) params.set("policyId", input.policyId);
5991
+ const qs = params.size > 0 ? `?${params.toString()}` : "";
5992
+ violations = await client.get(
5993
+ projectPath(input.project, `/policies/violations${qs}`)
5994
+ );
5995
+ allowed = violations.every((v) => v.onViolation !== "block");
5996
+ }
5997
+ if (input.nodeId) {
5998
+ violations = violations.filter(
5999
+ (v) => v.subject.nodeId === input.nodeId || v.subject.path?.includes(input.nodeId)
6000
+ );
6001
+ }
6002
+ if (violations.length === 0) {
6003
+ return {
6004
+ summary: hypothetical ? `No violations would result from the hypothetical action (${hypothetical.kind}).` : "No policy violations recorded."
6005
+ };
6006
+ }
6007
+ const blockCount = violations.filter((v) => v.onViolation === "block").length;
6008
+ const summaryParts = [];
6009
+ if (hypothetical) {
6010
+ summaryParts.push(
6011
+ `Hypothetical ${hypothetical.kind} would surface ${violations.length} violation${violations.length === 1 ? "" : "s"}`
6012
+ );
6013
+ } else {
6014
+ summaryParts.push(
6015
+ `${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
6016
+ );
6017
+ }
6018
+ if (blockCount > 0) summaryParts.push(`${blockCount} of which block`);
6019
+ if (!allowed && hypothetical) summaryParts.push("action denied");
6020
+ const summary = summaryParts.join("; ") + ".";
6021
+ const blockLines = violations.map((v) => {
6022
+ const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
6023
+ return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
6024
+ });
6025
+ const severities = [...new Set(violations.map((v) => v.severity))];
6026
+ return {
6027
+ summary,
6028
+ block: blockLines.join("\n"),
6029
+ confidence: hypothetical ? 0.7 : 1,
6030
+ provenance: severities.join(" ")
6031
+ };
6032
+ }
6033
+ function formatFooter(confidence, provenance) {
6034
+ const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
6035
+ const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
6036
+ return `confidence: ${c} \xB7 provenance: ${p}`;
6037
+ }
6038
+ function formatHuman(result) {
6039
+ const sections = [result.summary.trim()];
6040
+ if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
6041
+ sections.push(formatFooter(result.confidence, result.provenance));
6042
+ return sections.join("\n\n");
6043
+ }
6044
+ function formatJson(result) {
6045
+ return JSON.stringify(
6046
+ {
6047
+ summary: result.summary,
6048
+ block: result.block ?? "",
6049
+ confidence: result.confidence ?? null,
6050
+ provenance: result.provenance ?? null
6051
+ },
6052
+ null,
6053
+ 2
6054
+ );
6055
+ }
6056
+ function exitCodeForError(err) {
6057
+ if (err instanceof TransportError) return 3;
6058
+ if (err instanceof HttpError) return 1;
6059
+ return 1;
6060
+ }
6061
+
5303
6062
  // src/cli.ts
5304
6063
  function usage() {
5305
6064
  console.log("usage: neat <command> [args] [--project <name>]");
5306
6065
  console.log("");
5307
- console.log("commands:");
6066
+ console.log("lifecycle commands:");
5308
6067
  console.log(" init <path> One-time install: discover, extract, register, plan SDK install.");
5309
6068
  console.log(" Snapshot lands in <path>/neat-out/graph.json by default");
5310
6069
  console.log(" (or <path>/neat-out/<project>.json for non-default).");
@@ -5326,51 +6085,133 @@ function usage() {
5326
6085
  console.log(" --print-config print the JSON snippet to stdout");
5327
6086
  console.log(" --apply merge mcpServers.neat into ~/.claude.json");
5328
6087
  console.log("");
6088
+ console.log("query commands (mirror the MCP tools, ADR-050):");
6089
+ console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
6090
+ console.log(" example: neat root-cause service:<name>");
6091
+ console.log(" blast-radius <node-id> BFS outbound \u2014 what would break if this dies.");
6092
+ console.log(" example: neat blast-radius database:<host>");
6093
+ console.log(" dependencies <node-id> Transitive outbound dependencies.");
6094
+ console.log(" Flags: --depth N (default 3, max 10)");
6095
+ console.log(" example: neat dependencies service:<name> --depth 2");
6096
+ console.log(" observed-dependencies <node-id> OBSERVED-only outbound edges (runtime traffic).");
6097
+ console.log(" example: neat observed-dependencies service:<name>");
6098
+ console.log(" incidents [<node-id>] Recent error events; per-node when an id is given.");
6099
+ console.log(" Flags: --limit N (default 20)");
6100
+ console.log(" example: neat incidents service:<name> --limit 5");
6101
+ console.log(" search <query> Semantic (or substring) match on node names/ids.");
6102
+ console.log(' example: neat search "checkout"');
6103
+ console.log(" diff --against <snapshot> Compare the live graph to a saved snapshot.");
6104
+ console.log(" example: neat diff --against ./snapshots/baseline.json");
6105
+ console.log(" stale-edges Recent OBSERVED \u2192 STALE transitions.");
6106
+ console.log(" Flags: --limit N, --edge-type CALLS|CONNECTS_TO|...");
6107
+ console.log(" example: neat stale-edges --edge-type CALLS");
6108
+ console.log(" policies Current policy violations.");
6109
+ console.log(" Flags: --node <id>, --hypothetical-action <json>");
6110
+ console.log(" example: neat policies --node service:<name> --json");
6111
+ console.log("");
5329
6112
  console.log("flags:");
5330
6113
  console.log(' --project <name> Name the project this command targets. Default: "default".');
5331
- }
6114
+ console.log(" --json Emit machine-readable JSON instead of human text. Query verbs only.");
6115
+ console.log("");
6116
+ console.log("exit codes:");
6117
+ console.log(" 0 success");
6118
+ console.log(" 1 server error (4xx/5xx body printed to stderr)");
6119
+ console.log(" 2 misuse (missing args, bad flags) \u2014 handled before any network call");
6120
+ console.log(" 3 daemon not reachable (connection refused / timeout)");
6121
+ console.log("");
6122
+ console.log("environment:");
6123
+ console.log(" NEAT_API_URL base URL for the core REST API (default http://localhost:8080)");
6124
+ console.log(" NEAT_PROJECT project name when --project isn't passed");
6125
+ }
6126
+ var STRING_FLAGS = [
6127
+ ["--project", "project"],
6128
+ ["--depth", "depth"],
6129
+ ["--limit", "limit"],
6130
+ ["--edge-type", "edgeType"],
6131
+ ["--node", "node"],
6132
+ ["--since", "since"],
6133
+ ["--against", "against"],
6134
+ ["--error-id", "errorId"],
6135
+ ["--hypothetical-action", "hypotheticalAction"]
6136
+ ];
5332
6137
  function parseArgs(rest) {
5333
6138
  const positional = [];
5334
- let project = null;
5335
- let apply3 = false;
5336
- let dryRun = false;
5337
- let noInstall = false;
5338
- let printConfig = false;
6139
+ const out = {
6140
+ project: null,
6141
+ apply: false,
6142
+ dryRun: false,
6143
+ noInstall: false,
6144
+ printConfig: false,
6145
+ json: false,
6146
+ depth: null,
6147
+ limit: null,
6148
+ edgeType: null,
6149
+ node: null,
6150
+ since: null,
6151
+ against: null,
6152
+ errorId: null,
6153
+ hypotheticalAction: null,
6154
+ positional: []
6155
+ };
5339
6156
  for (let i = 0; i < rest.length; i++) {
5340
6157
  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
6158
  if (arg === "--apply") {
5356
- apply3 = true;
6159
+ out.apply = true;
5357
6160
  continue;
5358
6161
  }
5359
6162
  if (arg === "--dry-run") {
5360
- dryRun = true;
6163
+ out.dryRun = true;
5361
6164
  continue;
5362
6165
  }
5363
6166
  if (arg === "--no-install") {
5364
- noInstall = true;
6167
+ out.noInstall = true;
5365
6168
  continue;
5366
6169
  }
5367
6170
  if (arg === "--print-config") {
5368
- printConfig = true;
6171
+ out.printConfig = true;
6172
+ continue;
6173
+ }
6174
+ if (arg === "--json") {
6175
+ out.json = true;
5369
6176
  continue;
5370
6177
  }
6178
+ let matched = false;
6179
+ for (const [flag, field] of STRING_FLAGS) {
6180
+ if (arg === flag) {
6181
+ const next = rest[i + 1];
6182
+ if (next === void 0) {
6183
+ console.error(`neat: ${flag} requires a value`);
6184
+ process.exit(2);
6185
+ }
6186
+ assignFlag(out, field, next);
6187
+ i++;
6188
+ matched = true;
6189
+ break;
6190
+ }
6191
+ if (arg.startsWith(`${flag}=`)) {
6192
+ assignFlag(out, field, arg.slice(flag.length + 1));
6193
+ matched = true;
6194
+ break;
6195
+ }
6196
+ }
6197
+ if (matched) continue;
5371
6198
  positional.push(arg);
5372
6199
  }
5373
- return { project, apply: apply3, dryRun, noInstall, printConfig, positional };
6200
+ out.positional = positional;
6201
+ return out;
6202
+ }
6203
+ function assignFlag(out, field, value) {
6204
+ if (field === "depth" || field === "limit") {
6205
+ const n = Number(value);
6206
+ if (!Number.isFinite(n) || !Number.isInteger(n) || n < 1) {
6207
+ console.error(`neat: --${field === "depth" ? "depth" : "limit"} must be a positive integer`);
6208
+ process.exit(2);
6209
+ }
6210
+ out[field] = n;
6211
+ return;
6212
+ }
6213
+ ;
6214
+ out[field] = value;
5374
6215
  }
5375
6216
  function summarise(nodes, edges) {
5376
6217
  const byNode = /* @__PURE__ */ new Map();
@@ -5450,7 +6291,7 @@ async function buildPatchSections(services) {
5450
6291
  }
5451
6292
  async function runInit(opts) {
5452
6293
  const written = [];
5453
- const stat = await import_node_fs21.promises.stat(opts.scanPath).catch(() => null);
6294
+ const stat = await import_node_fs22.promises.stat(opts.scanPath).catch(() => null);
5454
6295
  if (!stat || !stat.isDirectory()) {
5455
6296
  console.error(`neat init: ${opts.scanPath} is not a directory`);
5456
6297
  return { exitCode: 2, writtenFiles: written };
@@ -5459,9 +6300,9 @@ async function runInit(opts) {
5459
6300
  printDiscoveryReport(opts, services);
5460
6301
  const sections = opts.noInstall ? [] : await buildPatchSections(services);
5461
6302
  const patch = renderPatch(sections);
5462
- const patchPath = import_node_path36.default.join(opts.scanPath, "neat.patch");
6303
+ const patchPath = import_node_path37.default.join(opts.scanPath, "neat.patch");
5463
6304
  if (opts.dryRun) {
5464
- await import_node_fs21.promises.writeFile(patchPath, patch, "utf8");
6305
+ await import_node_fs22.promises.writeFile(patchPath, patch, "utf8");
5465
6306
  written.push(patchPath);
5466
6307
  console.log(`dry-run: patch written to ${patchPath}`);
5467
6308
  console.log("rerun without --dry-run to register and snapshot.");
@@ -5501,7 +6342,7 @@ async function runInit(opts) {
5501
6342
  console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
5502
6343
  }
5503
6344
  } else {
5504
- await import_node_fs21.promises.writeFile(patchPath, patch, "utf8");
6345
+ await import_node_fs22.promises.writeFile(patchPath, patch, "utf8");
5505
6346
  written.push(patchPath);
5506
6347
  }
5507
6348
  }
@@ -5541,9 +6382,9 @@ var CLAUDE_SKILL_CONFIG = {
5541
6382
  };
5542
6383
  function claudeConfigPath() {
5543
6384
  const override = process.env.NEAT_CLAUDE_CONFIG;
5544
- if (override && override.length > 0) return import_node_path36.default.resolve(override);
6385
+ if (override && override.length > 0) return import_node_path37.default.resolve(override);
5545
6386
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
5546
- return import_node_path36.default.join(home, ".claude.json");
6387
+ return import_node_path37.default.join(home, ".claude.json");
5547
6388
  }
5548
6389
  async function runSkill(opts) {
5549
6390
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -5555,7 +6396,7 @@ async function runSkill(opts) {
5555
6396
  const target = claudeConfigPath();
5556
6397
  let existing = {};
5557
6398
  try {
5558
- existing = JSON.parse(await import_node_fs21.promises.readFile(target, "utf8"));
6399
+ existing = JSON.parse(await import_node_fs22.promises.readFile(target, "utf8"));
5559
6400
  } catch (err) {
5560
6401
  if (err.code !== "ENOENT") {
5561
6402
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -5567,8 +6408,8 @@ async function runSkill(opts) {
5567
6408
  ...existing,
5568
6409
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
5569
6410
  };
5570
- await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(target), { recursive: true });
5571
- await import_node_fs21.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
6411
+ await import_node_fs22.promises.mkdir(import_node_path37.default.dirname(target), { recursive: true });
6412
+ await import_node_fs22.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
5572
6413
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
5573
6414
  console.log("restart Claude Code to pick up the new MCP server.");
5574
6415
  return { exitCode: 0 };
@@ -5602,12 +6443,12 @@ async function main() {
5602
6443
  console.error("neat init: --apply and --dry-run are mutually exclusive");
5603
6444
  process.exit(2);
5604
6445
  }
5605
- const scanPath = import_node_path36.default.resolve(target);
6446
+ const scanPath = import_node_path37.default.resolve(target);
5606
6447
  const projectExplicit = parsed.project !== null;
5607
- const projectName = projectExplicit ? project : import_node_path36.default.basename(scanPath);
6448
+ const projectName = projectExplicit ? project : import_node_path37.default.basename(scanPath);
5608
6449
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
5609
- const fallback = pathsForProject(projectKey, import_node_path36.default.join(scanPath, "neat-out")).snapshotPath;
5610
- const outPath = import_node_path36.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
6450
+ const fallback = pathsForProject(projectKey, import_node_path37.default.join(scanPath, "neat-out")).snapshotPath;
6451
+ const outPath = import_node_path37.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
5611
6452
  const result = await runInit({
5612
6453
  scanPath,
5613
6454
  outPath,
@@ -5627,21 +6468,21 @@ async function main() {
5627
6468
  usage();
5628
6469
  process.exit(2);
5629
6470
  }
5630
- const scanPath = import_node_path36.default.resolve(target);
5631
- const stat = await import_node_fs21.promises.stat(scanPath).catch(() => null);
6471
+ const scanPath = import_node_path37.default.resolve(target);
6472
+ const stat = await import_node_fs22.promises.stat(scanPath).catch(() => null);
5632
6473
  if (!stat || !stat.isDirectory()) {
5633
6474
  console.error(`neat watch: ${scanPath} is not a directory`);
5634
6475
  process.exit(2);
5635
6476
  }
5636
- const projectPaths = pathsForProject(project, import_node_path36.default.join(scanPath, "neat-out"));
5637
- const outPath = import_node_path36.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
5638
- const errorsPath = import_node_path36.default.resolve(
5639
- process.env.NEAT_ERRORS_PATH ?? import_node_path36.default.join(import_node_path36.default.dirname(outPath), import_node_path36.default.basename(projectPaths.errorsPath))
6477
+ const projectPaths = pathsForProject(project, import_node_path37.default.join(scanPath, "neat-out"));
6478
+ const outPath = import_node_path37.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
6479
+ const errorsPath = import_node_path37.default.resolve(
6480
+ process.env.NEAT_ERRORS_PATH ?? import_node_path37.default.join(import_node_path37.default.dirname(outPath), import_node_path37.default.basename(projectPaths.errorsPath))
5640
6481
  );
5641
- const staleEventsPath = import_node_path36.default.resolve(
5642
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path36.default.join(import_node_path36.default.dirname(outPath), import_node_path36.default.basename(projectPaths.staleEventsPath))
6482
+ const staleEventsPath = import_node_path37.default.resolve(
6483
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path37.default.join(import_node_path37.default.dirname(outPath), import_node_path37.default.basename(projectPaths.staleEventsPath))
5643
6484
  );
5644
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path36.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
6485
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path37.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
5645
6486
  const handle = await startWatch(getGraph(project), {
5646
6487
  scanPath,
5647
6488
  outPath,
@@ -5734,10 +6575,167 @@ async function main() {
5734
6575
  console.log("note: neat-out/, policy.json, and other files at the project path were left in place.");
5735
6576
  return;
5736
6577
  }
6578
+ if (QUERY_VERBS.has(cmd)) {
6579
+ const code = await runQueryVerb(cmd, parsed);
6580
+ if (code !== 0) process.exit(code);
6581
+ return;
6582
+ }
5737
6583
  console.error(`neat: unknown command "${cmd}"`);
5738
6584
  usage();
5739
6585
  process.exit(1);
5740
6586
  }
6587
+ var QUERY_VERBS = /* @__PURE__ */ new Set([
6588
+ "root-cause",
6589
+ "blast-radius",
6590
+ "dependencies",
6591
+ "observed-dependencies",
6592
+ "incidents",
6593
+ "search",
6594
+ "diff",
6595
+ "stale-edges",
6596
+ "policies"
6597
+ ]);
6598
+ function resolveProjectFlag(parsed) {
6599
+ if (parsed.project) return parsed.project;
6600
+ const env = process.env.NEAT_PROJECT;
6601
+ if (env && env.length > 0 && env !== DEFAULT_PROJECT) return env;
6602
+ return void 0;
6603
+ }
6604
+ async function runQueryVerb(cmd, parsed) {
6605
+ const baseUrl = process.env.NEAT_API_URL ?? "http://localhost:8080";
6606
+ const client = createHttpClient(baseUrl);
6607
+ const project = resolveProjectFlag(parsed);
6608
+ const positional = parsed.positional;
6609
+ let work;
6610
+ switch (cmd) {
6611
+ case "root-cause": {
6612
+ const node = positional[0];
6613
+ if (!node) {
6614
+ console.error("neat root-cause: missing <node-id>");
6615
+ return 2;
6616
+ }
6617
+ work = runRootCause(client, {
6618
+ errorNode: node,
6619
+ ...parsed.errorId ? { errorId: parsed.errorId } : {},
6620
+ ...project ? { project } : {}
6621
+ });
6622
+ break;
6623
+ }
6624
+ case "blast-radius": {
6625
+ const node = positional[0];
6626
+ if (!node) {
6627
+ console.error("neat blast-radius: missing <node-id>");
6628
+ return 2;
6629
+ }
6630
+ work = runBlastRadius(client, {
6631
+ nodeId: node,
6632
+ ...parsed.depth !== null ? { depth: parsed.depth } : {},
6633
+ ...project ? { project } : {}
6634
+ });
6635
+ break;
6636
+ }
6637
+ case "dependencies": {
6638
+ const node = positional[0];
6639
+ if (!node) {
6640
+ console.error("neat dependencies: missing <node-id>");
6641
+ return 2;
6642
+ }
6643
+ work = runDependencies(client, {
6644
+ nodeId: node,
6645
+ ...parsed.depth !== null ? { depth: parsed.depth } : {},
6646
+ ...project ? { project } : {}
6647
+ });
6648
+ break;
6649
+ }
6650
+ case "observed-dependencies": {
6651
+ const node = positional[0];
6652
+ if (!node) {
6653
+ console.error("neat observed-dependencies: missing <node-id>");
6654
+ return 2;
6655
+ }
6656
+ work = runObservedDependencies(client, {
6657
+ nodeId: node,
6658
+ ...project ? { project } : {}
6659
+ });
6660
+ break;
6661
+ }
6662
+ case "incidents": {
6663
+ work = runIncidents(client, {
6664
+ ...positional[0] ? { nodeId: positional[0] } : {},
6665
+ ...parsed.limit !== null ? { limit: parsed.limit } : {},
6666
+ ...project ? { project } : {}
6667
+ });
6668
+ break;
6669
+ }
6670
+ case "search": {
6671
+ const q = positional.join(" ").trim();
6672
+ if (!q) {
6673
+ console.error("neat search: missing <query>");
6674
+ return 2;
6675
+ }
6676
+ work = runSearch(client, { query: q, ...project ? { project } : {} });
6677
+ break;
6678
+ }
6679
+ case "diff": {
6680
+ const against = parsed.against ?? parsed.since;
6681
+ if (!against) {
6682
+ console.error("neat diff: --against <snapshot-path> is required");
6683
+ return 2;
6684
+ }
6685
+ work = runDiff(client, {
6686
+ againstSnapshot: against,
6687
+ ...project ? { project } : {}
6688
+ });
6689
+ break;
6690
+ }
6691
+ case "stale-edges": {
6692
+ work = runStaleEdges(client, {
6693
+ ...parsed.limit !== null ? { limit: parsed.limit } : {},
6694
+ ...parsed.edgeType ? { edgeType: parsed.edgeType } : {},
6695
+ ...project ? { project } : {}
6696
+ });
6697
+ break;
6698
+ }
6699
+ case "policies": {
6700
+ let hypothetical;
6701
+ if (parsed.hypotheticalAction) {
6702
+ try {
6703
+ hypothetical = JSON.parse(parsed.hypotheticalAction);
6704
+ } catch (err) {
6705
+ console.error(
6706
+ `neat policies: --hypothetical-action must be valid JSON: ${err.message}`
6707
+ );
6708
+ return 2;
6709
+ }
6710
+ }
6711
+ work = runPolicies(client, {
6712
+ ...parsed.node ? { nodeId: parsed.node } : {},
6713
+ ...hypothetical ? { hypotheticalAction: hypothetical } : {},
6714
+ ...project ? { project } : {}
6715
+ });
6716
+ break;
6717
+ }
6718
+ default:
6719
+ console.error(`neat: unknown query verb "${cmd}"`);
6720
+ return 2;
6721
+ }
6722
+ try {
6723
+ const result = await work;
6724
+ if (parsed.json) process.stdout.write(formatJson(result) + "\n");
6725
+ else process.stdout.write(formatHuman(result) + "\n");
6726
+ return 0;
6727
+ } catch (err) {
6728
+ if (err instanceof HttpError) {
6729
+ const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
6730
+ console.error(`neat ${cmd}: ${detail.trim()}`);
6731
+ } else if (err instanceof TransportError) {
6732
+ console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${process.env.NEAT_API_URL ?? "http://localhost:8080"})`);
6733
+ } else {
6734
+ console.error(`neat ${cmd}: ${err.message}`);
6735
+ }
6736
+ return exitCodeForError(err);
6737
+ }
6738
+ }
5741
6739
  var entry = process.argv[1] ?? "";
5742
6740
  if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsWith("/neat")) {
5743
6741
  main().catch((err) => {
@@ -5748,7 +6746,10 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
5748
6746
  // Annotate the CommonJS export names for ESM import in node:
5749
6747
  0 && (module.exports = {
5750
6748
  CLAUDE_SKILL_CONFIG,
6749
+ QUERY_VERBS,
6750
+ parseArgs,
5751
6751
  runInit,
6752
+ runQueryVerb,
5752
6753
  runSkill
5753
6754
  });
5754
6755
  //# sourceMappingURL=cli.cjs.map