@neat.is/core 0.10.6-dev.20261007 → 0.10.6-dev.20261008

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.
Files changed (46) hide show
  1. package/dist/{chunk-KTJK2PAH.js → chunk-6QMWJAWR.js} +204 -609
  2. package/dist/chunk-6QMWJAWR.js.map +1 -0
  3. package/dist/{chunk-VL7ANDPI.js → chunk-EMFX3V77.js} +10 -8
  4. package/dist/{chunk-VL7ANDPI.js.map → chunk-EMFX3V77.js.map} +1 -1
  5. package/dist/chunk-MR64ICXX.js +19 -0
  6. package/dist/chunk-MR64ICXX.js.map +1 -0
  7. package/dist/{chunk-EFBWQIKG.js → chunk-QPG2QXCW.js} +11 -27
  8. package/dist/chunk-QPG2QXCW.js.map +1 -0
  9. package/dist/chunk-RGYC5U2Q.js +511 -0
  10. package/dist/chunk-RGYC5U2Q.js.map +1 -0
  11. package/dist/{chunk-LXQCL4MP.js → chunk-TZXSWSHX.js} +2 -2
  12. package/dist/{chunk-BC53SCT7.js → chunk-VFZ6CXLI.js} +20 -6
  13. package/dist/chunk-VFZ6CXLI.js.map +1 -0
  14. package/dist/cli.cjs +300 -74
  15. package/dist/cli.cjs.map +1 -1
  16. package/dist/cli.d.cts +2 -0
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +249 -76
  19. package/dist/cli.js.map +1 -1
  20. package/dist/index.cjs +58 -15
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +3 -99
  23. package/dist/index.d.ts +3 -99
  24. package/dist/index.js +18 -15
  25. package/dist/neatd.cjs +58 -15
  26. package/dist/neatd.cjs.map +1 -1
  27. package/dist/neatd.js +7 -4
  28. package/dist/neatd.js.map +1 -1
  29. package/dist/otel-grpc-CCCI772O.js +11 -0
  30. package/dist/registry.cjs +570 -0
  31. package/dist/registry.cjs.map +1 -0
  32. package/dist/registry.d.cts +220 -0
  33. package/dist/registry.d.ts +220 -0
  34. package/dist/registry.js +62 -0
  35. package/dist/registry.js.map +1 -0
  36. package/dist/server.cjs +78 -21
  37. package/dist/server.cjs.map +1 -1
  38. package/dist/server.js +10 -6
  39. package/dist/server.js.map +1 -1
  40. package/package.json +8 -3
  41. package/dist/chunk-BC53SCT7.js.map +0 -1
  42. package/dist/chunk-EFBWQIKG.js.map +0 -1
  43. package/dist/chunk-KTJK2PAH.js.map +0 -1
  44. package/dist/otel-grpc-B7ABHLV7.js +0 -10
  45. /package/dist/{chunk-LXQCL4MP.js.map → chunk-TZXSWSHX.js.map} +0 -0
  46. /package/dist/{otel-grpc-B7ABHLV7.js.map → otel-grpc-CCCI772O.js.map} +0 -0
