llm-cli-gateway 2.15.0 → 2.16.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +50 -1
  2. package/dist/acp/process-manager.js +2 -1
  3. package/dist/acp/provider-registry.js +8 -8
  4. package/dist/async-job-manager.d.ts +4 -1
  5. package/dist/async-job-manager.js +15 -6
  6. package/dist/compressor/estimate.d.ts +5 -0
  7. package/dist/compressor/estimate.js +21 -0
  8. package/dist/compressor/index.d.ts +23 -0
  9. package/dist/compressor/index.js +77 -0
  10. package/dist/compressor/router.d.ts +2 -0
  11. package/dist/compressor/router.js +57 -0
  12. package/dist/compressor/transforms/ansi.d.ts +3 -0
  13. package/dist/compressor/transforms/ansi.js +89 -0
  14. package/dist/compressor/transforms/json.d.ts +1 -0
  15. package/dist/compressor/transforms/json.js +156 -0
  16. package/dist/compressor/transforms/log.d.ts +12 -0
  17. package/dist/compressor/transforms/log.js +55 -0
  18. package/dist/compressor/transforms/whitespace.d.ts +8 -0
  19. package/dist/compressor/transforms/whitespace.js +79 -0
  20. package/dist/config.d.ts +8 -0
  21. package/dist/config.js +25 -0
  22. package/dist/executor.d.ts +1 -0
  23. package/dist/executor.js +5 -2
  24. package/dist/flight-recorder.d.ts +9 -0
  25. package/dist/flight-recorder.js +42 -0
  26. package/dist/index.d.ts +18 -2
  27. package/dist/index.js +273 -66
  28. package/dist/job-store.d.ts +4 -0
  29. package/dist/job-store.js +16 -0
  30. package/dist/provider-definitions.d.ts +1 -0
  31. package/dist/provider-definitions.js +28 -10
  32. package/dist/provider-tool-capabilities.js +3 -3
  33. package/dist/request-helpers.js +1 -1
  34. package/dist/spawn-env-isolation.d.ts +10 -0
  35. package/dist/spawn-env-isolation.js +55 -0
  36. package/dist/upstream-contracts.js +47 -27
  37. package/npm-shrinkwrap.json +2 -2
  38. package/package.json +7 -3
