CLI
The clarkcant command
Ask Clark, read and create conversations, stop work or call any route, from a terminal or a script.
Run it from a checkout
The CLI is the workspace package @clarkcant/cli in apps/cli. It is not published to npm yet, so run it from a ClarkCant checkout:
# The CLI is the workspace package @clarkcant/cli (apps/cli). It is not on npm yet.
git clone https://github.com/digitopvn/clarkcant.git
cd clarkcant
corepack enable && pnpm install
node apps/cli/src/main.ts ask "hello"
pnpm clarkcant ask "hello"
# Optional: a shell alias so the examples below work as written
alias clarkcant="node $PWD/apps/cli/src/main.ts"
Develop a widget
The package author command clark supports the widget lifecycle from a checkout. It needs no runtime account or provider:
node packages/widget-cli/src/cli.ts widget init ./my-widget --template form
node packages/widget-cli/src/cli.ts widget dev ./my-widget
node packages/widget-cli/src/cli.ts widget test ./my-widget
node packages/widget-cli/src/cli.ts widget pack ./my-widget
These are clark widget init/dev/test/pack when the workspace bin is on your PATH. Dev runs the widget in the isolated browser host on loopback. Its service panel simulates each declared capability as loading, ready, blocked or unhealthy, and lets you test offline, degraded and restart recovery with package fixtures. It does not start a real service or call a provider.
A binding to a capability that runs as a package job uses a job fixture in fixtures/dev-host-services.json instead of an outcome: its progress steps, the output it completes with and the error it fails with. A press then starts a simulated job, and the Simulated jobs list steps it with Next step, Complete or Fail; the widget's own cancel works too. Every ending says “(simulated by clark widget dev)”, nothing is written to disk, and clark widget test checks that exactly the job capabilities have a job fixture.
The semantic inspector shows the normalized proposal the runtime keeps, what was truncated or dropped, the delta, the context note and inspect_ui output. The composition panel sends declared events through their shared validators and records widget-emitted events. Invalid or undeclared events are refused; simulation stays local and cannot invoke capabilities. clark widget test checks fixture semantic limits and declared event schemas before pack builds the artifact and digest.
--template accepts blank, form, dashboard, pure-ui, ai-generator, ui-with-service, media-tool and connected-app today. pure-ui copies the reference text editor under your new package's own id, facet id and name, without the editor's tests, so you start from a working app that passes clark widget test. ai-generator and ui-with-service copy the reference image generator the same way: ai-generator keeps the provider, with the placeholder origin https://images.example.com to replace, and ui-with-service has a service that draws the image itself, declares no provider and only reads. media-tool copies the reference media render tool, a widget and a service, under your new package's own id, without the tool's tests. connected-app copies the reference connected app under your new package's own ids and name: a widget, a service whose capabilities name the scopes they need on one declared account connection, skills, the fake connector it is tested against, and the service's portable dev/service.test.mjs. Replace the provider, client id, scopes and endpoints with your provider's before you publish. The editor and media templates and the MCP App adapter are not implemented yet.
node packages/widget-cli/src/cli.ts widget init ./my-editor --template pure-ui
Package and publish a widget on npm
When a package has a package.json, clark widget pack builds its npm archive with pnpm pack into dist/<name>-<version>.tgz. It then extracts the archive with the same reader your node uses to install it, and refuses the archive unless it passes the conformance suite on its own, holds the same clarkcant.json and contains nothing credential-shaped, such as .npmrc, .env files, .git-credentials, private keys, node_modules or .git. A files list that leaves out something the widget needs is caught here, not on someone else's machine. clark widget init writes a package.json for every template. A package without one stays a local or git package, and pack builds no archive.
Pack refuses a package.json that breaks these rules, and names each one:
nameis a valid npm name, andversionis the version inclarkcant.json.licensematches the manifest'spublisher.license.keywordsincludeclarkcantand one keyword per facet kind, such asclarkcant-widgetorclarkcant-service. A Marketplace finds packages by them.- An explicit
fileslist, no dependencies, and nopreinstall,installorpostinstallscript. A package ships what it runs. - No
prepack,prepareorpostpackscript. Pack runs no package code, so ship the files a script would generate.
The generated package.json is named after the last part of your package id, and that name may already be taken on npm. Rename it, or use a scope you own such as @you/my-widget, before you publish. Pack needs pnpm (corepack enable pnpm). Adding a package.json to a package you already packed changes its author digest, so bump the version when you add it.
dist/artifact.json records three digests, and each answers one question:
npm.integrity: are these the bytes the registry serves? It is npm's sha512 of the archive.npm.contentDigest: is what your node extracted the package the listing named? Your node computes it after fetching that exact npm version, and refuses the install when it differs.authorDigest: did this version's files change since it was packed? Pack refuses to repack a version whose files changed; bump the version instead.
clark widget publish prepares the directory entry and uploads nothing. The entry names the exact npm version and the archive's content digest. The command prints three outcomes separately (prepared: yes; published to npm: no; Marketplace submission: no) and the command that publishes the archive it checked:
node packages/widget-cli/src/cli.ts widget pack ./my-widget
node packages/widget-cli/src/cli.ts widget publish ./my-widget
npm publish ./my-widget/dist/my-widget-0.1.0.tgz
Publish that file rather than running npm publish in the package folder, so the registry serves exactly the bytes the entry names. A Marketplace then lists npm packages that carry the clarkcant keyword. --source local prepares an entry for the package's own folder instead, which needs no npm account.
Reference app: a text editor
examples/reference-apps/text-editor is a whole widget built only on the public widget contracts: one isolated UI facet, with no service, no permissions and no requested capabilities. You open a text file, edit it, save it and ask Clark to rewrite a selection, and the widget never learns where the file lives.
- Open. The app's own file prompt, outside the widget, accepts plain text, Markdown, CSV and JSON. The editor refuses a file over 1 MiB before reading it, reads the rest in 256 KiB pieces, and refuses bytes that are not valid UTF-8 with the reason, instead of showing replacement characters a save would write back.
- Edit. The unsaved draft is kept in the widget's state, so a reload, a pinned copy or another device shows the same draft. A draft too large for the state is not cut: the editor says it will not survive a reload. When two views of the same editor both changed the draft, it offers Keep mine and Use theirs and throws neither away.
- Save. The editor writes a finished copy and hands it to the app (files a widget holds). In the desktop app you can replace the original, but only in the widget where you picked the file and only until it reloads; after that, or in a pinned copy or a detached window, the desktop offers Save As. In a browser the copy downloads. Ctrl+S (Cmd+S on macOS) saves, and Attach puts a copy in the composer.
- What Clark sees. A short summary, the file name, line count, whether there are unsaved changes, and the selected range with an excerpt of at most 200 UTF-16 units. Your machine bounds and redacts it and marks it as the widget's own words.
- Rewrite a selection. The editor's own button presses an
agentbinding that reads the selection and the widget (contextRefs: ["selection", "widget"]), named by therewriteBindingprop. It asks only about one line of at most 200 UTF-16 units that the app would pass to Clark unchanged, so a selection with hidden characters, unusual spaces or text the app redacts as possibly private is refused with the reason. Before the press runs, the app makes sure Clark reads the selection the editor just published (a press that reads what a widget shows). Clark's reply counts as a proposal only when it is exactly one closed fenced block. The editor shows it next to the text it would replace and changes the text only when you accept it and the range still holds the text Clark read; Ctrl+Z undoes it.
In a real install, Clark binds the rewrite button when it places the editor with that button, through its place_widget tool; without a binding the button stays disabled, with its reason shown. The editor also offers Clark the action replaceSelection (actions Clark asks a widget to perform), so a request typed in the composer, such as “make the second line shorter”, can replace the selected text through your execution policy. The editor refuses it when the selection no longer holds the text Clark read. The change is an unsaved edit, and saving stays yours. Saying the action's label while the editor is focused runs it through the same path and execution policy as asking Clark in the composer, on a page that can reach the editor's frame (digitopvn/clarkcant#444). When your policy asks first, the approval card appears in the conversation and you can answer it by click or out loud. A spoken answer decides only when every word is a yes word, such as “yes”, “ok” or “đồng ý”, or every word is a no word, such as “no”, “cancel” or “không”, apart from fillers like “please” or “nhé”. A question, a mix, or anything else, such as “not ok” or “chưa được”, gets the question again. Afterwards voice says what happened, with the widget's answer read as the widget's own words. A sentence that implies the action without saying its label, such as “make this shorter”, is not matched by voice yet; ask Clark instead.
Check the reference app from a checkout:
node packages/widget-cli/src/cli.ts widget test examples/reference-apps/text-editor
node packages/widget-cli/src/cli.ts widget pack examples/reference-apps/text-editor
Reference app: a spreadsheet
examples/reference-apps/spreadsheet is a second whole widget built only on the public widget contracts: one isolated UI facet and no service. It works on a file, keeps a large sheet within bounds, describes itself to Clark and applies a change Clark chose.
- Files. Import CSV/TSV opens the app's own file prompt (files a widget holds), and the sheet reads the file in 256 KiB pieces without ever seeing where it lives. The app now accepts tab-separated files (
text/tab-separated-values,.tsvor.tab) under the same checks as CSV; a binary file named as TSV is still refused. Export CSV and Export TSV write a new file with a byte-order mark, as the app's own table export does. XLSX is not supported. - Bounds. At most 25,000 cells, 64 columns and 5,000 rows are loaded, and no file is read past 8 MiB. The cell bound counts the rectangle the rows make, so a ragged file is cut where that rectangle stops fitting. A notice says how much is shown and that an export writes only that part. Rows and columns are drawn only while in view, so a large sheet stays responsive.
- What is kept. The widget's state holds the source file's reference, the edits since and the formats, never the sheet itself; the active cell and the selection stay in the frame. Right after an import the sheet is written to a file of the widget's own, because the grant on a file you chose lasts 24 hours. When the edits outgrow 10 KiB of the 16 KiB a widget's state may hold, the whole sheet is written to a new file of its own the same way, and the widget asks the app to discard the file it replaced. If such a write fails, the status line says so and the next edit tries again.
- Opening. On mount the sheet is read again from its source. Until then the grid takes no edits and the buttons wait. If the source cannot be read, the status line says so, the grid takes no edits and nothing is saved, so the saved sheet is still there next time. Importing a file starts over from that file.
- Formulas. A closed set: arithmetic, cell and range references, and
SUM,AVERAGE,MIN,MAXandCOUNT. A parser builds a tree that the widget walks, so no text is ever run as code. Errors are values (#DIV/0!,#VALUE!,#REF!,#NAME?,#PARSE!,#NUM!,#LIMIT!), and a circular reference is#CIRC!with its cells named in the notice.#LIMIT!marks only a formula that reads too far and the formulas that read it; a running total such as=SUM($A$1:A3000)down 3,000 rows fits. - Safe exports. An export carries computed values, never formulas. Text that starts with
=,+,-,@, a tab or a carriage return is written behind a', as the app's table export does, and so is text the sheet would read back as something else, such as007or1e3. The sheet reads a leading'as text, so an exported file imports back to the same values. Another spreadsheet application shows that'as part of the text. - What Clark sees. The selected range in A1 form, an excerpt of at most 12 rows by 8 columns, the active cell's formula and value, and the sheet's size, sized to fit within the app's limits so nothing is cut.
- Format through Clark. Ask Clark to format as percent presses an
agentbinding that reads the selection and the widget (contextRefs: ["selection", "widget"]), named by theformatBindingprop; without that prop the button is disabled. The press sends nothing: the app reads the range from what the widget published (a press that reads what a widget shows) and asks Clark for exactly one line,format: percent <range>. The widget treats the reply as untrusted. It accepts onlyformat: percent|number|plain <range>, and applies it only when the range is the one selected at the press, which stays locked until the reply arrives; otherwise it says so and changes nothing. The sheet keeps at most 32 formats and names one it had to drop. Undo format, or Ctrl+Z in the grid, takes back Clark's change. - Keyboard, pointer and touch. The grid is one tab stop, and Tab and Shift+Tab leave it, so the buttons stay reachable. Arrows move, Shift extends the selection, Home/End and Ctrl+Home/End jump, Page Up/Down pages, Enter or F2 edits, typing starts an edit, Escape cancels or collapses a range, and Delete clears the selection. A mouse selects by click, Shift+click or drag. On touch a tap selects a cell, a swipe scrolls, and Select range makes the next taps extend the selection. Buttons are at least 40 px high.
In a real install, Clark's place_widget tool places the sheet and binds both its offered format action and, when asked, the format button. The sheet offers Clark format ({ format: percent | number | plain, range? }, see actions Clark asks a widget to perform), so a request typed in the composer, such as “format this as a percentage”, formats the given range, or the selection when none is given, through your execution policy. The sheet refuses while it is busy or read-only, for an unknown format, and for a range it cannot read or that is past its bounds. “Undo format” steps back Clark's format like any other.
Check the reference app from a checkout:
node packages/widget-cli/src/cli.ts widget test examples/reference-apps/spreadsheet
node packages/widget-cli/src/cli.ts widget pack examples/reference-apps/spreadsheet
Reference app: an image generator
examples/reference-apps/image-generator is a package with an isolated UI facet and a service facet. A widget starts long work on its service, follows it as a job and gets an image back as a file, while the service reaches a provider with a key it never holds.
- Capability. The service offers
com.clarkcant.reference.image-generator.image.generate@1, run as a job and declaredexternal-write. A press answers at once with a JobRef; the image is made by a job your node owns, so it goes on while the widget is closed, reloaded or open on another device. - Why
external-write. Asking a provider to draw does work on someone else's service and spends your quota there. It is also what lets the service start the image with a POST whose prompt is in a JSON body: for areadcapability your node sends only GET and HEAD, and a prompt in a URL ends up in more logs than a body does. Under the default autonomous policy a press just runs; if your policy asks before external writes, the app's approval card comes first and the widget says the press is waiting. - Provider and key. The package declares one origin and one secret,
IMAGE_PROVIDER_KEY. The service starts an image, reads its status once per step and fetches the PNG only through your node's egress, which adds the key as a bearer header. The service, its container and the widget never receive the key. Until you store it for the package, the button is off with your node's reason. The provider in the repository is a fake one in the package's tests: it answers only requests carrying the key, refuses a prompt in the URL and returns a deterministic image. - Progress and Stop. Each step the provider finishes is reported by the service, and the widget shows that; it estimates nothing. Every running job has its own panel with its progress and its own Stop (Dừng), which cancels that job, and the service stops asking the provider.
- Errors. When the service reports an error, the failed job keeps the service's own words, quoted inside your node's sentence (a failed job may carry the service's own words). The widget shows them quoted as the service's, inside its own sentence. A key the provider echoes back appears as
[redacted]. - Gallery. When the app offers
jobs.list@1, the widget lists its own jobs, follows the open ones and reads finished images asartifactRefs in 256 KiB pieces, so a reload or another device shows the same jobs. On an app without it, the gallery holds the jobs started while the widget is open and says so. Each image can be attached to the conversation or exported; the widget learns only whether the app took it. - Widget, Clark and voice. Tạo ảnh presses an
invokebinding named by thegenerateBindingprop, which takes the prompt from the draft in the widget's state, or from what you said when there is input. Ctrl/Cmd+Enter generates. Saying the button's label with the widget open presses it, and the reply says the job started. Clark'sinvoke_capabilitystarts the job through the conversation's widget that has a binding to it, so that widget follows the job; with none, nothing runs and Clark says so, and with several, Clark names one by its instance and binding ids. Clark is refused a binding that would ask Clark itself, because that press would be sent as your own message. - Language. Like the text editor, the widget's own text is Vietnamese only; the app translates its own chrome, not a widget's text.
Clark places the image generator with its Tạo ảnh button through its place_widget tool, which binds the button to the package's own image.generate capability (digitopvn/clarkcant#445). A button binds only to a capability served by the package the widget comes from, and sends only inputs that capability declares. Until a provider key is stored, the button is disabled with your node's reason. No real image provider is wired up yet; that is digitopvn/clarkcant#321. An attached image's chip reads untitled.png, because a job's result file carries no name.
Check the reference app from a checkout, or start your own from it:
node packages/widget-cli/src/cli.ts widget test examples/reference-apps/image-generator
node packages/widget-cli/src/cli.ts widget pack examples/reference-apps/image-generator
node packages/widget-cli/src/cli.ts widget init ./my-generator --template ai-generator
Reference app: a media render tool
examples/reference-apps/media-render is a package with an isolated UI facet and a service facet. It renders a WAV clip you pick, with a gain change and a trim, as a job the widget follows and you can stop. The service reads the clip from your node a piece at a time and never gets a path or a handle to it (files a service reads).
- Capability. The service offers
com.clarkcant.reference.media-render.render@1, run as a job and declaredread. It names itssourceargument as a file ininputArtifacts, and takes a gain from −24 to +12 dB and an optional trim from the start and the end in milliseconds. The service declares no egress, and areadcall that holds a file could not use any. - Profile. The package asks for
background-computeby name (what a package runs with). Your node refuses a clip over that profile's 25 MiB input cap before the call is sent. The service refuses a clip longer than the profile's hour, or a render larger than one result may carry, before it renders anything. - Pick and render. Chọn tệp WAV opens the app's own file prompt, and the widget keeps only the file's
artifactRef(files a widget holds). Dựng presses aninvokebinding named by therenderBindingprop with the clip's id and the parameters, and the widget keeps the JobRef in its state, so a reloaded frame follows the same render. - Progress and Stop. Progress is the service's own, in bytes rendered; the widget estimates nothing. Dừng dựng, or Escape, cancels the job: the service stops reading and does not answer, so no partial file is kept. A stop your node refuses says why and offers Stop and Escape again. A new press replaces the shown render only once your node accepts it, so a refused press keeps the earlier result.
- Result. Only a completed job offers its file. The widget reads it back and draws a waveform, with the duration, the format and your node's sha256 digest. There is no audio player, because the widget frame's policy allows no media source. Đính kèm vào cuộc trò chuyện puts the file in the composer, and Lưu bản dựng goes through the app's save prompt. A completed render whose file your node could not keep says what failed and that the source file is untouched.
- Unavailable profile. When your node cannot grant
background-compute, because a policy rule refuses it or the container engine is too small, the service is not started. Render is disabled and the node's reason is shown in its place. - Touch. The gain field asks for a text keyboard, because a decimal keypad on iOS has no minus key; it accepts a typographic minus and a decimal comma. Every button and field is at least 44 px tall.
- Language. Like the other reference apps, the widget's own text is Vietnamese only.
Only 16-bit PCM WAV, mono or stereo, is rendered; there is no other codec. Clark places the widget with its Dựng button through place_widget, bound to the package's own render capability (digitopvn/clarkcant#445). Its browser tests need a container engine that runs Linux containers.
Check the reference app from a checkout, or start your own from it:
node packages/widget-cli/src/cli.ts widget test examples/reference-apps/media-render
node packages/widget-cli/src/cli.ts widget pack examples/reference-apps/media-render
node packages/widget-cli/src/cli.ts widget init ./my-render --template media-tool
Reference app: a connected app
examples/reference-apps/connected-app is a package that works on your account at a provider without ever holding it: a widget that lists tasks and renames one, a service that calls the provider, and skills that tell Clark how. Your node connects the account and signs the service's requests; the widget sees only a status (connecting an account).
- One declared connection. The tools facet declares
connection: the provider, the public client id, the authorization, token and revocation endpoints, the scopes with what each is for, the endpoints the service may reach with the account, and a probe. - Capabilities.
com.clarkcant.reference.connected-app.list-tasks@1isreadand needstasks.read.com.clarkcant.reference.connected-app.update-task@1renames a task, isexternal-writeand needstasks.write, so your execution policy decides it. - The widget. Tải công việc lists the tasks and Lưu saves a new title, through the bindings named by the
listBindingandupdateBindingprops. When a capability is not ready, the widget shows the node's reason, for example that the connection was revoked or did not granttasks.write. Like the other reference apps, the widget's own text is Vietnamese only. - One capability for the widget, Clark and voice. The widget's buttons, Clark's
invoke_capabilityand a spoken command reach the same capabilities through the same policy and the same audit log. The skill tells Clark never to ask you for a password, token or code, to say the node's reason when a capability is not ready, and not to resend a rename whose outcome is unknown. - The fake connector.
dev/fake-connector.mjsis a test and development fixture, not a live provider: a tiny OAuth server and task API on loopback port 8880, with an admin listener on 8881 for tests only. It has no real account and no client secret, and every code and token it issues is a random test value kept only in its memory. Your node reaches its loopback endpoints only when it runs withCC_EGRESS_ALLOW_PRIVATE_NETWORK=1.
No live provider ships yet; it is tracked in digitopvn/clarkcant#333. The desktop app opens only HTTPS addresses in the system browser, so you connect the loopback fake connector from the browser client; real providers use HTTPS. Clark places the widget with its list and rename buttons through place_widget, bound to the package's own capabilities (digitopvn/clarkcant#445).
Check the reference app from a checkout, try it against the fake connector, or start your own from it:
node packages/widget-cli/src/cli.ts widget test examples/reference-apps/connected-app
node packages/widget-cli/src/cli.ts widget pack examples/reference-apps/connected-app
node --test examples/reference-apps/connected-app/dev/service.test.mjs
node examples/reference-apps/connected-app/dev/fake-connector.mjs
node packages/widget-cli/src/cli.ts widget init ./my-tasks --template connected-app
Build a theme
The package author command clark also supports data-only theme facets. Run it from a checkout; no runtime account or provider is needed:
node packages/widget-cli/src/cli.ts theme init ./my-theme
node packages/widget-cli/src/cli.ts theme dev ./my-theme
node packages/widget-cli/src/cli.ts theme test ./my-theme
node packages/widget-cli/src/cli.ts theme pack ./my-theme
These are clark theme init/dev/test/pack when the workspace bin is on your PATH. Init writes a generalized package manifest and a checked JSON theme. Dev binds to 127.0.0.1:4319 (--port overrides it) and reloads edited data while retaining the local preview draft. Stop it with Ctrl-C. Theme Lab renders production conversation, composer, widgets, controls, Settings, modal, approval, error, status and Orb examples; its example interactions never operate your runtime. Switch scheme, normal/phone/compact width and reduced motion, and inspect compiled tokens and recipes.
Test audits arbitrary theme documents in both schemes: text/focus contrast, protected states and edges, bounded typography, reduced motion, contained regular assets, manifest and data-only execution. Themes accept no raw CSS, HTML, scripts, external resources or font URLs. Package symlinks are refused. Browser layout and keyboard checks remain explicitly requires-dev-host; a token audit does not certify a browser journey. Pack uses the existing immutable artifact format, includes theme document digests, records unverified checks and refuses changed bytes under the same version. Increase the version before packing an edit.
Connection
| Flag | Environment | Default |
|---|---|---|
--url | CLARKCANT_URL | http://127.0.0.1:8765 |
--token | CLARKCANT_TOKEN | Read from identity.json in the data dir, only for a node on this machine (localhost, 127.x, ::1); a remote --url needs --token, so the local token never leaves the machine |
--data-dir | CLARKCANT_DATA_DIR | ~/.clarkcant |
--json | – | Print raw JSON |
On the machine that runs the node, with the default data dir, it needs no flags. For a node elsewhere, set the URL and token:
export CLARKCANT_URL="https://clark.example.com"
export CLARKCANT_TOKEN="<token>"
clarkcant status
Commands
| Command | What it does |
|---|---|
clarkcant ask "<text>" [-c <conversationId>] | Streams Clark's reply to stdout. Without -c it creates a conversation and prints its id on stderr. When the message joins a reply Clark is already writing (resolution: "steered"), it says so on stderr and exits 0; read the conversation for the answer. |
clarkcant status | Health and node. |
clarkcant conversations | Lists conversations. |
clarkcant new [title] | Creates a conversation. |
clarkcant read <conversationId> | Prints the conversation. |
clarkcant stop | Emergency stop. |
clarkcant api <METHOD> <path> [jsonBody] | Raw call to any REST route except a person's decision (approving a guarded action, deciding a package capability, confirming an app intent, reporting what the app did with an action the agent asked for, trusting a paired peer or issuing a grant, installing the update a notice names) or a table's CSV export, which answers 403 PERSON_ONLY. |
clarkcant mcp | stdio MCP server bridged to the node's /mcp (see MCP). |
clarkcant discover | Prints /.well-known/clarkcant.json. |
clarkcant instructions check [file|folder] | Checks a project instructions file against the shared contract. It is the one command that runs offline, without a node. |
Examples
clarkcant status
clarkcant discover
clarkcant ask "how do I connect Cursor to you?" # new conversation; its id is printed on stderr
clarkcant ask "and Claude Desktop?" -c <conversationId>
clarkcant conversations
clarkcant new "Release notes"
clarkcant read <conversationId>
clarkcant api GET /node
clarkcant api POST /conversations '{ "title": "From the CLI" }'
clarkcant stop
Because the reply goes to stdout and the new conversation id goes to stderr, ask composes with pipes and files like any other command.