package/dist/cli.js CHANGED
@@ -8,16 +8,15 @@ import {
8
8
  resolveNeatVersion,
9
9
  startK8sSubstratePolling,
10
10
  writeDaemonRecord
11
- } from "./chunk-VL7ANDPI.js";
11
+ } from "./chunk-EMFX3V77.js";
12
12
  import {
13
- buildSearchIndex
14
- } from "./chunk-BC53SCT7.js";
13
+ buildSearchIndex,
14
+ searchProviderFromEnv
15
+ } from "./chunk-VFZ6CXLI.js";
15
16
  import {
16
17
  DEFAULT_PROJECT,
17
18
  PolicyViolationsLog,
18
- ProjectNameCollisionError,
19
19
  Projects,
20
- RegistryError,
21
20
  SCHEMA_VERSION,
22
21
  addCallEdges,
23
22
  addConfigNodes,
@@ -25,7 +24,6 @@ import {
25
24
  addFiles,
26
25
  addImports,
27
26
  addInfra,
28
- addProject,
29
27
  addRoutes,
30
28
  addServiceAliases,
31
29
  addServiceNodes,
@@ -42,59 +40,66 @@ import {
42
40
  ensureCompatLoaded,
43
41
  evaluateAllPolicies,
44
42
  extractFromDirectory,
45
- findDaemonByProject,
46
- findProjectByPath,
47
43
  formatExtractionBanner,
48
44
  formatPrecisionFloorBanner,
49
45
  formatSkippedBanner,
50
46
  getGraph,
51
- getProject,
52
47
  getProviderFieldSchema,
53
48
  isEnvRef,
54
49
  isPushProvider,
55
50
  isStrictExtractionEnabled,
56
51
  knownProviderNames,
57
- listMachineProjects,
58
- listProjects,
59
52
  loadGraphFromDisk,
60
53
  loadPolicyFile,
61
54
  makeErrorSpanWriter,
62
55
  makeSpanHandler,
63
- normalizeProjectPath,
64
56
  pathsForProject,
65
57
  promoteFrontierNodes,
66
58
  provisionConnector,
67
- pruneRegistry,
68
59
  readConnectorsConfig,
69
60
  readErrorEvents,
70
- readRegistry,
71
61
  removeConnectorEntry,
72
- removeDaemonRecord,
73
- removeProject,
74
62
  resetGraph,
75
63
  retireEdgesByFile,
76
64
  runPackageManagerInstall,
77
65
  saveGraphToDisk,
78
- setStatus,
79
- signalDaemonStop,
80
66
  startConnectorPolling,
81
67
  startPersistLoop,
82
68
  startStalenessLoop,
83
69
  upsertConnectorEntry,
84
70
  validateConnectorEntry
85
- } from "./chunk-KTJK2PAH.js";
71
+ } from "./chunk-6QMWJAWR.js";
72
+ import {
73
+ ProjectNameCollisionError,
74
+ RegistryError,
75
+ addProject,
76
+ findDaemonByProject,
77
+ findProjectByPath,
78
+ getProject,
79
+ listMachineProjects,
80
+ listProjects,
81
+ normalizeProjectPath,
82
+ pruneRegistry,
83
+ readRegistry,
84
+ removeDaemonRecord,
85
+ removeProject,
86
+ setStatus,
87
+ signalDaemonStop
88
+ } from "./chunk-RGYC5U2Q.js";
86
89
  import {
87
90
  startOtelGrpcReceiver
88
- } from "./chunk-LXQCL4MP.js";
91
+ } from "./chunk-TZXSWSHX.js";
89
92
  import {
90
- __dirname,
91
- __require,
92
93
  assertBindAuthority,
93
94
  buildOtelReceiver,
94
95
  isLoopbackHost,
95
96
  listenSteppingOtlp,
96
97
  readAuthEnv
97
- } from "./chunk-EFBWQIKG.js";
98
+ } from "./chunk-QPG2QXCW.js";
99
+ import {
100
+ __dirname,
101
+ __require
102
+ } from "./chunk-MR64ICXX.js";
98
103
 
99
104
  // src/cli.ts
100
105
  import path21 from "path";
@@ -278,11 +283,19 @@ ${NEAT_OUT_LINE}
278
283
 
279
284
  // src/summary.ts
280
285
  import { EdgeType, NodeType, Provenance as Provenance2 } from "@neat.is/types";
286
+
287
+ // src/otlp-headers.ts
288
+ var OTLP_BEARER_PREFIX = "Authorization=Bearer%20";
289
+ function otlpBearerHeader(token) {
290
+ return OTLP_BEARER_PREFIX + encodeURIComponent(token);
291
+ }
292
+
293
+ // src/summary.ts
281
294
  function renderOtelEnvBlock() {
282
295
  return [
283
296
  "for prod OTel routing, set these in your deploy platform's env:",
284
297
  " OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-neat-host>:4318",
285
- " OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <NEAT_AUTH_TOKEN>"
298
+ ` OTEL_EXPORTER_OTLP_HEADERS=${OTLP_BEARER_PREFIX}<NEAT_AUTH_TOKEN>`
286
299
  ].join("\n");
287
300
  }
