@neat.is/core 0.9.15-dev.20260906 → 0.9.15-dev.20260908

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
@@ -91,9 +91,9 @@ import {
91
91
  } from "./chunk-GDGUY4T6.js";
92
92
 
93
93
  // src/cli.ts
94
- import path17 from "path";
95
- import os4 from "os";
96
- import { promises as fs15 } from "fs";
94
+ import path18 from "path";
95
+ import os5 from "os";
96
+ import { promises as fs16 } from "fs";
97
97
 
98
98
  // src/banner.ts
99
99
  import path from "path";
@@ -4871,10 +4871,10 @@ function createHttpClient(baseUrl, bearerToken) {
4871
4871
  const root = baseUrl.replace(/\/$/, "");
4872
4872
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
4873
4873
  return {
4874
- async get(path18) {
4874
+ async get(path19) {
4875
4875
  let res;
4876
4876
  try {
4877
- res = await fetch(`${root}${path18}`, {
4877
+ res = await fetch(`${root}${path19}`, {
4878
4878
  headers: { ...authHeader }
4879
4879
  });
4880
4880
  } catch (err) {
@@ -4886,16 +4886,16 @@ function createHttpClient(baseUrl, bearerToken) {
4886
4886
  const body = await res.text().catch(() => "");
4887
4887
  throw new HttpError(
4888
4888
  res.status,
4889
- `${res.status} ${res.statusText} on GET ${path18}: ${body}`,
4889
+ `${res.status} ${res.statusText} on GET ${path19}: ${body}`,
4890
4890
  body
4891
4891
  );
4892
4892
  }
4893
4893
  return await res.json();
4894
4894
  },
4895
- async post(path18, body) {
4895
+ async post(path19, body) {
4896
4896
  let res;
4897
4897
  try {
4898
- res = await fetch(`${root}${path18}`, {
4898
+ res = await fetch(`${root}${path19}`, {
4899
4899
  method: "POST",
4900
4900
  headers: { "content-type": "application/json", ...authHeader },
4901
4901
  body: JSON.stringify(body)
@@ -4909,7 +4909,7 @@ function createHttpClient(baseUrl, bearerToken) {
4909
4909
  const text = await res.text().catch(() => "");
4910
4910
  throw new HttpError(
4911
4911
  res.status,
4912
- `${res.status} ${res.statusText} on POST ${path18}: ${text}`,
4912
+ `${res.status} ${res.statusText} on POST ${path19}: ${text}`,
4913
4913
  text
4914
4914
  );
4915
4915
  }
@@ -4923,12 +4923,12 @@ function projectPath(project, suffix) {
4923
4923
  }
4924
4924
  async function runRootCause(client, input) {
4925
4925
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
4926
- const path18 = projectPath(
4926
+ const path19 = projectPath(
4927
4927
  input.project,
4928
4928
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
4929
4929
  );
4930
4930
  try {
4931
- const result = await client.get(path18);
4931
+ const result = await client.get(path19);
4932
4932
  const arrowPath = result.traversalPath.join(" \u2190 ");
4933
4933
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
4934
4934
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -4954,12 +4954,12 @@ async function runRootCause(client, input) {
4954
4954
  }
4955
4955
  async function runBlastRadius(client, input) {
4956
4956
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
4957
- const path18 = projectPath(
4957
+ const path19 = projectPath(
4958
4958
  input.project,
4959
4959
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
4960
4960
  );
4961
4961
  try {
4962
- const result = await client.get(path18);
4962
+ const result = await client.get(path19);
4963
4963
  if (result.totalAffected === 0) {
4964
4964
  return {
4965
4965
  summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
@@ -4993,12 +4993,12 @@ function formatBlastEntry(n) {
4993
4993
  }
4994
4994
  async function runDependencies(client, input) {
4995
4995
  const depth = input.depth ?? 3;
4996
- const path18 = projectPath(
4996
+ const path19 = projectPath(
4997
4997
  input.project,
4998
4998
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
4999
4999
  );
5000
5000
  try {
5001
- const result = await client.get(path18);
5001
+ const result = await client.get(path19);
5002
5002
  if (result.total === 0) {
5003
5003
  return {
5004
5004
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -5090,9 +5090,9 @@ function formatDuration(ms) {
5090
5090
  return `${Math.round(h / 24)}d`;
5091
5091
  }
5092
5092
  async function runIncidents(client, input) {
5093
- const path18 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
5093
+ const path19 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
5094
5094
  try {
5095
- const body = await client.get(path18);
5095
+ const body = await client.get(path19);
5096
5096
  const events = body.events;
5097
5097
  if (events.length === 0) {
5098
5098
  return {
@@ -5563,28 +5563,462 @@ async function runDoctorCommand(argv, deps = {}) {
5563
5563
  return checks.every((c) => c.ok) ? 0 : 1;
5564
5564
  }
5565
5565
 
5566
- // src/hooks-cli.ts
5567
- import path12 from "path";
5568
- import os from "os";
5566
+ // src/login-cli.ts
5567
+ import readline3 from "readline/promises";
5568
+
5569
+ // src/profiles.ts
5569
5570
  import { promises as fs11 } from "fs";
5571
+ import os from "os";
5572
+ import path12 from "path";
5573
+ var PROFILES_CONFIG_VERSION = 1;
5574
+ function neatHome() {
5575
+ const override = process.env.NEAT_HOME;
5576
+ if (override && override.length > 0) return path12.resolve(override);
5577
+ return path12.join(os.homedir(), ".neat");
5578
+ }
5579
+ function profilesConfigPath(home = neatHome()) {
5580
+ return path12.join(home, "profiles.json");
5581
+ }
5582
+ function profilesConfigLockPath(home = neatHome()) {
5583
+ return path12.join(home, "profiles.json.lock");
5584
+ }
5585
+ var MODE_MASK_LOOSER_THAN_0600 = 63;
5586
+ async function warnIfModeLooserThan0600(file) {
5587
+ if (process.platform === "win32") return;
5588
+ try {
5589
+ const stat = await fs11.stat(file);
5590
+ if ((stat.mode & MODE_MASK_LOOSER_THAN_0600) !== 0) {
5591
+ const mode = (stat.mode & 511).toString(8).padStart(3, "0");
5592
+ console.warn(
5593
+ `[neat] ${file} is mode 0${mode}, looser than the 0600 this file's token calls for \u2014 run \`chmod 600 ${file}\``
5594
+ );
5595
+ }
5596
+ } catch {
5597
+ }
5598
+ }
5599
+ async function readProfilesConfig(home = neatHome()) {
5600
+ const file = profilesConfigPath(home);
5601
+ let raw;
5602
+ try {
5603
+ raw = await fs11.readFile(file, "utf8");
5604
+ } catch (err) {
5605
+ if (err.code === "ENOENT") {
5606
+ return { version: PROFILES_CONFIG_VERSION, profiles: [] };
5607
+ }
5608
+ throw err;
5609
+ }
5610
+ await warnIfModeLooserThan0600(file);
5611
+ let parsed;
5612
+ try {
5613
+ parsed = JSON.parse(raw);
5614
+ } catch (err) {
5615
+ throw new Error(`${file} is not valid JSON: ${err.message}`);
5616
+ }
5617
+ return validateConfig(parsed, file);
5618
+ }
5619
+ function validateConfig(parsed, file) {
5620
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5621
+ throw new Error(`${file} must be a JSON object with a "profiles" array`);
5622
+ }
5623
+ const obj = parsed;
5624
+ const version = obj.version === void 0 ? PROFILES_CONFIG_VERSION : obj.version;
5625
+ if (typeof version !== "number" || !Number.isInteger(version)) {
5626
+ throw new Error(`${file}: "version" must be an integer`);
5627
+ }
5628
+ const rawProfiles = obj.profiles;
5629
+ if (!Array.isArray(rawProfiles)) {
5630
+ throw new Error(`${file}: "profiles" must be an array`);
5631
+ }
5632
+ const profiles = rawProfiles.map((entry2, i) => validateEntry(entry2, i, file));
5633
+ const seen = /* @__PURE__ */ new Set();
5634
+ for (const p of profiles) {
5635
+ if (seen.has(p.name)) throw new Error(`${file}: duplicate profile name "${p.name}"`);
5636
+ seen.add(p.name);
5637
+ }
5638
+ let active;
5639
+ if (obj.active !== void 0) {
5640
+ if (typeof obj.active !== "string" || obj.active.length === 0) {
5641
+ throw new Error(`${file}: "active" must be a non-empty string when present`);
5642
+ }
5643
+ active = seen.has(obj.active) ? obj.active : void 0;
5644
+ }
5645
+ return { version, ...active ? { active } : {}, profiles };
5646
+ }
5647
+ function validateEntry(entry2, index, file) {
5648
+ const where = `${file}: profiles[${index}]`;
5649
+ if (typeof entry2 !== "object" || entry2 === null || Array.isArray(entry2)) {
5650
+ throw new Error(`${where} must be an object`);
5651
+ }
5652
+ const e = entry2;
5653
+ const name = e.name;
5654
+ if (typeof name !== "string" || name.length === 0) {
5655
+ throw new Error(`${where}.name must be a non-empty string`);
5656
+ }
5657
+ const endpoint = e.endpoint;
5658
+ if (typeof endpoint !== "string" || endpoint.length === 0) {
5659
+ throw new Error(`${where}.endpoint must be a non-empty string`);
5660
+ }
5661
+ let parsedUrl;
5662
+ try {
5663
+ parsedUrl = new URL(endpoint);
5664
+ } catch {
5665
+ throw new Error(`${where}.endpoint must be an absolute URL (got "${endpoint}")`);
5666
+ }
5667
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
5668
+ throw new Error(`${where}.endpoint must be an http(s) URL (got "${parsedUrl.protocol}")`);
5669
+ }
5670
+ if (e.authToken !== void 0 && (typeof e.authToken !== "string" || e.authToken.length === 0)) {
5671
+ throw new Error(`${where}.authToken must be a non-empty string when present`);
5672
+ }
5673
+ return {
5674
+ name,
5675
+ endpoint,
5676
+ ...typeof e.authToken === "string" ? { authToken: e.authToken } : {}
5677
+ };
5678
+ }
5679
+ async function resolveProfile(name, home = neatHome()) {
5680
+ const { profiles } = await readProfilesConfig(home);
5681
+ return profiles.find((p) => p.name === name);
5682
+ }
5683
+ async function getActiveProfile(home = neatHome()) {
5684
+ const { active, profiles } = await readProfilesConfig(home);
5685
+ if (!active) return void 0;
5686
+ return profiles.find((p) => p.name === active);
5687
+ }
5688
+ function serialize(config) {
5689
+ const names = new Set(config.profiles.map((p) => p.name));
5690
+ const active = config.active && names.has(config.active) ? config.active : void 0;
5691
+ const out = {
5692
+ version: config.version ?? PROFILES_CONFIG_VERSION,
5693
+ ...active ? { active } : {},
5694
+ profiles: config.profiles
5695
+ };
5696
+ return `${JSON.stringify(out, null, 2)}
5697
+ `;
5698
+ }
5699
+ async function writeConfigAtomic(config, home) {
5700
+ const file = profilesConfigPath(home);
5701
+ await fs11.mkdir(path12.dirname(file), { recursive: true });
5702
+ const tmp = `${file}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5703
+ const fd = await fs11.open(tmp, "w", 384);
5704
+ try {
5705
+ await fd.writeFile(serialize(config), "utf8");
5706
+ await fd.sync();
5707
+ } finally {
5708
+ await fd.close();
5709
+ }
5710
+ await fs11.rename(tmp, file);
5711
+ }
5712
+ var LOCK_RETRY_MS = 50;
5713
+ var LOCK_TIMEOUT_MS = 5e3;
5714
+ async function acquireLock(lockPath) {
5715
+ await fs11.mkdir(path12.dirname(lockPath), { recursive: true });
5716
+ const deadline = Date.now() + LOCK_TIMEOUT_MS;
5717
+ for (; ; ) {
5718
+ try {
5719
+ const fd = await fs11.open(lockPath, "wx");
5720
+ await fd.writeFile(`${process.pid}
5721
+ `, "utf8");
5722
+ await fd.close();
5723
+ return;
5724
+ } catch (err) {
5725
+ if (err.code !== "EEXIST") throw err;
5726
+ if (Date.now() >= deadline) {
5727
+ throw new Error(
5728
+ `timed out acquiring ${lockPath} after ${LOCK_TIMEOUT_MS}ms \u2014 if no other neat process is running, remove the stale lock file`
5729
+ );
5730
+ }
5731
+ await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_MS));
5732
+ }
5733
+ }
5734
+ }
5735
+ async function releaseLock(lockPath) {
5736
+ await fs11.rm(lockPath, { force: true });
5737
+ }
5738
+ async function withProfilesLock(home, fn) {
5739
+ const lockPath = profilesConfigLockPath(home);
5740
+ await acquireLock(lockPath);
5741
+ try {
5742
+ return await fn();
5743
+ } finally {
5744
+ await releaseLock(lockPath);
5745
+ }
5746
+ }
5747
+ async function upsertProfile(profile, opts = {}) {
5748
+ const home = opts.home ?? neatHome();
5749
+ const validated = validateEntry(profile, 0, profilesConfigPath(home));
5750
+ await withProfilesLock(home, async () => {
5751
+ const config = await readProfilesConfig(home);
5752
+ const others = config.profiles.filter((p) => p.name !== validated.name);
5753
+ const profiles = [...others, validated];
5754
+ const makeActive = opts.makeActive ?? config.profiles.length === 0;
5755
+ const active = makeActive ? validated.name : config.active;
5756
+ await writeConfigAtomic({ version: config.version, ...active ? { active } : {}, profiles }, home);
5757
+ });
5758
+ }
5759
+ async function removeProfile(name, home = neatHome()) {
5760
+ return withProfilesLock(home, async () => {
5761
+ const config = await readProfilesConfig(home);
5762
+ const profiles = config.profiles.filter((p) => p.name !== name);
5763
+ if (profiles.length === config.profiles.length) return false;
5764
+ const active = config.active === name ? void 0 : config.active;
5765
+ await writeConfigAtomic({ version: config.version, ...active ? { active } : {}, profiles }, home);
5766
+ return true;
5767
+ });
5768
+ }
5769
+ async function clearActiveProfile(home = neatHome()) {
5770
+ return withProfilesLock(home, async () => {
5771
+ const config = await readProfilesConfig(home);
5772
+ if (!config.active) return false;
5773
+ await writeConfigAtomic({ version: config.version, profiles: config.profiles }, home);
5774
+ return true;
5775
+ });
5776
+ }
5777
+
5778
+ // src/login-cli.ts
5779
+ var HEALTH_TIMEOUT_MS2 = 5e3;
5780
+ var DEFAULT_PROFILE_NAME = "hosted";
5781
+ function readFlagValue(argv, i) {
5782
+ const arg = argv[i];
5783
+ const eq = arg.indexOf("=");
5784
+ if (eq !== -1) return { value: arg.slice(eq + 1), next: i };
5785
+ return { value: argv[i + 1], next: i + 1 };
5786
+ }
5787
+ function parseLoginArgs(argv) {
5788
+ const parsed = { name: DEFAULT_PROFILE_NAME, json: false, help: false };
5789
+ for (let i = 0; i < argv.length; i++) {
5790
+ const arg = argv[i];
5791
+ if (arg === "-h" || arg === "--help") parsed.help = true;
5792
+ else if (arg === "--json") parsed.json = true;
5793
+ else if (arg === "--endpoint" || arg.startsWith("--endpoint=")) {
5794
+ const { value, next } = readFlagValue(argv, i);
5795
+ parsed.endpoint = value;
5796
+ i = next;
5797
+ } else if (arg === "--token" || arg.startsWith("--token=")) {
5798
+ const { value, next } = readFlagValue(argv, i);
5799
+ parsed.token = value;
5800
+ i = next;
5801
+ } else if (arg === "--name" || arg.startsWith("--name=")) {
5802
+ const { value, next } = readFlagValue(argv, i);
5803
+ if (value && value.length > 0) parsed.name = value;
5804
+ i = next;
5805
+ } else {
5806
+ parsed.error = `unknown argument "${arg}"`;
5807
+ break;
5808
+ }
5809
+ }
5810
+ return parsed;
5811
+ }
5812
+ function printLoginHelp(out) {
5813
+ out("usage: neat login [--endpoint <url>] [--token <token>] [--name <name>] [--json]");
5814
+ out(" Connect this machine to a hosted NEAT and make it the default for the");
5815
+ out(" neat CLI and the MCP server. Omit --endpoint / --token to be prompted");
5816
+ out(" (the token is read without echo). --name labels the profile (default");
5817
+ out(' "hosted"). The token can also come from NEAT_LOGIN_TOKEN.');
5818
+ out(" Exit 0 on success, 1 rejected token/endpoint, 2 misuse, 3 unreachable.");
5819
+ }
5820
+ async function probeDaemon(fetchImpl, endpoint, token) {
5821
+ const root = endpoint.replace(/\/$/, "");
5822
+ let res;
5823
+ try {
5824
+ res = await fetchImpl(`${root}/health`, {
5825
+ headers: { authorization: `Bearer ${token}` },
5826
+ signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS2)
5827
+ });
5828
+ } catch (err) {
5829
+ return { kind: "unreachable", detail: err.message };
5830
+ }
5831
+ if (res.status === 401 || res.status === 403) return { kind: "unauthorized", status: res.status };
5832
+ if (!res.ok) return { kind: "not-neat", status: res.status };
5833
+ const contentType = res.headers.get("content-type") ?? "";
5834
+ if (!contentType.includes("json")) return { kind: "not-neat", status: res.status };
5835
+ return { kind: "ok" };
5836
+ }
5837
+ async function runLoginCommand(argv, deps = {}) {
5838
+ const out = deps.out ?? ((line) => console.log(line));
5839
+ const err = deps.err ?? ((line) => console.error(line));
5840
+ const env = deps.env ?? process.env;
5841
+ const fetchImpl = deps.fetchImpl ?? fetch;
5842
+ const readLine = deps.readLine ?? defaultReadLine;
5843
+ const readSecret = deps.readSecret ?? defaultReadSecret;
5844
+ const args = parseLoginArgs(argv);
5845
+ if (args.help) {
5846
+ printLoginHelp(out);
5847
+ return 0;
5848
+ }
5849
+ if (args.error) {
5850
+ err(`neat login: ${args.error}`);
5851
+ return 2;
5852
+ }
5853
+ let endpoint = args.endpoint;
5854
+ if (!endpoint) endpoint = (await readLine("Hosted NEAT endpoint (https://\u2026): "))?.trim();
5855
+ if (!endpoint) {
5856
+ err("neat login: an endpoint is required \u2014 pass --endpoint <url> or run interactively");
5857
+ return 2;
5858
+ }
5859
+ let url;
5860
+ try {
5861
+ url = new URL(endpoint);
5862
+ } catch {
5863
+ err(`neat login: --endpoint must be an absolute URL (got "${endpoint}")`);
5864
+ return 2;
5865
+ }
5866
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
5867
+ err(`neat login: --endpoint must be an http(s) URL (got "${url.protocol}")`);
5868
+ return 2;
5869
+ }
5870
+ const envToken = env.NEAT_LOGIN_TOKEN;
5871
+ let token = args.token ?? (envToken && envToken.length > 0 ? envToken : void 0);
5872
+ if (!token) token = (await readSecret("Daemon token: "))?.trim();
5873
+ if (!token) {
5874
+ err("neat login: a token is required \u2014 pass --token, set NEAT_LOGIN_TOKEN, or run interactively");
5875
+ return 2;
5876
+ }
5877
+ const probe = await probeDaemon(fetchImpl, endpoint, token);
5878
+ if (probe.kind === "unreachable") {
5879
+ err(`neat login: can't reach ${endpoint} \u2014 ${probe.detail}`);
5880
+ return 3;
5881
+ }
5882
+ if (probe.kind === "unauthorized") {
5883
+ err(`neat login: ${endpoint} rejected the token (HTTP ${probe.status}). Check the token and try again.`);
5884
+ return 1;
5885
+ }
5886
+ if (probe.kind === "not-neat") {
5887
+ err(`neat login: ${endpoint} answered but does not look like a NEAT daemon (HTTP ${probe.status}).`);
5888
+ return 1;
5889
+ }
5890
+ await upsertProfile(
5891
+ { name: args.name, endpoint, authToken: token },
5892
+ { makeActive: true, ...deps.home ? { home: deps.home } : {} }
5893
+ );
5894
+ if (args.json) {
5895
+ out(JSON.stringify({ status: "logged-in", profile: args.name, endpoint }, null, 2));
5896
+ } else {
5897
+ out(`Logged in \u2014 profile "${args.name}" \u2192 ${endpoint}`);
5898
+ out("The neat CLI and the MCP server now read this hosted graph by default.");
5899
+ out("Run `neat logout` to switch back to your local daemon.");
5900
+ }
5901
+ return 0;
5902
+ }
5903
+ function parseLogoutArgs(argv) {
5904
+ const parsed = { help: false };
5905
+ for (let i = 0; i < argv.length; i++) {
5906
+ const arg = argv[i];
5907
+ if (arg === "-h" || arg === "--help") parsed.help = true;
5908
+ else if (arg === "--name" || arg.startsWith("--name=")) {
5909
+ const { value, next } = readFlagValue(argv, i);
5910
+ parsed.name = value;
5911
+ i = next;
5912
+ } else {
5913
+ parsed.error = `unknown argument "${arg}"`;
5914
+ break;
5915
+ }
5916
+ }
5917
+ return parsed;
5918
+ }
5919
+ async function runLogoutCommand(argv, deps = {}) {
5920
+ const out = deps.out ?? ((line) => console.log(line));
5921
+ const err = deps.err ?? ((line) => console.error(line));
5922
+ const home = deps.home;
5923
+ const args = parseLogoutArgs(argv);
5924
+ if (args.help) {
5925
+ out("usage: neat logout [--name <name>]");
5926
+ out(" With no argument, clears the active hosted profile so the CLI and MCP");
5927
+ out(" server go back to your local daemon (the stored profile is kept).");
5928
+ out(" --name <name> removes that profile from ~/.neat/profiles.json entirely.");
5929
+ return 0;
5930
+ }
5931
+ if (args.error) {
5932
+ err(`neat logout: ${args.error}`);
5933
+ return 2;
5934
+ }
5935
+ if (args.name !== void 0) {
5936
+ if (args.name.length === 0) {
5937
+ err("neat logout: --name needs a profile name");
5938
+ return 2;
5939
+ }
5940
+ const removed = await removeProfile(args.name, home ?? void 0);
5941
+ if (!removed) {
5942
+ err(`neat logout: no profile named "${args.name}"`);
5943
+ return 1;
5944
+ }
5945
+ out(`Removed profile "${args.name}".`);
5946
+ return 0;
5947
+ }
5948
+ const active = await getActiveProfile(home ?? void 0);
5949
+ if (!active) {
5950
+ out("Not logged in to a hosted NEAT \u2014 the CLI is already using your local daemon.");
5951
+ return 0;
5952
+ }
5953
+ await clearActiveProfile(home ?? void 0);
5954
+ out(`Logged out of "${active.name}" (${active.endpoint}). The CLI is back on your local daemon.`);
5955
+ return 0;
5956
+ }
5957
+ async function defaultReadLine(prompt) {
5958
+ if (!process.stdin.isTTY) return void 0;
5959
+ const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
5960
+ try {
5961
+ return await rl.question(prompt);
5962
+ } finally {
5963
+ rl.close();
5964
+ }
5965
+ }
5966
+ async function defaultReadSecret(prompt) {
5967
+ const stdin = process.stdin;
5968
+ if (!stdin.isTTY) return void 0;
5969
+ process.stdout.write(prompt);
5970
+ return new Promise((resolve) => {
5971
+ const chars = [];
5972
+ stdin.setRawMode(true);
5973
+ stdin.resume();
5974
+ stdin.setEncoding("utf8");
5975
+ const cleanup = () => {
5976
+ stdin.setRawMode(false);
5977
+ stdin.pause();
5978
+ stdin.off("data", onData);
5979
+ };
5980
+ const onData = (ch) => {
5981
+ const code = ch.charCodeAt(0);
5982
+ if (ch === "\n" || ch === "\r" || code === 4) {
5983
+ cleanup();
5984
+ process.stdout.write("\n");
5985
+ resolve(chars.join(""));
5986
+ } else if (code === 3) {
5987
+ cleanup();
5988
+ process.stdout.write("\n");
5989
+ process.exit(130);
5990
+ } else if (code === 127 || ch === "\b") {
5991
+ chars.pop();
5992
+ } else {
5993
+ chars.push(ch);
5994
+ }
5995
+ };
5996
+ stdin.on("data", onData);
5997
+ });
5998
+ }
5999
+
6000
+ // src/hooks-cli.ts
6001
+ import path13 from "path";
6002
+ import os2 from "os";
6003
+ import { promises as fs12 } from "fs";
5570
6004
  import { fileURLToPath as fileURLToPath3 } from "url";
5571
6005
  var HOOK_FILENAME = "neat-search-nudge.mjs";
5572
6006
  var GUIDE_FILENAME = "GRAPH_FIRST.md";
5573
6007
  var GUIDE_INSTALL_NAME = "neat-graph-first.md";
5574
6008
  var HOOK_MATCHER = "Grep|Glob|Bash";
5575
6009
  function moduleDir() {
5576
- return typeof __dirname !== "undefined" ? __dirname : path12.dirname(fileURLToPath3(import.meta.url));
6010
+ return typeof __dirname !== "undefined" ? __dirname : path13.dirname(fileURLToPath3(import.meta.url));
5577
6011
  }
5578
6012
  async function readSkillAsset(rel) {
5579
6013
  const here = moduleDir();
5580
6014
  const candidates = [
5581
- path12.resolve(here, "../../claude-skill", rel),
5582
- path12.resolve(here, "../../../claude-skill", rel),
5583
- path12.resolve(here, "../claude-skill", rel)
6015
+ path13.resolve(here, "../../claude-skill", rel),
6016
+ path13.resolve(here, "../../../claude-skill", rel),
6017
+ path13.resolve(here, "../claude-skill", rel)
5584
6018
  ];
5585
6019
  for (const candidate of candidates) {
5586
6020
  try {
5587
- return await fs11.readFile(candidate, "utf8");
6021
+ return await fs12.readFile(candidate, "utf8");
5588
6022
  } catch {
5589
6023
  }
5590
6024
  }
@@ -5592,22 +6026,22 @@ async function readSkillAsset(rel) {
5592
6026
  `neat hooks: could not find @neat.is/claude-skill/${rel} \u2014 is the package installed?`
5593
6027
  );
5594
6028
  }
5595
- function neatHome() {
6029
+ function neatHome2() {
5596
6030
  const override = process.env.NEAT_HOME;
5597
- if (override && override.length > 0) return path12.resolve(override);
5598
- return path12.join(os.homedir(), ".neat");
6031
+ if (override && override.length > 0) return path13.resolve(override);
6032
+ return path13.join(os2.homedir(), ".neat");
5599
6033
  }
5600
6034
  function claudeSettingsPath() {
5601
6035
  const override = process.env.NEAT_CLAUDE_SETTINGS;
5602
- if (override && override.length > 0) return path12.resolve(override);
5603
- const home = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
5604
- return path12.join(home, ".claude", "settings.json");
6036
+ if (override && override.length > 0) return path13.resolve(override);
6037
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? os2.homedir();
6038
+ return path13.join(home, ".claude", "settings.json");
5605
6039
  }
5606
6040
  function installedHookPath() {
5607
- return path12.join(neatHome(), "hooks", HOOK_FILENAME);
6041
+ return path13.join(neatHome2(), "hooks", HOOK_FILENAME);
5608
6042
  }
5609
6043
  function gateFlagPath() {
5610
- return path12.join(neatHome(), "hooks", "gate-enabled");
6044
+ return path13.join(neatHome2(), "hooks", "gate-enabled");
5611
6045
  }
5612
6046
  function isNeatSearchEntry(entry2) {
5613
6047
  return (entry2.hooks ?? []).some(
@@ -5640,14 +6074,14 @@ async function runHooks(opts) {
5640
6074
  const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
5641
6075
  const guide = await readSkillAsset(GUIDE_FILENAME);
5642
6076
  const scriptPath = installedHookPath();
5643
- await fs11.mkdir(path12.dirname(scriptPath), { recursive: true });
5644
- await fs11.writeFile(scriptPath, hookScript, { mode: 493 });
5645
- const guidePath = path12.join(neatHome(), GUIDE_INSTALL_NAME);
5646
- await fs11.writeFile(guidePath, guide, "utf8");
6077
+ await fs12.mkdir(path13.dirname(scriptPath), { recursive: true });
6078
+ await fs12.writeFile(scriptPath, hookScript, { mode: 493 });
6079
+ const guidePath = path13.join(neatHome2(), GUIDE_INSTALL_NAME);
6080
+ await fs12.writeFile(guidePath, guide, "utf8");
5647
6081
  const settingsFile = claudeSettingsPath();
5648
6082
  let settings = {};
5649
6083
  try {
5650
- settings = JSON.parse(await fs11.readFile(settingsFile, "utf8"));
6084
+ settings = JSON.parse(await fs12.readFile(settingsFile, "utf8"));
5651
6085
  } catch (err) {
5652
6086
  if (err.code !== "ENOENT") {
5653
6087
  console.error(
@@ -5669,14 +6103,14 @@ async function runHooks(opts) {
5669
6103
  ...settings,
5670
6104
  hooks: { ...hooks, PreToolUse: preToolUse }
5671
6105
  };
5672
- await fs11.mkdir(path12.dirname(settingsFile), { recursive: true });
5673
- await fs11.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
6106
+ await fs12.mkdir(path13.dirname(settingsFile), { recursive: true });
6107
+ await fs12.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
5674
6108
  const flag = gateFlagPath();
5675
6109
  if (opts.gate) {
5676
- await fs11.mkdir(path12.dirname(flag), { recursive: true });
5677
- await fs11.writeFile(flag, "1\n", "utf8");
6110
+ await fs12.mkdir(path13.dirname(flag), { recursive: true });
6111
+ await fs12.writeFile(flag, "1\n", "utf8");
5678
6112
  } else {
5679
- await fs11.rm(flag, { force: true });
6113
+ await fs12.rm(flag, { force: true });
5680
6114
  }
5681
6115
  const mode = opts.gate ? "GATE (deny search until you ask the graph)" : "nudge (search still runs)";
5682
6116
  console.log(`neat hooks: installed the search hook in ${opts.gate ? "gate" : "nudge"} mode`);
@@ -5765,8 +6199,8 @@ async function runHooksCommand(args) {
5765
6199
  }
5766
6200
 
5767
6201
  // src/claude-cli.ts
5768
- import path13 from "path";
5769
- import { promises as fs12 } from "fs";
6202
+ import path14 from "path";
6203
+ import { promises as fs13 } from "fs";
5770
6204
  var NEAT_SECTION_HEADING = "## neat";
5771
6205
  var NEAT_DIRECTIVE_BODY = `This project has NEAT wired in: a live, fused semantic graph of the system \u2014
5772
6206
  code and runtime behaviour (OpenTelemetry) in one model, every fact tagged with
@@ -5800,8 +6234,8 @@ ${NEAT_DIRECTIVE_BODY}
5800
6234
  }
5801
6235
  function claudeMdPath() {
5802
6236
  const override = process.env.NEAT_CLAUDE_MD;
5803
- if (override && override.length > 0) return path13.resolve(override);
5804
- return path13.join(process.cwd(), "CLAUDE.md");
6237
+ if (override && override.length > 0) return path14.resolve(override);
6238
+ return path14.join(process.cwd(), "CLAUDE.md");
5805
6239
  }
5806
6240
  function splitAroundSection(raw) {
5807
6241
  const lines = raw.split("\n");
@@ -5829,7 +6263,7 @@ function compose(before, after) {
5829
6263
  }
5830
6264
  async function readIfExists(file) {
5831
6265
  try {
5832
- return await fs12.readFile(file, "utf8");
6266
+ return await fs13.readFile(file, "utf8");
5833
6267
  } catch (err) {
5834
6268
  if (err.code === "ENOENT") return null;
5835
6269
  throw err;
@@ -5840,8 +6274,8 @@ async function runInstall() {
5840
6274
  const raw = await readIfExists(file) ?? "";
5841
6275
  const { before, after, found } = splitAroundSection(raw);
5842
6276
  const next = compose(before, after);
5843
- await fs12.mkdir(path13.dirname(file), { recursive: true });
5844
- await fs12.writeFile(file, next, "utf8");
6277
+ await fs13.mkdir(path14.dirname(file), { recursive: true });
6278
+ await fs13.writeFile(file, next, "utf8");
5845
6279
  const verb = raw.length === 0 ? "created" : found ? "refreshed" : "added";
5846
6280
  console.log(`neat claude: ${verb} the \`${NEAT_SECTION_HEADING}\` section in ${file}`);
5847
6281
  console.log("Your agent will now reach for `neat ask` before Read/Grep/Bash. Restart the");
@@ -5862,7 +6296,7 @@ async function runUninstall() {
5862
6296
  }
5863
6297
  const remaining = [before, after].filter((s) => s.length > 0).join("\n\n");
5864
6298
  const next = remaining.length > 0 ? remaining.replace(/\n*$/, "") + "\n" : "";
5865
- await fs12.writeFile(file, next, "utf8");
6299
+ await fs13.writeFile(file, next, "utf8");
5866
6300
  console.log(`neat claude: removed the \`${NEAT_SECTION_HEADING}\` section from ${file}.`);
5867
6301
  return { exitCode: 0 };
5868
6302
  }
@@ -5904,9 +6338,9 @@ async function runClaudeCommand(args) {
5904
6338
  }
5905
6339
 
5906
6340
  // src/codex-cli.ts
5907
- import path14 from "path";
5908
- import os2 from "os";
5909
- import { promises as fs13 } from "fs";
6341
+ import path15 from "path";
6342
+ import os3 from "os";
6343
+ import { promises as fs14 } from "fs";
5910
6344
  import { isDeepStrictEqual } from "util";
5911
6345
  import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
5912
6346
  var CODEX_MCP_SERVER = {
@@ -5924,14 +6358,14 @@ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
5924
6358
  var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
5925
6359
  function codexConfigPath() {
5926
6360
  const override = process.env.NEAT_CODEX_CONFIG;
5927
- if (override && override.length > 0) return path14.resolve(override);
5928
- const home = process.env.HOME ?? process.env.USERPROFILE ?? os2.homedir();
5929
- return path14.join(home, ".codex", "config.toml");
6361
+ if (override && override.length > 0) return path15.resolve(override);
6362
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? os3.homedir();
6363
+ return path15.join(home, ".codex", "config.toml");
5930
6364
  }
5931
6365
  function agentsFilePath() {
5932
6366
  const override = process.env.NEAT_CODEX_AGENTS;
5933
- if (override && override.length > 0) return path14.resolve(override);
5934
- return path14.join(process.cwd(), "AGENTS.md");
6367
+ if (override && override.length > 0) return path15.resolve(override);
6368
+ return path15.join(process.cwd(), "AGENTS.md");
5935
6369
  }
5936
6370
  function isTableHeader(line) {
5937
6371
  return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
@@ -6065,7 +6499,7 @@ async function runCodex(opts) {
6065
6499
  const agentsPath = agentsFilePath();
6066
6500
  let configRaw = "";
6067
6501
  try {
6068
- configRaw = await fs13.readFile(configPath, "utf8");
6502
+ configRaw = await fs14.readFile(configPath, "utf8");
6069
6503
  } catch (err) {
6070
6504
  if (err.code !== "ENOENT") {
6071
6505
  console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
@@ -6074,7 +6508,7 @@ async function runCodex(opts) {
6074
6508
  }
6075
6509
  let agentsRaw = "";
6076
6510
  try {
6077
- agentsRaw = await fs13.readFile(agentsPath, "utf8");
6511
+ agentsRaw = await fs14.readFile(agentsPath, "utf8");
6078
6512
  } catch (err) {
6079
6513
  if (err.code !== "ENOENT") {
6080
6514
  console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
@@ -6114,15 +6548,15 @@ async function runCodex(opts) {
6114
6548
  return { exitCode: 0 };
6115
6549
  }
6116
6550
  if (config.changed) {
6117
- await fs13.mkdir(path14.dirname(configPath), { recursive: true });
6118
- await fs13.writeFile(configPath, config.text, "utf8");
6551
+ await fs14.mkdir(path15.dirname(configPath), { recursive: true });
6552
+ await fs14.writeFile(configPath, config.text, "utf8");
6119
6553
  console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
6120
6554
  } else {
6121
6555
  console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
6122
6556
  }
6123
6557
  if (agents.changed) {
6124
- await fs13.mkdir(path14.dirname(agentsPath), { recursive: true });
6125
- await fs13.writeFile(agentsPath, agents.text, "utf8");
6558
+ await fs14.mkdir(path15.dirname(agentsPath), { recursive: true });
6559
+ await fs14.writeFile(agentsPath, agents.text, "utf8");
6126
6560
  console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
6127
6561
  } else {
6128
6562
  console.log(`neat codex: ${agentsPath} already has the graph-first block`);
@@ -6178,9 +6612,9 @@ async function runCodexCommand(args) {
6178
6612
  }
6179
6613
 
6180
6614
  // src/editors-cli.ts
6181
- import path15 from "path";
6182
- import os3 from "os";
6183
- import { promises as fs14 } from "fs";
6615
+ import path16 from "path";
6616
+ import os4 from "os";
6617
+ import { promises as fs15 } from "fs";
6184
6618
  import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
6185
6619
  import * as jsonc from "jsonc-parser";
6186
6620
  var NEAT_MCP_SERVER = {
@@ -6200,21 +6634,21 @@ var NEAT_CRUSH_SERVER = {
6200
6634
  var GRAPH_FIRST_MARKER_OPEN = "<!-- neat:graph-first -->";
6201
6635
  var GRAPH_FIRST_MARKER_CLOSE = "<!-- /neat:graph-first -->";
6202
6636
  function homeDir() {
6203
- return process.env.HOME ?? process.env.USERPROFILE ?? os3.homedir();
6637
+ return process.env.HOME ?? process.env.USERPROFILE ?? os4.homedir();
6204
6638
  }
6205
6639
  function xdgConfigDir() {
6206
6640
  const xdg = process.env.XDG_CONFIG_HOME;
6207
- return xdg && xdg.length > 0 ? path15.resolve(xdg) : path15.join(homeDir(), ".config");
6641
+ return xdg && xdg.length > 0 ? path16.resolve(xdg) : path16.join(homeDir(), ".config");
6208
6642
  }
6209
6643
  function envOverride(name) {
6210
6644
  const v = process.env[name];
6211
- return v && v.length > 0 ? path15.resolve(v) : void 0;
6645
+ return v && v.length > 0 ? path16.resolve(v) : void 0;
6212
6646
  }
6213
6647
  var CURSOR_CLIENT = {
6214
6648
  id: "cursor",
6215
6649
  label: "Cursor",
6216
6650
  docsUrl: "https://docs.cursor.com/context/mcp",
6217
- mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? path15.join(homeDir(), ".cursor", "mcp.json"),
6651
+ mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? path16.join(homeDir(), ".cursor", "mcp.json"),
6218
6652
  mcpContainerKey: "mcpServers",
6219
6653
  format: "json",
6220
6654
  // Cursor still reads a single `.cursorrules` at the project root (the modern
@@ -6226,7 +6660,7 @@ var DEVIN_CLIENT = {
6226
6660
  id: "devin",
6227
6661
  label: "Devin Desktop (Cascade)",
6228
6662
  docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
6229
- mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? path15.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
6663
+ mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? path16.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
6230
6664
  mcpContainerKey: "mcpServers",
6231
6665
  format: "json",
6232
6666
  rulesFileName: ".windsurfrules"
@@ -6235,7 +6669,7 @@ var GEMINI_CLIENT = {
6235
6669
  id: "gemini",
6236
6670
  label: "Gemini CLI",
6237
6671
  docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
6238
- mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? path15.join(homeDir(), ".gemini", "settings.json"),
6672
+ mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? path16.join(homeDir(), ".gemini", "settings.json"),
6239
6673
  mcpContainerKey: "mcpServers",
6240
6674
  format: "json",
6241
6675
  rulesFileName: "GEMINI.md"
@@ -6244,7 +6678,7 @@ var QWEN_CLIENT = {
6244
6678
  id: "qwen",
6245
6679
  label: "Qwen Code",
6246
6680
  docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
6247
- mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? path15.join(homeDir(), ".qwen", "settings.json"),
6681
+ mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? path16.join(homeDir(), ".qwen", "settings.json"),
6248
6682
  mcpContainerKey: "mcpServers",
6249
6683
  format: "json",
6250
6684
  rulesFileName: "QWEN.md"
@@ -6253,7 +6687,7 @@ var AMAZONQ_CLIENT = {
6253
6687
  id: "amazonq",
6254
6688
  label: "Amazon Q Developer CLI",
6255
6689
  docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
6256
- mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? path15.join(homeDir(), ".aws", "amazonq", "mcp.json"),
6690
+ mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? path16.join(homeDir(), ".aws", "amazonq", "mcp.json"),
6257
6691
  mcpContainerKey: "mcpServers",
6258
6692
  format: "json"
6259
6693
  };
@@ -6261,7 +6695,7 @@ var ROOCODE_CLIENT = {
6261
6695
  id: "roocode",
6262
6696
  label: "Roo Code",
6263
6697
  docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
6264
- mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? path15.join(process.cwd(), ".roo", "mcp.json"),
6698
+ mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? path16.join(process.cwd(), ".roo", "mcp.json"),
6265
6699
  mcpContainerKey: "mcpServers",
6266
6700
  format: "json"
6267
6701
  };
@@ -6274,9 +6708,9 @@ var ZED_CLIENT = {
6274
6708
  if (override) return override;
6275
6709
  if (process.platform === "win32") {
6276
6710
  const appData = process.env.APPDATA;
6277
- if (appData && appData.length > 0) return path15.join(appData, "Zed", "settings.json");
6711
+ if (appData && appData.length > 0) return path16.join(appData, "Zed", "settings.json");
6278
6712
  }
6279
- return path15.join(homeDir(), ".config", "zed", "settings.json");
6713
+ return path16.join(homeDir(), ".config", "zed", "settings.json");
6280
6714
  },
6281
6715
  mcpContainerKey: "context_servers",
6282
6716
  format: "jsonc",
@@ -6286,7 +6720,7 @@ var OPENCODE_CLIENT = {
6286
6720
  id: "opencode",
6287
6721
  label: "OpenCode",
6288
6722
  docsUrl: "https://opencode.ai/docs/mcp-servers/",
6289
- mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? path15.join(xdgConfigDir(), "opencode", "opencode.json"),
6723
+ mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? path16.join(xdgConfigDir(), "opencode", "opencode.json"),
6290
6724
  mcpContainerKey: "mcp",
6291
6725
  format: "json",
6292
6726
  serverEntry: NEAT_OPENCODE_SERVER,
@@ -6296,7 +6730,7 @@ var CRUSH_CLIENT = {
6296
6730
  id: "crush",
6297
6731
  label: "Crush",
6298
6732
  docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
6299
- mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? path15.join(xdgConfigDir(), "crush", "crush.json"),
6733
+ mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? path16.join(xdgConfigDir(), "crush", "crush.json"),
6300
6734
  mcpContainerKey: "mcp",
6301
6735
  format: "json",
6302
6736
  serverEntry: NEAT_CRUSH_SERVER,
@@ -6359,7 +6793,7 @@ async function planMcp(client, mcpPath) {
6359
6793
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
6360
6794
  let raw = "";
6361
6795
  try {
6362
- raw = await fs14.readFile(mcpPath, "utf8");
6796
+ raw = await fs15.readFile(mcpPath, "utf8");
6363
6797
  } catch (err) {
6364
6798
  const e = err;
6365
6799
  if (e.code === "ENOENT") {
@@ -6401,7 +6835,7 @@ async function runEditorInstall(client, opts) {
6401
6835
  const mcpPath = client.mcpConfigPath();
6402
6836
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
6403
6837
  const hasRules = typeof client.rulesFileName === "string";
6404
- const rulesPath = hasRules ? path15.join(opts.projectDir, client.rulesFileName) : "";
6838
+ const rulesPath = hasRules ? path16.join(opts.projectDir, client.rulesFileName) : "";
6405
6839
  const mcp = await planMcp(client, mcpPath);
6406
6840
  if (mcp === null) return { exitCode: 1 };
6407
6841
  let existingRules = "";
@@ -6410,7 +6844,7 @@ async function runEditorInstall(client, opts) {
6410
6844
  let block = "";
6411
6845
  if (hasRules) {
6412
6846
  try {
6413
- existingRules = await fs14.readFile(rulesPath, "utf8");
6847
+ existingRules = await fs15.readFile(rulesPath, "utf8");
6414
6848
  } catch (err) {
6415
6849
  if (err.code !== "ENOENT") {
6416
6850
  console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
@@ -6444,11 +6878,11 @@ async function runEditorInstall(client, opts) {
6444
6878
  );
6445
6879
  return { exitCode: 0 };
6446
6880
  }
6447
- await fs14.mkdir(path15.dirname(mcpPath), { recursive: true });
6448
- await fs14.writeFile(mcpPath, mcp.text, "utf8");
6881
+ await fs15.mkdir(path16.dirname(mcpPath), { recursive: true });
6882
+ await fs15.writeFile(mcpPath, mcp.text, "utf8");
6449
6883
  if (hasRules) {
6450
- await fs14.mkdir(path15.dirname(rulesPath), { recursive: true });
6451
- await fs14.writeFile(rulesPath, newRules, "utf8");
6884
+ await fs15.mkdir(path16.dirname(rulesPath), { recursive: true });
6885
+ await fs15.writeFile(rulesPath, newRules, "utf8");
6452
6886
  }
6453
6887
  console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
6454
6888
  console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
@@ -6939,7 +7373,7 @@ function sleep(ms, signal) {
6939
7373
  }
6940
7374
 
6941
7375
  // src/cli-verbs.ts
6942
- import path16 from "path";
7376
+ import path17 from "path";
6943
7377
  async function resolveProjectEntry(opts) {
6944
7378
  const entries = await listProjects();
6945
7379
  if (opts.project) {
@@ -6949,7 +7383,7 @@ async function resolveProjectEntry(opts) {
6949
7383
  const cwd = opts.cwd ?? process.cwd();
6950
7384
  const resolvedCwd = await normalizeProjectPath(cwd);
6951
7385
  for (const entry2 of entries) {
6952
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path16.sep}`)) {
7386
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path17.sep}`)) {
6953
7387
  return entry2;
6954
7388
  }
6955
7389
  }
@@ -7254,6 +7688,12 @@ function usage5() {
7254
7688
  console.log(" doctor Preflight this directory's setup \u2014 Node version, project,");
7255
7689
  console.log(" and daemon reachability \u2014 and print a fix for anything down.");
7256
7690
  console.log(" Flags: --json. Exits 0 when all pass, 1 when a check fails.");
7691
+ console.log(" login Connect this machine to a hosted NEAT and make it the default");
7692
+ console.log(" for the CLI and the MCP server. Paste the daemon endpoint +");
7693
+ console.log(" token, or run interactively (the token is read without echo).");
7694
+ console.log(" Flags: --endpoint <url>, --token <token>, --name <name>, --json.");
7695
+ console.log(" logout Clear the active hosted profile (back to your local daemon);");
7696
+ console.log(" --name <name> removes that profile entirely.");
7257
7697
  console.log("");
7258
7698
  console.log("query commands (mirror the MCP tools, ADR-050):");
7259
7699
  console.log(" ask <question> Plain-language door: resolves the question to");
@@ -7304,6 +7744,7 @@ function usage5() {
7304
7744
  }
7305
7745
  var STRING_FLAGS = [
7306
7746
  ["--project", "project"],
7747
+ ["--profile", "profile"],
7307
7748
  ["--depth", "depth"],
7308
7749
  ["--limit", "limit"],
7309
7750
  ["--edge-type", "edgeType"],
@@ -7321,6 +7762,7 @@ function parseArgs(rest) {
7321
7762
  const positional = [];
7322
7763
  const out = {
7323
7764
  project: null,
7765
+ profile: null,
7324
7766
  apply: false,
7325
7767
  dryRun: false,
7326
7768
  noInstall: false,
@@ -7479,7 +7921,7 @@ async function buildPatchSections(services, project) {
7479
7921
  }
7480
7922
  async function runInit(opts) {
7481
7923
  const written = [];
7482
- const stat = await fs15.stat(opts.scanPath).catch(() => null);
7924
+ const stat = await fs16.stat(opts.scanPath).catch(() => null);
7483
7925
  if (!stat || !stat.isDirectory()) {
7484
7926
  console.error(`neat init: ${opts.scanPath} is not a directory`);
7485
7927
  return { exitCode: 2, writtenFiles: written };
@@ -7488,13 +7930,13 @@ async function runInit(opts) {
7488
7930
  printDiscoveryReport(opts, services);
7489
7931
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
7490
7932
  const patch = renderPatch(sections);
7491
- const patchPath = path17.join(opts.scanPath, "neat.patch");
7933
+ const patchPath = path18.join(opts.scanPath, "neat.patch");
7492
7934
  if (opts.dryRun) {
7493
- await fs15.writeFile(patchPath, patch, "utf8");
7935
+ await fs16.writeFile(patchPath, patch, "utf8");
7494
7936
  written.push(patchPath);
7495
7937
  console.log(`dry-run: patch written to ${patchPath}`);
7496
- const gitignorePath = path17.join(opts.scanPath, ".gitignore");
7497
- const gitignoreExists = await fs15.stat(gitignorePath).then(() => true).catch(() => false);
7938
+ const gitignorePath = path18.join(opts.scanPath, ".gitignore");
7939
+ const gitignoreExists = await fs16.stat(gitignorePath).then(() => true).catch(() => false);
7498
7940
  const verb = gitignoreExists ? "append" : "create";
7499
7941
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
7500
7942
  console.log("rerun without --dry-run to register and snapshot.");
@@ -7505,9 +7947,9 @@ async function runInit(opts) {
7505
7947
  const graph = getGraph(graphKey);
7506
7948
  const projectPaths = pathsForProject(
7507
7949
  graphKey,
7508
- path17.join(opts.scanPath, "neat-out")
7950
+ path18.join(opts.scanPath, "neat-out")
7509
7951
  );
7510
- const errorsPath = path17.join(path17.dirname(opts.outPath), path17.basename(projectPaths.errorsPath));
7952
+ const errorsPath = path18.join(path18.dirname(opts.outPath), path18.basename(projectPaths.errorsPath));
7511
7953
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
7512
7954
  await saveGraphToDisk(graph, opts.outPath);
7513
7955
  written.push(opts.outPath);
@@ -7586,7 +8028,7 @@ async function runInit(opts) {
7586
8028
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
7587
8029
  }
7588
8030
  } else {
7589
- await fs15.writeFile(patchPath, patch, "utf8");
8031
+ await fs16.writeFile(patchPath, patch, "utf8");
7590
8032
  written.push(patchPath);
7591
8033
  }
7592
8034
  }
@@ -7627,9 +8069,9 @@ var CLAUDE_SKILL_CONFIG = {
7627
8069
  };
7628
8070
  function claudeConfigPath() {
7629
8071
  const override = process.env.NEAT_CLAUDE_CONFIG;
7630
- if (override && override.length > 0) return path17.resolve(override);
8072
+ if (override && override.length > 0) return path18.resolve(override);
7631
8073
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7632
- return path17.join(home, ".claude.json");
8074
+ return path18.join(home, ".claude.json");
7633
8075
  }
7634
8076
  async function runSkill(opts) {
7635
8077
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -7641,7 +8083,7 @@ async function runSkill(opts) {
7641
8083
  const target = claudeConfigPath();
7642
8084
  let existing = {};
7643
8085
  try {
7644
- existing = JSON.parse(await fs15.readFile(target, "utf8"));
8086
+ existing = JSON.parse(await fs16.readFile(target, "utf8"));
7645
8087
  } catch (err) {
7646
8088
  if (err.code !== "ENOENT") {
7647
8089
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -7653,8 +8095,8 @@ async function runSkill(opts) {
7653
8095
  ...existing,
7654
8096
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
7655
8097
  };
7656
- await fs15.mkdir(path17.dirname(target), { recursive: true });
7657
- await fs15.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
8098
+ await fs16.mkdir(path18.dirname(target), { recursive: true });
8099
+ await fs16.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
7658
8100
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
7659
8101
  console.log("restart Claude Code to pick up the new MCP server.");
7660
8102
  console.log("");
@@ -7698,6 +8140,16 @@ async function main() {
7698
8140
  if (code !== 0) process.exit(code);
7699
8141
  return;
7700
8142
  }
8143
+ if (cmd0 === "login") {
8144
+ const code = await runLoginCommand(argv.slice(1));
8145
+ if (code !== 0) process.exit(code);
8146
+ return;
8147
+ }
8148
+ if (cmd0 === "logout") {
8149
+ const code = await runLogoutCommand(argv.slice(1));
8150
+ if (code !== 0) process.exit(code);
8151
+ return;
8152
+ }
7701
8153
  if (cmd0 === "hooks") {
7702
8154
  const code = await runHooksCommand(argv.slice(1));
7703
8155
  if (code !== 0) process.exit(code);
@@ -7750,12 +8202,12 @@ async function main() {
7750
8202
  console.error("neat init: --apply and --dry-run are mutually exclusive");
7751
8203
  process.exit(2);
7752
8204
  }
7753
- const scanPath = path17.resolve(target);
8205
+ const scanPath = path18.resolve(target);
7754
8206
  const projectExplicit = parsed.project !== null;
7755
- const projectName = projectExplicit ? project : path17.basename(scanPath);
8207
+ const projectName = projectExplicit ? project : path18.basename(scanPath);
7756
8208
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
7757
- const fallback = pathsForProject(projectKey, path17.join(scanPath, "neat-out")).snapshotPath;
7758
- const outPath = path17.resolve(process.env.NEAT_OUT_PATH ?? fallback);
8209
+ const fallback = pathsForProject(projectKey, path18.join(scanPath, "neat-out")).snapshotPath;
8210
+ const outPath = path18.resolve(process.env.NEAT_OUT_PATH ?? fallback);
7759
8211
  const result = await runInit({
7760
8212
  scanPath,
7761
8213
  outPath,
@@ -7776,21 +8228,21 @@ async function main() {
7776
8228
  usage5();
7777
8229
  process.exit(2);
7778
8230
  }
7779
- const scanPath = path17.resolve(target);
7780
- const stat = await fs15.stat(scanPath).catch(() => null);
8231
+ const scanPath = path18.resolve(target);
8232
+ const stat = await fs16.stat(scanPath).catch(() => null);
7781
8233
  if (!stat || !stat.isDirectory()) {
7782
8234
  console.error(`neat watch: ${scanPath} is not a directory`);
7783
8235
  process.exit(2);
7784
8236
  }
7785
- const projectPaths = pathsForProject(project, path17.join(scanPath, "neat-out"));
7786
- const outPath = path17.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
7787
- const errorsPath = path17.resolve(
7788
- process.env.NEAT_ERRORS_PATH ?? path17.join(path17.dirname(outPath), path17.basename(projectPaths.errorsPath))
8237
+ const projectPaths = pathsForProject(project, path18.join(scanPath, "neat-out"));
8238
+ const outPath = path18.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
8239
+ const errorsPath = path18.resolve(
8240
+ process.env.NEAT_ERRORS_PATH ?? path18.join(path18.dirname(outPath), path18.basename(projectPaths.errorsPath))
7789
8241
  );
7790
- const staleEventsPath = path17.resolve(
7791
- process.env.NEAT_STALE_EVENTS_PATH ?? path17.join(path17.dirname(outPath), path17.basename(projectPaths.staleEventsPath))
8242
+ const staleEventsPath = path18.resolve(
8243
+ process.env.NEAT_STALE_EVENTS_PATH ?? path18.join(path18.dirname(outPath), path18.basename(projectPaths.staleEventsPath))
7792
8244
  );
7793
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path17.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
8245
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path18.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
7794
8246
  const handle = await startWatch(getGraph(project), {
7795
8247
  scanPath,
7796
8248
  outPath,
@@ -7799,7 +8251,7 @@ async function main() {
7799
8251
  project,
7800
8252
  // Resolve NEAT_HOME so a `neat watch` picks up connectors added to
7801
8253
  // ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
7802
- neatHome: process.env.NEAT_HOME ? path17.resolve(process.env.NEAT_HOME) : path17.join(os4.homedir(), ".neat"),
8254
+ neatHome: process.env.NEAT_HOME ? path18.resolve(process.env.NEAT_HOME) : path18.join(os5.homedir(), ".neat"),
7803
8255
  ...embeddingsCachePath ? { embeddingsCachePath } : {},
7804
8256
  host: process.env.HOST ?? "0.0.0.0",
7805
8257
  port: Number(process.env.PORT ?? 8080),
@@ -7981,11 +8433,11 @@ async function main() {
7981
8433
  process.exit(1);
7982
8434
  }
7983
8435
  async function tryOrchestrator(cmd, parsed) {
7984
- const scanPath = path17.resolve(cmd);
7985
- const stat = await fs15.stat(scanPath).catch(() => null);
8436
+ const scanPath = path18.resolve(cmd);
8437
+ const stat = await fs16.stat(scanPath).catch(() => null);
7986
8438
  if (!stat || !stat.isDirectory()) return null;
7987
8439
  const projectExplicit = parsed.project !== null;
7988
- const projectName = projectExplicit ? parsed.project : path17.basename(scanPath);
8440
+ const projectName = projectExplicit ? parsed.project : path18.basename(scanPath);
7989
8441
  const result = await runOrchestrator({
7990
8442
  scanPath,
7991
8443
  project: projectName,
@@ -8048,19 +8500,50 @@ Pass --project <name> to choose:
8048
8500
  ${names}`
8049
8501
  );
8050
8502
  }
8051
- async function resolveDaemonUrl(project) {
8052
- const explicit = process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL;
8053
- if (explicit) return explicit;
8054
- if (project) {
8055
- const daemon = await findDaemonByProject(project);
8056
- if (daemon) return `http://localhost:${daemon.record.ports.rest}`;
8503
+ var UnknownProfileError = class extends Error {
8504
+ constructor(name) {
8505
+ super(
8506
+ `no profile named "${name}" in ~/.neat/profiles.json \u2014 run \`neat login\` first, or drop --profile / NEAT_PROFILE`
8507
+ );
8508
+ this.name = "UnknownProfileError";
8057
8509
  }
8058
- return "http://localhost:8080";
8510
+ };
8511
+ async function resolveClientTarget(opts = {}) {
8512
+ const named = opts.profile ?? process.env.NEAT_PROFILE;
8513
+ if (named && named.length > 0) {
8514
+ const profile = await resolveProfile(named);
8515
+ if (!profile) throw new UnknownProfileError(named);
8516
+ return { endpoint: profile.endpoint, authToken: profile.authToken, source: "profile" };
8517
+ }
8518
+ const pin = process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL;
8519
+ if (pin) return { endpoint: pin, authToken: resolveAuthToken(), source: "env" };
8520
+ const active = await getActiveProfile().catch(() => void 0);
8521
+ if (active) return { endpoint: active.endpoint, authToken: active.authToken, source: "active" };
8522
+ if (opts.project) {
8523
+ const daemon = await findDaemonByProject(opts.project);
8524
+ if (daemon) {
8525
+ return { endpoint: `http://localhost:${daemon.record.ports.rest}`, source: "daemon-record" };
8526
+ }
8527
+ }
8528
+ return { endpoint: "http://localhost:8080", source: "default" };
8529
+ }
8530
+ async function resolveDaemonUrl(project, profile) {
8531
+ return (await resolveClientTarget({ project, profile })).endpoint;
8059
8532
  }
8060
8533
  async function runQueryVerb(cmd, parsed) {
8061
8534
  const requestedProject = resolveProjectFlag(parsed);
8062
- const baseUrl = await resolveDaemonUrl(requestedProject);
8063
- const client = createHttpClient(baseUrl, resolveAuthToken());
8535
+ let target;
8536
+ try {
8537
+ target = await resolveClientTarget({ project: requestedProject, profile: parsed.profile ?? void 0 });
8538
+ } catch (err) {
8539
+ if (err instanceof UnknownProfileError) {
8540
+ process.stderr.write(`${err.message}
8541
+ `);
8542
+ return 2;
8543
+ }
8544
+ throw err;
8545
+ }
8546
+ const client = createHttpClient(target.endpoint, target.authToken);
8064
8547
  const positional = parsed.positional;
8065
8548
  let makeWork;
8066
8549
  switch (cmd) {
@@ -8224,7 +8707,7 @@ async function runQueryVerb(cmd, parsed) {
8224
8707
  const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
8225
8708
  console.error(`neat ${cmd}: ${detail.trim()}`);
8226
8709
  } else if (err instanceof TransportError) {
8227
- console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (endpoint=${baseUrl})`);
8710
+ console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (endpoint=${target.endpoint})`);
8228
8711
  } else {
8229
8712
  console.error(`neat ${cmd}: ${err.message}`);
8230
8713
  }
@@ -8233,9 +8716,14 @@ async function runQueryVerb(cmd, parsed) {
8233
8716
  }
8234
8717
  async function runMonitorVerb(parsed) {
8235
8718
  const requestedProject = resolveProjectFlag(parsed);
8236
- const baseUrl = await resolveDaemonUrl(requestedProject);
8237
- const token = resolveAuthToken();
8238
- const client = createHttpClient(baseUrl, token);
8719
+ let target;
8720
+ try {
8721
+ target = await resolveClientTarget({ project: requestedProject, profile: parsed.profile ?? void 0 });
8722
+ } catch (err) {
8723
+ if (err instanceof UnknownProfileError) return 0;
8724
+ throw err;
8725
+ }
8726
+ const client = createHttpClient(target.endpoint, target.authToken);
8239
8727
  let project;
8240
8728
  try {
8241
8729
  project = await resolveProjectForVerb(client, parsed);
@@ -8251,10 +8739,10 @@ async function runMonitorVerb(parsed) {
8251
8739
  process.once("SIGTERM", onSignal);
8252
8740
  try {
8253
8741
  return await runMonitor({
8254
- baseUrl,
8742
+ baseUrl: target.endpoint,
8255
8743
  project,
8256
8744
  json: parsed.json,
8257
- authToken: token,
8745
+ authToken: target.authToken,
8258
8746
  signal: controller.signal
8259
8747
  });
8260
8748
  } finally {
@@ -8273,12 +8761,14 @@ export {
8273
8761
  CLAUDE_SKILL_CONFIG,
8274
8762
  ProjectResolutionError,
8275
8763
  QUERY_VERBS,
8764
+ UnknownProfileError,
8276
8765
  commandPrefix,
8277
8766
  isNpxInvocation,
8278
8767
  main,
8279
8768
  parseArgs,
8280
8769
  printBanner,
8281
8770
  readPackageVersion,
8771
+ resolveClientTarget,
8282
8772
  resolveDaemonUrl,
8283
8773
  resolveProjectForVerb,
8284
8774
  runInit,