portmind-monorepo
v0.3.1
Published
Local-first CLI + web dashboard that scans listening ports, enriches them with process/Docker/IANA detail, and remembers what normally runs where.
Maintainers
Readme
portmind
Local-first CLI + web dashboard that scans listening ports on your machine, enriches them with process/Docker/IANA detail, remembers what normally runs on each port, and flags what's unusual — no cloud, no telemetry, no background AI calls.
portmind-monorepois published on npm (see the badge above), but it has nobinfield — installing it does not give you aportmindcommand.@portmind/cli, the package that actually would, is not published yet.
Status
Working today:
portmind listruns a real scan (lsof-based) of TCP and UDP listening sockets on macOS/Linux- Each result is enriched with process command line, working directory, and start time (
ps+lsof -d cwd) --range <min-max>,--docker-only,--unusual, and--jsonfilters/output modes- Table and JSON output share one
PortEntrytype, defined once in@portmind/core portmind webstarts a local-only dashboard (127.0.0.1, plain HTML/JS, no framework, no build step) serving the same data asportmind list --jsonoverGET /api/ports, with a sortable/filterable table and a row-click detail panel- Real known-port descriptions via the official IANA Service Name and Port Number Registry (11,394 entries bundled, fetched and verified from
iana.org— e.g. port 5432 shows "PostgreSQL Database", not a placeholder) - A real, working config file loader —
~/.portmind/config.yaml(user) and.portmind.yaml(project) are both read and merged over the defaults;portmind config show/config pathexpose the resolved result and file locations known_ports.source: customorbothin config lets you point at your own JSON file to override or add service descriptions IANA doesn't have (internal services, dev conventions like Redis/MongoDB that aren't officially IANA-registered)- AI
explain(opt-in, off by default) —portmind explain <port>and the web dashboard's "Explain with AI" button both make a real call to Anthropic or OpenAI (your choice, viaai.provider), sending only the explicit field allowlist (never full env vars or file contents), with cmdline secrets/tokens redacted first and results cached in SQLite so repeat queries don't re-call the API. Not tested against a live API key — the request-construction logic (allowlist, sanitization, exact payload shape) is unit-tested with a mocked HTTP layer, but no real call to Anthropic/OpenAI has been made to confirm end-to-end correctness.
Not implemented yet (see Next steps): Docker cross-reference, local history/"usual" detection, risk flags, watch/free/history/ssh/tui commands. Until those land, every PortEntry.docker and .riskFlags will be empty, and .history.usual is always true — so the web dashboard's Docker/unusual filters currently have nothing to filter.
See CHANGELOG.md for a dated record of what shipped when.
Requirements
- Node.js 22+
- pnpm 9+
- macOS or Linux —
lsofmust be on$PATH(this is the only scan backend implemented so far; Linux'sssis not wired up yet despite being mentioned in the original design) - Docker CLI (
docker) optional — not yet used, but scans are designed to degrade gracefully once Docker cross-referencing is added
Install (development)
pnpm install
pnpm buildThere's no published package yet, so there's nothing to npm install -g. Run the CLI directly from this repo:
node packages/cli/dist/index.js listUsage
portmind list # full table, all listening ports on localhost
portmind list --range 3000-9000 # filter by port range
portmind list --docker-only # only Docker-backed ports (no-op until Docker cross-reference ships)
portmind list --unusual # only ports flagged unusual or risky (no-op until history/risk flags ship)
portmind list --json # machine-readable output for scripting/CIExample output:
PORT PROTO PROCESS PID DOCKER USUAL NOTE
3000 tcp node 41822 - yes /Users/you/Projects/react-dashboard
5432 tcp postgres 1758 - yes PostgreSQL DatabaseExit codes: 0 success, 1 scan error, 2 config error.
Web dashboard
portmind web # starts on http://127.0.0.1:4400 and opens your browser
portmind web --port 4401 # use a different port for the dashboard itself
portmind web --no-open # don't open the browser automaticallyWhy a plain server + vanilla JS instead of a framework: the dashboard is three routes (the page, /api/ports, /api/explain), it never leaves your machine except for the explicit opt-in AI call, and there's no build pipeline to maintain — consistent with the "local-first, no telemetry" design of the rest of the tool. The page polls /api/ports on load and on manual refresh (or a 5-second auto-refresh you opt into). Clicking a row opens a fixed panel from the right edge of the screen (with a dimmed overlay behind it) showing full detail — no scrolling required regardless of where in the table you clicked. Its "Explain with AI" button only fires /api/explain when clicked, never automatically.
AI explain
portmind explain 5432 # runs AI deep-search for whatever is currently on port 5432Requires ai.enabled: true in config and an API key in the matching environment variable (ANTHROPIC_API_KEY or OPENAI_API_KEY depending on ai.provider — never put the key in the config file itself). Disabled by default; both the CLI command and the web button fail with a clear config error (exit code 2 for the CLI) rather than silently doing nothing if it's not configured. Results are cached in SQLite keyed by (process_name, port), so asking about the same shape of thing twice doesn't re-call the API.
Config
portmind config show # print the fully resolved config (defaults + user + project merged), as JSON
portmind config path # print ~/.portmind/config.yaml and ./.portmind.yaml, and whether each existsCopy portmind.config.example.yaml to ~/.portmind/config.yaml or ./.portmind.yaml to change behavior — only the keys you include override the defaults, anything omitted falls back. Malformed YAML in a config file that exists is a hard error (exit code 2), not silently ignored.
Planned commands not yet implemented: watch, free, history, ssh <host> list|check, tui.
Storage
History will be stored locally in a SQLite database at ~/.portmind/portmind.db (configurable) once the history feature (Phase 4) lands - the same db file already holds the ai_explanations cache table today. Nothing leaves the machine unless you explicitly run portmind explain <port> (or click "Explain with AI") with AI enabled in config, and even then only an explicit allowlist of fields is sent — never full environment variables or file contents, and cmdline values are scanned for secret/token-shaped substrings and redacted before sending.
Configuration
Real and working. Resolution order (later overrides earlier, and only the keys you actually set are overridden - everything else falls back): built-in defaults → ~/.portmind/config.yaml (user) → .portmind.yaml in the current directory (project). See portmind.config.example.yaml for a copy-and-edit starting point.
scan:
interval_seconds: 5
include_udp: true
docker: true
ssh_hosts: []
known_ports:
source: iana # iana | custom | both
custom_db_path: null
refresh_days: 30
history:
enabled: true
db_path: ~/.portmind/portmind.db
retention_days: 180
risk_rules:
flag_bound_all_interfaces: true
flag_unsigned_binary: true # macOS only, no-op elsewhere
flag_no_known_service: true
ai:
enabled: false # opt-in, off by default
provider: anthropic # anthropic | openai | none
model: claude-sonnet-4-6
trigger: manual
fields_sent: # explicit allowlist - nothing outside this list is ever sent
- process_name
- cmdline
- port
- protocol
- docker_image
cmdline_sanitization: true
output:
default_format: table # table | json
color: auto # auto | always | never
logging:
level: info # debug | info | warn | error
audit_log: falseThe TypeScript shape (PortmindConfig), its defaults, and the loader/merge logic live in packages/core/src/config.ts and packages/core/src/configLoader.ts. Not yet wired to config: history.retention_days (history isn't implemented), risk_rules.* (risk flags aren't implemented), known_ports.refresh_days (the bundled IANA cache doesn't auto-refresh yet - refreshing it means re-fetching the CSV and regenerating packages/core/data/iana-cache.json, which isn't automated).
Architecture
packages/
├── core/ # scanning, enrichment, data model, config — no UI, no AI dependency
├── cli/ # table/JSON output over @portmind/core (implemented)
├── tui/ # live terminal dashboard — not started
├── web/ # local HTML dashboard (implemented: /api/ports + static page)
└── ai/ # optional AI deep-search plugin — not startedcore has zero UI and zero AI dependencies. cli, tui, and web are meant to be thin renderers over the same PortEntry[] shape — no duplicated scanning logic between them.
Next steps
Remaining phases, in build order:
- History and "usual" detection — SQLite
observations/port_fingerprintstables,history <port>command - Docker cross-reference — match
docker psoutput against scanned ports, populatePortEntry.docker - Risk flags —
bound_all_interfaces,unsigned_binary,no_known_service, each independently configurable - TUI (
inkorblessed— undecided) — live table with inlineexplain/free - SSH remote support —
ssh_hostsconfig, samePortEntryshape withhostset to the remote name - Audit logging — log every
free/explainaction perlogging.audit_log - IANA cache auto-refresh — automate re-fetching the CSV on
known_ports.refresh_days, rather than the current bundled-once snapshot - Polish — full
--helptext, packaging fornpm install -g @portmind/cli - AI explain: real end-to-end verification — the feature is built and unit-tested with a mocked HTTP layer, but has never actually been called against Anthropic or OpenAI with a real API key. Verifying that happens whenever a key becomes available to test with.
Web dashboard, IANA enrichment, the config file loader, and AI explain (untested end-to-end - see above) are done — see Status.
Two decisions still open: TUI library (ink vs blessed), and whether free on a Docker-backed port needs anything beyond the interactive stop/kill/cancel prompt already agreed on.
Development
pnpm test # run all package test suites (Vitest)
pnpm lint # Biome check
pnpm format # Biome format --writeTests live in each package's tests/ directory, not alongside source.