288
301
  function findIncompatServices(nodes) {
@@ -698,9 +711,10 @@ async function startWatch(graph, opts) {
698
711
  const port = opts.port ?? 8080;
699
712
  const otelPort = opts.otelPort ?? 4318;
700
713
  const cachePath = opts.embeddingsCachePath ?? path3.join(path3.dirname(opts.outPath), "embeddings.json");
714
+ const forceProvider = searchProviderFromEnv();
701
715
  let searchIndex;
702
716
  try {
703
- searchIndex = await buildSearchIndex(graph, { cachePath });
717
+ searchIndex = await buildSearchIndex(graph, { cachePath, forceProvider });
704
718
  console.log(`semantic_search: ${searchIndex.provider} provider`);
705
719
  } catch (err) {
706
720
  console.warn(
@@ -983,7 +997,7 @@ function emitDockerRunSnippet() {
983
997
  function renderOtelEnvBlock2(token, host = "<host>") {
984
998
  return [
985
999
  `OTEL_EXPORTER_OTLP_ENDPOINT=https://${host}:4318`,
986
- `OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${token}`,
1000
+ `OTEL_EXPORTER_OTLP_HEADERS=${otlpBearerHeader(token)}`,
987
1001
  "OTEL_SERVICE_NAME=<service>"
988
1002
  ].join("\n");
989
1003
  }
@@ -1043,7 +1057,7 @@ import semver from "semver";
1043
1057
  // src/installers/templates.ts
1044
1058
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
1045
1059
  var OTEL_INIT_STAMP = "// neat-template-version: 8 \u2014 CommonJS TypeScript uses a synchronous bootstrap (#910); OTel init degrades to no-OBSERVED when dependencies are absent (#820); daemon.json endpoint resolution (ADR-096) + layered file-first capture (ADR-090).";
1046
- var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer ' + process.env.NEAT_OTEL_TOKEN";
1060
+ var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer%20' + encodeURIComponent(process.env.NEAT_OTEL_TOKEN)";
1047
1061
  var OTEL_OTLP_PROTOCOL_JS = "process.env.OTEL_EXPORTER_OTLP_PROTOCOL ||= 'http/json'";
1048
1062
  var OTEL_ENDPOINT_RESOLVER_CJS = `;(function () {
1049
1063
  try {
@@ -1488,7 +1502,31 @@ ${registrations.join("\n")}
1488
1502
  `;
1489
1503
  return template.replace(/__SERVICE_NAME__/g, serviceName).replace(/__PROJECT__/g, projectName).replace(/__INSTRUMENTATION_BLOCK__\n?/g, block);
1490
1504
  }
1491
- function renderEnvNeat(serviceName, projectName) {
1505
+ function renderEnvNeat(serviceName, projectName, nodeOptions, extraInstrumentations = []) {
1506
+ if (nodeOptions) {
1507
+ return [
1508
+ "# Generated by `neat init --apply` / `neat <path>` (ADR-069, ADR-232).",
1509
+ "# NEAT attaches its OpenTelemetry bootstrap at process start; no source is edited.",
1510
+ "# NODE_OPTIONS is read once when the process starts, so load this file before",
1511
+ "# your start command:",
1512
+ "# set -a; . ./.env.neat; set +a; <your start command>",
1513
+ "# or copy these settings into your process manager / platform environment.",
1514
+ `OTEL_SERVICE_NAME=${serviceName}`,
1515
+ "# The preload sends spans to this project's daemon, read from",
1516
+ "# <project>/neat-out/daemon.json; NEAT_PROJECT names the route when no record",
1517
+ "# is found. Set OTEL_EXPORTER_OTLP_ENDPOINT only to send spans elsewhere.",
1518
+ `NEAT_PROJECT=${projectName}`,
1519
+ "OTEL_EXPORTER_OTLP_PROTOCOL=http/json",
1520
+ "# Set NEAT_OTEL_TOKEN to the daemon's OTLP secret to authenticate exported spans (#410).",
1521
+ "# NEAT_OTEL_TOKEN=",
1522
+ ...extraInstrumentations.length > 0 ? [
1523
+ "# Instrumentations outside the auto bundle, loaded by the preload (<package>#<export>).",
1524
+ `NEAT_OTEL_INSTRUMENTATIONS=${extraInstrumentations.join(",")}`
1525
+ ] : [],
1526
+ `NODE_OPTIONS="${nodeOptions}"`,
1527
+ ""
1528
+ ].join("\n");
1529
+ }
1492
1530
  return [
1493
1531
  "# Generated by `neat init --apply` (ADR-069).",
1494
1532
  `OTEL_SERVICE_NAME=${serviceName}`,
@@ -1684,14 +1722,16 @@ function detectNonBundledInstrumentations(pkg) {
1684
1722
  out.push({
1685
1723
  pkg: "@prisma/instrumentation",
1686
1724
  version: prismaInstrVersion,
1687
- registration: "instrumentations.push(new (require('@prisma/instrumentation').PrismaInstrumentation)())"
1725
+ registration: "instrumentations.push(new (require('@prisma/instrumentation').PrismaInstrumentation)())",
1726
+ exportName: "PrismaInstrumentation"
1688
1727
  });
1689
1728
  }
1690
1729
  if (getMajor(deps["@nestjs/core"]) >= 11) {
1691
1730
  out.push({
1692
1731
  pkg: "@opentelemetry/instrumentation-nestjs-core",
1693
1732
  version: "^0.67.0",
1694
- registration: "instrumentations.push(new (require('@opentelemetry/instrumentation-nestjs-core').NestInstrumentation)())"
1733
+ registration: "instrumentations.push(new (require('@opentelemetry/instrumentation-nestjs-core').NestInstrumentation)())",
1734
+ exportName: "NestInstrumentation"
1695
1735
  });
1696
1736
  }
1697
1737
  return out;
@@ -2502,6 +2542,49 @@ async function findFrameworkDispatch(serviceDir, pkg, manifestPath, project) {
2502
2542
  }
2503
2543
  return null;
2504
2544
  }
2545
+ var ATTACH_PACKAGE_NAME = "@neat.is/otel-node";
2546
+ function attachPackageRange(coreVersion = readPackageVersion()) {
2547
+ return semver.valid(coreVersion) ? `^${coreVersion}` : "latest";
2548
+ }
2549
+ async function planAttachment(serviceDir, pkg, manifestPath, project) {
2550
+ const svcName = serviceNodeName(pkg, serviceDir);
2551
+ const projectName = projectToken(pkg, serviceDir, project);
2552
+ const envNeatFile = path5.join(serviceDir, ".env.neat");
2553
+ const flag = pkg.type === "module" ? "--import" : "--require";
2554
+ const nodeOptions = `${flag} @neat.is/otel-node/register`;
2555
+ const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
2556
+ const dependencyEdits = [];
2557
+ if (!(ATTACH_PACKAGE_NAME in existingDeps)) {
2558
+ dependencyEdits.push({ file: manifestPath, kind: "add", name: ATTACH_PACKAGE_NAME, version: attachPackageRange() });
2559
+ }
2560
+ const nonBundled = detectNonBundledInstrumentations(pkg);
2561
+ for (const inst of nonBundled) {
2562
+ if (inst.pkg in existingDeps) {
2563
+ if (needsVersionUpgrade(existingDeps[inst.pkg], inst.version)) {
2564
+ dependencyEdits.push({ file: manifestPath, kind: "upgrade", name: inst.pkg, version: inst.version, fromVersion: existingDeps[inst.pkg] });
2565
+ }
2566
+ continue;
2567
+ }
2568
+ dependencyEdits.push({ file: manifestPath, kind: "add", name: inst.pkg, version: inst.version });
2569
+ }
2570
+ const extraInstrumentations = nonBundled.map((i) => `${i.pkg}#${i.exportName}`);
2571
+ const generatedFiles = [];
2572
+ if (!await exists(envNeatFile)) {
2573
+ generatedFiles.push({
2574
+ file: envNeatFile,
2575
+ contents: renderEnvNeat(svcName, projectName, nodeOptions, extraInstrumentations),
2576
+ skipIfExists: true
2577
+ });
2578
+ }
2579
+ return {
2580
+ language: "javascript",
2581
+ serviceDir,
2582
+ dependencyEdits,
2583
+ entrypointEdits: [],
2584
+ envEdits: [OTEL_ENV],
2585
+ generatedFiles
2586
+ };
2587
+ }
2505
2588
  async function plan(serviceDir, opts) {
2506
2589
  const pkg = await readPackageJson(serviceDir);
2507
2590
  const manifestPath = path5.join(serviceDir, "package.json");
@@ -2527,14 +2610,17 @@ async function plan(serviceDir, opts) {
2527
2610
  if (!entryFile) {
2528
2611
  return { ...empty, libOnly: true };
2529
2612
  }
2613
+ const runtimeKind = await detectRuntimeKind(serviceDir, pkg);
2614
+ if (runtimeKind !== "node") {
2615
+ return { ...empty, runtimeKind };
2616
+ }
2617
+ }
2618
+ if (!opts?.sourceEdit) {
2619
+ return planAttachment(serviceDir, pkg, manifestPath, project);
2530
2620
  }
2531
2621
  if (frameworkDispatch) {
2532
2622
  return frameworkDispatch();
2533
2623
  }
2534
- const runtimeKind = await detectRuntimeKind(serviceDir, pkg);
2535
- if (runtimeKind !== "node") {
2536
- return { ...empty, runtimeKind };
2537
- }
2538
2624
  if (!entryFile) {
2539
2625
  return { ...empty, libOnly: true };
2540
2626
  }
@@ -2851,6 +2937,7 @@ var SDK_PACKAGES2 = [
2851
2937
  { name: "opentelemetry-distro", version: ">=0.49b0" },
2852
2938
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
2853
2939
  ];
2940
+ var NEAT_OTEL_PACKAGE = { name: "neat-otel", version: ">=0.1.0" };
2854
2941
  var OTEL_ENV2 = {
2855
2942
  file: null,
2856
2943
  key: "OTEL_EXPORTER_OTLP_ENDPOINT",
@@ -3002,34 +3089,37 @@ function reqPackageName(line) {
3002
3089
  const head = stripped.split(/[\s;]/)[0] ?? "";
3003
3090
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
3004
3091
  }
3005
- async function planRequirementsTxtEdits(serviceDir) {
3092
+ async function planRequirementsTxtEdits(serviceDir, packages = SDK_PACKAGES2) {
3006
3093
  const file = path6.join(serviceDir, "requirements.txt");
3007
3094
  if (!await exists2(file)) return null;
3008
3095
  const raw = await fs5.readFile(file, "utf8");
3009
3096
  const presentNames = new Set(
3010
3097
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
3011
3098
  );
3012
- const missing = SDK_PACKAGES2.filter((p) => !presentNames.has(p.name.toLowerCase()));
3099
+ const missing = packages.filter((p) => !presentNames.has(p.name.toLowerCase()));
3013
3100
  return { manifest: file, missing: [...missing] };
3014
3101
  }
3015
- async function planProcfileEdits(serviceDir) {
3102
+ var PY_LAUNCHER_PYTHON = /^python3?\b/;
3103
+ var PY_LAUNCHERS_BROAD = /^(python3?|uvicorn|gunicorn|hypercorn|daphne|granian|celery|flask|fastapi)\b/;
3104
+ async function planProcfileEdits(serviceDir, broad = false) {
3016
3105
  const procfile = path6.join(serviceDir, "Procfile");
3017
3106
  if (!await exists2(procfile)) return [];
3018
3107
  const raw = await fs5.readFile(procfile, "utf8");
3108
+ const launcher = broad ? PY_LAUNCHERS_BROAD : PY_LAUNCHER_PYTHON;
3019
3109
  const edits = [];
3020
3110
  for (const line of raw.split(/\r?\n/)) {
3021
3111
  if (line.length === 0) continue;
3022
3112
  const m = line.match(/^([a-zA-Z0-9_-]+):\s*(.+)$/);
3023
3113
  if (!m) continue;
3024
3114
  const cmd = m[2];
3025
- if (!/^python\b/.test(cmd)) continue;
3115
+ if (!launcher.test(cmd)) continue;
3026
3116
  if (cmd.startsWith("opentelemetry-instrument ")) continue;
3027
3117
  const after = `${m[1]}: opentelemetry-instrument ${cmd}`;
3028
3118
  edits.push({ file: procfile, before: line, after });
3029
3119
  }
3030
3120
  return edits;
3031
3121
  }
3032
- async function plan2(serviceDir) {
3122
+ async function plan2(serviceDir, opts) {
3033
3123
  const empty = {
3034
3124
  language: "python",
3035
3125
  serviceDir,
@@ -3037,6 +3127,25 @@ async function plan2(serviceDir) {
3037
3127
  entrypointEdits: [],
3038
3128
  envEdits: []
3039
3129
  };
3130
+ if (!opts?.sourceEdit) {
3131
+ const entrypointEdits2 = await planProcfileEdits(serviceDir, true);
3132
+ const reqs2 = await planRequirementsTxtEdits(serviceDir, [NEAT_OTEL_PACKAGE, ...SDK_PACKAGES2]);
3133
+ const dependencyEdits2 = reqs2 ? reqs2.missing.map((sdk) => ({
3134
+ file: reqs2.manifest,
3135
+ kind: "add",
3136
+ name: sdk.name,
3137
+ version: sdk.version
3138
+ })) : [];
3139
+ if (dependencyEdits2.length === 0 && entrypointEdits2.length === 0) return empty;
3140
+ return {
3141
+ language: "python",
3142
+ serviceDir,
3143
+ dependencyEdits: dependencyEdits2,
3144
+ entrypointEdits: entrypointEdits2,
3145
+ envEdits: [OTEL_ENV2]
3146
+ };
3147
+ }
3148
+ const entrypointEdits = await planProcfileEdits(serviceDir);
3040
3149
  const dependencyEdits = [];
3041
3150
  const reqs = await planRequirementsTxtEdits(serviceDir);
3042
3151
  if (reqs) {
@@ -3049,7 +3158,6 @@ async function plan2(serviceDir) {
3049
3158
  });
3050
3159
  }
3051
3160
  }
3052
- const entrypointEdits = await planProcfileEdits(serviceDir);
3053
3161
  const entryFile = await resolvePyEntrypoint(serviceDir);
3054
3162
  const generatedFiles = entryFile ? [{ file: path6.join(serviceDir, NEAT_OTEL_FILENAME), contents: neatOtelPy() }] : [];
3055
3163
  if (dependencyEdits.length === 0 && entrypointEdits.length === 0 && !entryFile) {
@@ -4000,7 +4108,7 @@ async function applyInstallersOver(services, project, options = {}) {
4000
4108
  for (const svc of services) {
4001
4109
  const installer = await pickInstaller(svc.dir);
4002
4110
  if (!installer) continue;
4003
- const plan6 = await installer.plan(svc.dir, { project });
4111
+ const plan6 = await installer.plan(svc.dir, { project, sourceEdit: options.sourceEdit });
4004
4112
  if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) {
4005
4113
  already++;
4006
4114
  continue;
@@ -4574,8 +4682,12 @@ async function runOrchestrator(opts) {
4574
4682
  result.steps.apply.skipped = true;
4575
4683
  console.log("skipped instrumentation (--no-instrument)");
4576
4684
  } else {
4577
- const tally = await applyInstallersOver(services, opts.project);
4578
- result.steps.apply = { ...tally, skipped: false };
4685
+ const tally = await applyInstallersOver(services, opts.project, { sourceEdit: opts.sourceEdit });
4686
+ result.steps.apply = {
4687
+ ...tally,
4688
+ skipped: false,
4689
+ ...!opts.sourceEdit && tally.instrumented > 0 ? { attached: true } : {}
4690
+ };
4579
4691
  console.log(
4580
4692
  `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
4581
4693
  );
@@ -4688,6 +4800,12 @@ function printSummary(result, graph, daemonLog) {
4688
4800
  for (const i of failedInstalls) {
4689
4801
  console.log(` run \`${i.pm} install\` in ${i.cwd}`);
4690
4802
  }
4803
+ } else if (daemonLog !== null && result.steps.apply.attached === true) {
4804
+ console.log("next: start your app with NEAT attached \u2014 OBSERVED edges fill in as it runs,");
4805
+ console.log(" and divergences surface where code and runtime disagree.");
4806
+ console.log(" Node: in the service directory, `set -a; . ./.env.neat; set +a`, then your");
4807
+ console.log(" usual start command (or put its NODE_OPTIONS in your process manager).");
4808
+ console.log(" Python: run your start command under `opentelemetry-instrument`.");
4691
4809
  } else if (daemonLog !== null) {
4692
4810
  console.log(
4693
4811
  "next: run your app or your test suite \u2014 OBSERVED edges fill in as it executes,"
@@ -5856,7 +5974,7 @@ async function runSsoLogin(opts, deps) {
5856
5974
  out("");
5857
5975
  out("To fill the OBSERVED layer, instrument your app to send OpenTelemetry to the hosted daemon:");
5858
5976
  out(` OTEL_EXPORTER_OTLP_ENDPOINT=${cred.ingestEndpoint}`);
5859
- out(` OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${cred.otelToken}`);
5977
+ out(` OTEL_EXPORTER_OTLP_HEADERS=${otlpBearerHeader(cred.otelToken)}`);
5860
5978
  }
5861
5979
  await runPostLoginConnect({
5862
5980
  cpUrl: opts.cpUrl,
@@ -7092,6 +7210,11 @@ async function defaultReadSecret(prompt) {
7092
7210
  import { promises as fs13 } from "fs";
7093
7211
  import path15 from "path";
7094
7212
  import readline4 from "readline/promises";
7213
+
7214
+ // src/agent-directive.generated.ts
7215
+ 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';
7216
+
7217
+ // src/welcome.ts
7095
7218
  var WORDMARK = [
7096
7219
  "\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557",
7097
7220
  "\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D",
@@ -7100,21 +7223,27 @@ var WORDMARK = [
7100
7223
  "\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 ",
7101
7224
  "\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D "
7102
7225
  ];
7103
- var AGENT_SETUP_PROMPT = `Set up self-hosted NEAT for this project. NEAT keeps a live graph of the code
7104
- fused with runtime OpenTelemetry data, queryable over MCP \u2014 so you can ask it
7105
- about the system instead of grepping files.
7226
+ function renderAgentSetupPrompt(project, restPort) {
7227
+ const endpoint = 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}.`;
7228
+ return `NEAT is set up for project "${project}". ${endpoint}
7106
7229
 
7107
- 1. Extract the code graph and instrument the services for OpenTelemetry:
7108
- npx neat.is init . --apply
7109
- Then install the deps it added (npm install, or your language's equivalent).
7110
- 2. Start the NEAT daemon so it watches the code and ingests live traces:
7111
- npx neat.is watch .
7112
- 3. Wire NEAT's MCP server into your agent so it queries the graph first:
7230
+ NEAT models this software system as one graph: code, data, infrastructure,
7231
+ runtime traffic, incidents, and supported provider telemetry fused where evidence
7232
+ permits. Use it to see what exists, what actually ran, where a failure began, and
7233
+ what a change could affect. Missing observations do not prove a path never runs.
7234
+
7235
+ 1. Wire NEAT's MCP server into this agent session:
7113
7236
  npx neat.is skill --apply
7114
- (other agents: npx neat.is codex|cursor|gemini \u2026 --apply)
7237
+ For Codex: npx neat.is codex --apply
7238
+ For Cursor or Gemini, use the corresponding cursor or gemini subcommand.
7239
+ 2. Verify the MCP \`ask\` tool answers:
7240
+ ask "what services are in this project and what do they talk to?"
7241
+ If the daemon is unreachable, run \`npx neat.is up\`.
7115
7242
 
7116
- Then ask NEAT instead of reading files, e.g.
7117
- npx neat.is ask "why is checkout failing?"`;
7243
+ ${AGENT_DIRECTIVE}
7244
+ As the app or tests run, traces fill the OBSERVED layer. Daemon log:
7245
+ neat-out/daemon.log. Reopen this door with \`npx neat.is welcome\`.`;
7246
+ }
7118
7247
  function printHeader(out) {
7119
7248
  for (const line of WORDMARK) out(line);
7120
7249
  out("");
@@ -7151,15 +7280,38 @@ async function runFrontDoor(deps, out) {
7151
7280
  const cwd = deps.cwd ?? process.cwd();
7152
7281
  const flagGiven = deps.instrumentFlagGiven ?? false;
7153
7282
  const readRegistry2 = deps.readRegistry ?? (() => readRegistry());
7283
+ const readDaemon = deps.readDaemon ?? (async (project) => {
7284
+ const daemon = await findDaemonByProject(project);
7285
+ return daemon?.live ? { projectPath: daemon.record.projectPath, restPort: daemon.record.ports.rest } : void 0;
7286
+ });
7154
7287
  printHeader(out);
7155
7288
  out("Welcome to NEAT. Let's get you a graph of this system.");
7156
7289
  out("");
7157
7290
  const keyReader = deps.readKey !== void 0 ? { read: deps.readKey, done: () => {
7158
7291
  } } : createKeyReader();
7159
7292
  const picked = keyReader !== null ? await selectOption(out, keyReader, deps.moveCursorUp ?? defaultMoveCursorUp) : await selectByNumber(out, readLine);
7160
- if (picked === void 0) return runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, flagGiven);
7293
+ if (picked === void 0)
7294
+ return runSelfHosted(
7295
+ out,
7296
+ readLine,
7297
+ orchestrator,
7298
+ cwd,
7299
+ readRegistry2,
7300
+ readDaemon,
7301
+ flagGiven,
7302
+ deps.dryRun === true
7303
+ );
7161
7304
  if (picked === 0) return login(["--browser"]);
7162
- return runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, flagGiven);
7305
+ return runSelfHosted(
7306
+ out,
7307
+ readLine,
7308
+ orchestrator,
7309
+ cwd,
7310
+ readRegistry2,
7311
+ readDaemon,
7312
+ flagGiven,
7313
+ deps.dryRun === true
7314
+ );
7163
7315
  }
7164
7316
  var MENU_OPTIONS = [
7165
7317
  "Log me into Hosted Neat",
@@ -7262,17 +7414,7 @@ function decodeKey(buf) {
7262
7414
  function defaultMoveCursorUp(rows) {
7263
7415
  if (rows > 0) process.stdout.write(`\x1B[${rows}A`);
7264
7416
  }
7265
- async function runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, instrumentFlagGiven) {
7266
- const wantsPrompt = (await readLine("Print a copy-paste setup prompt for your coding agent? [Y/n]: "))?.trim().toLowerCase();
7267
- if (wantsPrompt !== "n" && wantsPrompt !== "no") {
7268
- out("");
7269
- out("\u2500\u2500\u2500 copy the prompt below into your coding agent \u2500\u2500\u2500");
7270
- out("");
7271
- out(AGENT_SETUP_PROMPT);
7272
- out("");
7273
- out("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
7274
- out("");
7275
- }
7417
+ async function runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, readDaemon, instrumentFlagGiven, dryRun) {
7276
7418
  const project = await resolveProjectName(out, readLine, cwd, readRegistry2);
7277
7419
  const declined = instrumentFlagGiven ? void 0 : await askToInstrument(out, readLine);
7278
7420
  out("Building your local graph now\u2026");
@@ -7282,7 +7424,22 @@ async function runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, in
7282
7424
  ...project !== void 0 ? { project } : {},
7283
7425
  ...declined === void 0 ? {} : declined ? { noInstrument: true } : { yes: true }
7284
7426
  };
7285
- return orchestrator(cwd, overrides);
7427
+ const code = await orchestrator(cwd, overrides);
7428
+ if (code !== 0 || dryRun) return code;
7429
+ const wantsPrompt = (await readLine("Print a copy-paste setup prompt for your coding agent? [Y/n]: "))?.trim().toLowerCase();
7430
+ if (wantsPrompt !== "n" && wantsPrompt !== "no") {
7431
+ const projectName = project ?? path15.basename(path15.resolve(cwd));
7432
+ const daemon = await readDaemon(projectName).catch(() => void 0);
7433
+ const port = daemon && path15.resolve(daemon.projectPath) === path15.resolve(cwd) ? daemon.restPort : void 0;
7434
+ out("");
7435
+ out("\u2500\u2500\u2500 copy the directive below into your coding agent \u2500\u2500\u2500");
7436
+ out("");
7437
+ out(renderAgentSetupPrompt(projectName, port));
7438
+ out("");
7439
+ out("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
7440
+ out("");
7441
+ }
7442
+ return code;
7286
7443
  }
7287
7444
  async function askToInstrument(out, readLine) {
7288
7445
  out("Instrument the services for OpenTelemetry now?");
@@ -7292,7 +7449,7 @@ async function askToInstrument(out, readLine) {
7292
7449
  if (declined) {
7293
7450
  out("");
7294
7451
  out("Skipping instrumentation \u2014 the graph will hold the declared side only.");
7295
- out("Run `neat init . --apply` when you want the runtime half.");
7452
+ out("Run `npx neat.is init . --apply` when you want the runtime half.");
7296
7453
  }
7297
7454
  out("");
7298
7455
  return declined;
@@ -8854,7 +9011,7 @@ async function runSync(opts) {
8854
9011
  snapshotPath = target;
8855
9012
  }
8856
9013
  const skipApply = opts.dryRun || opts.noInstrument;
8857
- const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, browserBundle: 0, reactNative: 0 } : await applyInstallersOver(persisted.services, entry2.name);
9014
+ const applyTally = skipApply ? { instrumented: 0, alreadyInstrumented: 0, libOnly: 0, browserBundle: 0, reactNative: 0 } : await applyInstallersOver(persisted.services, entry2.name, { sourceEdit: opts.sourceEdit });
8858
9015
  const warnings = [];
8859
9016
  let daemonState = "skipped";
8860
9017
  let exitCode = 0;
@@ -8966,6 +9123,8 @@ function usage5() {
8966
9123
  console.log(" --apply run the SDK install patch in place");
8967
9124
  console.log(" --dry-run write only neat.patch; do not register or snapshot");
8968
9125
  console.log(" --no-install skip SDK install planning entirely");
9126
+ console.log(" --source-edit inject the OTel init into entry points instead of");
9127
+ console.log(" attaching it at process start (ADR-232)");
8969
9128
  console.log(" watch <path> Start neat-core, watch <path>, re-extract on changes.");
8970
9129
  console.log(" PORT (default 8080), OTEL_PORT (4318), HOST (0.0.0.0)");
8971
9130
  console.log(" control listeners. NEAT_OTLP_GRPC=true also opens 4317.");
@@ -9073,6 +9232,7 @@ function usage5() {
9073
9232
  console.log(" --token <token> bearer token for --to (or $NEAT_REMOTE_TOKEN)");
9074
9233
  console.log(" --dry-run run extraction in-memory; do not write");
9075
9234
  console.log(" --no-instrument skip the SDK install apply step");
9235
+ console.log(" --source-edit inject into entry points instead of attaching");
9076
9236
  console.log(" --json emit the delta summary as JSON");
9077
9237
  console.log(" connector Configure OBSERVED connectors \u2014 pull (supabase, railway,");
9078
9238
  console.log(" firebase, cloudflare, neon, cloud-run) and push (vercel,");
@@ -9171,6 +9331,7 @@ function parseArgs(rest) {
9171
9331
  dryRun: false,
9172
9332
  noInstall: false,
9173
9333
  noInstrument: false,
9334
+ sourceEdit: false,
9174
9335
  open: false,
9175
9336
  noOpen: false,
9176
9337
  yes: false,
@@ -9209,6 +9370,10 @@ function parseArgs(rest) {
9209
9370
  out.noInstrument = true;
9210
9371
  continue;
9211
9372
  }
9373
+ if (arg === "--source-edit") {
9374
+ out.sourceEdit = true;
9375
+ continue;
9376
+ }
9212
9377
  if (arg === "--open") {
9213
9378
  out.open = true;
9214
9379
  continue;
@@ -9334,12 +9499,12 @@ function printDiscoveryReport(opts, services) {
9334
9499
  }
9335
9500
  console.log("");
9336
9501
  }
9337
- async function buildPatchSections(services, project) {
9502
+ async function buildPatchSections(services, project, sourceEdit) {
9338
9503
  const sections = [];
9339
9504
  for (const svc of services) {
9340
9505
  const installer = await pickInstaller(svc.dir);
9341
9506
  if (!installer) continue;
9342
- const plan6 = await installer.plan(svc.dir, { project });
9507
+ const plan6 = await installer.plan(svc.dir, { project, sourceEdit });
9343
9508
  if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) continue;
9344
9509
  sections.push({ installer: installer.name, plan: plan6 });
9345
9510
  }
@@ -9354,7 +9519,7 @@ async function runInit(opts) {
9354
9519
  }
9355
9520
  const services = await discoverServices(opts.scanPath);
9356
9521
  printDiscoveryReport(opts, services);
9357
- const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
9522
+ const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project, opts.sourceEdit);
9358
9523
  const patch = renderPatch(sections);
9359
9524
  const patchPath = path21.join(opts.scanPath, "neat.patch");
9360
9525
  if (opts.dryRun) {
@@ -9658,6 +9823,7 @@ async function main() {
9658
9823
  apply: apply6,
9659
9824
  dryRun,
9660
9825
  noInstall,
9826
+ sourceEdit: parsed.sourceEdit,
9661
9827
  verbose: parsed.verbose
9662
9828
  });
9663
9829
  if (result.exitCode !== 0) process.exit(result.exitCode);
@@ -9850,6 +10016,7 @@ async function main() {
9850
10016
  ...parsed.token ? { token: parsed.token } : {},
9851
10017
  dryRun: parsed.dryRun,
9852
10018
  noInstrument: parsed.noInstrument,
10019
+ sourceEdit: parsed.sourceEdit,
9853
10020
  json: parsed.json
9854
10021
  });
9855
10022
  if (result.exitCode !== 0) process.exit(result.exitCode);
@@ -9885,6 +10052,7 @@ async function tryOrchestrator(cmd, parsed, overrides = {}) {
9885
10052
  project: projectName,
9886
10053
  projectExplicit,
9887
10054
  noInstrument: parsed.noInstrument,
10055
+ sourceEdit: parsed.sourceEdit,
9888
10056
  open: parsed.open,
9889
10057
  noOpen: parsed.noOpen,
9890
10058
  yes: parsed.yes,
@@ -9897,6 +10065,10 @@ async function runWelcomeFlow(parsed) {
9897
10065
  cwd,
9898
10066
  {
9899
10067
  ...parsed,
10068
+ // ADR-232 — the front-door / local-menu run never source-edits, even if
10069
+ // `--source-edit` was on the command line. Source-edit stays an explicit
10070
+ // choice on a deliberately-typed `neat <path> --source-edit`.
10071
+ sourceEdit: false,
9900
10072
  ...opts?.project !== void 0 ? { project: opts.project } : {},
9901
10073
  ...opts?.noInstrument !== void 0 ? { noInstrument: opts.noInstrument } : {},
9902
10074
  ...opts?.yes !== void 0 ? { yes: opts.yes } : {}
@@ -9912,7 +10084,8 @@ async function runWelcomeFlow(parsed) {
9912
10084
  // door's wordmark already opened this run, so the orchestrator's banner is skipped.
9913
10085
  orchestrator: runLocal,
9914
10086
  // An explicit flag is an answer; don't ask again.
9915
- instrumentFlagGiven: parsed.noInstrument || parsed.dryRun
10087
+ instrumentFlagGiven: parsed.noInstrument || parsed.dryRun,
10088
+ dryRun: parsed.dryRun
9916
10089
  });
9917
10090
  }
9918
10091
  var QUERY_VERBS = /* @__PURE__ */ new Set([