@neat.is/core 0.10.1-dev.20260927 → 0.10.1

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.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  resolveNeatVersion,
9
9
  startK8sSubstratePolling,
10
10
  writeDaemonRecord
11
- } from "./chunk-EJPNEEJA.js";
11
+ } from "./chunk-AWIK6T3G.js";
12
12
  import {
13
13
  buildSearchIndex
14
14
  } from "./chunk-BC53SCT7.js";
@@ -63,6 +63,7 @@ import {
63
63
  pruneRegistry,
64
64
  readConnectorsConfig,
65
65
  readErrorEvents,
66
+ readRegistry,
66
67
  removeConnectorEntry,
67
68
  removeDaemonRecord,
68
69
  removeProject,
@@ -77,23 +78,24 @@ import {
77
78
  startStalenessLoop,
78
79
  upsertConnectorEntry,
79
80
  validateConnectorEntry
80
- } from "./chunk-J3VVEB45.js";
81
+ } from "./chunk-E3YFFIHE.js";
81
82
  import {
82
83
  startOtelGrpcReceiver
83
- } from "./chunk-ERE47MCR.js";
84
+ } from "./chunk-5SJHA5ZH.js";
84
85
  import {
85
86
  __dirname,
86
87
  __require,
87
88
  assertBindAuthority,
88
89
  buildOtelReceiver,
90
+ isLoopbackHost,
89
91
  listenSteppingOtlp,
90
92
  readAuthEnv
91
- } from "./chunk-GDGUY4T6.js";
93
+ } from "./chunk-M7CGATVC.js";
92
94
 
93
95
  // src/cli.ts
94
- import path18 from "path";
96
+ import path20 from "path";
95
97
  import os5 from "os";
96
- import { promises as fs16 } from "fs";
98
+ import { promises as fs18 } from "fs";
97
99
 
98
100
  // src/banner.ts
99
101
  import path from "path";
@@ -4451,7 +4453,8 @@ async function runOrchestrator(opts) {
4451
4453
  } catch (err) {
4452
4454
  if (!(err instanceof ProjectNameCollisionError)) throw err;
4453
4455
  console.error(`neat: ${err.message}`);
4454
- console.error("pass --project <other-name> to register under a different name.");
4456
+ console.error(`neat: run \`neat <path> --project <other-name>\` to register it under a different name,`);
4457
+ console.error(`neat: or \`neat uninstall ${opts.project}\` if the old registration is stale.`);
4455
4458
  result.exitCode = 1;
4456
4459
  return result;
4457
4460
  }
@@ -5244,8 +5247,198 @@ async function clearActiveProfile(home = neatHome()) {
5244
5247
  });
5245
5248
  }
5246
5249
 
5250
+ // src/daemon-probe.ts
5251
+ var PROBE_ATTEMPT_TIMEOUT_MS = 3e4;
5252
+ var PROBE_TOTAL_BUDGET_MS = 12e4;
5253
+ var PROBE_RETRY_PAUSE_MS = 2e3;
5254
+ function isTimeoutError(err) {
5255
+ const name = err?.name;
5256
+ return name === "TimeoutError" || name === "AbortError";
5257
+ }
5258
+ async function probeOnce(fetchImpl, root, token, timeoutMs) {
5259
+ let res;
5260
+ try {
5261
+ res = await fetchImpl(`${root}/health`, {
5262
+ headers: { authorization: `Bearer ${token}` },
5263
+ signal: AbortSignal.timeout(timeoutMs)
5264
+ });
5265
+ } catch (err) {
5266
+ if (isTimeoutError(err)) return { kind: "timeout" };
5267
+ return { kind: "unreachable", detail: err.message };
5268
+ }
5269
+ if (res.status === 401 || res.status === 403) return { kind: "unauthorized", status: res.status };
5270
+ if (!res.ok) return { kind: "not-neat", status: res.status };
5271
+ const contentType = res.headers.get("content-type") ?? "";
5272
+ if (!contentType.includes("json")) return { kind: "not-neat", status: res.status };
5273
+ return { kind: "ok" };
5274
+ }
5275
+ async function probeDaemon(fetchImpl, endpoint, token, hooks) {
5276
+ const root = endpoint.replace(/\/$/, "");
5277
+ const deadline = hooks.now() + PROBE_TOTAL_BUDGET_MS;
5278
+ let warned = false;
5279
+ for (; ; ) {
5280
+ const result = await probeOnce(fetchImpl, root, token, PROBE_ATTEMPT_TIMEOUT_MS);
5281
+ if (result.kind !== "timeout") return result;
5282
+ if (!warned) {
5283
+ hooks.onWaiting();
5284
+ warned = true;
5285
+ }
5286
+ if (hooks.now() >= deadline) {
5287
+ return { kind: "unreachable", detail: `no response after ${Math.round(PROBE_TOTAL_BUDGET_MS / 1e3)}s` };
5288
+ }
5289
+ await hooks.sleep(PROBE_RETRY_PAUSE_MS);
5290
+ }
5291
+ }
5292
+
5293
+ // src/login-connect.ts
5294
+ import { execFile } from "child_process";
5295
+ import { promisify } from "util";
5296
+ import { promises as fs11 } from "fs";
5297
+ import path12 from "path";
5298
+ var execFileAsync = promisify(execFile);
5299
+ async function hasLocalSnapshot(cwd) {
5300
+ try {
5301
+ await fs11.access(path12.join(cwd, "neat-out", "graph.json"));
5302
+ return true;
5303
+ } catch {
5304
+ return false;
5305
+ }
5306
+ }
5307
+ var POLL_INTERVAL_MS = 3e3;
5308
+ var POLL_BUDGET_MS = 10 * 6e4;
5309
+ var CP_TIMEOUT_MS = 1e4;
5310
+ async function detectGitHubRepo(cwd) {
5311
+ const read = async (args) => {
5312
+ try {
5313
+ const { stdout } = await execFileAsync("git", args, { cwd, timeout: 5e3 });
5314
+ return stdout.trim();
5315
+ } catch {
5316
+ return null;
5317
+ }
5318
+ };
5319
+ let url = await read(["remote", "get-url", "origin"]);
5320
+ if (!url) {
5321
+ const names = await read(["remote"]);
5322
+ const first = names?.split(/\r?\n/).find((l) => l.trim().length > 0)?.trim();
5323
+ if (!first) return null;
5324
+ url = await read(["remote", "get-url", first]);
5325
+ }
5326
+ if (!url) return null;
5327
+ return parseGitHubRemote(url);
5328
+ }
5329
+ function parseGitHubRemote(url) {
5330
+ const trimmed = url.trim().replace(/\.git$/, "");
5331
+ const m = trimmed.match(/^https?:\/\/(?:[^@/]+@)?github\.com\/([^/]+)\/([^/]+)$/) ?? trimmed.match(/^git@github\.com:([^/]+)\/([^/]+)$/) ?? trimmed.match(/^ssh:\/\/git@github\.com\/([^/]+)\/([^/]+)$/);
5332
+ if (!m) return null;
5333
+ const owner = m[1];
5334
+ const name = m[2];
5335
+ if (!owner || !name) return null;
5336
+ return { owner, name };
5337
+ }
5338
+ async function cpGetJson(fetchImpl, cpUrl, path21, accessToken) {
5339
+ try {
5340
+ const res = await fetchImpl(`${cpUrl}${path21}`, {
5341
+ headers: { authorization: `Bearer ${accessToken}`, accept: "application/json" },
5342
+ signal: AbortSignal.timeout(CP_TIMEOUT_MS)
5343
+ });
5344
+ if (!res.ok) return { ok: false };
5345
+ return { ok: true, body: await res.json() };
5346
+ } catch {
5347
+ return { ok: false };
5348
+ }
5349
+ }
5350
+ async function listBoundRepos(fetchImpl, cpUrl, accessToken, projectId) {
5351
+ const res = await cpGetJson(
5352
+ fetchImpl,
5353
+ cpUrl,
5354
+ `/me/projects/${encodeURIComponent(projectId)}/repos`,
5355
+ accessToken
5356
+ );
5357
+ if (!res.ok || !Array.isArray(res.body)) return null;
5358
+ return res.body;
5359
+ }
5360
+ async function waitForFirstProject(fetchImpl, cpUrl, accessToken, deps) {
5361
+ const pollMs = deps.pollMs ?? POLL_INTERVAL_MS;
5362
+ const deadline = deps.now() + (deps.budgetMs ?? POLL_BUDGET_MS);
5363
+ for (; ; ) {
5364
+ if (deps.signal?.aborted) return { kind: "aborted" };
5365
+ const me = await cpGetJson(fetchImpl, cpUrl, "/me", accessToken);
5366
+ if (me.ok) {
5367
+ const running = (me.body.projects ?? []).find((p) => p.status === "running");
5368
+ if (running) return { kind: "project", project: running };
5369
+ }
5370
+ if (deps.now() >= deadline) return { kind: "timeout" };
5371
+ await deps.sleep(pollMs);
5372
+ if (deps.signal?.aborted) return { kind: "aborted" };
5373
+ }
5374
+ }
5375
+ function bindUrl(webUrl, projectId) {
5376
+ return `${webUrl}/config/repos?project=${encodeURIComponent(projectId)}`;
5377
+ }
5378
+ function onboardingUrl(webUrl) {
5379
+ return `${webUrl}/onboarding`;
5380
+ }
5381
+ async function runPostLoginConnect(input) {
5382
+ const d = input.deps;
5383
+ const out = d.out ?? ((line) => console.log(line));
5384
+ const fetchImpl = d.fetchImpl ?? fetch;
5385
+ const cwd = d.cwd ?? process.cwd();
5386
+ const detect6 = d.detectRepo ?? detectGitHubRepo;
5387
+ const readLine = d.readLine;
5388
+ const repo = await detect6(cwd);
5389
+ out("");
5390
+ if (!repo) {
5391
+ out("This directory has no GitHub remote, so it can't be bound to the project.");
5392
+ out(` Push its graph instead: neat sync --to ${input.project.name}`);
5393
+ out(` Manage projects and repos: ${input.webUrl}`);
5394
+ return;
5395
+ }
5396
+ const slug = `${repo.owner}/${repo.name}`;
5397
+ const bound = await listBoundRepos(fetchImpl, input.cpUrl, input.accessToken, input.project.id);
5398
+ const match = bound?.find((r) => r.owner === repo.owner && r.name === repo.name);
5399
+ if (match) {
5400
+ const state = match.syncStatus ? ` (${match.syncStatus})` : "";
5401
+ out(`${slug} is bound to ${input.project.name}${state} \u2014 hosted NEAT extracts it for you.`);
5402
+ out(` See the graph: ${input.webUrl}`);
5403
+ return;
5404
+ }
5405
+ out(`${slug} isn't part of ${input.project.name} yet \u2014 logging in connected your account, not this repo.`);
5406
+ out("");
5407
+ const installed = input.me?.github?.installed;
5408
+ const bindLine = installed === false ? "1) Install the GitHub App and bind this repo \u2014 hosted NEAT keeps it in sync" : installed === true ? "1) Bind this repo \u2014 hosted NEAT clones and extracts it, and keeps it in sync" : "1) Bind this repo through the console \u2014 hosted NEAT keeps it in sync";
5409
+ out(bindLine);
5410
+ out(`2) Push the graph on this machine now \u2014 a snapshot, not a subscription`);
5411
+ out("3) Neither, for now");
5412
+ const answer = readLine ? (await readLine("Choose 1, 2 or 3 (Enter skips): "))?.trim() : void 0;
5413
+ if (answer === "1") {
5414
+ const url = bindUrl(input.webUrl, input.project.id);
5415
+ const opened = (d.openBrowser ?? (() => false))(url);
5416
+ out("");
5417
+ out(opened ? `Opened ${url}` : `Open this to bind the repo:
5418
+ ${url}`);
5419
+ return;
5420
+ }
5421
+ if (answer === "2") {
5422
+ const hasGraph = await (d.hasLocalGraph ?? hasLocalSnapshot)(cwd);
5423
+ out("");
5424
+ if (!hasGraph) {
5425
+ out("There is no local graph to push yet. Build one, then push it:");
5426
+ out(" neat");
5427
+ out(` neat sync --to ${input.project.name}`);
5428
+ return;
5429
+ }
5430
+ out("Push the local graph with:");
5431
+ out(` neat sync --to ${input.project.name}`);
5432
+ return;
5433
+ }
5434
+ out("");
5435
+ out("No problem \u2014 when you want this repo in the graph:");
5436
+ out(` Bind it: ${bindUrl(input.webUrl, input.project.id)}`);
5437
+ out(` Or push it: neat sync --to ${input.project.name}`);
5438
+ }
5439
+
5247
5440
  // src/login-sso.ts
