Toolbench

Manifest

tool.json, the run function, and cases.json: every attribute and rule.

A tool is a directory with three files: tool.json, the manifest; index.ts, the module whose run computes the result; and cases.json, the known answers its fixtures check.

Manifest attributes

The keys of tool.json.

AttributeTypeRequiredDefaultSinceNotes
sdknumberyesv1The contract version this tool was written against.
idstringyesv1Stable, lowercase, hyphenated. Also the directory name and the URL segment.
namestringyesv1The tool's title, shown as its heading.
blurbstringyesv1One sentence. Shown on cards and used as a page description.
versionstringyesv1The tool's own version, independent of sdk.
capabilitiesCapability[]yesv1What the tool is allowed to do. At least one; pure is the only capability today.
inputsInputSpec[]yesv1The form, one control per entry, in order. At least one.
kindsOutputKind[]yesv1Every kind run can return, so a host can refuse a tool it cannot draw.
runtimeobjectyesv1How the runtime loads and runs the tool.
status"live" | "deprecated" | "retired"nov1Lifecycle. Default live. deprecated still runs, with a visible marker a host can style, and is refused as a live card. retired does not run: the element explains and renders links, so a bookmarked URL is not a 404.
card"live" | "info" | "none"nov1How the tool appears in a compact slot: runnable, a summary that links out, or not at all.
cardFieldsnumbernov1How many fields a card shows before it stops. Errors are never truncated.
autoRunbooleannov1Re-run as the reader types, instead of waiting for the Run button. Off by default, and that default is deliberate. A tool that runs on every keystroke does work nobody asked for, makes the Run button look broken (the answer is already there), and takes the decision away from the reader. Opt in only for a tool that is genuinely instant and reads better live, a unit converter say. Anything with a worker or a heavy loop should leave it alone.
resetbooleannotruev8false hides the runtime's Reset control, which puts every input back to the values the tool opened with: for a tool with one input, where retyping it is the reset.
timeoutMsnumbernov1Worker mode only. A tool that declares this on the main thread is rejected: nothing there can be stopped.
helpstringnov1Markdown file, relative to the tool's directory, rendered as the tool's help.
tagsstring[]nov1Free-form labels a host can filter or group tools by.
linksLink[]nov1Anything the tool wants to point at: a spec it implements, a post explaining it, a reference decoder. Deliberately generic: a host site's own content relations are the host's business.
samplesSample[]nov3Example inputs, rendered as a row of buttons under the form. Clicking one fills the form. It is an input change like any other, so the result goes stale and waits for Run, or re-runs if the tool set autoRun. A compact card does not show samples at all, because it has room for one input and a Run button, and a row of buttons there would crowd out the result the card exists to show.

runtime

AttributeTypeRequiredSinceNotes
entrystringyesv1Entry file, relative to the tool's directory.
thread"main" | "worker"nov1main is the default and costs nothing. worker is for a tool whose running time depends on its input. It is the only mode where a timeout can actually stop anything.
JSON
{  "runtime": {"entry": "index.ts", "thread": "worker"},  "timeoutMs": 2000}

From tools/regex-explainer/tool.json.

Each entry of links.

AttributeTypeRequiredSinceNotes
labelstringyesv1The link text.
hrefstringyesv1An http, https or mailto URL, or a relative path. Anything else is refused.
JSON
{  "links": [    {      "label": "UTF-8 (RFC 3629)",      "href": "https://www.rfc-editor.org/rfc/rfc3629"    }  ]}

From tools/utf8-bytes/tool.json.

Sample

Each entry of samples, a button under the form that fills the inputs.

AttributeTypeRequiredSinceNotes
labelstringyesv1What the button says. Short: it sits in a row with the others.
inputInputValuesyesv1Values keyed by input id. Partial on purpose: a sample may set one field and leave the rest as the reader left them.
JSON
{  "samples": [    {      "label": "Two clusters",      "input": {        "values": "2 2 3 3 3 4 4 4 5 5 5 6 6 7 7 180 190 195 210 240 260 300 340 900"      }    },    {      "label": "Definitions disagree",      "input": {"values": "3 4 4 5 6 7 9 14 40 260", "method": "linear"}    }  ]}

From tools/percentiles/tool.json, the first two samples.

Capabilities

CapabilityMeaning
pureNo network, no storage, no clock, no randomness that is not seeded: the same input always gives the same result

Example

