@papia/text-cli
v0.3.0
Published
papia — CLI for ALUPEC orthography on Cape Verdean Kriolu Markdown (lint, fix, suggest).
Maintainers
Readme
@papia/text-cli
papia is a Node CLI for ALUPEC orthography tooling on Cape Verdean Kriolu markdown:
papia lint— flag ALUPEC orthography violationspapia fix— apply auto-fixable correctionspapia suggest— call an LLM for replacement suggestions on each violation
Pure-stateless v1. Reads from stdin or a file path, writes to stdout. No filesystem-backed .papia.json sidecar yet — that lands in a future release.
Install
From npm (recommended)
npm install -g @papia/text-cli # globally — `papia` on $PATH
# or in a project
npm install -D @papia/text-cli
npx papia --versionThe bundle is self-contained: tsup inlines @papia/text-core and @papia/llm-core into dist/cli.js, so there are no @papia/* runtime dependencies to resolve.
Inside this monorepo
pnpm install
pnpm -C packages/text-cli build # produces a self-contained dist/cli.js
pnpm exec papia --version # prints 0.1.1From a local tarball
pnpm -C packages/text-cli build
pnpm -C packages/text-cli pack
# → packages/text-cli/papia-text-cli-0.1.1.tgz
npm install -g /absolute/path/to/papia-text-cli-0.1.1.tgzThe tarball is git-ignored (*.tgz), so rebuild whenever you bump.
Commands
papia lint
papia lint <file.md> # read a file, emit diagnostics to stdout
cat draft.md | papia lint # read stdin
papia lint --reporter json # JSON output (one document, stable shape)pretty (default) emits eslint-style path:line:col severity rule message lines, color-aware via process.stdout.isTTY. json emits { "diagnostics": [...], "summary": { files, errors, warnings } } — diagnostics can additionally carry an editors map per finding when editor flags are set (see Editor deep-links). markdown emits a GFM-table report with optional per-diagnostic editor deep-links — see Editor deep-links below.
Exit codes: 0 clean, 1 violations present, 2 runtime error.
papia fix
papia fix <file.md> # corrected text to stdout
papia fix <file.md> --write # rewrite the file in place
cat draft.md | papia fix # corrected text to stdout (stdin)--write requires a file argument; it is rejected (exit 2) when input comes from stdin. Auto-fixes are applied right-to-left so unfixed offsets stay valid; running papia fix twice on the same input is byte-identical.
Exit codes: 0 after a successful pass (whether or not fixes were applied), 1 when unfixable violations remain after the auto-fix pass, 2 runtime error.
papia suggest
GOOGLE_GENERATIVE_AI_API_KEY=… papia suggest <file.md>
papia suggest <file.md> --reporter json
papia suggest --config ./papia.config.json <file.md>Lints, then for each diagnostic asks @papia/llm-core's suggest() for a replacement. The pretty reporter appends a dim → Suggestion: … line under each diagnostic. The JSON reporter adds an optional aiSuggestion: string field per diagnostic.
papia suggest is sequential — one round-trip per diagnostic, no batching in v1. Pre-filter very large files if cost or latency is a concern.
Exit codes: 0 after a successful pass, 1 when violations remain, 2 runtime error or unresolved API key.
Editor deep-links
When --reporter=markdown or --reporter=json is used, papia lint can emit a clickable deep-link per finding so reviewers (and downstream tools) can jump straight from the report to the file in their editor.
papia lint --reporter=markdown --editor=skrebe drafts/lesson.md
papia lint --reporter=markdown --editor=skrebe,vscode,finder drafts/lesson.md
papia lint --reporter=markdown \
--editor-url-template='zed://file/{absPath:raw}:{line}:{col}' \
drafts/lesson.md
# JSON consumers — URLs embedded per-diagnostic under `editors`
papia lint --reporter=json --editor=skrebe,vscode,finder drafts/lesson.mdFor --reporter=json, each diagnostic gains an optional editors field:
{
"diagnostics": [
{
"ruleId": "ALUPEC_CHAR_CEDILLA",
"severity": "error",
"message": "Use 's' instead of 'ç' in ALUPEC",
"filePath": "drafts/lesson.md",
"line": 3, "column": 6,
"editors": {
"skrebe": "http://localhost:3001/editor/lesson?file=drafts%2Flesson.md",
"vscode": "vscode://file/.../drafts/lesson.md:3:6",
"finder": "file:///.../drafts/lesson.md",
"custom": ["zed://file/.../drafts/lesson.md:3:6"]
}
}
]
}The editors field is omitted entirely when no editor flags are set, so existing JSON consumers see a byte-identical schema unless they opt in.
Named editors (--editor=<csv>):
| Name | URL shape |
|---|---|
| skrebe | http://localhost:3001/editor/<bundle>?file=<encoded-relpath> (override host via --skrebe-host) |
| vscode | vscode://file<absolute-path>:<line>:<column> |
| finder | file://<absolute-path> (macOS) |
Custom editors (--editor-url-template=<template>):
Template placeholders are substituted with workspace-resolved values from each diagnostic. Substitutions are URL-encoded by default; append :raw to skip encoding.
| Placeholder | Meaning |
|---|---|
| {absPath} | Absolute path to the file |
| {relPath} | Path relative to the workspace root (cwd at invocation time) |
| {basename} | Filename without .md / .mdx extension |
| {line} | 1-based line number of the finding |
| {col} | 1-based column number of the finding |
| {absPath:raw} (etc.) | Same value, no URL-encoding |
Unknown placeholders are silently substituted with an empty string (a stderr warning is also emitted so typos like {absPth} surface during invocation).
Constraints (v0.1.1): --editor accepts a comma-delimited list rather than being repeated. --editor-url-template takes a single template; to emit multiple custom URLs, invoke papia lint once per template and concatenate.
--editor, --editor-url-template, and --skrebe-host apply to both --reporter=markdown and --reporter=json. They have no effect with --reporter=pretty — the CLI emits a stderr warning if any are set on a pretty-reporter invocation.
Configuration
The CLI walks up from the current working directory looking for papia.config.json. --config <path> overrides the walk-up; missing file at the explicit path is exit 2. Missing config is fine for lint and fix (no API key is needed); suggest falls back to the provider's default env var.
{
"$schema": "https://papia.studio/schema/papia.config.json",
"version": 1,
"aiProvider": "google", // optional — "google" | "anthropic" | "openai"
"aiModel": "gemini-2.5-flash", // optional — defaults to llm-core's default
"apiKey": { "envVar": "MY_KEY" } // optional — see precedence below
}API-key resolution precedence
papia suggest resolves the key in this order, stopping at the first hit:
apiKey.literalinpapia.config.json(discouraged for committed configs)apiKey.envVarinpapia.config.json→process.env[envVar]apiKey.keychain— not implemented in v1; the CLI returns a clear error pointing at the env var fallback- Provider default env var:
- Google →
GOOGLE_GENERATIVE_AI_API_KEY - Anthropic →
ANTHROPIC_API_KEY - OpenAI →
OPENAI_API_KEY
- Google →
If none yield a value, papia suggest exits 2 before contacting the LLM and names the env var on stderr.
Exit codes
| Code | Meaning |
|------|---------|
| 0 | Clean run (no violations, or fix succeeded) |
| 1 | Violations present (lint, suggest) or unfixable violations remain after fix |
| 2 | Runtime error: file not found, invalid config, unresolved API key, LLM failure, --write with stdin |
Limitations (v0.1.1)
- Single file or stdin per invocation. No directories, globs, or watch mode.
- No filesystem-backed sidecar (
.papia.json). State per invocation only. - No glossary integration;
suggestcalls usevariant: "general"and an empty glossary. - No OS keychain bridging.
apiKey.keychainparses but resolves toundefined. - No batching for
suggest— one LLM round-trip per diagnostic. --editoris comma-delimited (--editor=skrebe,vscode) rather than repeatable across multiple--editor=flags. Citty 0.2.2 does not exposemultiple: truethrough its declarative arg API; a future release may switch to repeatable.--editor-url-templateaccepts a single template per invocation.
Roadmap
Future releases will unlock filesystem-backed .papia.json sidecar state, folder and watch mode, and glossary management commands.