5248
- var DEFAULT_CP_URL = "https://neat-control-plane-bg5yqctn2q-nw.a.run.app";
5441
+ var DEFAULT_CP_URL = "https://api.neat.is";
5249
5442
  var DEFAULT_WEB_URL = "https://app.neat.is";
5250
5443
  var CALLBACK_TIMEOUT_MS = 5 * 6e4;
5251
5444
  function resolveCpUrl(env = process.env, override) {
@@ -5269,6 +5462,7 @@ function pickProject(projects, want) {
5269
5462
  return {
5270
5463
  error: {
5271
5464
  code: 1,
5465
+ kind: "no-project",
5272
5466
  message: `no running project to connect to \u2014 create + provision one in the console first${listed}`
5273
5467
  }
5274
5468
  };
@@ -5319,7 +5513,7 @@ async function exchangeCredential(cpUrl, accessToken, opts, deps) {
5319
5513
  if (!cred.endpoint || !cred.authToken) {
5320
5514
  return { error: { code: 1, message: "the credential response was missing endpoint/authToken" } };
5321
5515
  }
5322
- return { project, cred };
5516
+ return { project, cred, me };
5323
5517
  }
5324
5518
  function defaultOpenBrowser(url) {
5325
5519
  const platform = process.platform;
@@ -5401,12 +5595,50 @@ async function runSsoLogin(opts, deps) {
5401
5595
  }
5402
5596
  accessToken = lb.token;
5403
5597
  }
5404
- const ex = await exchangeCredential(opts.cpUrl, accessToken, { project: opts.project }, deps);
5598
+ const fetchImpl = deps.fetchImpl ?? fetch;
5599
+ const sleep2 = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
5600
+ const now = deps.now ?? Date.now;
5601
+ let ex = await exchangeCredential(opts.cpUrl, accessToken, { project: opts.project }, deps);
5602
+ if ("error" in ex && ex.error.kind === "no-project" && !opts.json) {
5603
+ const url = onboardingUrl(opts.webUrl);
5604
+ out("");
5605
+ out("You don't have a project yet \u2014 let's make one.");
5606
+ const opened = (deps.openBrowser ?? defaultOpenBrowser)(url);
5607
+ out(opened ? `Opened ${url}` : `Open this to create your first project:
5608
+ ${url}`);
5609
+ out("Waiting for it to come up \u2014 Ctrl-C to stop and finish later.");
5610
+ const waited = await waitForFirstProject(fetchImpl, opts.cpUrl, accessToken, {
5611
+ sleep: sleep2,
5612
+ now,
5613
+ ...deps.signal ? { signal: deps.signal } : {}
5614
+ });
5615
+ if (waited.kind === "aborted") {
5616
+ out("Stopped. Run `neat login` again once your project is up.");
5617
+ return 130;
5618
+ }
5619
+ if (waited.kind === "timeout") {
5620
+ err("neat login: no project came up while waiting \u2014 run `neat login` again once it does");
5621
+ return 1;
5622
+ }
5623
+ out(`${waited.project.name} is up.`);
5624
+ ex = await exchangeCredential(opts.cpUrl, accessToken, { project: opts.project }, deps);
5625
+ }
5405
5626
  if ("error" in ex) {
5406
5627
  err(`neat login: ${ex.error.message}`);
5407
5628
  return ex.error.code;
5408
5629
  }
5409
- const { project, cred } = ex;
5630
+ const { project, cred, me } = ex;
5631
+ const probe = await probeDaemon(fetchImpl, cred.endpoint, cred.authToken, {
5632
+ sleep: sleep2,
5633
+ now,
5634
+ onWaiting: () => err("Waking the hosted daemon \u2014 a cold instance can take up to a minute\u2026")
5635
+ });
5636
+ if (probe.kind !== "ok") {
5637
+ const why = probe.kind === "unreachable" ? probe.detail : probe.kind === "unauthorized" ? `it rejected the credential (HTTP ${probe.status})` : `it answered HTTP ${probe.status} and doesn't look like NEAT`;
5638
+ err(`neat login: "${project.name}" is provisioned, but its daemon isn't answering \u2014 ${why}.`);
5639
+ err(`Nothing was changed. Check the project at ${opts.webUrl} and run \`neat login\` again.`);
5640
+ return probe.kind === "unreachable" ? 3 : 1;
5641
+ }
5410
5642
  await upsertProfile(
5411
5643
  { name: opts.name, endpoint: cred.endpoint, authToken: cred.authToken },
5412
5644
  { makeActive: true, ...deps.home ? { home: deps.home } : {} }
@@ -5434,6 +5666,21 @@ async function runSsoLogin(opts, deps) {
5434
5666
  out(` OTEL_EXPORTER_OTLP_ENDPOINT=${cred.ingestEndpoint}`);
5435
5667
  out(` OTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer ${cred.otelToken}`);
5436
5668
  }
5669
+ await runPostLoginConnect({
5670
+ cpUrl: opts.cpUrl,
5671
+ webUrl: opts.webUrl,
5672
+ accessToken,
5673
+ project: { id: project.id, name: project.name },
5674
+ me,
5675
+ deps: {
5676
+ fetchImpl,
5677
+ out,
5678
+ ...deps.readLine ? { readLine: deps.readLine } : {},
5679
+ ...deps.openBrowser ? { openBrowser: deps.openBrowser } : { openBrowser: defaultOpenBrowser },
5680
+ ...deps.connect ?? {}
5681
+ }
5682
+ });
5683
+ out("");
5437
5684
  out("Run `neat logout` to switch back to your local daemon.");
5438
5685
  }
5439
5686
  return 0;
@@ -5539,8 +5786,8 @@ async function runConnectCommand(rawArgs, deps = {}) {
5539
5786
  }
5540
5787
 
5541
5788
  // src/doctor-cli.ts
5542
- import { promises as fs11 } from "fs";
5543
- import path12 from "path";
5789
+ import { promises as fs12 } from "fs";
5790
+ import path13 from "path";
5544
5791
 
5545
5792
  // src/cli-client.ts
5546
5793
  import { Provenance as Provenance3 } from "@neat.is/types";
@@ -5568,10 +5815,10 @@ function createHttpClient(baseUrl, bearerToken, fetchImpl = fetch) {
5568
5815
  const root = baseUrl.replace(/\/$/, "");
5569
5816
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
5570
5817
  return {
5571
- async get(path19) {
5818
+ async get(path21) {
5572
5819
  let res;
5573
5820
  try {
5574
- res = await fetchImpl(`${root}${path19}`, {
5821
+ res = await fetchImpl(`${root}${path21}`, {
5575
5822
  headers: { ...authHeader }
5576
5823
  });
5577
5824
  } catch (err) {
@@ -5583,16 +5830,16 @@ function createHttpClient(baseUrl, bearerToken, fetchImpl = fetch) {
5583
5830
  const body = await res.text().catch(() => "");
5584
5831
  throw new HttpError(
5585
5832
  res.status,
5586
- `${res.status} ${res.statusText} on GET ${path19}: ${body}`,
5833
+ `${res.status} ${res.statusText} on GET ${path21}: ${body}`,
5587
5834
  body
5588
5835
  );
5589
5836
  }
5590
5837
  return await res.json();
5591
5838
  },
5592
- async post(path19, body) {
5839
+ async post(path21, body) {
5593
5840
  let res;
5594
5841
  try {
5595
- res = await fetchImpl(`${root}${path19}`, {
5842
+ res = await fetchImpl(`${root}${path21}`, {
5596
5843
  method: "POST",
5597
5844
  headers: { "content-type": "application/json", ...authHeader },
5598
5845
  body: JSON.stringify(body)
@@ -5606,7 +5853,7 @@ function createHttpClient(baseUrl, bearerToken, fetchImpl = fetch) {
5606
5853
  const text = await res.text().catch(() => "");
5607
5854
  throw new HttpError(
5608
5855
  res.status,
5609
- `${res.status} ${res.statusText} on POST ${path19}: ${text}`,
5856
+ `${res.status} ${res.statusText} on POST ${path21}: ${text}`,
5610
5857
  text
5611
5858
  );
5612
5859
  }
@@ -5620,12 +5867,12 @@ function projectPath(project, suffix) {
5620
5867
  }
5621
5868
  async function runRootCause(client, input) {
5622
5869
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
5623
- const path19 = projectPath(
5870
+ const path21 = projectPath(
5624
5871
  input.project,
5625
5872
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
5626
5873
  );
5627
5874
  try {
5628
- const result = await client.get(path19);
5875
+ const result = await client.get(path21);
5629
5876
  const arrowPath = result.traversalPath.join(" \u2190 ");
5630
5877
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
5631
5878
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -5651,12 +5898,12 @@ async function runRootCause(client, input) {
5651
5898
  }
5652
5899
  async function runBlastRadius(client, input) {
5653
5900
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
5654
- const path19 = projectPath(
5901
+ const path21 = projectPath(
5655
5902
  input.project,
5656
5903
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
5657
5904
  );
5658
5905
  try {
5659
- const result = await client.get(path19);
5906
+ const result = await client.get(path21);
5660
5907
  if (result.totalAffected === 0) {
5661
5908
  return {
5662
5909
  summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
@@ -5690,12 +5937,12 @@ function formatBlastEntry(n) {
5690
5937
  }
5691
5938
  async function runDependencies(client, input) {
5692
5939
  const depth = input.depth ?? 3;
5693
- const path19 = projectPath(
5940
+ const path21 = projectPath(
5694
5941
  input.project,
5695
5942
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
5696
5943
  );
5697
5944
  try {
5698
- const result = await client.get(path19);
5945
+ const result = await client.get(path21);
5699
5946
  if (result.total === 0) {
5700
5947
  return {
5701
5948
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -5787,9 +6034,9 @@ function formatDuration(ms) {
5787
6034
  return `${Math.round(h / 24)}d`;
5788
6035
  }
5789
6036
  async function runIncidents(client, input) {
5790
- const path19 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
6037
+ const path21 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
5791
6038
  try {
5792
- const body = await client.get(path19);
6039
+ const body = await client.get(path21);
5793
6040
  const events = body.events;
5794
6041
  if (events.length === 0) {
5795
6042
  return {
@@ -6153,7 +6400,7 @@ function checkNode(nodeVersion) {
6153
6400
  }
6154
6401
  async function neatOutExists(cwd) {
6155
6402
  try {
6156
- const st = await fs11.stat(path12.join(cwd, "neat-out"));
6403
+ const st = await fs12.stat(path13.join(cwd, "neat-out"));
6157
6404
  return st.isDirectory();
6158
6405
  } catch {
6159
6406
  return false;
@@ -6272,9 +6519,6 @@ async function runDoctorCommand(argv, deps = {}) {
6272
6519
 
6273
6520
  // src/login-cli.ts
6274
6521
  import readline3 from "readline/promises";
6275
- var PROBE_ATTEMPT_TIMEOUT_MS = 3e4;
6276
- var PROBE_TOTAL_BUDGET_MS = 12e4;
6277
- var PROBE_RETRY_PAUSE_MS = 2e3;
6278
6522
  var DEFAULT_PROFILE_NAME = "hosted";
6279
6523
  function readFlagValue(argv, i) {
6280
6524
  const arg = argv[i];
@@ -6339,44 +6583,6 @@ function printLoginHelp(out) {
6339
6583
  out(' the profile (default "hosted"); the paste token can also come from NEAT_LOGIN_TOKEN.');
6340
6584
  out(" Exit 0 on success, 1 rejected/needs-action, 2 misuse, 3 unreachable.");
6341
6585
  }
6342
- function isTimeoutError(err) {
6343
- const name = err?.name;
6344
- return name === "TimeoutError" || name === "AbortError";
6345
- }
6346
- async function probeOnce(fetchImpl, root, token, timeoutMs) {
6347
- let res;
6348
- try {
6349
- res = await fetchImpl(`${root}/health`, {
6350
- headers: { authorization: `Bearer ${token}` },
6351
- signal: AbortSignal.timeout(timeoutMs)
6352
- });
6353
- } catch (err) {
6354
- if (isTimeoutError(err)) return { kind: "timeout" };
6355
- return { kind: "unreachable", detail: err.message };
6356
- }
6357
- if (res.status === 401 || res.status === 403) return { kind: "unauthorized", status: res.status };
6358
- if (!res.ok) return { kind: "not-neat", status: res.status };
6359
- const contentType = res.headers.get("content-type") ?? "";
6360
- if (!contentType.includes("json")) return { kind: "not-neat", status: res.status };
6361
- return { kind: "ok" };
6362
- }
6363
- async function probeDaemon(fetchImpl, endpoint, token, hooks) {
6364
- const root = endpoint.replace(/\/$/, "");
6365
- const deadline = hooks.now() + PROBE_TOTAL_BUDGET_MS;
6366
- let warned = false;
6367
- for (; ; ) {
6368
- const result = await probeOnce(fetchImpl, root, token, PROBE_ATTEMPT_TIMEOUT_MS);
6369
- if (result.kind !== "timeout") return result;
6370
- if (!warned) {
6371
- hooks.onWaiting();
6372
- warned = true;
6373
- }
6374
- if (hooks.now() >= deadline) {
6375
- return { kind: "unreachable", detail: `no response after ${Math.round(PROBE_TOTAL_BUDGET_MS / 1e3)}s` };
6376
- }
6377
- await hooks.sleep(PROBE_RETRY_PAUSE_MS);
6378
- }
6379
- }
6380
6586
  async function runLoginCommand(argv, deps = {}) {
6381
6587
  const out = deps.out ?? ((line) => console.log(line));
6382
6588
  const err = deps.err ?? ((line) => console.error(line));
@@ -6409,8 +6615,13 @@ async function runLoginCommand(argv, deps = {}) {
6409
6615
  fetchImpl,
6410
6616
  out,
6411
6617
  err,
6618
+ readLine,
6619
+ sleep: sleep2,
6620
+ now,
6412
6621
  ...deps.openBrowser ? { openBrowser: deps.openBrowser } : {},
6413
- ...deps.home ? { home: deps.home } : {}
6622
+ ...deps.home ? { home: deps.home } : {},
6623
+ ...deps.signal ? { signal: deps.signal } : {},
6624
+ ...deps.connect ? { connect: deps.connect } : {}
6414
6625
  }
6415
6626
  );
6416
6627
  }
@@ -6566,6 +6777,8 @@ async function defaultReadSecret(prompt) {
6566
6777
  }
6567
6778
 
6568
6779
  // src/welcome.ts
6780
+ import { promises as fs13 } from "fs";
6781
+ import path14 from "path";
6569
6782
  import readline4 from "readline/promises";
6570
6783
  var WORDMARK = [
6571
6784
  "\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",
@@ -6598,29 +6811,146 @@ function printHeader(out) {
6598
6811
  }
6599
6812
  async function runWelcome(deps = {}) {
6600
6813
  const out = deps.out ?? ((line) => console.log(line));
6814
+ try {
6815
+ return await runFrontDoor(deps, out);
6816
+ } catch (err) {
6817
+ if (isPromptCancelled(err)) {
6818
+ out("");
6819
+ return 130;
6820
+ }
6821
+ throw err;
6822
+ }
6823
+ }
6824
+ var PromptCancelled = class extends Error {
6825
+ constructor() {
6826
+ super("prompt cancelled");
6827
+ this.name = "PromptCancelled";
6828
+ }
6829
+ };
6830
+ function isPromptCancelled(err) {
6831
+ if (err instanceof PromptCancelled) return true;
6832
+ const e = err;
6833
+ return e?.code === "ABORT_ERR" || e?.name === "AbortError";
6834
+ }
6835
+ async function runFrontDoor(deps, out) {
6601
6836
  const readLine = deps.readLine ?? defaultReadLine2;
6602
6837
  const login = deps.login ?? (() => Promise.resolve(0));
6603
6838
  const orchestrator = deps.orchestrator ?? (() => Promise.resolve(0));
6604
6839
  const cwd = deps.cwd ?? process.cwd();
6840
+ const flagGiven = deps.instrumentFlagGiven ?? false;
6841
+ const readRegistry2 = deps.readRegistry ?? (() => readRegistry());
6605
6842
  printHeader(out);
6606
6843
  out("Welcome to NEAT. Let's get you a graph of this system.");
6607
6844
  out("");
6608
- out(" 1) I have a NEAT account \u2014 log in");
6609
- out(" 2) Self-hosted \u2014 set up NEAT on this machine");
6845
+ const keyReader = deps.readKey !== void 0 ? { read: deps.readKey, done: () => {
6846
+ } } : createKeyReader();
6847
+ const picked = keyReader !== null ? await selectOption(out, keyReader, deps.moveCursorUp ?? defaultMoveCursorUp) : await selectByNumber(out, readLine);
6848
+ if (picked === void 0) return runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, flagGiven);
6849
+ if (picked === 0) return login(["--browser"]);
6850
+ return runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, flagGiven);
6851
+ }
6852
+ var MENU_OPTIONS = [
6853
+ "Log me into Hosted Neat",
6854
+ "I'd like to self-host or use it locally (copy a prompt)"
6855
+ ];
6856
+ async function selectOption(out, reader, moveCursorUp) {
6857
+ let cursor = 0;
6858
+ const render = () => {
6859
+ for (const [i, label] of MENU_OPTIONS.entries()) {
6860
+ out(i === cursor ? ` \u276F ${i + 1}) ${label}` : ` ${i + 1}) ${label}`);
6861
+ }
6862
+ };
6863
+ try {
6864
+ render();
6865
+ out("");
6866
+ out(" \u2191/\u2193 to move \xB7 Enter to choose \xB7 1 or 2 to jump");
6867
+ for (; ; ) {
6868
+ const key = await reader.read();
6869
+ if (key === void 0) return void 0;
6870
+ if (key === "cancel") throw new PromptCancelled();
6871
+ if (key === "enter") return cursor;
6872
+ if (key === "select-1") return 0;
6873
+ if (key === "select-2") return 1;
6874
+ if (key === "up") cursor = (cursor + MENU_OPTIONS.length - 1) % MENU_OPTIONS.length;
6875
+ else if (key === "down") cursor = (cursor + 1) % MENU_OPTIONS.length;
6876
+ else continue;
6877
+ moveCursorUp(MENU_OPTIONS.length + 2);
6878
+ render();
6879
+ out("");
6880
+ out(" \u2191/\u2193 to move \xB7 Enter to choose \xB7 1 or 2 to jump");
6881
+ }
6882
+ } finally {
6883
+ reader.done();
6884
+ }
6885
+ }
6886
+ async function selectByNumber(out, readLine) {
6887
+ for (const [i, label] of MENU_OPTIONS.entries()) out(` ${i + 1}) ${label}`);
6610
6888
  out("");
6611
6889
  for (; ; ) {
6612
6890
  const choice = (await readLine("Choose 1 or 2 (default 2): "))?.trim();
6613
- if (choice === void 0) return runSelfHosted(out, readLine, orchestrator, cwd);
6614
- if (choice === "1") {
6615
- return login(["--browser"]);
6616
- }
6617
- if (choice === "2" || choice === "") {
6618
- return runSelfHosted(out, readLine, orchestrator, cwd);
6619
- }
6620
- out(`"${choice}" isn't 1 or 2 \u2014 try again, or press Enter for self-hosted.`);
6891
+ if (choice === void 0) return void 0;
6892
+ if (choice === "1") return 0;
6893
+ if (choice === "2" || choice === "") return 1;
6894
+ out(`"${choice}" isn't 1 or 2 \u2014 try again, or press Enter for the local path.`);
6621
6895
  }
6622
6896
  }
6623
- async function runSelfHosted(out, readLine, orchestrator, cwd) {
6897
+ function createKeyReader() {
6898
+ const stdin = process.stdin;
6899
+ if (!stdin.isTTY || typeof stdin.setRawMode !== "function") return null;
6900
+ stdin.setRawMode(true);
6901
+ stdin.resume();
6902
+ let closed = false;
6903
+ let ended = false;
6904
+ const markEnded = () => {
6905
+ ended = true;
6906
+ };
6907
+ stdin.once("end", markEnded);
6908
+ stdin.once("close", markEnded);
6909
+ return {
6910
+ read: () => new Promise((resolve) => {
6911
+ if (ended) {
6912
+ resolve(void 0);
6913
+ return;
6914
+ }
6915
+ const finish = (value) => {
6916
+ stdin.removeListener("data", onData);
6917
+ stdin.removeListener("end", onClosed);
6918
+ stdin.removeListener("close", onClosed);
6919
+ resolve(value);
6920
+ };
6921
+ const onData = (buf) => finish(decodeKey(buf));
6922
+ const onClosed = () => {
6923
+ ended = true;
6924
+ finish(void 0);
6925
+ };
6926
+ stdin.on("data", onData);
6927
+ stdin.once("end", onClosed);
6928
+ stdin.once("close", onClosed);
6929
+ }),
6930
+ done: () => {
6931
+ if (closed) return;
6932
+ closed = true;
6933
+ stdin.removeListener("end", markEnded);
6934
+ stdin.removeListener("close", markEnded);
6935
+ stdin.setRawMode(false);
6936
+ stdin.pause();
6937
+ }
6938
+ };
6939
+ }
6940
+ function decodeKey(buf) {
6941
+ const s = buf.toString("utf8");
6942
+ if (s === "\x1B[A" || s === "k") return "up";
6943
+ if (s === "\x1B[B" || s === "j") return "down";
6944
+ if (s === "\r" || s === "\n") return "enter";
6945
+ if (s === "1") return "select-1";
6946
+ if (s === "2") return "select-2";
6947
+ if (s === "" || s === "\x1B") return "cancel";
6948
+ return "ignore";
6949
+ }
6950
+ function defaultMoveCursorUp(rows) {
6951
+ if (rows > 0) process.stdout.write(`\x1B[${rows}A`);
6952
+ }
6953
+ async function runSelfHosted(out, readLine, orchestrator, cwd, readRegistry2, instrumentFlagGiven) {
6624
6954
  const wantsPrompt = (await readLine("Print a copy-paste setup prompt for your coding agent? [Y/n]: "))?.trim().toLowerCase();
6625
6955
  if (wantsPrompt !== "n" && wantsPrompt !== "no") {
6626
6956
  out("");
@@ -6631,18 +6961,91 @@ async function runSelfHosted(out, readLine, orchestrator, cwd) {
6631
6961
  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");
6632
6962
  out("");
6633
6963
  }
6964
+ const project = await resolveProjectName(out, readLine, cwd, readRegistry2);
6965
+ const declined = instrumentFlagGiven ? void 0 : await askToInstrument(out, readLine);
6634
6966
  out("Building your local graph now\u2026");
6635
6967
  out("");
6636
- return orchestrator(cwd);
6968
+ const overrides = {
6969
+ ...project !== void 0 ? { project } : {},
6970
+ ...declined === void 0 ? {} : declined ? { noInstrument: true } : { yes: true }
6971
+ };
6972
+ return orchestrator(cwd, Object.keys(overrides).length > 0 ? overrides : void 0);
6973
+ }
6974
+ async function askToInstrument(out, readLine) {
6975
+ out("Instrument the services for OpenTelemetry now?");
6976
+ out("This edits package.json / requirements.txt / go.mod and runs the package manager.");
6977
+ const answer = (await readLine("[Y/n]: "))?.trim().toLowerCase();
6978
+ const declined = answer === "n" || answer === "no";
6979
+ if (declined) {
6980
+ out("");
6981
+ out("Skipping instrumentation \u2014 the graph will hold the declared side only.");
6982
+ out("Run `neat init . --apply` when you want the runtime half.");
6983
+ }
6984
+ out("");
6985
+ return declined;
6986
+ }
6987
+ async function resolveProjectName(out, readLine, cwd, readRegistry2) {
6988
+ const here = path14.resolve(cwd);
6989
+ const base = path14.basename(here);
6990
+ let taken;
6991
+ try {
6992
+ const { projects } = await readRegistry2();
6993
+ taken = new Map(projects.map((p) => [p.name, p.path]));
6994
+ } catch {
6995
+ return void 0;
6996
+ }
6997
+ const clash = taken.get(base);
6998
+ if (clash === void 0 || path14.resolve(clash) === here) return void 0;
6999
+ out("");
7000
+ out(`A project named \`${base}\` is already registered (${clash}).`);
7001
+ const suggested = firstFreeName(base, taken);
7002
+ for (; ; ) {
7003
+ const answer = (await readLine(`Name this one: [${suggested}] `))?.trim();
7004
+ if (answer === void 0) return suggested;
7005
+ const chosen = answer.length === 0 ? suggested : answer;
7006
+ if (!isUsableProjectName(chosen)) {
7007
+ out(`"${chosen}" won't work as a project name \u2014 letters, digits, dot, dash and underscore.`);
7008
+ continue;
7009
+ }
7010
+ const other = taken.get(chosen);
7011
+ if (other !== void 0 && path14.resolve(other) !== here) {
7012
+ out(`\`${chosen}\` is registered too (${other}).`);
7013
+ continue;
7014
+ }
7015
+ out("");
7016
+ return chosen;
7017
+ }
7018
+ }
7019
+ function firstFreeName(base, taken) {
7020
+ for (let n = 2; ; n++) {
7021
+ const candidate = `${base}-${n}`;
7022
+ if (!taken.has(candidate)) return candidate;
7023
+ }
7024
+ }
7025
+ function isUsableProjectName(name) {
7026
+ return name.length > 0 && name !== "." && name !== ".." && /^[A-Za-z0-9._-]+$/.test(name);
6637
7027
  }
6638
7028
  async function shouldShowWelcome(deps = {}) {
6639
7029
  const stdinTTY = deps.stdinIsTTY ?? Boolean(process.stdin.isTTY);
6640
7030
  const stdoutTTY = deps.stdoutIsTTY ?? Boolean(process.stdout.isTTY);
6641
7031
  if (!stdinTTY || !stdoutTTY) return false;
6642
- const readProfiles = deps.readProfiles ?? (() => readProfilesConfig());
7032
+ const cwd = deps.cwd ?? process.cwd();
7033
+ const readRegistry2 = deps.readRegistry ?? (() => readRegistry());
7034
+ const hasSnapshot = deps.hasSnapshot ?? defaultHasSnapshot;
6643
7035
  try {
6644
- const { profiles } = await readProfiles();
6645
- return profiles.length === 0;
7036
+ const here = path14.resolve(cwd);
7037
+ const { projects } = await readRegistry2();
7038
+ if (projects.some((p) => path14.resolve(p.path) === here)) return false;
7039
+ if (await hasSnapshot(here)) return false;
7040
+ return true;
7041
+ } catch {
7042
+ return false;
7043
+ }
7044
+ }
7045
+ async function defaultHasSnapshot(cwd) {
7046
+ try {
7047
+ const entries = await fs13.readdir(path14.join(cwd, "neat-out"));
7048
+ return entries.some((e) => e.endsWith(".json"));
6646
7049
  } catch {
6647
7050
  return false;
6648
7051
  }
@@ -6658,27 +7061,27 @@ async function defaultReadLine2(prompt) {
6658
7061
  }
6659
7062
 
6660
7063
  // src/hooks-cli.ts
6661
- import path13 from "path";
7064
+ import path15 from "path";
6662
7065
  import os2 from "os";
6663
- import { promises as fs12 } from "fs";
7066
+ import { promises as fs14 } from "fs";
6664
7067
  import { fileURLToPath as fileURLToPath3 } from "url";
6665
7068
  var HOOK_FILENAME = "neat-search-nudge.mjs";
6666
7069
  var GUIDE_FILENAME = "GRAPH_FIRST.md";
6667
7070
  var GUIDE_INSTALL_NAME = "neat-graph-first.md";
6668
7071
  var HOOK_MATCHER = "Grep|Glob|Bash";
6669
7072
  function moduleDir() {
6670
- return typeof __dirname !== "undefined" ? __dirname : path13.dirname(fileURLToPath3(import.meta.url));
7073
+ return typeof __dirname !== "undefined" ? __dirname : path15.dirname(fileURLToPath3(import.meta.url));
6671
7074
  }
6672
7075
  async function readSkillAsset(rel) {
6673
7076
  const here = moduleDir();
6674
7077
  const candidates = [
6675
- path13.resolve(here, "../../claude-skill", rel),
6676
- path13.resolve(here, "../../../claude-skill", rel),
6677
- path13.resolve(here, "../claude-skill", rel)
7078
+ path15.resolve(here, "../../claude-skill", rel),
7079
+ path15.resolve(here, "../../../claude-skill", rel),
7080
+ path15.resolve(here, "../claude-skill", rel)
6678
7081
  ];
6679
7082
  for (const candidate of candidates) {
6680
7083
  try {
6681
- return await fs12.readFile(candidate, "utf8");
7084
+ return await fs14.readFile(candidate, "utf8");
6682
7085
  } catch {
6683
7086
  }
6684
7087
  }
@@ -6688,20 +7091,20 @@ async function readSkillAsset(rel) {
6688
7091
  }
6689
7092
  function neatHome2() {
6690
7093
  const override = process.env.NEAT_HOME;
6691
- if (override && override.length > 0) return path13.resolve(override);
6692
- return path13.join(os2.homedir(), ".neat");
7094
+ if (override && override.length > 0) return path15.resolve(override);
7095
+ return path15.join(os2.homedir(), ".neat");
6693
7096
  }
6694
7097
  function claudeSettingsPath() {
6695
7098
  const override = process.env.NEAT_CLAUDE_SETTINGS;
6696
- if (override && override.length > 0) return path13.resolve(override);
7099
+ if (override && override.length > 0) return path15.resolve(override);
6697
7100
  const home = process.env.HOME ?? process.env.USERPROFILE ?? os2.homedir();
6698
- return path13.join(home, ".claude", "settings.json");
7101
+ return path15.join(home, ".claude", "settings.json");
6699
7102
  }
6700
7103
  function installedHookPath() {
6701
- return path13.join(neatHome2(), "hooks", HOOK_FILENAME);
7104
+ return path15.join(neatHome2(), "hooks", HOOK_FILENAME);
6702
7105
  }
6703
7106
  function gateFlagPath() {
6704
- return path13.join(neatHome2(), "hooks", "gate-enabled");
7107
+ return path15.join(neatHome2(), "hooks", "gate-enabled");
6705
7108
  }
6706
7109
  function isNeatSearchEntry(entry2) {
6707
7110
  return (entry2.hooks ?? []).some(
@@ -6734,14 +7137,14 @@ async function runHooks(opts) {
6734
7137
  const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
6735
7138
  const guide = await readSkillAsset(GUIDE_FILENAME);
6736
7139
  const scriptPath = installedHookPath();
6737
- await fs12.mkdir(path13.dirname(scriptPath), { recursive: true });
6738
- await fs12.writeFile(scriptPath, hookScript, { mode: 493 });
6739
- const guidePath = path13.join(neatHome2(), GUIDE_INSTALL_NAME);
6740
- await fs12.writeFile(guidePath, guide, "utf8");
7140
+ await fs14.mkdir(path15.dirname(scriptPath), { recursive: true });
7141
+ await fs14.writeFile(scriptPath, hookScript, { mode: 493 });
7142
+ const guidePath = path15.join(neatHome2(), GUIDE_INSTALL_NAME);
7143
+ await fs14.writeFile(guidePath, guide, "utf8");
6741
7144
  const settingsFile = claudeSettingsPath();
6742
7145
  let settings = {};
6743
7146
  try {
6744
- settings = JSON.parse(await fs12.readFile(settingsFile, "utf8"));
7147
+ settings = JSON.parse(await fs14.readFile(settingsFile, "utf8"));
6745
7148
  } catch (err) {
6746
7149
  if (err.code !== "ENOENT") {
6747
7150
  console.error(
@@ -6763,14 +7166,14 @@ async function runHooks(opts) {
6763
7166
  ...settings,
6764
7167
  hooks: { ...hooks, PreToolUse: preToolUse }
6765
7168
  };
6766
- await fs12.mkdir(path13.dirname(settingsFile), { recursive: true });
6767
- await fs12.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
7169
+ await fs14.mkdir(path15.dirname(settingsFile), { recursive: true });
7170
+ await fs14.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
6768
7171
  const flag = gateFlagPath();
6769
7172
  if (opts.gate) {
6770
- await fs12.mkdir(path13.dirname(flag), { recursive: true });
6771
- await fs12.writeFile(flag, "1\n", "utf8");
7173
+ await fs14.mkdir(path15.dirname(flag), { recursive: true });
7174
+ await fs14.writeFile(flag, "1\n", "utf8");
6772
7175
  } else {
6773
- await fs12.rm(flag, { force: true });
7176
+ await fs14.rm(flag, { force: true });
6774
7177
  }
6775
7178
  const mode = opts.gate ? "GATE (deny search until you ask the graph)" : "nudge (search still runs)";
6776
7179
  console.log(`neat hooks: installed the search hook in ${opts.gate ? "gate" : "nudge"} mode`);
@@ -6859,8 +7262,8 @@ async function runHooksCommand(args) {
6859
7262
  }
6860
7263
 
6861
7264
  // src/claude-cli.ts
6862
- import path14 from "path";
6863
- import { promises as fs13 } from "fs";
7265
+ import path16 from "path";
7266
+ import { promises as fs15 } from "fs";
6864
7267
  var NEAT_SECTION_HEADING = "## neat";
6865
7268
  var NEAT_DIRECTIVE_BODY = `This project has NEAT wired in: a live, fused semantic graph of the system \u2014
6866
7269
  code and runtime behaviour (OpenTelemetry) in one model, every fact tagged with
@@ -6894,8 +7297,8 @@ ${NEAT_DIRECTIVE_BODY}
6894
7297
  }
6895
7298
  function claudeMdPath() {
6896
7299
  const override = process.env.NEAT_CLAUDE_MD;
6897
- if (override && override.length > 0) return path14.resolve(override);
6898
- return path14.join(process.cwd(), "CLAUDE.md");
7300
+ if (override && override.length > 0) return path16.resolve(override);
7301
+ return path16.join(process.cwd(), "CLAUDE.md");
6899
7302
  }
6900
7303
  function splitAroundSection(raw) {
6901
7304
  const lines = raw.split("\n");
@@ -6923,7 +7326,7 @@ function compose(before, after) {
6923
7326
  }
6924
7327
  async function readIfExists(file) {
6925
7328
  try {
6926
- return await fs13.readFile(file, "utf8");
7329
+ return await fs15.readFile(file, "utf8");
6927
7330
  } catch (err) {
6928
7331
  if (err.code === "ENOENT") return null;
6929
7332
  throw err;
@@ -6934,8 +7337,8 @@ async function runInstall() {
6934
7337
  const raw = await readIfExists(file) ?? "";
6935
7338
  const { before, after, found } = splitAroundSection(raw);
6936
7339
  const next = compose(before, after);
6937
- await fs13.mkdir(path14.dirname(file), { recursive: true });
6938
- await fs13.writeFile(file, next, "utf8");
7340
+ await fs15.mkdir(path16.dirname(file), { recursive: true });
7341
+ await fs15.writeFile(file, next, "utf8");
6939
7342
  const verb = raw.length === 0 ? "created" : found ? "refreshed" : "added";
6940
7343
  console.log(`neat claude: ${verb} the \`${NEAT_SECTION_HEADING}\` section in ${file}`);
6941
7344
  console.log("Your agent will now reach for `neat ask` before Read/Grep/Bash. Restart the");
@@ -6956,7 +7359,7 @@ async function runUninstall() {
6956
7359
  }
6957
7360
  const remaining = [before, after].filter((s) => s.length > 0).join("\n\n");
6958
7361
  const next = remaining.length > 0 ? remaining.replace(/\n*$/, "") + "\n" : "";
6959
- await fs13.writeFile(file, next, "utf8");
7362
+ await fs15.writeFile(file, next, "utf8");
6960
7363
  console.log(`neat claude: removed the \`${NEAT_SECTION_HEADING}\` section from ${file}.`);
6961
7364
  return { exitCode: 0 };
6962
7365
  }
@@ -6998,9 +7401,9 @@ async function runClaudeCommand(args) {
6998
7401
  }
6999
7402
 
7000
7403
  // src/codex-cli.ts
7001
- import path15 from "path";
7404
+ import path17 from "path";
7002
7405
  import os3 from "os";
7003
- import { promises as fs14 } from "fs";
7406
+ import { promises as fs16 } from "fs";
7004
7407
  import { isDeepStrictEqual } from "util";
7005
7408
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
7006
7409
  var CODEX_MCP_SERVER = {
@@ -7018,14 +7421,14 @@ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
7018
7421
  var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
7019
7422
  function codexConfigPath() {
7020
7423
  const override = process.env.NEAT_CODEX_CONFIG;
7021
- if (override && override.length > 0) return path15.resolve(override);
7424
+ if (override && override.length > 0) return path17.resolve(override);
7022
7425
  const home = process.env.HOME ?? process.env.USERPROFILE ?? os3.homedir();
7023
- return path15.join(home, ".codex", "config.toml");
7426
+ return path17.join(home, ".codex", "config.toml");
7024
7427
  }
7025
7428
  function agentsFilePath() {
7026
7429
  const override = process.env.NEAT_CODEX_AGENTS;
7027
- if (override && override.length > 0) return path15.resolve(override);
7028
- return path15.join(process.cwd(), "AGENTS.md");
7430
+ if (override && override.length > 0) return path17.resolve(override);
7431
+ return path17.join(process.cwd(), "AGENTS.md");
7029
7432
  }
7030
7433
  function isTableHeader(line) {
7031
7434
  return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
@@ -7159,7 +7562,7 @@ async function runCodex(opts) {
7159
7562
  const agentsPath = agentsFilePath();
7160
7563
  let configRaw = "";
7161
7564
  try {
7162
- configRaw = await fs14.readFile(configPath, "utf8");
7565
+ configRaw = await fs16.readFile(configPath, "utf8");
7163
7566
  } catch (err) {
7164
7567
  if (err.code !== "ENOENT") {
7165
7568
  console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
@@ -7168,7 +7571,7 @@ async function runCodex(opts) {
7168
7571
  }
7169
7572
  let agentsRaw = "";
7170
7573
  try {
7171
- agentsRaw = await fs14.readFile(agentsPath, "utf8");
7574
+ agentsRaw = await fs16.readFile(agentsPath, "utf8");
7172
7575
  } catch (err) {
7173
7576
  if (err.code !== "ENOENT") {
7174
7577
  console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
@@ -7208,15 +7611,15 @@ async function runCodex(opts) {
7208
7611
  return { exitCode: 0 };
7209
7612
  }
7210
7613
  if (config.changed) {
7211
- await fs14.mkdir(path15.dirname(configPath), { recursive: true });
7212
- await fs14.writeFile(configPath, config.text, "utf8");
7614
+ await fs16.mkdir(path17.dirname(configPath), { recursive: true });
7615
+ await fs16.writeFile(configPath, config.text, "utf8");
7213
7616
  console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
7214
7617
  } else {
7215
7618
  console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
7216
7619
  }
7217
7620
  if (agents.changed) {
7218
- await fs14.mkdir(path15.dirname(agentsPath), { recursive: true });
7219
- await fs14.writeFile(agentsPath, agents.text, "utf8");
7621
+ await fs16.mkdir(path17.dirname(agentsPath), { recursive: true });
7622
+ await fs16.writeFile(agentsPath, agents.text, "utf8");
7220
7623
  console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
7221
7624
  } else {
7222
7625
  console.log(`neat codex: ${agentsPath} already has the graph-first block`);
@@ -7272,9 +7675,9 @@ async function runCodexCommand(args) {
7272
7675
  }
7273
7676
 
7274
7677
  // src/editors-cli.ts
7275
- import path16 from "path";
7678
+ import path18 from "path";
7276
7679
  import os4 from "os";
7277
- import { promises as fs15 } from "fs";
7680
+ import { promises as fs17 } from "fs";
7278
7681
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
7279
7682
  import * as jsonc from "jsonc-parser";
7280
7683
  var NEAT_MCP_SERVER = {
@@ -7298,17 +7701,17 @@ function homeDir() {
7298
7701
  }
7299
7702
  function xdgConfigDir() {
7300
7703
  const xdg = process.env.XDG_CONFIG_HOME;
7301
- return xdg && xdg.length > 0 ? path16.resolve(xdg) : path16.join(homeDir(), ".config");
7704
+ return xdg && xdg.length > 0 ? path18.resolve(xdg) : path18.join(homeDir(), ".config");
7302
7705
  }
7303
7706
  function envOverride(name) {
7304
7707
  const v = process.env[name];
7305
- return v && v.length > 0 ? path16.resolve(v) : void 0;
7708
+ return v && v.length > 0 ? path18.resolve(v) : void 0;
7306
7709
  }
7307
7710
  var CURSOR_CLIENT = {
7308
7711
  id: "cursor",
7309
7712
  label: "Cursor",
7310
7713
  docsUrl: "https://docs.cursor.com/context/mcp",
7311
- mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? path16.join(homeDir(), ".cursor", "mcp.json"),
7714
+ mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? path18.join(homeDir(), ".cursor", "mcp.json"),
7312
7715
  mcpContainerKey: "mcpServers",
7313
7716
  format: "json",
7314
7717
  // Cursor still reads a single `.cursorrules` at the project root (the modern
@@ -7320,7 +7723,7 @@ var DEVIN_CLIENT = {
7320
7723
  id: "devin",
7321
7724
  label: "Devin Desktop (Cascade)",
7322
7725
  docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
7323
- mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? path16.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
7726
+ mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? path18.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
7324
7727
  mcpContainerKey: "mcpServers",
7325
7728
  format: "json",
7326
7729
  rulesFileName: ".windsurfrules"
@@ -7329,7 +7732,7 @@ var GEMINI_CLIENT = {
7329
7732
  id: "gemini",
7330
7733
  label: "Gemini CLI",
7331
7734
  docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
7332
- mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? path16.join(homeDir(), ".gemini", "settings.json"),
7735
+ mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? path18.join(homeDir(), ".gemini", "settings.json"),
7333
7736
  mcpContainerKey: "mcpServers",
7334
7737
  format: "json",
7335
7738
  rulesFileName: "GEMINI.md"
@@ -7338,7 +7741,7 @@ var QWEN_CLIENT = {
7338
7741
  id: "qwen",
7339
7742
  label: "Qwen Code",
7340
7743
  docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
7341
- mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? path16.join(homeDir(), ".qwen", "settings.json"),
7744
+ mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? path18.join(homeDir(), ".qwen", "settings.json"),
7342
7745
  mcpContainerKey: "mcpServers",
7343
7746
  format: "json",
7344
7747
  rulesFileName: "QWEN.md"
@@ -7347,7 +7750,7 @@ var AMAZONQ_CLIENT = {
7347
7750
  id: "amazonq",
7348
7751
  label: "Amazon Q Developer CLI",
7349
7752
  docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
7350
- mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? path16.join(homeDir(), ".aws", "amazonq", "mcp.json"),
7753
+ mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? path18.join(homeDir(), ".aws", "amazonq", "mcp.json"),
7351
7754
  mcpContainerKey: "mcpServers",
7352
7755
  format: "json"
7353
7756
  };
@@ -7355,7 +7758,7 @@ var ROOCODE_CLIENT = {
7355
7758
  id: "roocode",
7356
7759
  label: "Roo Code",
7357
7760
  docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
7358
- mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? path16.join(process.cwd(), ".roo", "mcp.json"),
7761
+ mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? path18.join(process.cwd(), ".roo", "mcp.json"),
7359
7762
  mcpContainerKey: "mcpServers",
7360
7763
  format: "json"
7361
7764
  };
@@ -7368,9 +7771,9 @@ var ZED_CLIENT = {
7368
7771
  if (override) return override;
7369
7772
  if (process.platform === "win32") {
7370
7773
  const appData = process.env.APPDATA;
7371
- if (appData && appData.length > 0) return path16.join(appData, "Zed", "settings.json");
7774
+ if (appData && appData.length > 0) return path18.join(appData, "Zed", "settings.json");
7372
7775
  }
7373
- return path16.join(homeDir(), ".config", "zed", "settings.json");
7776
+ return path18.join(homeDir(), ".config", "zed", "settings.json");
7374
7777
  },
7375
7778
  mcpContainerKey: "context_servers",
7376
7779
  format: "jsonc",
@@ -7380,7 +7783,7 @@ var OPENCODE_CLIENT = {
7380
7783
  id: "opencode",
7381
7784
  label: "OpenCode",
7382
7785
  docsUrl: "https://opencode.ai/docs/mcp-servers/",
7383
- mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? path16.join(xdgConfigDir(), "opencode", "opencode.json"),
7786
+ mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? path18.join(xdgConfigDir(), "opencode", "opencode.json"),
7384
7787
  mcpContainerKey: "mcp",
7385
7788
  format: "json",
7386
7789
  serverEntry: NEAT_OPENCODE_SERVER,
@@ -7390,7 +7793,7 @@ var CRUSH_CLIENT = {
7390
7793
  id: "crush",
7391
7794
  label: "Crush",
7392
7795
  docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
7393
- mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? path16.join(xdgConfigDir(), "crush", "crush.json"),
7796
+ mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? path18.join(xdgConfigDir(), "crush", "crush.json"),
7394
7797
  mcpContainerKey: "mcp",
7395
7798
  format: "json",
7396
7799
  serverEntry: NEAT_CRUSH_SERVER,
@@ -7453,7 +7856,7 @@ async function planMcp(client, mcpPath) {
7453
7856
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
7454
7857
  let raw = "";
7455
7858
  try {
7456
- raw = await fs15.readFile(mcpPath, "utf8");
7859
+ raw = await fs17.readFile(mcpPath, "utf8");
7457
7860
  } catch (err) {
7458
7861
  const e = err;
7459
7862
  if (e.code === "ENOENT") {
@@ -7495,7 +7898,7 @@ async function runEditorInstall(client, opts) {
7495
7898
  const mcpPath = client.mcpConfigPath();
7496
7899
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
7497
7900
  const hasRules = typeof client.rulesFileName === "string";
7498
- const rulesPath = hasRules ? path16.join(opts.projectDir, client.rulesFileName) : "";
7901
+ const rulesPath = hasRules ? path18.join(opts.projectDir, client.rulesFileName) : "";
7499
7902
  const mcp = await planMcp(client, mcpPath);
7500
7903
  if (mcp === null) return { exitCode: 1 };
7501
7904
  let existingRules = "";
@@ -7504,7 +7907,7 @@ async function runEditorInstall(client, opts) {
7504
7907
  let block = "";
7505
7908
  if (hasRules) {
7506
7909
  try {
7507
- existingRules = await fs15.readFile(rulesPath, "utf8");
7910
+ existingRules = await fs17.readFile(rulesPath, "utf8");
7508
7911
  } catch (err) {
7509
7912
  if (err.code !== "ENOENT") {
7510
7913
  console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
@@ -7538,11 +7941,11 @@ async function runEditorInstall(client, opts) {
7538
7941
  );
7539
7942
  return { exitCode: 0 };
7540
7943
  }
7541
- await fs15.mkdir(path16.dirname(mcpPath), { recursive: true });
7542
- await fs15.writeFile(mcpPath, mcp.text, "utf8");
7944
+ await fs17.mkdir(path18.dirname(mcpPath), { recursive: true });
7945
+ await fs17.writeFile(mcpPath, mcp.text, "utf8");
7543
7946
  if (hasRules) {
7544
- await fs15.mkdir(path16.dirname(rulesPath), { recursive: true });
7545
- await fs15.writeFile(rulesPath, newRules, "utf8");
7947
+ await fs17.mkdir(path18.dirname(rulesPath), { recursive: true });
7948
+ await fs17.writeFile(rulesPath, newRules, "utf8");
7546
7949
  }
7547
7950
  console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
7548
7951
  console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
@@ -8042,7 +8445,7 @@ function sleep(ms, signal) {
8042
8445
  }
8043
8446
 
8044
8447
  // src/cli-verbs.ts
8045
- import path17 from "path";
8448
+ import path19 from "path";
8046
8449
  async function resolveProjectEntry(opts) {
8047
8450
  const entries = await listProjects();
8048
8451
  if (opts.project) {
@@ -8052,7 +8455,7 @@ async function resolveProjectEntry(opts) {
8052
8455
  const cwd = opts.cwd ?? process.cwd();
8053
8456
  const resolvedCwd = await normalizeProjectPath(cwd);
8054
8457
  for (const entry2 of entries) {
8055
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path17.sep}`)) {
8458
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path19.sep}`)) {
8056
8459
  return entry2;
8057
8460
  }
8058
8461
  }
@@ -8629,7 +9032,7 @@ async function buildPatchSections(services, project) {
8629
9032
  }
8630
9033
  async function runInit(opts) {
8631
9034
  const written = [];
8632
- const stat = await fs16.stat(opts.scanPath).catch(() => null);
9035
+ const stat = await fs18.stat(opts.scanPath).catch(() => null);
8633
9036
  if (!stat || !stat.isDirectory()) {
8634
9037
  console.error(`neat init: ${opts.scanPath} is not a directory`);
8635
9038
  return { exitCode: 2, writtenFiles: written };
@@ -8638,13 +9041,13 @@ async function runInit(opts) {
8638
9041
  printDiscoveryReport(opts, services);
8639
9042
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
8640
9043
  const patch = renderPatch(sections);
8641
- const patchPath = path18.join(opts.scanPath, "neat.patch");
9044
+ const patchPath = path20.join(opts.scanPath, "neat.patch");
8642
9045
  if (opts.dryRun) {
8643
- await fs16.writeFile(patchPath, patch, "utf8");
9046
+ await fs18.writeFile(patchPath, patch, "utf8");
8644
9047
  written.push(patchPath);
8645
9048
  console.log(`dry-run: patch written to ${patchPath}`);
8646
- const gitignorePath = path18.join(opts.scanPath, ".gitignore");
8647
- const gitignoreExists = await fs16.stat(gitignorePath).then(() => true).catch(() => false);
9049
+ const gitignorePath = path20.join(opts.scanPath, ".gitignore");
9050
+ const gitignoreExists = await fs18.stat(gitignorePath).then(() => true).catch(() => false);
8648
9051
  const verb = gitignoreExists ? "append" : "create";
8649
9052
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
8650
9053
  console.log("rerun without --dry-run to register and snapshot.");
@@ -8655,9 +9058,9 @@ async function runInit(opts) {
8655
9058
  const graph = getGraph(graphKey);
8656
9059
  const projectPaths = pathsForProject(
8657
9060
  graphKey,
8658
- path18.join(opts.scanPath, "neat-out")
9061
+ path20.join(opts.scanPath, "neat-out")
8659
9062
  );
8660
- const errorsPath = path18.join(path18.dirname(opts.outPath), path18.basename(projectPaths.errorsPath));
9063
+ const errorsPath = path20.join(path20.dirname(opts.outPath), path20.basename(projectPaths.errorsPath));
8661
9064
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
8662
9065
  await saveGraphToDisk(graph, opts.outPath);
8663
9066
  written.push(opts.outPath);
@@ -8736,7 +9139,7 @@ async function runInit(opts) {
8736
9139
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
8737
9140
  }
8738
9141
  } else {
8739
- await fs16.writeFile(patchPath, patch, "utf8");
9142
+ await fs18.writeFile(patchPath, patch, "utf8");
8740
9143
  written.push(patchPath);
8741
9144
  }
8742
9145
  }
@@ -8777,9 +9180,9 @@ var CLAUDE_SKILL_CONFIG = {
8777
9180
  };
8778
9181
  function claudeConfigPath() {
8779
9182
  const override = process.env.NEAT_CLAUDE_CONFIG;
8780
- if (override && override.length > 0) return path18.resolve(override);
9183
+ if (override && override.length > 0) return path20.resolve(override);
8781
9184
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
8782
- return path18.join(home, ".claude.json");
9185
+ return path20.join(home, ".claude.json");
8783
9186
  }
8784
9187
  async function runSkill(opts) {
8785
9188
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -8791,7 +9194,7 @@ async function runSkill(opts) {
8791
9194
  const target = claudeConfigPath();
8792
9195
  let existing = {};
8793
9196
  try {
8794
- existing = JSON.parse(await fs16.readFile(target, "utf8"));
9197
+ existing = JSON.parse(await fs18.readFile(target, "utf8"));
8795
9198
  } catch (err) {
8796
9199
  if (err.code !== "ENOENT") {
8797
9200
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -8803,8 +9206,8 @@ async function runSkill(opts) {
8803
9206
  ...existing,
8804
9207
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
8805
9208
  };
8806
- await fs16.mkdir(path18.dirname(target), { recursive: true });
8807
- await fs16.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
9209
+ await fs18.mkdir(path20.dirname(target), { recursive: true });
9210
+ await fs18.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
8808
9211
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
8809
9212
  console.log("restart Claude Code to pick up the new MCP server.");
8810
9213
  console.log("");
@@ -8830,7 +9233,7 @@ async function runSkill(opts) {
8830
9233
  async function main() {
8831
9234
  const argv = process.argv.slice(2);
8832
9235
  const cmd0 = argv[0];
8833
- if (cmd0 === "-h" || cmd0 === "--help") {
9236
+ if (argv.some((a) => a === "-h" || a === "--help")) {
8834
9237
  usage5();
8835
9238
  process.exit(0);
8836
9239
  }
@@ -8925,12 +9328,12 @@ async function main() {
8925
9328
  console.error("neat init: --apply and --dry-run are mutually exclusive");
8926
9329
  process.exit(2);
8927
9330
  }
8928
- const scanPath = path18.resolve(target);
9331
+ const scanPath = path20.resolve(target);
8929
9332
  const projectExplicit = parsed.project !== null;
8930
- const projectName = projectExplicit ? project : path18.basename(scanPath);
9333
+ const projectName = projectExplicit ? project : path20.basename(scanPath);
8931
9334
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
8932
- const fallback = pathsForProject(projectKey, path18.join(scanPath, "neat-out")).snapshotPath;
8933
- const outPath = path18.resolve(process.env.NEAT_OUT_PATH ?? fallback);
9335
+ const fallback = pathsForProject(projectKey, path20.join(scanPath, "neat-out")).snapshotPath;
9336
+ const outPath = path20.resolve(process.env.NEAT_OUT_PATH ?? fallback);
8934
9337
  const result = await runInit({
8935
9338
  scanPath,
8936
9339
  outPath,
@@ -8951,21 +9354,21 @@ async function main() {
8951
9354
  usage5();
8952
9355
  process.exit(2);
8953
9356
  }
8954
- const scanPath = path18.resolve(target);
8955
- const stat = await fs16.stat(scanPath).catch(() => null);
9357
+ const scanPath = path20.resolve(target);
9358
+ const stat = await fs18.stat(scanPath).catch(() => null);
8956
9359
  if (!stat || !stat.isDirectory()) {
8957
9360
  console.error(`neat watch: ${scanPath} is not a directory`);
8958
9361
  process.exit(2);
8959
9362
  }
8960
- const projectPaths = pathsForProject(project, path18.join(scanPath, "neat-out"));
8961
- const outPath = path18.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
8962
- const errorsPath = path18.resolve(
8963
- process.env.NEAT_ERRORS_PATH ?? path18.join(path18.dirname(outPath), path18.basename(projectPaths.errorsPath))
9363
+ const projectPaths = pathsForProject(project, path20.join(scanPath, "neat-out"));
9364
+ const outPath = path20.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
9365
+ const errorsPath = path20.resolve(
9366
+ process.env.NEAT_ERRORS_PATH ?? path20.join(path20.dirname(outPath), path20.basename(projectPaths.errorsPath))
8964
9367
  );
8965
- const staleEventsPath = path18.resolve(
8966
- process.env.NEAT_STALE_EVENTS_PATH ?? path18.join(path18.dirname(outPath), path18.basename(projectPaths.staleEventsPath))
9368
+ const staleEventsPath = path20.resolve(
9369
+ process.env.NEAT_STALE_EVENTS_PATH ?? path20.join(path20.dirname(outPath), path20.basename(projectPaths.staleEventsPath))
8967
9370
  );
8968
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path18.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
9371
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path20.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
8969
9372
  const handle = await startWatch(getGraph(project), {
8970
9373
  scanPath,
8971
9374
  outPath,
@@ -8974,7 +9377,7 @@ async function main() {
8974
9377
  project,
8975
9378
  // Resolve NEAT_HOME so a `neat watch` picks up connectors added to
8976
9379
  // ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
8977
- neatHome: process.env.NEAT_HOME ? path18.resolve(process.env.NEAT_HOME) : path18.join(os5.homedir(), ".neat"),
9380
+ neatHome: process.env.NEAT_HOME ? path20.resolve(process.env.NEAT_HOME) : path20.join(os5.homedir(), ".neat"),
8978
9381
  ...embeddingsCachePath ? { embeddingsCachePath } : {},
8979
9382
  host: process.env.HOST ?? "0.0.0.0",
8980
9383
  port: Number(process.env.PORT ?? 8080),
@@ -9156,11 +9559,11 @@ async function main() {
9156
9559
  process.exit(1);
9157
9560
  }
9158
9561
  async function tryOrchestrator(cmd, parsed) {
9159
- const scanPath = path18.resolve(cmd);
9160
- const stat = await fs16.stat(scanPath).catch(() => null);
9562
+ const scanPath = path20.resolve(cmd);
9563
+ const stat = await fs18.stat(scanPath).catch(() => null);
9161
9564
  if (!stat || !stat.isDirectory()) return null;
9162
9565
  const projectExplicit = parsed.project !== null;
9163
- const projectName = projectExplicit ? parsed.project : path18.basename(scanPath);
9566
+ const projectName = projectExplicit ? parsed.project : path20.basename(scanPath);
9164
9567
  const result = await runOrchestrator({
9165
9568
  scanPath,
9166
9569
  project: projectName,
@@ -9175,7 +9578,18 @@ async function tryOrchestrator(cmd, parsed) {
9175
9578
  async function runWelcomeFlow(parsed) {
9176
9579
  return runWelcome({
9177
9580
  login: (loginArgv) => runLoginCommand(loginArgv),
9178
- orchestrator: async (cwd) => await tryOrchestrator(cwd, parsed) ?? 0
9581
+ // What the person chose at the door reaches the orchestrator as the flags they
9582
+ // stand for: the name as `--project`, the instrument answer as `--no-instrument`
9583
+ // or as `yes` (which keeps the orchestrator's own prompt quiet, since the door
9584
+ // already asked).
9585
+ orchestrator: async (cwd, opts) => await tryOrchestrator(cwd, {
9586
+ ...parsed,
9587
+ ...opts?.project !== void 0 ? { project: opts.project } : {},
9588
+ ...opts?.noInstrument !== void 0 ? { noInstrument: opts.noInstrument } : {},
9589
+ ...opts?.yes !== void 0 ? { yes: opts.yes } : {}
9590
+ }) ?? 0,
9591
+ // An explicit flag is an answer; don't ask again.
9592
+ instrumentFlagGiven: parsed.noInstrument || parsed.dryRun
9179
9593
  });
9180
9594
  }
9181
9595
  var QUERY_VERBS = /* @__PURE__ */ new Set([
@@ -9208,6 +9622,13 @@ var ProjectResolutionError = class extends Error {
9208
9622
  }
9209
9623
  exitCode;
9210
9624
  };
9625
+ function isLoopbackEndpoint(endpoint) {
9626
+ try {
9627
+ return isLoopbackHost(new URL(endpoint).hostname.replace(/^\[|\]$/g, ""));
9628
+ } catch {
9629
+ return false;
9630
+ }
9631
+ }
9211
9632
  async function resolveProjectForVerb(client, parsed) {
9212
9633
  const explicit = resolveProjectFlag(parsed);
9213
9634
  if (explicit) return explicit;
@@ -9438,6 +9859,9 @@ async function runQueryVerb(cmd, parsed) {
9438
9859
  console.error(`neat ${cmd}: ${detail.trim()}`);
9439
9860
  } else if (err instanceof TransportError) {
9440
9861
  console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (endpoint=${target.endpoint})`);
9862
+ if (isLoopbackEndpoint(target.endpoint)) {
9863
+ console.error(`neat ${cmd}: start one with \`neat watch .\`, or \`neat\` to extract and start in one step.`);
9864
+ }
9441
9865
  } else {
9442
9866
  console.error(`neat ${cmd}: ${err.message}`);
9443
9867
  }
@@ -9493,6 +9917,7 @@ export {
9493
9917
  QUERY_VERBS,
9494
9918
  UnknownProfileError,
9495
9919
  commandPrefix,
9920
+ isLoopbackEndpoint,
9496
9921
  isNpxInvocation,
9497
9922
  main,
9498
9923
  parseArgs,