Skip to content

CLI

No binary to install: you add the SDK to your own CLI. One report() call in your top-level catch reports the failure and returns a known fix if one exists.

import { createProblems, formatReport } from "@problemsdev/sdk";
const problems = createProblems({ slug: "acme", toolName: "acme-cli@2.1.0" });
try {
await runCommand(argv);
} catch (err) {
const result = await problems.report(err, {
surface: "cli",
operation: argv[0],
});
const guidance = formatReport(result);
if (guidance) console.error(guidance); // stderr keeps stdout usable for JSON
printError(err);
process.exitCode = 1; // retain your command's existing failure code
}

report() never throws and never changes control flow. It returns a ReportResult and swallows its own errors, so it’s safe in an existing catch: the CLI behaves as before, the failure is reported, and r.answer may carry a fix to show the user.

The second argument, ReportContext, is optional. goal and operation speed up triage:

await problems.report(err, {
surface: "cli",
operation: "deploy",
goal: "deploy the staging environment",
argKeys: ["env", "region"], // argument NAMES only, never values
versions: { node: process.versions.node, "acme-cli": "2.1.0" },
});

Two opt-in extras.

Catch network failures. A wrapped fetch reports any 5xx or network error your CLI makes, and passes responses through unchanged:

globalThis.fetch = problems.wrapFetch();

Observe uncaught crashes. This reports before the process dies. It only observes, and never changes crash or exit behavior:

const uninstall = problems.installProcessHooks();
// call uninstall() to remove it.

formatReport(result) returns plain text containing the report reference and, when available, the full answer, numbered steps, documentation links, and answer ID. Cached results are labeled. Failed or disabled reports produce an empty string. The formatter removes terminal control characters from text guidance. It does not print, change exit codes, or make requests.

Send human guidance to stderr using your CLI’s existing output handling. For a JSON command, embed the report in the command’s existing response instead of appending a second JSON document to stdout:

const output = { error: "Deployment failed", problems: result };
process.stdout.write(JSON.stringify(output) + "\n");

For a standalone report JSON value, use formatReport(result, { format: "json" }). This preserves the ReportResult fields, including failure reasons, and adds no trailing newline. JSON retains field values; parse it as data rather than printing decoded fields to a terminal without sanitization. Sending or receiving an answer does not mean the command succeeded; preserve your existing exit code.

Before normal shutdown, wait for background reports started by wrapFetch() or unawaited report() calls:

try {
await runCommand(argv);
} finally {
const flushed = await problems.flush({ timeoutMs: 1_000 });
// flushed.completed: all reports in this snapshot settled (success or failure)
// flushed.pending: reports still in flight when the budget expired
}

flush() defaults to a one-second wait. Zero checks without waiting; negative values also become zero and non-finite values use the default. It takes a snapshot of unique report deliveries already in flight. It excludes later reports, status/verification calls, and HTTP operations still awaiting a response or body capture. Await those operations before flushing.

The wait is bounded even if a custom fetch ignores cancellation. Expiring the wait does not cancel requests or retry failures; requests retain their own timeoutMs. A completed flush means settled, not necessarily delivered. The SDK does not install shutdown handlers or call process.exit(). Active requests may still keep Node alive after a flush times out; this is not a process-exit deadline. Forced exits and crashes remain best effort. flush() cannot make an uncaughtExceptionMonitor callback wait before Node terminates.