@neat.is/core 0.10.6 → 0.10.7-dev.20261011

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
@@ -801,6 +801,7 @@ __export(cli_exports, {
801
801
  cwdProjectApplies: () => cwdProjectApplies,
802
802
  isLoopbackEndpoint: () => isLoopbackEndpoint,
803
803
  isNpxInvocation: () => isNpxInvocation,
804
+ loopbackResolutionHint: () => loopbackResolutionHint,
804
805
  main: () => main,
805
806
  parseArgs: () => parseArgs,
806
807
  printBanner: () => printBanner,
@@ -18500,6 +18501,36 @@ var import_types64 = require("@neat.is/types");
18500
18501
  var CODE_FILEPATH_ATTR2 = "code.filepath";
18501
18502
  var CODE_LINENO_ATTR2 = "code.lineno";
18502
18503
  var BLAST_NEAREST_LIMIT = 5;
18504
+ function deployContextOf(ev) {
18505
+ const a = ev.attributes;
18506
+ if (!a) return void 0;
18507
+ const str = (k) => typeof a[k] === "string" ? a[k] : void 0;
18508
+ const int = (k) => typeof a[k] === "number" && Number.isFinite(a[k]) ? Math.trunc(a[k]) : void 0;
18509
+ const strs = (k) => {
18510
+ const v = a[k];
18511
+ return Array.isArray(v) && v.every((x) => typeof x === "string") && v.length > 0 ? v : void 0;
18512
+ };
18513
+ const ctx = {};
18514
+ const fault = str("k8s.fault");
18515
+ if (fault) ctx.fault = fault;
18516
+ const waiting = str("k8s.waitingReason");
18517
+ if (waiting) ctx.waitingReason = waiting;
18518
+ const image = str("k8s.image");
18519
+ if (image) ctx.image = image;
18520
+ const restarts = int("k8s.restartCount");
18521
+ if (restarts !== void 0) ctx.restartCount = restarts;
18522
+ const termReason = str("k8s.terminatedReason");
18523
+ if (termReason) ctx.terminatedReason = termReason;
18524
+ const exitCode = int("k8s.exitCode");
18525
+ if (exitCode !== void 0) ctx.exitCode = exitCode;
18526
+ const env = strs("k8s.containerEnv");
18527
+ if (env) ctx.containerEnv = env;
18528
+ const args = strs("k8s.containerArgs");
18529
+ if (args) ctx.containerArgs = args;
18530
+ const log = str("k8s.processLog");
18531
+ if (log) ctx.processLog = log;
18532
+ return Object.keys(ctx).length > 0 ? ctx : void 0;
18533
+ }
18503
18534
  function grainOf2(graph, nodeId) {
18504
18535
  if (graph.hasNode(nodeId)) {
18505
18536
  const t = graph.getNodeAttributes(nodeId).type;
@@ -18552,6 +18583,11 @@ function promoteCauseLocus(graph, causeNode, incidents) {
18552
18583
  return locusFromNode(graph, causeNode);
18553
18584
  }
18554
18585
  function divergenceSummary(d) {
18586
+ if (d.type === "observed-symbol-mismatch" || d.source === d.target) {
18587
+ const symbol = "symbol" in d && d.symbol ? d.symbol : d.source;
18588
+ const at = "location" in d && d.location ? ` at ${d.location}` : "";
18589
+ return `${symbol}${at}`;
18590
+ }
18555
18591
  const column = "column" in d && d.column ? `.${d.column}` : "";
18556
18592
  const label = "table" in d && d.table ? `${d.table}${column}` : `${d.source} \u2192 ${d.target}`;
18557
18593
  return label;
@@ -18642,6 +18678,10 @@ function buildIncidentCard(graph, errorEvent, incidents, policies) {
18642
18678
  ...nearest.length > 0 ? { blastRadius: { totalAffected: blast.totalAffected, nearest } } : {},
18643
18679
  ...policyCards.length > 0 ? { policies: policyCards } : {},
18644
18680
  ...divergences.length > 0 ? { divergence: divergences } : {},
18681
+ ...(() => {
18682
+ const dc = deployContextOf(errorEvent);
18683
+ return dc ? { deployContext: dc } : {};
18684
+ })(),
18645
18685
  headline: renderHeadline(graph, errorEvent, locus, rootCause?.node ?? null)
18646
18686
  };
18647
18687
  card.grade = gradeIncidentCard(card);
@@ -18996,6 +19036,15 @@ var TYPE_NOUNS = [
18996
19036
  ];
18997
19037
  var EMBED_MIN_SCORE = 0.35;
18998
19038
  async function resolveEntities(graph, question, searchIndex, maxNodes) {
19039
+ const verbatim = [];
19040
+ for (const word of question.split(/\s+/)) {
19041
+ const id = word.replace(/^["'`(<]+|["'`)>?!.,;]+$/g, "");
19042
+ if (!id || !graph.hasNode(id) || verbatim.some((m) => m.nodeId === id)) continue;
19043
+ const node = graph.getNodeAttributes(id);
19044
+ if (node.type === import_types65.NodeType.FrontierNode) continue;
19045
+ verbatim.push({ nodeId: id, label: nodeName(node), via: "id", score: 1 });
19046
+ }
19047
+ if (verbatim.length > 0) return { matched: verbatim.slice(0, maxNodes) };
18999
19048
  const qTokens = queryTokens(question);
19000
19049
  const normalized = question.toLowerCase();
19001
19050
  const best = /* @__PURE__ */ new Map();
@@ -31503,7 +31552,7 @@ async function connectorAdd(args, deps) {
31503
31552
  }
31504
31553
  const { replaced } = await upsertConnectorEntry(entry2, deps.home);
31505
31554
  const verb = replaced ? "updated" : "added";
31506
- const where = project ? `project "${project}"` : "the bootstrapping project";
31555
+ const where = project ? `project "${project}"` : "every project (no --project set)";
31507
31556
  deps.out(`${verb} connector "${id}" (${provider}) for ${where}.`);
31508
31557
  if (push) {
31509
31558
  deps.out("The drain is live \u2014 traces arrive at the daemon's OTLP receiver. No poll or restart needed.");
@@ -31566,7 +31615,7 @@ async function connectorList(deps, filterProject, json = false) {
31566
31615
  }
31567
31616
  deps.out("id provider project credential");
31568
31617
  for (const e of entries) {
31569
- const project = e.project ?? "(bootstrapping project)";
31618
+ const project = e.project ?? "(every project)";
31570
31619
  const credential = describeCredential(e.credential, deps.env).map((c) => {
31571
31620
  const status2 = c.status ? ` (${c.status})` : c.kind === "plaintext" ? " (plaintext)" : "";
31572
31621
  return c.field ? `${c.field}=${c.display}${status2}` : `${c.display}${status2}`;
@@ -32580,6 +32629,47 @@ var import_node_path96 = __toESM(require("path"), 1);
32580
32629
  // src/cli-client.ts
32581
32630
  init_cjs_shims();
32582
32631
  var import_types113 = require("@neat.is/types");
32632
+
32633
+ // src/incidents-render.ts
32634
+ init_cjs_shims();
32635
+ function deployHintLines(ev) {
32636
+ const a = ev.attributes;
32637
+ if (!a) return [];
32638
+ const s = (k) => typeof a[k] === "string" ? a[k] : void 0;
32639
+ const n = (k) => typeof a[k] === "number" ? a[k] : void 0;
32640
+ const arr = (k) => Array.isArray(a[k]) ? a[k].filter((x) => typeof x === "string") : [];
32641
+ const fault = s("k8s.fault");
32642
+ const env = arr("k8s.containerEnv");
32643
+ const args = arr("k8s.containerArgs");
32644
+ const log = s("k8s.processLog");
32645
+ if (!fault && env.length === 0 && args.length === 0 && !log) return [];
32646
+ const out = [];
32647
+ if (fault) {
32648
+ const bits = [fault];
32649
+ const wr = s("k8s.waitingReason");
32650
+ if (wr) bits.push(wr);
32651
+ const rc = n("k8s.restartCount");
32652
+ if (rc !== void 0) bits.push(`restarts=${rc}`);
32653
+ const ec = n("k8s.exitCode");
32654
+ if (ec !== void 0) bits.push(`exit=${ec}`);
32655
+ const tr = s("k8s.terminatedReason");
32656
+ if (tr) bits.push(tr);
32657
+ out.push(`deploy: ${bits.join(" ")}`);
32658
+ }
32659
+ const cap = (xs, max) => {
32660
+ const joined = xs.join(" ");
32661
+ return joined.length > max ? `${joined.slice(0, max)}\u2026(+${xs.length} entries)` : joined;
32662
+ };
32663
+ if (env.length > 0) out.push(`env: ${cap(env, 700)}`);
32664
+ if (args.length > 0) out.push(`args: ${cap(args, 300)}`);
32665
+ if (log) {
32666
+ const tail = log.split("\n").filter((l) => l.trim().length > 0).slice(-4);
32667
+ if (tail.length > 0) out.push(`log: ${tail.join(" \u23CE ")}`);
32668
+ }
32669
+ return out;
32670
+ }
32671
+
32672
+ // src/cli-client.ts
32583
32673
  var HttpError = class extends Error {
32584
32674
  constructor(status2, message, responseBody = "") {
32585
32675
  super(message);
@@ -32836,6 +32926,8 @@ async function runIncidents(client, input) {
32836
32926
  const blockLines = [];
32837
32927
  for (const ev of ordered) {
32838
32928
  blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
32929
+ if (ev.exceptionType) blockLines.push(` exception: ${ev.exceptionType}`);
32930
+ for (const l of deployHintLines(ev)) blockLines.push(` ${l}`);
32839
32931
  blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
32840
32932
  }
32841
32933
  const target = input.nodeId ?? "the project";
@@ -33691,7 +33783,7 @@ var import_promises5 = __toESM(require("readline/promises"), 1);
33691
33783
 
33692
33784
  // src/agent-directive.generated.ts
33693
33785
  init_cjs_shims();
33694
- var AGENT_DIRECTIVE = '## Query NEAT before searching files\n\nThis project has a live NEAT graph: code, data and infrastructure declarations\nfused with runtime traffic, incidents and supported provider telemetry. It can\nshow what exists, what actually ran, where a failure began, and what may break\nwhen a node changes. The graph is deterministic: the agent supplies the model;\nNEAT resolves named nodes and traverses recorded evidence rather than asking an\nLLM to infer architecture from file names.\n\nEvery claim carries provenance and confidence. `EXTRACTED` comes from recognized\nsource or configuration, `OBSERVED` from spans or supported provider signals,\n`INFERRED` from a bridged relationship, and `STALE` marks an observed edge that\nwent quiet. Missing runtime evidence does not prove a path never runs.\n\n**For questions about this system\'s behavior, structure, data dependencies,\nfailures, or change impact, call the MCP `ask` tool (`mcp__neat__ask`) before\nRead/Grep/Glob/Bash.** You do not need an exact node id. `ask` resolves names\nand routes to the relevant graph traversal, returning provenance-tagged facts.\nWhen MCP is unavailable, use the same door through the CLI:\n\n```\nnpx neat.is ask "why is checkout failing?"\nnpx neat.is ask "what breaks if I change the orders table?"\nnpx neat.is ask "what does the payments service depend on at runtime?"\n```\n\nFor a failure, ask first, then call `get_incident_card` on the named service,\nfile or symbol. The card combines the incident, likely cause, blast radius,\npolicies and divergence into a work order. Use `expand` one hop at a time and\n`relate` to test whether the suspected cause and symptom share a signal. Check\nprovenance and confidence before acting.\n\nBefore a change, use `get_blast_radius` and `check_policies` to see dependents\nand advisory architectural rules. Use `get_divergences` to compare declared\nintent with observed behavior, `get_graph_diff` to compare a saved snapshot\nwith the live graph, and `get_recent_stale_edges` when traffic goes quiet.\n`get_dependencies` maps a node\'s outgoing graph; `get_observed_dependencies`\nshows only seen runtime/provider calls. `semantic_search` finds node labels,\nnot arbitrary source text.\n\nRead source when the graph does not model what you need: comments, arbitrary\nstring literals, config details, unsupported syntax, or a repository that has\nnot been extracted or connected. If a graph answer is empty, check the daemon\nwith `npx neat.is list` or restart it with `npx neat.is up`; wire MCP with\n`npx neat.is skill --apply`.\n';
33786
+ var AGENT_DIRECTIVE = '## Let NEAT investigate before you search files\n\nThis project has NEAT. It does the investigation for this system so you can\nspend your effort on the code: what exists, what actually ran, where a failure\nbegan, and what a change will reach. It answers from a live graph that fuses\ncode, data and infrastructure declarations with runtime traffic, incidents and\nsupported provider telemetry. The graph is deterministic: you supply the\nreasoning about the fix; NEAT resolves named nodes and traverses recorded\nevidence rather than asking an LLM to infer architecture from file names.\n\nEvery claim carries provenance and confidence. `EXTRACTED` comes from recognized\nsource or configuration, `OBSERVED` from spans or supported provider signals,\n`INFERRED` from a bridged relationship, and `STALE` marks an observed edge that\nwent quiet. Missing runtime evidence does not prove a path never runs.\n\n**For questions about this system\'s behavior, structure, data dependencies,\nfailures, or change impact, call the MCP `ask` tool (`mcp__neat__ask`) before\nRead/Grep/Glob/Bash.** You do not need an exact node id. `ask` resolves names\nand routes to the relevant graph traversal, returning provenance-tagged facts.\nWhen MCP is unavailable, use the same door through the CLI:\n\n```\nnpx neat.is ask "why is checkout failing?"\nnpx neat.is ask "what breaks if I change the orders table?"\nnpx neat.is ask "what does the payments service depend on at runtime?"\n```\n\nFor a failure, call `get_incident_card` on the failing service, file or symbol\n(if you don\'t know which, `ask` what is failing first). The card is the\ninvestigation already done: the incident, its likely cause chain, blast radius,\nthe governing policies and divergence, in one work order. Use `expand` one hop\nat a time and `relate` to test whether the suspected cause and symptom share a\nsignal. Check provenance and confidence before acting.\n\nBefore a change, use `get_blast_radius` and `check_policies` to see dependents\nand advisory architectural rules. Use `get_divergences` to compare declared\nintent with observed behavior, `get_graph_diff` to compare a saved snapshot\nwith the live graph, and `get_recent_stale_edges` when traffic goes quiet.\n`get_dependencies` maps a node\'s outgoing graph; `get_observed_dependencies`\nshows only seen runtime/provider calls. `semantic_search` finds node labels,\nnot arbitrary source text.\n\nRead source when the graph does not model what you need: comments, arbitrary\nstring literals, config details, unsupported syntax, or a repository that has\nnot been extracted or connected. If a graph answer is empty, check the daemon\nwith `npx neat.is list` or restart it with `npx neat.is up`; wire MCP with\n`npx neat.is skill --apply`.\n';
33695
33787
 
33696
33788
  // src/welcome.ts
33697
33789
  var WORDMARK = [
@@ -33706,10 +33798,13 @@ function renderAgentSetupPrompt(project, restPort) {
33706
33798
  const endpoint2 = restPort === void 0 ? "The daemon endpoint is unavailable; run `npx neat.is up` to start it." : `The local daemon is serving it at http://127.0.0.1:${restPort}.`;
33707
33799
  return `NEAT is set up for project "${project}". ${endpoint2}
33708
33800
 
33709
- NEAT models this software system as one graph: code, data, infrastructure,
33710
- runtime traffic, incidents, and supported provider telemetry fused where evidence
33711
- permits. Use it to see what exists, what actually ran, where a failure began, and
33712
- what a change could affect. Missing observations do not prove a path never runs.
33801
+ NEAT does the investigation for this system, so you can spend your effort on the
33802
+ code. When something fails, call get_incident_card on the failing service, file
33803
+ or symbol: one work order with where the failure is, the cause chain (provenance
33804
+ on every hop), what a fix reaches, the policies that govern the code, and where
33805
+ code and runtime disagree. For anything else about how the system works, ask.
33806
+ NEAT answers from a live graph that fuses the code with what actually ran; a
33807
+ path it hasn't seen run is not proof the path never runs.
33713
33808
 
33714
33809
  1. Wire NEAT's MCP server into this agent session:
33715
33810
  npx neat.is skill --apply
@@ -33720,8 +33815,10 @@ what a change could affect. Missing observations do not prove a path never runs.
33720
33815
  If the daemon is unreachable, run \`npx neat.is up\`.
33721
33816
 
33722
33817
  ${AGENT_DIRECTIVE}
33723
- As the app or tests run, traces fill the OBSERVED layer. Daemon log:
33724
- neat-out/daemon.log. Reopen this door with \`npx neat.is welcome\`.`;
33818
+ Runtime evidence arrives once the app runs with NEAT attached: for Node, load
33819
+ .env.neat first (\`set -a; . ./.env.neat; set +a\`); for Python, start under
33820
+ \`opentelemetry-instrument\`. Daemon log: neat-out/daemon.log. Reopen this
33821
+ door with \`npx neat.is welcome\`.`;
33725
33822
  }
33726
33823
  function printHeader(out) {
33727
33824
  for (const line of WORDMARK) out(line);
@@ -36497,10 +36594,15 @@ async function main() {
36497
36594
  return;
36498
36595
  }
36499
36596
  if (cmd === "sync") {
36597
+ const syncTarget = await resolveClientTarget({
36598
+ ...parsed.project ? { project: parsed.project } : {},
36599
+ ...parsed.profile ? { profile: parsed.profile } : {}
36600
+ });
36500
36601
  const result = await runSync({
36501
36602
  ...parsed.project ? { project: parsed.project } : {},
36502
36603
  ...parsed.to ? { to: parsed.to } : {},
36503
36604
  ...parsed.token ? { token: parsed.token } : {},
36605
+ daemonUrl: syncTarget.endpoint,
36504
36606
  dryRun: parsed.dryRun,
36505
36607
  noInstrument: parsed.noInstrument,
36506
36608
  sourceEdit: parsed.sourceEdit,
@@ -36659,9 +36761,28 @@ async function resolveClientTarget(opts = {}) {
36659
36761
  if (daemon) {
36660
36762
  return { endpoint: `http://localhost:${daemon.record.ports.rest}`, source: "daemon-record" };
36661
36763
  }
36764
+ } else {
36765
+ const live = (await discoverDaemons()).filter((d) => d.live);
36766
+ if (live.length === 1) {
36767
+ return {
36768
+ endpoint: `http://localhost:${live[0].record.ports.rest}`,
36769
+ source: "sole-daemon"
36770
+ };
36771
+ }
36662
36772
  }
36663
36773
  return { endpoint: "http://localhost:8080", source: "default" };
36664
36774
  }
36775
+ async function loopbackResolutionHint() {
36776
+ const live = (await discoverDaemons()).filter((d) => d.live);
36777
+ if (live.length === 0) {
36778
+ return "No NEAT daemon is running. Start one with `npx neat.is` in your project (or `neat up`), then re-run \u2014 or set NEAT_API_URL to a daemon.";
36779
+ }
36780
+ if (live.length > 1) {
36781
+ const list = live.map((d) => ` ${d.record.project} \u2192 http://localhost:${d.record.ports.rest}`).join("\n");
36782
+ return "This directory is not a registered NEAT project and several daemons are running, so NEAT cannot pick one. Run from the project directory, pass --project <name>, or set NEAT_API_URL:\n" + list;
36783
+ }
36784
+ return void 0;
36785
+ }
36665
36786
  async function projectForCwd(cwd = process.cwd()) {
36666
36787
  try {
36667
36788
  return (await findProjectByPath(cwd))?.name;
@@ -36670,7 +36791,7 @@ async function projectForCwd(cwd = process.cwd()) {
36670
36791
  }
36671
36792
  }
36672
36793
  function cwdProjectApplies(source) {
36673
- return source === "daemon-record" || source === "default";
36794
+ return source === "daemon-record" || source === "sole-daemon" || source === "default";
36674
36795
  }
36675
36796
  async function resolveDaemonUrl(project, profile) {
36676
36797
  return (await resolveClientTarget({ project, profile })).endpoint;
@@ -36692,6 +36813,11 @@ async function runQueryVerb(cmd, parsed) {
36692
36813
  }
36693
36814
  throw err;
36694
36815
  }
36816
+ if (target.source === "default" && !requestedProject && !cwdProject) {
36817
+ const hint = await loopbackResolutionHint();
36818
+ if (hint) process.stderr.write(`${hint}
36819
+ `);
36820
+ }
36695
36821
  const client = createHttpClient(target.endpoint, target.authToken);
36696
36822
  const positional = parsed.positional;
36697
36823
  let makeWork;
@@ -36939,6 +37065,7 @@ if (/[\\/](?:cli\.(?:cjs|js)|cli|neat|neat\.is)$/.test(entry)) {
36939
37065
  cwdProjectApplies,
36940
37066
  isLoopbackEndpoint,
36941
37067
  isNpxInvocation,
37068
+ loopbackResolutionHint,
36942
37069
  main,
36943
37070
  parseArgs,
36944
37071
  printBanner,