@neat.is/core 0.7.4 → 0.7.6

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
@@ -6,7 +6,7 @@ import {
6
6
  resolveHost,
7
7
  resolveNeatVersion,
8
8
  writeDaemonRecord
9
- } from "./chunk-KN6O7KMK.js";
9
+ } from "./chunk-XHSK4BGL.js";
10
10
  import {
11
11
  buildSearchIndex
12
12
  } from "./chunk-BIY46Q6U.js";
@@ -74,7 +74,7 @@ import {
74
74
  startStalenessLoop,
75
75
  upsertConnectorEntry,
76
76
  validateConnectorEntry
77
- } from "./chunk-UU4XKSTO.js";
77
+ } from "./chunk-XT4NNFH6.js";
78
78
  import {
79
79
  startOtelGrpcReceiver
80
80
  } from "./chunk-6H757ZNM.js";
@@ -88,9 +88,9 @@ import {
88
88
  } from "./chunk-P2ZEKJ35.js";
89
89
 
90
90
  // src/cli.ts
91
- import path11 from "path";
92
- import os2 from "os";
93
- import { promises as fs9 } from "fs";
91
+ import path13 from "path";
92
+ import os4 from "os";
93
+ import { promises as fs11 } from "fs";
94
94
 
95
95
  // src/banner.ts
96
96
  import path from "path";
@@ -4471,6 +4471,612 @@ async function runHooksCommand(args) {
4471
4471
  }
4472
4472
  }
4473
4473
 
