Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
797d51c
feat(cli): add strict argument parsing
mohanagy Jul 11, 2026
661185d
fix(cli): accept dashed equals values
mohanagy Jul 11, 2026
b6148d0
feat(cli): follow redacted audit logs
mohanagy Jul 11, 2026
e8c99da
fix(cli): harden audit log following
mohanagy Jul 11, 2026
11f22e4
fix(cli): verify complete audit cursor
mohanagy Jul 11, 2026
b18fc4c
fix(cli): bound audit JSONL reader memory
mohanagy Jul 11, 2026
ed3c972
fix(cli): make audit snapshots transactional
mohanagy Jul 11, 2026
fa2a9b1
fix(cli): spool audit snapshots in OS temp
mohanagy Jul 11, 2026
cc4e5d0
fix(cli): skip verified idle audit polls
mohanagy Jul 11, 2026
0fa145d
fix(cli): respect stdout backpressure
mohanagy Jul 11, 2026
d4d74c3
test(cli): verify audit spool cleanup
mohanagy Jul 11, 2026
7b0cfb7
fix(cli): harden audit follow lifecycle
mohanagy Jul 11, 2026
18ac337
test(audit): stabilize same-inode rewrite fixture
mohanagy Jul 11, 2026
a525fa4
docs(audit): explain prefix rehash
mohanagy Jul 11, 2026
39ef249
test(cli): cover installed binary contract
mohanagy Jul 11, 2026
f7e78ab
test(ci): run packed CLI contract in compatibility matrix
mohanagy Jul 11, 2026
c1bbedc
test(audit): make same-inode race deterministic
mohanagy Jul 11, 2026
4ee9bec
test(audit): require partial snapshot spool signal
mohanagy Jul 11, 2026
a07be8b
fix(pack): isolate verifier from executable wrapper
mohanagy Jul 11, 2026
11493cb
fix(test): bound packed consumer install
mohanagy Jul 11, 2026
2488a73
fix(test): quote Windows packed binary invocation
mohanagy Jul 11, 2026
1003217
fix(cli): resolve review findings
mohanagy Jul 11, 2026
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format

## [Unreleased]

### Added

