@neat.is/core 0.9.5-dev.20260825 → 0.9.5
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/{chunk-TGCWMMF6.js → chunk-4SLKQNG7.js} +2 -2
- package/dist/{chunk-IVVF37OU.js → chunk-DGAI4VOE.js} +264 -19
- package/dist/chunk-DGAI4VOE.js.map +1 -0
- package/dist/cli.cjs +1873 -1466
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1534 -1382
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +280 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +280 -26
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +278 -24
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-IVVF37OU.js.map +0 -1
- /package/dist/{chunk-TGCWMMF6.js.map → chunk-4SLKQNG7.js.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
resolveHost,
|
|
7
7
|
resolveNeatVersion,
|
|
8
8
|
writeDaemonRecord
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-4SLKQNG7.js";
|
|
10
10
|
import {
|
|
11
11
|
buildSearchIndex
|
|
12
12
|
} from "./chunk-BC53SCT7.js";
|
|
@@ -75,7 +75,7 @@ import {
|
|
|
75
75
|
startStalenessLoop,
|
|
76
76
|
upsertConnectorEntry,
|
|
77
77
|
validateConnectorEntry
|
|
78
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-DGAI4VOE.js";
|
|
79
79
|
import {
|
|
80
80
|
startOtelGrpcReceiver
|
|
81
81
|
} from "./chunk-ERE47MCR.js";
|
|
@@ -89,9 +89,9 @@ import {
|
|
|
89
89
|
} from "./chunk-GDGUY4T6.js";
|
|
90
90
|
|
|
91
91
|
// src/cli.ts
|
|
92
|
-
import
|
|
92
|
+
import path17 from "path";
|
|
93
93
|
import os4 from "os";
|
|
94
|
-
import { promises as
|
|
94
|
+
import { promises as fs15 } from "fs";
|
|
95
95
|
|
|
96
96
|
// src/banner.ts
|
|
97
97
|
import path from "path";
|
|
@@ -4827,945 +4827,908 @@ async function runConnectorCommand(rawArgs, deps = {}) {
|
|
|
4827
4827
|
}
|
|
4828
4828
|
}
|
|
4829
4829
|
|
|
4830
|
-
// src/
|
|
4831
|
-
import path11 from "path";
|
|
4832
|
-
import os from "os";
|
|
4830
|
+
// src/doctor-cli.ts
|
|
4833
4831
|
import { promises as fs10 } from "fs";
|
|
4834
|
-
import
|
|
4835
|
-
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
var
|
|
4839
|
-
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
const candidates = [
|
|
4845
|
-
path11.resolve(here, "../../claude-skill", rel),
|
|
4846
|
-
path11.resolve(here, "../../../claude-skill", rel),
|
|
4847
|
-
path11.resolve(here, "../claude-skill", rel)
|
|
4848
|
-
];
|
|
4849
|
-
for (const candidate of candidates) {
|
|
4850
|
-
try {
|
|
4851
|
-
return await fs10.readFile(candidate, "utf8");
|
|
4852
|
-
} catch {
|
|
4853
|
-
}
|
|
4832
|
+
import path11 from "path";
|
|
4833
|
+
|
|
4834
|
+
// src/cli-client.ts
|
|
4835
|
+
import { Provenance as Provenance2 } from "@neat.is/types";
|
|
4836
|
+
var HttpError = class extends Error {
|
|
4837
|
+
constructor(status, message, responseBody = "") {
|
|
4838
|
+
super(message);
|
|
4839
|
+
this.status = status;
|
|
4840
|
+
this.responseBody = responseBody;
|
|
4841
|
+
this.name = "HttpError";
|
|
4854
4842
|
}
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
}
|
|
4864
|
-
function
|
|
4865
|
-
const
|
|
4866
|
-
|
|
4867
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
|
|
4868
|
-
return path11.join(home, ".claude", "settings.json");
|
|
4843
|
+
status;
|
|
4844
|
+
responseBody;
|
|
4845
|
+
};
|
|
4846
|
+
var TransportError = class extends Error {
|
|
4847
|
+
constructor(message) {
|
|
4848
|
+
super(message);
|
|
4849
|
+
this.name = "TransportError";
|
|
4850
|
+
}
|
|
4851
|
+
};
|
|
4852
|
+
function resolveAuthToken(env = process.env) {
|
|
4853
|
+
const t = env.NEAT_AUTH_TOKEN;
|
|
4854
|
+
return t && t.length > 0 ? t : void 0;
|
|
4869
4855
|
}
|
|
4870
|
-
function
|
|
4871
|
-
|
|
4856
|
+
function createHttpClient(baseUrl, bearerToken) {
|
|
4857
|
+
const root = baseUrl.replace(/\/$/, "");
|
|
4858
|
+
const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
|
|
4859
|
+
return {
|
|
4860
|
+
async get(path18) {
|
|
4861
|
+
let res;
|
|
4862
|
+
try {
|
|
4863
|
+
res = await fetch(`${root}${path18}`, {
|
|
4864
|
+
headers: { ...authHeader }
|
|
4865
|
+
});
|
|
4866
|
+
} catch (err) {
|
|
4867
|
+
throw new TransportError(
|
|
4868
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
4869
|
+
);
|
|
4870
|
+
}
|
|
4871
|
+
if (!res.ok) {
|
|
4872
|
+
const body = await res.text().catch(() => "");
|
|
4873
|
+
throw new HttpError(
|
|
4874
|
+
res.status,
|
|
4875
|
+
`${res.status} ${res.statusText} on GET ${path18}: ${body}`,
|
|
4876
|
+
body
|
|
4877
|
+
);
|
|
4878
|
+
}
|
|
4879
|
+
return await res.json();
|
|
4880
|
+
},
|
|
4881
|
+
async post(path18, body) {
|
|
4882
|
+
let res;
|
|
4883
|
+
try {
|
|
4884
|
+
res = await fetch(`${root}${path18}`, {
|
|
4885
|
+
method: "POST",
|
|
4886
|
+
headers: { "content-type": "application/json", ...authHeader },
|
|
4887
|
+
body: JSON.stringify(body)
|
|
4888
|
+
});
|
|
4889
|
+
} catch (err) {
|
|
4890
|
+
throw new TransportError(
|
|
4891
|
+
`cannot reach neat-core at ${root}: ${err.message}`
|
|
4892
|
+
);
|
|
4893
|
+
}
|
|
4894
|
+
if (!res.ok) {
|
|
4895
|
+
const text = await res.text().catch(() => "");
|
|
4896
|
+
throw new HttpError(
|
|
4897
|
+
res.status,
|
|
4898
|
+
`${res.status} ${res.statusText} on POST ${path18}: ${text}`,
|
|
4899
|
+
text
|
|
4900
|
+
);
|
|
4901
|
+
}
|
|
4902
|
+
return await res.json();
|
|
4903
|
+
}
|
|
4904
|
+
};
|
|
4872
4905
|
}
|
|
4873
|
-
function
|
|
4874
|
-
|
|
4906
|
+
function projectPath(project, suffix) {
|
|
4907
|
+
if (!project) return suffix;
|
|
4908
|
+
return `/projects/${encodeURIComponent(project)}${suffix}`;
|
|
4875
4909
|
}
|
|
4876
|
-
function
|
|
4877
|
-
|
|
4878
|
-
|
|
4910
|
+
async function runRootCause(client, input) {
|
|
4911
|
+
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
4912
|
+
const path18 = projectPath(
|
|
4913
|
+
input.project,
|
|
4914
|
+
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
4879
4915
|
);
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
}
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
return {
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
}
|
|
4896
|
-
if (opts.printSettings) {
|
|
4897
|
-
const block = {
|
|
4898
|
-
hooks: { PreToolUse: [neatHookEntry(hookCommand(installedHookPath()))] }
|
|
4916
|
+
try {
|
|
4917
|
+
const result = await client.get(path18);
|
|
4918
|
+
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
4919
|
+
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
4920
|
+
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
4921
|
+
const blockLines = [
|
|
4922
|
+
`Traversal path: ${arrowPath}`,
|
|
4923
|
+
`Edge provenances: ${provenances}`
|
|
4924
|
+
];
|
|
4925
|
+
if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
|
|
4926
|
+
return {
|
|
4927
|
+
summary,
|
|
4928
|
+
block: blockLines.join("\n"),
|
|
4929
|
+
confidence: result.confidence,
|
|
4930
|
+
provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
|
|
4899
4931
|
};
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
const guide = await readSkillAsset(GUIDE_FILENAME);
|
|
4906
|
-
const scriptPath = installedHookPath();
|
|
4907
|
-
await fs10.mkdir(path11.dirname(scriptPath), { recursive: true });
|
|
4908
|
-
await fs10.writeFile(scriptPath, hookScript, { mode: 493 });
|
|
4909
|
-
const guidePath = path11.join(neatHome(), GUIDE_INSTALL_NAME);
|
|
4910
|
-
await fs10.writeFile(guidePath, guide, "utf8");
|
|
4911
|
-
const settingsFile = claudeSettingsPath();
|
|
4912
|
-
let settings = {};
|
|
4913
|
-
try {
|
|
4914
|
-
settings = JSON.parse(await fs10.readFile(settingsFile, "utf8"));
|
|
4915
|
-
} catch (err) {
|
|
4916
|
-
if (err.code !== "ENOENT") {
|
|
4917
|
-
console.error(
|
|
4918
|
-
`neat hooks: failed to read ${settingsFile} \u2014 ${err.message}`
|
|
4919
|
-
);
|
|
4920
|
-
return { exitCode: 1 };
|
|
4921
|
-
}
|
|
4932
|
+
} catch (err) {
|
|
4933
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
4934
|
+
return {
|
|
4935
|
+
summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
|
|
4936
|
+
};
|
|
4922
4937
|
}
|
|
4923
|
-
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4938
|
+
throw err;
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
async function runBlastRadius(client, input) {
|
|
4942
|
+
const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
|
|
4943
|
+
const path18 = projectPath(
|
|
4944
|
+
input.project,
|
|
4945
|
+
`/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
|
|
4946
|
+
);
|
|
4947
|
+
try {
|
|
4948
|
+
const result = await client.get(path18);
|
|
4949
|
+
if (result.totalAffected === 0) {
|
|
4950
|
+
return {
|
|
4951
|
+
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
4952
|
+
};
|
|
4931
4953
|
}
|
|
4932
|
-
const
|
|
4933
|
-
|
|
4934
|
-
|
|
4954
|
+
const sorted = [...result.affectedNodes].sort(
|
|
4955
|
+
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
4956
|
+
);
|
|
4957
|
+
const blockLines = sorted.map(formatBlastEntry);
|
|
4958
|
+
const minConfidence = sorted.reduce(
|
|
4959
|
+
(m, n) => Math.min(m, n.confidence),
|
|
4960
|
+
Number.POSITIVE_INFINITY
|
|
4961
|
+
);
|
|
4962
|
+
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
4963
|
+
return {
|
|
4964
|
+
summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
|
|
4965
|
+
block: blockLines.join("\n"),
|
|
4966
|
+
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
4967
|
+
provenance: provenances.length ? provenances : void 0
|
|
4935
4968
|
};
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
if (opts.gate) {
|
|
4940
|
-
await fs10.mkdir(path11.dirname(flag), { recursive: true });
|
|
4941
|
-
await fs10.writeFile(flag, "1\n", "utf8");
|
|
4942
|
-
} else {
|
|
4943
|
-
await fs10.rm(flag, { force: true });
|
|
4944
|
-
}
|
|
4945
|
-
const mode = opts.gate ? "GATE (deny search until you ask the graph)" : "nudge (search still runs)";
|
|
4946
|
-
console.log(`neat hooks: installed the search hook in ${opts.gate ? "gate" : "nudge"} mode`);
|
|
4947
|
-
console.log(` script: ${scriptPath}`);
|
|
4948
|
-
console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
|
|
4949
|
-
console.log(` guidance: ${guidePath}`);
|
|
4950
|
-
console.log(` mode: ${mode}`);
|
|
4951
|
-
console.log("");
|
|
4952
|
-
if (opts.gate) {
|
|
4953
|
-
console.log("restart Claude Code to load the hook. A Grep/Glob or Bash grep is now DENIED");
|
|
4954
|
-
console.log('until you run `neat ask "<question>"` (or the ask MCP tool) once this session;');
|
|
4955
|
-
console.log("after that, search is allowed as a fallback. Set NEAT_SEARCH_GATE=0 to fall");
|
|
4956
|
-
console.log("back to nudge-only without re-running.");
|
|
4957
|
-
} else {
|
|
4958
|
-
console.log("restart Claude Code to load the hook. On a Grep/Glob or a Bash grep,");
|
|
4959
|
-
console.log("your agent will now be nudged to query NEAT first (the search still runs).");
|
|
4960
|
-
console.log("Re-run with --gate to hard-force the graph-first orientation.");
|
|
4969
|
+
} catch (err) {
|
|
4970
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
4971
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
4961
4972
|
}
|
|
4962
|
-
|
|
4963
|
-
console.log("The hook is Claude-Code-specific. For agents on other harnesses, paste");
|
|
4964
|
-
console.log(`the guidance above into your project instructions (CLAUDE.md / AGENTS.md).`);
|
|
4965
|
-
return { exitCode: 0 };
|
|
4973
|
+
throw err;
|
|
4966
4974
|
}
|
|
4967
|
-
usage();
|
|
4968
|
-
return { exitCode: 0 };
|
|
4969
4975
|
}
|
|
4970
|
-
function
|
|
4971
|
-
|
|
4972
|
-
|
|
4973
|
-
console.log(" --apply install the Claude Code search hook and write the");
|
|
4974
|
-
console.log(" graph-first guidance to ~/.neat/, merging into");
|
|
4975
|
-
console.log(" ~/.claude/settings.json without touching your other hooks");
|
|
4976
|
-
console.log(" --gate with --apply, enable hard-gate mode: DENY Grep/Glob/grep-Bash");
|
|
4977
|
-
console.log(" until `neat ask` has run this session (default is nudge-only).");
|
|
4978
|
-
console.log(" Toggle off at run time with NEAT_SEARCH_GATE=0.");
|
|
4979
|
-
console.log(" --print-hook print the hook script to stdout");
|
|
4980
|
-
console.log(" --print-guide print the agent-agnostic graph-first guidance to stdout");
|
|
4981
|
-
console.log(" --print-settings print the settings.json PreToolUse block --apply would add");
|
|
4982
|
-
console.log("");
|
|
4983
|
-
console.log("By default the hook is a gentle, non-blocking nudge \u2014 searches still run.");
|
|
4984
|
-
console.log("--gate turns it into a hard forcing mechanism. It is Claude-Code-specific;");
|
|
4985
|
-
console.log("other harnesses get the same steer from the graph-first guidance.");
|
|
4976
|
+
function formatBlastEntry(n) {
|
|
4977
|
+
const tag = n.edgeProvenance === Provenance2.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
4978
|
+
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
4986
4979
|
}
|
|
4987
|
-
async function
|
|
4988
|
-
const
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
gate: false
|
|
4994
|
-
};
|
|
4995
|
-
for (const arg of args) {
|
|
4996
|
-
switch (arg) {
|
|
4997
|
-
case "--apply":
|
|
4998
|
-
opts.apply = true;
|
|
4999
|
-
break;
|
|
5000
|
-
case "--gate":
|
|
5001
|
-
opts.gate = true;
|
|
5002
|
-
break;
|
|
5003
|
-
case "--print-hook":
|
|
5004
|
-
opts.printHook = true;
|
|
5005
|
-
break;
|
|
5006
|
-
case "--print-guide":
|
|
5007
|
-
opts.printGuide = true;
|
|
5008
|
-
break;
|
|
5009
|
-
case "--print-settings":
|
|
5010
|
-
opts.printSettings = true;
|
|
5011
|
-
break;
|
|
5012
|
-
case "-h":
|
|
5013
|
-
case "--help":
|
|
5014
|
-
usage();
|
|
5015
|
-
return 0;
|
|
5016
|
-
default:
|
|
5017
|
-
console.error(`neat hooks: unknown flag "${arg}"`);
|
|
5018
|
-
usage();
|
|
5019
|
-
return 2;
|
|
5020
|
-
}
|
|
5021
|
-
}
|
|
4980
|
+
async function runDependencies(client, input) {
|
|
4981
|
+
const depth = input.depth ?? 3;
|
|
4982
|
+
const path18 = projectPath(
|
|
4983
|
+
input.project,
|
|
4984
|
+
`/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
|
|
4985
|
+
);
|
|
5022
4986
|
try {
|
|
5023
|
-
const
|
|
5024
|
-
|
|
4987
|
+
const result = await client.get(path18);
|
|
4988
|
+
if (result.total === 0) {
|
|
4989
|
+
return {
|
|
4990
|
+
summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
const byDistance = /* @__PURE__ */ new Map();
|
|
4994
|
+
for (const dep of result.dependencies) {
|
|
4995
|
+
const ring = byDistance.get(dep.distance) ?? [];
|
|
4996
|
+
ring.push(dep);
|
|
4997
|
+
byDistance.set(dep.distance, ring);
|
|
4998
|
+
}
|
|
4999
|
+
const blockLines = [];
|
|
5000
|
+
for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
|
|
5001
|
+
const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
|
|
5002
|
+
blockLines.push(`${label}:`);
|
|
5003
|
+
for (const dep of byDistance.get(distance)) {
|
|
5004
|
+
blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
|
|
5005
|
+
}
|
|
5006
|
+
}
|
|
5007
|
+
const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
|
|
5008
|
+
const directCount = byDistance.get(1)?.length ?? 0;
|
|
5009
|
+
const summary = depth === 1 ? `${input.nodeId} has ${directCount} direct dependenc${directCount === 1 ? "y" : "ies"}.` : `${input.nodeId} has ${result.total} dependenc${result.total === 1 ? "y" : "ies"} reachable to depth ${depth} (${directCount} direct).`;
|
|
5010
|
+
return { summary, block: blockLines.join("\n"), provenance: provenances };
|
|
5025
5011
|
} catch (err) {
|
|
5026
|
-
|
|
5027
|
-
|
|
5028
|
-
}
|
|
5029
|
-
}
|
|
5030
|
-
|
|
5031
|
-
// src/claude-cli.ts
|
|
5032
|
-
import path12 from "path";
|
|
5033
|
-
import { promises as fs11 } from "fs";
|
|
5034
|
-
var NEAT_SECTION_HEADING = "## neat";
|
|
5035
|
-
var NEAT_DIRECTIVE_BODY = `This project has NEAT wired in: a live, fused semantic graph of the system \u2014
|
|
5036
|
-
code and runtime behaviour (OpenTelemetry) in one model, every fact tagged with
|
|
5037
|
-
its provenance (EXTRACTED / OBSERVED / INFERRED / STALE) and a confidence.
|
|
5038
|
-
|
|
5039
|
-
For ANY question about this system's behaviour, dependencies, failures, root
|
|
5040
|
-
cause, or blast radius, call \`neat ask\` FIRST \u2014 before Read, Grep, Glob, or Bash:
|
|
5041
|
-
|
|
5042
|
-
neat ask "why is checkout failing?"
|
|
5043
|
-
neat ask "what breaks if I change the orders table?"
|
|
5044
|
-
neat ask "what does the payments service depend on at runtime?"
|
|
5045
|
-
|
|
5046
|
-
\`ask\` resolves your question to graph nodes and routes it to the right traversal
|
|
5047
|
-
\u2014 root cause, dependencies, observed runtime calls, incidents, divergences,
|
|
5048
|
-
blast radius \u2014 and returns one compact, provenance-tagged answer. The same door
|
|
5049
|
-
is the \`ask\` MCP tool (\`mcp__neat__ask\`) when NEAT's MCP server is wired in.
|
|
5050
|
-
|
|
5051
|
-
The graph is live and fused: it is faster and more accurate than scanning files,
|
|
5052
|
-
and it can tell you what the system actually does at runtime, not only what the
|
|
5053
|
-
source declares. Fall back to Read/Grep only when the graph does not have what
|
|
5054
|
-
you need \u2014 comments, string literals, config minutiae. Ask the graph first,
|
|
5055
|
-
then scan.
|
|
5056
|
-
|
|
5057
|
-
If \`neat ask\` errors, the daemon may not be running (\`neat list\`) \u2014 start it
|
|
5058
|
-
with \`neat <path>\`, then re-ask.`;
|
|
5059
|
-
function neatSection() {
|
|
5060
|
-
return `${NEAT_SECTION_HEADING}
|
|
5061
|
-
|
|
5062
|
-
${NEAT_DIRECTIVE_BODY}
|
|
5063
|
-
`;
|
|
5064
|
-
}
|
|
5065
|
-
function claudeMdPath() {
|
|
5066
|
-
const override = process.env.NEAT_CLAUDE_MD;
|
|
5067
|
-
if (override && override.length > 0) return path12.resolve(override);
|
|
5068
|
-
return path12.join(process.cwd(), "CLAUDE.md");
|
|
5069
|
-
}
|
|
5070
|
-
function splitAroundSection(raw) {
|
|
5071
|
-
const lines = raw.split("\n");
|
|
5072
|
-
const startIdx = lines.findIndex((l) => l.replace(/\s+$/, "") === NEAT_SECTION_HEADING);
|
|
5073
|
-
if (startIdx === -1) {
|
|
5074
|
-
return { before: raw.replace(/\n*$/, ""), after: "", found: false };
|
|
5075
|
-
}
|
|
5076
|
-
let endIdx = lines.length;
|
|
5077
|
-
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
5078
|
-
if (/^#{1,2}\s+/.test(lines[i] ?? "")) {
|
|
5079
|
-
endIdx = i;
|
|
5080
|
-
break;
|
|
5012
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5013
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5081
5014
|
}
|
|
5015
|
+
throw err;
|
|
5082
5016
|
}
|
|
5083
|
-
const before = lines.slice(0, startIdx).join("\n").replace(/\n*$/, "");
|
|
5084
|
-
const after = lines.slice(endIdx).join("\n").replace(/^\n*/, "");
|
|
5085
|
-
return { before, after, found: true };
|
|
5086
5017
|
}
|
|
5087
|
-
function
|
|
5088
|
-
const
|
|
5089
|
-
|
|
5090
|
-
parts.push(neatSection().replace(/\n+$/, ""));
|
|
5091
|
-
if (after.length > 0) parts.push(after);
|
|
5092
|
-
return parts.join("\n\n").replace(/\n*$/, "") + "\n";
|
|
5018
|
+
function observedDepLine(nodeId, e) {
|
|
5019
|
+
const via = e.source !== nodeId ? ` (via ${e.source})` : "";
|
|
5020
|
+
return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
|
|
5093
5021
|
}
|
|
5094
|
-
async function
|
|
5022
|
+
async function runObservedDependencies(client, input) {
|
|
5095
5023
|
try {
|
|
5096
|
-
|
|
5024
|
+
const result = await client.get(
|
|
5025
|
+
projectPath(
|
|
5026
|
+
input.project,
|
|
5027
|
+
`/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
|
|
5028
|
+
)
|
|
5029
|
+
);
|
|
5030
|
+
if (result.dependencies.length === 0) {
|
|
5031
|
+
if (result.observed) {
|
|
5032
|
+
return {
|
|
5033
|
+
summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
|
|
5034
|
+
provenance: Provenance2.OBSERVED
|
|
5035
|
+
};
|
|
5036
|
+
}
|
|
5037
|
+
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
5038
|
+
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
5039
|
+
}
|
|
5040
|
+
const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
|
|
5041
|
+
return {
|
|
5042
|
+
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
5043
|
+
block: blockLines.join("\n"),
|
|
5044
|
+
provenance: Provenance2.OBSERVED
|
|
5045
|
+
};
|
|
5097
5046
|
} catch (err) {
|
|
5098
|
-
if (err.
|
|
5047
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5048
|
+
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5049
|
+
}
|
|
5099
5050
|
throw err;
|
|
5100
5051
|
}
|
|
5101
5052
|
}
|
|
5102
|
-
|
|
5103
|
-
const
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
console.log("session (or reload CLAUDE.md) to pick it up.");
|
|
5113
|
-
return { exitCode: 0 };
|
|
5114
|
-
}
|
|
5115
|
-
async function runUninstall() {
|
|
5116
|
-
const file = claudeMdPath();
|
|
5117
|
-
const raw = await readIfExists(file);
|
|
5118
|
-
if (raw === null) {
|
|
5119
|
-
console.log(`neat claude: no CLAUDE.md at ${file} \u2014 nothing to remove.`);
|
|
5120
|
-
return { exitCode: 0 };
|
|
5121
|
-
}
|
|
5122
|
-
const { before, after, found } = splitAroundSection(raw);
|
|
5123
|
-
if (!found) {
|
|
5124
|
-
console.log(`neat claude: no \`${NEAT_SECTION_HEADING}\` section in ${file} \u2014 nothing to remove.`);
|
|
5125
|
-
return { exitCode: 0 };
|
|
5053
|
+
function edgeMeta(e) {
|
|
5054
|
+
const bits = [];
|
|
5055
|
+
if (e.signal) {
|
|
5056
|
+
bits.push(`spans=${e.signal.spanCount}`);
|
|
5057
|
+
if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
|
|
5058
|
+
if (e.signal.lastObservedAgeMs !== void 0) {
|
|
5059
|
+
bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
|
|
5060
|
+
}
|
|
5061
|
+
} else if (e.callCount !== void 0) {
|
|
5062
|
+
bits.push(`callCount=${e.callCount}`);
|
|
5126
5063
|
}
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
console.log(`neat claude: removed the \`${NEAT_SECTION_HEADING}\` section from ${file}.`);
|
|
5131
|
-
return { exitCode: 0 };
|
|
5064
|
+
if (e.lastObserved) bits.push(`lastObserved=${e.lastObserved}`);
|
|
5065
|
+
if (e.confidence !== void 0) bits.push(`confidence=${e.confidence}`);
|
|
5066
|
+
return bits.length ? ` [${bits.join(", ")}]` : "";
|
|
5132
5067
|
}
|
|
5133
|
-
function
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
console.log("Target file overridable via NEAT_CLAUDE_MD.");
|
|
5068
|
+
function formatDuration(ms) {
|
|
5069
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
5070
|
+
const s = Math.round(ms / 1e3);
|
|
5071
|
+
if (s < 60) return `${s}s`;
|
|
5072
|
+
const m = Math.round(s / 60);
|
|
5073
|
+
if (m < 60) return `${m}m`;
|
|
5074
|
+
const h = Math.round(m / 60);
|
|
5075
|
+
if (h < 48) return `${h}h`;
|
|
5076
|
+
return `${Math.round(h / 24)}d`;
|
|
5143
5077
|
}
|
|
5144
|
-
async function
|
|
5145
|
-
const
|
|
5146
|
-
if (sub === "-h" || sub === "--help" || sub === void 0) {
|
|
5147
|
-
usage2();
|
|
5148
|
-
return sub === void 0 ? 2 : 0;
|
|
5149
|
-
}
|
|
5078
|
+
async function runIncidents(client, input) {
|
|
5079
|
+
const path18 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
|
|
5150
5080
|
try {
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5155
|
-
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
|
|
5159
|
-
|
|
5160
|
-
|
|
5161
|
-
|
|
5162
|
-
|
|
5081
|
+
const body = await client.get(path18);
|
|
5082
|
+
const events = body.events;
|
|
5083
|
+
if (events.length === 0) {
|
|
5084
|
+
return {
|
|
5085
|
+
summary: input.nodeId ? `No incidents recorded against ${input.nodeId}.` : "No incidents recorded."
|
|
5086
|
+
};
|
|
5087
|
+
}
|
|
5088
|
+
const ordered = [...events].reverse().slice(0, input.limit ?? 20);
|
|
5089
|
+
const blockLines = [];
|
|
5090
|
+
for (const ev of ordered) {
|
|
5091
|
+
blockLines.push(` ${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`);
|
|
5092
|
+
blockLines.push(` trace=${ev.traceId} span=${ev.spanId}`);
|
|
5163
5093
|
}
|
|
5094
|
+
const target = input.nodeId ?? "the project";
|
|
5095
|
+
return {
|
|
5096
|
+
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
5097
|
+
block: blockLines.join("\n"),
|
|
5098
|
+
provenance: Provenance2.OBSERVED
|
|
5099
|
+
};
|
|
5164
5100
|
} catch (err) {
|
|
5165
|
-
|
|
5166
|
-
|
|
5101
|
+
if (err instanceof HttpError && err.status === 404) {
|
|
5102
|
+
return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
|
|
5103
|
+
}
|
|
5104
|
+
throw err;
|
|
5167
5105
|
}
|
|
5168
5106
|
}
|
|
5169
|
-
|
|
5170
|
-
|
|
5171
|
-
|
|
5172
|
-
|
|
5173
|
-
|
|
5174
|
-
|
|
5175
|
-
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
5176
|
-
var CODEX_MCP_SERVER = {
|
|
5177
|
-
command: "npx",
|
|
5178
|
-
args: ["-y", "@neat.is/mcp"],
|
|
5179
|
-
env: { NEAT_CORE_URL: "http://localhost:8080" }
|
|
5180
|
-
};
|
|
5181
|
-
var CODEX_NEAT_BLOCK = [
|
|
5182
|
-
"[mcp_servers.neat]",
|
|
5183
|
-
'command = "npx"',
|
|
5184
|
-
'args = ["-y", "@neat.is/mcp"]',
|
|
5185
|
-
'env = { NEAT_CORE_URL = "http://localhost:8080" }'
|
|
5186
|
-
].join("\n");
|
|
5187
|
-
var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
|
|
5188
|
-
var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
|
|
5189
|
-
function codexConfigPath() {
|
|
5190
|
-
const override = process.env.NEAT_CODEX_CONFIG;
|
|
5191
|
-
if (override && override.length > 0) return path13.resolve(override);
|
|
5192
|
-
const home = process.env.HOME ?? process.env.USERPROFILE ?? os2.homedir();
|
|
5193
|
-
return path13.join(home, ".codex", "config.toml");
|
|
5194
|
-
}
|
|
5195
|
-
function agentsFilePath() {
|
|
5196
|
-
const override = process.env.NEAT_CODEX_AGENTS;
|
|
5197
|
-
if (override && override.length > 0) return path13.resolve(override);
|
|
5198
|
-
return path13.join(process.cwd(), "AGENTS.md");
|
|
5199
|
-
}
|
|
5200
|
-
function isTableHeader(line) {
|
|
5201
|
-
return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
|
|
5202
|
-
}
|
|
5203
|
-
function tableName(line) {
|
|
5204
|
-
return line.trim().replace(/^\[\[?/, "").replace(/\]\]?$/, "").trim();
|
|
5205
|
-
}
|
|
5206
|
-
function isNeatHeader(line) {
|
|
5207
|
-
return isTableHeader(line) && tableName(line) === "mcp_servers.neat";
|
|
5208
|
-
}
|
|
5209
|
-
function isNeatChildHeader(line) {
|
|
5210
|
-
if (!isTableHeader(line)) return false;
|
|
5211
|
-
const name = tableName(line);
|
|
5212
|
-
return name === "mcp_servers.neat" || name.startsWith("mcp_servers.neat.");
|
|
5213
|
-
}
|
|
5214
|
-
function upsertCodexConfig(raw) {
|
|
5215
|
-
const trimmed = raw.trim();
|
|
5216
|
-
const parsed = trimmed.length > 0 ? parseToml(raw) : {};
|
|
5217
|
-
const existingServers = parsed.mcp_servers ?? {};
|
|
5218
|
-
const spliced = spliceNeatBlock(raw);
|
|
5219
|
-
let text;
|
|
5220
|
-
if (verifyPreserved(raw, spliced, parsed)) {
|
|
5221
|
-
text = spliced;
|
|
5222
|
-
} else {
|
|
5223
|
-
const merged = {
|
|
5224
|
-
...parsed,
|
|
5225
|
-
mcp_servers: { ...existingServers, neat: CODEX_MCP_SERVER }
|
|
5226
|
-
};
|
|
5227
|
-
text = stringifyToml(merged);
|
|
5228
|
-
if (!text.endsWith("\n")) text += "\n";
|
|
5229
|
-
}
|
|
5230
|
-
return { text, changed: text !== raw };
|
|
5231
|
-
}
|
|
5232
|
-
function spliceNeatBlock(raw) {
|
|
5233
|
-
const block = CODEX_NEAT_BLOCK;
|
|
5234
|
-
const lines = raw.length > 0 ? raw.split("\n") : [];
|
|
5235
|
-
const start = lines.findIndex(isNeatHeader);
|
|
5236
|
-
if (start === -1) {
|
|
5237
|
-
const base = raw.replace(/\n+$/, "");
|
|
5238
|
-
return base.length > 0 ? `${base}
|
|
5239
|
-
|
|
5240
|
-
${block}
|
|
5241
|
-
` : `${block}
|
|
5242
|
-
`;
|
|
5107
|
+
async function runSearch(client, input) {
|
|
5108
|
+
const result = await client.get(
|
|
5109
|
+
projectPath(input.project, `/search?q=${encodeURIComponent(input.query)}`)
|
|
5110
|
+
);
|
|
5111
|
+
if (result.matches.length === 0) {
|
|
5112
|
+
return { summary: `No matches for "${input.query}".` };
|
|
5243
5113
|
}
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
|
|
5247
|
-
|
|
5248
|
-
|
|
5249
|
-
|
|
5250
|
-
|
|
5251
|
-
|
|
5114
|
+
const provider = result.provider ?? "substring";
|
|
5115
|
+
const blockLines = [];
|
|
5116
|
+
let topScore;
|
|
5117
|
+
for (const n of result.matches) {
|
|
5118
|
+
const score = provider !== "substring" && typeof n.score === "number" ? n.score : void 0;
|
|
5119
|
+
const scoreBit = score !== void 0 ? ` [score=${score.toFixed(2)}]` : "";
|
|
5120
|
+
if (score !== void 0 && (topScore === void 0 || score > topScore)) topScore = score;
|
|
5121
|
+
blockLines.push(
|
|
5122
|
+
` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
|
|
5123
|
+
);
|
|
5252
5124
|
}
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5257
|
-
|
|
5258
|
-
`;
|
|
5259
|
-
text += `${block}
|
|
5260
|
-
`;
|
|
5261
|
-
if (after.length > 0) text += `
|
|
5262
|
-
${after}`;
|
|
5263
|
-
return `${text.replace(/\n*$/, "")}
|
|
5264
|
-
`;
|
|
5125
|
+
return {
|
|
5126
|
+
summary: `Found ${result.matches.length} match${result.matches.length === 1 ? "" : "es"} for "${input.query}" via ${provider} provider.`,
|
|
5127
|
+
block: blockLines.join("\n"),
|
|
5128
|
+
confidence: topScore
|
|
5129
|
+
};
|
|
5265
5130
|
}
|
|
5266
|
-
function
|
|
5267
|
-
|
|
5268
|
-
|
|
5269
|
-
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
const
|
|
5274
|
-
const
|
|
5275
|
-
if (
|
|
5276
|
-
|
|
5277
|
-
|
|
5278
|
-
|
|
5131
|
+
async function runDiff(client, input) {
|
|
5132
|
+
const result = await client.get(
|
|
5133
|
+
projectPath(
|
|
5134
|
+
input.project,
|
|
5135
|
+
`/graph/diff?against=${encodeURIComponent(input.againstSnapshot)}`
|
|
5136
|
+
)
|
|
5137
|
+
);
|
|
5138
|
+
const total = result.added.nodes.length + result.added.edges.length + result.removed.nodes.length + result.removed.edges.length + result.changed.nodes.length + result.changed.edges.length;
|
|
5139
|
+
const baseLabel = result.base.exportedAt ?? "unknown";
|
|
5140
|
+
if (total === 0) {
|
|
5141
|
+
return {
|
|
5142
|
+
summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
|
|
5143
|
+
};
|
|
5279
5144
|
}
|
|
5280
|
-
|
|
5281
|
-
|
|
5145
|
+
const blockLines = [
|
|
5146
|
+
` base exportedAt: ${baseLabel}`,
|
|
5147
|
+
` current exportedAt: ${result.current.exportedAt}`,
|
|
5148
|
+
""
|
|
5149
|
+
];
|
|
5150
|
+
if (result.added.nodes.length || result.added.edges.length) {
|
|
5151
|
+
blockLines.push("Added:");
|
|
5152
|
+
for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
|
|
5153
|
+
for (const e of result.added.edges)
|
|
5154
|
+
blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
5155
|
+
blockLines.push("");
|
|
5282
5156
|
}
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5157
|
+
if (result.removed.nodes.length || result.removed.edges.length) {
|
|
5158
|
+
blockLines.push("Removed:");
|
|
5159
|
+
for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
|
|
5160
|
+
for (const e of result.removed.edges)
|
|
5161
|
+
blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
5162
|
+
blockLines.push("");
|
|
5286
5163
|
}
|
|
5287
|
-
|
|
5288
|
-
|
|
5164
|
+
if (result.changed.nodes.length || result.changed.edges.length) {
|
|
5165
|
+
blockLines.push("Changed:");
|
|
5166
|
+
for (const c of result.changed.nodes) {
|
|
5167
|
+
blockLines.push(` ~ node ${c.id} \u2014 ${summariseAttrDiff(c.before, c.after)}`);
|
|
5168
|
+
}
|
|
5169
|
+
for (const c of result.changed.edges) {
|
|
5170
|
+
const provBit = c.before.provenance !== c.after.provenance ? `provenance ${c.before.provenance} \u2192 ${c.after.provenance}` : summariseAttrDiff(c.before, c.after);
|
|
5171
|
+
blockLines.push(` ~ edge ${c.id} \u2014 ${provBit}`);
|
|
5172
|
+
}
|
|
5289
5173
|
}
|
|
5290
|
-
return
|
|
5291
|
-
}
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
${guide.replace(/\s+$/, "")}
|
|
5295
|
-
${NEAT_GRAPH_FIRST_END}
|
|
5296
|
-
`;
|
|
5174
|
+
return {
|
|
5175
|
+
summary: `Diff against ${input.againstSnapshot}: ${total} change${total === 1 ? "" : "s"} between the snapshot and the live graph.`,
|
|
5176
|
+
block: blockLines.join("\n").trimEnd()
|
|
5177
|
+
};
|
|
5297
5178
|
}
|
|
5298
|
-
function
|
|
5299
|
-
const
|
|
5300
|
-
|
|
5301
|
-
const
|
|
5302
|
-
|
|
5303
|
-
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
5304
|
-
const before = raw.slice(0, startIdx);
|
|
5305
|
-
const after = raw.slice(endIdx + NEAT_GRAPH_FIRST_END.length);
|
|
5306
|
-
const text2 = `${before}${block.replace(/\n+$/, "")}${after}`;
|
|
5307
|
-
return { text: text2, changed: text2 !== raw };
|
|
5179
|
+
function summariseAttrDiff(before, after) {
|
|
5180
|
+
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
5181
|
+
const changed = [];
|
|
5182
|
+
for (const k of keys) {
|
|
5183
|
+
if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.push(k);
|
|
5308
5184
|
}
|
|
5309
|
-
|
|
5310
|
-
const text = base.length > 0 ? `${base}
|
|
5311
|
-
|
|
5312
|
-
${block}` : block;
|
|
5313
|
-
return { text, changed: text !== raw };
|
|
5314
|
-
}
|
|
5315
|
-
async function readGuide() {
|
|
5316
|
-
return readSkillAsset(GUIDE_FILENAME);
|
|
5185
|
+
return changed.length === 0 ? "attributes differ" : `fields changed: ${changed.sort().join(", ")}`;
|
|
5317
5186
|
}
|
|
5318
|
-
async function
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5187
|
+
async function runStaleEdges(client, input) {
|
|
5188
|
+
const params = new URLSearchParams();
|
|
5189
|
+
if (input.limit !== void 0) params.set("limit", String(input.limit));
|
|
5190
|
+
if (input.edgeType) params.set("edgeType", input.edgeType);
|
|
5191
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5192
|
+
const body = await client.get(
|
|
5193
|
+
projectPath(input.project, `/stale-events${qs}`)
|
|
5194
|
+
);
|
|
5195
|
+
const events = body.events;
|
|
5196
|
+
if (events.length === 0) {
|
|
5197
|
+
return {
|
|
5198
|
+
summary: input.edgeType ? `No stale ${input.edgeType} edges recorded.` : "No stale-edge transitions recorded yet."
|
|
5199
|
+
};
|
|
5327
5200
|
}
|
|
5328
|
-
const
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
|
|
5335
|
-
|
|
5336
|
-
|
|
5201
|
+
const blockLines = events.map(
|
|
5202
|
+
(e) => ` ${e.transitionedAt} \u2014 ${e.source} -[${e.edgeType}]-> ${e.target} (last seen ${e.lastObserved}, threshold ${formatDuration(e.thresholdMs)})`
|
|
5203
|
+
);
|
|
5204
|
+
return {
|
|
5205
|
+
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
5206
|
+
block: blockLines.join("\n"),
|
|
5207
|
+
provenance: Provenance2.STALE
|
|
5208
|
+
};
|
|
5209
|
+
}
|
|
5210
|
+
async function runPolicies(client, input) {
|
|
5211
|
+
let violations;
|
|
5212
|
+
let allowed = true;
|
|
5213
|
+
let hypothetical;
|
|
5214
|
+
if (input.hypotheticalAction) {
|
|
5215
|
+
if (typeof client.post !== "function") {
|
|
5216
|
+
throw new Error("HttpClient does not support POST \u2014 required for policies dry-run");
|
|
5337
5217
|
}
|
|
5338
|
-
|
|
5339
|
-
|
|
5340
|
-
|
|
5341
|
-
agentsRaw = await fs12.readFile(agentsPath, "utf8");
|
|
5342
|
-
} catch (err) {
|
|
5343
|
-
if (err.code !== "ENOENT") {
|
|
5344
|
-
console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
|
|
5345
|
-
return { exitCode: 1 };
|
|
5346
|
-
}
|
|
5347
|
-
}
|
|
5348
|
-
let config;
|
|
5349
|
-
try {
|
|
5350
|
-
config = upsertCodexConfig(configRaw);
|
|
5351
|
-
} catch (err) {
|
|
5352
|
-
console.error(
|
|
5353
|
-
`neat codex: ${configPath} is not valid TOML \u2014 ${err.message}`
|
|
5218
|
+
const body = await client.post(
|
|
5219
|
+
projectPath(input.project, "/policies/check"),
|
|
5220
|
+
{ hypotheticalAction: input.hypotheticalAction }
|
|
5354
5221
|
);
|
|
5355
|
-
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
console.log(
|
|
5365
|
-
config.changed ? ` ${configRaw ? "update" : "create"} the [mcp_servers.neat] table:` : " already up to date \u2014 [mcp_servers.neat] matches"
|
|
5222
|
+
violations = body.violations;
|
|
5223
|
+
allowed = body.allowed;
|
|
5224
|
+
hypothetical = body.hypotheticalAction;
|
|
5225
|
+
} else {
|
|
5226
|
+
const params = new URLSearchParams();
|
|
5227
|
+
if (input.policyId) params.set("policyId", input.policyId);
|
|
5228
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5229
|
+
const body = await client.get(
|
|
5230
|
+
projectPath(input.project, `/policies/violations${qs}`)
|
|
5366
5231
|
);
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
agents.changed ? ` ${agentsRaw ? "update" : "create"} the graph-first block (between ${NEAT_GRAPH_FIRST_START} markers)` : " already up to date \u2014 graph-first block matches"
|
|
5232
|
+
violations = body.violations;
|
|
5233
|
+
allowed = violations.every((v) => v.onViolation !== "block");
|
|
5234
|
+
}
|
|
5235
|
+
if (input.nodeId) {
|
|
5236
|
+
violations = violations.filter(
|
|
5237
|
+
(v) => v.subject.nodeId === input.nodeId || v.subject.path?.includes(input.nodeId)
|
|
5374
5238
|
);
|
|
5375
|
-
console.log("");
|
|
5376
|
-
console.log("The MCP server reads NEAT_CORE_URL for the daemon URL \u2014 edit that value in");
|
|
5377
|
-
console.log("the generated table to point Codex at a non-default daemon.");
|
|
5378
|
-
return { exitCode: 0 };
|
|
5379
5239
|
}
|
|
5380
|
-
if (
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
} else {
|
|
5385
|
-
console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
|
|
5240
|
+
if (violations.length === 0) {
|
|
5241
|
+
return {
|
|
5242
|
+
summary: hypothetical ? `No violations would result from the hypothetical action (${hypothetical.kind}).` : "No policy violations recorded."
|
|
5243
|
+
};
|
|
5386
5244
|
}
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5245
|
+
const blockCount = violations.filter((v) => v.onViolation === "block").length;
|
|
5246
|
+
const summaryParts = [];
|
|
5247
|
+
if (hypothetical) {
|
|
5248
|
+
summaryParts.push(
|
|
5249
|
+
`Hypothetical ${hypothetical.kind} would surface ${violations.length} violation${violations.length === 1 ? "" : "s"}`
|
|
5250
|
+
);
|
|
5391
5251
|
} else {
|
|
5392
|
-
|
|
5252
|
+
summaryParts.push(
|
|
5253
|
+
`${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
|
|
5254
|
+
);
|
|
5393
5255
|
}
|
|
5394
|
-
|
|
5395
|
-
|
|
5396
|
-
|
|
5397
|
-
|
|
5398
|
-
|
|
5399
|
-
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
5404
|
-
|
|
5405
|
-
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
console.log("");
|
|
5409
|
-
console.log("Existing config is preserved and a re-run is a no-op. A malformed");
|
|
5410
|
-
console.log("config.toml is a clear error with no partial write.");
|
|
5256
|
+
if (blockCount > 0) summaryParts.push(`${blockCount} of which block`);
|
|
5257
|
+
if (!allowed && hypothetical) summaryParts.push("action denied");
|
|
5258
|
+
const summary = summaryParts.join("; ") + ".";
|
|
5259
|
+
const blockLines = violations.map((v) => {
|
|
5260
|
+
const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
|
|
5261
|
+
return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
|
|
5262
|
+
});
|
|
5263
|
+
const severities = [...new Set(violations.map((v) => v.severity))];
|
|
5264
|
+
return {
|
|
5265
|
+
summary,
|
|
5266
|
+
block: blockLines.join("\n"),
|
|
5267
|
+
confidence: hypothetical ? 0.7 : 1,
|
|
5268
|
+
provenance: severities.join(" ")
|
|
5269
|
+
};
|
|
5411
5270
|
}
|
|
5412
|
-
|
|
5413
|
-
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
console.error(`neat codex: unknown flag "${arg}"`);
|
|
5431
|
-
usage3();
|
|
5432
|
-
return 2;
|
|
5271
|
+
function formatDivergenceLine(d) {
|
|
5272
|
+
switch (d.type) {
|
|
5273
|
+
case "missing-observed":
|
|
5274
|
+
case "missing-extracted":
|
|
5275
|
+
if (d.column) {
|
|
5276
|
+
return ` \u2022 [${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5277
|
+
}
|
|
5278
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} (${d.edgeType}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5279
|
+
case "version-mismatch":
|
|
5280
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared ${d.extractedVersion}, observed engine ${d.observedVersion} (${d.compatibility})`;
|
|
5281
|
+
case "host-mismatch":
|
|
5282
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 declared host ${d.extractedHost}, observed host ${d.observedHost}`;
|
|
5283
|
+
case "compat-violation":
|
|
5284
|
+
return ` \u2022 [${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.rule.kind}${d.rule.package ? ` (${d.rule.package})` : ""}`;
|
|
5285
|
+
case "observed-symbol-mismatch": {
|
|
5286
|
+
const at = d.location ? ` at ${d.location}` : "";
|
|
5287
|
+
const member = d.symbol ? ` ${d.symbol}` : "";
|
|
5288
|
+
return ` \u2022 [${d.type}] ${d.source}${member}${at} (${d.mismatchKind}) \u2014 confidence ${d.confidence.toFixed(2)}`;
|
|
5433
5289
|
}
|
|
5434
5290
|
}
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
|
|
5440
|
-
|
|
5291
|
+
}
|
|
5292
|
+
async function runDivergences(client, input) {
|
|
5293
|
+
const params = new URLSearchParams();
|
|
5294
|
+
if (input.type && input.type.length > 0) params.set("type", input.type.join(","));
|
|
5295
|
+
if (input.minConfidence !== void 0) {
|
|
5296
|
+
params.set("minConfidence", String(input.minConfidence));
|
|
5297
|
+
}
|
|
5298
|
+
if (input.node) params.set("node", input.node);
|
|
5299
|
+
const qs = params.size > 0 ? `?${params.toString()}` : "";
|
|
5300
|
+
const result = await client.get(
|
|
5301
|
+
projectPath(input.project, `/graph/divergences${qs}`)
|
|
5302
|
+
);
|
|
5303
|
+
if (result.totalAffected === 0) {
|
|
5304
|
+
return {
|
|
5305
|
+
summary: "No divergences found between the declared (EXTRACTED) and observed (OBSERVED) views of the graph."
|
|
5306
|
+
};
|
|
5307
|
+
}
|
|
5308
|
+
const headline = result.divergences[0];
|
|
5309
|
+
const summary = `Found ${result.totalAffected} divergence${result.totalAffected === 1 ? "" : "s"} between code and production. Highest-confidence: ${headline.type} on ${headline.source} \u2192 ${headline.target}. ${headline.reason}`;
|
|
5310
|
+
const blockLines = [];
|
|
5311
|
+
for (const d of result.divergences) {
|
|
5312
|
+
blockLines.push(formatDivergenceLine(d));
|
|
5313
|
+
blockLines.push(` reason: ${d.reason}`);
|
|
5314
|
+
blockLines.push(` recommendation: ${d.recommendation}`);
|
|
5441
5315
|
}
|
|
5316
|
+
const maxConfidence = result.divergences.reduce(
|
|
5317
|
+
(m, d) => Math.max(m, d.confidence),
|
|
5318
|
+
0
|
|
5319
|
+
);
|
|
5320
|
+
return {
|
|
5321
|
+
summary,
|
|
5322
|
+
block: blockLines.join("\n"),
|
|
5323
|
+
confidence: maxConfidence,
|
|
5324
|
+
provenance: "composite (EXTRACTED + OBSERVED)"
|
|
5325
|
+
};
|
|
5442
5326
|
}
|
|
5443
|
-
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
}
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
5461
|
-
|
|
5462
|
-
|
|
5463
|
-
|
|
5464
|
-
|
|
5465
|
-
|
|
5466
|
-
|
|
5467
|
-
|
|
5327
|
+
async function runAsk(client, input) {
|
|
5328
|
+
const result = await client.get(
|
|
5329
|
+
projectPath(input.project, `/graph/ask?q=${encodeURIComponent(input.question)}`)
|
|
5330
|
+
);
|
|
5331
|
+
const blockLines = [];
|
|
5332
|
+
if (result.matched.length > 0) {
|
|
5333
|
+
blockLines.push(
|
|
5334
|
+
`Matched: ${result.matched.map((m) => `${m.nodeId} [${m.via} ${m.score.toFixed(2)}]`).join(", ")}`
|
|
5335
|
+
);
|
|
5336
|
+
blockLines.push(`Intent: ${result.intent}`);
|
|
5337
|
+
} else if (result.scope === "global") {
|
|
5338
|
+
blockLines.push(`Graph-wide answer (${result.intent}) \u2014 no entity named.`);
|
|
5339
|
+
}
|
|
5340
|
+
for (const section of result.sections) {
|
|
5341
|
+
blockLines.push("", section.heading + ":");
|
|
5342
|
+
for (const fact of section.facts) {
|
|
5343
|
+
const tag = fact.provenance ? ` [${fact.provenance}${fact.confidence !== void 0 ? ` ${fact.confidence.toFixed(2)}` : ""}]` : fact.confidence !== void 0 ? ` [confidence ${fact.confidence.toFixed(2)}]` : "";
|
|
5344
|
+
blockLines.push(` \u2022 ${fact.text}${tag}`);
|
|
5345
|
+
}
|
|
5346
|
+
}
|
|
5347
|
+
return {
|
|
5348
|
+
summary: result.answer,
|
|
5349
|
+
block: blockLines.join("\n").trim(),
|
|
5350
|
+
...result.confidence !== void 0 ? { confidence: result.confidence } : {},
|
|
5351
|
+
...result.provenance.length > 0 ? { provenance: result.provenance } : {}
|
|
5352
|
+
};
|
|
5468
5353
|
}
|
|
5469
|
-
function
|
|
5470
|
-
const
|
|
5471
|
-
|
|
5354
|
+
function formatFooter(confidence, provenance) {
|
|
5355
|
+
const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
|
|
5356
|
+
const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
|
|
5357
|
+
return `confidence: ${c} \xB7 provenance: ${p}`;
|
|
5472
5358
|
}
|
|
5473
|
-
function
|
|
5474
|
-
const
|
|
5475
|
-
|
|
5359
|
+
function formatHuman(result) {
|
|
5360
|
+
const sections = [result.summary.trim()];
|
|
5361
|
+
if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
|
|
5362
|
+
sections.push(formatFooter(result.confidence, result.provenance));
|
|
5363
|
+
return sections.join("\n\n");
|
|
5476
5364
|
}
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
};
|
|
5489
|
-
var DEVIN_CLIENT = {
|
|
5490
|
-
id: "devin",
|
|
5491
|
-
label: "Devin Desktop (Cascade)",
|
|
5492
|
-
docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
|
|
5493
|
-
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? path14.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
5494
|
-
mcpContainerKey: "mcpServers",
|
|
5495
|
-
format: "json",
|
|
5496
|
-
rulesFileName: ".windsurfrules"
|
|
5497
|
-
};
|
|
5498
|
-
var GEMINI_CLIENT = {
|
|
5499
|
-
id: "gemini",
|
|
5500
|
-
label: "Gemini CLI",
|
|
5501
|
-
docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
|
|
5502
|
-
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? path14.join(homeDir(), ".gemini", "settings.json"),
|
|
5503
|
-
mcpContainerKey: "mcpServers",
|
|
5504
|
-
format: "json",
|
|
5505
|
-
rulesFileName: "GEMINI.md"
|
|
5506
|
-
};
|
|
5507
|
-
var QWEN_CLIENT = {
|
|
5508
|
-
id: "qwen",
|
|
5509
|
-
label: "Qwen Code",
|
|
5510
|
-
docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
|
|
5511
|
-
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? path14.join(homeDir(), ".qwen", "settings.json"),
|
|
5512
|
-
mcpContainerKey: "mcpServers",
|
|
5513
|
-
format: "json",
|
|
5514
|
-
rulesFileName: "QWEN.md"
|
|
5515
|
-
};
|
|
5516
|
-
var AMAZONQ_CLIENT = {
|
|
5517
|
-
id: "amazonq",
|
|
5518
|
-
label: "Amazon Q Developer CLI",
|
|
5519
|
-
docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
|
|
5520
|
-
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? path14.join(homeDir(), ".aws", "amazonq", "mcp.json"),
|
|
5521
|
-
mcpContainerKey: "mcpServers",
|
|
5522
|
-
format: "json"
|
|
5523
|
-
};
|
|
5524
|
-
var ROOCODE_CLIENT = {
|
|
5525
|
-
id: "roocode",
|
|
5526
|
-
label: "Roo Code",
|
|
5527
|
-
docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
|
|
5528
|
-
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? path14.join(process.cwd(), ".roo", "mcp.json"),
|
|
5529
|
-
mcpContainerKey: "mcpServers",
|
|
5530
|
-
format: "json"
|
|
5531
|
-
};
|
|
5532
|
-
var ZED_CLIENT = {
|
|
5533
|
-
id: "zed",
|
|
5534
|
-
label: "Zed",
|
|
5535
|
-
docsUrl: "https://zed.dev/docs/ai/mcp",
|
|
5536
|
-
mcpConfigPath: () => {
|
|
5537
|
-
const override = envOverride("NEAT_ZED_CONFIG");
|
|
5538
|
-
if (override) return override;
|
|
5539
|
-
if (process.platform === "win32") {
|
|
5540
|
-
const appData = process.env.APPDATA;
|
|
5541
|
-
if (appData && appData.length > 0) return path14.join(appData, "Zed", "settings.json");
|
|
5542
|
-
}
|
|
5543
|
-
return path14.join(homeDir(), ".config", "zed", "settings.json");
|
|
5544
|
-
},
|
|
5545
|
-
mcpContainerKey: "context_servers",
|
|
5546
|
-
format: "jsonc",
|
|
5547
|
-
rulesFileName: ".rules"
|
|
5548
|
-
};
|
|
5549
|
-
var OPENCODE_CLIENT = {
|
|
5550
|
-
id: "opencode",
|
|
5551
|
-
label: "OpenCode",
|
|
5552
|
-
docsUrl: "https://opencode.ai/docs/mcp-servers/",
|
|
5553
|
-
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? path14.join(xdgConfigDir(), "opencode", "opencode.json"),
|
|
5554
|
-
mcpContainerKey: "mcp",
|
|
5555
|
-
format: "json",
|
|
5556
|
-
serverEntry: NEAT_OPENCODE_SERVER,
|
|
5557
|
-
rulesFileName: "AGENTS.md"
|
|
5558
|
-
};
|
|
5559
|
-
var CRUSH_CLIENT = {
|
|
5560
|
-
id: "crush",
|
|
5561
|
-
label: "Crush",
|
|
5562
|
-
docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
|
|
5563
|
-
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? path14.join(xdgConfigDir(), "crush", "crush.json"),
|
|
5564
|
-
mcpContainerKey: "mcp",
|
|
5565
|
-
format: "json",
|
|
5566
|
-
serverEntry: NEAT_CRUSH_SERVER,
|
|
5567
|
-
rulesFileName: "AGENTS.md"
|
|
5568
|
-
};
|
|
5569
|
-
var CLIENTS = {
|
|
5570
|
-
cursor: CURSOR_CLIENT,
|
|
5571
|
-
devin: DEVIN_CLIENT,
|
|
5572
|
-
gemini: GEMINI_CLIENT,
|
|
5573
|
-
qwen: QWEN_CLIENT,
|
|
5574
|
-
amazonq: AMAZONQ_CLIENT,
|
|
5575
|
-
roocode: ROOCODE_CLIENT,
|
|
5576
|
-
zed: ZED_CLIENT,
|
|
5577
|
-
opencode: OPENCODE_CLIENT,
|
|
5578
|
-
crush: CRUSH_CLIENT
|
|
5579
|
-
};
|
|
5580
|
-
function mergeJsonMcp(existing, containerKey, serverEntry) {
|
|
5581
|
-
const servers = existing[containerKey] ?? {};
|
|
5582
|
-
const already = isDeepStrictEqual2(servers.neat, serverEntry);
|
|
5583
|
-
const merged = {
|
|
5584
|
-
...existing,
|
|
5585
|
-
[containerKey]: { ...servers, neat: serverEntry }
|
|
5586
|
-
};
|
|
5587
|
-
return { merged, changed: !already };
|
|
5588
|
-
}
|
|
5589
|
-
function mergeJsoncMcp(raw, containerKey, serverEntry) {
|
|
5590
|
-
const base = raw.trim().length > 0 ? raw : "{}";
|
|
5591
|
-
const parsed = jsonc.parse(base) ?? {};
|
|
5592
|
-
const servers = parsed[containerKey] ?? {};
|
|
5593
|
-
if (isDeepStrictEqual2(servers.neat, serverEntry)) {
|
|
5594
|
-
return { text: raw, changed: false };
|
|
5595
|
-
}
|
|
5596
|
-
const edits = jsonc.modify(base, [containerKey, "neat"], serverEntry, {
|
|
5597
|
-
formattingOptions: { tabSize: 2, insertSpaces: true }
|
|
5598
|
-
});
|
|
5599
|
-
let text = jsonc.applyEdits(base, edits);
|
|
5600
|
-
if (!text.endsWith("\n")) text += "\n";
|
|
5601
|
-
return { text, changed: text !== raw };
|
|
5365
|
+
function formatJson(result) {
|
|
5366
|
+
return JSON.stringify(
|
|
5367
|
+
{
|
|
5368
|
+
summary: result.summary,
|
|
5369
|
+
block: result.block ?? "",
|
|
5370
|
+
confidence: result.confidence ?? null,
|
|
5371
|
+
provenance: result.provenance ?? null
|
|
5372
|
+
},
|
|
5373
|
+
null,
|
|
5374
|
+
2
|
|
5375
|
+
);
|
|
5602
5376
|
}
|
|
5603
|
-
function
|
|
5604
|
-
|
|
5377
|
+
function exitCodeForError(err) {
|
|
5378
|
+
if (err instanceof TransportError) return 3;
|
|
5379
|
+
if (err instanceof HttpError) return 1;
|
|
5380
|
+
return 1;
|
|
5605
5381
|
}
|
|
5606
|
-
function
|
|
5607
|
-
return
|
|
5608
|
-
${guide.trim()}
|
|
5609
|
-
${GRAPH_FIRST_MARKER_CLOSE}
|
|
5610
|
-
`;
|
|
5382
|
+
function createSnapshotPushClient(baseUrl, token) {
|
|
5383
|
+
return createHttpClient(baseUrl, token && token.length > 0 ? token : void 0);
|
|
5611
5384
|
}
|
|
5612
|
-
function
|
|
5613
|
-
const
|
|
5614
|
-
|
|
5385
|
+
async function pushSnapshotToRemote(input) {
|
|
5386
|
+
const client = createSnapshotPushClient(input.baseUrl, input.token);
|
|
5387
|
+
if (typeof client.post !== "function") {
|
|
5388
|
+
throw new Error("HttpClient does not support POST \u2014 required for snapshot push");
|
|
5389
|
+
}
|
|
5390
|
+
return client.post(
|
|
5391
|
+
`/projects/${encodeURIComponent(input.project)}/snapshot`,
|
|
5392
|
+
{ snapshot: input.snapshot }
|
|
5615
5393
|
);
|
|
5616
|
-
|
|
5617
|
-
if (existing.trim().length === 0) return block;
|
|
5618
|
-
return `${existing.replace(/\s+$/, "")}
|
|
5394
|
+
}
|
|
5619
5395
|
|
|
5620
|
-
|
|
5396
|
+
// src/doctor-cli.ts
|
|
5397
|
+
function resolveDeps2(deps) {
|
|
5398
|
+
return {
|
|
5399
|
+
cwd: deps.cwd ?? process.cwd(),
|
|
5400
|
+
env: deps.env ?? process.env,
|
|
5401
|
+
nodeVersion: deps.nodeVersion ?? process.versions.node,
|
|
5402
|
+
fetchImpl: deps.fetchImpl ?? fetch,
|
|
5403
|
+
readRecord: deps.readRecord ?? readDaemonRecord,
|
|
5404
|
+
out: deps.out ?? ((line) => console.log(line))
|
|
5405
|
+
};
|
|
5621
5406
|
}
|
|
5622
|
-
|
|
5623
|
-
|
|
5624
|
-
|
|
5407
|
+
var NODE_FLOOR = 20;
|
|
5408
|
+
var HEALTH_TIMEOUT_MS = 3e3;
|
|
5409
|
+
function checkNode(nodeVersion) {
|
|
5410
|
+
const major = Number.parseInt(nodeVersion.split(".")[0] ?? "", 10);
|
|
5411
|
+
const ok = Number.isFinite(major) && major >= NODE_FLOOR;
|
|
5412
|
+
return ok ? { name: "node", ok, detail: `v${nodeVersion} (>= ${NODE_FLOOR} required)` } : {
|
|
5413
|
+
name: "node",
|
|
5414
|
+
ok,
|
|
5415
|
+
detail: `v${nodeVersion} \u2014 NEAT needs Node ${NODE_FLOOR} or newer`,
|
|
5416
|
+
fix: `install Node ${NODE_FLOOR}.x (e.g. \`nvm install ${NODE_FLOOR}\`) and re-run`
|
|
5417
|
+
};
|
|
5418
|
+
}
|
|
5419
|
+
async function neatOutExists(cwd) {
|
|
5625
5420
|
try {
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
raw = "";
|
|
5631
|
-
} else {
|
|
5632
|
-
console.error(`neat ${client.id}: failed to read ${mcpPath} \u2014 ${e.message}`);
|
|
5633
|
-
return null;
|
|
5634
|
-
}
|
|
5421
|
+
const st = await fs10.stat(path11.join(cwd, "neat-out"));
|
|
5422
|
+
return st.isDirectory();
|
|
5423
|
+
} catch {
|
|
5424
|
+
return false;
|
|
5635
5425
|
}
|
|
5636
|
-
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5426
|
+
}
|
|
5427
|
+
async function checkProject(cwd, record) {
|
|
5428
|
+
if (record) {
|
|
5429
|
+
return {
|
|
5430
|
+
name: "project",
|
|
5431
|
+
ok: true,
|
|
5432
|
+
detail: `"${record.project}" \u2014 set up in this directory (daemon record on REST ${record.ports.rest})`
|
|
5433
|
+
};
|
|
5434
|
+
}
|
|
5435
|
+
if (await neatOutExists(cwd)) {
|
|
5436
|
+
return {
|
|
5437
|
+
name: "project",
|
|
5438
|
+
ok: true,
|
|
5439
|
+
detail: "set up in this directory (no live daemon record \u2014 it may be stopped)"
|
|
5440
|
+
};
|
|
5441
|
+
}
|
|
5442
|
+
return {
|
|
5443
|
+
name: "project",
|
|
5444
|
+
ok: false,
|
|
5445
|
+
detail: "no NEAT project in this directory",
|
|
5446
|
+
fix: "set one up: `neat .`"
|
|
5447
|
+
};
|
|
5448
|
+
}
|
|
5449
|
+
function resolveHealthUrl(env, record) {
|
|
5450
|
+
const explicit = env.NEAT_API_URL ?? env.NEAT_CORE_URL;
|
|
5451
|
+
if (explicit && explicit.length > 0) return explicit.replace(/\/$/, "");
|
|
5452
|
+
if (record) return `http://localhost:${record.ports.rest}`;
|
|
5453
|
+
return "http://localhost:8080";
|
|
5454
|
+
}
|
|
5455
|
+
function summariseHealth(url, body) {
|
|
5456
|
+
const projects = body.projects ?? [];
|
|
5457
|
+
const nodes = projects.reduce((n, p) => n + (p.nodeCount ?? 0), 0);
|
|
5458
|
+
const edges = projects.reduce((n, p) => n + (p.edgeCount ?? 0), 0);
|
|
5459
|
+
const proj = body.project ?? projects[0]?.name;
|
|
5460
|
+
const graph = projects.length > 0 ? ` \u2014 ${nodes} nodes / ${edges} edges` : "";
|
|
5461
|
+
return `up at ${url}${proj ? ` (project "${proj}"${graph})` : ""}`;
|
|
5462
|
+
}
|
|
5463
|
+
async function checkDaemon(deps, record) {
|
|
5464
|
+
const url = resolveHealthUrl(deps.env, record);
|
|
5465
|
+
const token = resolveAuthToken(deps.env);
|
|
5466
|
+
const headers = token ? { authorization: `Bearer ${token}` } : {};
|
|
5467
|
+
try {
|
|
5468
|
+
const res = await deps.fetchImpl(`${url}/health`, {
|
|
5469
|
+
headers,
|
|
5470
|
+
signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
|
|
5471
|
+
});
|
|
5472
|
+
if (res.status === 401 || res.status === 403) {
|
|
5473
|
+
return {
|
|
5474
|
+
name: "daemon",
|
|
5475
|
+
ok: false,
|
|
5476
|
+
detail: `up at ${url}, but rejected the request (${res.status})`,
|
|
5477
|
+
fix: "set NEAT_AUTH_TOKEN to the daemon's token"
|
|
5478
|
+
};
|
|
5647
5479
|
}
|
|
5648
|
-
|
|
5480
|
+
if (!res.ok) {
|
|
5481
|
+
return {
|
|
5482
|
+
name: "daemon",
|
|
5483
|
+
ok: false,
|
|
5484
|
+
detail: `reachable at ${url} but /health returned ${res.status}`,
|
|
5485
|
+
fix: "check the daemon logs"
|
|
5486
|
+
};
|
|
5487
|
+
}
|
|
5488
|
+
const body = await res.json().catch(() => ({}));
|
|
5489
|
+
return { name: "daemon", ok: true, detail: summariseHealth(url, body) };
|
|
5490
|
+
} catch {
|
|
5491
|
+
return {
|
|
5492
|
+
name: "daemon",
|
|
5493
|
+
ok: false,
|
|
5494
|
+
detail: `down \u2014 nothing answering at ${url}`,
|
|
5495
|
+
fix: "start it: `neat .` (or `neat watch`)"
|
|
5496
|
+
};
|
|
5649
5497
|
}
|
|
5650
|
-
|
|
5651
|
-
|
|
5652
|
-
|
|
5653
|
-
|
|
5654
|
-
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5498
|
+
}
|
|
5499
|
+
async function runDoctorChecks(deps = {}) {
|
|
5500
|
+
const d = resolveDeps2(deps);
|
|
5501
|
+
const record = await d.readRecord(d.cwd).catch(() => null);
|
|
5502
|
+
return [checkNode(d.nodeVersion), await checkProject(d.cwd, record), await checkDaemon(d, record)];
|
|
5503
|
+
}
|
|
5504
|
+
var NAME_COL = "project".length;
|
|
5505
|
+
function renderHuman(checks, out) {
|
|
5506
|
+
out("neat doctor \u2014 checking this project's setup");
|
|
5507
|
+
out("");
|
|
5508
|
+
for (const c of checks) {
|
|
5509
|
+
const mark = c.ok ? "\u2713" : "\u2717";
|
|
5510
|
+
out(` ${mark} ${c.name.padEnd(NAME_COL)} ${c.detail}`);
|
|
5511
|
+
if (!c.ok && c.fix) out(` ${" ".repeat(NAME_COL + 3)}fix: ${c.fix}`);
|
|
5512
|
+
}
|
|
5513
|
+
out("");
|
|
5514
|
+
const failed = checks.filter((c) => !c.ok).length;
|
|
5515
|
+
out(failed === 0 ? "all good." : `${failed} check${failed === 1 ? "" : "s"} failed.`);
|
|
5516
|
+
}
|
|
5517
|
+
async function runDoctorCommand(argv, deps = {}) {
|
|
5518
|
+
const out = deps.out ?? ((line) => console.log(line));
|
|
5519
|
+
let json = false;
|
|
5520
|
+
for (const arg of argv) {
|
|
5521
|
+
if (arg === "--json") json = true;
|
|
5522
|
+
else if (arg === "-h" || arg === "--help") {
|
|
5523
|
+
out("usage: neat doctor [--json]");
|
|
5524
|
+
out(" Probe this directory's NEAT setup: Node version, project, daemon.");
|
|
5525
|
+
out(" Exit 0 when every check passes, 1 when any fails.");
|
|
5526
|
+
return 0;
|
|
5527
|
+
} else {
|
|
5528
|
+
out(`neat doctor: unknown argument "${arg}"`);
|
|
5529
|
+
return 2;
|
|
5659
5530
|
}
|
|
5660
5531
|
}
|
|
5661
|
-
const
|
|
5662
|
-
|
|
5532
|
+
const checks = await runDoctorChecks(deps);
|
|
5533
|
+
if (json) out(JSON.stringify({ ok: checks.every((c) => c.ok), checks }, null, 2));
|
|
5534
|
+
else renderHuman(checks, out);
|
|
5535
|
+
return checks.every((c) => c.ok) ? 0 : 1;
|
|
5663
5536
|
}
|
|
5664
|
-
|
|
5665
|
-
|
|
5666
|
-
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5537
|
+
|
|
5538
|
+
// src/hooks-cli.ts
|
|
5539
|
+
import path12 from "path";
|
|
5540
|
+
import os from "os";
|
|
5541
|
+
import { promises as fs11 } from "fs";
|
|
5542
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
5543
|
+
var HOOK_FILENAME = "neat-search-nudge.mjs";
|
|
5544
|
+
var GUIDE_FILENAME = "GRAPH_FIRST.md";
|
|
5545
|
+
var GUIDE_INSTALL_NAME = "neat-graph-first.md";
|
|
5546
|
+
var HOOK_MATCHER = "Grep|Glob|Bash";
|
|
5547
|
+
function moduleDir() {
|
|
5548
|
+
return typeof __dirname !== "undefined" ? __dirname : path12.dirname(fileURLToPath3(import.meta.url));
|
|
5549
|
+
}
|
|
5550
|
+
async function readSkillAsset(rel) {
|
|
5551
|
+
const here = moduleDir();
|
|
5552
|
+
const candidates = [
|
|
5553
|
+
path12.resolve(here, "../../claude-skill", rel),
|
|
5554
|
+
path12.resolve(here, "../../../claude-skill", rel),
|
|
5555
|
+
path12.resolve(here, "../claude-skill", rel)
|
|
5556
|
+
];
|
|
5557
|
+
for (const candidate of candidates) {
|
|
5676
5558
|
try {
|
|
5677
|
-
|
|
5678
|
-
} catch
|
|
5679
|
-
if (err.code !== "ENOENT") {
|
|
5680
|
-
console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
|
|
5681
|
-
return { exitCode: 1 };
|
|
5682
|
-
}
|
|
5559
|
+
return await fs11.readFile(candidate, "utf8");
|
|
5560
|
+
} catch {
|
|
5683
5561
|
}
|
|
5684
|
-
const guide = await readSkillAsset(GUIDE_FILENAME);
|
|
5685
|
-
block = buildGuidanceBlock(guide);
|
|
5686
|
-
newRules = mergeRulesFile(existingRules, block);
|
|
5687
|
-
rulesChanged = newRules !== existingRules;
|
|
5688
5562
|
}
|
|
5689
|
-
|
|
5690
|
-
|
|
5691
|
-
|
|
5692
|
-
|
|
5693
|
-
|
|
5694
|
-
|
|
5695
|
-
|
|
5696
|
-
|
|
5697
|
-
|
|
5698
|
-
|
|
5699
|
-
|
|
5700
|
-
|
|
5701
|
-
|
|
5702
|
-
|
|
5703
|
-
|
|
5563
|
+
throw new Error(
|
|
5564
|
+
`neat hooks: could not find @neat.is/claude-skill/${rel} \u2014 is the package installed?`
|
|
5565
|
+
);
|
|
5566
|
+
}
|
|
5567
|
+
function neatHome() {
|
|
5568
|
+
const override = process.env.NEAT_HOME;
|
|
5569
|
+
if (override && override.length > 0) return path12.resolve(override);
|
|
5570
|
+
return path12.join(os.homedir(), ".neat");
|
|
5571
|
+
}
|
|
5572
|
+
function claudeSettingsPath() {
|
|
5573
|
+
const override = process.env.NEAT_CLAUDE_SETTINGS;
|
|
5574
|
+
if (override && override.length > 0) return path12.resolve(override);
|
|
5575
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? os.homedir();
|
|
5576
|
+
return path12.join(home, ".claude", "settings.json");
|
|
5577
|
+
}
|
|
5578
|
+
function installedHookPath() {
|
|
5579
|
+
return path12.join(neatHome(), "hooks", HOOK_FILENAME);
|
|
5580
|
+
}
|
|
5581
|
+
function gateFlagPath() {
|
|
5582
|
+
return path12.join(neatHome(), "hooks", "gate-enabled");
|
|
5583
|
+
}
|
|
5584
|
+
function isNeatSearchEntry(entry2) {
|
|
5585
|
+
return (entry2.hooks ?? []).some(
|
|
5586
|
+
(h) => typeof h.command === "string" && h.command.includes(HOOK_FILENAME)
|
|
5587
|
+
);
|
|
5588
|
+
}
|
|
5589
|
+
function neatHookEntry(command) {
|
|
5590
|
+
return { matcher: HOOK_MATCHER, hooks: [{ type: "command", command }] };
|
|
5591
|
+
}
|
|
5592
|
+
function hookCommand(scriptPath) {
|
|
5593
|
+
return `node "${scriptPath}"`;
|
|
5594
|
+
}
|
|
5595
|
+
async function runHooks(opts) {
|
|
5596
|
+
if (opts.printHook) {
|
|
5597
|
+
process.stdout.write(await readSkillAsset(`hooks/${HOOK_FILENAME}`));
|
|
5598
|
+
return { exitCode: 0 };
|
|
5599
|
+
}
|
|
5600
|
+
if (opts.printGuide) {
|
|
5601
|
+
process.stdout.write(await readSkillAsset(GUIDE_FILENAME));
|
|
5602
|
+
return { exitCode: 0 };
|
|
5603
|
+
}
|
|
5604
|
+
if (opts.printSettings) {
|
|
5605
|
+
const block = {
|
|
5606
|
+
hooks: { PreToolUse: [neatHookEntry(hookCommand(installedHookPath()))] }
|
|
5607
|
+
};
|
|
5608
|
+
process.stdout.write(JSON.stringify(block, null, 2) + "\n");
|
|
5609
|
+
return { exitCode: 0 };
|
|
5610
|
+
}
|
|
5611
|
+
if (opts.apply) {
|
|
5612
|
+
const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
|
|
5613
|
+
const guide = await readSkillAsset(GUIDE_FILENAME);
|
|
5614
|
+
const scriptPath = installedHookPath();
|
|
5615
|
+
await fs11.mkdir(path12.dirname(scriptPath), { recursive: true });
|
|
5616
|
+
await fs11.writeFile(scriptPath, hookScript, { mode: 493 });
|
|
5617
|
+
const guidePath = path12.join(neatHome(), GUIDE_INSTALL_NAME);
|
|
5618
|
+
await fs11.writeFile(guidePath, guide, "utf8");
|
|
5619
|
+
const settingsFile = claudeSettingsPath();
|
|
5620
|
+
let settings = {};
|
|
5621
|
+
try {
|
|
5622
|
+
settings = JSON.parse(await fs11.readFile(settingsFile, "utf8"));
|
|
5623
|
+
} catch (err) {
|
|
5624
|
+
if (err.code !== "ENOENT") {
|
|
5625
|
+
console.error(
|
|
5626
|
+
`neat hooks: failed to read ${settingsFile} \u2014 ${err.message}`
|
|
5627
|
+
);
|
|
5628
|
+
return { exitCode: 1 };
|
|
5629
|
+
}
|
|
5630
|
+
}
|
|
5631
|
+
const hooks = settings.hooks ?? {};
|
|
5632
|
+
const preToolUse = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
|
|
5633
|
+
const command = hookCommand(scriptPath);
|
|
5634
|
+
const existingIdx = preToolUse.findIndex(isNeatSearchEntry);
|
|
5635
|
+
if (existingIdx >= 0) {
|
|
5636
|
+
preToolUse[existingIdx] = neatHookEntry(command);
|
|
5637
|
+
} else {
|
|
5638
|
+
preToolUse.push(neatHookEntry(command));
|
|
5639
|
+
}
|
|
5640
|
+
const merged = {
|
|
5641
|
+
...settings,
|
|
5642
|
+
hooks: { ...hooks, PreToolUse: preToolUse }
|
|
5643
|
+
};
|
|
5644
|
+
await fs11.mkdir(path12.dirname(settingsFile), { recursive: true });
|
|
5645
|
+
await fs11.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
5646
|
+
const flag = gateFlagPath();
|
|
5647
|
+
if (opts.gate) {
|
|
5648
|
+
await fs11.mkdir(path12.dirname(flag), { recursive: true });
|
|
5649
|
+
await fs11.writeFile(flag, "1\n", "utf8");
|
|
5650
|
+
} else {
|
|
5651
|
+
await fs11.rm(flag, { force: true });
|
|
5704
5652
|
}
|
|
5653
|
+
const mode = opts.gate ? "GATE (deny search until you ask the graph)" : "nudge (search still runs)";
|
|
5654
|
+
console.log(`neat hooks: installed the search hook in ${opts.gate ? "gate" : "nudge"} mode`);
|
|
5655
|
+
console.log(` script: ${scriptPath}`);
|
|
5656
|
+
console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
|
|
5657
|
+
console.log(` guidance: ${guidePath}`);
|
|
5658
|
+
console.log(` mode: ${mode}`);
|
|
5705
5659
|
console.log("");
|
|
5706
|
-
|
|
5707
|
-
|
|
5708
|
-
|
|
5660
|
+
if (opts.gate) {
|
|
5661
|
+
console.log("restart Claude Code to load the hook. A Grep/Glob or Bash grep is now DENIED");
|
|
5662
|
+
console.log('until you run `neat ask "<question>"` (or the ask MCP tool) once this session;');
|
|
5663
|
+
console.log("after that, search is allowed as a fallback. Set NEAT_SEARCH_GATE=0 to fall");
|
|
5664
|
+
console.log("back to nudge-only without re-running.");
|
|
5665
|
+
} else {
|
|
5666
|
+
console.log("restart Claude Code to load the hook. On a Grep/Glob or a Bash grep,");
|
|
5667
|
+
console.log("your agent will now be nudged to query NEAT first (the search still runs).");
|
|
5668
|
+
console.log("Re-run with --gate to hard-force the graph-first orientation.");
|
|
5669
|
+
}
|
|
5670
|
+
console.log("");
|
|
5671
|
+
console.log("The hook is Claude-Code-specific. For agents on other harnesses, paste");
|
|
5672
|
+
console.log(`the guidance above into your project instructions (CLAUDE.md / AGENTS.md).`);
|
|
5709
5673
|
return { exitCode: 0 };
|
|
5710
5674
|
}
|
|
5711
|
-
|
|
5712
|
-
await fs13.writeFile(mcpPath, mcp.text, "utf8");
|
|
5713
|
-
if (hasRules) {
|
|
5714
|
-
await fs13.mkdir(path14.dirname(rulesPath), { recursive: true });
|
|
5715
|
-
await fs13.writeFile(rulesPath, newRules, "utf8");
|
|
5716
|
-
}
|
|
5717
|
-
console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
|
|
5718
|
-
console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
|
|
5719
|
-
if (hasRules) console.log(` guidance: ${rulesPath} (neat:graph-first block)`);
|
|
5720
|
-
console.log("");
|
|
5721
|
-
console.log(`restart ${client.label} to pick up the MCP server. Point it at a non-default`);
|
|
5722
|
-
console.log(`daemon by setting NEAT_CORE_URL in the neat server's env in that config.`);
|
|
5675
|
+
usage();
|
|
5723
5676
|
return { exitCode: 0 };
|
|
5724
5677
|
}
|
|
5725
|
-
function
|
|
5726
|
-
|
|
5727
|
-
}
|
|
5728
|
-
function usage4(client) {
|
|
5729
|
-
const hasRules = typeof client.rulesFileName === "string";
|
|
5730
|
-
console.log(
|
|
5731
|
-
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}`
|
|
5732
|
-
);
|
|
5733
|
-
console.log("");
|
|
5734
|
-
console.log(
|
|
5735
|
-
hasRules ? " --apply write the MCP config and the rules file (default: plan only)" : " --apply write the MCP config (default: plan only)"
|
|
5736
|
-
);
|
|
5678
|
+
function usage() {
|
|
5679
|
+
console.log("neat hooks \u2014 wire NEAT into your agent so it queries the graph before grepping");
|
|
5737
5680
|
console.log("");
|
|
5738
|
-
console.log("
|
|
5739
|
-
console.log(
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5745
|
-
|
|
5681
|
+
console.log(" --apply install the Claude Code search hook and write the");
|
|
5682
|
+
console.log(" graph-first guidance to ~/.neat/, merging into");
|
|
5683
|
+
console.log(" ~/.claude/settings.json without touching your other hooks");
|
|
5684
|
+
console.log(" --gate with --apply, enable hard-gate mode: DENY Grep/Glob/grep-Bash");
|
|
5685
|
+
console.log(" until `neat ask` has run this session (default is nudge-only).");
|
|
5686
|
+
console.log(" Toggle off at run time with NEAT_SEARCH_GATE=0.");
|
|
5687
|
+
console.log(" --print-hook print the hook script to stdout");
|
|
5688
|
+
console.log(" --print-guide print the agent-agnostic graph-first guidance to stdout");
|
|
5689
|
+
console.log(" --print-settings print the settings.json PreToolUse block --apply would add");
|
|
5746
5690
|
console.log("");
|
|
5747
|
-
console.log(
|
|
5691
|
+
console.log("By default the hook is a gentle, non-blocking nudge \u2014 searches still run.");
|
|
5692
|
+
console.log("--gate turns it into a hard forcing mechanism. It is Claude-Code-specific;");
|
|
5693
|
+
console.log("other harnesses get the same steer from the graph-first guidance.");
|
|
5748
5694
|
}
|
|
5749
|
-
async function
|
|
5750
|
-
const
|
|
5751
|
-
|
|
5695
|
+
async function runHooksCommand(args) {
|
|
5696
|
+
const opts = {
|
|
5697
|
+
apply: false,
|
|
5698
|
+
printHook: false,
|
|
5699
|
+
printGuide: false,
|
|
5700
|
+
printSettings: false,
|
|
5701
|
+
gate: false
|
|
5702
|
+
};
|
|
5752
5703
|
for (const arg of args) {
|
|
5753
5704
|
switch (arg) {
|
|
5754
5705
|
case "--apply":
|
|
5755
|
-
|
|
5706
|
+
opts.apply = true;
|
|
5707
|
+
break;
|
|
5708
|
+
case "--gate":
|
|
5709
|
+
opts.gate = true;
|
|
5710
|
+
break;
|
|
5711
|
+
case "--print-hook":
|
|
5712
|
+
opts.printHook = true;
|
|
5713
|
+
break;
|
|
5714
|
+
case "--print-guide":
|
|
5715
|
+
opts.printGuide = true;
|
|
5716
|
+
break;
|
|
5717
|
+
case "--print-settings":
|
|
5718
|
+
opts.printSettings = true;
|
|
5756
5719
|
break;
|
|
5757
5720
|
case "-h":
|
|
5758
5721
|
case "--help":
|
|
5759
|
-
|
|
5722
|
+
usage();
|
|
5760
5723
|
return 0;
|
|
5761
5724
|
default:
|
|
5762
|
-
console.error(`neat
|
|
5763
|
-
|
|
5725
|
+
console.error(`neat hooks: unknown flag "${arg}"`);
|
|
5726
|
+
usage();
|
|
5764
5727
|
return 2;
|
|
5765
5728
|
}
|
|
5766
5729
|
}
|
|
5767
5730
|
try {
|
|
5768
|
-
const { exitCode } = await
|
|
5731
|
+
const { exitCode } = await runHooks(opts);
|
|
5769
5732
|
return exitCode;
|
|
5770
5733
|
} catch (err) {
|
|
5771
5734
|
console.error(err.message);
|
|
@@ -5773,572 +5736,753 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
|
|
|
5773
5736
|
}
|
|
5774
5737
|
}
|
|
5775
5738
|
|
|
5776
|
-
// src/
|
|
5777
|
-
import
|
|
5739
|
+
// src/claude-cli.ts
|
|
5740
|
+
import path13 from "path";
|
|
5741
|
+
import { promises as fs12 } from "fs";
|
|
5742
|
+
var NEAT_SECTION_HEADING = "## neat";
|
|
5743
|
+
var NEAT_DIRECTIVE_BODY = `This project has NEAT wired in: a live, fused semantic graph of the system \u2014
|
|
5744
|
+
code and runtime behaviour (OpenTelemetry) in one model, every fact tagged with
|
|
5745
|
+
its provenance (EXTRACTED / OBSERVED / INFERRED / STALE) and a confidence.
|
|
5778
5746
|
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5747
|
+
For ANY question about this system's behaviour, dependencies, failures, root
|
|
5748
|
+
cause, or blast radius, call \`neat ask\` FIRST \u2014 before Read, Grep, Glob, or Bash:
|
|
5749
|
+
|
|
5750
|
+
neat ask "why is checkout failing?"
|
|
5751
|
+
neat ask "what breaks if I change the orders table?"
|
|
5752
|
+
neat ask "what does the payments service depend on at runtime?"
|
|
5753
|
+
|
|
5754
|
+
\`ask\` resolves your question to graph nodes and routes it to the right traversal
|
|
5755
|
+
\u2014 root cause, dependencies, observed runtime calls, incidents, divergences,
|
|
5756
|
+
blast radius \u2014 and returns one compact, provenance-tagged answer. The same door
|
|
5757
|
+
is the \`ask\` MCP tool (\`mcp__neat__ask\`) when NEAT's MCP server is wired in.
|
|
5758
|
+
|
|
5759
|
+
The graph is live and fused: it is faster and more accurate than scanning files,
|
|
5760
|
+
and it can tell you what the system actually does at runtime, not only what the
|
|
5761
|
+
source declares. Fall back to Read/Grep only when the graph does not have what
|
|
5762
|
+
you need \u2014 comments, string literals, config minutiae. Ask the graph first,
|
|
5763
|
+
then scan.
|
|
5764
|
+
|
|
5765
|
+
If \`neat ask\` errors, the daemon may not be running (\`neat list\`) \u2014 start it
|
|
5766
|
+
with \`neat <path>\`, then re-ask.`;
|
|
5767
|
+
function neatSection() {
|
|
5768
|
+
return `${NEAT_SECTION_HEADING}
|
|
5769
|
+
|
|
5770
|
+
${NEAT_DIRECTIVE_BODY}
|
|
5771
|
+
`;
|
|
5772
|
+
}
|
|
5773
|
+
function claudeMdPath() {
|
|
5774
|
+
const override = process.env.NEAT_CLAUDE_MD;
|
|
5775
|
+
if (override && override.length > 0) return path13.resolve(override);
|
|
5776
|
+
return path13.join(process.cwd(), "CLAUDE.md");
|
|
5777
|
+
}
|
|
5778
|
+
function splitAroundSection(raw) {
|
|
5779
|
+
const lines = raw.split("\n");
|
|
5780
|
+
const startIdx = lines.findIndex((l) => l.replace(/\s+$/, "") === NEAT_SECTION_HEADING);
|
|
5781
|
+
if (startIdx === -1) {
|
|
5782
|
+
return { before: raw.replace(/\n*$/, ""), after: "", found: false };
|
|
5787
5783
|
}
|
|
5788
|
-
|
|
5789
|
-
|
|
5790
|
-
}
|
|
5791
|
-
|
|
5792
|
-
|
|
5793
|
-
|
|
5794
|
-
this.name = "TransportError";
|
|
5784
|
+
let endIdx = lines.length;
|
|
5785
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
5786
|
+
if (/^#{1,2}\s+/.test(lines[i] ?? "")) {
|
|
5787
|
+
endIdx = i;
|
|
5788
|
+
break;
|
|
5789
|
+
}
|
|
5795
5790
|
}
|
|
5796
|
-
|
|
5797
|
-
|
|
5798
|
-
|
|
5799
|
-
return t && t.length > 0 ? t : void 0;
|
|
5791
|
+
const before = lines.slice(0, startIdx).join("\n").replace(/\n*$/, "");
|
|
5792
|
+
const after = lines.slice(endIdx).join("\n").replace(/^\n*/, "");
|
|
5793
|
+
return { before, after, found: true };
|
|
5800
5794
|
}
|
|
5801
|
-
function
|
|
5802
|
-
const
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
try {
|
|
5808
|
-
res = await fetch(`${root}${path17}`, {
|
|
5809
|
-
headers: { ...authHeader }
|
|
5810
|
-
});
|
|
5811
|
-
} catch (err) {
|
|
5812
|
-
throw new TransportError(
|
|
5813
|
-
`cannot reach neat-core at ${root}: ${err.message}`
|
|
5814
|
-
);
|
|
5815
|
-
}
|
|
5816
|
-
if (!res.ok) {
|
|
5817
|
-
const body = await res.text().catch(() => "");
|
|
5818
|
-
throw new HttpError(
|
|
5819
|
-
res.status,
|
|
5820
|
-
`${res.status} ${res.statusText} on GET ${path17}: ${body}`,
|
|
5821
|
-
body
|
|
5822
|
-
);
|
|
5823
|
-
}
|
|
5824
|
-
return await res.json();
|
|
5825
|
-
},
|
|
5826
|
-
async post(path17, body) {
|
|
5827
|
-
let res;
|
|
5828
|
-
try {
|
|
5829
|
-
res = await fetch(`${root}${path17}`, {
|
|
5830
|
-
method: "POST",
|
|
5831
|
-
headers: { "content-type": "application/json", ...authHeader },
|
|
5832
|
-
body: JSON.stringify(body)
|
|
5833
|
-
});
|
|
5834
|
-
} catch (err) {
|
|
5835
|
-
throw new TransportError(
|
|
5836
|
-
`cannot reach neat-core at ${root}: ${err.message}`
|
|
5837
|
-
);
|
|
5838
|
-
}
|
|
5839
|
-
if (!res.ok) {
|
|
5840
|
-
const text = await res.text().catch(() => "");
|
|
5841
|
-
throw new HttpError(
|
|
5842
|
-
res.status,
|
|
5843
|
-
`${res.status} ${res.statusText} on POST ${path17}: ${text}`,
|
|
5844
|
-
text
|
|
5845
|
-
);
|
|
5846
|
-
}
|
|
5847
|
-
return await res.json();
|
|
5848
|
-
}
|
|
5849
|
-
};
|
|
5850
|
-
}
|
|
5851
|
-
function projectPath(project, suffix) {
|
|
5852
|
-
if (!project) return suffix;
|
|
5853
|
-
return `/projects/${encodeURIComponent(project)}${suffix}`;
|
|
5795
|
+
function compose(before, after) {
|
|
5796
|
+
const parts = [];
|
|
5797
|
+
if (before.length > 0) parts.push(before);
|
|
5798
|
+
parts.push(neatSection().replace(/\n+$/, ""));
|
|
5799
|
+
if (after.length > 0) parts.push(after);
|
|
5800
|
+
return parts.join("\n\n").replace(/\n*$/, "") + "\n";
|
|
5854
5801
|
}
|
|
5855
|
-
async function
|
|
5856
|
-
const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
|
|
5857
|
-
const path17 = projectPath(
|
|
5858
|
-
input.project,
|
|
5859
|
-
`/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
|
|
5860
|
-
);
|
|
5802
|
+
async function readIfExists(file) {
|
|
5861
5803
|
try {
|
|
5862
|
-
|
|
5863
|
-
const arrowPath = result.traversalPath.join(" \u2190 ");
|
|
5864
|
-
const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
|
|
5865
|
-
const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
|
|
5866
|
-
const blockLines = [
|
|
5867
|
-
`Traversal path: ${arrowPath}`,
|
|
5868
|
-
`Edge provenances: ${provenances}`
|
|
5869
|
-
];
|
|
5870
|
-
if (result.fixRecommendation) blockLines.push(`Recommended fix: ${result.fixRecommendation}`);
|
|
5871
|
-
return {
|
|
5872
|
-
summary,
|
|
5873
|
-
block: blockLines.join("\n"),
|
|
5874
|
-
confidence: result.confidence,
|
|
5875
|
-
provenance: result.edgeProvenances.length ? result.edgeProvenances : void 0
|
|
5876
|
-
};
|
|
5804
|
+
return await fs12.readFile(file, "utf8");
|
|
5877
5805
|
} catch (err) {
|
|
5878
|
-
if (err
|
|
5879
|
-
return {
|
|
5880
|
-
summary: `No root cause found for ${input.errorNode}. The node may be healthy, or it may not exist in the graph.`
|
|
5881
|
-
};
|
|
5882
|
-
}
|
|
5806
|
+
if (err.code === "ENOENT") return null;
|
|
5883
5807
|
throw err;
|
|
5884
5808
|
}
|
|
5885
5809
|
}
|
|
5886
|
-
async function
|
|
5887
|
-
const
|
|
5888
|
-
const
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
);
|
|
5892
|
-
|
|
5893
|
-
|
|
5894
|
-
|
|
5895
|
-
|
|
5896
|
-
|
|
5897
|
-
|
|
5898
|
-
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5902
|
-
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5907
|
-
|
|
5908
|
-
|
|
5909
|
-
|
|
5910
|
-
block: blockLines.join("\n"),
|
|
5911
|
-
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
5912
|
-
provenance: provenances.length ? provenances : void 0
|
|
5913
|
-
};
|
|
5914
|
-
} catch (err) {
|
|
5915
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
5916
|
-
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5917
|
-
}
|
|
5918
|
-
throw err;
|
|
5810
|
+
async function runInstall() {
|
|
5811
|
+
const file = claudeMdPath();
|
|
5812
|
+
const raw = await readIfExists(file) ?? "";
|
|
5813
|
+
const { before, after, found } = splitAroundSection(raw);
|
|
5814
|
+
const next = compose(before, after);
|
|
5815
|
+
await fs12.mkdir(path13.dirname(file), { recursive: true });
|
|
5816
|
+
await fs12.writeFile(file, next, "utf8");
|
|
5817
|
+
const verb = raw.length === 0 ? "created" : found ? "refreshed" : "added";
|
|
5818
|
+
console.log(`neat claude: ${verb} the \`${NEAT_SECTION_HEADING}\` section in ${file}`);
|
|
5819
|
+
console.log("Your agent will now reach for `neat ask` before Read/Grep/Bash. Restart the");
|
|
5820
|
+
console.log("session (or reload CLAUDE.md) to pick it up.");
|
|
5821
|
+
return { exitCode: 0 };
|
|
5822
|
+
}
|
|
5823
|
+
async function runUninstall() {
|
|
5824
|
+
const file = claudeMdPath();
|
|
5825
|
+
const raw = await readIfExists(file);
|
|
5826
|
+
if (raw === null) {
|
|
5827
|
+
console.log(`neat claude: no CLAUDE.md at ${file} \u2014 nothing to remove.`);
|
|
5828
|
+
return { exitCode: 0 };
|
|
5829
|
+
}
|
|
5830
|
+
const { before, after, found } = splitAroundSection(raw);
|
|
5831
|
+
if (!found) {
|
|
5832
|
+
console.log(`neat claude: no \`${NEAT_SECTION_HEADING}\` section in ${file} \u2014 nothing to remove.`);
|
|
5833
|
+
return { exitCode: 0 };
|
|
5919
5834
|
}
|
|
5835
|
+
const remaining = [before, after].filter((s) => s.length > 0).join("\n\n");
|
|
5836
|
+
const next = remaining.length > 0 ? remaining.replace(/\n*$/, "") + "\n" : "";
|
|
5837
|
+
await fs12.writeFile(file, next, "utf8");
|
|
5838
|
+
console.log(`neat claude: removed the \`${NEAT_SECTION_HEADING}\` section from ${file}.`);
|
|
5839
|
+
return { exitCode: 0 };
|
|
5920
5840
|
}
|
|
5921
|
-
function
|
|
5922
|
-
|
|
5923
|
-
|
|
5841
|
+
function usage2() {
|
|
5842
|
+
console.log("neat claude \u2014 make the query-first directive always-on in Claude Code");
|
|
5843
|
+
console.log("");
|
|
5844
|
+
console.log(" install write (or refresh) a `## neat` section in ./CLAUDE.md so your");
|
|
5845
|
+
console.log(" agent reaches for `neat ask` before Read/Grep/Bash");
|
|
5846
|
+
console.log(" uninstall remove the `## neat` section from ./CLAUDE.md");
|
|
5847
|
+
console.log(" print print the directive block to stdout (for a manual paste)");
|
|
5848
|
+
console.log("");
|
|
5849
|
+
console.log("Idempotent: re-running install replaces its own section, never duplicates it.");
|
|
5850
|
+
console.log("Target file overridable via NEAT_CLAUDE_MD.");
|
|
5924
5851
|
}
|
|
5925
|
-
async function
|
|
5926
|
-
const
|
|
5927
|
-
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5852
|
+
async function runClaudeCommand(args) {
|
|
5853
|
+
const sub = args[0];
|
|
5854
|
+
if (sub === "-h" || sub === "--help" || sub === void 0) {
|
|
5855
|
+
usage2();
|
|
5856
|
+
return sub === void 0 ? 2 : 0;
|
|
5857
|
+
}
|
|
5931
5858
|
try {
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
const blockLines = [];
|
|
5945
|
-
for (const distance of [...byDistance.keys()].sort((a, b) => a - b)) {
|
|
5946
|
-
const label = distance === 1 ? "Direct (distance 1)" : `Distance ${distance}`;
|
|
5947
|
-
blockLines.push(`${label}:`);
|
|
5948
|
-
for (const dep of byDistance.get(distance)) {
|
|
5949
|
-
blockLines.push(` \u2022 ${dep.nodeId} \u2014 ${dep.edgeType} (${dep.provenance})`);
|
|
5950
|
-
}
|
|
5859
|
+
switch (sub) {
|
|
5860
|
+
case "install":
|
|
5861
|
+
return (await runInstall()).exitCode;
|
|
5862
|
+
case "uninstall":
|
|
5863
|
+
return (await runUninstall()).exitCode;
|
|
5864
|
+
case "print":
|
|
5865
|
+
process.stdout.write(neatSection());
|
|
5866
|
+
return 0;
|
|
5867
|
+
default:
|
|
5868
|
+
console.error(`neat claude: unknown subcommand "${sub}"`);
|
|
5869
|
+
usage2();
|
|
5870
|
+
return 2;
|
|
5951
5871
|
}
|
|
5952
|
-
const provenances = [...new Set(result.dependencies.map((d) => d.provenance))];
|
|
5953
|
-
const directCount = byDistance.get(1)?.length ?? 0;
|
|
5954
|
-
const summary = depth === 1 ? `${input.nodeId} has ${directCount} direct dependenc${directCount === 1 ? "y" : "ies"}.` : `${input.nodeId} has ${result.total} dependenc${result.total === 1 ? "y" : "ies"} reachable to depth ${depth} (${directCount} direct).`;
|
|
5955
|
-
return { summary, block: blockLines.join("\n"), provenance: provenances };
|
|
5956
5872
|
} catch (err) {
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
}
|
|
5960
|
-
throw err;
|
|
5873
|
+
console.error(`neat claude: ${err.message}`);
|
|
5874
|
+
return 1;
|
|
5961
5875
|
}
|
|
5962
5876
|
}
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5877
|
+
|
|
5878
|
+
// src/codex-cli.ts
|
|
5879
|
+
import path14 from "path";
|
|
5880
|
+
import os2 from "os";
|
|
5881
|
+
import { promises as fs13 } from "fs";
|
|
5882
|
+
import { isDeepStrictEqual } from "util";
|
|
5883
|
+
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
5884
|
+
var CODEX_MCP_SERVER = {
|
|
5885
|
+
command: "npx",
|
|
5886
|
+
args: ["-y", "@neat.is/mcp"],
|
|
5887
|
+
env: { NEAT_CORE_URL: "http://localhost:8080" }
|
|
5888
|
+
};
|
|
5889
|
+
var CODEX_NEAT_BLOCK = [
|
|
5890
|
+
"[mcp_servers.neat]",
|
|
5891
|
+
'command = "npx"',
|
|
5892
|
+
'args = ["-y", "@neat.is/mcp"]',
|
|
5893
|
+
'env = { NEAT_CORE_URL = "http://localhost:8080" }'
|
|
5894
|
+
].join("\n");
|
|
5895
|
+
var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
|
|
5896
|
+
var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
|
|
5897
|
+
function codexConfigPath() {
|
|
5898
|
+
const override = process.env.NEAT_CODEX_CONFIG;
|
|
5899
|
+
if (override && override.length > 0) return path14.resolve(override);
|
|
5900
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? os2.homedir();
|
|
5901
|
+
return path14.join(home, ".codex", "config.toml");
|
|
5966
5902
|
}
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
input.project,
|
|
5972
|
-
`/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
|
|
5973
|
-
)
|
|
5974
|
-
);
|
|
5975
|
-
if (result.dependencies.length === 0) {
|
|
5976
|
-
if (result.observed) {
|
|
5977
|
-
return {
|
|
5978
|
-
summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
|
|
5979
|
-
provenance: Provenance2.OBSERVED
|
|
5980
|
-
};
|
|
5981
|
-
}
|
|
5982
|
-
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
5983
|
-
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
5984
|
-
}
|
|
5985
|
-
const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
|
|
5986
|
-
return {
|
|
5987
|
-
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
5988
|
-
block: blockLines.join("\n"),
|
|
5989
|
-
provenance: Provenance2.OBSERVED
|
|
5990
|
-
};
|
|
5991
|
-
} catch (err) {
|
|
5992
|
-
if (err instanceof HttpError && err.status === 404) {
|
|
5993
|
-
return { summary: `Node ${input.nodeId} not found in the graph.` };
|
|
5994
|
-
}
|
|
5995
|
-
throw err;
|
|
5996
|
-
}
|
|
5903
|
+
function agentsFilePath() {
|
|
5904
|
+
const override = process.env.NEAT_CODEX_AGENTS;
|
|
5905
|
+
if (override && override.length > 0) return path14.resolve(override);
|
|
5906
|
+
return path14.join(process.cwd(), "AGENTS.md");
|
|
5997
5907
|
}
|
|
5998
|
-
function
|
|
5999
|
-
|
|
6000
|
-
if (e.signal) {
|
|
6001
|
-
bits.push(`spans=${e.signal.spanCount}`);
|
|
6002
|
-
if (e.signal.errorCount > 0) bits.push(`errors=${e.signal.errorCount}`);
|
|
6003
|
-
if (e.signal.lastObservedAgeMs !== void 0) {
|
|
6004
|
-
bits.push(`age=${formatDuration(e.signal.lastObservedAgeMs)}`);
|
|
6005
|
-
}
|
|
6006
|
-
} else if (e.callCount !== void 0) {
|
|
6007
|
-
bits.push(`callCount=${e.callCount}`);
|
|
6008
|
-
}
|
|
6009
|
-
if (e.lastObserved) bits.push(`lastObserved=${e.lastObserved}`);
|
|
6010
|
-
if (e.confidence !== void 0) bits.push(`confidence=${e.confidence}`);
|
|
6011
|
-
return bits.length ? ` [${bits.join(", ")}]` : "";
|
|
5908
|
+
function isTableHeader(line) {
|
|
5909
|
+
return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
|
|
6012
5910
|
}
|
|
6013
|
-
function
|
|
6014
|
-
|
|
6015
|
-
const s = Math.round(ms / 1e3);
|
|
6016
|
-
if (s < 60) return `${s}s`;
|
|
6017
|
-
const m = Math.round(s / 60);
|
|
6018
|
-
if (m < 60) return `${m}m`;
|
|
6019
|
-
const h = Math.round(m / 60);
|
|
6020
|
-
if (h < 48) return `${h}h`;
|
|
6021
|
-
return `${Math.round(h / 24)}d`;
|
|
5911
|
+
function tableName(line) {
|
|
5912
|
+
return line.trim().replace(/^\[\[?/, "").replace(/\]\]?$/, "").trim();
|
|
6022
5913
|
}
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
6026
|
-
|
|
6027
|
-
|
|
6028
|
-
|
|
6029
|
-
|
|
6030
|
-
|
|
6031
|
-
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
6036
|
-
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
|
|
6043
|
-
provenance: Provenance2.OBSERVED
|
|
5914
|
+
function isNeatHeader(line) {
|
|
5915
|
+
return isTableHeader(line) && tableName(line) === "mcp_servers.neat";
|
|
5916
|
+
}
|
|
5917
|
+
function isNeatChildHeader(line) {
|
|
5918
|
+
if (!isTableHeader(line)) return false;
|
|
5919
|
+
const name = tableName(line);
|
|
5920
|
+
return name === "mcp_servers.neat" || name.startsWith("mcp_servers.neat.");
|
|
5921
|
+
}
|
|
5922
|
+
function upsertCodexConfig(raw) {
|
|
5923
|
+
const trimmed = raw.trim();
|
|
5924
|
+
const parsed = trimmed.length > 0 ? parseToml(raw) : {};
|
|
5925
|
+
const existingServers = parsed.mcp_servers ?? {};
|
|
5926
|
+
const spliced = spliceNeatBlock(raw);
|
|
5927
|
+
let text;
|
|
5928
|
+
if (verifyPreserved(raw, spliced, parsed)) {
|
|
5929
|
+
text = spliced;
|
|
5930
|
+
} else {
|
|
5931
|
+
const merged = {
|
|
5932
|
+
...parsed,
|
|
5933
|
+
mcp_servers: { ...existingServers, neat: CODEX_MCP_SERVER }
|
|
6044
5934
|
};
|
|
6045
|
-
|
|
6046
|
-
if (
|
|
6047
|
-
return { summary: `Node ${input.nodeId ?? ""} not found in the graph.` };
|
|
6048
|
-
}
|
|
6049
|
-
throw err;
|
|
5935
|
+
text = stringifyToml(merged);
|
|
5936
|
+
if (!text.endsWith("\n")) text += "\n";
|
|
6050
5937
|
}
|
|
5938
|
+
return { text, changed: text !== raw };
|
|
6051
5939
|
}
|
|
6052
|
-
|
|
6053
|
-
const
|
|
6054
|
-
|
|
6055
|
-
);
|
|
6056
|
-
if (
|
|
6057
|
-
|
|
5940
|
+
function spliceNeatBlock(raw) {
|
|
5941
|
+
const block = CODEX_NEAT_BLOCK;
|
|
5942
|
+
const lines = raw.length > 0 ? raw.split("\n") : [];
|
|
5943
|
+
const start = lines.findIndex(isNeatHeader);
|
|
5944
|
+
if (start === -1) {
|
|
5945
|
+
const base = raw.replace(/\n+$/, "");
|
|
5946
|
+
return base.length > 0 ? `${base}
|
|
5947
|
+
|
|
5948
|
+
${block}
|
|
5949
|
+
` : `${block}
|
|
5950
|
+
`;
|
|
6058
5951
|
}
|
|
6059
|
-
|
|
6060
|
-
|
|
6061
|
-
|
|
6062
|
-
|
|
6063
|
-
|
|
6064
|
-
|
|
6065
|
-
|
|
6066
|
-
|
|
6067
|
-
` \u2022 ${n.id} (${n.type}) \u2014 ${n.name ?? n.id}${scoreBit}`
|
|
6068
|
-
);
|
|
5952
|
+
let end = start + 1;
|
|
5953
|
+
for (; ; ) {
|
|
5954
|
+
while (end < lines.length && !isTableHeader(lines[end])) end++;
|
|
5955
|
+
if (end < lines.length && isNeatChildHeader(lines[end])) {
|
|
5956
|
+
end++;
|
|
5957
|
+
continue;
|
|
5958
|
+
}
|
|
5959
|
+
break;
|
|
6069
5960
|
}
|
|
6070
|
-
|
|
6071
|
-
|
|
6072
|
-
|
|
6073
|
-
|
|
6074
|
-
|
|
5961
|
+
const before = lines.slice(0, start).join("\n").replace(/\n*$/, "");
|
|
5962
|
+
const after = lines.slice(end).join("\n").replace(/^\n*/, "");
|
|
5963
|
+
let text = "";
|
|
5964
|
+
if (before.length > 0) text += `${before}
|
|
5965
|
+
|
|
5966
|
+
`;
|
|
5967
|
+
text += `${block}
|
|
5968
|
+
`;
|
|
5969
|
+
if (after.length > 0) text += `
|
|
5970
|
+
${after}`;
|
|
5971
|
+
return `${text.replace(/\n*$/, "")}
|
|
5972
|
+
`;
|
|
6075
5973
|
}
|
|
6076
|
-
|
|
6077
|
-
|
|
6078
|
-
|
|
6079
|
-
|
|
6080
|
-
|
|
6081
|
-
|
|
6082
|
-
);
|
|
6083
|
-
const total = result.added.nodes.length + result.added.edges.length + result.removed.nodes.length + result.removed.edges.length + result.changed.nodes.length + result.changed.edges.length;
|
|
6084
|
-
const baseLabel = result.base.exportedAt ?? "unknown";
|
|
6085
|
-
if (total === 0) {
|
|
6086
|
-
return {
|
|
6087
|
-
summary: `No differences between the current graph and ${input.againstSnapshot} (base exportedAt=${baseLabel}).`
|
|
6088
|
-
};
|
|
5974
|
+
function verifyPreserved(raw, spliced, original) {
|
|
5975
|
+
let next;
|
|
5976
|
+
try {
|
|
5977
|
+
next = parseToml(spliced);
|
|
5978
|
+
} catch {
|
|
5979
|
+
return false;
|
|
6089
5980
|
}
|
|
6090
|
-
const
|
|
6091
|
-
|
|
6092
|
-
|
|
6093
|
-
|
|
6094
|
-
|
|
6095
|
-
|
|
6096
|
-
blockLines.push("Added:");
|
|
6097
|
-
for (const n of result.added.nodes) blockLines.push(` + node ${n.id} (${n.type})`);
|
|
6098
|
-
for (const e of result.added.edges)
|
|
6099
|
-
blockLines.push(` + edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
6100
|
-
blockLines.push("");
|
|
5981
|
+
const origServers = original.mcp_servers ?? {};
|
|
5982
|
+
const nextServers = next.mcp_servers ?? {};
|
|
5983
|
+
if (!isDeepStrictEqual(nextServers.neat, CODEX_MCP_SERVER)) return false;
|
|
5984
|
+
for (const name of Object.keys(origServers)) {
|
|
5985
|
+
if (name === "neat") continue;
|
|
5986
|
+
if (!isDeepStrictEqual(nextServers[name], origServers[name])) return false;
|
|
6101
5987
|
}
|
|
6102
|
-
|
|
6103
|
-
|
|
6104
|
-
for (const n of result.removed.nodes) blockLines.push(` - node ${n.id} (${n.type})`);
|
|
6105
|
-
for (const e of result.removed.edges)
|
|
6106
|
-
blockLines.push(` - edge ${e.id} \u2014 ${e.source} -> ${e.target} (${e.type}, ${e.provenance})`);
|
|
6107
|
-
blockLines.push("");
|
|
5988
|
+
for (const name of Object.keys(nextServers)) {
|
|
5989
|
+
if (name !== "neat" && !(name in origServers)) return false;
|
|
6108
5990
|
}
|
|
6109
|
-
|
|
6110
|
-
|
|
6111
|
-
|
|
6112
|
-
blockLines.push(` ~ node ${c.id} \u2014 ${summariseAttrDiff(c.before, c.after)}`);
|
|
6113
|
-
}
|
|
6114
|
-
for (const c of result.changed.edges) {
|
|
6115
|
-
const provBit = c.before.provenance !== c.after.provenance ? `provenance ${c.before.provenance} \u2192 ${c.after.provenance}` : summariseAttrDiff(c.before, c.after);
|
|
6116
|
-
blockLines.push(` ~ edge ${c.id} \u2014 ${provBit}`);
|
|
6117
|
-
}
|
|
5991
|
+
for (const key of Object.keys(original)) {
|
|
5992
|
+
if (key === "mcp_servers") continue;
|
|
5993
|
+
if (!isDeepStrictEqual(next[key], original[key])) return false;
|
|
6118
5994
|
}
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
block: blockLines.join("\n").trimEnd()
|
|
6122
|
-
};
|
|
6123
|
-
}
|
|
6124
|
-
function summariseAttrDiff(before, after) {
|
|
6125
|
-
const keys = /* @__PURE__ */ new Set([...Object.keys(before), ...Object.keys(after)]);
|
|
6126
|
-
const changed = [];
|
|
6127
|
-
for (const k of keys) {
|
|
6128
|
-
if (JSON.stringify(before[k]) !== JSON.stringify(after[k])) changed.push(k);
|
|
5995
|
+
for (const key of Object.keys(next)) {
|
|
5996
|
+
if (key !== "mcp_servers" && !(key in original)) return false;
|
|
6129
5997
|
}
|
|
6130
|
-
return
|
|
5998
|
+
return true;
|
|
6131
5999
|
}
|
|
6132
|
-
|
|
6133
|
-
|
|
6134
|
-
|
|
6135
|
-
|
|
6136
|
-
|
|
6137
|
-
|
|
6138
|
-
|
|
6139
|
-
);
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
|
|
6144
|
-
|
|
6000
|
+
function agentsBlock(guide) {
|
|
6001
|
+
return `${NEAT_GRAPH_FIRST_START}
|
|
6002
|
+
${guide.replace(/\s+$/, "")}
|
|
6003
|
+
${NEAT_GRAPH_FIRST_END}
|
|
6004
|
+
`;
|
|
6005
|
+
}
|
|
6006
|
+
function upsertAgents(raw, guide) {
|
|
6007
|
+
const block = agentsBlock(guide);
|
|
6008
|
+
if (raw.length === 0) return { text: block, changed: true };
|
|
6009
|
+
const startIdx = raw.indexOf(NEAT_GRAPH_FIRST_START);
|
|
6010
|
+
const endIdx = raw.indexOf(NEAT_GRAPH_FIRST_END);
|
|
6011
|
+
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
|
6012
|
+
const before = raw.slice(0, startIdx);
|
|
6013
|
+
const after = raw.slice(endIdx + NEAT_GRAPH_FIRST_END.length);
|
|
6014
|
+
const text2 = `${before}${block.replace(/\n+$/, "")}${after}`;
|
|
6015
|
+
return { text: text2, changed: text2 !== raw };
|
|
6145
6016
|
}
|
|
6146
|
-
const
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
block: blockLines.join("\n"),
|
|
6152
|
-
provenance: Provenance2.STALE
|
|
6153
|
-
};
|
|
6017
|
+
const base = raw.replace(/\n+$/, "");
|
|
6018
|
+
const text = base.length > 0 ? `${base}
|
|
6019
|
+
|
|
6020
|
+
${block}` : block;
|
|
6021
|
+
return { text, changed: text !== raw };
|
|
6154
6022
|
}
|
|
6155
|
-
async function
|
|
6156
|
-
|
|
6157
|
-
|
|
6158
|
-
|
|
6159
|
-
if (
|
|
6160
|
-
|
|
6161
|
-
|
|
6162
|
-
}
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
|
|
6169
|
-
|
|
6170
|
-
|
|
6171
|
-
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6023
|
+
async function readGuide() {
|
|
6024
|
+
return readSkillAsset(GUIDE_FILENAME);
|
|
6025
|
+
}
|
|
6026
|
+
async function runCodex(opts) {
|
|
6027
|
+
if (opts.printConfig) {
|
|
6028
|
+
process.stdout.write(`${CODEX_NEAT_BLOCK}
|
|
6029
|
+
`);
|
|
6030
|
+
return { exitCode: 0 };
|
|
6031
|
+
}
|
|
6032
|
+
if (opts.printGuide) {
|
|
6033
|
+
process.stdout.write(agentsBlock(await readGuide()));
|
|
6034
|
+
return { exitCode: 0 };
|
|
6035
|
+
}
|
|
6036
|
+
const configPath = codexConfigPath();
|
|
6037
|
+
const agentsPath = agentsFilePath();
|
|
6038
|
+
let configRaw = "";
|
|
6039
|
+
try {
|
|
6040
|
+
configRaw = await fs13.readFile(configPath, "utf8");
|
|
6041
|
+
} catch (err) {
|
|
6042
|
+
if (err.code !== "ENOENT") {
|
|
6043
|
+
console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
|
|
6044
|
+
return { exitCode: 1 };
|
|
6045
|
+
}
|
|
6046
|
+
}
|
|
6047
|
+
let agentsRaw = "";
|
|
6048
|
+
try {
|
|
6049
|
+
agentsRaw = await fs13.readFile(agentsPath, "utf8");
|
|
6050
|
+
} catch (err) {
|
|
6051
|
+
if (err.code !== "ENOENT") {
|
|
6052
|
+
console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
|
|
6053
|
+
return { exitCode: 1 };
|
|
6054
|
+
}
|
|
6055
|
+
}
|
|
6056
|
+
let config;
|
|
6057
|
+
try {
|
|
6058
|
+
config = upsertCodexConfig(configRaw);
|
|
6059
|
+
} catch (err) {
|
|
6060
|
+
console.error(
|
|
6061
|
+
`neat codex: ${configPath} is not valid TOML \u2014 ${err.message}`
|
|
6176
6062
|
);
|
|
6177
|
-
|
|
6178
|
-
|
|
6063
|
+
console.error("neat codex: fix the file and re-run; nothing was written.");
|
|
6064
|
+
return { exitCode: 1 };
|
|
6179
6065
|
}
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6066
|
+
const guide = await readGuide();
|
|
6067
|
+
const agents = upsertAgents(agentsRaw, guide);
|
|
6068
|
+
if (!opts.apply) {
|
|
6069
|
+
console.log("neat codex \u2014 plan (nothing written; re-run with --apply to write)");
|
|
6070
|
+
console.log("");
|
|
6071
|
+
console.log(` Codex MCP config: ${configPath}`);
|
|
6072
|
+
console.log(
|
|
6073
|
+
config.changed ? ` ${configRaw ? "update" : "create"} the [mcp_servers.neat] table:` : " already up to date \u2014 [mcp_servers.neat] matches"
|
|
6074
|
+
);
|
|
6075
|
+
if (config.changed) {
|
|
6076
|
+
for (const line of CODEX_NEAT_BLOCK.split("\n")) console.log(` ${line}`);
|
|
6077
|
+
}
|
|
6078
|
+
console.log("");
|
|
6079
|
+
console.log(` Project instructions: ${agentsPath}`);
|
|
6080
|
+
console.log(
|
|
6081
|
+
agents.changed ? ` ${agentsRaw ? "update" : "create"} the graph-first block (between ${NEAT_GRAPH_FIRST_START} markers)` : " already up to date \u2014 graph-first block matches"
|
|
6183
6082
|
);
|
|
6083
|
+
console.log("");
|
|
6084
|
+
console.log("The MCP server reads NEAT_CORE_URL for the daemon URL \u2014 edit that value in");
|
|
6085
|
+
console.log("the generated table to point Codex at a non-default daemon.");
|
|
6086
|
+
return { exitCode: 0 };
|
|
6184
6087
|
}
|
|
6185
|
-
if (
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
};
|
|
6088
|
+
if (config.changed) {
|
|
6089
|
+
await fs13.mkdir(path14.dirname(configPath), { recursive: true });
|
|
6090
|
+
await fs13.writeFile(configPath, config.text, "utf8");
|
|
6091
|
+
console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
|
|
6092
|
+
} else {
|
|
6093
|
+
console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
|
|
6189
6094
|
}
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
|
|
6194
|
-
`Hypothetical ${hypothetical.kind} would surface ${violations.length} violation${violations.length === 1 ? "" : "s"}`
|
|
6195
|
-
);
|
|
6095
|
+
if (agents.changed) {
|
|
6096
|
+
await fs13.mkdir(path14.dirname(agentsPath), { recursive: true });
|
|
6097
|
+
await fs13.writeFile(agentsPath, agents.text, "utf8");
|
|
6098
|
+
console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
|
|
6196
6099
|
} else {
|
|
6197
|
-
|
|
6198
|
-
`${violations.length} policy violation${violations.length === 1 ? "" : "s"} currently recorded`
|
|
6199
|
-
);
|
|
6100
|
+
console.log(`neat codex: ${agentsPath} already has the graph-first block`);
|
|
6200
6101
|
}
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6204
|
-
|
|
6205
|
-
const subject = v.subject.nodeId ?? v.subject.edgeId ?? v.subject.path?.[0] ?? "(global)";
|
|
6206
|
-
return ` \u2022 [${v.severity}/${v.onViolation}] ${v.policyName}: ${v.message} \u2014 ${subject}`;
|
|
6207
|
-
});
|
|
6208
|
-
const severities = [...new Set(violations.map((v) => v.severity))];
|
|
6209
|
-
return {
|
|
6210
|
-
summary,
|
|
6211
|
-
block: blockLines.join("\n"),
|
|
6212
|
-
confidence: hypothetical ? 0.7 : 1,
|
|
6213
|
-
provenance: severities.join(" ")
|
|
6214
|
-
};
|
|
6102
|
+
console.log("");
|
|
6103
|
+
console.log("restart Codex to pick up the new MCP server. NEAT_CORE_URL in the table");
|
|
6104
|
+
console.log("points the server at the local daemon \u2014 edit it for a non-default one.");
|
|
6105
|
+
return { exitCode: 0 };
|
|
6215
6106
|
}
|
|
6216
|
-
function
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6107
|
+
function usage3() {
|
|
6108
|
+
console.log("neat codex \u2014 install NEAT into the OpenAI Codex CLI (MCP server + AGENTS.md)");
|
|
6109
|
+
console.log("");
|
|
6110
|
+
console.log(" (no flag) plan: print what would change, write nothing");
|
|
6111
|
+
console.log(" --apply add [mcp_servers.neat] to ~/.codex/config.toml and write");
|
|
6112
|
+
console.log(" the graph-first block into ./AGENTS.md, merging into both");
|
|
6113
|
+
console.log(" without touching your other servers or instructions");
|
|
6114
|
+
console.log(" --print-config print the [mcp_servers.neat] TOML block to stdout");
|
|
6115
|
+
console.log(" --print-guide print the AGENTS.md graph-first block to stdout");
|
|
6116
|
+
console.log("");
|
|
6117
|
+
console.log("Existing config is preserved and a re-run is a no-op. A malformed");
|
|
6118
|
+
console.log("config.toml is a clear error with no partial write.");
|
|
6119
|
+
}
|
|
6120
|
+
async function runCodexCommand(args) {
|
|
6121
|
+
const opts = { apply: false, printConfig: false, printGuide: false };
|
|
6122
|
+
for (const arg of args) {
|
|
6123
|
+
switch (arg) {
|
|
6124
|
+
case "--apply":
|
|
6125
|
+
opts.apply = true;
|
|
6126
|
+
break;
|
|
6127
|
+
case "--print-config":
|
|
6128
|
+
opts.printConfig = true;
|
|
6129
|
+
break;
|
|
6130
|
+
case "--print-guide":
|
|
6131
|
+
opts.printGuide = true;
|
|
6132
|
+
break;
|
|
6133
|
+
case "-h":
|
|
6134
|
+
case "--help":
|
|
6135
|
+
usage3();
|
|
6136
|
+
return 0;
|
|
6137
|
+
default:
|
|
6138
|
+
console.error(`neat codex: unknown flag "${arg}"`);
|
|
6139
|
+
usage3();
|
|
6140
|
+
return 2;
|
|
6141
|
+
}
|
|
6142
|
+
}
|
|
6143
|
+
try {
|
|
6144
|
+
const { exitCode } = await runCodex(opts);
|
|
6145
|
+
return exitCode;
|
|
6146
|
+
} catch (err) {
|
|
6147
|
+
console.error(err.message);
|
|
6148
|
+
return 1;
|
|
6149
|
+
}
|
|
6150
|
+
}
|
|
6151
|
+
|
|
6152
|
+
// src/editors-cli.ts
|
|
6153
|
+
import path15 from "path";
|
|
6154
|
+
import os3 from "os";
|
|
6155
|
+
import { promises as fs14 } from "fs";
|
|
6156
|
+
import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
|
|
6157
|
+
import * as jsonc from "jsonc-parser";
|
|
6158
|
+
var NEAT_MCP_SERVER = {
|
|
6159
|
+
command: "npx",
|
|
6160
|
+
args: ["-y", "@neat.is/mcp"]
|
|
6161
|
+
};
|
|
6162
|
+
var NEAT_OPENCODE_SERVER = {
|
|
6163
|
+
type: "local",
|
|
6164
|
+
command: ["npx", "-y", "@neat.is/mcp"],
|
|
6165
|
+
enabled: true
|
|
6166
|
+
};
|
|
6167
|
+
var NEAT_CRUSH_SERVER = {
|
|
6168
|
+
type: "stdio",
|
|
6169
|
+
command: "npx",
|
|
6170
|
+
args: ["-y", "@neat.is/mcp"]
|
|
6171
|
+
};
|
|
6172
|
+
var GRAPH_FIRST_MARKER_OPEN = "<!-- neat:graph-first -->";
|
|
6173
|
+
var GRAPH_FIRST_MARKER_CLOSE = "<!-- /neat:graph-first -->";
|
|
6174
|
+
function homeDir() {
|
|
6175
|
+
return process.env.HOME ?? process.env.USERPROFILE ?? os3.homedir();
|
|
6176
|
+
}
|
|
6177
|
+
function xdgConfigDir() {
|
|
6178
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
6179
|
+
return xdg && xdg.length > 0 ? path15.resolve(xdg) : path15.join(homeDir(), ".config");
|
|
6180
|
+
}
|
|
6181
|
+
function envOverride(name) {
|
|
6182
|
+
const v = process.env[name];
|
|
6183
|
+
return v && v.length > 0 ? path15.resolve(v) : void 0;
|
|
6184
|
+
}
|
|
6185
|
+
var CURSOR_CLIENT = {
|
|
6186
|
+
id: "cursor",
|
|
6187
|
+
label: "Cursor",
|
|
6188
|
+
docsUrl: "https://docs.cursor.com/context/mcp",
|
|
6189
|
+
mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? path15.join(homeDir(), ".cursor", "mcp.json"),
|
|
6190
|
+
mcpContainerKey: "mcpServers",
|
|
6191
|
+
format: "json",
|
|
6192
|
+
// Cursor still reads a single `.cursorrules` at the project root (the modern
|
|
6193
|
+
// `.cursor/rules/*.mdc` split is one-rule-per-file with frontmatter — a worse
|
|
6194
|
+
// fit for a marker-fenced block). GRAPH_FIRST.md names this file directly.
|
|
6195
|
+
rulesFileName: ".cursorrules"
|
|
6196
|
+
};
|
|
6197
|
+
var DEVIN_CLIENT = {
|
|
6198
|
+
id: "devin",
|
|
6199
|
+
label: "Devin Desktop (Cascade)",
|
|
6200
|
+
docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
|
|
6201
|
+
mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? path15.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
|
|
6202
|
+
mcpContainerKey: "mcpServers",
|
|
6203
|
+
format: "json",
|
|
6204
|
+
rulesFileName: ".windsurfrules"
|
|
6205
|
+
};
|
|
6206
|
+
var GEMINI_CLIENT = {
|
|
6207
|
+
id: "gemini",
|
|
6208
|
+
label: "Gemini CLI",
|
|
6209
|
+
docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
|
|
6210
|
+
mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? path15.join(homeDir(), ".gemini", "settings.json"),
|
|
6211
|
+
mcpContainerKey: "mcpServers",
|
|
6212
|
+
format: "json",
|
|
6213
|
+
rulesFileName: "GEMINI.md"
|
|
6214
|
+
};
|
|
6215
|
+
var QWEN_CLIENT = {
|
|
6216
|
+
id: "qwen",
|
|
6217
|
+
label: "Qwen Code",
|
|
6218
|
+
docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
|
|
6219
|
+
mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? path15.join(homeDir(), ".qwen", "settings.json"),
|
|
6220
|
+
mcpContainerKey: "mcpServers",
|
|
6221
|
+
format: "json",
|
|
6222
|
+
rulesFileName: "QWEN.md"
|
|
6223
|
+
};
|
|
6224
|
+
var AMAZONQ_CLIENT = {
|
|
6225
|
+
id: "amazonq",
|
|
6226
|
+
label: "Amazon Q Developer CLI",
|
|
6227
|
+
docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
|
|
6228
|
+
mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? path15.join(homeDir(), ".aws", "amazonq", "mcp.json"),
|
|
6229
|
+
mcpContainerKey: "mcpServers",
|
|
6230
|
+
format: "json"
|
|
6231
|
+
};
|
|
6232
|
+
var ROOCODE_CLIENT = {
|
|
6233
|
+
id: "roocode",
|
|
6234
|
+
label: "Roo Code",
|
|
6235
|
+
docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
|
|
6236
|
+
mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? path15.join(process.cwd(), ".roo", "mcp.json"),
|
|
6237
|
+
mcpContainerKey: "mcpServers",
|
|
6238
|
+
format: "json"
|
|
6239
|
+
};
|
|
6240
|
+
var ZED_CLIENT = {
|
|
6241
|
+
id: "zed",
|
|
6242
|
+
label: "Zed",
|
|
6243
|
+
docsUrl: "https://zed.dev/docs/ai/mcp",
|
|
6244
|
+
mcpConfigPath: () => {
|
|
6245
|
+
const override = envOverride("NEAT_ZED_CONFIG");
|
|
6246
|
+
if (override) return override;
|
|
6247
|
+
if (process.platform === "win32") {
|
|
6248
|
+
const appData = process.env.APPDATA;
|
|
6249
|
+
if (appData && appData.length > 0) return path15.join(appData, "Zed", "settings.json");
|
|
6234
6250
|
}
|
|
6251
|
+
return path15.join(homeDir(), ".config", "zed", "settings.json");
|
|
6252
|
+
},
|
|
6253
|
+
mcpContainerKey: "context_servers",
|
|
6254
|
+
format: "jsonc",
|
|
6255
|
+
rulesFileName: ".rules"
|
|
6256
|
+
};
|
|
6257
|
+
var OPENCODE_CLIENT = {
|
|
6258
|
+
id: "opencode",
|
|
6259
|
+
label: "OpenCode",
|
|
6260
|
+
docsUrl: "https://opencode.ai/docs/mcp-servers/",
|
|
6261
|
+
mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? path15.join(xdgConfigDir(), "opencode", "opencode.json"),
|
|
6262
|
+
mcpContainerKey: "mcp",
|
|
6263
|
+
format: "json",
|
|
6264
|
+
serverEntry: NEAT_OPENCODE_SERVER,
|
|
6265
|
+
rulesFileName: "AGENTS.md"
|
|
6266
|
+
};
|
|
6267
|
+
var CRUSH_CLIENT = {
|
|
6268
|
+
id: "crush",
|
|
6269
|
+
label: "Crush",
|
|
6270
|
+
docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
|
|
6271
|
+
mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? path15.join(xdgConfigDir(), "crush", "crush.json"),
|
|
6272
|
+
mcpContainerKey: "mcp",
|
|
6273
|
+
format: "json",
|
|
6274
|
+
serverEntry: NEAT_CRUSH_SERVER,
|
|
6275
|
+
rulesFileName: "AGENTS.md"
|
|
6276
|
+
};
|
|
6277
|
+
var CLIENTS = {
|
|
6278
|
+
cursor: CURSOR_CLIENT,
|
|
6279
|
+
devin: DEVIN_CLIENT,
|
|
6280
|
+
gemini: GEMINI_CLIENT,
|
|
6281
|
+
qwen: QWEN_CLIENT,
|
|
6282
|
+
amazonq: AMAZONQ_CLIENT,
|
|
6283
|
+
roocode: ROOCODE_CLIENT,
|
|
6284
|
+
zed: ZED_CLIENT,
|
|
6285
|
+
opencode: OPENCODE_CLIENT,
|
|
6286
|
+
crush: CRUSH_CLIENT
|
|
6287
|
+
};
|
|
6288
|
+
function mergeJsonMcp(existing, containerKey, serverEntry) {
|
|
6289
|
+
const servers = existing[containerKey] ?? {};
|
|
6290
|
+
const already = isDeepStrictEqual2(servers.neat, serverEntry);
|
|
6291
|
+
const merged = {
|
|
6292
|
+
...existing,
|
|
6293
|
+
[containerKey]: { ...servers, neat: serverEntry }
|
|
6294
|
+
};
|
|
6295
|
+
return { merged, changed: !already };
|
|
6296
|
+
}
|
|
6297
|
+
function mergeJsoncMcp(raw, containerKey, serverEntry) {
|
|
6298
|
+
const base = raw.trim().length > 0 ? raw : "{}";
|
|
6299
|
+
const parsed = jsonc.parse(base) ?? {};
|
|
6300
|
+
const servers = parsed[containerKey] ?? {};
|
|
6301
|
+
if (isDeepStrictEqual2(servers.neat, serverEntry)) {
|
|
6302
|
+
return { text: raw, changed: false };
|
|
6235
6303
|
}
|
|
6304
|
+
const edits = jsonc.modify(base, [containerKey, "neat"], serverEntry, {
|
|
6305
|
+
formattingOptions: { tabSize: 2, insertSpaces: true }
|
|
6306
|
+
});
|
|
6307
|
+
let text = jsonc.applyEdits(base, edits);
|
|
6308
|
+
if (!text.endsWith("\n")) text += "\n";
|
|
6309
|
+
return { text, changed: text !== raw };
|
|
6236
6310
|
}
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6311
|
+
function escapeRegExp(s) {
|
|
6312
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6313
|
+
}
|
|
6314
|
+
function buildGuidanceBlock(guide) {
|
|
6315
|
+
return `${GRAPH_FIRST_MARKER_OPEN}
|
|
6316
|
+
${guide.trim()}
|
|
6317
|
+
${GRAPH_FIRST_MARKER_CLOSE}
|
|
6318
|
+
`;
|
|
6319
|
+
}
|
|
6320
|
+
function mergeRulesFile(existing, block) {
|
|
6321
|
+
const region = new RegExp(
|
|
6322
|
+
`${escapeRegExp(GRAPH_FIRST_MARKER_OPEN)}[\\s\\S]*?${escapeRegExp(GRAPH_FIRST_MARKER_CLOSE)}\\n?`
|
|
6247
6323
|
);
|
|
6248
|
-
if (
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6324
|
+
if (region.test(existing)) return existing.replace(region, block);
|
|
6325
|
+
if (existing.trim().length === 0) return block;
|
|
6326
|
+
return `${existing.replace(/\s+$/, "")}
|
|
6327
|
+
|
|
6328
|
+
${block}`;
|
|
6329
|
+
}
|
|
6330
|
+
async function planMcp(client, mcpPath) {
|
|
6331
|
+
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
6332
|
+
let raw = "";
|
|
6333
|
+
try {
|
|
6334
|
+
raw = await fs14.readFile(mcpPath, "utf8");
|
|
6335
|
+
} catch (err) {
|
|
6336
|
+
const e = err;
|
|
6337
|
+
if (e.code === "ENOENT") {
|
|
6338
|
+
raw = "";
|
|
6339
|
+
} else {
|
|
6340
|
+
console.error(`neat ${client.id}: failed to read ${mcpPath} \u2014 ${e.message}`);
|
|
6341
|
+
return null;
|
|
6342
|
+
}
|
|
6252
6343
|
}
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6344
|
+
if (client.format === "jsonc") {
|
|
6345
|
+
if (raw.trim().length > 0) {
|
|
6346
|
+
const errors = [];
|
|
6347
|
+
jsonc.parse(raw, errors, { allowTrailingComma: true });
|
|
6348
|
+
if (errors.length > 0) {
|
|
6349
|
+
const first = errors[0];
|
|
6350
|
+
console.error(
|
|
6351
|
+
`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.`
|
|
6352
|
+
);
|
|
6353
|
+
return null;
|
|
6354
|
+
}
|
|
6355
|
+
}
|
|
6356
|
+
return mergeJsoncMcp(raw, client.mcpContainerKey, serverEntry);
|
|
6260
6357
|
}
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6358
|
+
let existing = {};
|
|
6359
|
+
if (raw.trim().length > 0) {
|
|
6360
|
+
try {
|
|
6361
|
+
existing = JSON.parse(raw);
|
|
6362
|
+
} catch (err) {
|
|
6363
|
+
console.error(
|
|
6364
|
+
`neat ${client.id}: ${mcpPath} is not valid JSON \u2014 ${err.message}. Fix it (or move it aside) and re-run; nothing was written.`
|
|
6365
|
+
);
|
|
6366
|
+
return null;
|
|
6367
|
+
}
|
|
6368
|
+
}
|
|
6369
|
+
const { merged, changed } = mergeJsonMcp(existing, client.mcpContainerKey, serverEntry);
|
|
6370
|
+
return { text: JSON.stringify(merged, null, 2) + "\n", changed };
|
|
6271
6371
|
}
|
|
6272
|
-
async function
|
|
6273
|
-
const
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
const
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6372
|
+
async function runEditorInstall(client, opts) {
|
|
6373
|
+
const mcpPath = client.mcpConfigPath();
|
|
6374
|
+
const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
|
|
6375
|
+
const hasRules = typeof client.rulesFileName === "string";
|
|
6376
|
+
const rulesPath = hasRules ? path15.join(opts.projectDir, client.rulesFileName) : "";
|
|
6377
|
+
const mcp = await planMcp(client, mcpPath);
|
|
6378
|
+
if (mcp === null) return { exitCode: 1 };
|
|
6379
|
+
let existingRules = "";
|
|
6380
|
+
let newRules = "";
|
|
6381
|
+
let rulesChanged = false;
|
|
6382
|
+
let block = "";
|
|
6383
|
+
if (hasRules) {
|
|
6384
|
+
try {
|
|
6385
|
+
existingRules = await fs14.readFile(rulesPath, "utf8");
|
|
6386
|
+
} catch (err) {
|
|
6387
|
+
if (err.code !== "ENOENT") {
|
|
6388
|
+
console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
|
|
6389
|
+
return { exitCode: 1 };
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
const guide = await readSkillAsset(GUIDE_FILENAME);
|
|
6393
|
+
block = buildGuidanceBlock(guide);
|
|
6394
|
+
newRules = mergeRulesFile(existingRules, block);
|
|
6395
|
+
rulesChanged = newRules !== existingRules;
|
|
6284
6396
|
}
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6397
|
+
if (!opts.apply) {
|
|
6398
|
+
console.log(`neat ${client.id} \u2014 wire NEAT into ${client.label} (plan; nothing written)`);
|
|
6399
|
+
console.log("");
|
|
6400
|
+
console.log(`MCP server \u2192 ${mcpPath}`);
|
|
6401
|
+
console.log(
|
|
6402
|
+
mcp.changed ? ` would add ${client.mcpContainerKey}.neat:` : ` ${client.mcpContainerKey}.neat already present and current \u2014 no change:`
|
|
6403
|
+
);
|
|
6404
|
+
console.log(indent(JSON.stringify({ neat: serverEntry }, null, 2)));
|
|
6405
|
+
if (hasRules) {
|
|
6406
|
+
console.log("");
|
|
6407
|
+
console.log(`Graph-first guidance \u2192 ${rulesPath}`);
|
|
6408
|
+
console.log(
|
|
6409
|
+
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."
|
|
6410
|
+
);
|
|
6411
|
+
if (rulesChanged) console.log(indent(block.trimEnd()));
|
|
6290
6412
|
}
|
|
6413
|
+
console.log("");
|
|
6414
|
+
console.log(
|
|
6415
|
+
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.`
|
|
6416
|
+
);
|
|
6417
|
+
return { exitCode: 0 };
|
|
6291
6418
|
}
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
}
|
|
6298
|
-
}
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6419
|
+
await fs14.mkdir(path15.dirname(mcpPath), { recursive: true });
|
|
6420
|
+
await fs14.writeFile(mcpPath, mcp.text, "utf8");
|
|
6421
|
+
if (hasRules) {
|
|
6422
|
+
await fs14.mkdir(path15.dirname(rulesPath), { recursive: true });
|
|
6423
|
+
await fs14.writeFile(rulesPath, newRules, "utf8");
|
|
6424
|
+
}
|
|
6425
|
+
console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
|
|
6426
|
+
console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
|
|
6427
|
+
if (hasRules) console.log(` guidance: ${rulesPath} (neat:graph-first block)`);
|
|
6428
|
+
console.log("");
|
|
6429
|
+
console.log(`restart ${client.label} to pick up the MCP server. Point it at a non-default`);
|
|
6430
|
+
console.log(`daemon by setting NEAT_CORE_URL in the neat server's env in that config.`);
|
|
6431
|
+
return { exitCode: 0 };
|
|
6303
6432
|
}
|
|
6304
|
-
function
|
|
6305
|
-
|
|
6306
|
-
if (result.block && result.block.trim().length > 0) sections.push(result.block.trimEnd());
|
|
6307
|
-
sections.push(formatFooter(result.confidence, result.provenance));
|
|
6308
|
-
return sections.join("\n\n");
|
|
6433
|
+
function indent(text) {
|
|
6434
|
+
return text.split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n");
|
|
6309
6435
|
}
|
|
6310
|
-
function
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
block: result.block ?? "",
|
|
6315
|
-
confidence: result.confidence ?? null,
|
|
6316
|
-
provenance: result.provenance ?? null
|
|
6317
|
-
},
|
|
6318
|
-
null,
|
|
6319
|
-
2
|
|
6436
|
+
function usage4(client) {
|
|
6437
|
+
const hasRules = typeof client.rulesFileName === "string";
|
|
6438
|
+
console.log(
|
|
6439
|
+
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}`
|
|
6320
6440
|
);
|
|
6441
|
+
console.log("");
|
|
6442
|
+
console.log(
|
|
6443
|
+
hasRules ? " --apply write the MCP config and the rules file (default: plan only)" : " --apply write the MCP config (default: plan only)"
|
|
6444
|
+
);
|
|
6445
|
+
console.log("");
|
|
6446
|
+
console.log("Writes NEAT's stdio MCP server (npx -y @neat.is/mcp) into");
|
|
6447
|
+
console.log(` ${client.mcpConfigPath()}`);
|
|
6448
|
+
if (hasRules) {
|
|
6449
|
+
console.log(`and the graph-first guidance block into ./${client.rulesFileName}, both`);
|
|
6450
|
+
console.log("additively \u2014 existing servers and rules are preserved, a re-run is a no-op.");
|
|
6451
|
+
} else {
|
|
6452
|
+
console.log("additively \u2014 existing servers are preserved, a re-run is a no-op.");
|
|
6453
|
+
}
|
|
6454
|
+
console.log("");
|
|
6455
|
+
console.log(`See ${client.docsUrl} for ${client.label}'s MCP config format.`);
|
|
6321
6456
|
}
|
|
6322
|
-
function
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6457
|
+
async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
|
|
6458
|
+
const client = CLIENTS[clientId];
|
|
6459
|
+
let apply6 = false;
|
|
6460
|
+
for (const arg of args) {
|
|
6461
|
+
switch (arg) {
|
|
6462
|
+
case "--apply":
|
|
6463
|
+
apply6 = true;
|
|
6464
|
+
break;
|
|
6465
|
+
case "-h":
|
|
6466
|
+
case "--help":
|
|
6467
|
+
usage4(client);
|
|
6468
|
+
return 0;
|
|
6469
|
+
default:
|
|
6470
|
+
console.error(`neat ${client.id}: unknown flag "${arg}"`);
|
|
6471
|
+
usage4(client);
|
|
6472
|
+
return 2;
|
|
6473
|
+
}
|
|
6474
|
+
}
|
|
6475
|
+
try {
|
|
6476
|
+
const { exitCode } = await runEditorInstall(client, { apply: apply6, projectDir });
|
|
6477
|
+
return exitCode;
|
|
6478
|
+
} catch (err) {
|
|
6479
|
+
console.error(err.message);
|
|
6480
|
+
return 1;
|
|
6334
6481
|
}
|
|
6335
|
-
return client.post(
|
|
6336
|
-
`/projects/${encodeURIComponent(input.project)}/snapshot`,
|
|
6337
|
-
{ snapshot: input.snapshot }
|
|
6338
|
-
);
|
|
6339
6482
|
}
|
|
6340
6483
|
|
|
6341
6484
|
// src/monitor.ts
|
|
6485
|
+
import { EdgeType, parseEdgeId, Provenance as Provenance3 } from "@neat.is/types";
|
|
6342
6486
|
var OBSERVED_DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
6343
6487
|
EdgeType.CALLS,
|
|
6344
6488
|
EdgeType.CONNECTS_TO,
|
|
@@ -6713,7 +6857,7 @@ function sleep(ms, signal) {
|
|
|
6713
6857
|
}
|
|
6714
6858
|
|
|
6715
6859
|
// src/cli-verbs.ts
|
|
6716
|
-
import
|
|
6860
|
+
import path16 from "path";
|
|
6717
6861
|
async function resolveProjectEntry(opts) {
|
|
6718
6862
|
const entries = await listProjects();
|
|
6719
6863
|
if (opts.project) {
|
|
@@ -6723,7 +6867,7 @@ async function resolveProjectEntry(opts) {
|
|
|
6723
6867
|
const cwd = opts.cwd ?? process.cwd();
|
|
6724
6868
|
const resolvedCwd = await normalizeProjectPath(cwd);
|
|
6725
6869
|
for (const entry2 of entries) {
|
|
6726
|
-
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${
|
|
6870
|
+
if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${path16.sep}`)) {
|
|
6727
6871
|
return entry2;
|
|
6728
6872
|
}
|
|
6729
6873
|
}
|
|
@@ -7025,6 +7169,9 @@ function usage5() {
|
|
|
7025
7169
|
console.log(" test <id> re-check an existing connector's credential");
|
|
7026
7170
|
console.log(" Credentials default to an env-var reference ($VAR) resolved at");
|
|
7027
7171
|
console.log(" run time; the config file is written owner-only (0600).");
|
|
7172
|
+
console.log(" doctor Preflight this directory's setup \u2014 Node version, project,");
|
|
7173
|
+
console.log(" and daemon reachability \u2014 and print a fix for anything down.");
|
|
7174
|
+
console.log(" Flags: --json. Exits 0 when all pass, 1 when a check fails.");
|
|
7028
7175
|
console.log("");
|
|
7029
7176
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
7030
7177
|
console.log(" ask <question> Plain-language door: resolves the question to");
|
|
@@ -7250,7 +7397,7 @@ async function buildPatchSections(services, project) {
|
|
|
7250
7397
|
}
|
|
7251
7398
|
async function runInit(opts) {
|
|
7252
7399
|
const written = [];
|
|
7253
|
-
const stat = await
|
|
7400
|
+
const stat = await fs15.stat(opts.scanPath).catch(() => null);
|
|
7254
7401
|
if (!stat || !stat.isDirectory()) {
|
|
7255
7402
|
console.error(`neat init: ${opts.scanPath} is not a directory`);
|
|
7256
7403
|
return { exitCode: 2, writtenFiles: written };
|
|
@@ -7259,13 +7406,13 @@ async function runInit(opts) {
|
|
|
7259
7406
|
printDiscoveryReport(opts, services);
|
|
7260
7407
|
const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
|
|
7261
7408
|
const patch = renderPatch(sections);
|
|
7262
|
-
const patchPath =
|
|
7409
|
+
const patchPath = path17.join(opts.scanPath, "neat.patch");
|
|
7263
7410
|
if (opts.dryRun) {
|
|
7264
|
-
await
|
|
7411
|
+
await fs15.writeFile(patchPath, patch, "utf8");
|
|
7265
7412
|
written.push(patchPath);
|
|
7266
7413
|
console.log(`dry-run: patch written to ${patchPath}`);
|
|
7267
|
-
const gitignorePath =
|
|
7268
|
-
const gitignoreExists = await
|
|
7414
|
+
const gitignorePath = path17.join(opts.scanPath, ".gitignore");
|
|
7415
|
+
const gitignoreExists = await fs15.stat(gitignorePath).then(() => true).catch(() => false);
|
|
7269
7416
|
const verb = gitignoreExists ? "append" : "create";
|
|
7270
7417
|
console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
|
|
7271
7418
|
console.log("rerun without --dry-run to register and snapshot.");
|
|
@@ -7276,9 +7423,9 @@ async function runInit(opts) {
|
|
|
7276
7423
|
const graph = getGraph(graphKey);
|
|
7277
7424
|
const projectPaths = pathsForProject(
|
|
7278
7425
|
graphKey,
|
|
7279
|
-
|
|
7426
|
+
path17.join(opts.scanPath, "neat-out")
|
|
7280
7427
|
);
|
|
7281
|
-
const errorsPath =
|
|
7428
|
+
const errorsPath = path17.join(path17.dirname(opts.outPath), path17.basename(projectPaths.errorsPath));
|
|
7282
7429
|
const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
|
|
7283
7430
|
await saveGraphToDisk(graph, opts.outPath);
|
|
7284
7431
|
written.push(opts.outPath);
|
|
@@ -7357,7 +7504,7 @@ async function runInit(opts) {
|
|
|
7357
7504
|
console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
|
|
7358
7505
|
}
|
|
7359
7506
|
} else {
|
|
7360
|
-
await
|
|
7507
|
+
await fs15.writeFile(patchPath, patch, "utf8");
|
|
7361
7508
|
written.push(patchPath);
|
|
7362
7509
|
}
|
|
7363
7510
|
}
|
|
@@ -7398,9 +7545,9 @@ var CLAUDE_SKILL_CONFIG = {
|
|
|
7398
7545
|
};
|
|
7399
7546
|
function claudeConfigPath() {
|
|
7400
7547
|
const override = process.env.NEAT_CLAUDE_CONFIG;
|
|
7401
|
-
if (override && override.length > 0) return
|
|
7548
|
+
if (override && override.length > 0) return path17.resolve(override);
|
|
7402
7549
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
7403
|
-
return
|
|
7550
|
+
return path17.join(home, ".claude.json");
|
|
7404
7551
|
}
|
|
7405
7552
|
async function runSkill(opts) {
|
|
7406
7553
|
const snippet = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
|
|
@@ -7412,7 +7559,7 @@ async function runSkill(opts) {
|
|
|
7412
7559
|
const target = claudeConfigPath();
|
|
7413
7560
|
let existing = {};
|
|
7414
7561
|
try {
|
|
7415
|
-
existing = JSON.parse(await
|
|
7562
|
+
existing = JSON.parse(await fs15.readFile(target, "utf8"));
|
|
7416
7563
|
} catch (err) {
|
|
7417
7564
|
if (err.code !== "ENOENT") {
|
|
7418
7565
|
console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
|
|
@@ -7424,8 +7571,8 @@ async function runSkill(opts) {
|
|
|
7424
7571
|
...existing,
|
|
7425
7572
|
mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
|
|
7426
7573
|
};
|
|
7427
|
-
await
|
|
7428
|
-
await
|
|
7574
|
+
await fs15.mkdir(path17.dirname(target), { recursive: true });
|
|
7575
|
+
await fs15.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
|
|
7429
7576
|
console.log(`neat skill: wrote mcpServers.neat to ${target}`);
|
|
7430
7577
|
console.log("restart Claude Code to pick up the new MCP server.");
|
|
7431
7578
|
console.log("");
|
|
@@ -7464,6 +7611,11 @@ async function main() {
|
|
|
7464
7611
|
if (code !== 0) process.exit(code);
|
|
7465
7612
|
return;
|
|
7466
7613
|
}
|
|
7614
|
+
if (cmd0 === "doctor") {
|
|
7615
|
+
const code = await runDoctorCommand(argv.slice(1));
|
|
7616
|
+
if (code !== 0) process.exit(code);
|
|
7617
|
+
return;
|
|
7618
|
+
}
|
|
7467
7619
|
if (cmd0 === "hooks") {
|
|
7468
7620
|
const code = await runHooksCommand(argv.slice(1));
|
|
7469
7621
|
if (code !== 0) process.exit(code);
|
|
@@ -7516,12 +7668,12 @@ async function main() {
|
|
|
7516
7668
|
console.error("neat init: --apply and --dry-run are mutually exclusive");
|
|
7517
7669
|
process.exit(2);
|
|
7518
7670
|
}
|
|
7519
|
-
const scanPath =
|
|
7671
|
+
const scanPath = path17.resolve(target);
|
|
7520
7672
|
const projectExplicit = parsed.project !== null;
|
|
7521
|
-
const projectName = projectExplicit ? project :
|
|
7673
|
+
const projectName = projectExplicit ? project : path17.basename(scanPath);
|
|
7522
7674
|
const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
|
|
7523
|
-
const fallback = pathsForProject(projectKey,
|
|
7524
|
-
const outPath =
|
|
7675
|
+
const fallback = pathsForProject(projectKey, path17.join(scanPath, "neat-out")).snapshotPath;
|
|
7676
|
+
const outPath = path17.resolve(process.env.NEAT_OUT_PATH ?? fallback);
|
|
7525
7677
|
const result = await runInit({
|
|
7526
7678
|
scanPath,
|
|
7527
7679
|
outPath,
|
|
@@ -7542,21 +7694,21 @@ async function main() {
|
|
|
7542
7694
|
usage5();
|
|
7543
7695
|
process.exit(2);
|
|
7544
7696
|
}
|
|
7545
|
-
const scanPath =
|
|
7546
|
-
const stat = await
|
|
7697
|
+
const scanPath = path17.resolve(target);
|
|
7698
|
+
const stat = await fs15.stat(scanPath).catch(() => null);
|
|
7547
7699
|
if (!stat || !stat.isDirectory()) {
|
|
7548
7700
|
console.error(`neat watch: ${scanPath} is not a directory`);
|
|
7549
7701
|
process.exit(2);
|
|
7550
7702
|
}
|
|
7551
|
-
const projectPaths = pathsForProject(project,
|
|
7552
|
-
const outPath =
|
|
7553
|
-
const errorsPath =
|
|
7554
|
-
process.env.NEAT_ERRORS_PATH ??
|
|
7703
|
+
const projectPaths = pathsForProject(project, path17.join(scanPath, "neat-out"));
|
|
7704
|
+
const outPath = path17.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
|
|
7705
|
+
const errorsPath = path17.resolve(
|
|
7706
|
+
process.env.NEAT_ERRORS_PATH ?? path17.join(path17.dirname(outPath), path17.basename(projectPaths.errorsPath))
|
|
7555
7707
|
);
|
|
7556
|
-
const staleEventsPath =
|
|
7557
|
-
process.env.NEAT_STALE_EVENTS_PATH ??
|
|
7708
|
+
const staleEventsPath = path17.resolve(
|
|
7709
|
+
process.env.NEAT_STALE_EVENTS_PATH ?? path17.join(path17.dirname(outPath), path17.basename(projectPaths.staleEventsPath))
|
|
7558
7710
|
);
|
|
7559
|
-
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ?
|
|
7711
|
+
const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? path17.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
|
|
7560
7712
|
const handle = await startWatch(getGraph(project), {
|
|
7561
7713
|
scanPath,
|
|
7562
7714
|
outPath,
|
|
@@ -7565,7 +7717,7 @@ async function main() {
|
|
|
7565
7717
|
project,
|
|
7566
7718
|
// Resolve NEAT_HOME so a `neat watch` picks up connectors added to
|
|
7567
7719
|
// ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
|
|
7568
|
-
neatHome: process.env.NEAT_HOME ?
|
|
7720
|
+
neatHome: process.env.NEAT_HOME ? path17.resolve(process.env.NEAT_HOME) : path17.join(os4.homedir(), ".neat"),
|
|
7569
7721
|
...embeddingsCachePath ? { embeddingsCachePath } : {},
|
|
7570
7722
|
host: process.env.HOST ?? "0.0.0.0",
|
|
7571
7723
|
port: Number(process.env.PORT ?? 8080),
|
|
@@ -7747,11 +7899,11 @@ async function main() {
|
|
|
7747
7899
|
process.exit(1);
|
|
7748
7900
|
}
|
|
7749
7901
|
async function tryOrchestrator(cmd, parsed) {
|
|
7750
|
-
const scanPath =
|
|
7751
|
-
const stat = await
|
|
7902
|
+
const scanPath = path17.resolve(cmd);
|
|
7903
|
+
const stat = await fs15.stat(scanPath).catch(() => null);
|
|
7752
7904
|
if (!stat || !stat.isDirectory()) return null;
|
|
7753
7905
|
const projectExplicit = parsed.project !== null;
|
|
7754
|
-
const projectName = projectExplicit ? parsed.project :
|
|
7906
|
+
const projectName = projectExplicit ? parsed.project : path17.basename(scanPath);
|
|
7755
7907
|
const result = await runOrchestrator({
|
|
7756
7908
|
scanPath,
|
|
7757
7909
|
project: projectName,
|