Skip to content

Repository files navigation

claude-sort

Sorts an array by asking Claude Opus to do it.

import { claudeSort } from "claude-sort";

const result = await claudeSort([3, 1, 2]);
// { outcome: "sorted", items: [1, 2, 3], attempts: 1, usage: { inputTokens: 212, outputTokens: 51 } }

Array.prototype.sort does this in about 40 nanoseconds and no dollars. This package does it in about four seconds and a fraction of a cent, over the network, with retries. It is a joke about how much of that trade we have been making lately, and it is built like it is not one: strict TypeScript, a pure core, schema-validated I/O, assertions on both sides of every boundary, and 73 tests that never touch the network.

Install

npm install claude-sort

Credentials resolve the way the Anthropic SDK resolves them: ANTHROPIC_API_KEY, then ANTHROPIC_AUTH_TOKEN, then an ant auth login profile. Pass apiKey to override that for one call.

Usage

Every ending is a named outcome, so nothing has to be caught for a sort that simply did not happen:

import { claudeSort } from "claude-sort";

const result = await claudeSort(["Neptune", "Mercury", "Earth", "Jupiter"], {
  criterion: "distance from the Sun, nearest first",
});

switch (result.outcome) {
  case "sorted":
    console.log(result.items); // ["Mercury", "Earth", "Jupiter", "Neptune"]
    break;
  case "limitExceeded":
    console.log(`too many: ${result.limit} was ${result.actual}`);
    break;
  case "unsorted":
    console.log(`no valid ordering in ${result.attempts} attempts`);
    break;
  case "refused":
    console.log(result.detail);
    break;
  case "apiError":
    console.log(`api error ${result.httpStatus}`);
    break;
}

The comparator is a sentence, which is the one thing this does that Array.prototype.sort cannot:

await claudeSort(bugReports, { criterion: "how angry the reporter sounds" });
await claudeSort(names, { criterion: "Vietnamese alphabetical order" });
await claudeSort(commits, { criterion: "most likely to have caused the outage" });

Objects are labelled before they are sent, and the sorted array holds the original references — the model never sees or returns your objects, only their labels and a permutation of their indices:

const albums = [
  { title: "Kid A", year: 2000 },
  { title: "The Bends", year: 1995 },
];

const result = await claudeSort(albums, {
  criterion: "release year, oldest first",
  describeItem: (album) => `${album.title} (${album.year})`,
});
// result.items[0] === albums[1], by reference

Options

Option Default What it does
criterion ascending, numbers numerically and strings lexicographically The comparator, in English
model claude-opus-5 Any model id the Messages API accepts
effort low low through max. Sorting is shallow work
describeItem JSON-ish labels Renders one item as the label the model sees
attemptsMax 3 Total tries. A non-permutation answer costs one
timeoutMs 120000 Deadline for a single request
apiKey from the environment Overrides credential resolution
client built per call Supplies your own Anthropic client

Limits

  • 100 items per call. A longer list is yours to page. It is not truncated.
  • 512 characters per label. One pathological item cannot fill the context window on everything else's behalf.
  • The returned array is frozen. Array.from(result.items) if you need to mutate it.

Both limits come back as { outcome: "limitExceeded", limit, actual, allowed } before anything is spent.

What it costs

result.usage reports the tokens each sort took, summed across attempts. At Opus 5 rates, sorting three integers costs somewhere around one fiftieth of a cent, which is roughly infinity times what .sort() costs. The comparison is the point of the package.

What it does not do

  • Guarantee an answer. After attemptsMax rejected orderings you get { outcome: "unsorted" }. The model is asked again each time with the reason its last answer was refused.
  • Guarantee a correct answer. Every result is a genuine permutation of your input — validated, never trusted — so nothing is dropped, duplicated, or invented. Whether that permutation is sorted is the model's opinion.
  • Sort stably. Ask for a tiebreaker in the criterion if you need one.
  • Log anything. No credential, item, or label is written anywhere.

A missing credential throws rather than returning an outcome: that is a configuration error, not a sort that failed.

Design

Functional core, imperative shell. describe, prompt, ordering, plan, config, and attempt are pure and hold every decision — including what to do with a rejected answer; anthropic/request.ts and sort.ts are the only code that touches a network, and sort.ts only sends, appends, and returns. The split is what lets the core be tested exhaustively with no key, no mocks, and no spy assertions — the tests assert on returned values, never on which calls were made.

The model answers with indices, never with your data. An answer is accepted only after validateOrdering proves it a permutation of the input's indices, so a hallucinated, dropped, or duplicated element is a rejected attempt rather than a corrupted array.

Read src/anthropic/AGENTS.md before changing anything that talks to the API.

Development

pnpm install
pnpm test        # 73 unit tests, no network
pnpm typecheck
pnpm lint
pnpm build
pnpm example     # sorts [3, 1, 2] for real; needs credentials

test/sort.integration.test.ts exercises the shell against the live Messages API and skips itself unless ANTHROPIC_API_KEY is set. It is the only coverage the wiring has, by choice: once the decisions were extracted into pure functions, what was left holds no decision worth asserting against a fake.

License

MIT

About

Sorts an array by asking Claude Opus to do it. A rigorously engineered joke.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages