@neat.is/core 0.4.20-dev.20260629 → 0.4.22

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.d.cts CHANGED
@@ -78,6 +78,7 @@ declare class ProjectResolutionError extends Error {
78
78
  constructor(message: string, exitCode?: number);
79
79
  }
80
80
  declare function resolveProjectForVerb(client: HttpClient, parsed: ParsedArgs): Promise<string | undefined>;
81
+ declare function resolveDaemonUrl(project?: string): Promise<string>;
81
82
  declare function runQueryVerb(cmd: string, parsed: ParsedArgs): Promise<number>;
82
83
 
83
- export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveProjectForVerb, runInit, runQueryVerb, runSkill, usage };
84
+ export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveDaemonUrl, resolveProjectForVerb, runInit, runQueryVerb, runSkill, usage };
package/dist/cli.d.ts CHANGED
@@ -78,6 +78,7 @@ declare class ProjectResolutionError extends Error {
78
78
  constructor(message: string, exitCode?: number);
79
79
  }
80
80
  declare function resolveProjectForVerb(client: HttpClient, parsed: ParsedArgs): Promise<string | undefined>;
81
+ declare function resolveDaemonUrl(project?: string): Promise<string>;
81
82
  declare function runQueryVerb(cmd: string, parsed: ParsedArgs): Promise<number>;
82
83
 
83
- export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveProjectForVerb, runInit, runQueryVerb, runSkill, usage };
84
+ export { CLAUDE_SKILL_CONFIG, type InitOptions, type InitResult, ProjectResolutionError, QUERY_VERBS, type SkillOptions, commandPrefix, isNpxInvocation, main, parseArgs, printBanner, readPackageVersion, resolveDaemonUrl, resolveProjectForVerb, runInit, runQueryVerb, runSkill, usage };
package/dist/cli.js CHANGED
@@ -1,7 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- readDaemonRecord
4
- } from "./chunk-62ELQVIP.js";
3
+ clearDaemonRecord,
4
+ portFromListenAddress,
5
+ readDaemonRecord,
6
+ resolveHost,
7
+ resolveNeatVersion,
8
+ writeDaemonRecord
9
+ } from "./chunk-XRE4OQWJ.js";
5
10
  import {
6
11
  buildSearchIndex
7
12
  } from "./chunk-XOOCA5T7.js";
@@ -14,6 +19,7 @@ import {
14
19
  addCallEdges,
15
20
  addConfigNodes,
16
21
  addDatabasesAndCompat,
22
+ addFiles,
17
23
  addImports,
18
24
  addInfra,
19
25
  addProject,
@@ -53,17 +59,18 @@ import {
53
59
  signalDaemonStop,
54
60
  startPersistLoop,
55
61
  startStalenessLoop
56
- } from "./chunk-GNAX2CBF.js";
62
+ } from "./chunk-OQ7CHOY2.js";
57
63
  import {
58
64
  startOtelGrpcReceiver
59
- } from "./chunk-MTXF77TN.js";
65
+ } from "./chunk-GAFTW2OX.js";
60
66
  import {
61
67
  __dirname,
62
68
  __require,
63
69
  assertBindAuthority,
64
70
  buildOtelReceiver,
71
+ listenSteppingOtlp,
65
72
  readAuthEnv
66
- } from "./chunk-BGPWBRLU.js";
73
+ } from "./chunk-ZX7PCMGZ.js";
67
74
 
68
75
  // src/cli.ts
69
76
  import path9 from "path";
@@ -241,6 +248,7 @@ import chokidar from "chokidar";
241
248
  var ALL_PHASES = [
242
249
  "services",
243
250
  "aliases",
251
+ "files",
244
252
  "imports",
245
253
  "databases",
246
254
  "configs",
@@ -265,6 +273,7 @@ function classifyChange(relPath) {
265
273
  phases.add("aliases");
266
274
  }
267
275
  if (/\.(?:js|jsx|mjs|cjs|ts|tsx|py)$/.test(base)) {
276
+ phases.add("files");
268
277
  phases.add("imports");
269
278
  phases.add("calls");
270
279
  }
@@ -287,6 +296,11 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
287
296
  if (phases.has("aliases")) {
288
297
  await addServiceAliases(graph, scanPath, services);
289
298
  }
299
+ if (phases.has("files")) {
300
+ const r = await addFiles(graph, services);
301
+ nodesAdded += r.nodesAdded;
302
+ edgesAdded += r.edgesAdded;
303
+ }
290
304
  if (phases.has("imports")) {
291
305
  const r = await addImports(graph, services);
292
306
  nodesAdded += r.nodesAdded;
@@ -321,6 +335,7 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
321
335
  durationMs: Date.now() - started
322
336
  };
323
337
  }
