Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,11 @@ next-env.d.ts
# build
build/

# distributable bundle — regenerated by `npm run pack:standalone` (prepack).
# Build output, not source: it is shipped in the npm tarball via package.json
# "files", never committed.
dist/
*.tgz

# vercel
.vercel
21 changes: 21 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# npm reads THIS file instead of .gitignore when it exists — which is the whole
# point of it existing.
#
# .gitignore lists `node_modules/` and `.next/` (correct for git). npm applies those
# same rules *inside* dist/, which silently strips the entire Next runtime and the
# compiled app out of the tarball and ships a dist/server.js that cannot boot. The
# publish succeeds; every install fails. This file stops that fallback.
#
# The real allowlist is package.json "files" (bin/, dist/, and the docs). Anything
# not listed there never ships regardless of what appears below.
#
# Defence in depth for the things that must never leave this machine: the local
# database and the user's imported source projects.
data/
*.db
*.db-shm
*.db-wal
*.sqlite
.env
.env.*
*.tgz
79 changes: 79 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Changelog

All notable changes to NirmiqCodeSensei are documented here.

Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
Versioning: [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [1.0.0] — 2026-07-16

First public release. NirmiqCodeSensei is a local-first learning OS: point it at a project
and it produces a learning map, an 8-lens senior-engineer review, a code-grounded DSA
breakdown, and explain-back questions. It binds to 127.0.0.1, stores everything in a local
SQLite database, and sends no telemetry.

```bash
npx nirmiqcodesensei@latest
```

### Added

- **Installable distribution.** `npx nirmiqcodesensei` runs the app on a machine that has
never seen the repo. The published package carries a prebuilt Next.js standalone server;
native modules resolve per-platform at install time, so one tarball serves Windows, Linux
and macOS. Aliases: `codesensei`, `nirmiq`.
- **MCP server in the published package** (`npx nirmiqcodesensei mcp`) — 12 tools over stdio
for Claude Code, Cursor and Windsurf. Bundled at pack time, so no dev toolchain is needed.
- **Import & analysis.** Local path or GitHub URL → learning map, architecture graph,
code-grounded DSA concepts, explain-back questions. Offline by default; an optional
`ANTHROPIC_API_KEY` enriches the analysis and sends only computed findings, never source.
- **8-lens senior review** with an overall grade, scored on code *density* rather than project
size, plus an incremental re-analysis path that skips unchanged sources.
- **Learning surfaces:** knowledge graph, explain-back with confidence tracking, DSA bridge,
debug lab, daily log, session log, BM25 search, and Markdown export of the whole pipeline.
- **Data ownership:** the database lives in `data/` in the directory you launch from — your
project, not the install — and can be downloaded as a backup at any time.
- `CHANGELOG.md`, and a scaling-N/A architecture decision (REVIEW-013) recording that
horizontal scaling is a deliberate non-goal for a single-user local-first tool.

### Fixed

Found by installing the real tarball into an empty directory — each would have shipped a
broken or unsafe 1.0:

- Next.js file tracing copied `data/` into the build output — the local database *and* the
user's imported projects, including their `.git` history. Publishing would have leaked
third-party source. The bundle is now filtered at copy time and the build hard-fails if a
database, dotenv file, git history or native binary reaches it.
- The standalone server `chdir`s to its own directory, so a published install resolved the
database to `node_modules/…/dist/data/`. Every `npx …@latest` would have silently destroyed
the user's learning history. The launcher now pins the data directory to your working
directory.
- The standalone server defaulted to `0.0.0.0`, exposing a 127.0.0.1-only product across the
local network. The launcher pins the loopback interface.
- The repo's own `.gitignore` was traced into the bundle; npm honours nested ignore files, so
the Next runtime and compiled app were dropped from the tarball — a package that installs
and cannot boot.
- Turbopack requires external packages by a hashed name, so removing the build machine's
`better-sqlite3` made every request fail with `Failed to load external module`.
- The MCP server printed its banner to stdout, corrupting the JSON-RPC stream that stdio
transport requires; it now goes to stderr.
- Install paths containing spaces (`C:\Program Files\…`) broke the launcher.

### Security

- Ingests private source safely: symlink-confined tree walk, shell-free git, realpath and
credential-directory blocks, per-file size caps. Self-scanned security lens: **A/100**.
- Production CSP drops `unsafe-eval` (dev-only). CI gates on critical `npm audit` advisories.
- No telemetry, no analytics, no cloud dependency.

### Known limitations

- Image optimisation needs the optional `sharp` dependency; without it Next serves unoptimised
images.
- macOS is untested in CI (Windows + Linux are covered); it is expected to work via the same
per-platform native resolution.

[1.0.0]: https://github.com/SheeshDarth/NirmiqCodeSensei/releases/tag/v1.0.0
46 changes: 28 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,34 @@ Everything is stored locally in SQLite. Nothing leaves your machine.

## Quick start

### 1. Clone and install
```bash
cd your-project
npx nirmiqcodesensei@latest
```

That's it. The dashboard opens at [http://127.0.0.1:3000](http://127.0.0.1:3000) — bound to localhost only. Requires Node.js 20+.

Your learning data lives in `data/` **in the directory you launch from**, so each project keeps its own history, and it's yours to back up, move or delete. Migrations are applied automatically on first start.

```bash
git clone https://github.com/SheeshDarth/NirmiqCodeSensei.git
cd NirmiqCodeSensei
npm install
npx nirmiqcodesensei # start the app + open the dashboard
npx nirmiqcodesensei mcp # start the MCP server (stdio)
npx nirmiqcodesensei open # just open the dashboard
npx nirmiqcodesensei --help # all commands
```

### 2. Run the app
Aliases: `npx codesensei` and `npx nirmiq` do the same thing.

### From a clone (contributors)

```bash
git clone https://github.com/SheeshDarth/NirmiqCodeSensei.git
cd NirmiqCodeSensei
npm install
npm run dev
```

Database migrations are applied automatically on first start — no manual setup needed. Requires Node.js 20+.

Open [http://127.0.0.1:3000](http://127.0.0.1:3000) — the app binds to localhost only.
The CLI detects which shape it's running in: a published install boots the prebuilt server, a checkout runs `next dev` against the source.

---

Expand All @@ -58,7 +69,7 @@ Open [http://127.0.0.1:3000](http://127.0.0.1:3000) — the app binds to localho
Start the MCP server:

```bash
npm run mcp
npx nirmiqcodesensei mcp
```

### Claude Code
Expand All @@ -69,9 +80,8 @@ Add to `.claude/mcp.json` in your project (or user MCP settings):
{
"mcpServers": {
"nirmiqcodesensei": {
"command": "npm",
"args": ["run", "mcp"],
"cwd": "/absolute/path/to/NirmiqCodeSensei"
"command": "npx",
"args": ["nirmiqcodesensei", "mcp"]
}
}
}
Expand All @@ -84,9 +94,8 @@ Add to Cursor MCP settings (`Settings → Features → MCP`):
```json
{
"nirmiqcodesensei": {
"command": "npm",
"args": ["run", "mcp"],
"cwd": "/absolute/path/to/NirmiqCodeSensei"
"command": "npx",
"args": ["nirmiqcodesensei", "mcp"]
}
}
```
Expand All @@ -99,14 +108,15 @@ Add to `~/.codeium/windsurf/mcp_config.json`:
{
"mcpServers": {
"nirmiqcodesensei": {
"command": "npm",
"args": ["run", "mcp"],
"cwd": "/absolute/path/to/NirmiqCodeSensei"
"command": "npx",
"args": ["nirmiqcodesensei", "mcp"]
}
}
}
```

> Running from a clone instead? Use `"command": "npm", "args": ["run", "mcp"]` with `"cwd"` set to the repo root.

---

## MCP Tools
Expand Down
120 changes: 90 additions & 30 deletions bin/nirmiq.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,61 @@
* nirmiqcodesensei — NirmiqCodeSensei CLI (aliases: codesensei, nirmiq)
*
* Usage:
* npx nirmiqcodesensei # start the app (dev mode)
* npx nirmiqcodesensei start # start in production mode
* npx nirmiqcodesensei # start the app
* npx nirmiqcodesensei start # same, explicit
* npx nirmiqcodesensei mcp # start the MCP server
* npx nirmiqcodesensei open # open the dashboard in the browser
*
* The CLI must be run from the NirmiqCodeSensei repo root.
* Two install shapes, detected at runtime:
* - Published package → dist/server.js exists; run the prebuilt standalone server.
* - Repo checkout → no dist/; fall back to `next dev` against the source.
*
* All data is stored locally — nothing is sent to any server.
*/

import { spawn, execSync } from "node:child_process";
import { existsSync, readFileSync, appendFileSync } from "node:fs";
import { resolve, join } from "node:path";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { platform } from "node:os";

const ROOT = new URL("..", import.meta.url).pathname.replace(/^\/([A-Z]:)/, "$1");
// fileURLToPath, not URL.pathname: the latter leaves paths percent-encoded, so a
// global install under "C:\Program Files\…" would resolve to "…\Program%20Files\…".
const ROOT = fileURLToPath(new URL("..", import.meta.url));
const PKG = JSON.parse(readFileSync(join(ROOT, "package.json"), "utf-8"));
const VERSION = PKG.version ?? "0.1.0";
const VERSION = PKG.version ?? "0.0.0";

// The published server is a prebuilt Next standalone bundle; a repo checkout has none.
const DIST_SERVER = join(ROOT, "dist", "server.js");
// CJS, not ESM: the bundle pulls in @typescript-eslint/typescript-estree, which
// reads __filename — undefined in an ESM bundle, so an .mjs build crashes on load.
const DIST_MCP = join(ROOT, "dist", "mcp-server.cjs");
const IS_PACKAGED = existsSync(DIST_SERVER);

const DASHBOARD_URL = "http://127.0.0.1:3000/dashboard";
const PORT = process.env.PORT ?? "3000";
const DASHBOARD_URL = `http://127.0.0.1:${PORT}/dashboard`;

/**
* Env for any spawned server process.
*
* NCS_DATA_DIR is the load-bearing one. Next's standalone server.js does
* `process.chdir(__dirname)` on boot, and lib/db/client.ts resolves the database
* from process.cwd() — so without this, a published install would write the
* user's entire learning history into node_modules/nirmiqcodesensei/dist/data/,
* and the next `npx nirmiqcodesensei@latest` would silently wipe it. Pin the data
* dir to the directory the user actually invoked us from.
*
* HOSTNAME matters too: standalone server.js defaults to 0.0.0.0, which would
* expose the app across the LAN. This product is 127.0.0.1-only by design.
*/
function serverEnv() {
return {
...process.env,
NCS_DATA_DIR: process.env.NCS_DATA_DIR ?? join(process.cwd(), "data"),
HOSTNAME: "127.0.0.1",
PORT,
};
}

// ── ANSI helpers ───────────────────────────────────────────────────────────────
const C = {
Expand All @@ -33,13 +69,16 @@ const C = {
bold: (s) => `\x1b[1m${s}\x1b[0m`,
};

function banner() {
console.log("");
console.log(C.cyan(" ╔══════════════════════════════════════╗"));
console.log(C.cyan(" ║") + C.bold(" NirmiqCodeSensei") + C.dim(` v${VERSION}`) + C.cyan(" ║"));
console.log(C.cyan(" ║") + C.dim(" Build with AI, learn like a real engineer") + C.cyan(" ║"));
console.log(C.cyan(" ╚══════════════════════════════════════╝"));
console.log("");
// `write` is injectable because the MCP transport owns stdout: it speaks
// line-delimited JSON-RPC there, and a banner on the same stream is unparseable
// noise that breaks the handshake. MCP passes console.error.
function banner(write = console.log) {
write("");
write(C.cyan(" ╔══════════════════════════════════════╗"));
write(C.cyan(" ║") + C.bold(" NirmiqCodeSensei") + C.dim(` v${VERSION}`) + C.cyan(" ║"));
write(C.cyan(" ║") + C.dim(" Build with AI, learn like a real engineer") + C.cyan(" ║"));
write(C.cyan(" ╚══════════════════════════════════════╝"));
write("");
}

function openBrowser(url) {
Expand All @@ -56,11 +95,16 @@ function openBrowser(url) {
}
}

function run(command, args, cwd = ROOT) {
// shell defaults to true because `npx` on Windows is a .cmd shim and won't spawn
// otherwise. Pass shell:false when invoking node directly — a shell concatenates
// args instead of escaping them, so an install path containing a space
// ("C:\Program Files\…") would be split into two broken arguments.
function run(command, args, cwd = ROOT, env = process.env, shell = true) {
const proc = spawn(command, args, {
cwd,
env,
stdio: "inherit",
shell: true,
shell,
});
proc.on("error", (e) => {
console.error(C.red(`Failed to start: ${e.message}`));
Expand Down Expand Up @@ -92,42 +136,58 @@ switch (cmd) {
case "start": {
banner();
ensureGitignore(process.cwd());
const mode = cmd === "start" ? "start" : "dev";
const modeArgs =
mode === "dev"
? ["dev", "--turbopack", "--hostname", "127.0.0.1"]
: ["start", "--hostname", "127.0.0.1"];

const env = serverEnv();
console.log(
C.cyan(` Starting NirmiqCodeSensei`) +
C.dim(` (${mode} mode) …`)
C.cyan(" Starting NirmiqCodeSensei") +
C.dim(IS_PACKAGED ? " …" : " (dev mode — repo checkout) …")
);
console.log(C.dim(` Dashboard → ${DASHBOARD_URL}`));
console.log(C.dim(` Data → ${env.NCS_DATA_DIR}`));
console.log(C.dim(` Privacy → All data stays local. Zero telemetry.`));
console.log("");

// Open browser after a short delay to let the server start
setTimeout(() => openBrowser(DASHBOARD_URL), 3000);

run("npx", ["next", ...modeArgs], ROOT);
if (IS_PACKAGED) {
// Prebuilt standalone server — no Next CLI, no devDependencies needed.
run(process.execPath, [DIST_SERVER], ROOT, env, false);
} else {
// Repo checkout: `next dev` compiles from source. `start` would need a
// prior `npm run build`, so dev is the sane default for contributors.
const modeArgs =
cmd === "start"
? ["start", "--hostname", "127.0.0.1"]
: ["dev", "--turbopack", "--hostname", "127.0.0.1"];
run("npx", ["next", ...modeArgs], ROOT, env);
}
break;
}

case "mcp": {
banner();
console.log(C.cyan(" Starting NirmiqCodeSensei MCP server…"));
console.log(
// Everything human-readable goes to stderr: stdout is the JSON-RPC transport.
banner(console.error);
console.error(C.cyan(" Starting NirmiqCodeSensei MCP server…"));
console.error(
C.dim(
" Connect this to Claude Code / Cursor / Windsurf via their MCP config."
)
);
console.log(
console.error(
C.dim(
` Transport: stdio | Config: { "command": "npx", "args": ["nirmiqcodesensei", "mcp"] }`
)
);
console.log("");
run("npx", ["tsx", join(ROOT, "mcp-server", "index.ts")]);
console.error("");
// Packaged: a single prebuilt CJS bundle (esbuild, at prepack time) — tsx is a
// devDependency and does not exist in a published install.
// Checkout: run the TypeScript source through tsx.
if (existsSync(DIST_MCP)) {
run(process.execPath, [DIST_MCP], ROOT, serverEnv(), false);
} else {
run("npx", ["tsx", join(ROOT, "mcp-server", "index.ts")], ROOT, serverEnv());
}
break;
}

Expand Down
Loading
Loading