4474
+ // src/codex-cli.ts
4475
+ import path10 from "path";
4476
+ import os2 from "os";
4477
+ import { promises as fs9 } from "fs";
4478
+ import { isDeepStrictEqual } from "util";
4479
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
4480
+ var CODEX_MCP_SERVER = {
4481
+ command: "npx",
4482
+ args: ["-y", "@neat.is/mcp"],
4483
+ env: { NEAT_CORE_URL: "http://localhost:8080" }
4484
+ };
4485
+ var CODEX_NEAT_BLOCK = [
4486
+ "[mcp_servers.neat]",
4487
+ 'command = "npx"',
4488
+ 'args = ["-y", "@neat.is/mcp"]',
4489
+ 'env = { NEAT_CORE_URL = "http://localhost:8080" }'
4490
+ ].join("\n");
4491
+ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
4492
+ var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
4493
+ function codexConfigPath() {
4494
+ const override = process.env.NEAT_CODEX_CONFIG;
4495
+ if (override && override.length > 0) return path10.resolve(override);
4496
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? os2.homedir();
4497
+ return path10.join(home, ".codex", "config.toml");
4498
+ }
4499
+ function agentsFilePath() {
4500
+ const override = process.env.NEAT_CODEX_AGENTS;
4501
+ if (override && override.length > 0) return path10.resolve(override);
4502
+ return path10.join(process.cwd(), "AGENTS.md");
4503
+ }
4504
+ function isTableHeader(line) {
4505
+ return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
4506
+ }
4507
+ function tableName(line) {
4508
+ return line.trim().replace(/^\[\[?/, "").replace(/\]\]?$/, "").trim();
4509
+ }
4510
+ function isNeatHeader(line) {
4511
+ return isTableHeader(line) && tableName(line) === "mcp_servers.neat";
4512
+ }
4513
+ function isNeatChildHeader(line) {
4514
+ if (!isTableHeader(line)) return false;
4515
+ const name = tableName(line);
4516
+ return name === "mcp_servers.neat" || name.startsWith("mcp_servers.neat.");
4517
+ }
4518
+ function upsertCodexConfig(raw) {
4519
+ const trimmed = raw.trim();
4520
+ const parsed = trimmed.length > 0 ? parseToml(raw) : {};
4521
+ const existingServers = parsed.mcp_servers ?? {};
4522
+ const spliced = spliceNeatBlock(raw);
4523
+ let text;
4524
+ if (verifyPreserved(raw, spliced, parsed)) {
4525
+ text = spliced;
4526
+ } else {
4527
+ const merged = {
4528
+ ...parsed,
4529
+ mcp_servers: { ...existingServers, neat: CODEX_MCP_SERVER }
4530
+ };
4531
+ text = stringifyToml(merged);
4532
+ if (!text.endsWith("\n")) text += "\n";
4533
+ }
4534
+ return { text, changed: text !== raw };
4535
+ }
4536
+ function spliceNeatBlock(raw) {
4537
+ const block = CODEX_NEAT_BLOCK;
4538
+ const lines = raw.length > 0 ? raw.split("\n") : [];
4539
+ const start = lines.findIndex(isNeatHeader);
4540
+ if (start === -1) {
4541
+ const base = raw.replace(/\n+$/, "");
4542
+ return base.length > 0 ? `${base}
4543
+
4544
+ ${block}
4545
+ ` : `${block}
4546
+ `;
4547
+ }
4548
+ let end = start + 1;
4549
+ for (; ; ) {
4550
+ while (end < lines.length && !isTableHeader(lines[end])) end++;
4551
+ if (end < lines.length && isNeatChildHeader(lines[end])) {
4552
+ end++;
4553
+ continue;
4554
+ }
4555
+ break;
4556
+ }
4557
+ const before = lines.slice(0, start).join("\n").replace(/\n*$/, "");
4558
+ const after = lines.slice(end).join("\n").replace(/^\n*/, "");
4559
+ let text = "";
4560
+ if (before.length > 0) text += `${before}
4561
+
4562
+ `;
4563
+ text += `${block}
4564
+ `;
4565
+ if (after.length > 0) text += `
4566
+ ${after}`;
4567
+ return `${text.replace(/\n*$/, "")}
4568
+ `;
4569
+ }
4570
+ function verifyPreserved(raw, spliced, original) {
4571
+ let next;
4572
+ try {
4573
+ next = parseToml(spliced);
4574
+ } catch {
4575
+ return false;
4576
+ }
4577
+ const origServers = original.mcp_servers ?? {};
4578
+ const nextServers = next.mcp_servers ?? {};
4579
+ if (!isDeepStrictEqual(nextServers.neat, CODEX_MCP_SERVER)) return false;
4580
+ for (const name of Object.keys(origServers)) {
4581
+ if (name === "neat") continue;
4582
+ if (!isDeepStrictEqual(nextServers[name], origServers[name])) return false;
4583
+ }
4584
+ for (const name of Object.keys(nextServers)) {
4585
+ if (name !== "neat" && !(name in origServers)) return false;
4586
+ }
4587
+ for (const key of Object.keys(original)) {
4588
+ if (key === "mcp_servers") continue;
4589
+ if (!isDeepStrictEqual(next[key], original[key])) return false;
4590
+ }
4591
+ for (const key of Object.keys(next)) {
4592
+ if (key !== "mcp_servers" && !(key in original)) return false;
4593
+ }
4594
+ return true;
4595
+ }
4596
+ function agentsBlock(guide) {
4597
+ return `${NEAT_GRAPH_FIRST_START}
4598
+ ${guide.replace(/\s+$/, "")}
4599
+ ${NEAT_GRAPH_FIRST_END}
4600
+ `;
4601
+ }
4602
+ function upsertAgents(raw, guide) {
4603
+ const block = agentsBlock(guide);
4604
+ if (raw.length === 0) return { text: block, changed: true };
4605
+ const startIdx = raw.indexOf(NEAT_GRAPH_FIRST_START);
4606
+ const endIdx = raw.indexOf(NEAT_GRAPH_FIRST_END);
4607
+ if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
4608
+ const before = raw.slice(0, startIdx);
4609
+ const after = raw.slice(endIdx + NEAT_GRAPH_FIRST_END.length);
4610
+ const text2 = `${before}${block.replace(/\n+$/, "")}${after}`;
4611
+ return { text: text2, changed: text2 !== raw };
4612
+ }
4613
+ const base = raw.replace(/\n+$/, "");
4614
+ const text = base.length > 0 ? `${base}
4615
+
4616
+ ${block}` : block;
4617
+ return { text, changed: text !== raw };
4618
+ }
4619
+ async function readGuide() {
4620
+ return readSkillAsset(GUIDE_FILENAME);
4621
+ }
4622
+ async function runCodex(opts) {
4623
+ if (opts.printConfig) {
4624
+ process.stdout.write(`${CODEX_NEAT_BLOCK}
4625
+ `);
4626
+ return { exitCode: 0 };
4627
+ }
4628
+ if (opts.printGuide) {
4629
+ process.stdout.write(agentsBlock(await readGuide()));
4630
+ return { exitCode: 0 };
4631
+ }
4632
+ const configPath = codexConfigPath();
4633
+ const agentsPath = agentsFilePath();
4634
+ let configRaw = "";
4635
+ try {
4636
+ configRaw = await fs9.readFile(configPath, "utf8");
4637
+ } catch (err) {
4638
+ if (err.code !== "ENOENT") {
4639
+ console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
4640
+ return { exitCode: 1 };
4641
+ }
4642
+ }
4643
+ let agentsRaw = "";
4644
+ try {
4645
+ agentsRaw = await fs9.readFile(agentsPath, "utf8");
4646
+ } catch (err) {
4647
+ if (err.code !== "ENOENT") {
4648
+ console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
4649
+ return { exitCode: 1 };
4650
+ }
4651
+ }
4652
+ let config;
4653
+ try {
4654
+ config = upsertCodexConfig(configRaw);
4655
+ } catch (err) {
4656
+ console.error(
4657
+ `neat codex: ${configPath} is not valid TOML \u2014 ${err.message}`
4658
+ );
4659
+ console.error("neat codex: fix the file and re-run; nothing was written.");
4660
+ return { exitCode: 1 };
4661
+ }
4662
+ const guide = await readGuide();
4663
+ const agents = upsertAgents(agentsRaw, guide);
4664
+ if (!opts.apply) {
4665
+ console.log("neat codex \u2014 plan (nothing written; re-run with --apply to write)");
4666
+ console.log("");
4667
+ console.log(` Codex MCP config: ${configPath}`);
4668
+ console.log(
4669
+ config.changed ? ` ${configRaw ? "update" : "create"} the [mcp_servers.neat] table:` : " already up to date \u2014 [mcp_servers.neat] matches"
4670
+ );
4671
+ if (config.changed) {
4672
+ for (const line of CODEX_NEAT_BLOCK.split("\n")) console.log(` ${line}`);
4673
+ }
4674
+ console.log("");
4675
+ console.log(` Project instructions: ${agentsPath}`);
4676
+ console.log(
4677
+ agents.changed ? ` ${agentsRaw ? "update" : "create"} the graph-first block (between ${NEAT_GRAPH_FIRST_START} markers)` : " already up to date \u2014 graph-first block matches"
4678
+ );
4679
+ console.log("");
4680
+ console.log("The MCP server reads NEAT_CORE_URL for the daemon URL \u2014 edit that value in");
4681
+ console.log("the generated table to point Codex at a non-default daemon.");
4682
+ return { exitCode: 0 };
4683
+ }
4684
+ if (config.changed) {
4685
+ await fs9.mkdir(path10.dirname(configPath), { recursive: true });
4686
+ await fs9.writeFile(configPath, config.text, "utf8");
4687
+ console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
4688
+ } else {
4689
+ console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
4690
+ }
4691
+ if (agents.changed) {
4692
+ await fs9.mkdir(path10.dirname(agentsPath), { recursive: true });
4693
+ await fs9.writeFile(agentsPath, agents.text, "utf8");
4694
+ console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
4695
+ } else {
4696
+ console.log(`neat codex: ${agentsPath} already has the graph-first block`);
4697
+ }
4698
+ console.log("");
4699
+ console.log("restart Codex to pick up the new MCP server. NEAT_CORE_URL in the table");
4700
+ console.log("points the server at the local daemon \u2014 edit it for a non-default one.");
4701
+ return { exitCode: 0 };
4702
+ }
4703
+ function usage2() {
4704
+ console.log("neat codex \u2014 install NEAT into the OpenAI Codex CLI (MCP server + AGENTS.md)");
4705
+ console.log("");
4706
+ console.log(" (no flag) plan: print what would change, write nothing");
4707
+ console.log(" --apply add [mcp_servers.neat] to ~/.codex/config.toml and write");
4708
+ console.log(" the graph-first block into ./AGENTS.md, merging into both");
4709
+ console.log(" without touching your other servers or instructions");
4710
+ console.log(" --print-config print the [mcp_servers.neat] TOML block to stdout");
4711
+ console.log(" --print-guide print the AGENTS.md graph-first block to stdout");
4712
+ console.log("");
4713
+ console.log("Existing config is preserved and a re-run is a no-op. A malformed");
4714
+ console.log("config.toml is a clear error with no partial write.");
4715
+ }
4716
+ async function runCodexCommand(args) {
4717
+ const opts = { apply: false, printConfig: false, printGuide: false };
4718
+ for (const arg of args) {
4719
+ switch (arg) {
4720
+ case "--apply":
4721
+ opts.apply = true;
4722
+ break;
4723
+ case "--print-config":
4724
+ opts.printConfig = true;
4725
+ break;
4726
+ case "--print-guide":
4727
+ opts.printGuide = true;
4728
+ break;
4729
+ case "-h":
4730
+ case "--help":
4731
+ usage2();
4732
+ return 0;
4733
+ default:
4734
+ console.error(`neat codex: unknown flag "${arg}"`);
4735
+ usage2();
4736
+ return 2;
4737
+ }
4738
+ }
4739
+ try {
4740
+ const { exitCode } = await runCodex(opts);
4741
+ return exitCode;
4742
+ } catch (err) {
4743
+ console.error(err.message);
4744
+ return 1;
4745
+ }
4746
+ }
4747
+
4748
+ // src/editors-cli.ts
4749
+ import path11 from "path";
4750
+ import os3 from "os";
4751
+ import { promises as fs10 } from "fs";
4752
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
4753
+ import * as jsonc from "jsonc-parser";
4754
+ var NEAT_MCP_SERVER = {
4755
+ command: "npx",
4756
+ args: ["-y", "@neat.is/mcp"]
4757
+ };
4758
+ var NEAT_OPENCODE_SERVER = {
4759
+ type: "local",
4760
+ command: ["npx", "-y", "@neat.is/mcp"],
4761
+ enabled: true
4762
+ };
4763
+ var NEAT_CRUSH_SERVER = {
4764
+ type: "stdio",
4765
+ command: "npx",
4766
+ args: ["-y", "@neat.is/mcp"]
4767
+ };
4768
+ var GRAPH_FIRST_MARKER_OPEN = "<!-- neat:graph-first -->";
4769
+ var GRAPH_FIRST_MARKER_CLOSE = "<!-- /neat:graph-first -->";
4770
+ function homeDir() {
4771
+ return process.env.HOME ?? process.env.USERPROFILE ?? os3.homedir();
4772
+ }
4773
+ function xdgConfigDir() {
4774
+ const xdg = process.env.XDG_CONFIG_HOME;
4775
+ return xdg && xdg.length > 0 ? path11.resolve(xdg) : path11.join(homeDir(), ".config");
4776
+ }
4777
+ function envOverride(name) {
4778
+ const v = process.env[name];
4779
+ return v && v.length > 0 ? path11.resolve(v) : void 0;
4780
+ }
4781
+ var CURSOR_CLIENT = {
4782
+ id: "cursor",
4783
+ label: "Cursor",
4784
+ docsUrl: "https://docs.cursor.com/context/mcp",
4785
+ mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? path11.join(homeDir(), ".cursor", "mcp.json"),
4786
+ mcpContainerKey: "mcpServers",
4787
+ format: "json",
4788
+ // Cursor still reads a single `.cursorrules` at the project root (the modern
4789
+ // `.cursor/rules/*.mdc` split is one-rule-per-file with frontmatter — a worse
4790
+ // fit for a marker-fenced block). GRAPH_FIRST.md names this file directly.
4791
+ rulesFileName: ".cursorrules"
4792
+ };
4793
+ var DEVIN_CLIENT = {
4794
+ id: "devin",
4795
+ label: "Devin Desktop (Cascade)",
4796
+ docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
4797
+ mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? path11.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
4798
+ mcpContainerKey: "mcpServers",
4799
+ format: "json",
4800
+ rulesFileName: ".windsurfrules"
4801
+ };
4802
+ var GEMINI_CLIENT = {
4803
+ id: "gemini",
4804
+ label: "Gemini CLI",
4805
+ docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
4806
+ mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? path11.join(homeDir(), ".gemini", "settings.json"),
4807
+ mcpContainerKey: "mcpServers",
4808
+ format: "json",
4809
+ rulesFileName: "GEMINI.md"
4810
+ };
4811
+ var QWEN_CLIENT = {
4812
+ id: "qwen",
4813
+ label: "Qwen Code",
4814
+ docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
4815
+ mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? path11.join(homeDir(), ".qwen", "settings.json"),
4816
+ mcpContainerKey: "mcpServers",
4817
+ format: "json",
4818
+ rulesFileName: "QWEN.md"
4819
+ };
4820
+ var AMAZONQ_CLIENT = {
4821
+ id: "amazonq",
4822
+ label: "Amazon Q Developer CLI",
4823
+ docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
4824
+ mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? path11.join(homeDir(), ".aws", "amazonq", "mcp.json"),
4825
+ mcpContainerKey: "mcpServers",
4826
+ format: "json"
4827
+ };
4828
+ var ROOCODE_CLIENT = {
4829
+ id: "roocode",
4830
+ label: "Roo Code",
4831
+ docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
4832
+ mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? path11.join(process.cwd(), ".roo", "mcp.json"),
4833
+ mcpContainerKey: "mcpServers",
4834
+ format: "json"
4835
+ };
4836
+ var ZED_CLIENT = {
4837
+ id: "zed",
4838
+ label: "Zed",
4839
+ docsUrl: "https://zed.dev/docs/ai/mcp",
4840
+ mcpConfigPath: () => {
4841
+ const override = envOverride("NEAT_ZED_CONFIG");
4842
+ if (override) return override;
4843
+ if (process.platform === "win32") {
4844
+ const appData = process.env.APPDATA;
4845
+ if (appData && appData.length > 0) return path11.join(appData, "Zed", "settings.json");
4846
+ }
4847
+ return path11.join(homeDir(), ".config", "zed", "settings.json");
4848
+ },
4849
+ mcpContainerKey: "context_servers",
4850
+ format: "jsonc",
4851
+ rulesFileName: ".rules"
4852
+ };
4853
+ var OPENCODE_CLIENT = {
4854
+ id: "opencode",
4855
+ label: "OpenCode",
4856
+ docsUrl: "https://opencode.ai/docs/mcp-servers/",
4857
+ mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? path11.join(xdgConfigDir(), "opencode", "opencode.json"),
4858
+ mcpContainerKey: "mcp",
4859
+ format: "json",
4860
+ serverEntry: NEAT_OPENCODE_SERVER,
4861
+ rulesFileName: "AGENTS.md"
4862
+ };
4863
+ var CRUSH_CLIENT = {
4864
+ id: "crush",
4865
+ label: "Crush",
4866
+ docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
4867
+ mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? path11.join(xdgConfigDir(), "crush", "crush.json"),
4868
+ mcpContainerKey: "mcp",
4869
+ format: "json",
4870
+ serverEntry: NEAT_CRUSH_SERVER,
4871
+ rulesFileName: "AGENTS.md"
4872
+ };
4873
+ var CLIENTS = {
4874
+ cursor: CURSOR_CLIENT,
4875
+ devin: DEVIN_CLIENT,
4876
+ gemini: GEMINI_CLIENT,
4877
+ qwen: QWEN_CLIENT,
4878
+ amazonq: AMAZONQ_CLIENT,
4879
+ roocode: ROOCODE_CLIENT,
4880
+ zed: ZED_CLIENT,
4881
+ opencode: OPENCODE_CLIENT,
4882
+ crush: CRUSH_CLIENT
4883
+ };
4884
+ function mergeJsonMcp(existing, containerKey, serverEntry) {
4885
+ const servers = existing[containerKey] ?? {};
4886
+ const already = isDeepStrictEqual2(servers.neat, serverEntry);
4887
+ const merged = {
4888
+ ...existing,
4889
+ [containerKey]: { ...servers, neat: serverEntry }
4890
+ };
4891
+ return { merged, changed: !already };
4892
+ }
4893
+ function mergeJsoncMcp(raw, containerKey, serverEntry) {
4894
+ const base = raw.trim().length > 0 ? raw : "{}";
4895
+ const parsed = jsonc.parse(base) ?? {};
4896
+ const servers = parsed[containerKey] ?? {};
4897
+ if (isDeepStrictEqual2(servers.neat, serverEntry)) {
4898
+ return { text: raw, changed: false };
4899
+ }
4900
+ const edits = jsonc.modify(base, [containerKey, "neat"], serverEntry, {
4901
+ formattingOptions: { tabSize: 2, insertSpaces: true }
4902
+ });
4903
+ let text = jsonc.applyEdits(base, edits);
4904
+ if (!text.endsWith("\n")) text += "\n";
4905
+ return { text, changed: text !== raw };
4906
+ }
4907
+ function escapeRegExp(s) {
4908
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4909
+ }
4910
+ function buildGuidanceBlock(guide) {
4911
+ return `${GRAPH_FIRST_MARKER_OPEN}
4912
+ ${guide.trim()}
4913
+ ${GRAPH_FIRST_MARKER_CLOSE}
4914
+ `;
4915
+ }
4916
+ function mergeRulesFile(existing, block) {
4917
+ const region = new RegExp(
4918
+ `${escapeRegExp(GRAPH_FIRST_MARKER_OPEN)}[\\s\\S]*?${escapeRegExp(GRAPH_FIRST_MARKER_CLOSE)}\\n?`
4919
+ );
4920
+ if (region.test(existing)) return existing.replace(region, block);
4921
+ if (existing.trim().length === 0) return block;
4922
+ return `${existing.replace(/\s+$/, "")}
4923
+
4924
+ ${block}`;
4925
+ }
4926
+ async function planMcp(client, mcpPath) {
4927
+ const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
4928
+ let raw = "";
4929
+ try {
4930
+ raw = await fs10.readFile(mcpPath, "utf8");
4931
+ } catch (err) {
4932
+ const e = err;
4933
+ if (e.code === "ENOENT") {
4934
+ raw = "";
4935
+ } else {
4936
+ console.error(`neat ${client.id}: failed to read ${mcpPath} \u2014 ${e.message}`);
4937
+ return null;
4938
+ }
4939
+ }
4940
+ if (client.format === "jsonc") {
4941
+ if (raw.trim().length > 0) {
4942
+ const errors = [];
4943
+ jsonc.parse(raw, errors, { allowTrailingComma: true });
4944
+ if (errors.length > 0) {
4945
+ const first = errors[0];
4946
+ console.error(
4947
+ `neat ${client.id}: ${mcpPath} is not valid JSONC \u2014 ${jsonc.printParseErrorCode(first.error)} at offset ${first.offset}. Fix it (or move it aside) and re-run; nothing was written.`
4948
+ );
4949
+ return null;
4950
+ }
4951
+ }
4952
+ return mergeJsoncMcp(raw, client.mcpContainerKey, serverEntry);
4953
+ }
4954
+ let existing = {};
4955
+ if (raw.trim().length > 0) {
4956
+ try {
4957
+ existing = JSON.parse(raw);
4958
+ } catch (err) {
4959
+ console.error(
4960
+ `neat ${client.id}: ${mcpPath} is not valid JSON \u2014 ${err.message}. Fix it (or move it aside) and re-run; nothing was written.`
4961
+ );
4962
+ return null;
4963
+ }
4964
+ }
4965
+ const { merged, changed } = mergeJsonMcp(existing, client.mcpContainerKey, serverEntry);
4966
+ return { text: JSON.stringify(merged, null, 2) + "\n", changed };
4967
+ }
4968
+ async function runEditorInstall(client, opts) {
4969
+ const mcpPath = client.mcpConfigPath();
4970
+ const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
4971
+ const hasRules = typeof client.rulesFileName === "string";
4972
+ const rulesPath = hasRules ? path11.join(opts.projectDir, client.rulesFileName) : "";
4973
+ const mcp = await planMcp(client, mcpPath);
4974
+ if (mcp === null) return { exitCode: 1 };
4975
+ let existingRules = "";
4976
+ let newRules = "";
4977
+ let rulesChanged = false;
4978
+ let block = "";
4979
+ if (hasRules) {
4980
+ try {
4981
+ existingRules = await fs10.readFile(rulesPath, "utf8");
4982
+ } catch (err) {
4983
+ if (err.code !== "ENOENT") {
4984
+ console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
4985
+ return { exitCode: 1 };
4986
+ }
4987
+ }
4988
+ const guide = await readSkillAsset(GUIDE_FILENAME);
4989
+ block = buildGuidanceBlock(guide);
4990
+ newRules = mergeRulesFile(existingRules, block);
4991
+ rulesChanged = newRules !== existingRules;
4992
+ }
4993
+ if (!opts.apply) {
4994
+ console.log(`neat ${client.id} \u2014 wire NEAT into ${client.label} (plan; nothing written)`);
4995
+ console.log("");
4996
+ console.log(`MCP server \u2192 ${mcpPath}`);
4997
+ console.log(
4998
+ mcp.changed ? ` would add ${client.mcpContainerKey}.neat:` : ` ${client.mcpContainerKey}.neat already present and current \u2014 no change:`
4999
+ );
5000
+ console.log(indent(JSON.stringify({ neat: serverEntry }, null, 2)));
5001
+ if (hasRules) {
5002
+ console.log("");
5003
+ console.log(`Graph-first guidance \u2192 ${rulesPath}`);
5004
+ console.log(
5005
+ rulesChanged ? existingRules.includes(GRAPH_FIRST_MARKER_OPEN) ? " would refresh the neat:graph-first block:" : " would add the neat:graph-first block:" : " neat:graph-first block already present and current \u2014 no change."
5006
+ );
5007
+ if (rulesChanged) console.log(indent(block.trimEnd()));
5008
+ }
5009
+ console.log("");
5010
+ console.log(
5011
+ hasRules ? `Re-run with --apply to write both files. Existing servers and rules are kept.` : `Re-run with --apply to write the config. Existing servers are kept.`
5012
+ );
5013
+ return { exitCode: 0 };
5014
+ }
5015
+ await fs10.mkdir(path11.dirname(mcpPath), { recursive: true });
5016
+ await fs10.writeFile(mcpPath, mcp.text, "utf8");
5017
+ if (hasRules) {
5018
+ await fs10.mkdir(path11.dirname(rulesPath), { recursive: true });
5019
+ await fs10.writeFile(rulesPath, newRules, "utf8");
5020
+ }
5021
+ console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
5022
+ console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
5023
+ if (hasRules) console.log(` guidance: ${rulesPath} (neat:graph-first block)`);
5024
+ console.log("");
5025
+ console.log(`restart ${client.label} to pick up the MCP server. Point it at a non-default`);
5026
+ console.log(`daemon by setting NEAT_CORE_URL in the neat server's env in that config.`);
5027
+ return { exitCode: 0 };
5028
+ }
5029
+ function indent(text) {
5030
+ return text.split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n");
5031
+ }
5032
+ function usage3(client) {
5033
+ const hasRules = typeof client.rulesFileName === "string";
5034
+ console.log(
5035
+ hasRules ? `neat ${client.id} \u2014 install NEAT's MCP server + graph-first guidance into ${client.label}` : `neat ${client.id} \u2014 install NEAT's MCP server into ${client.label}`
5036
+ );
5037
+ console.log("");
5038
+ console.log(
5039
+ hasRules ? " --apply write the MCP config and the rules file (default: plan only)" : " --apply write the MCP config (default: plan only)"
5040
+ );
5041
+ console.log("");
5042
+ console.log("Writes NEAT's stdio MCP server (npx -y @neat.is/mcp) into");
5043
+ console.log(` ${client.mcpConfigPath()}`);
5044
+ if (hasRules) {
5045
+ console.log(`and the graph-first guidance block into ./${client.rulesFileName}, both`);
5046
+ console.log("additively \u2014 existing servers and rules are preserved, a re-run is a no-op.");
5047
+ } else {
5048
+ console.log("additively \u2014 existing servers are preserved, a re-run is a no-op.");
5049
+ }
5050
+ console.log("");
5051
+ console.log(`See ${client.docsUrl} for ${client.label}'s MCP config format.`);
5052
+ }
5053
+ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
5054
+ const client = CLIENTS[clientId];
5055
+ let apply4 = false;
5056
+ for (const arg of args) {
5057
+ switch (arg) {
5058
+ case "--apply":
5059
+ apply4 = true;
5060
+ break;
5061
+ case "-h":
5062
+ case "--help":
5063
+ usage3(client);
5064
+ return 0;
5065
+ default:
5066
+ console.error(`neat ${client.id}: unknown flag "${arg}"`);
5067
+ usage3(client);
5068
+ return 2;
5069
+ }
5070
+ }
5071
+ try {
5072
+ const { exitCode } = await runEditorInstall(client, { apply: apply4, projectDir });
5073
+ return exitCode;
5074
+ } catch (err) {
5075
+ console.error(err.message);
5076
+ return 1;
5077
+ }
5078
+ }
5079
+
4474
5080
  // src/monitor.ts