338
+ var DEFAULT_WATCH_WEB_PORT = 6328;
324
339
  var IGNORED_WATCH_GLOBS = [
325
340
  "**/node_modules/**",
326
341
  "**/.git/**",
@@ -469,7 +484,7 @@ async function startWatch(graph, opts) {
469
484
  searchIndex
470
485
  });
471
486
  const api = await buildApi({ projects: registry });
472
- await api.listen({ port, host });
487
+ const restAddress = await api.listen({ port, host });
473
488
  console.log(`neat-core listening on http://${host}:${port}`);
474
489
  console.log(` scan path: ${opts.scanPath} (watching for changes)`);
475
490
  console.log(` snapshot path: ${opts.outPath}`);
@@ -482,10 +497,41 @@ async function startWatch(graph, opts) {
482
497
  writeErrorEventInline: false,
483
498
  onPolicyTrigger
484
499
  });
485
- const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath);
500
+ const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath, graph, opts.scanPath);
486
501
  const otelHttp = await buildOtelReceiver({ onSpan, onErrorSpanSync });
487
- await otelHttp.listen({ port: otelPort, host });
488
- console.log(`neat-core OTLP receiver on http://${host}:${otelPort}/v1/traces`);
502
+ const otelAddress = await listenSteppingOtlp(otelHttp, otelPort, host);
503
+ const boundOtelPort = portFromListenAddress(otelAddress, otelPort);
504
+ console.log(`neat-core OTLP receiver on ${otelAddress}/v1/traces`);
505
+ const boundRestPort = portFromListenAddress(restAddress, port);
506
+ const daemonRecord = {
507
+ project: projectName,
508
+ projectPath: opts.scanPath,
509
+ pid: process.pid,
510
+ status: "running",
511
+ ports: {
512
+ rest: boundRestPort,
513
+ otlp: boundOtelPort,
514
+ // watch serves no dashboard of its own; record the canonical web port so
515
+ // the record shape is valid. Only ports.otlp is load-bearing here.
516
+ web: DEFAULT_WATCH_WEB_PORT
517
+ },
518
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
519
+ neatVersion: resolveNeatVersion()
520
+ };
521
+ try {
522
+ await writeDaemonRecord(daemonRecord);
523
+ console.log(
524
+ `neat watch: wrote daemon.json (REST ${boundRestPort} / OTLP ${boundOtelPort})`
525
+ );
526
+ } catch (err) {
527
+ await api.close().catch(() => {
528
+ });
529
+ await otelHttp.close().catch(() => {
530
+ });
531
+ throw new Error(
532
+ `neat watch: failed to write daemon.json \u2014 ${err.message}`
533
+ );
534
+ }
489
535
  let grpcReceiver = null;
490
536
  if (opts.otelGrpc) {
491
537
  const grpcPort = opts.otelGrpcPort ?? 4317;
@@ -598,6 +644,8 @@ async function startWatch(graph, opts) {
598
644
  await api.close();
599
645
  await otelHttp.close();
600
646
  if (grpcReceiver) await grpcReceiver.stop();
647
+ await clearDaemonRecord(daemonRecord).catch(() => {
648
+ });
601
649
  };
602
650
  return { api, stop };
603
651
  }
@@ -1448,9 +1496,23 @@ var WEB_FRAMEWORK_DEPS = [
1448
1496
  "polka",
1449
1497
  "micro"
1450
1498
  ];
