Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Changelog

## [Unreleased]

### Fixed

- Invalid or negative numeric CLI flags now fail clearly instead of producing
`NaN` costs or silently skipping token-budget compaction.

## [0.1.1] - 2026-08-06

### Changed
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ tokencut <payload.json> --compact cut, print savings
--json machine-readable output
```

Numeric flags must be finite and non-negative. A zero token budget is valid.

## License

[MIT](LICENSE)
19 changes: 16 additions & 3 deletions bin/tokencut.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,20 @@ Payload: an array of messages, or { system, messages } (Anthropic or OpenAI styl
process.exit(file ? 0 : 1);
}

const price = Number(flag("--price", 3));
const numericFlag = (name, def) => {
const raw = flag(name, def);
const value = Number(raw);
if (typeof raw === "boolean" || !Number.isFinite(value) || value < 0) {
console.error(
`${name} must be a finite, non-negative number; received ${JSON.stringify(raw)}`,
);
process.exit(1);
}
return value;
};
const price = numericFlag("--price", 3);
const maxTokens = has("--max") ? numericFlag("--max", null) : null;
const maxToolResultTokens = numericFlag("--max-tool", 500);
let payload;
try { payload = JSON.parse(readFileSync(file, "utf8")); }
catch (e) { console.error(`could not read ${file}: ${e.message}`); process.exit(1); }
Expand All @@ -33,8 +46,8 @@ const usd = (n) => "$" + n.toFixed(n < 0.01 ? 5 : 4);

if (has("--compact")) {
const res = compact(payload, {
maxTokens: flag("--max", null) ? Number(flag("--max", null)) : null,
maxToolResultTokens: Number(flag("--max-tool", 500)),
maxTokens,
maxToolResultTokens,
dropDuplicates: !has("--no-dedupe"),
});
if (flag("--out", null)) { writeFileSync(String(flag("--out", null)), JSON.stringify(res.payload, null, 2)); }
Expand Down
58 changes: 58 additions & 0 deletions test/cli.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import assert from "node:assert/strict";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import test, { after } from "node:test";

const fixtureDir = mkdtempSync(join(tmpdir(), "tokencut-cli-"));
const fixture = join(fixtureDir, "payload.json");
writeFileSync(fixture, JSON.stringify([{ role: "user", content: "hello" }]));
after(() => rmSync(fixtureDir, { recursive: true, force: true }));

function run(...args) {
return spawnSync(process.execPath, ["bin/tokencut.mjs", fixture, ...args], {
cwd: new URL("..", import.meta.url),
encoding: "utf8",
});
}

test("rejects invalid numeric flags with the flag and received value", async (t) => {
const cases = [
{
args: ["--compact", "--max", "abc"],
flag: "--max",
received: '"abc"',
},
{
args: ["--compact", "--max-tool", "-1"],
flag: "--max-tool",
received: '"-1"',
},
{ args: ["--price", "NaN"], flag: "--price", received: '"NaN"' },
{
args: ["--price", "Infinity"],
flag: "--price",
received: '"Infinity"',
},
{ args: ["--price"], flag: "--price", received: "true" },
];

for (const scenario of cases) {
await t.test(scenario.flag + " " + scenario.received, () => {
const result = run(...scenario.args);
assert.notEqual(result.status, 0);
assert.equal(
result.stderr,
`${scenario.flag} must be a finite, non-negative number; received ${scenario.received}\n`,
);
assert.equal(result.stdout, "");
});
}
});

test("accepts zero as a compact token budget", () => {
const result = run("--compact", "--max", "0", "--json");
assert.equal(result.status, 0, result.stderr);
assert.doesNotThrow(() => JSON.parse(result.stdout));
});
Loading