@j4k-oss/tropkod-client (3.0.0)
Installation
@j4k-oss:registry=npm install @j4k-oss/tropkod-client@3.0.0"@j4k-oss/tropkod-client": "3.0.0"About this package
@j4k-oss/tropkod-client
Stateless CLI client for the tropkod dependency-analysis API. It submits one natural-language question about a public npm or crates.io dependency, prints the created job id, and exits successfully while the hosted service analyzes the question against the dependency's real source. Inspect the returned job status and poll its id until it reaches a terminal state; opt into a synchronous wait with --wait-ms.
The client holds no state: it opens no local store, spawns no agent, and inspects no source. It only sends HTTP requests to a tropkod service and renders the response.
Install
The package is published to the code.j4k.dev registry under the @j4k-oss scope, which allows anonymous reads (no token needed):
npm config set @j4k-oss:registry https://code.j4k.dev/api/packages/j4k-oss/npm/
npm i -g @j4k-oss/tropkod-client
Authentication
Requests need a service URL and an API key. Keys are provisioned manually — there is no self-serve signup. Set both (or pass --url / --api-key):
export TROPKOD_URL=https://api.tropkod.ai
export TROPKOD_API_KEY=<provisioned-key>
TROPKOD_URL has no default: every request requires --url or TROPKOD_URL, otherwise the client exits 3 with requests require --url or TROPKOD_URL. The hosted deployment is https://api.tropkod.ai; point at a self-hosted service with its own URL.
Usage
tropkod-client "Does npm:fastify@5.6.0 expose schemaCompiler?"
Name the dependency inline as npm:<package>@<version> or crate:<name>@<version>.
The default submission returns immediately with a job envelope and its id. Pending, running, and completed jobs exit 0; failed jobs exit 1. When job.status is pending or running, poll about every 30 seconds until it is completed or failed. Use the same service URL and credentials for every poll:
tropkod-client --job <job-id>
For a pending or running job, human output labels a one-line JSON array as Poll argv (JSON data, not shell syntax; do not paste it into a shell):. Parse that JSON and preserve each array element as one argument when invoking the client; never evaluate it with a shell. The rendered URL is the normalized credential-free service URL, and the API key is never printed — configure the same TROPKOD_API_KEY before polling.
To wait synchronously instead, pass a wait budget: --wait-ms 120000 blocks up to two minutes for the answer.
Pending and running jobs are successful protocol responses, so they exit 0 and are safe to use under set -e and set -o pipefail. Scripts should branch on job.status, not an exit code:
set -euo pipefail
if submission=$(tropkod-client --json "Does npm:zod@4 expose z.url?"); then
status=$(printf '%s' "$submission" | jq -er '
.job.status | select(. == "pending" or . == "running" or . == "completed")
')
case "$status" in
pending|running) printf '%s\n' "poll the returned job id later" ;;
completed) printf '%s\n' "analysis is ready" ;;
esac
else
exit "$?" # failed job or an invocation failure
fi
Read the question from stdin when no positional argument is given and stdin is not a terminal:
echo "Does crate serde@1.0.197 expose Deserialize?" | tropkod-client
Stdin is read to EOF: an open pipe that never closes blocks the command indefinitely, outside the exit-code taxonomy entirely. Pass the question as an argument, or pipe from a source that closes. URL and credential errors are checked before stdin is read, so a missing --url or --api-key exits 3 immediately instead of blocking.
JSON output
--json makes every invocation that reaches a verdict print exactly one JSON document on stdout: .job present means the parsed submission envelope (the fields this client build knows — unknown server keys are stripped, and a null analysis is omitted); .error present means the invocation failed. Two exceptions: (1) --help/--version print human text and exit 0; (2) a crashed client exits with empty stdout — exit 7 from the bin wrapper's catch, or the runtime's own exit 1 on a crash outside the handlers. Empty stdout is never a verdict.
Discriminate both shapes in one expression:
tropkod-client --json "Does npm:zod@4 expose z.url?" | jq -r '.error.kind // .job.status'
A failed invocation prints an error envelope:
{
"error": {
"kind": "http-error",
"message": "invalid api key (HTTP 401)",
"status": 401,
"exit_code": 4
}
}
| Field | Meaning |
|---|---|
kind |
usage, transport, http-error, invalid-response, or unexpected |
message |
The failure message; what non-JSON mode prints to stderr (see the commander caveat below) |
status |
Numeric HTTP status; present iff kind is http-error or invalid-response |
job_id |
Present when an invalid-response body still carried a readable job id |
exit_code |
The process exit code, so stdout alone recovers the full verdict |
Without --json, a failed invocation prints the bare message on stderr and nothing on stdout. Exit 7 additionally writes the original error, stack included, to stderr in both modes — the envelope's one-line message is not a filable bug report.
One caveat for commander-originated usage errors (an unknown option, a rejected --wait-ms value): commander prints its own prose on stderr even under --json — the stdout contract still holds (exactly one JSON document) — and the envelope's message carries commander's wording (error: prefix, possible embedded newlines) minus the trailing (add --help for usage) hint, so it is close to but not byte-identical with stderr. The CLI's own errors match stderr exactly.
Flags
| Flag | Env fallback | Default | Notes |
|---|---|---|---|
--url <url> |
TROPKOD_URL |
none (required) | tropkod service URL; the hosted deployment is https://api.tropkod.ai; must be an http(s) URL with no credentials, query, or fragment, on a port fetch will connect to (the WHATWG bad-port list is rejected client-side) |
--api-key <key> |
TROPKOD_API_KEY |
none (required) | Provisioned manually |
--session <id> |
— | — | Reuse a session for follow-up questions |
--job <id> |
— | — | Fetch an existing job instead of submitting |
--wait-ms <ms> |
— | 0 |
Synchronous wait budget; 0 (the default) submits without waiting. Values above 300000 are rejected client-side, matching the server cap |
--json |
— | false |
Print the full JSON envelope |
Pass either a question or --job, not both. --session and --wait-ms apply only to submissions; when --job is given they are ignored.
Exit codes
| Code | Meaning | On a submit: job created? | On a poll (--job) |
|---|---|---|---|
0 |
Valid job state: pending, running, or completed (answered or unresolved). Also --help / --version. |
Yes — envelope printed, id in hand; inspect .job.status and poll while nonterminal |
A pending/running job remains to be polled; a completed job has its verdict |
1 |
Job failed — the server's own verdict, envelope printed. No other CLI verdict exits 1. |
Yes — id in hand | Verdict: failed |
3 |
Usage error — invalid flags/arguments/environment, including URL and key shapes the HTTP client is known to refuse before connecting; no HTTP request was dispatched. | No | Nothing was sent — fix the input, resume polling the same id |
4 |
HTTP error — non-2xx response; numeric status in message and envelope. The status line alone decides: an error body that stalls or resets loses only the message decoration, never the code. | 4xx except 408: no. 408 and 5xx: unknown (an intermediary may mask an accepted request) — branch on status |
The GET touched nothing: 401/403 → fix credential, keep polling; 404 → re-check the id (a confirmed-correct-id 404 ends the poll); 408/429/5xx → keep polling |
5 |
Transport failure — no response completed (network failure, the client abort after max(wait_ms + 5000, 30000) ms on a submit / the fixed 30 s poll timeout — including an abort mid-body of a 2xx response; a non-2xx with an unreadable body stays exit 4). |
Unknown — assume yes | Job untouched — keep polling |
6 |
Invalid response — 2xx received but the body was not a parseable submission envelope. | Yes — id recovered iff the envelope carries .error.job_id (the body held a readable job.id); lost otherwise |
Job untouched. A transient blip (truncated body, proxy garbling) recovers — retry once or twice. A repeated exit 6 with an identical message is deterministic schema drift, terminal for this client build: the job has likely already ended in a shape this client cannot render — report it, don't keep looping |
7 |
Unexpected client error (a bug in this CLI). | Unknown — assume yes | Job untouched — report the bug; polling the same id stays safe |
After a failed submit, resubmitting is safe iff code == 3 || (code == 4 && status < 500 && status != 408) — and only when every earlier submit in the task is accounted for. 408 is excluded for the same reason as 5xx: an intermediary can mint it after forwarding the POST. A failed poll never licenses a resubmit: GET /v1/jobs/<id> can neither create nor destroy a job — repair the input and keep polling.
--help and --version exit 0. Exit 1 with empty stdout is a crashed client, not a failed job: the discriminator is the envelope, not the code.
Scripts that must distinguish an answered result from an unresolved one should use --json and inspect analysis.status.
Orphaned jobs
A submit that dies before printing its job id — a harness-killed process, a transport failure (exit 5), an accepted response without a recoverable id (exit 6 with no .error.job_id; an exit 6 whose envelope carries .error.job_id orphans nothing), or a client crash (exit 7, or a bare runtime exit 1 with empty stdout) — orphans the job, not the analysis: the server never cancels a job because the client went away, and runs the paid analysis to completion on its single processing slot. The client has exactly two routes, POST /v1/queries and GET /v1/jobs/<id>; there is no job-listing or lookup route, so an id that was never printed is unrecoverable. --session does not help: it is submit-only input, and reusing it queues a new paid analysis behind the invisible first one. After losing a submit without an id, report and stop — do not resubmit the question. Exit 3, and exit 4 on a 4xx other than 408, are the codes that prove no job was created by the failed invocation.
Agent Rule
Reach for tropkod-client when you need to check a claim about a public npm or crates.io dependency ("does fastify@5.6.0 expose schemaCompiler?") against the dependency's real source rather than guessing from memory or package metadata. Run tropkod-client --help before first use to confirm the current flags, then submit one inline-named question (npm:<pkg>@<version> or crate:<name>@<version>) with --json. The default submit is non-blocking: it prints a job envelope and exits 0; inspect .job.status, and poll --job <id> --json while it is pending or running. Wait about 30 seconds between polls — a tight loop earns the 429 the table then tells you to poll through — and bound the whole loop at about 35 minutes, since the server's own analysis budget is 30. Branch on exit codes 1 and 3–7, and on .error.kind/.error.status instead of stderr wording; on polls, the loop ends at a completed or failed job envelope, a confirmed-correct-id 404, a repeated exit 6 with an identical message (deterministic schema drift — report, don't loop), or the overall bound expiring — a job still pending past the bound, or a service answering only exit 5/5xx for that long, means stop and report the job id, not poll on. It needs TROPKOD_URL and TROPKOD_API_KEY in the environment.
Development
This repo lints with @j4k/oxlint-config, which is published only to the code.j4k.dev registry under the private @j4k scope. pnpm install therefore needs a code.j4k.dev read token even though the repository is public:
npm config set //code.j4k.dev/api/packages/j4k/npm/:_authToken "$FORGEJO_NPM_TOKEN"
pnpm install
pnpm test
pnpm lint
pnpm typecheck
pnpm build
License
MIT. See LICENSE.
Dependencies
Dependencies
| ID | Version |
|---|---|
| @commander-js/extra-typings | ^15.0.0 |
| commander | ^15.0.0 |
| zod | ^4.4.3 |
Development dependencies
| ID | Version |
|---|---|
| @j4k/oxlint-config | ^2.11.0 |
| @j4k/semantic-release-forgejo | ^1.2.1 |
| @total-typescript/ts-reset | ^0.6.1 |
| @types/node | ^24.0.0 |
| @vitest/coverage-v8 | ^4.1.8 |
| conventional-changelog-conventionalcommits | ^9.3.1 |
| fta-check | ^1.5.2 |
| fta-cli | ^3.0.0 |
| knip | ^6.16.1 |
| oxfmt | ^0.57.0 |
| oxlint | ^1.72.0 |
| oxlint-tsgolint | ^0.23.0 |
| semantic-release | ^25.0.7 |
| typescript | ^7.0.2 |
| vitest | ^4.1.8 |