1451
- function looksLikeWebApp(pkg) {
1499
+ var WORKER_FRAMEWORK_DEPS = [
1500
+ "bullmq",
1501
+ "bull",
1502
+ "bee-queue",
1503
+ "agenda",
1504
+ "kafkajs",
1505
+ "amqplib",
1506
+ "amqp-connection-manager",
1507
+ "nats",
1508
+ "node-resque",
1509
+ "pg-boss",
1510
+ "graphile-worker"
1511
+ ];
1512
+ var APP_FRAMEWORK_DEPS = [...WEB_FRAMEWORK_DEPS, ...WORKER_FRAMEWORK_DEPS];
1513
+ function appFrameworkDependencies(pkg) {
1452
1514
  const deps = allDeps(pkg);
1453
- return WEB_FRAMEWORK_DEPS.some((name) => name in deps);
1515
+ return APP_FRAMEWORK_DEPS.filter((name) => name in deps);
1454
1516
  }
1455
1517
  function uninstrumentedLibraries(pkg) {
1456
1518
  const deps = allDeps(pkg);
@@ -1537,10 +1599,10 @@ async function isTypeScriptProject(serviceDir) {
1537
1599
  var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
1538
1600
  var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
1539
1601
  var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
1540
- var SRC_NAMED_CANDIDATES = ["server", "main", "app"].flatMap(
1602
+ var SRC_NAMED_CANDIDATES = ["server", "main", "app", "worker"].flatMap(
1541
1603
  (name) => INDEX_EXTENSIONS.map((ext) => `src/${name}${ext}`)
1542
1604
  );
1543
- var ROOT_NAMED_CANDIDATES = ["server", "app", "main"].flatMap(
1605
+ var ROOT_NAMED_CANDIDATES = ["server", "app", "main", "worker"].flatMap(
1544
1606
  (name) => INDEX_EXTENSIONS.map((ext) => `${name}${ext}`)
1545
1607
  );
1546
1608
  var SCRIPT_LAUNCHERS = /* @__PURE__ */ new Set([
@@ -2797,11 +2859,13 @@ async function applyInstallersOver(services, project, options = {}) {
2797
2859
  } else if (outcome.outcome === "already-instrumented") already++;
2798
2860
  else if (outcome.outcome === "lib-only") {
2799
2861
  libOnly++;
2800
- if (svc.pkg && looksLikeWebApp(svc.pkg)) {
2862
+ const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
2863
+ if (appDeps.length > 0) {
2801
2864
  const svcName = path7.basename(svc.dir);
2865
+ const list = appDeps.join(", ");
2802
2866
  console.warn(
2803
2867
  `neat: runtime layer won't engage for ${svcName}: no entry point found.
2804
- ${svc.dir} depends on a web framework but neat couldn't resolve an entry to instrument.
2868
+ ${svc.dir} depends on ${list} \u2014 a runnable app \u2014 but neat couldn't resolve an entry to instrument.
2805
2869
  Add a "start" script to package.json, or point neat at the entry file directly.`
2806
2870
  );
2807
2871
  }
@@ -2869,10 +2933,11 @@ async function applyInstallersOver(services, project, options = {}) {
2869
2933
  if (gaps.length > 0) {
2870
2934
  const svcName = path7.basename(svc.dir);
2871
2935
  const list = gaps.join(", ");
2872
- const verb = gaps.length === 1 ? "this library" : "these libraries";
2936
+ const subject = gaps.length === 1 ? "this library" : "these libraries";
2937
+ const aux = gaps.length === 1 ? "isn't" : "aren't";
2873
2938
  console.warn(
2874
2939
  `neat: calls to ${list} won't be observed by default in ${svcName}.
2875
- ${verb} aren't in the default instrumentation set, so they produce no OBSERVED edges.
2940
+ ${subject} ${aux} in the default instrumentation set, so they produce no OBSERVED edges.
2876
2941
  Run \`neat list-uninstrumented\` to review them, then \`neat extend\` to capture them.`
2877
2942
  );
2878
2943
  }
@@ -2966,29 +3031,22 @@ async function probeProjectHealth(restPort, name) {
2966
3031
  });
2967
3032
  });
2968
3033
  }
2969
- async function snapshotProjectStatus(restPort, body) {
3034
+ async function snapshotProjectStatus(restPort, project, body) {
2970
3035
  if (body.projects && body.projects.length > 0) {
2971
- return body.projects.map((p) => ({
2972
- name: p.name,
2973
- status: p.status ?? "active"
2974
- }));
2975
- }
2976
- const entries = await listProjects().catch(() => []);
2977
- if (entries.length === 0) return [];
2978
- return Promise.all(
2979
- entries.map(async (entry2) => ({
2980
- name: entry2.name,
2981
- status: await probeProjectHealth(restPort, entry2.name)
2982
- }))
2983
- );
3036
+ const mine = body.projects.filter((p) => p.name === project);
3037
+ if (mine.length > 0) {
3038
+ return mine.map((p) => ({ name: p.name, status: p.status ?? "active" }));
3039
+ }
3040
+ }
3041
+ return [{ name: project, status: await probeProjectHealth(restPort, project) }];
2984
3042
  }
2985
- async function waitForDaemonReady(restPort, timeoutMs) {
3043
+ async function waitForDaemonReady(restPort, project, timeoutMs) {
2986
3044
  const deadline = Date.now() + timeoutMs;
2987
3045
  let lastBootstrapping = [];
2988
3046
  while (Date.now() < deadline) {
2989
3047
  const body = await fetchDaemonHealth(restPort);
2990
3048
  if (body !== null) {
2991
- const projects2 = await snapshotProjectStatus(restPort, body);
3049
+ const projects2 = await snapshotProjectStatus(restPort, project, body);
2992
3050
  const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
2993
3051
  const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
2994
3052
  if (bootstrapping.length === 0) {
@@ -3007,7 +3065,7 @@ async function waitForDaemonReady(restPort, timeoutMs) {
3007
3065
  await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
3008
3066
  }
3009
3067
  const final = await fetchDaemonHealth(restPort);
3010
- const projects = final ? await snapshotProjectStatus(restPort, final) : [];
3068
+ const projects = final ? await snapshotProjectStatus(restPort, project, final) : [];
3011
3069
  return {
3012
3070
  ready: false,
3013
3071
  brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
@@ -3015,14 +3073,37 @@ async function waitForDaemonReady(restPort, timeoutMs) {
3015
3073
  };
3016
3074
  }
3017
3075
  var NEAT_PORTS = [8080, 4318, 6328];
3018
- async function isPortFree(port) {
3076
+ function probeBind(port, host) {
3019
3077
  return new Promise((resolve) => {
3020
3078
  const server = net.createServer();
3021
- server.once("error", () => resolve(false));
3022
- server.once("listening", () => server.close(() => resolve(true)));
3023
- server.listen(port, "127.0.0.1");
3079
+ server.once("error", (err) => {
3080
+ resolve(err.code === "EADDRINUSE" ? "in-use" : "unavailable");
3081
+ });
3082
+ server.once("listening", () => server.close(() => resolve("free")));
3083
+ server.listen(port, host);
3024
3084
  });
3025
3085
  }
3086
+ function crossFamilyHost(host) {
3087
+ switch (host) {
3088
+ case "127.0.0.1":
3089
+ case "localhost":
3090
+ return "::1";
3091
+ case "::1":
3092
+ return "127.0.0.1";
3093
+ case "0.0.0.0":
3094
+ return "::";
3095
+ case "::":
3096
+ return "0.0.0.0";
3097
+ default:
3098
+ return null;
3099
+ }
3100
+ }
3101
+ async function isPortFree(port, host = "127.0.0.1") {
3102
+ if (await probeBind(port, host) !== "free") return false;
3103
+ const sibling = crossFamilyHost(host);
3104
+ if (sibling && await probeBind(port, sibling) === "in-use") return false;
3105
+ return true;
3106
+ }
3026
3107
  function formatPortCollisionMessage(port) {
3027
3108
  return [
3028
3109
  `neat: port ${port} is in use; the NEAT daemon needs it.`,
@@ -3032,13 +3113,13 @@ function formatPortCollisionMessage(port) {
3032
3113
  }
3033
3114
  var PORT_ALLOCATION_ATTEMPTS = 8;
3034
3115
  var PORT_STRIDE = 1;
3035
- async function tripleFree(ports) {
3116
+ async function tripleFree(ports, host = "127.0.0.1") {
3036
3117
  for (const p of [ports.rest, ports.otlp, ports.web]) {
3037
- if (!await isPortFree(p)) return false;
3118
+ if (!await isPortFree(p, host)) return false;
3038
3119
  }
3039
3120
  return true;
3040
3121
  }
3041
- async function allocatePorts() {
3122
+ async function allocatePorts(host = "127.0.0.1") {
3042
3123
  const [baseRest, baseOtlp, baseWeb] = NEAT_PORTS;
3043
3124
  for (let i = 0; i < PORT_ALLOCATION_ATTEMPTS; i++) {
3044
3125
  const candidate = {
@@ -3046,7 +3127,7 @@ async function allocatePorts() {
3046
3127
  otlp: baseOtlp + i * PORT_STRIDE,
3047
3128
  web: baseWeb + i * PORT_STRIDE
3048
3129
  };
3049
- if (await tripleFree(candidate)) return candidate;
3130
+ if (await tripleFree(candidate, host)) return candidate;
3050
3131
  }
3051
3132
  return null;
3052
3133
  }
@@ -3252,16 +3333,20 @@ async function runOrchestrator(opts) {
3252
3333
  }
3253
3334
  }
3254
3335
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
3336
+ const bindHost = resolveHost(
3337
+ {},
3338
+ typeof process.env.NEAT_AUTH_TOKEN === "string" && process.env.NEAT_AUTH_TOKEN.length > 0
3339
+ );
3255
3340
  const persistedPorts = await persistedPortsFor(opts.scanPath);
3256
3341
  let allocated = null;
3257
3342
  if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
3258
3343
  result.steps.daemon = "already-running";
3259
3344
  allocated = persistedPorts;
3260
3345
  } else {
3261
- if (persistedPorts && await isPortFree(persistedPorts.rest) && await tripleFree(persistedPorts)) {
3346
+ if (persistedPorts && await isPortFree(persistedPorts.rest, bindHost) && await tripleFree(persistedPorts, bindHost)) {
3262
3347
  allocated = persistedPorts;
3263
3348
  } else {
3264
- allocated = await allocatePorts();
3349
+ allocated = await allocatePorts(bindHost);
3265
3350
  }
3266
3351
  if (!allocated) {
3267
3352
  for (const line of formatPortCollisionMessage(NEAT_PORTS[0])) {
@@ -3290,7 +3375,7 @@ async function runOrchestrator(opts) {
3290
3375
  projectPath: opts.scanPath,
3291
3376
  ports: allocated
3292
3377
  });
3293
- const ready = await waitForDaemonReady(allocated.rest, timeoutMs);
3378
+ const ready = await waitForDaemonReady(allocated.rest, currentProjectName, timeoutMs);
3294
3379
  result.steps.daemon = ready.ready ? "spawned" : "timed-out";
3295
3380
  if (!ready.ready) {
3296
3381
  console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
@@ -3472,7 +3557,7 @@ async function runBlastRadius(client, input) {
3472
3557
  const result = await client.get(path10);
3473
3558
  if (result.totalAffected === 0) {
3474
3559
  return {
3475
- summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
3560
+ summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
3476
3561
  };
3477
3562
  }
3478
3563
  const sorted = [...result.affectedNodes].sort(
@@ -3485,7 +3570,7 @@ async function runBlastRadius(client, input) {
3485
3570
  );
3486
3571
  const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
3487
3572
  return {
3488
- summary: `Blast radius for ${result.origin}: ${result.totalAffected} affected node${result.totalAffected === 1 ? "" : "s"} reachable downstream.`,
3573
+ summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
3489
3574
  block: blockLines.join("\n"),
3490
3575
  confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
3491
3576
  provenance: provenances.length ? provenances : void 0
@@ -3539,20 +3624,31 @@ async function runDependencies(client, input) {
3539
3624
  throw err;
3540
3625
  }
3541
3626
  }
3627
+ function observedDepLine(nodeId, e) {
3628
+ const via = e.source !== nodeId ? ` (via ${e.source})` : "";
3629
+ return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
3630
+ }
3542
3631
  async function runObservedDependencies(client, input) {
3543
3632
  try {
3544
- const edges = await client.get(
3545
- projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
3633
+ const result = await client.get(
3634
+ projectPath(
3635
+ input.project,
3636
+ `/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
3637
+ )
3546
3638
  );
3547
- const observed = edges.outbound.filter((e) => e.provenance === Provenance2.OBSERVED);
3548
- if (observed.length === 0) {
3549
- const hasExtracted = edges.outbound.some((e) => e.provenance === Provenance2.EXTRACTED);
3550
- const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
3639
+ if (result.dependencies.length === 0) {
3640
+ if (result.observed) {
3641
+ return {
3642
+ summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
3643
+ provenance: Provenance2.OBSERVED
3644
+ };
3645
+ }
3646
+ const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
3551
3647
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
3552
3648
  }
3553
- const blockLines = observed.map((e) => ` \u2022 ${e.target} \u2014 ${e.type}${edgeMeta(e)}`);
3649
+ const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
3554
3650
  return {
3555
- summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
3651
+ summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
3556
3652
  block: blockLines.join("\n"),
3557
3653
  provenance: Provenance2.OBSERVED
3558
3654
  };
@@ -4099,7 +4195,7 @@ function usage() {
4099
4195
  console.log("query commands (mirror the MCP tools, ADR-050):");
4100
4196
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
4101
4197
  console.log(` example: ${neat} root-cause service:<name>`);
4102
- console.log(" blast-radius <node-id> BFS outbound \u2014 what would break if this dies.");
4198
+ console.log(" blast-radius <node-id> BFS inbound \u2014 the dependents that break if this dies.");
4103
4199
  console.log(` example: ${neat} blast-radius database:<host>`);
4104
4200
  console.log(" dependencies <node-id> Transitive outbound dependencies.");
4105
4201
  console.log(" Flags: --depth N (default 3, max 10)");
@@ -4826,11 +4922,18 @@ Pass --project <name> to choose:
4826
4922
  ${names}`
4827
4923
  );
4828
4924
  }
4829
- function resolveDaemonUrl() {
4830
- return process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL ?? "http://localhost:8080";
4925
+ async function resolveDaemonUrl(project) {
4926
+ const explicit = process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL;
4927
+ if (explicit) return explicit;
4928
+ if (project) {
4929
+ const daemon = await findDaemonByProject(project);
4930
+ if (daemon) return `http://localhost:${daemon.record.ports.rest}`;
4931
+ }
4932
+ return "http://localhost:8080";
4831
4933
  }
4832
4934
  async function runQueryVerb(cmd, parsed) {
4833
- const baseUrl = resolveDaemonUrl();
4935
+ const requestedProject = resolveProjectFlag(parsed);
4936
+ const baseUrl = await resolveDaemonUrl(requestedProject);
4834
4937
  const client = createHttpClient(baseUrl, resolveAuthToken());
4835
4938
  const positional = parsed.positional;
4836
4939
  let makeWork;
@@ -4986,7 +5089,7 @@ async function runQueryVerb(cmd, parsed) {
4986
5089
  const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
4987
5090
  console.error(`neat ${cmd}: ${detail.trim()}`);
4988
5091
  } else if (err instanceof TransportError) {
4989
- console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (NEAT_API_URL=${resolveDaemonUrl()})`);
5092
+ console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (endpoint=${baseUrl})`);
4990
5093
  } else {
4991
5094
  console.error(`neat ${cmd}: ${err.message}`);
4992
5095
  }
@@ -5010,6 +5113,7 @@ export {
5010
5113
  parseArgs,
5011
5114
  printBanner,
5012
5115
  readPackageVersion,
5116
+ resolveDaemonUrl,
5013
5117
  resolveProjectForVerb,
5014
5118
  runInit,
5015
5119
  runQueryVerb,