npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@telepath-computer/fslint

v0.3.3

Published

Declarative linter for a directory of files. Structural validation for agents managing files.

Readme

fslint

⚠️ Experimental. Early-stage software. The config schema, CLI, and API may change without notice between releases. Pin exact versions if you depend on it.

A declarative linter for a directory of files. Rules live in a .fslint.yml at the root and are grouped by scopes — a where: predicate block (path glob and/or frontmatter values), optionally narrowed by a segment (a slice of content within each file). Running fslint walks the root, matches scopes, extracts segments, and runs each scope's rules.

Report-only. No auto-fix, no daemon, no plugins — just a config, a walk, and violations printed to the terminal.

Why

Expected use case: agents managing files. When LLM-driven tools or scripts need to read, mutate, and write files in a directory, the structure has to be verifiable. fslint runs as a gate — locally, in CI, in pre-commit — to catch files that drift from the agreed-upon shape. The rule set (kebab-case paths, typed frontmatter, segment invariants, structural heading rules) targets the kinds of things an agent needs to parse reliably, not the kinds of things humans care about for readability.

Why not ?

  • markdownlint is a formatting linter — line lengths, list markers, consistent emphasis style. It does not validate frontmatter schemas, filenames, folder structure, or content within a specific section.
  • Vale is a prose / style linter — grammar, passive voice, terminology consistency. A different axis entirely.
  • remark-lint is powerful but AST-based and plugin-assembled — you wire up a pipeline from the unified ecosystem. fslint trades flexibility for a single config with a small set of predictable rules.
  • ls-lint validates filenames and directory names but doesn't look at file contents. fslint covers both paths and content in one pass.
  • ESLint and friends target JavaScript / TypeScript source. Different problem.

fslint sits in the gap: structural validation across a directory of files, scoped by globs and segments, producing violations a script (human or agent) can act on.

Install

Requires Node 20+.

git clone <this repo>
cd notelint
npm install
npm run build
npm link          # makes `fslint` available on your PATH

Quick start

# .fslint.yml at the root of the directory you want to lint
ignore:
  - ".obsidian/**"
  - ".trash/**"

scopes:
  - where:
      path: "**/*.md"
    rules:
      - type: headings
        min_level: 2
        blank_before: 1
        blank_after: 1
      - type: filename
        case: kebab-case

  - where:
      kind: folder
      path: "**"
    rules:
      - type: folder
        case: kebab-case

Then:

fslint .

Exits non-zero on violations; prints per-file lists with rule ids.

Rules

Each rule's full config and semantics live in docs/rules/<name>.md.

| Rule | Target | Purpose | |---|---|---| | frontmatter | file | JSON-Schema-style validation of YAML frontmatter — properties, types, case: on strings, array items + style: flow \| block, additional_properties, required per key, plus blank_after spacing | | headings | file | ATX headings: blank_before / blank_after exact counts, min_level | | checkbox_list | file | Body (or segment slice) must be GFM task-list items + blanks only | | filename | file | Checks the file's own basename against a case: preset | | indent | file | Leading whitespace style — style: tab \| space and width: for space | | folder | folder | Checks each directory's own name against a case: preset |

Shared case presets (used by filename, folder, and frontmatter's string case:): kebab-case, snake_case, camelCase, PascalCase, lower.

Scopes

Each scope filters via a where: predicate block whose keys are ANDed together. File scopes (the default, kind: file) run file-level rules against every markdown file that matches. Folder scopes (kind: folder) run folder-level rules against every directory that matches, via a separate directory walk.

scopes:
  # File scope, whole-file rules — `kind: file` is the default.
  - where:
      path: "**/*.md"
    rules: [ ... ]

  # File scope narrowed to a frontmatter type — predicate values are
  # equality checks against parsed YAML scalars; multiple keys AND.
  - where:
      path: "**/*.md"
      frontmatter:
        type: person
    rules: [ ... ]

  # File scope narrowed to the `## Actions` segment of matching files
  - where:
      path: "projects/**/*.md"
    segment:
      starts_at: { heading: "Actions" }
      ends_at:   { same_or_higher_heading: true }
    rules:
      - type: checkbox_list

  # Folder scope
  - where:
      kind: folder
      path: "**"
    rules: [ ... ]

Predicate kinds in v1: kind: (file/folder, default file), path: (string glob), frontmatter: (file scopes only — map of literal-equality checks against scalar YAML values). An absent or empty where: matches every walked path. frontmatter: on a folder scope is a config error.

Rule-target mismatch (e.g. a file rule in a folder scope) is also a config error.

Segment boundaries supported in v0.1: heading, same_or_higher_heading.

Library use

lint() is pure: it takes a root and a Config. Build the config yourself, or use loadConfig / findConfig for the disk flow.

import { lint, findConfig, loadConfig } from "fslint";

// Inline config — no disk needed
const result = await lint("./my-project", {
  ignore: [".obsidian/**"],
  scopes: [
    {
      kind: "file",
      where: { path: "**/*.md" },
      rules: [
        {
          id: "heading-spacing",
          type: "headings",
          blankBefore: 1,
          blankAfter: 1,
        },
      ],
    },
  ],
});

// Or, disk-based: the same flow the CLI uses
const configPath = await findConfig("./my-project");
if (!configPath) throw new Error("no .fslint.yml");
const config = await loadConfig(configPath);
const result2 = await lint("./my-project", config);

Status

v0.1 / pre-release. API and config shape may still change. Tests cover the runner, each rule's happy path and edge cases, and one end-to-end CLI test.

Run npm test for the suite. npm run type-check for types only.