@neat.is/core 0.6.2-dev.20260723 → 0.6.2

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
@@ -706,6 +706,7 @@ __export(cli_exports, {
706
706
  module.exports = __toCommonJS(cli_exports);
707
707
  init_cjs_shims();
708
708
  var import_node_path63 = __toESM(require("path"), 1);
709
+ var import_node_os6 = __toESM(require("os"), 1);
709
710
  var import_node_fs41 = require("fs");
710
711
 
711
712
  // src/banner.ts
@@ -5689,7 +5690,7 @@ function keywordArrayStrings(argsNode, key) {
5689
5690
  }
5690
5691
  return [];
5691
5692
  }
5692
- function collectApiRouterPrefixes(root) {
5693
+ function collectPythonRouterPrefixes(root) {
5693
5694
  const prefixes = /* @__PURE__ */ new Map();
5694
5695
  walk(root, (node) => {
5695
5696
  if (node.type !== "assignment") return;
@@ -5698,7 +5699,8 @@ function collectApiRouterPrefixes(root) {
5698
5699
  const fn = right.childForFieldName("function");
5699
5700
  if (!fn) return;
5700
5701
  const ctor = fn.type === "attribute" ? fn.childForFieldName("attribute")?.text : fn.text;
5701
- if (ctor !== "APIRouter") return;
5702
+ const prefixKey = ctor === "APIRouter" ? "prefix" : ctor === "Blueprint" ? "url_prefix" : null;
5703
+ if (!prefixKey) return;
5702
5704
  const left = node.childForFieldName("left");
5703
5705
  if (!left || left.type !== "identifier") return;
5704
5706
  const args = right.childForFieldName("arguments");
@@ -5706,7 +5708,7 @@ function collectApiRouterPrefixes(root) {
5706
5708
  for (let i = 0; i < args.namedChildCount; i++) {
5707
5709
  const arg = args.namedChild(i);
5708
5710
  if (arg?.type !== "keyword_argument") continue;
5709
- if (arg.childForFieldName("name")?.text !== "prefix") continue;
5711
+ if (arg.childForFieldName("name")?.text !== prefixKey) continue;
5710
5712
  const val = arg.childForFieldName("value");
5711
5713
  const p = val ? pyStaticStringText(val) : null;
5712
5714
  if (p !== null) prefixes.set(left.text, p);
@@ -5714,9 +5716,9 @@ function collectApiRouterPrefixes(root) {
5714
5716
  });
5715
5717
  return prefixes;
5716
5718
  }
5717
- function fastapiRoutesFromSource(source, parser) {
5719
+ function pythonRoutesFromSource(source, parser, framework) {
5718
5720
  const tree = parseSource2(parser, source);
5719
- const prefixes = collectApiRouterPrefixes(tree.rootNode);
5721
+ const prefixes = collectPythonRouterPrefixes(tree.rootNode);
5720
5722
  const out = [];
5721
5723
  walk(tree.rootNode, (node) => {
5722
5724
  if (node.type !== "decorator") return;
@@ -5727,7 +5729,9 @@ function fastapiRoutesFromSource(source, parser) {
5727
5729
  const method = fn.childForFieldName("attribute")?.text?.toLowerCase();
5728
5730
  if (!method) return;
5729
5731
  const isVerb = FASTAPI_METHODS.has(method);
5730
- if (!isVerb && method !== "api_route") return;
5732
+ const isFlaskRoute = method === "route";
5733
+ const isApiRoute = method === "api_route";
5734
+ if (!isVerb && !isFlaskRoute && !isApiRoute) return;
5731
5735
  const args = call.childForFieldName("arguments");
5732
5736
  const first = args?.namedChild(0);
5733
5737
  if (!first || first.type !== "string") return;
@@ -5738,17 +5742,41 @@ function fastapiRoutesFromSource(source, parser) {
5738
5742
  const pathTemplate = canonicalizeTemplate(prefix + rawPath);
5739
5743
  const line = node.startPosition.row + 1;
5740
5744
  if (isVerb) {
5741
- out.push({ method: method.toUpperCase(), pathTemplate, line, framework: "fastapi" });
5745
+ out.push({ method: method.toUpperCase(), pathTemplate, line, framework });
5742
5746
  return;
5743
5747
  }
5744
5748
  const methods = keywordArrayStrings(args, "methods");
5745
- const list = methods.length > 0 ? methods : ["ALL"];
5749
+ const list = methods.length > 0 ? methods : isFlaskRoute ? ["GET"] : ["ALL"];
5746
5750
  for (const m of list) {
5751
+ out.push({ method: m === "ALL" ? "ALL" : m.toUpperCase(), pathTemplate, line, framework });
5752
+ }
5753
+ });
5754
+ return out;
5755
+ }
5756
+ function djangoRoutesFromSource(source, parser) {
5757
+ const tree = parseSource2(parser, source);
5758
+ const out = [];
5759
+ walk(tree.rootNode, (node) => {
5760
+ if (node.type !== "assignment") return;
5761
+ if (node.childForFieldName("left")?.text !== "urlpatterns") return;
5762
+ const list = node.childForFieldName("right");
5763
+ if (!list || list.type !== "list") return;
5764
+ for (let i = 0; i < list.namedChildCount; i++) {
5765
+ const el = list.namedChild(i);
5766
+ if (el?.type !== "call") continue;
5767
+ if (el.childForFieldName("function")?.text !== "path") continue;
5768
+ const args = el.childForFieldName("arguments");
5769
+ const first = args?.namedChild(0);
5770
+ if (first?.type !== "string") continue;
5771
+ const raw = pyStaticStringText(first);
5772
+ if (raw === null) continue;
5773
+ const second = args?.namedChild(1);
5774
+ if (second?.type === "call" && second.childForFieldName("function")?.text === "include") continue;
5747
5775
  out.push({
5748
- method: m === "ALL" ? "ALL" : m.toUpperCase(),
5749
- pathTemplate,
5750
- line,
5751
- framework: "fastapi"
5776
+ method: "ALL",
5777
+ pathTemplate: canonicalizeTemplate(raw),
5778
+ line: el.startPosition.row + 1,
5779
+ framework: "django"
5752
5780
  });
5753
5781
  }
5754
5782
  });
@@ -5769,7 +5797,10 @@ async function addRoutes(graph, services) {
5769
5797
  const hasHono = deps["hono"] !== void 0;
5770
5798
  const hasNext = deps["next"] !== void 0;
5771
5799
  const hasFastapi = deps["fastapi"] !== void 0;
5772
- if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasFastapi) continue;
5800
+ const hasFlask = deps["flask"] !== void 0;
5801
+ const hasDjango = deps["django"] !== void 0;
5802
+ if (!hasExpress && !hasFastify && !hasHono && !hasNext && !hasFastapi && !hasFlask && !hasDjango)
5803
+ continue;
5773
5804
  const files = await loadSourceFiles(service.dir);
5774
5805
  for (const file of files) {
5775
5806
  if (isTestPath(file.path)) continue;
@@ -5780,7 +5811,8 @@ async function addRoutes(graph, services) {
5780
5811
  let routes;
5781
5812
  try {
5782
5813
  if (isPy) {
5783
- routes = hasFastapi ? fastapiRoutesFromSource(file.content, pyParser) : [];
5814
+ routes = hasFastapi || hasFlask ? pythonRoutesFromSource(file.content, pyParser, hasFastapi ? "fastapi" : "flask") : [];
5815
+ if (hasDjango) routes = routes.concat(djangoRoutesFromSource(file.content, pyParser));
5784
5816
  } else if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
5785
5817
  routes = nextRoutesFromFile(file.content, relFile, jsParser);
5786
5818
  } else if (hasExpress || hasFastify || hasHono) {
@@ -9946,6 +9978,22 @@ function describeCredential(ref, env = process.env) {
9946
9978
  init_cjs_shims();
9947
9979
  var CONNECTOR_STALE_THRESHOLD_MS = 5 * 6e4;
9948
9980
  var records = /* @__PURE__ */ new Map();
9981
+ var MAX_ERROR_LEN = 200;
9982
+ function sanitizePollError(err) {
9983
+ const msg = err instanceof Error ? err.message : String(err);
9984
+ const collapsed = msg.replace(/\s+/g, " ").trim();
9985
+ return collapsed.length > MAX_ERROR_LEN ? `${collapsed.slice(0, MAX_ERROR_LEN - 1)}\u2026` : collapsed;
9986
+ }
9987
+ function recordConnectorPoll(id, tick) {
9988
+ const prev = records.get(id);
9989
+ records.set(id, {
9990
+ lastPollAt: tick.at,
9991
+ lastOutcome: tick.outcome,
9992
+ lastError: tick.outcome === "error" ? tick.error ?? null : null,
9993
+ signalsLastPoll: tick.signalsLastPoll ?? 0,
9994
+ lastOkAt: tick.outcome === "ok" ? tick.at : prev?.lastOkAt ?? null
9995
+ });
9996
+ }
9949
9997
  function deriveState(rec, now, thresholdMs) {
9950
9998
  if (rec.lastOutcome === "error") return "error";
9951
9999
  const okAt = rec.lastOkAt ? Date.parse(rec.lastOkAt) : NaN;
@@ -10736,19 +10784,128 @@ async function buildApi(opts) {
10736
10784
  init_auth();
10737
10785
  init_otel();
10738
10786
 
10739
- // src/daemon.ts
10787
+ // src/connectors/registry.ts
10740
10788
  init_cjs_shims();
10741
- var import_node_fs33 = require("fs");
10742
- var import_node_path54 = __toESM(require("path"), 1);
10743
- var import_node_module = require("module");
10744
- init_otel();
10745
10789
 
10746
10790
  // src/connectors/index.ts
10747
10791
  init_cjs_shims();
10748
10792
  var import_types37 = require("@neat.is/types");
10749
-
10750
- // src/connectors/registry.ts
10751
- init_cjs_shims();
10793
+ var NO_ENV = "unknown";
10794
+ function staticCallSiteFor(graph, serviceName, targetNodeId) {
10795
+ if (!graph.hasNode(targetNodeId)) return void 0;
10796
+ const sites = [];
10797
+ for (const edgeId of graph.inboundEdges(targetNodeId)) {
10798
+ const edge = graph.getEdgeAttributes(edgeId);
10799
+ if (edge.provenance !== import_types37.Provenance.EXTRACTED) continue;
10800
+ const parsed = (0, import_types37.parseFileId)(edge.source);
10801
+ if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
10802
+ const site = { relPath: edge.evidence.file };
10803
+ if (edge.evidence.line !== void 0) site.line = edge.evidence.line;
10804
+ sites.push(site);
10805
+ }
10806
+ return sites.length === 1 ? sites[0] : void 0;
10807
+ }
10808
+ function routeCallSiteFor(graph, targetNodeId) {
10809
+ if (!graph.hasNode(targetNodeId)) return void 0;
10810
+ const attrs = graph.getNodeAttributes(targetNodeId);
10811
+ if (attrs.type !== import_types37.NodeType.RouteNode || !attrs.path) return void 0;
10812
+ const site = { relPath: attrs.path };
10813
+ if (attrs.line !== void 0) site.line = attrs.line;
10814
+ return site;
10815
+ }
10816
+ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
10817
+ const signals = await connector.poll(ctx);
10818
+ let edgesCreated = 0;
10819
+ let edgesUpdated = 0;
10820
+ let unresolved = 0;
10821
+ for (const signal of signals) {
10822
+ const resolved = resolveTarget(signal, ctx);
10823
+ if (!resolved) {
10824
+ unresolved++;
10825
+ continue;
10826
+ }
10827
+ if (resolved.ensureInfraNode) {
10828
+ const { kind, name, provider } = resolved.ensureInfraNode;
10829
+ ensureInfraNode(graph, kind, name, provider);
10830
+ }
10831
+ const serviceNodeId = ensureServiceNode(graph, resolved.serviceName, NO_ENV);
10832
+ const callSite = signal.callSite ? { relPath: signal.callSite.file, line: signal.callSite.line } : routeCallSiteFor(graph, resolved.targetNodeId) ?? staticCallSiteFor(graph, resolved.serviceName, resolved.targetNodeId);
10833
+ const sourceId = callSite ? ensureObservedFileNode(graph, resolved.serviceName, serviceNodeId, callSite) : serviceNodeId;
10834
+ const evidence = callSite ? {
10835
+ file: reconcileObservedRelPath(graph, resolved.serviceName, callSite.relPath),
10836
+ line: callSite.line
10837
+ } : void 0;
10838
+ const calls = Math.trunc(signal.callCount);
10839
+ if (!Number.isFinite(calls) || calls < 1) continue;
10840
+ const errors = Number.isFinite(signal.errorCount) ? Math.min(Math.max(Math.trunc(signal.errorCount), 0), calls) : 0;
10841
+ let created = false;
10842
+ let ok = true;
10843
+ for (let i = 0; i < calls; i++) {
10844
+ const result = upsertObservedEdge(
10845
+ graph,
10846
+ resolved.edgeType,
10847
+ sourceId,
10848
+ resolved.targetNodeId,
10849
+ signal.lastObservedIso,
10850
+ i < errors,
10851
+ evidence
10852
+ );
10853
+ if (!result) {
10854
+ ok = false;
10855
+ break;
10856
+ }
10857
+ if (i === 0) created = result.created;
10858
+ }
10859
+ if (!ok) {
10860
+ unresolved++;
10861
+ continue;
10862
+ }
10863
+ if (created) edgesCreated++;
10864
+ else edgesUpdated++;
10865
+ }
10866
+ return { signalCount: signals.length, edgesCreated, edgesUpdated, unresolved };
10867
+ }
10868
+ var DEFAULT_POLL_INTERVAL_MS = 6e4;
10869
+ function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options = {}) {
10870
+ let stopped = false;
10871
+ let since = ctx.since;
10872
+ const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
10873
+ const connectorId = options.connectorId;
10874
+ const onError = options.onError ?? ((err) => console.error(`[neatd] connector poll failed (${connector.provider})`, err));
10875
+ const tick = () => {
10876
+ if (stopped) return;
10877
+ void (async () => {
10878
+ const tickStartedAt = (/* @__PURE__ */ new Date()).toISOString();
10879
+ try {
10880
+ const result = await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget);
10881
+ since = tickStartedAt;
10882
+ if (connectorId) {
10883
+ recordConnectorPoll(connectorId, {
10884
+ outcome: "ok",
10885
+ at: tickStartedAt,
10886
+ signalsLastPoll: result.signalCount
10887
+ });
10888
+ }
10889
+ } catch (err) {
10890
+ onError(err);
10891
+ if (connectorId) {
10892
+ recordConnectorPoll(connectorId, {
10893
+ outcome: "error",
10894
+ at: tickStartedAt,
10895
+ error: sanitizePollError(err)
10896
+ });
10897
+ }
10898
+ }
10899
+ })();
10900
+ };
10901
+ tick();
10902
+ const interval = setInterval(tick, intervalMs);
10903
+ if (typeof interval.unref === "function") interval.unref();
10904
+ return () => {
10905
+ stopped = true;
10906
+ clearInterval(interval);
10907
+ };
10908
+ }
10752
10909
 
10753
10910
  // src/connectors/junction.ts
10754
10911
  init_cjs_shims();
@@ -11357,17 +11514,22 @@ function readRailwayToken(credentials) {
11357
11514
  }
11358
11515
  return token;
11359
11516
  }
11360
- function projectAccessTokenHeader(token) {
11361
- return { "Project-Access-Token": token };
11517
+ var RAILWAY_AUTH_STYLES = ["bearer", "project-access-token"];
11518
+ function railwayAuthHeader(style, token) {
11519
+ return style === "bearer" ? { Authorization: `Bearer ${token}` } : { "Project-Access-Token": token };
11362
11520
  }
11363
- async function railwayGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
11521
+ var resolvedRailwayAuthStyle = /* @__PURE__ */ new Map();
11522
+ function isRailwayNotAuthorized(err) {
11523
+ return err instanceof Error && /not authorized/i.test(err.message);
11524
+ }
11525
+ async function railwayGraphQLOnce(apiUrl, style, token, query, variables, accountKey, fetchImpl) {
11364
11526
  const res = await junctionFetch(
11365
11527
  apiUrl,
11366
11528
  {
11367
11529
  method: "POST",
11368
11530
  headers: {
11369
11531
  "Content-Type": "application/json",
11370
- ...projectAccessTokenHeader(token)
11532
+ ...railwayAuthHeader(style, token)
11371
11533
  },
11372
11534
  body: JSON.stringify({ query, variables })
11373
11535
  },
@@ -11383,6 +11545,26 @@ async function railwayGraphQL(apiUrl, token, query, variables, accountKey, fetch
11383
11545
  if (!body.data) throw new Error("Railway GraphQL response carried no data");
11384
11546
  return body.data;
11385
11547
  }
11548
+ async function railwayGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
11549
+ const known = resolvedRailwayAuthStyle.get(token);
11550
+ const styles = known ? [known] : RAILWAY_AUTH_STYLES;
11551
+ let lastNotAuthorized;
11552
+ for (let i = 0; i < styles.length; i++) {
11553
+ const style = styles[i];
11554
+ try {
11555
+ const data = await railwayGraphQLOnce(apiUrl, style, token, query, variables, accountKey, fetchImpl);
11556
+ resolvedRailwayAuthStyle.set(token, style);
11557
+ return data;
11558
+ } catch (err) {
11559
+ if (isRailwayNotAuthorized(err) && i < styles.length - 1) {
11560
+ lastNotAuthorized = err;
11561
+ continue;
11562
+ }
11563
+ throw err;
11564
+ }
11565
+ }
11566
+ throw lastNotAuthorized ?? new Error("Railway GraphQL: no auth style resolved");
11567
+ }
11386
11568
  var HTTP_LOGS_QUERY = `
11387
11569
  query HttpLogs($deploymentId: String!, $startDate: String, $endDate: String, $limit: Int) {
11388
11570
  httpLogs(deploymentId: $deploymentId, startDate: $startDate, endDate: $endDate, limit: $limit) {
@@ -12431,6 +12613,49 @@ function resolveEntryCredentials(dispatch, entry2, env) {
12431
12613
  }
12432
12614
  return { ok: true, credentials };
12433
12615
  }
12616
+ function buildRegistration(entry2, graph, env = process.env) {
12617
+ const dispatch = PROVIDER_DISPATCH[entry2.provider];
12618
+ if (!dispatch) {
12619
+ if (isPushProvider(entry2.provider)) {
12620
+ return {
12621
+ ok: false,
12622
+ push: true,
12623
+ reason: `push provider "${entry2.provider}" ingests via the OTLP receiver \u2014 nothing to poll`
12624
+ };
12625
+ }
12626
+ return { ok: false, reason: `unknown provider "${entry2.provider}"` };
12627
+ }
12628
+ const creds = resolveEntryCredentials(dispatch, entry2, env);
12629
+ if (!creds.ok) return { ok: false, reason: creds.reason };
12630
+ const credentials = creds.credentials;
12631
+ const options = entry2.options ?? {};
12632
+ const missingOpts = dispatch.requiredOptionFields.filter((k) => !(k in options));
12633
+ if (missingOpts.length > 0) {
12634
+ return {
12635
+ ok: false,
12636
+ reason: `options missing required field(s): ${missingOpts.join(", ")}`
12637
+ };
12638
+ }
12639
+ let built;
12640
+ try {
12641
+ built = dispatch.build(graph, options);
12642
+ } catch (err) {
12643
+ return { ok: false, reason: err.message };
12644
+ }
12645
+ const intervalMs = typeof options.intervalMs === "number" ? options.intervalMs : void 0;
12646
+ return {
12647
+ ok: true,
12648
+ registration: {
12649
+ // Carry the entry id so the daemon can key this connector's poll-status
12650
+ // records to it (ADR-136).
12651
+ id: entry2.id,
12652
+ connector: built.connector,
12653
+ credentials,
12654
+ resolveTarget: built.resolveTarget,
12655
+ ...intervalMs !== void 0 ? { intervalMs } : {}
12656
+ }
12657
+ };
12658
+ }
12434
12659
  async function validateConnectorEntry(entry2, env = process.env, fetchImpl) {
12435
12660
  const dispatch = PROVIDER_DISPATCH[entry2.provider] ?? PUSH_PROVIDER_DISPATCH[entry2.provider];
12436
12661
  if (!dispatch) {
@@ -12456,6 +12681,48 @@ async function validateConnectorEntry(entry2, env = process.env, fetchImpl) {
12456
12681
  });
12457
12682
  return result.ok ? { status: "ok" } : { status: "rejected", reason: result.reason };
12458
12683
  }
12684
+ async function loadConnectorRegistrations(input) {
12685
+ const { project, graph, home, env = process.env, onSkip } = input;
12686
+ let connectors;
12687
+ try {
12688
+ connectors = (await readConnectorsConfig(home)).connectors;
12689
+ } catch (err) {
12690
+ onSkip?.(
12691
+ { id: "(file)", provider: "(all)", credential: "" },
12692
+ `connectors.json unreadable \u2014 ${err.message}`
12693
+ );
12694
+ return [];
12695
+ }
12696
+ const registrations = [];
12697
+ for (const entry2 of connectors) {
12698
+ if (!connectorMatchesProject(entry2, project)) continue;
12699
+ const result = buildRegistration(entry2, graph, env);
12700
+ if (result.ok) registrations.push(result.registration);
12701
+ else if (!result.push) onSkip?.(entry2, result.reason);
12702
+ }
12703
+ return registrations;
12704
+ }
12705
+ async function startConnectorPolling(input) {
12706
+ const fileConnectors = input.home ? await loadConnectorRegistrations({
12707
+ project: input.project,
12708
+ graph: input.graph,
12709
+ home: input.home,
12710
+ ...input.onSkip ? { onSkip: input.onSkip } : {}
12711
+ }) : [];
12712
+ const all = [...input.extra ?? [], ...fileConnectors];
12713
+ const stopFns = all.map(
12714
+ (registration) => startConnectorPollLoop(
12715
+ registration.connector,
12716
+ { projectDir: input.projectDir, credentials: registration.credentials },
12717
+ input.graph,
12718
+ registration.resolveTarget,
12719
+ { intervalMs: registration.intervalMs, connectorId: registration.id }
12720
+ )
12721
+ );
12722
+ return () => {
12723
+ for (const stop of stopFns) stop();
12724
+ };
12725
+ }
12459
12726
  function resolvePushEntry(entry2, env) {
12460
12727
  const dispatch = PUSH_PROVIDER_DISPATCH[entry2.provider];
12461
12728
  if (!dispatch) {
@@ -12506,6 +12773,11 @@ async function deprovisionConnector(entry2, env = process.env, fetchImpl) {
12506
12773
  }
12507
12774
 
12508
12775
  // src/daemon.ts
12776
+ init_cjs_shims();
12777
+ var import_node_fs33 = require("fs");
12778
+ var import_node_path54 = __toESM(require("path"), 1);
12779
+ var import_node_module = require("module");
12780
+ init_otel();
12509
12781
  init_auth();
12510
12782
 
12511
12783
  // src/unrouted.ts
@@ -13120,6 +13392,15 @@ async function startWatch(graph, opts) {
13120
13392
  project: projectName,
13121
13393
  onPolicyTrigger
13122
13394
  });
13395
+ const stopConnectors = await startConnectorPolling({
13396
+ project: projectName,
13397
+ graph,
13398
+ projectDir: opts.scanPath,
13399
+ ...opts.neatHome ? { home: opts.neatHome } : {},
13400
+ onSkip: (skipped, reason) => console.warn(
13401
+ `neat watch: connector "${skipped.id}" (${skipped.provider}) skipped for project "${projectName}" \u2014 ${reason}`
13402
+ )
13403
+ });
13123
13404
  const auth = readAuthEnv();
13124
13405
  const host = opts.host ?? (auth.authToken ? "0.0.0.0" : "127.0.0.1");
13125
13406
  assertBindAuthority(host, auth.authToken);
@@ -13305,6 +13586,7 @@ async function startWatch(graph, opts) {
13305
13586
  }
13306
13587
  }
13307
13588
  await watcher.close();
13589
+ stopConnectors();
13308
13590
  stopStaleness();
13309
13591
  stopPersist();
13310
13592
  detachEventBus();
@@ -18247,6 +18529,9 @@ async function main() {
18247
18529
  errorsPath,
18248
18530
  staleEventsPath,
18249
18531
  project,
18532
+ // Resolve NEAT_HOME so a `neat watch` picks up connectors added to
18533
+ // ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
18534
+ neatHome: process.env.NEAT_HOME ? import_node_path63.default.resolve(process.env.NEAT_HOME) : import_node_path63.default.join(import_node_os6.default.homedir(), ".neat"),
18250
18535
  ...embeddingsCachePath ? { embeddingsCachePath } : {},
18251
18536
  host: process.env.HOST ?? "0.0.0.0",
18252
18537
  port: Number(process.env.PORT ?? 8080),