JSON
{  "sdk": 3,  "id": "histogram",  "name": "Histogram",  "blurb": "Bins a list of numbers and draws the counts as bars, with the running total on a second axis.",  "version": "1.0.0",  "capabilities": ["pure"],  "runtime": { "entry": "index.ts", "thread": "main" },  "kinds": ["series", "error"],  "card": "live",  "autoRun": true,  "inputs": [    {      "id": "values",      "type": "textarea",      "label": "Measurements",      "description": "Numbers separated by spaces, commas or newlines. Units are yours; the tool does not care.",      "dir": "ltr",      "rows": 4,      "maxLength": 20000,      "default": "2 3 3 4 4 4 5 5 5 5 6 6 6 7 7 8 9 11 14 18"    },    {      "id": "bins",      "type": "number",      "label": "Bins",      "description": "Equal-width buckets between the smallest and largest value. More bins show finer shape and more empty buckets.",      "unit": "bins",      "default": 8,      "min": 2,      "max": 30,      "step": 1    }  ],  "samples": [    {      "label": "Even spread",      "input": { "values": "1 2 2 3 3 3 4 4 5", "bins": 4 }    },    {      "label": "One spike",      "input": { "values": "1 1 1 1 1 1 1 1 1 20" }    },    {      "label": "All the same",      "input": { "values": "7 7 7 7 7" }    },    {      "label": "Not a number",      "input": { "values": "2 3 4 nope 6" }    }  ],  "help": "README.md",  "tags": ["statistics", "chart"]}

tools/histogram/tool.json.

The tool module

index.ts default-exports an object with run.

TypeScript
import type { Tool } from "@toolbench/sdk"; type Input = { values: string; bins: number }; // a type, not an interface export default {  run({ values, bins }, ctx) {    return { kind: "fields", fields: [{ label: "bins", value: String(bins) }] };  },} satisfies Tool<Input>;

Declare the input type with type, not interface: an interface does not satisfy InputValues, and TypeScript's error does not say why.

Ctx

The second argument to run.

AttributeTypeRequiredSinceNotes
signalAbortSignalyesv1Aborted when the reader cancels, when the input changes mid-run, or when a worker-mode timeout fires. A loop whose length depends on input MUST check it: on the main thread it is the only way to stop.
progress(fraction: number, partial?: Output)voidyesv1Progress, and optionally a partial result. A runtime renders partial at most once per frame, which is what lets a simulation show itself converging instead of hiding behind a bar.

A simulation that stops when the reader cancels, and shows its running mean as it goes:

TypeScript
const simulated = simulate(lambda, mu, samples, seed, (done, meanSoFar) => {  if (ctx.signal.aborted) return;  ctx.progress(done / samples, {    kind: "fields",    fields: [      { label: "samples so far", value: done.toLocaleString() },      { label: "simulated mean", value: `${fmt(meanSoFar)} ms` },      { label: "formula says", value: `${fmt(a.w)} ms` },    ],  });});

From tools/queue-explorer/index.ts.

Fixtures

cases.json is an array of cases. pnpm check runs each and compares the result.

AttributeTypeRequiredSinceNotes
namestringyesv1What the case checks, shown when it fails.
inputInputValuesyesv1The values run receives, keyed by input id, passed exactly as given.
expectOutputyesv1The result run must return.
match"exact" | "subset"nov1Exact deep equality by default, because that is what catches a duplicated field, a reordered one, or garbage emitted beside a correct error. subset is an opt-in escape for a tool whose output is genuinely open-ended, and it demands two things in return: a why, and a fieldCount, so a dropped field is still a failure.
whystringnov1Why this case needs subset. Required with it.
fieldCountnumbernov1How many fields the result has. Required with subset, so a dropped field still fails.
JSON
{  "name": "empty input is an error, not an empty chart",  "input": {"values": "   ", "bins": 8},  "expect": {    "kind": "error",    "message": "No numbers yet. Paste measurements separated by spaces, commas or newlines.",    "input": "values"  }}

An exact case for bad input, from tools/histogram/cases.json.

JSON
{  "name": "linear interpolation returns a point between measurements, not one of them",  "input": {"values": "1 2 3 4 5 6 7 8 9 10", "method": "linear"},  "match": "subset",  "why": "this case is about the two summary percentiles only; the full group is asserted exactly by the case above",  "fieldCount": 6,  "expect": {    "kind": "fields",    "fields": [      {"label": "p50", "value": "5.50"},      {"label": "p99", "value": "9.91"}    ]  }}

A subset case, with the why and fieldCount it requires, from tools/percentiles/cases.json.

Validation rules

validateManifest checks these before anything runs. Rules for inputs are on the Inputs page.

RuleMessage
sdk is a contract version this SDK readssdk: this tool needs contract version 99, but this runtime only speaks up to 9. Upgrade @toolbench/runtime, or lower the tool's sdk if it does not use the newer features.
id is lowercase letters, digits and hyphensid: "My Tool" must be lowercase letters, digits and hyphens: it is also a directory name and a URL segment
blurb is under 200 charactersblurb: is 240 characters; keep it under 200 so it works as a card line and a page description
At least one capabilitycapabilities: must declare at least one of pure
kinds is not emptykinds: declare every output kind run can return, so a host can refuse a tool it cannot draw
kinds includes errorkinds: must include "error": every tool can be given bad input, and saying so is part of working correctly
cardFields is a positive numbercardFields: must be a positive number of fields
timeoutMs is 100 to 30000timeoutMs: must be between 100 and 30000 ms
samples is not empty when presentsamples: is empty; omit the key rather than declaring that there are no examples
A link is http, https, mailto or relativelinks[0].href: must be an http, https or mailto URL, or a relative path, with no spaces or control characters