- [#18](https://github.com/mohanagy/miftah/issues/18) The packed-package contract now exercises the installed CLI through shell and Windows command quoting, paths with spaces, generated help, stable category exits, JSON automation output, and normalized/redacted audit-log output.
- [#18](https://github.com/mohanagy/miftah/issues/18) The CLI reference now documents generated help, every command and option, version compatibility output, exit statuses, JSON streams, and audit snapshot/follow safety boundaries.

### Changed

- [#16](https://github.com/mohanagy/miftah/issues/16) The library root export is now an intentional, documented public API. Internal server, process, profile, routing, policy, audit, and secret-management classes are no longer available from `@lubab/miftah`; use the configuration utilities and `createMiftahRuntime()` instead. This pre-1.0 breaking change requires a minor release.
Expand Down
22 changes: 13 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,17 +175,21 @@ New audit directories and files use owner-only permissions where the platform su

## CLI

Use `miftah --help` for the generated command list and `miftah <command> --help` for command-specific options. The available commands are:

| Command | Purpose |
| --- | --- |
| `miftah --config <file>` | Run the STDIO MCP wrapper |
| `miftah serve --config <file>` | Run the wrapper explicitly |
| `miftah validate --config <file>` | Parse and validate JSON config |
| `miftah doctor --config <file>` | Validate config and report redacted runtime details |
| `miftah init <name> --preset <generic\|github\|sentry>` | Generate a template |
| `miftah schema` | Print the JSON Schema |
| `miftah list-tools --config <file> [--profile <name>]` | Discover upstream tools |
| `miftah test-profile --config <file> --profile <name>` | Start and initialize one profile |
| `miftah logs --config <file>` | Read the configured JSONL audit log |
| `miftah --config <file>` / `miftah serve --config <file>` | Run the STDIO MCP wrapper. |
| `miftah validate --config <file>` | Parse and validate JSON config; writes JSON. |
| `miftah doctor --config <file> [--json]` | Report redacted configuration and upstream readiness. |
| `miftah init [name] [--name <name>] [--preset <name>] [--output <file>]` | Generate a generic, GitHub, or Sentry template. |
| `miftah schema` | Print the JSON Schema. |
| `miftah list-tools --config <file> [--profile <name>]` | Discover upstream tools as JSON. |
| `miftah test-profile --config <file> [--profile <name>]` | Start and initialize one profile; writes JSON. |
| `miftah logs --config <file> [--follow]` | Read normalized, redacted audit JSONL; follow rotation safely when requested. |
| `miftah --version` / `miftah -v` / `miftah version [--json]` | Print the package SemVer. `--json` intentionally preserves bare SemVer output. |

Structured success output is written to stdout with stderr empty. Stable nonzero categories are usage (`2`), configuration (`3`), secret resolution (`4`), upstream (`5`), and policy (`6`); `1` is an uncategorized operational failure. Quote config and output paths with spaces. `logs --follow` handles appends, truncation, and rotation, and exits cleanly on `SIGINT` or `SIGTERM` without starting an upstream. See the complete [CLI reference](docs/cli.md) for help behavior, defaults, JSON contracts, redaction, and audit reader boundaries.

## Documentation

Expand Down
95 changes: 72 additions & 23 deletions docs/cli.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,93 @@
# CLI reference

Run the wrapper for an MCP client:
`miftah` is a local STDIO MCP wrapper. Run `miftah --help` for the generated command list, or `miftah <command> --help` for the options accepted by one command. The help text is the authoritative grammar for the installed version.

```bash
miftah --config ~/.config/miftah/github.json
miftah serve --config ~/.config/miftah/github.json
## Help

```text
Usage: miftah [command] [options]
```

Inspect a config without starting an upstream:
The root command list is `serve`, `validate`, `doctor`, `schema`, `init`, `list-tools`, `test-profile`, `logs`, and `version`. With no command, Miftah runs `serve`.

`--help` and `-h` print help and exit successfully. They can appear before or after a command. Help never reads configuration, resolves secrets, or starts an upstream.

## Commands

| Command | Required input | Options | Output and behavior |
| --- | --- | --- | --- |
| `miftah serve --config <file>` | `--config` | `--config <file>` | Runs the STDIO MCP wrapper until it is stopped. `miftah --config <file>` is the equivalent default-command form. |
| `miftah validate --config <file>` | `--config` | `--config <file>` | Validates the JSON configuration without starting an upstream. Writes a JSON object with `ok`, `name`, and `profiles`. |
| `miftah doctor --config <file>` | `--config` | `--config <file>`, `--json` | Validates configuration and checks upstream readiness. Default output is a human-readable report; `--json` writes only the JSON report. A healthy or degraded report exits `0`; a failed report exits `1`. |
| `miftah schema` | none | none | Writes the Miftah JSON Schema as pretty-printed JSON. |
| `miftah init [name]` | none | `--name <name>`, `--preset <name>`, `--output <file>` | Writes a starter configuration with exclusive creation. The positional `name` and `--name` are alternatives; the default name is `miftah-wrapper`. |
| `miftah list-tools --config <file>` | `--config` | `--config <file>`, `--profile <name>` | Starts the selected profile, discovers its upstream tools, writes a JSON array, then closes the manager. `--profile` defaults to the configured default profile. |
| `miftah test-profile --config <file>` | `--config` | `--config <file>`, `--profile <name>` | Starts and initializes one profile, writes `{"ok":true,"profile":"…"}`, then closes the manager. `--profile` defaults to the configured default profile. |
| `miftah logs --config <file>` | `--config` | `--config <file>`, `--follow` | Reads the configured audit JSONL as normalized, redacted JSONL. `--follow` continues watching it. This command does not construct an upstream manager. |
| `miftah version` | none | `--json` | Writes the package version as a bare SemVer line. `--json` is retained for automation compatibility and intentionally writes the same bare SemVer line. |

Every command also accepts `--help` and `-h`; those generated per-command help screens show only the options valid for that command.

```bash
miftah validate --config github.json
miftah schema
### `init` presets and paths

`--preset` defaults to `generic`. The named templates are `generic`, `github`, and `sentry`; an unrecognized preset name falls back to the generic template. `--output` defaults to `<name>.miftah.json`. Miftah resolves the output path from the current working directory, creates missing parent directories, and refuses to overwrite an existing file. Quote shell paths and names containing spaces:

```sh
miftah init "work wrapper" --preset github --output "$HOME/Miftah configs/work wrapper.json"
miftah validate --config "$HOME/Miftah configs/work wrapper.json"
```

## Readiness diagnostics
### `doctor`

Run readiness diagnostics before deploying a configured upstream:
`doctor` checks configuration, secret references, redaction, permissions, configured audit storage, executable availability, upstream startup, discovery, and clean shutdown where applicable. Its checks have stable `code`, `status`, `target`, `explanation`, and `remediation` fields.

```bash
```sh
miftah doctor --config github.json
miftah doctor --json --config github.json
```

The default report is human-readable. For automation, emit only the pretty-printed JSON report:
The JSON report intentionally omits resolved secret values, raw configuration paths, configured upstream command arguments, and its synthetic redaction canary.

```bash
miftah doctor --json --config github.json
## Global version options

These forms all print the package SemVer to stdout and nothing to stderr:

```sh
miftah --version
miftah -v
miftah version
miftah version --json
```

Reports are `healthy`, `degraded`, or `failed`. Every check has a stable `code`, `status`, `target`, `explanation`, and `remediation`. Doctor validates configuration and secret references, checks redaction, permissions and configured audit storage, and probes executable availability, upstream startup, discovery, and clean shutdown where applicable.
`--version` and `-v` are root options, so they cannot be combined with another command. `--version --json` is accepted and also preserves the bare SemVer output.

## Exit status and streams

Successful machine-readable commands write only their documented JSON or JSONL to stdout and keep stderr empty. Failures write diagnostics to stderr; their messages redact resolved secret values and upstream output containing them. Usage errors happen before configuration loading, secret resolution, or process startup.

Doctor never emits resolved secret values, raw configuration paths, configured upstream command arguments, or the per-run synthetic redaction canary in its report. A `healthy` or `degraded` report exits `0`; warnings therefore do not block automation. A `failed` report has blocking checks and exits `1`.
| Exit | Category | Examples |
| --- | --- | --- |
| `0` | Success | Help, schema, validation, successful commands, and healthy or degraded doctor reports. |
| `1` | Operation | A failed doctor report or another uncategorized operational failure. |
| `2` | Usage | Unknown flags or commands, missing option values, duplicate options, misplaced options, or a command without its required `--config`. |
| `3` | Configuration | Missing or unreadable config, invalid JSON or schema, and invalid configuration references. |
| `4` | Secret resolution | A required environment or secret provider value could not be resolved. |
| `5` | Upstream | Upstream process startup, initialization, discovery, transport, or shutdown failures. |
| `6` | Policy | Runtime policy, routing-blocked, routing-ambiguous, or confirmation-required failures. |

Inspect or initialize an upstream:
Configuration errors can name an unresolved environment variable so it can be repaired, but never print a resolved secret value. Code `6` is reserved for policy failures surfaced by runtime operations; no standalone diagnostic command currently performs a policy-governed MCP operation.

```bash
miftah init github --preset github --output github.miftah.json
miftah list-tools --config github.miftah.json --profile work
miftah test-profile --config github.miftah.json --profile work
miftah logs --config github.miftah.json
## Audit logs

`logs` resolves the same configured secret-bearing maps as runtime startup, including named upstream and non-default profile maps, before it renders an audit record. Each complete JSONL record is parsed, normalized, and redacted before output. Configured secret values, URI userinfo, fragments, and URI query values are removed or redacted; malformed or invalid UTF-8 records become a fixed valid JSON marker instead of being copied through.

```sh
miftah logs --config "$HOME/Miftah configs/work wrapper.json"
miftah logs --config "$HOME/Miftah configs/work wrapper.json" --follow
```

Commands return non-zero status for unreadable, invalid, or unsafe configurations. Output is JSON where it is intended for scripts and never includes resolved secret values.
Without `--follow`, Miftah creates a stable finite snapshot before emitting it. If the file changes continually, it retries a bounded number of times and fails without emitting a mixed snapshot. Snapshot staging is private and removed after output or failure.

With `--follow`, Miftah polls at a bounded interval (250 ms by default), detects appends, truncation, copy-truncate rewrites, and replacement/rename rotation, and never keeps an audit file handle between polls. An absent file is treated as temporarily unavailable while following. `SIGINT` and `SIGTERM` stop the follower promptly, abandon pending output safely, remove signal listeners and temporary staging files, and do not start or signal an upstream process.

The reader uses fixed-size chunks and bounds an unterminated record at 64 KiB. This prevents an unbounded partial line from consuming memory, but means a record that exceeds that boundary is represented by the malformed-record marker rather than recovered. Audit output is an integrity and observability interface, not a replacement for retaining the original audit file under an external rotation and retention policy.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@
"dev": "tsup --watch",
"test": "vitest run",
"test:core": "vitest run tests/config.test.ts tests/config-loader.test.ts tests/config-diagnostics.test.ts tests/config-public-contract.test.ts tests/config-runtime-parity.test.ts tests/config-schema-contract.test.ts tests/presets.test.ts tests/profile-manager.test.ts tests/routing-policy.test.ts tests/secrets.test.ts tests/tooling-config.test.ts",
"test:package": "vitest run tests/package-contract.test.ts",
"test:coverage": "vitest run --coverage",
"smoke:cli": "node dist/cli/main.js schema",
"test:cli": "npm run build && npm run smoke:cli",
"test:cli": "npm run test:package && npm run smoke:cli",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"lint": "eslint .",
Expand Down
105 changes: 1 addition & 104 deletions scripts/check-pack.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,7 @@ import { spawnSync } from "node:child_process";
import { dirname, resolve } from "node:path";
import process from "node:process";
import { fileURLToPath } from "node:url";

const REQUIRED_PATHS = [
"LICENSE",
"README.md",
"dist/cli/main.js",
"dist/index.d.ts",
"dist/index.js",
"docs/cli.md",
"docs/library-api.md",
"examples/generic.miftah.json",
"package.json"
];

const ALLOWED_ROOT_PATHS = new Set(["LICENSE", "README.md", "package.json"]);
const ALLOWED_PATH_PATTERNS = [
/^dist\/(?:[A-Za-z0-9_.-]+\/)*[A-Za-z0-9_.-]+\.(?:d\.ts|d\.ts\.map|js|js\.map)$/u,
/^docs\/(?:[A-Za-z0-9_.-]+\/)*[A-Za-z0-9_.-]+\.md$/u,
/^examples\/(?:[A-Za-z0-9_.-]+\/)*[A-Za-z0-9_.-]+\.miftah\.json$/u
];
import { parsePackOutput, verifyPackPaths } from "./pack-verifier.mjs";

const scriptPath = fileURLToPath(import.meta.url);
const repositoryRoot = dirname(dirname(scriptPath));
Expand All @@ -38,91 +20,6 @@ function npmInvocation(args) {
return { command: "npm", args };
}

/**
* Formats package paths as an indented list for actionable verification errors.
*
* @param {readonly string[]} paths package-relative paths
* @returns {string} newline-delimited diagnostic text
*/
function formatPaths(paths) {
return paths.map((path) => ` - ${path}`).join("\n");
}

/**
* Checks that a package-relative path is normalized and matches an intentional publish allowlist.
*
* @param {string} path package-relative path reported by npm
* @returns {boolean} whether the path is safe and expected
*/
function isAllowedPath(path) {
if (path.startsWith("/") || path.includes("\\") || path.split("/").some((part) => part === "." || part === "..")) {
return false;
}
return ALLOWED_ROOT_PATHS.has(path) || ALLOWED_PATH_PATTERNS.some((pattern) => pattern.test(path));
}

/**
* Enforces the package path contract, including required entries, allowed patterns, and uniqueness.
*
* @param {readonly string[]} paths package-relative paths reported by npm
* @returns {string[]} a copy of the verified paths
* @throws {TypeError} when the input is not an array of strings
* @throws {Error} when paths are missing, duplicated, unsafe, or unexpected
*/
export function verifyPackPaths(paths) {
if (!Array.isArray(paths) || paths.some((path) => typeof path !== "string")) {
throw new TypeError("Package paths must be an array of strings.");
}

const duplicates = [...new Set(paths.filter((path, index) => paths.indexOf(path) !== index))].sort();
const unexpected = paths.filter((path) => !isAllowedPath(path)).sort();
const missing = REQUIRED_PATHS.filter((path) => !paths.includes(path));
const problems = [];

if (duplicates.length > 0) {
problems.push(`duplicate package paths:\n${formatPaths(duplicates)}`);
}
if (unexpected.length > 0) {
problems.push(`unexpected package paths:\n${formatPaths(unexpected)}`);
}
if (missing.length > 0) {
problems.push(`missing required package paths:\n${formatPaths(missing)}`);
}

if (problems.length > 0) {
throw new Error(`Package contract verification failed:\n${problems.join("\n")}`);
}

return [...paths];
}

/**
* Parses one `npm pack --dry-run --json` result and extracts its package-relative paths.
*
* @param {string} output JSON emitted by npm pack
* @returns {string[]} package-relative paths from the single packed artifact
* @throws {Error} when npm emits invalid JSON or an unexpected result shape
*/
export function parsePackOutput(output) {
let results;
try {
results = JSON.parse(output);
} catch (error) {
throw new Error("npm pack returned invalid JSON.", { cause: error });
}

if (!Array.isArray(results) || results.length !== 1 || !Array.isArray(results[0]?.files)) {
throw new Error("npm pack must return exactly one package with a files array.");
}

return results[0].files.map((file) => {
if (typeof file?.path !== "string") {
throw new Error("npm pack returned a file entry without a string path.");
}
return file.path;
});
}

/**
* Runs a real npm pack dry run from the repository root and verifies every reported path.
*
Expand Down
Loading
Loading