package/CHANGELOG.md CHANGED
@@ -4,6 +4,55 @@ All notable changes to the llm-cli-gateway project.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [2.16.0] - 2026-07-10
8
+
9
+ ### Added
10
+
11
+ - **Native outbound display-text compressor, PR-1 (#151).** A built-in,
12
+ provider-agnostic compressor that shrinks provider display text before it is
13
+ returned to the caller. It is ANSI- and fence-aware (protects code fences,
14
+ inline code, OSC sequences, and wide characters from lossy rewriting) and is
15
+ **off by default**; opt in per the compressor configuration. This is the first
16
+ slice; response-side wiring and API-provider coverage are named follow-ups.
17
+ - **Site machine-discovery generation (#169).** The machine-readable discovery
18
+ artifacts under `site/` (`llms.txt`, `.well-known/*`, `tools.md`,
19
+ `openapi.json`, ...) are now generated from the live MCP tool surface by
20
+ `scripts/generate-site-discovery.mjs`. Regenerate with `npm run site:generate`;
21
+ `npm run site:generate:check` + `npm run site:validate` gate them in
22
+ `npm run check` so the published surface can never silently drift from the
23
+ tools the server actually exposes.
24
+
25
+ ### Changed
26
+
27
+ - **Vibe/Mistral no longer defaults to `--agent auto-approve` (#155).** Under the
28
+ default `legacy` approval strategy, Vibe was spawned in auto-approve ("YOLO"),
29
+ auto-executing every tool call including shell. It now defaults to
30
+ `--agent accept-edits` (auto-accept file edits; dangerous ops such as shell stay
31
+ gated, and Vibe denies rather than blocks on them in programmatic mode), matching
32
+ the `mcp_managed` posture and the other providers' non-bypass defaults. This
33
+ shrinks the blast radius of a prompt-injected tool call. `auto-approve` remains
34
+ reachable deliberately: pass `permissionMode: "auto-approve"` on the request, or
35
+ set `LLM_GATEWAY_APPROVAL_ALLOW_BYPASS` operator-wide. **Behaviour change:**
36
+ `legacy` callers that relied on Vibe running shell unattended will now see the
37
+ gated op denied unless they adopt one of those opt-ins.
38
+ - **Provider contract and target-version refreshes (#157 through #171).**
39
+ `PROVIDER_TARGET_VERSIONS` is now the single source of truth for provider
40
+ target versions (#165), and every provider's declared upstream contract was
41
+ re-probed against the installed CLI and refreshed: codex-cli 0.144.1, claude
42
+ 2.1.206, grok 0.2.93, gemini/agy 1.1.0, mistral vibe 2.19.1 (with
43
+ `--disabled-tools` acknowledged, #170), cursor-agent 2026.07.09, and the devin
44
+ ACP target 3000.1.27. Internal metadata only; no request-time behaviour change.
45
+
46
+ ### Security
47
+
48
+ - **Opt-in isolation of provider child-process environment (#154).** A new
49
+ `LLM_GATEWAY_ISOLATE_SPAWN_ENV` opt-in scrubs the spawned provider CLI's
50
+ environment at the single `spawnCliProcess` chokepoint, shrinking the blast
51
+ radius of a hostile provider attempting to exfiltrate redirected/inherited
52
+ environment secrets.
53
+ - **Installer Go toolchain bumped 1.26.4 to 1.26.5 (#166).** Clears a Go stdlib
54
+ vulnerability (GO-2026-4970) in the shipped installer binaries.
55
+
7
56
  ## [2.15.0] - 2026-07-03
8
57
 
9
58
  ### Added
@@ -32,7 +81,7 @@ All notable changes to the llm-cli-gateway project.
32
81
  - **Durable instance-lease orphan recovery (#139).** The blanket
33
82
  `markOrphanedOnStartup` sweep in the `AsyncJobManager` constructor rewrote
34
83
  every `running` row to `orphaned`, so on a SHARED store (`backend =
35
- "postgres"`) a fresh instance (especially an ephemeral stdio spawn)
84
+ "postgres"`) a fresh instance (especially an ephemeral stdio spawn)
36
85
  transiently orphaned other live instances' in-flight jobs (a `running` ->
37
86
  `orphaned` -> `completed` flap a poller can trip on). The sweep is now a
38
87
  per-job fencing lease: each instance registers a lease and advances a
@@ -6,6 +6,7 @@ import { AcpError, AcpProcessExitError, ProviderUnavailableError } from "./error
6
6
  import { JsonRpcStdioTransport, } from "./json-rpc-stdio.js";
7
7
  import { getAcpProviderEntry } from "./provider-registry.js";
8
8
  import { envWithExtendedPath, getExtendedPath } from "../executor.js";
9
+ import { applySpawnEnvIsolation } from "../spawn-env-isolation.js";
9
10
  import { noopLogger } from "../logger.js";
10
11
  const SHUTDOWN_KILL_GRACE_MS = 5000;
11
12
  const SHELL_METACHARACTERS = /[\s|&;<>(){}$`"'\\*?[\]~#! ]/;
@@ -15,7 +16,7 @@ function assertSafeExecutable(command, provider) {
15
16
  }
16
17
  }
17
18
  export function buildProviderEnv(provider, providerConfig, baseEnv) {
18
- const env = envWithExtendedPath(baseEnv, getExtendedPath());
19
+ const env = applySpawnEnvIsolation(envWithExtendedPath(baseEnv, getExtendedPath()));
19
20
  if (provider === "grok" && providerConfig.isolatedLeaderSocket) {
20
21
  const socketDir = tmpdir();
21
22
  const socketName = `grok-acp-leader-${process.pid}-${Date.now()}.sock`;
@@ -1,4 +1,4 @@
1
- import { getProviderDefinition } from "../provider-definitions.js";
1
+ import { getProviderDefinition, PROVIDER_TARGET_VERSIONS } from "../provider-definitions.js";
2
2
  function acpEntrypointFromDefinition(provider) {
3
3
  const entry = getProviderDefinition(provider).acp.entrypoint;
4
4
  if (!entry) {
@@ -12,7 +12,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
12
12
  displayName: "Mistral Vibe",
13
13
  status: "native_smoke_passed",
14
14
  supportKind: "native",
15
- targetVersion: "vibe 2.18.3",
15
+ targetVersion: PROVIDER_TARGET_VERSIONS.mistral,
16
16
  entrypoint: acpEntrypointFromDefinition("mistral"),
17
17
  runtimeEnabledDefault: false,
18
18
  shipRuntimePilot: true,
@@ -25,7 +25,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
25
25
  displayName: "xAI Grok CLI",
26
26
  status: "native_smoke_passed",
27
27
  supportKind: "native",
28
- targetVersion: "grok 0.2.77 (44e77bec3a)",
28
+ targetVersion: PROVIDER_TARGET_VERSIONS.grok,
29
29
  entrypoint: acpEntrypointFromDefinition("grok"),
30
30
  runtimeEnabledDefault: false,
31
31
  shipRuntimePilot: true,
@@ -38,7 +38,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
38
38
  displayName: "OpenAI Codex CLI",
39
39
  status: "adapter_mediated_deferred",
40
40
  supportKind: "adapter_mediated",
41
- targetVersion: "codex-cli 0.142.4",
41
+ targetVersion: PROVIDER_TARGET_VERSIONS.codex,
42
42
  entrypoint: acpEntrypointFromDefinition("codex"),
43
43
  runtimeEnabledDefault: false,
44
44
  shipRuntimePilot: false,
@@ -51,7 +51,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
51
51
  displayName: "Anthropic Claude Code",
52
52
  status: "adapter_mediated_deferred",
53
53
  supportKind: "adapter_mediated",
54
- targetVersion: "claude 2.1.198",
54
+ targetVersion: PROVIDER_TARGET_VERSIONS.claude,
55
55
  entrypoint: acpEntrypointFromDefinition("claude"),
56
56
  runtimeEnabledDefault: false,
57
57
  shipRuntimePilot: false,
@@ -64,7 +64,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
64
64
  displayName: "Google Antigravity",
65
65
  status: "absent_watchlist",
66
66
  supportKind: "none",
67
- targetVersion: "agy 1.0.14",
67
+ targetVersion: PROVIDER_TARGET_VERSIONS.gemini,
68
68
  entrypoint: acpEntrypointFromDefinition("gemini"),
69
69
  runtimeEnabledDefault: false,
70
70
  shipRuntimePilot: false,
@@ -77,7 +77,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
77
77
  displayName: "Cognition Devin CLI",
78
78
  status: "native_smoke_passed",
79
79
  supportKind: "native",
80
- targetVersion: "devin 2026.8.18 (16737566)",
80
+ targetVersion: PROVIDER_TARGET_VERSIONS.devin,
81
81
  entrypoint: acpEntrypointFromDefinition("devin"),
82
82
  runtimeEnabledDefault: false,
83
83
  shipRuntimePilot: true,
@@ -90,7 +90,7 @@ const ACP_PROVIDER_REGISTRY = Object.freeze({
90
90
  displayName: "Cursor Agent CLI",
91
91
  status: "native_smoke_passed",
92
92
  supportKind: "native",
93
- targetVersion: "cursor-agent 2026.06.29-2ad2186",
93
+ targetVersion: PROVIDER_TARGET_VERSIONS.cursor,
94
94
  entrypoint: acpEntrypointFromDefinition("cursor"),
95
95
  runtimeEnabledDefault: false,
96
96
  shipRuntimePilot: true,
@@ -45,6 +45,7 @@ export interface AsyncJobFlightRecorderEntry {
45
45
  stablePrefixTokens?: number;
46
46
  cacheControlBlocks?: number;
47
47
  cacheControlTtlSeconds?: number;
48
+ optimizationApplied?: boolean;
48
49
  }
49
50
  export type AsyncJobUsageExtractor = (stdout: string) => {
50
51
  inputTokens?: number;
@@ -91,6 +92,7 @@ export interface StartJobOptions {
91
92
  writeFlightStart?: boolean;
92
93
  flightRecorderEntry?: AsyncJobFlightRecorderEntry;
93
94
  extractUsage?: AsyncJobUsageExtractor;
95
+ compressResponse?: boolean;
94
96
  }
95
97
  export interface StartJobOutcome {
96
98
  snapshot: AsyncJobSnapshot;
@@ -169,7 +171,7 @@ export declare class AsyncJobManager {
169
171
  private persistComplete;
170
172
  private hydrateFromStore;
171
173
  getJobOwner(jobId: string): string | null | undefined;
172
- startJob(cli: LlmCli, args: string[], correlationId: string, cwd?: string, idleTimeoutMs?: number, outputFormat?: string, forceRefresh?: boolean, env?: Record<string, string>, onComplete?: () => void, flightRecorderEntry?: AsyncJobFlightRecorderEntry, extractUsage?: AsyncJobUsageExtractor, writeFlightStart?: boolean, stdin?: string): AsyncJobSnapshot;
174
+ startJob(cli: LlmCli, args: string[], correlationId: string, cwd?: string, idleTimeoutMs?: number, outputFormat?: string, forceRefresh?: boolean, env?: Record<string, string>, onComplete?: () => void, flightRecorderEntry?: AsyncJobFlightRecorderEntry, extractUsage?: AsyncJobUsageExtractor, writeFlightStart?: boolean, stdin?: string, compressResponse?: boolean): AsyncJobSnapshot;
173
175
  startJobWithDedup(cli: LlmCli, args: string[], correlationId: string, opts?: StartJobOptions): StartJobOutcome;
174
176
  private launchProcessJob;
175
177
  private failQueuedJob;
@@ -199,6 +201,7 @@ export declare class AsyncJobManager {
199
201
  getLimiterSnapshot(): JobLimiterSnapshot;
200
202
  getConfiguredLimits(): JobLimitsConfig;
201
203
  getJobOutputFormat(jobId: string): string | undefined;
204
+ getJobCompressResponse(jobId: string): boolean;
202
205
  getJobCli(jobId: string): JobProvider | undefined;
203
206
  private snapshot;
204
207
  private appendOutput;
@@ -657,11 +657,12 @@ export class AsyncJobManager {
657
657
  }
658
658
  }
659
659
  }
660
- buildRequestKey(cli, args, env, stdin, cwd, outputFormat) {
660
+ buildRequestKey(cli, args, env, stdin, cwd, outputFormat, compressResponse) {
661
661
  const extraEnv = canonicaliseEnvForKey(env);
662
662
  const withStdin = stdin === undefined ? extraEnv : `${extraEnv}|stdin:${stdin}`;
663
663
  const withCwd = cwd === undefined ? withStdin : `${withStdin}|cwd:${cwd}`;
664
- const extra = cli === "codex" ? `${withCwd}|fmt:${outputFormat ?? "text"}` : withCwd;
664
+ const withFmt = cli === "codex" ? `${withCwd}|fmt:${outputFormat ?? "text"}` : withCwd;
665
+ const extra = compressResponse ? `${withFmt}|compress:1` : withFmt;
665
666
  const principal = resolveOwnerPrincipal(getRequestContext());
666
667
  const scoped = principal === "local" ? extra : `${extra}|principal:${principal}`;
667
668
  return computeRequestKey(cli, args, scoped);
@@ -927,7 +928,7 @@ export class AsyncJobManager {
927
928
  durationMs,
928
929
  retryCount: 0,
929
930
  circuitBreakerState: "closed",
930
- optimizationApplied: false,
931
+ optimizationApplied: job.flightRecorderEntry.optimizationApplied ?? false,
931
932
  exitCode,
932
933
  httpStatus: job.transport === "http" ? (job.httpStatus ?? undefined) : undefined,
933
934
  errorMessage,
@@ -1068,6 +1069,7 @@ export class AsyncJobManager {
1068
1069
  exited: row.status !== "running" && row.status !== "queued",
1069
1070
  metricsRecorded: true,
1070
1071
  outputFormat: row.outputFormat ?? undefined,
1072
+ compressResponse: row.compressResponse ?? null,
1071
1073
  ownerPrincipal: row.ownerPrincipal,
1072
1074
  outputDirty: false,
1073
1075
  lastOutputFlushAt: Date.now(),
@@ -1081,7 +1083,7 @@ export class AsyncJobManager {
1081
1083
  job = this.hydrateFromStore(jobId) ?? undefined;
1082
1084
  return job?.ownerPrincipal;
1083
1085
  }
1084
- startJob(cli, args, correlationId, cwd, idleTimeoutMs, outputFormat, forceRefresh, env, onComplete, flightRecorderEntry, extractUsage, writeFlightStart, stdin) {
1086
+ startJob(cli, args, correlationId, cwd, idleTimeoutMs, outputFormat, forceRefresh, env, onComplete, flightRecorderEntry, extractUsage, writeFlightStart, stdin, compressResponse) {
1085
1087
  return this.startJobWithDedup(cli, args, correlationId, {
1086
1088
  cwd,
1087
1089
  idleTimeoutMs,
@@ -1093,11 +1095,12 @@ export class AsyncJobManager {
1093
1095
  flightRecorderEntry,
1094
1096
  extractUsage,
1095
1097
  writeFlightStart,
1098
+ compressResponse,
1096
1099
  }).snapshot;
1097
1100
  }
1098
1101
  startJobWithDedup(cli, args, correlationId, opts = {}) {
1099
- const { cwd, idleTimeoutMs, outputFormat, forceRefresh, env: extraEnv, stdin, onComplete, flightRecorderEntry, extractUsage, writeFlightStart, } = opts;
1100
- const requestKey = this.buildRequestKey(cli, args, extraEnv, stdin, cwd, outputFormat);
1102
+ const { cwd, idleTimeoutMs, outputFormat, forceRefresh, env: extraEnv, stdin, onComplete, flightRecorderEntry, extractUsage, writeFlightStart, compressResponse, } = opts;
1103
+ const requestKey = this.buildRequestKey(cli, args, extraEnv, stdin, cwd, outputFormat, compressResponse);
1101
1104
  if (!forceRefresh) {
1102
1105
  const reused = this.tryReuseDedupedJob(requestKey, correlationId, cli, onComplete);
1103
1106
  if (reused)
@@ -1130,6 +1133,7 @@ export class AsyncJobManager {
1130
1133
  exited: false,
1131
1134
  metricsRecorded: false,
1132
1135
  outputFormat,
1136
+ compressResponse: compressResponse ?? null,
1133
1137
  onComplete,
1134
1138
  onCompleteFired: false,
1135
1139
  outputDirty: false,
@@ -1178,6 +1182,7 @@ export class AsyncJobManager {
1178
1182
  cli,
1179
1183
  args: [...args],
1180
1184
  outputFormat,
1185
+ compressResponse,
1181
1186
  startedAt,
1182
1187
  pid: null,
1183
1188
  ownerPrincipal,
@@ -1222,6 +1227,7 @@ export class AsyncJobManager {
1222
1227
  cwd,
1223
1228
  stdio: stdin === undefined ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
1224
1229
  env: { ...baseEnv, ...(extraEnv ?? {}) },
1230
+ logger: this.logger,
1225
1231
  });
1226
1232
  job.process = child;
1227
1233
  try {
@@ -1531,6 +1537,9 @@ export class AsyncJobManager {
1531
1537
  getJobOutputFormat(jobId) {
1532
1538
  return this.jobs.get(jobId)?.outputFormat;
1533
1539
  }
1540
+ getJobCompressResponse(jobId) {
1541
+ return this.jobs.get(jobId)?.compressResponse === true;
1542
+ }
1534
1543
  getJobCli(jobId) {
1535
1544
  return this.jobs.get(jobId)?.cli;
1536
1545
  }
@@ -0,0 +1,5 @@
1
+ import type { ContentRoute } from "./router.js";
2
+ export type DevTokenizer = (text: string) => number;
3
+ export declare function setDevTokenizer(tokenizer: DevTokenizer | null): void;
4
+ export declare function estimateTokensForRoute(text: string, route: ContentRoute): number;
5
+ export declare function estimateTokensSaved(originalText: string, compressedText: string, route: ContentRoute): number;
@@ -0,0 +1,21 @@
1
+ const DIVISORS = {
2
+ json: 3.0,
3
+ log: 3.4,
4
+ "ansi-text": 3.4,
5
+ plain: 3.6,
6
+ identity: 3.6,
7
+ };
8
+ let devTokenizer = null;
9
+ export function setDevTokenizer(tokenizer) {
10
+ devTokenizer = tokenizer;
11
+ }
12
+ export function estimateTokensForRoute(text, route) {
13
+ if (devTokenizer)
14
+ return devTokenizer(text);
15
+ if (text.length === 0)
16
+ return 0;
17
+ return Math.ceil(text.length / DIVISORS[route]);
18
+ }
19
+ export function estimateTokensSaved(originalText, compressedText, route) {
20
+ return Math.max(0, estimateTokensForRoute(originalText, route) - estimateTokensForRoute(compressedText, route));
21
+ }
@@ -0,0 +1,23 @@
1
+ import { type ContentRoute } from "./router.js";
2
+ export interface CompressCtx {
3
+ provider: string;
4
+ direction: "outbound";
5
+ outputFormat?: string;
6
+ outputSchemaDeclared: boolean;
7
+ lossless: true;
8
+ }
9
+ export interface CompressResult {
10
+ text: string;
11
+ originalChars: number;
12
+ compressedChars: number;
13
+ route: ContentRoute;
14
+ transforms: string[];
15
+ estimatedTokensSaved: number;
16
+ }
17
+ export interface Compressor {
18
+ compact(text: string, ctx: CompressCtx): CompressResult;
19
+ }
20
+ export declare class NativeCompressor implements Compressor {
21
+ compact(text: string, ctx: CompressCtx): CompressResult;
22
+ }
23
+ export declare function compressDisplayText(text: string, ctx: CompressCtx): CompressResult;
@@ -0,0 +1,77 @@
1
+ import { classify } from "./router.js";
2
+ import { minifyJson } from "./transforms/json.js";
3
+ import { dedupAdjacentLines, escapeSentinelLikeLines, noteLine, } from "./transforms/log.js";
4
+ import { stripAnsi } from "./transforms/ansi.js";
5
+ import { normalizeWhitespace } from "./transforms/whitespace.js";
6
+ import { estimateTokensSaved } from "./estimate.js";
7
+ function identityResult(text, route = "identity") {
8
+ return {
9
+ text,
10
+ originalChars: text.length,
11
+ compressedChars: text.length,
12
+ route,
13
+ transforms: [],
14
+ estimatedTokensSaved: 0,
15
+ };
16
+ }
17
+ export class NativeCompressor {
18
+ compact(text, ctx) {
19
+ if (ctx.outputFormat === "json" || ctx.outputSchemaDeclared) {
20
+ return identityResult(text);
21
+ }
22
+ const route = classify(text);
23
+ if (route === "identity")
24
+ return identityResult(text);
25
+ if (route === "json") {
26
+ const minified = minifyJson(text.trim());
27
+ if (minified === null || minified.length >= text.length) {
28
+ return identityResult(text);
29
+ }
30
+ return {
31
+ text: minified,
32
+ originalChars: text.length,
33
+ compressedChars: minified.length,
34
+ route,
35
+ transforms: ["json-minify"],
36
+ estimatedTokensSaved: estimateTokensSaved(text, minified, route),
37
+ };
38
+ }
39
+ const counts = { folded: 0, escaped: 0 };
40
+ const transforms = [];
41
+ let current = text;
42
+ const apply = (name, fn) => {
43
+ const next = fn(current);
44
+ if (next !== current)
45
+ transforms.push(name);
46
+ current = next;
47
+ };
48
+ apply("lit-escape", input => escapeSentinelLikeLines(input, counts));
49
+ if (route === "ansi-text") {
50
+ apply("ansi-strip", input => stripAnsi(input, counts));
51
+ apply("dedup", input => dedupAdjacentLines(input, counts));
52
+ }
53
+ if (route === "log") {
54
+ apply("dedup", input => dedupAdjacentLines(input, counts));
55
+ }
56
+ apply("whitespace", normalizeWhitespace);
57
+ if (counts.folded + counts.escaped > 0) {
58
+ current = `${noteLine(counts.folded, counts.escaped)}\n${current}`;
59
+ transforms.push("leading-note");
60
+ }
61
+ if (current.length >= text.length) {
62
+ return identityResult(text, route);
63
+ }
64
+ return {
65
+ text: current,
66
+ originalChars: text.length,
67
+ compressedChars: current.length,
68
+ route,
69
+ transforms,
70
+ estimatedTokensSaved: estimateTokensSaved(text, current, route),
71
+ };
72
+ }
73
+ }
74
+ const defaultCompressor = new NativeCompressor();
75
+ export function compressDisplayText(text, ctx) {
76
+ return defaultCompressor.compact(text, ctx);
77
+ }
@@ -0,0 +1,2 @@
1
+ export type ContentRoute = "json" | "log" | "ansi-text" | "plain" | "identity";
2
+ export declare function classify(text: string): ContentRoute;
@@ -0,0 +1,57 @@
1
+ import { hasDangerousSequences } from "./transforms/ansi.js";
2
+ import { minifyJson } from "./transforms/json.js";
3
+ import { splitFences } from "./transforms/whitespace.js";
4
+ import { MIN_RUN } from "./transforms/log.js";
5
+ const HAS_ESCAPE = /\x1b/;
6
+ const HAS_INTERNAL_CR = /\r(?!\n|$)/;
7
+ const HAS_FENCE = /^ {0,3}(?:`{3,}|~{3,})/m;
8
+ function hasFence(text) {
9
+ return HAS_FENCE.test(text);
10
+ }
11
+ function hasRepeatedRuns(text) {
12
+ for (const segment of splitFences(text)) {
13
+ if (segment.fenced)
14
+ continue;
15
+ const lines = segment.text.split("\n");
16
+ let run = 1;
17
+ for (let i = 1; i < lines.length; i += 1) {
18
+ if (lines[i] === lines[i - 1] && lines[i].trim() !== "") {
19
+ run += 1;
20
+ if (run >= MIN_RUN)
21
+ return true;
22
+ }
23
+ else {
24
+ run = 1;
25
+ }
26
+ }
27
+ }
28
+ return false;
29
+ }
30
+ function isStructuralJson(text) {
31
+ const trimmed = text.trim();
32
+ if (trimmed.length < 2)
33
+ return false;
34
+ const first = trimmed[0];
35
+ const last = trimmed[trimmed.length - 1];
36
+ const structural = (first === "{" && last === "}") || (first === "[" && last === "]");
37
+ if (!structural)
38
+ return false;
39
+ return minifyJson(trimmed) !== null;
40
+ }
41
+ export function classify(text) {
42
+ if (text.length === 0)
43
+ return "identity";
44
+ const escapes = HAS_ESCAPE.test(text);
45
+ const internalCr = HAS_INTERNAL_CR.test(text);
46
+ if (escapes || internalCr) {
47
+ if (hasDangerousSequences(text))
48
+ return "identity";
49
+ if (!hasFence(text))
50
+ return "ansi-text";
51
+ }
52
+ if (isStructuralJson(text))
53
+ return "json";
54
+ if (hasRepeatedRuns(text))
55
+ return "log";
56
+ return "plain";
57
+ }
@@ -0,0 +1,3 @@
1
+ import { type MarkerCounts } from "./log.js";
2
+ export declare function hasDangerousSequences(text: string): boolean;
3
+ export declare function stripAnsi(text: string, counts: MarkerCounts): string;
@@ -0,0 +1,89 @@
1
+ import { mapUnfenced } from "./whitespace.js";
2
+ import { crMarker } from "./log.js";
3
+ const CSI = /\x1b\[([0-9:;<=>?]*)[ -/]*([@-~])/g;
4
+ const OSC = /\x1b\][\s\S]*?(?:\x07|\x1b\\)/g;
5
+ const DCS_APC_PM_SOS = /\x1b[PX^_][\s\S]*?\x1b\\/g;
6
+ const ESC_TWO_BYTE = /\x1b[@-Z\\-_]/g;
7
+ const STRAY_C0 = /[\x00-\x07\x0b\x0c\x0e-\x1f]/g;
8
+ const DANGEROUS_CSI_FINALS = new Set([
9
+ "A",
10
+ "B",
11
+ "C",
12
+ "D",
13
+ "E",
14
+ "F",
15
+ "G",
16
+ "H",
17
+ "f",
18
+ "S",
19
+ "T",
20
+ "J",
21
+ "s",
22
+ "u",
23
+ ]);
24
+ export function hasDangerousSequences(text) {
25
+ if (text.includes("\b"))
26
+ return true;
27
+ CSI.lastIndex = 0;
28
+ let match;
29
+ while ((match = CSI.exec(text)) !== null) {
30
+ const params = match[1];
31
+ const final = match[2];
32
+ if (DANGEROUS_CSI_FINALS.has(final))
33
+ return true;
34
+ if ((final === "h" || final === "l") && params.includes("?")) {
35
+ return true;
36
+ }
37
+ }
38
+ return false;
39
+ }
40
+ function stripEscapes(text) {
41
+ return text
42
+ .replace(OSC, "")
43
+ .replace(DCS_APC_PM_SOS, "")
44
+ .replace(CSI, "")
45
+ .replace(ESC_TWO_BYTE, "")
46
+ .replace(STRAY_C0, "");
47
+ }
48
+ const ASCII_ONLY = /^[\x09\x0d\x20-\x7e]*$/;
49
+ function overlayFrames(frames) {
50
+ const cells = [];
51
+ for (const frame of frames) {
52
+ for (let i = 0; i < frame.length; i += 1)
53
+ cells[i] = frame[i];
54
+ }
55
+ return cells.join("");
56
+ }
57
+ function collapseLine(line, out, counts) {
58
+ const crlf = line.endsWith("\r");
59
+ const body = crlf ? line.slice(0, -1) : line;
60
+ if (!body.includes("\r")) {
61
+ out.push(line);
62
+ return;
63
+ }
64
+ if (!ASCII_ONLY.test(body)) {
65
+ out.push(line);
66
+ return;
67
+ }
68
+ const frames = body.split("\r");
69
+ const visible = overlayFrames(frames);
70
+ out.push(crlf ? `${visible}\r` : visible);
71
+ out.push(crMarker(frames.length - 1));
72
+ counts.folded += 1;
73
+ }
74
+ export function stripAnsi(text, counts) {
75
+ if (hasDangerousSequences(text))
76
+ return text;
77
+ return mapUnfenced(text, segment => {
78
+ const stripped = stripEscapes(segment);
79
+ const out = [];
80
+ for (const line of stripped.split("\n")) {
81
+ if (line.includes("`")) {
82
+ out.push(line);
83
+ continue;
84
+ }
85
+ collapseLine(line, out, counts);
86
+ }
87
+ return out.join("\n");
88
+ });
89
+ }
@@ -0,0 +1 @@
1
+ export declare function minifyJson(text: string): string | null;