scrollport
v0.5.0
Published
scrollport CLI — provision an agent's key against one prepaid wallet.
Downloads
577
Readme
scrollport CLI
Provision an agent's key against one prepaid wallet.
npx scrollport setupTwo clicks, no typing, no per-provider signup. The command prints a link with the approval code already in it, opens your browser, and waits. You sign in, you approve, it stores the key. The key is never shown, never pasted, and never touches your shell history.
Published as
scrollport, sonpx scrollport setupresolves. The package name and the bin are now the same word, which is what the API's 401 hint has always promised.The root workspace package was renamed
scrollport-monorepoin the same change: with both namedscrollport,pnpm --filter scrollport …resolved to the root and silently ranpnpm -r test— the DB-backed API suites — instead of this package's hermetic ones.
Commands
| command | what it does |
| --- | --- |
| scrollport setup | device flow: get a key and store it |
| scrollport status | is there a key, and does the API still accept it? |
| scrollport logout | remove the key stored on this machine |
| scrollport list_apps [provider] | check which workspace apps are available and connected |
| scrollport search_tools <query> | find live catalog tools for a job |
| scrollport inspect_tool <tool-id> | read one tool's schema, worked example and teaching errors |
| scrollport run_tool <tool-id> --input '<json>'\|@file [--confirm] [--new] | submit one retry-safe paid intent and poll its persisted run |
| scrollport get_run <run-id> [--wait-seconds 0..120] | read status, result, artifacts and cost; never starts or bills |
| scrollport get_wallet | read available funds and human-controlled spend limits |
| scrollport help / version | |
The CLI is a thin client over the API's permanent nine control tools. The API
remains the authority for pricing, lifecycle, wallet holds and human approval.
search_tools and inspect_tool show the published customer unit rate. Single
lookups display a request price. Fractional usage shows its minimum billable
charge when declared; JSON preserves minimum_call_usd (null for new
request/result pricing).
When a run needs approval, the CLI prints the approval URL. --confirm only
waits for a human decision; it cannot approve or alter a spend limit.
Before submitting, the CLI stores a UUID for the canonical API/tool/input
intent under SCROLLPORT_HOME/run-intents without storing the input or API key.
If the process dies or the response is lost, rerunning the identical command
reuses that UUID and the API returns the original run. The file is cleared after
the API returns a durable run ID. Use --new only after checking Activity and only
when a second paid execution is intentional.
get_run defaults to a 50-second wait; --wait-seconds 0 returns immediately. It accepts no input or new-run flags. Retired control commands and run --resume are not supported.
Flags
| flag | |
| --- | --- |
| --json | machine-readable output (see below) |
| --api-url <url> | API origin. Default https://api.scrollport.com |
| --force | setup: provision a new key even if one already works |
| --no-browser | setup: print the link, open nothing |
| --offline | status: report what is on disk, skip the API check |
| --input <json>\|@file | run_tool: JSON literal or a local JSON file prefixed with @ |
| --confirm | run_tool: keep polling after printing a human approval URL |
| --wait-seconds <0..120> | get_run: bounded wait for a terminal state; default 50 |
| --new | run_tool: abandon a retained uncertain intent and intentionally create a fresh paid run |
Environment
| variable | |
| --- | --- |
| SCROLLPORT_API_KEY | a key to use instead of the stored one. Read, never written — this is the CI path, where the right store is your platform's secret manager |
| SCROLLPORT_API_URL | default for --api-url |
| SCROLLPORT_HOME | config directory (default ~/.scrollport) |
| SCROLLPORT_NO_BROWSER | set to anything to never open a browser |
--json
stdout carries exactly one JSON object. Everything else goes to stderr. That
holds for progress, warnings and errors alike, so JSON.parse(stdout) is always
safe.
Success:
{ "ok": true, "command": "setup", "account_id": "…", "api_key_masked": "sp_live_…9f2c",
"api_url": "https://api.scrollport.com", "credentials_path": "/home/you/.scrollport/credentials.json",
"verified": true, "already_configured": false }Failure — the API's own { error, message, hint } envelope, plus the exit code:
{ "ok": false, "command": "setup", "error": "access_denied",
"message": "The approval was declined on the consent screen.",
"hint": "Nothing was provisioned … run `scrollport setup` again for a fresh code.",
"exit_code": 3 }Fields present per command:
setup—already_configured,account_id,api_key_masked,api_url,credentials_path,verified(truewhen the API accepted the key,falsewhen it could not be reached to check).status—authenticated,source("file"or"env"),account_id,api_key_masked,api_url,credentials_path(nullfor an env key),verified(nullwhen unchecked), andpermissions_warningwhen the file is readable beyond its owner.logout—removed,credentials_path,env_key_still_set.search_tools—query,discovery(the API's typed capability/tool result).inspect_tool—tool_id,tool(the API's full inspection response).get_run—run_idplus the currentrunor approval state; terminal reads includeestimate. No idempotency key is used.list_apps— the account-scoped app inventory.run_tool—run_id,estimate,run_tool. A completed failed run returns JSON withok: falseand exit code1; API refusal errors retain their own teaching error and exit code.get_wallet—wallet(the API's current wallet response).
The API key itself is never emitted, in either mode. Masked only. A key in stdout is a key in a log, a CI artifact and a screen recording.
Exit codes
Agents branch on these, so they are part of the contract.
| code | meaning |
| --- | --- |
| 0 | success |
| 1 | unexpected error (a bug in the CLI) |
| 2 | usage: unknown command, bad flag |
| 3 | a human declined the approval |
| 4 | the code expired, or was already spent |
| 5 | the API could not be reached |
| 6 | no usable key — run scrollport setup |
| 130 | interrupted (Ctrl-C) |
Where the key is stored
~/.scrollport/credentials.json, file mode 0600, in a 0700 directory,
written atomically (temp file created with the mode already set, then renamed —
so the key is never briefly visible at your umask).
Why a file and not the OS keychain. The keychain is the better answer for a
desktop app and the wrong answer here. macOS Keychain, libsecret and Windows
Credential Manager all need a logged-in, unlocked user session; in a container,
over SSH, or in CI, that is not a degraded experience but a hang with no visible
prompt — and containers, SSH and CI are exactly where agents run. Every keychain
binding also drags in a native module whose prebuilt binaries are a recurring
npx failure. A file is what issue #12 specifies and what gh, aws and fly
all do, for the same reasons.
What the file has to earn in exchange, and does:
0600in a0700directory, verified on write and warned about on read if something has loosened it.- Atomic write, so an interrupted
setupcannot leave half a credential. - The key is never a command-line argument and is never echoed — the CLI writes it itself, straight from the poll response. Nothing about the happy path puts it in shell history or terminal scrollback.
SCROLLPORT_API_KEYis read but never written, so CI keeps its key in a real secret manager rather than in our file.
The residual risk, stated plainly: anything running as your user can read this
file. A keychain would narrow that to "anything running as your user that can
also satisfy an unlock prompt". If we ship a desktop-first surface later, a
keychain backend belongs behind credentials.ts — nothing above that module
assumes a file.
Development
pnpm --filter scrollport build
pnpm --filter scrollport test # hermetic: no server, no database
node packages/cli/dist/cli.js search_tools "research company funding" --api-url http://localhost:8787The tests stub fetch and point SCROLLPORT_HOME at a temp directory. They
stand up nothing and touch no database.
Workspace files
scrollport upload_file /path/reference.png streams a local file into private
workspace storage. scrollport upload_file prints a human drag-and-drop link;
scrollport upload_file --source-url https://example.com/reference.png imports
a directly downloadable public HTTPS file. Binary/base64 never goes into MCP.
scrollport get_files [file-id] [--query reference] [--offset 25] lists files,
refreshes signed reference URLs and reports the workspace's shared 1 GB usage.
Use the ready file URL in the inspected tool's reference field.
scrollport delete_file <file-id> permanently deletes an authorised file.
Uploads reserve space and expire after one hour if unfinished. On an uncertain
response, read the returned file ID before retrying to avoid duplicate uploads.
Existing agent connections need the files scope before managing storage; new
human-approved connections include it. Files stay until deleted or workspace
closure, except connected Google content's shorter retention.