4475
5081
  import { EdgeType, parseEdgeId, Provenance as Provenance3 } from "@neat.is/types";
4476
5082
 
@@ -4500,10 +5106,10 @@ function createHttpClient(baseUrl, bearerToken) {
4500
5106
  const root = baseUrl.replace(/\/$/, "");
4501
5107
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
4502
5108
  return {
4503
- async get(path12) {
5109
+ async get(path14) {
4504
5110
  let res;
4505
5111
  try {
4506
- res = await fetch(`${root}${path12}`, {
5112
+ res = await fetch(`${root}${path14}`, {
4507
5113
  headers: { ...authHeader }
4508
5114
  });
4509
5115
  } catch (err) {
@@ -4515,16 +5121,16 @@ function createHttpClient(baseUrl, bearerToken) {
4515
5121
  const body = await res.text().catch(() => "");
4516
5122
  throw new HttpError(
4517
5123
  res.status,
4518
- `${res.status} ${res.statusText} on GET ${path12}: ${body}`,
5124
+ `${res.status} ${res.statusText} on GET ${path14}: ${body}`,
4519
5125
  body
4520
5126
  );
4521
5127
  }
4522
5128
  return await res.json();
4523
5129
  },
4524
- async post(path12, body) {
5130
+ async post(path14, body) {
4525
5131
  let res;
4526
5132
  try {
4527
- res = await fetch(`${root}${path12}`, {
5133
+ res = await fetch(`${root}${path14}`, {
4528
5134
  method: "POST",
4529
5135
  headers: { "content-type": "application/json", ...authHeader },
4530
5136
  body: JSON.stringify(body)
@@ -4538,7 +5144,7 @@ function createHttpClient(baseUrl, bearerToken) {
4538
5144
  const text = await res.text().catch(() => "");
4539
5145
  throw new HttpError(
4540
5146
  res.status,
4541
- `${res.status} ${res.statusText} on POST ${path12}: ${text}`,
5147
+ `${res.status} ${res.statusText} on POST ${path14}: ${text}`,
4542
5148
  text
4543
5149
  );
4544
5150
  }
@@ -4552,12 +5158,12 @@ function projectPath(project, suffix) {
4552
5158
  }
4553
5159
  async function runRootCause(client, input) {
4554
5160
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
4555
- const path12 = projectPath(
5161
+ const path14 = projectPath(
4556
5162
  input.project,
4557
5163
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
4558
5164
  );
4559
5165
  try {
4560
- const result = await client.get(path12);
5166
+ const result = await client.get(path14);
4561
5167
  const arrowPath = result.traversalPath.join(" \u2190 ");
4562
5168
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
4563
5169
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -4583,12 +5189,12 @@ async function runRootCause(client, input) {
4583
5189
  }
4584
5190
  async function runBlastRadius(client, input) {
4585
5191
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
4586
- const path12 = projectPath(
5192
+ const path14 = projectPath(
4587
5193
  input.project,
4588
5194
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
4589
5195
  );
4590
5196
  try {
4591
- const result = await client.get(path12);
5197
+ const result = await client.get(path14);
4592
5198
  if (result.totalAffected === 0) {
4593
5199
  return {
4594
5200
  summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
@@ -4622,12 +5228,12 @@ function formatBlastEntry(n) {
4622
5228
  }
4623
5229
  async function runDependencies(client, input) {
4624
5230
  const depth = input.depth ?? 3;
4625
- const path12 = projectPath(
5231
+ const path14 = projectPath(
4626
5232
  input.project,
4627
5233
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
4628
5234
  );
4629
5235
  try {
4630
- const result = await client.get(path12);
5236
+ const result = await client.get(path14);
4631
5237
  if (result.total === 0) {
4632
5238
  return {
4633
5239
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -4719,9 +5325,9 @@ function formatDuration(ms) {
4719
5325
  return `${Math.round(h / 24)}d`;
4720
5326
  }
4721
5327
  async function runIncidents(client, input) {
4722
- const path12 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
5328
+ const path14 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
4723
5329
  try {
4724
- const body = await client.get(path12);
5330
+ const body = await client.get(path14);
4725
5331
  const events = body.events;
4726
5332
  if (events.length === 0) {
4727
5333
  return {
@@ -5084,6 +5690,24 @@ function observedEdgeJson(edge) {
5084
5690
  provenance: edge.provenance
5085
5691
  });
5086
5692
  }
5693
+ function policyKey(v) {
5694
+ return `policy|${v.id}`;
5695
+ }
5696
+ function policySubject(v) {
5697
+ const s = v.subject;
5698
+ if (s.nodeId) return s.nodeId;
5699
+ if (s.edgeId) return s.edgeId;
5700
+ if (s.path && s.path.length > 0) return s.path.join(" \u2192 ");
5701
+ return "";
5702
+ }
5703
+ function formatPolicyLine(v) {
5704
+ const subject = policySubject(v);
5705
+ const where = subject ? ` (${subject})` : "";
5706
+ return `\u26A0 policy [${v.severity}] ${v.policyName} \u2014 ${v.message}${where}`;
5707
+ }
5708
+ function policyJson(v) {
5709
+ return JSON.stringify({ kind: "policy", ...v });
5710
+ }
5087
5711
  var MonitorEmitter = class {
5088
5712
  constructor(opts) {
5089
5713
  this.opts = opts;
@@ -5127,6 +5751,20 @@ var MonitorEmitter = class {
5127
5751
  this.out(this.opts.json ? observedEdgeJson(edge) : formatObservedEdgeLine(edge));
5128
5752
  return true;
5129
5753
  }
5754
+ // Emit every not-yet-seen policy violation in a fresh /policies/violations
5755
+ // read. Returns the count newly emitted (0 → nothing printed, the silent
5756
+ // path). Idempotent across re-reads: the seen-set keys off the violation id.
5757
+ emitPolicies(response) {
5758
+ let emitted = 0;
5759
+ for (const v of response.violations) {
5760
+ const key = policyKey(v);
5761
+ if (this.seen.has(key)) continue;
5762
+ this.seen.add(key);
5763
+ this.out(this.opts.json ? policyJson(v) : formatPolicyLine(v));
5764
+ emitted++;
5765
+ }
5766
+ return emitted;
5767
+ }
5130
5768
  };
5131
5769
  function parseFrame(raw) {
5132
5770
  let event = "message";
@@ -5181,54 +5819,83 @@ function projectPath2(project, suffix) {
5181
5819
  function backoffDelay(attempt, capMs) {
5182
5820
  return Math.min(capMs, 500 * 2 ** Math.min(attempt, 6));
5183
5821
  }
5184
- async function runMonitor(opts) {
5185
- const write = opts.write ?? ((line) => process.stdout.write(line));
5186
- const debounceMs = opts.debounceMs ?? 400;
5187
- const backoffCapMs = opts.backoffCapMs ?? 1e4;
5188
- const maxReconnects = opts.maxReconnects ?? Number.POSITIVE_INFINITY;
5189
- const client = createHttpClient(opts.baseUrl, opts.authToken);
5190
- const emitter = new MonitorEmitter({ json: opts.json, write });
5191
- const divergencesPath = projectPath2(opts.project, "/graph/divergences");
5192
- const eventsUrl = `${opts.baseUrl.replace(/\/$/, "")}${projectPath2(opts.project, "/events")}`;
5193
- let readTimer = null;
5194
- let reading = false;
5195
- let readPending = false;
5196
- const doRead = async () => {
5197
- if (reading) {
5198
- readPending = true;
5822
+ var defaultOpenEvents = async (url, headers, signal) => {
5823
+ const res = await fetch(url, { headers, signal });
5824
+ return { ok: res.ok, status: res.status, body: res.body };
5825
+ };
5826
+ function makeDebouncedReader(read, debounceMs) {
5827
+ let timer = null;
5828
+ let running = false;
5829
+ let pending = false;
5830
+ const cancel = () => {
5831
+ if (timer) {
5832
+ clearTimeout(timer);
5833
+ timer = null;
5834
+ }
5835
+ };
5836
+ const run = async () => {
5837
+ if (running) {
5838
+ pending = true;
5199
5839
  return;
5200
5840
  }
5201
- reading = true;
5841
+ running = true;
5202
5842
  try {
5203
- const result = await client.get(divergencesPath);
5204
- emitter.emitDivergences(result);
5843
+ await read();
5205
5844
  } catch {
5206
5845
  } finally {
5207
- reading = false;
5208
- if (readPending) {
5209
- readPending = false;
5210
- scheduleRead();
5846
+ running = false;
5847
+ if (pending) {
5848
+ pending = false;
5849
+ schedule();
5211
5850
  }
5212
5851
  }
5213
5852
  };
5214
- const scheduleRead = () => {
5215
- if (readTimer) clearTimeout(readTimer);
5216
- readTimer = setTimeout(() => {
5217
- readTimer = null;
5218
- void doRead();
5853
+ const schedule = () => {
5854
+ cancel();
5855
+ timer = setTimeout(() => {
5856
+ timer = null;
5857
+ void run();
5219
5858
  }, debounceMs);
5220
- if (typeof readTimer.unref === "function") readTimer.unref();
5859
+ if (typeof timer.unref === "function") timer.unref();
5221
5860
  };
5861
+ return {
5862
+ schedule,
5863
+ runNow: async () => {
5864
+ cancel();
5865
+ await run();
5866
+ },
5867
+ cancel
5868
+ };
5869
+ }
5870
+ async function runMonitor(opts) {
5871
+ const write = opts.write ?? ((line) => process.stdout.write(line));
5872
+ const debounceMs = opts.debounceMs ?? 400;
5873
+ const backoffCapMs = opts.backoffCapMs ?? 1e4;
5874
+ const maxReconnects = opts.maxReconnects ?? Number.POSITIVE_INFINITY;
5875
+ const client = opts.httpClient ?? createHttpClient(opts.baseUrl, opts.authToken);
5876
+ const openEvents = opts.openEvents ?? defaultOpenEvents;
5877
+ const emitter = new MonitorEmitter({ json: opts.json, write });
5878
+ const divergencesPath = projectPath2(opts.project, "/graph/divergences");
5879
+ const policiesPath = projectPath2(opts.project, "/policies/violations");
5880
+ const eventsUrl = `${opts.baseUrl.replace(/\/$/, "")}${projectPath2(opts.project, "/events")}`;
5881
+ const divergences = makeDebouncedReader(async () => {
5882
+ const result = await client.get(divergencesPath);
5883
+ emitter.emitDivergences(result);
5884
+ }, debounceMs);
5885
+ const policies = makeDebouncedReader(async () => {
5886
+ const result = await client.get(policiesPath);
5887
+ emitter.emitPolicies(result);
5888
+ }, debounceMs);
5222
5889
  const onFrame = (frame) => {
5223
5890
  switch (frame.event) {
5224
5891
  case "extraction-complete":
5225
- scheduleRead();
5892
+ divergences.schedule();
5226
5893
  break;
5227
5894
  case "stale-transition": {
5228
5895
  const payload = safeParse(frame.data);
5229
5896
  const edgeId = payload && typeof payload.edgeId === "string" ? payload.edgeId : void 0;
5230
5897
  if (edgeId) emitter.emitStale(edgeId);
5231
- scheduleRead();
5898
+ divergences.schedule();
5232
5899
  break;
5233
5900
  }
5234
5901
  case "edge-added": {
@@ -5236,10 +5903,13 @@ async function runMonitor(opts) {
5236
5903
  const edge = payload?.edge;
5237
5904
  if (edge && edge.provenance === Provenance3.OBSERVED) {
5238
5905
  emitter.emitObservedEdge(edge);
5239
- scheduleRead();
5906
+ divergences.schedule();
5240
5907
  }
5241
5908
  break;
5242
5909
  }
5910
+ case "policy-violation":
5911
+ policies.schedule();
5912
+ break;
5243
5913
  default:
5244
5914
  break;
5245
5915
  }
@@ -5252,17 +5922,17 @@ async function runMonitor(opts) {
5252
5922
  let firstConnect = true;
5253
5923
  for (let attempt = 0; ; attempt++) {
5254
5924
  if (opts.signal?.aborted) break;
5255
- let res;
5925
+ let conn;
5256
5926
  try {
5257
- res = await fetch(eventsUrl, { headers, signal: opts.signal });
5927
+ conn = await openEvents(eventsUrl, headers, opts.signal);
5258
5928
  } catch (err) {
5259
5929
  if (err.name === "AbortError") break;
5260
5930
  if (!connectedOnce || attempt >= maxReconnects) break;
5261
5931
  await sleep(backoffDelay(attempt, backoffCapMs), opts.signal);
5262
5932
  continue;
5263
5933
  }
5264
- if (!res.ok || !res.body) {
5265
- await res.body?.cancel().catch(() => {
5934
+ if (!conn.ok || !conn.body) {
5935
+ await conn.body?.cancel().catch(() => {
5266
5936
  });
5267
5937
  if (!connectedOnce || attempt >= maxReconnects) break;
5268
5938
  await sleep(backoffDelay(attempt, backoffCapMs), opts.signal);
@@ -5272,19 +5942,22 @@ async function runMonitor(opts) {
5272
5942
  attempt = 0;
5273
5943
  if (firstConnect) {
5274
5944
  firstConnect = false;
5275
- await doRead();
5945
+ await divergences.runNow();
5946
+ await policies.runNow();
5276
5947
  } else {
5277
- scheduleRead();
5948
+ divergences.schedule();
5949
+ policies.schedule();
5278
5950
  }
5279
5951
  try {
5280
- await drainSse(res.body, onFrame);
5952
+ await drainSse(conn.body, onFrame);
5281
5953
  } catch {
5282
5954
  }
5283
5955
  if (opts.signal?.aborted) break;
5284
5956
  if (attempt >= maxReconnects) break;
5285
5957
  await sleep(backoffDelay(attempt, backoffCapMs), opts.signal);
5286
5958
  }
5287
- if (readTimer) clearTimeout(readTimer);
5959
+ divergences.cancel();
5960
+ policies.cancel();
5288
5961
  return 0;
5289
5962
  }
5290
5963
  function sleep(ms, signal) {
@@ -5307,7 +5980,7 @@ function sleep(ms, signal) {
5307
5980
  }
5308
5981
 
5309
5982
  // src/cli-verbs.ts
5310
- import path10 from "path";
5983
+ import path12 from "path";
5311
5984
  async function resolveProjectEntry(opts) {
5312
5985
  const entries = await listProjects();
5313
5986
  if (opts.project) {
@@ -5317,7 +5990,7 @@ async function resolveProjectEntry(opts) {
5317
5990
  const cwd = opts.cwd ?? process.cwd();
5318
5991
  const resolvedCwd = await normalizeProjectPath(cwd);
5319
5992
  for (const entry2 of entries) {
5320
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path10.sep}`)) {
5993
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path12.sep}`)) {
5321
5994
  return entry2;
5322
5995
  }
5323
5996
  }
@@ -5482,7 +6155,7 @@ function isNpxInvocation() {
5482
6155
  function commandPrefix() {
5483
6156
  return isNpxInvocation() ? "npx neat.is" : "neat";
5484
6157
  }
5485
- function usage2() {
6158
+ function usage4() {
5486
6159
  const neat = commandPrefix();
5487
6160
  console.log("Installed via npx? Prefix commands with `npx neat.is`, or install once: `npm i -g neat.is`.");
5488
6161
  console.log("");
@@ -5502,10 +6175,12 @@ function usage2() {
5502
6175
  console.log(" PORT (default 8080), OTEL_PORT (4318), HOST (0.0.0.0)");
5503
6176
  console.log(" control listeners. NEAT_OTLP_GRPC=true also opens 4317.");
5504
6177
  console.log(" monitor Stream live graph facts to stdout \u2014 one line per new");
5505
- console.log(" fact \u2014 as the daemon learns them: fresh divergences,");
5506
- console.log(" integrations that just went stale, and new observed");
5507
- console.log(" runtime dependencies. Silent when nothing is new; exits");
5508
- console.log(" clean with no output when no daemon is reachable.");
6178
+ console.log(" fact \u2014 as the daemon learns them: fresh divergences");
6179
+ console.log(" (down to the drifting column), integrations that just");
6180
+ console.log(" went stale, new observed runtime dependencies, and");
6181
+ console.log(" freshly-tripped policy violations. Silent when nothing");
6182
+ console.log(" is new; exits clean with no output when no daemon is");
6183
+ console.log(" reachable.");
5509
6184
  console.log(" Flags:");
5510
6185
  console.log(" --project <name> watch a registered project by name");
5511
6186
  console.log(" --json emit one JSON object per line");
@@ -5535,6 +6210,55 @@ function usage2() {
5535
6210
  console.log(" --print-hook print the hook script");
5536
6211
  console.log(" --print-guide print the graph-first guidance");
5537
6212
  console.log(" --print-settings print the settings.json block --apply adds");
6213
+ console.log(" codex Install NEAT into the OpenAI Codex CLI: add [mcp_servers.neat]");
6214
+ console.log(" to ~/.codex/config.toml and the graph-first block to ./AGENTS.md.");
6215
+ console.log(" Plan by default; --apply to write.");
6216
+ console.log(" Flags:");
6217
+ console.log(" --apply add the MCP server + AGENTS.md block");
6218
+ console.log(" --print-config print the config.toml table");
6219
+ console.log(" --print-guide print the AGENTS.md block");
6220
+ console.log(" cursor Wire NEAT into Cursor: add the MCP server to ~/.cursor/mcp.json");
6221
+ console.log(" and the graph-first guidance to ./.cursorrules. Plan by default.");
6222
+ console.log(" Flags:");
6223
+ console.log(" --apply write the MCP config + rules file (default: plan)");
6224
+ console.log(" devin Wire NEAT into Devin Desktop (Cascade): add the MCP server to");
6225
+ console.log(" ~/.codeium/windsurf/mcp_config.json and the graph-first guidance");
6226
+ console.log(" to ./.windsurfrules. Plan by default.");
6227
+ console.log(" Flags:");
6228
+ console.log(" --apply write the MCP config + rules file (default: plan)");
6229
+ console.log(" gemini Wire NEAT into the Gemini CLI: add the MCP server to");
6230
+ console.log(" ~/.gemini/settings.json and the graph-first guidance to");
6231
+ console.log(" ./GEMINI.md. Plan by default.");
6232
+ console.log(" Flags:");
6233
+ console.log(" --apply write the MCP config + rules file (default: plan)");
6234
+ console.log(" qwen Wire NEAT into Qwen Code: add the MCP server to");
6235
+ console.log(" ~/.qwen/settings.json and the graph-first guidance to");
6236
+ console.log(" ./QWEN.md. Plan by default.");
6237
+ console.log(" Flags:");
6238
+ console.log(" --apply write the MCP config + rules file (default: plan)");
6239
+ console.log(" amazonq Wire NEAT into the Amazon Q Developer CLI: add the MCP server to");
6240
+ console.log(" ~/.aws/amazonq/mcp.json. Plan by default.");
6241
+ console.log(" Flags:");
6242
+ console.log(" --apply write the MCP config (default: plan)");
6243
+ console.log(" roocode Wire NEAT into Roo Code: add the MCP server to the project's");
6244
+ console.log(" ./.roo/mcp.json. Plan by default.");
6245
+ console.log(" Flags:");
6246
+ console.log(" --apply write the MCP config (default: plan)");
6247
+ console.log(" zed Wire NEAT into Zed: add the MCP server under context_servers in");
6248
+ console.log(" ~/.config/zed/settings.json (comments preserved) and the");
6249
+ console.log(" graph-first guidance to ./.rules. Plan by default.");
6250
+ console.log(" Flags:");
6251
+ console.log(" --apply write the MCP config + rules file (default: plan)");
6252
+ console.log(" opencode Wire NEAT into OpenCode: add the MCP server under mcp in");
6253
+ console.log(" ~/.config/opencode/opencode.json and the graph-first guidance");
6254
+ console.log(" to ./AGENTS.md. Plan by default.");
6255
+ console.log(" Flags:");
6256
+ console.log(" --apply write the MCP config + rules file (default: plan)");
6257
+ console.log(" crush Wire NEAT into Crush: add the MCP server under mcp in");
6258
+ console.log(" ~/.config/crush/crush.json and the graph-first guidance");
6259
+ console.log(" to ./AGENTS.md. Plan by default.");
6260
+ console.log(" Flags:");
6261
+ console.log(" --apply write the MCP config + rules file (default: plan)");
5538
6262
  console.log(" deploy Detect the deploy substrate, generate NEAT_AUTH_TOKEN,");
5539
6263
  console.log(" emit a docker-compose / systemd / docker run artifact, and");
5540
6264
  console.log(" print the OTel env-vars block to paste into your platform.");
@@ -5548,7 +6272,8 @@ function usage2() {
5548
6272
  console.log(" --no-instrument skip the SDK install apply step");
5549
6273
  console.log(" --json emit the delta summary as JSON");
5550
6274
  console.log(" connector Configure OBSERVED connectors \u2014 pull (supabase, railway,");
5551
- console.log(" firebase, cloudflare) and push (vercel, via a trace Drain).");
6275
+ console.log(" firebase, cloudflare, neon, cloud-run) and push (vercel,");
6276
+ console.log(" via a trace Drain).");
5552
6277
  console.log(" Subcommands:");
5553
6278
  console.log(" add <provider> add a connector; validates the credential");
5554
6279
  console.log(" against the provider first (--skip-validate to skip)");
@@ -5780,7 +6505,7 @@ async function buildPatchSections(services, project) {
5780
6505
  }
5781
6506
  async function runInit(opts) {
5782
6507
  const written = [];
5783
- const stat = await fs9.stat(opts.scanPath).catch(() => null);
6508
+ const stat = await fs11.stat(opts.scanPath).catch(() => null);
5784
6509
  if (!stat || !stat.isDirectory()) {
5785
6510
  console.error(`neat init: ${opts.scanPath} is not a directory`);
5786
6511
  return { exitCode: 2, writtenFiles: written };
@@ -5789,13 +6514,13 @@ async function runInit(opts) {
5789
6514
  printDiscoveryReport(opts, services);
5790
6515
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
5791
6516
  const patch = renderPatch(sections);
5792
- const patchPath = path11.join(opts.scanPath, "neat.patch");
6517
+ const patchPath = path13.join(opts.scanPath, "neat.patch");
5793
6518
  if (opts.dryRun) {
5794
- await fs9.writeFile(patchPath, patch, "utf8");
6519
+ await fs11.writeFile(patchPath, patch, "utf8");
5795
6520
  written.push(patchPath);
5796
6521
  console.log(`dry-run: patch written to ${patchPath}`);
5797
- const gitignorePath = path11.join(opts.scanPath, ".gitignore");
5798
- const gitignoreExists = await fs9.stat(gitignorePath).then(() => true).catch(() => false);
6522
+ const gitignorePath = path13.join(opts.scanPath, ".gitignore");
6523
+ const gitignoreExists = await fs11.stat(gitignorePath).then(() => true).catch(() => false);
5799
6524
  const verb = gitignoreExists ? "append" : "create";
5800
6525
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
5801
6526
  console.log("rerun without --dry-run to register and snapshot.");
@@ -5806,9 +6531,9 @@ async function runInit(opts) {
5806
6531
  const graph = getGraph(graphKey);
5807
6532
  const projectPaths = pathsForProject(
5808
6533
  graphKey,
5809
- path11.join(opts.scanPath, "neat-out")
6534
+ path13.join(opts.scanPath, "neat-out")
5810
6535
  );
5811
- const errorsPath = path11.join(path11.dirname(opts.outPath), path11.basename(projectPaths.errorsPath));
6536
+ const errorsPath = path13.join(path13.dirname(opts.outPath), path13.basename(projectPaths.errorsPath));
5812
6537
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
5813
6538
  await saveGraphToDisk(graph, opts.outPath);
5814
6539
  written.push(opts.outPath);
@@ -5887,7 +6612,7 @@ async function runInit(opts) {
5887
6612
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
5888
6613
  }
5889
6614
  } else {
5890
- await fs9.writeFile(patchPath, patch, "utf8");
6615
+ await fs11.writeFile(patchPath, patch, "utf8");
5891
6616
  written.push(patchPath);
5892
6617
  }
5893
6618
  }
@@ -5927,9 +6652,9 @@ var CLAUDE_SKILL_CONFIG = {
5927
6652
  };
5928
6653
  function claudeConfigPath() {
5929
6654
  const override = process.env.NEAT_CLAUDE_CONFIG;
5930
- if (override && override.length > 0) return path11.resolve(override);
6655
+ if (override && override.length > 0) return path13.resolve(override);
5931
6656
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
5932
- return path11.join(home, ".claude.json");
6657
+ return path13.join(home, ".claude.json");
5933
6658
  }
5934
6659
  async function runSkill(opts) {
5935
6660
  const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -5941,7 +6666,7 @@ async function runSkill(opts) {
5941
6666
  const target = claudeConfigPath();
5942
6667
  let existing = {};
5943
6668
  try {
5944
- existing = JSON.parse(await fs9.readFile(target, "utf8"));
6669
+ existing = JSON.parse(await fs11.readFile(target, "utf8"));
5945
6670
  } catch (err) {
5946
6671
  if (err.code !== "ENOENT") {
5947
6672
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -5953,8 +6678,8 @@ async function runSkill(opts) {
5953
6678
  ...existing,
5954
6679
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
5955
6680
  };
5956
- await fs9.mkdir(path11.dirname(target), { recursive: true });
5957
- await fs9.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
6681
+ await fs11.mkdir(path13.dirname(target), { recursive: true });
6682
+ await fs11.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
5958
6683
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
5959
6684
  console.log("restart Claude Code to pick up the new MCP server.");
5960
6685
  console.log("");
@@ -5981,7 +6706,7 @@ async function main() {
5981
6706
  const argv = process.argv.slice(2);
5982
6707
  const cmd0 = argv[0];
5983
6708
  if (cmd0 === "-h" || cmd0 === "--help") {
5984
- usage2();
6709
+ usage4();
5985
6710
  process.exit(0);
5986
6711
  }
5987
6712
  if (cmd0 === "--version" || cmd0 === "-v" || cmd0 === "version") {
@@ -5998,6 +6723,27 @@ async function main() {
5998
6723
  if (code !== 0) process.exit(code);
5999
6724
  return;
6000
6725
  }
6726
+ if (cmd0 === "codex") {
6727
+ const code = await runCodexCommand(argv.slice(1));
6728
+ if (code !== 0) process.exit(code);
6729
+ return;
6730
+ }
6731
+ const EDITOR_VERBS = [
6732
+ "cursor",
6733
+ "devin",
6734
+ "gemini",
6735
+ "qwen",
6736
+ "amazonq",
6737
+ "roocode",
6738
+ "zed",
6739
+ "opencode",
6740
+ "crush"
6741
+ ];
6742
+ if (typeof cmd0 === "string" && EDITOR_VERBS.includes(cmd0)) {
6743
+ const code = await runEditorCommand(cmd0, argv.slice(1));
6744
+ if (code !== 0) process.exit(code);
6745
+ return;
6746
+ }
6001
6747
  const argvParsed = parseArgs(argv);
6002
6748
  if (argvParsed.positional.length === 0) {
6003
6749
  const orchestratorCode2 = await tryOrchestrator(process.cwd(), argvParsed);
@@ -6012,19 +6758,19 @@ async function main() {
6012
6758
  const target = positional[0];
6013
6759
  if (!target) {
6014
6760
  console.error("neat init: missing <path>");
6015
- usage2();
6761
+ usage4();
6016
6762
  process.exit(2);
6017
6763
  }
6018
6764
  if (apply4 && dryRun) {
6019
6765
  console.error("neat init: --apply and --dry-run are mutually exclusive");
6020
6766
  process.exit(2);
6021
6767
  }
6022
- const scanPath = path11.resolve(target);
6768
+ const scanPath = path13.resolve(target);
6023
6769
  const projectExplicit = parsed.project !== null;
6024
- const projectName = projectExplicit ? project : path11.basename(scanPath);
6770
+ const projectName = projectExplicit ? project : path13.basename(scanPath);
6025
6771
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
6026
- const fallback = pathsForProject(projectKey, path11.join(scanPath, "neat-out")).snapshotPath;
6027
- const outPath = path11.resolve(process.env.NEAT_OUT_PATH ?? fallback);
6772
+ const fallback = pathsForProject(projectKey, path13.join(scanPath, "neat-out")).snapshotPath;
6773
+ const outPath = path13.resolve(process.env.NEAT_OUT_PATH ?? fallback);
6028
6774
  const result = await runInit({
6029
6775
  scanPath,
6030
6776
  outPath,
@@ -6042,24 +6788,24 @@ async function main() {
6042
6788
  const target = positional[0];
6043
6789
  if (!target) {
6044
6790
  console.error("neat watch: missing <path>");
6045
- usage2();
6791
+ usage4();
6046
6792
  process.exit(2);
6047
6793
  }
6048
- const scanPath = path11.resolve(target);
6049
- const stat = await fs9.stat(scanPath).catch(() => null);
6794
+ const scanPath = path13.resolve(target);
6795
+ const stat = await fs11.stat(scanPath).catch(() => null);
6050
6796
  if (!stat || !stat.isDirectory()) {
6051
6797
  console.error(`neat watch: ${scanPath} is not a directory`);
6052
6798
  process.exit(2);
6053
6799
  }
6054
- const projectPaths = pathsForProject(project, path11.join(scanPath, "neat-out"));
6055
- const outPath = path11.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
6056
- const errorsPath = path11.resolve(
6057
- process.env.NEAT_ERRORS_PATH ?? path11.join(path11.dirname(outPath), path11.basename(projectPaths.errorsPath))
6800
+ const projectPaths = pathsForProject(project, path13.join(scanPath, "neat-out"));
6801
+ const outPath = path13.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
6802
+ const errorsPath = path13.resolve(
6803
+ process.env.NEAT_ERRORS_PATH ?? path13.join(path13.dirname(outPath), path13.basename(projectPaths.errorsPath))
6058
6804
  );
6059
- const staleEventsPath = path11.resolve(
6060
- process.env.NEAT_STALE_EVENTS_PATH ?? path11.join(path11.dirname(outPath), path11.basename(projectPaths.staleEventsPath))
6805
+ const staleEventsPath = path13.resolve(
6806
+ process.env.NEAT_STALE_EVENTS_PATH ?? path13.join(path13.dirname(outPath), path13.basename(projectPaths.staleEventsPath))
6061
6807
  );
6062
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path11.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
6808
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path13.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
6063
6809
  const handle = await startWatch(getGraph(project), {
6064
6810
  scanPath,
6065
6811
  outPath,
@@ -6068,7 +6814,7 @@ async function main() {
6068
6814
  project,
6069
6815
  // Resolve NEAT_HOME so a `neat watch` picks up connectors added to
6070
6816
  // ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
6071
- neatHome: process.env.NEAT_HOME ? path11.resolve(process.env.NEAT_HOME) : path11.join(os2.homedir(), ".neat"),
6817
+ neatHome: process.env.NEAT_HOME ? path13.resolve(process.env.NEAT_HOME) : path13.join(os4.homedir(), ".neat"),
6072
6818
  ...embeddingsCachePath ? { embeddingsCachePath } : {},
6073
6819
  host: process.env.HOST ?? "0.0.0.0",
6074
6820
  port: Number(process.env.PORT ?? 8080),
@@ -6104,7 +6850,7 @@ async function main() {
6104
6850
  const name = positional[0];
6105
6851
  if (!name) {
6106
6852
  console.error("neat pause: missing <name>");
6107
- usage2();
6853
+ usage4();
6108
6854
  process.exit(2);
6109
6855
  }
6110
6856
  const daemon = await findDaemonByProject(name);
@@ -6129,7 +6875,7 @@ async function main() {
6129
6875
  const name = positional[0];
6130
6876
  if (!name) {
6131
6877
  console.error("neat resume: missing <name>");
6132
- usage2();
6878
+ usage4();
6133
6879
  process.exit(2);
6134
6880
  }
6135
6881
  const daemon = await findDaemonByProject(name);
@@ -6159,7 +6905,7 @@ async function main() {
6159
6905
  const name = positional[0];
6160
6906
  if (!name) {
6161
6907
  console.error("neat uninstall: missing <name>");
6162
- usage2();
6908
+ usage4();
6163
6909
  process.exit(2);
6164
6910
  }
6165
6911
  const daemon = await findDaemonByProject(name);
@@ -6246,15 +6992,15 @@ async function main() {
6246
6992
  return;
6247
6993
  }
6248
6994
  console.error(`neat: unknown command "${cmd}"`);
6249
- usage2();
6995
+ usage4();
6250
6996
  process.exit(1);
6251
6997
  }
6252
6998
  async function tryOrchestrator(cmd, parsed) {
6253
- const scanPath = path11.resolve(cmd);
6254
- const stat = await fs9.stat(scanPath).catch(() => null);
6999
+ const scanPath = path13.resolve(cmd);
7000
+ const stat = await fs11.stat(scanPath).catch(() => null);
6255
7001
  if (!stat || !stat.isDirectory()) return null;
6256
7002
  const projectExplicit = parsed.project !== null;
6257
- const projectName = projectExplicit ? parsed.project : path11.basename(scanPath);
7003
+ const projectName = projectExplicit ? parsed.project : path13.basename(scanPath);
6258
7004
  const result = await runOrchestrator({
6259
7005
  scanPath,
6260
7006
  project: projectName,
@@ -6542,6 +7288,6 @@ export {
6542
7288
  runMonitorVerb,
6543
7289
  runQueryVerb,
6544
7290
  runSkill,
6545
- usage2 as usage
7291
+ usage4 as usage
6546
7292
  };
6547
7293
  //# sourceMappingURL=cli.js.map