tokenfold 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,38 +1,48 @@
1
- # tokenfold for Node.js
1
+ # tokenfold for Node.js
2
2
 
3
- Zero-runtime-dependency TypeScript bindings for the tokenfold Rust CLI.
3
+ Zero-runtime-dependency TypeScript bindings for the Tokenfold Rust CLI.
4
4
 
5
5
  ```sh
6
6
  npm install tokenfold
7
7
  ```
8
8
 
9
9
  ```ts
10
- import { compress } from "tokenfold";
10
+ import { compress, decode, inspect } from "tokenfold";
11
11
 
12
- const { payload, report } = await compress(input, {
12
+ const receipt = await inspect(input, { format: "json", preset: "balanced" });
13
+ const { payload, report, text } = await compress(input, {
13
14
  format: "json",
14
- mode: "balanced",
15
+ preset: "balanced",
15
16
  });
16
17
  ```
17
18
 
18
- Opt-in lossy array pruning drops whole array items to hit a token budget instead
19
- of only restructuring them, replacing each with a `$tf_ref` marker that resolves
20
- through the local retrieval store. Generic JSON only:
19
+ Payloads are `Uint8Array`; the `text` convenience getter decodes UTF-8 strictly.
20
+ `inspect` returns only the side-effect-free receipt.
21
+
22
+ Recoverable pruning is explicit and generic-JSON-only:
21
23
 
22
24
  ```ts
23
25
  import { compress, retrieve } from "tokenfold";
24
26
 
25
- const { payload, report } = await compress(feed, {
27
+ const result = await compress(feed, {
26
28
  format: "json",
27
- lossy: "heuristic",
28
- lossyRatio: 0.35, // selection hint, not an enforced budget
29
- lossyPreserve: ["meta"], // arrays that are never pruned
29
+ targetTokens: 2_000,
30
+ pruning: {
31
+ keepRatio: 0.35,
32
+ preservePaths: ["meta"],
33
+ retrievalStore: ".tokenfold/retrieve",
34
+ },
30
35
  });
31
- const original = await retrieve(hash); // a dropped item's original bytes
36
+ const marker = JSON.parse(result.text).items.find((item) => item.$tf_ref);
37
+ const original = await retrieve(marker, { retrievalStore: ".tokenfold/retrieve" });
32
38
  ```
33
39
 
34
- Pass the same options to `inspect` to preview the projected savings without
35
- writing anything to the store.
40
+ Explicit TOON output is verified before emission and restored with `decode`:
41
+
42
+ ```ts
43
+ const encoded = await compress(input, { format: "json", encoding: "toon" });
44
+ const jsonBytes = await decode(encoded.payload, { from: "toon" });
45
+ ```
36
46
 
37
47
  Requires Node.js 22 or newer. The matching native CLI is installed through an
38
48
  optional platform package; set `TOKENFOLD_BINARY_PATH` to use a custom binary.
package/dist/index.d.ts CHANGED
@@ -3,35 +3,23 @@ import { TokenFoldProcessError } from "./errors.js";
3
3
  import { run, type Input, type ProcessResult, type RunOptions } from "./process.js";
4
4
  export { binaryPath, run, TokenFoldProcessError };
5
5
  export type { Input, ProcessResult, RunOptions };
6
- export type CompressionMode = "conservative" | "balanced" | "aggressive";
7
- export type TaskScope = "all" | "general" | "code_review" | "change_summary" | "debugging" | "generation" | "api_overview" | "retrieval_qa" | "agent_history";
8
- /** Selection backend for opt-in lossy pruning. `heuristic` is the only Phase 1 path. */
9
- export type LossyPath = "heuristic";
6
+ export type Preset = "conservative" | "balanced" | "aggressive";
7
+ export type OutputEncoding = "json" | "toon";
8
+ export type InputFormat = "auto" | "openai" | "anthropic" | "json" | "text" | "command" | "diff";
9
+ export type DecodeFormat = "auto" | "json" | "toon" | "text";
10
+ export interface PruningPolicy {
11
+ keepRatio?: number;
12
+ preservePaths?: readonly string[];
13
+ retrievalStore?: string;
14
+ retrievalNamespace?: string;
15
+ }
10
16
  export interface CompressionOptions {
11
- format?: "auto" | "openai" | "anthropic" | "json" | "text" | "command" | "diff";
12
- mode?: CompressionMode;
17
+ format?: InputFormat;
18
+ preset?: Preset;
13
19
  targetTokens?: number;
14
- disable?: readonly string[];
15
- taskScope?: TaskScope;
16
- experimental?: boolean;
17
- storeOriginals?: boolean;
18
- retrieveNamespace?: string;
19
- /**
20
- * Opt-in LOSSY JSON array-item pruning (`--lossy`): drops whole array items to hit a token
21
- * budget instead of only restructuring them. Generic JSON only — on any other format the run
22
- * is a no-op and `json_prune` comes back as `status: "skipped"` with
23
- * `skipped_reason: "not_applicable_to_format"`, persisting nothing. Dropped items are replaced
24
- * by `$tf_ref` markers and stay recoverable via {@link retrieve}; pruning needs a durable
25
- * filesystem retrieval store, so pass `configPath` when you want one other than the default.
26
- */
27
- lossy?: LossyPath;
28
- /**
29
- * `--lossy-ratio`: a best-effort selection hint (0.0..=1.0), not an enforced budget — the
30
- * achieved ratio differs by design. Use `targetTokens` for a real ceiling.
31
- */
32
- lossyRatio?: number;
33
- /** `--lossy-preserve`: dot-separated paths (e.g. `data.results`) whose arrays are never pruned. */
34
- lossyPreserve?: readonly string[];
20
+ requireTarget?: boolean;
21
+ encoding?: OutputEncoding;
22
+ pruning?: PruningPolicy;
35
23
  configPath?: string;
36
24
  signal?: AbortSignal;
37
25
  }
@@ -40,19 +28,6 @@ export interface EstimatorInfo {
40
28
  model: string | null;
41
29
  is_exact: boolean;
42
30
  }
43
- export interface BudgetReport {
44
- target_tokens: number | null;
45
- protected_floor: number;
46
- achieved_tokens: number;
47
- }
48
- export interface QualityReport {
49
- eval_profile_id: string;
50
- task_scope: string;
51
- validated_ratio_band: string | null;
52
- quality_retention: number;
53
- contrastive_failure_rate: number;
54
- gate_passed: boolean;
55
- }
56
31
  export interface Warning {
57
32
  code: string;
58
33
  severity: "info" | "warn" | "critical";
@@ -71,12 +46,39 @@ export interface TransformReport {
71
46
  skipped_reason: string | null;
72
47
  warnings: readonly Warning[];
73
48
  }
74
- export interface CacheReport {
75
- boundary_kind: string | null;
76
- protected_bytes: number;
77
- prefix_byte_identical: boolean;
49
+ export interface BudgetReport {
50
+ status: "not_requested" | "met" | "best_effort" | "unreachable";
51
+ target_tokens: number | null;
52
+ protected_floor: number;
53
+ achieved_tokens: number;
54
+ }
55
+ export interface EncodingReport {
56
+ codec: string;
57
+ version: string;
58
+ roundtrip_verified: boolean;
59
+ tokens_before: number;
60
+ tokens_after: number;
61
+ token_delta: number;
78
62
  warnings: readonly Warning[];
79
63
  }
64
+ export interface PruningReport {
65
+ requested: boolean;
66
+ applied: boolean;
67
+ preview: boolean;
68
+ candidate_items: number;
69
+ retained_items: number;
70
+ pruned_items: number;
71
+ evidence_refs: number;
72
+ preserve_paths: readonly string[];
73
+ }
74
+ export interface QualityReport {
75
+ eval_profile_id: string;
76
+ task_scope: string;
77
+ validated_ratio_band: string | null;
78
+ quality_retention: number | null;
79
+ contrastive_failure_rate: number | null;
80
+ gate_passed: boolean;
81
+ }
80
82
  export interface RetrievalReport {
81
83
  store_namespace: string;
82
84
  hash_algorithm: string;
@@ -85,82 +87,81 @@ export interface RetrievalReport {
85
87
  persisted_original_bytes: number;
86
88
  skipped_original_bytes: number;
87
89
  }
88
- export interface OutputSavingsReport {
89
- profile: string;
90
- estimated_output_tokens_saved: number | null;
91
- measured_output_tokens_saved: number | null;
92
- provenance: string;
93
- }
94
- export interface BypassReport {
95
- reason: string;
96
- source: string;
97
- }
98
- export interface CommandReport {
99
- command_family: string | null;
100
- child_exit_code: number | null;
101
- duration_ms: number;
102
- raw_output_bytes: number;
103
- stdout_bytes: number;
104
- stderr_bytes: number;
105
- stderr_mode: string;
106
- stderr_truncated: boolean;
107
- compressed_output_bytes: number;
108
- filter_pack_id: string | null;
109
- filter_version: string | null;
110
- never_worse_applied: boolean;
90
+ export interface PipelineStageReport {
91
+ id: string;
92
+ version: string | null;
93
+ input_bytes: number | null;
94
+ output_bytes: number | null;
95
+ saved_bytes: number | null;
96
+ input_tokens: number | null;
97
+ output_tokens: number | null;
98
+ saved_tokens: number | null;
99
+ estimator: EstimatorInfo | null;
100
+ status: string;
101
+ duration_ms: number | null;
111
102
  bypass_reason: string | null;
103
+ provenance: string;
104
+ recoverability: string;
105
+ evidence_ref: string | null;
112
106
  }
113
- export interface LedgerReport {
114
- recorded: boolean;
115
- scope: string | null;
116
- project_hash: string | null;
117
- record_id: string | null;
107
+ export interface PipelineReport {
108
+ raw_input_bytes: number | null;
109
+ raw_input_tokens: number | null;
110
+ final_output_bytes: number;
111
+ final_output_tokens: number;
112
+ total_saved_tokens: number | null;
113
+ raw_capture: string;
114
+ upstream_recoverability: string;
115
+ stages: readonly PipelineStageReport[];
118
116
  }
119
- export interface CompressionReport {
117
+ export interface CompressionReceipt {
120
118
  schema_version: string;
119
+ status: "compressed" | "passthrough";
121
120
  original_tokens: number;
122
121
  compressed_tokens: number;
123
122
  saved_tokens: number;
124
123
  savings_ratio: number;
125
124
  savings_pct: number;
126
125
  estimator: EstimatorInfo;
127
- status: "compressed" | "passthrough" | "best_effort" | "unreachable_target";
128
- mode: string;
126
+ preset: Preset;
129
127
  format: string;
128
+ output_encoding: string;
130
129
  task_scope: string;
131
130
  request_id: string | null;
131
+ pipeline: PipelineReport | null;
132
132
  quality: QualityReport | null;
133
133
  budget: BudgetReport | null;
134
- cache: CacheReport | null;
134
+ encoding: EncodingReport | null;
135
+ pruning: PruningReport | null;
135
136
  retrieval: RetrievalReport | null;
136
- output_savings: OutputSavingsReport | null;
137
- bypass: BypassReport | null;
138
- command: CommandReport | null;
139
- ledger: LedgerReport | null;
140
137
  transforms: readonly TransformReport[];
141
138
  warnings: readonly Warning[];
139
+ cache: unknown;
140
+ output_savings: unknown;
141
+ bypass: unknown;
142
+ command: unknown;
143
+ ledger: unknown;
142
144
  }
145
+ export type CompressionReport = CompressionReceipt;
143
146
  export interface CompressionResult {
144
147
  payload: Uint8Array;
145
- report: CompressionReport;
148
+ readonly text: string;
149
+ report: CompressionReceipt;
150
+ }
151
+ export declare class BudgetUnmetError extends Error {
152
+ readonly receipt: CompressionReceipt;
153
+ constructor(receipt: CompressionReceipt);
146
154
  }
147
155
  export declare function compress(input: Input, options?: CompressionOptions): Promise<CompressionResult>;
148
- export declare function inspect(input: Input, options?: CompressionOptions): Promise<CompressionResult>;
156
+ export declare function inspect(input: Input, options?: CompressionOptions): Promise<CompressionReceipt>;
157
+ export declare function decode(input: Input, options?: {
158
+ from?: DecodeFormat;
159
+ signal?: AbortSignal;
160
+ }): Promise<Uint8Array>;
149
161
  export interface RetrieveOptions {
150
- /** `--retrieve-namespace`: the namespace the item was stored under. */
162
+ retrievalStore?: string;
151
163
  namespace?: string;
152
164
  configPath?: string;
153
165
  signal?: AbortSignal;
154
166
  }
155
- /**
156
- * Restores the original bytes of something a lossy run dropped, or a `storeOriginals` run saved,
157
- * mirroring `tokenfold retrieve`. `reference` is either a raw hex SHA-256 hash — a compressed
158
- * payload's `$tf_ref.hash` — or a `[tokenfold:retrieve hash=... namespace=...]` text marker,
159
- * whose embedded namespace is used when `options.namespace` is omitted. A CompressionReport
160
- * path is NOT a valid reference: the current report schema carries no per-entry hash, and the
161
- * CLI rejects it rather than guessing.
162
- *
163
- * Throws `TokenFoldProcessError` (`code: "tokenfold_exit"`) when the hash is unknown, its TTL
164
- * has elapsed, or the reference is malformed — the CLI reports all of those as a non-zero exit.
165
- */
166
- export declare function retrieve(reference: string, options?: RetrieveOptions): Promise<Uint8Array>;
167
+ export declare function retrieve(reference: string | Record<string, unknown>, options?: RetrieveOptions): Promise<Uint8Array>;
package/dist/index.js CHANGED
@@ -2,114 +2,98 @@ import { binaryPath } from "./binary.js";
2
2
  import { TokenFoldProcessError } from "./errors.js";
3
3
  import { run } from "./process.js";
4
4
  export { binaryPath, run, TokenFoldProcessError };
5
+ export class BudgetUnmetError extends Error {
6
+ receipt;
7
+ constructor(receipt) { super(`token budget unmet: achieved ${receipt.compressed_tokens} tokens`); this.name = "BudgetUnmetError"; this.receipt = receipt; }
8
+ }
5
9
  function argumentsFor(command, options) {
6
- // The `inspect` subcommand has no --lossy flags of its own; the CLI's lossy preview is
7
- // `compress --dry-run`, which routes to the same code path and, exactly like `inspect --json`,
8
- // writes the report to stdout and no payload. So a lossy inspect() becomes that instead.
9
- const wantsLossy = options.lossy !== undefined ||
10
- options.lossyRatio !== undefined ||
11
- (options.lossyPreserve?.length ?? 0) > 0;
12
- const previewLossy = command === "inspect" && wantsLossy;
13
- const args = previewLossy ? ["compress", "--json", "--dry-run"] : [command, "--json"];
14
- const compressing = command === "compress" || previewLossy;
10
+ const args = [command, "--receipt-format", "json"];
15
11
  if (options.format)
16
12
  args.push("--format", options.format);
17
- if (options.mode)
18
- args.push("--mode", options.mode);
13
+ if (options.preset)
14
+ args.push("--preset", options.preset);
19
15
  if (options.targetTokens !== undefined)
20
16
  args.push("--target-tokens", String(options.targetTokens));
21
- if (options.disable?.length && compressing)
22
- args.push("--disable", options.disable.join(","));
23
- if (options.taskScope)
24
- args.push("--task-scope", options.taskScope);
25
- if (options.experimental)
26
- args.push("--experimental");
27
- if (options.storeOriginals && compressing)
28
- args.push("--store-originals");
29
- if (options.retrieveNamespace && compressing) {
30
- args.push("--retrieve-namespace", options.retrieveNamespace);
31
- }
32
- if (wantsLossy && compressing) {
33
- // `--lossy-ratio`/`--lossy-preserve` are `requires = "lossy"` in the CLI. Forward them even
34
- // when `lossy` is unset rather than dropping them: silently ignoring a pruning-aggression
35
- // knob is worse than the CLI's own "the following required arguments were not provided".
36
- if (options.lossy)
37
- args.push("--lossy", options.lossy);
38
- if (options.lossyRatio !== undefined)
39
- args.push("--lossy-ratio", String(options.lossyRatio));
40
- for (const path of options.lossyPreserve ?? [])
41
- args.push("--lossy-preserve", path);
17
+ if (options.requireTarget)
18
+ args.push("--require-target");
19
+ if (options.encoding)
20
+ args.push("--encoding", options.encoding);
21
+ if (options.pruning) {
22
+ args.push("--prune");
23
+ if (options.pruning.keepRatio !== undefined)
24
+ args.push("--keep-ratio", String(options.pruning.keepRatio));
25
+ for (const path of options.pruning.preservePaths ?? [])
26
+ args.push("--preserve", path);
27
+ if (options.pruning.retrievalStore)
28
+ args.push("--retrieval-store", options.pruning.retrievalStore);
29
+ if (options.pruning.retrievalNamespace)
30
+ args.push("--retrieval-namespace", options.pruning.retrievalNamespace);
42
31
  }
43
32
  if (options.configPath)
44
33
  args.push("--config", options.configPath);
45
34
  return args;
46
35
  }
47
- function parseReport(bytes, result) {
36
+ function parseReceipt(bytes, result) {
37
+ const text = Buffer.from(bytes).toString("utf8");
38
+ const json = text.split("\ntokenfold:", 1)[0] ?? "";
48
39
  try {
49
- return JSON.parse(Buffer.from(bytes).toString("utf8"));
40
+ return JSON.parse(json);
50
41
  }
51
42
  catch (cause) {
52
- throw new TokenFoldProcessError("tokenfold returned an invalid JSON report", {
53
- code: "invalid_report",
54
- exitCode: result.exitCode,
55
- signal: result.signal,
56
- stderr: result.stderr,
57
- cause,
58
- });
43
+ throw new TokenFoldProcessError("tokenfold returned an invalid JSON receipt", { code: "invalid_report", exitCode: result.exitCode, signal: result.signal, stderr: result.stderr, cause });
59
44
  }
60
45
  }
61
- function throwIfFailed(result) {
62
- if (result.exitCode === 0)
63
- return;
64
- throw new TokenFoldProcessError(`tokenfold exited with status ${result.exitCode ?? result.signal}`, {
65
- code: "tokenfold_exit",
66
- exitCode: result.exitCode,
67
- signal: result.signal,
68
- stderr: result.stderr,
69
- });
46
+ function optionsFor(input, signal) {
47
+ const options = { env: { TOKENFOLD_ANALYTICS_ENABLED: "false" } };
48
+ if (input !== undefined)
49
+ options.stdin = input;
50
+ if (signal)
51
+ options.signal = signal;
52
+ return options;
70
53
  }
71
- async function execute(command, input, options) {
72
- const runOptions = {
73
- stdin: input,
74
- env: { TOKENFOLD_ANALYTICS_ENABLED: "false" },
75
- };
76
- if (options.signal)
77
- runOptions.signal = options.signal;
78
- const result = await run(argumentsFor(command, options), runOptions);
79
- throwIfFailed(result);
80
- const reportBytes = command === "compress" ? result.stderr : result.stdout;
81
- return {
82
- payload: command === "compress" ? result.stdout : Uint8Array.from(Buffer.from(input)),
83
- report: parseReport(reportBytes, result),
84
- };
54
+ function withText(payload, report) {
55
+ return { payload, report, get text() { return new TextDecoder("utf-8", { fatal: true }).decode(payload); } };
85
56
  }
86
- export function compress(input, options = {}) {
87
- return execute("compress", input, options);
57
+ export async function compress(input, options = {}) {
58
+ const result = await run(argumentsFor("compress", options), optionsFor(input, options.signal));
59
+ if (result.exitCode !== 0 && result.exitCode !== 7)
60
+ throwProcess(result);
61
+ const receipt = parseReceipt(result.stderr, result);
62
+ if (result.exitCode === 7)
63
+ throw new BudgetUnmetError(receipt);
64
+ return withText(result.stdout, receipt);
88
65
  }
89
- export function inspect(input, options = {}) {
90
- return execute("inspect", input, options);
66
+ export async function inspect(input, options = {}) {
67
+ const result = await run(argumentsFor("inspect", options), optionsFor(input, options.signal));
68
+ if (result.exitCode !== 0 && result.exitCode !== 7)
69
+ throwProcess(result);
70
+ const receipt = parseReceipt(result.stdout, result);
71
+ if (result.exitCode === 7)
72
+ throw new BudgetUnmetError(receipt);
73
+ return receipt;
74
+ }
75
+ export async function decode(input, options = {}) {
76
+ const args = ["decode"];
77
+ if (options.from)
78
+ args.push("--from", options.from);
79
+ const result = await run(args, optionsFor(input, options.signal));
80
+ if (result.exitCode !== 0)
81
+ throwProcess(result);
82
+ return result.stdout;
91
83
  }
92
- /**
93
- * Restores the original bytes of something a lossy run dropped, or a `storeOriginals` run saved,
94
- * mirroring `tokenfold retrieve`. `reference` is either a raw hex SHA-256 hash — a compressed
95
- * payload's `$tf_ref.hash` — or a `[tokenfold:retrieve hash=... namespace=...]` text marker,
96
- * whose embedded namespace is used when `options.namespace` is omitted. A CompressionReport
97
- * path is NOT a valid reference: the current report schema carries no per-entry hash, and the
98
- * CLI rejects it rather than guessing.
99
- *
100
- * Throws `TokenFoldProcessError` (`code: "tokenfold_exit"`) when the hash is unknown, its TTL
101
- * has elapsed, or the reference is malformed — the CLI reports all of those as a non-zero exit.
102
- */
103
84
  export async function retrieve(reference, options = {}) {
104
- const args = ["retrieve", reference];
85
+ const args = ["retrieve", typeof reference === "string" ? reference : JSON.stringify(reference)];
86
+ if (options.retrievalStore)
87
+ args.push("--retrieval-store", options.retrievalStore);
105
88
  if (options.namespace)
106
- args.push("--retrieve-namespace", options.namespace);
89
+ args.push("--retrieval-namespace", options.namespace);
107
90
  if (options.configPath)
108
91
  args.push("--config", options.configPath);
109
- const runOptions = { env: { TOKENFOLD_ANALYTICS_ENABLED: "false" } };
110
- if (options.signal)
111
- runOptions.signal = options.signal;
112
- const result = await run(args, runOptions);
113
- throwIfFailed(result);
92
+ const result = await run(args, optionsFor(undefined, options.signal));
93
+ if (result.exitCode !== 0)
94
+ throwProcess(result);
114
95
  return result.stdout;
115
96
  }
97
+ function throwProcess(result) {
98
+ throw new TokenFoldProcessError(`tokenfold exited with status ${result.exitCode ?? result.signal}`, { code: "tokenfold_exit", exitCode: result.exitCode, signal: result.signal, stderr: result.stderr });
99
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokenfold",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Token-aware compression for LLM payloads, backed by the tokenfold Rust CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -49,11 +49,11 @@
49
49
  "prepack": "npm run build"
50
50
  },
51
51
  "optionalDependencies": {
52
- "@tokenfold/cli-darwin-x64": "0.4.1",
53
- "@tokenfold/cli-darwin-arm64": "0.4.1",
54
- "@tokenfold/cli-linux-x64": "0.4.1",
55
- "@tokenfold/cli-linux-arm64": "0.4.1",
56
- "@tokenfold/cli-win32-x64": "0.4.1"
52
+ "@tokenfold/cli-darwin-x64": "0.5.0",
53
+ "@tokenfold/cli-darwin-arm64": "0.5.0",
54
+ "@tokenfold/cli-linux-x64": "0.5.0",
55
+ "@tokenfold/cli-linux-arm64": "0.5.0",
56
+ "@tokenfold/cli-win32-x64": "0.5.0"
57
57
  },
58
58
  "devDependencies": {
59
59
  "@types/node": "^24.0.0",