llm-cli-gateway 2.15.0 → 2.17.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 (80) hide show
  1. package/.agents/skills/least-cost-routing/SKILL.md +123 -0
  2. package/CHANGELOG.md +78 -1
  3. package/dist/acp/client.js +5 -0
  4. package/dist/acp/flight-redaction.d.ts +2 -0
  5. package/dist/acp/flight-redaction.js +2 -0
  6. package/dist/acp/process-manager.js +2 -1
  7. package/dist/acp/provider-registry.js +8 -8
  8. package/dist/acp/runtime.d.ts +1 -0
  9. package/dist/acp/runtime.js +20 -0
  10. package/dist/acp/types.d.ts +52 -0
  11. package/dist/acp/types.js +8 -0
  12. package/dist/api-provider.d.ts +1 -0
  13. package/dist/api-provider.js +1 -0
  14. package/dist/async-job-manager.d.ts +20 -1
  15. package/dist/async-job-manager.js +85 -28
  16. package/dist/claude-mcp-config.js +1 -1
  17. package/dist/compressor/estimate.d.ts +5 -0
  18. package/dist/compressor/estimate.js +21 -0
  19. package/dist/compressor/index.d.ts +23 -0
  20. package/dist/compressor/index.js +77 -0
  21. package/dist/compressor/router.d.ts +2 -0
  22. package/dist/compressor/router.js +57 -0
  23. package/dist/compressor/transforms/ansi.d.ts +3 -0
  24. package/dist/compressor/transforms/ansi.js +99 -0
  25. package/dist/compressor/transforms/json.d.ts +1 -0
  26. package/dist/compressor/transforms/json.js +156 -0
  27. package/dist/compressor/transforms/log.d.ts +12 -0
  28. package/dist/compressor/transforms/log.js +55 -0
  29. package/dist/compressor/transforms/whitespace.d.ts +8 -0
  30. package/dist/compressor/transforms/whitespace.js +79 -0
  31. package/dist/config.d.ts +39 -0
  32. package/dist/config.js +166 -6
  33. package/dist/db.js +4 -4
  34. package/dist/doctor.d.ts +34 -1
  35. package/dist/doctor.js +85 -3
  36. package/dist/executor.d.ts +6 -0
  37. package/dist/executor.js +16 -3
  38. package/dist/flight-recorder.d.ts +19 -0
  39. package/dist/flight-recorder.js +110 -2
  40. package/dist/http-transport.js +19 -21
  41. package/dist/index.d.ts +58 -2
  42. package/dist/index.js +929 -94
  43. package/dist/job-store.d.ts +14 -2
  44. package/dist/job-store.js +170 -43
  45. package/dist/lcr-priors.d.ts +60 -0
  46. package/dist/lcr-priors.js +190 -0
  47. package/dist/lcr-router-env.d.ts +20 -0
  48. package/dist/lcr-router-env.js +133 -0
  49. package/dist/lcr-telemetry.d.ts +2 -0
  50. package/dist/lcr-telemetry.js +17 -0
  51. package/dist/least-cost-router.d.ts +86 -0
  52. package/dist/least-cost-router.js +296 -0
  53. package/dist/least-cost-types.d.ts +34 -0
  54. package/dist/least-cost-types.js +1 -0
  55. package/dist/migrate-sessions.js +1 -1
  56. package/dist/migrate.js +1 -1
  57. package/dist/model-registry.js +1 -1
  58. package/dist/postgres-job-store-worker.js +56 -13
  59. package/dist/pricing.d.ts +17 -0
  60. package/dist/pricing.js +167 -0
  61. package/dist/provider-definitions.d.ts +5 -0
  62. package/dist/provider-definitions.js +56 -10
  63. package/dist/provider-tool-capabilities.js +3 -3
  64. package/dist/request-helpers.d.ts +2 -2
  65. package/dist/request-helpers.js +1 -1
  66. package/dist/resources.d.ts +37 -2
  67. package/dist/resources.js +96 -1
  68. package/dist/retry.d.ts +1 -0
  69. package/dist/retry.js +1 -1
  70. package/dist/spawn-env-isolation.d.ts +10 -0
  71. package/dist/spawn-env-isolation.js +55 -0
  72. package/dist/token-estimator.d.ts +6 -0
  73. package/dist/token-estimator.js +59 -0
  74. package/dist/upstream-contracts.js +48 -28
  75. package/dist/validation-receipt.js +1 -1
  76. package/dist/validation-tools.d.ts +6 -0
  77. package/dist/validation-tools.js +174 -54
  78. package/npm-shrinkwrap.json +2 -2
  79. package/package.json +15 -6
  80. package/setup/status.schema.json +68 -0
@@ -20,6 +20,7 @@ const DEFAULT_LEASE_RUNTIME_CONFIG = {
20
20
  role: "gateway",
21
21
  };
22
22
  const MAX_CONSECUTIVE_HEARTBEAT_FAILURES = 3;
23
+ const MIN_CONSECUTIVE_HEARTBEAT_SUCCESSES_TO_RECOVER = 3;
23
24
  export function extractApiHttpStatus(error) {
24
25
  for (const candidate of [error, error?.cause]) {
25
26
  if (candidate instanceof ApiHttpError && typeof candidate.status === "number") {
@@ -72,6 +73,9 @@ export class JobSaturationError extends Error {
72
73
  this.name = "JobSaturationError";
73
74
  }
74
75
  }
76
+ export function providerAtCapacity(snapshot, provider) {
77
+ return (snapshot.runningByProvider[provider] ?? 0) >= snapshot.maxRunningPerProvider;
78
+ }
75
79
  class JobLimiter {
76
80
  cfg;
77
81
  logger;
@@ -258,6 +262,10 @@ export class AsyncJobManager {
258
262
  sweepTimer = null;
259
263
  durableAdmission;
260
264
  consecutiveHeartbeatFailures = 0;
265
+ consecutiveHeartbeatSuccesses = 0;
266
+ lastHeartbeatFailureAt = null;
267
+ lastHeartbeatRecoveryAt = null;
268
+ lastHeartbeatErrorName = null;
261
269
  skipSweepThisCycle = false;
262
270
  nextHeartbeatExpectedAt = 0;
263
271
  disposed = false;
@@ -278,25 +286,11 @@ export class AsyncJobManager {
278
286
  maxQueuedJobs: limits.maxQueuedJobs,
279
287
  queueTimeoutMs: limits.queueTimeoutMs,
280
288
  }, logger);
281
- this.durableAdmission = store !== null;
289
+ this.durableAdmission = false;
282
290
  if (this.store) {
283
- try {
284
- this.store.registerInstance({
285
- instanceId: this.instanceId,
286
- role: this.lease.role ?? "gateway",
287
- hostname: this.hostname,
288
- pid: this.instancePid,
289
- });
290
- }
291
- catch (err) {
292
- this.durableAdmission = false;
293
- this.logger.error("#139 registerInstance failed; durable async admission is disabled for this instance", err);
294
- }
295
- if (this.durableAdmission) {
296
- this.runOrphanSweep();
297
- this.startHeartbeat();
298
- this.startReaper();
299
- }
291
+ this.restoreDurableAdmission("startup");
292
+ this.startHeartbeat();
293
+ this.startReaper();
300
294
  }
301
295
  this.evictionTimer = setInterval(() => this.evictCompletedJobs(), EVICTION_INTERVAL_MS);
302
296
  if (this.evictionTimer.unref) {
@@ -310,6 +304,17 @@ export class AsyncJobManager {
310
304
  canAdmitDurableJobs() {
311
305
  return this.store !== null && this.durableAdmission;
312
306
  }
307
+ getDurableAdmissionHealth() {
308
+ return {
309
+ storeAttached: this.store !== null,
310
+ admitting: this.canAdmitDurableJobs(),
311
+ consecutiveHeartbeatFailures: this.consecutiveHeartbeatFailures,
312
+ consecutiveHeartbeatSuccesses: this.consecutiveHeartbeatSuccesses,
313
+ lastHeartbeatFailureAt: this.lastHeartbeatFailureAt,
314
+ lastHeartbeatRecoveryAt: this.lastHeartbeatRecoveryAt,
315
+ lastHeartbeatErrorName: this.lastHeartbeatErrorName,
316
+ };
317
+ }
313
318
  assertDurableAdmission(provider) {
314
319
  if (this.store && !this.durableAdmission) {
315
320
  throw new Error(`Durable async admission is disabled for ${provider}: this gateway instance could not register or lost its heartbeat lease. Retry after the gateway recovers.`);
@@ -394,13 +399,23 @@ export class AsyncJobManager {
394
399
  try {
395
400
  this.store.heartbeat(this.instanceId);
396
401
  this.consecutiveHeartbeatFailures = 0;
402
+ if (!this.durableAdmission) {
403
+ this.consecutiveHeartbeatSuccesses++;
404
+ if (this.consecutiveHeartbeatSuccesses >= MIN_CONSECUTIVE_HEARTBEAT_SUCCESSES_TO_RECOVER) {
405
+ this.restoreDurableAdmission("heartbeat recovery");
406
+ }
407
+ }
397
408
  }
398
409
  catch (err) {
399
410
  this.consecutiveHeartbeatFailures++;
411
+ this.consecutiveHeartbeatSuccesses = 0;
412
+ this.lastHeartbeatFailureAt = new Date().toISOString();
413
+ this.lastHeartbeatErrorName = err instanceof Error ? err.name : typeof err;
400
414
  this.logger.error(`#139 heartbeat failed (${this.consecutiveHeartbeatFailures}/${MAX_CONSECUTIVE_HEARTBEAT_FAILURES})`, err);
401
415
  if (this.consecutiveHeartbeatFailures >= MAX_CONSECUTIVE_HEARTBEAT_FAILURES) {
402
416
  if (this.durableAdmission) {
403
417
  this.durableAdmission = false;
418
+ this.consecutiveHeartbeatSuccesses = 0;
404
419
  this.logger.error("#139 sustained heartbeat failure; disabling durable admission and orphan sweeping on this instance");
405
420
  }
406
421
  }
@@ -412,8 +427,10 @@ export class AsyncJobManager {
412
427
  startReaper() {
413
428
  this.sweepTimer = setInterval(() => {
414
429
  this.runOrphanSweep();
430
+ if (!this.store || this.disposed || !this.durableAdmission)
431
+ return;
415
432
  try {
416
- this.store?.gcInstances(this.lease.instanceGcMs);
433
+ this.store.gcInstances(this.lease.instanceGcMs);
417
434
  }
418
435
  catch (err) {
419
436
  this.logger.error("#139 gateway_instances GC failed", err);
@@ -422,6 +439,35 @@ export class AsyncJobManager {
422
439
  if (this.sweepTimer.unref)
423
440
  this.sweepTimer.unref();
424
441
  }
442
+ restoreDurableAdmission(reason) {
443
+ if (!this.store || this.disposed)
444
+ return;
445
+ try {
446
+ this.store.registerInstance({
447
+ instanceId: this.instanceId,
448
+ role: this.lease.role ?? "gateway",
449
+ hostname: this.hostname,
450
+ pid: this.instancePid,
451
+ });
452
+ this.store.heartbeat(this.instanceId);
453
+ const wasDisabled = !this.durableAdmission;
454
+ this.durableAdmission = true;
455
+ this.consecutiveHeartbeatFailures = 0;
456
+ this.consecutiveHeartbeatSuccesses = 0;
457
+ this.lastHeartbeatRecoveryAt = new Date().toISOString();
458
+ if (wasDisabled && reason === "heartbeat recovery") {
459
+ this.logger.info("#139 durable heartbeat recovered; re-enabled async admission and orphan sweeping");
460
+ }
461
+ this.runOrphanSweep();
462
+ }
463
+ catch (err) {
464
+ this.durableAdmission = false;
465
+ this.consecutiveHeartbeatSuccesses = 0;
466
+ this.lastHeartbeatFailureAt = new Date().toISOString();
467
+ this.lastHeartbeatErrorName = err instanceof Error ? err.name : typeof err;
468
+ this.logger.error(`#139 ${reason} register/heartbeat failed; durable async admission remains disabled`, err);
469
+ }
470
+ }
425
471
  runOrphanSweep() {
426
472
  if (!this.store || this.disposed || !this.durableAdmission)
427
473
  return;
@@ -493,13 +539,13 @@ export class AsyncJobManager {
493
539
  acq.cancel();
494
540
  this.jobs.delete(job.id);
495
541
  this.logger.error(`#139 durable recordStart failed for job ${job.id}; failing the request (fail-closed)`, err);
496
- throw new Error(`Durable job admission failed for ${job.cli}: the job store rejected recordStart`);
542
+ throw new Error(`Durable job admission failed for ${job.cli}: the job store rejected recordStart`, { cause: err });
497
543
  }
498
544
  }
499
545
  markRunningDurable(job, pid, failClosed = false) {
500
546
  if (!this.store)
501
547
  return;
502
- let transitioned = false;
548
+ let transitioned;
503
549
  try {
504
550
  transitioned = this.store.markRunning(job.id, { pid });
505
551
  }
@@ -645,7 +691,7 @@ export class AsyncJobManager {
645
691
  if (evicted > 0) {
646
692
  this.logger.debug(`Evicted ${evicted} completed jobs from memory (durable store retains them)`);
647
693
  }
648
- if (this.store) {
694
+ if (this.store && this.durableAdmission) {
649
695
  try {
650
696
  const removed = this.store.evictExpired();
651
697
  if (removed > 0) {
@@ -657,11 +703,12 @@ export class AsyncJobManager {
657
703
  }
658
704
  }
659
705
  }
660
- buildRequestKey(cli, args, env, stdin, cwd, outputFormat) {
706
+ buildRequestKey(cli, args, env, stdin, cwd, outputFormat, compressResponse) {
661
707
  const extraEnv = canonicaliseEnvForKey(env);
662
708
  const withStdin = stdin === undefined ? extraEnv : `${extraEnv}|stdin:${stdin}`;
663
709
  const withCwd = cwd === undefined ? withStdin : `${withStdin}|cwd:${cwd}`;
664
- const extra = cli === "codex" ? `${withCwd}|fmt:${outputFormat ?? "text"}` : withCwd;
710
+ const withFmt = cli === "codex" ? `${withCwd}|fmt:${outputFormat ?? "text"}` : withCwd;
711
+ const extra = compressResponse ? `${withFmt}|compress:1` : withFmt;
665
712
  const principal = resolveOwnerPrincipal(getRequestContext());
666
713
  const scoped = principal === "local" ? extra : `${extra}|principal:${principal}`;
667
714
  return computeRequestKey(cli, args, scoped);
@@ -927,7 +974,7 @@ export class AsyncJobManager {
927
974
  durationMs,
928
975
  retryCount: 0,
929
976
  circuitBreakerState: "closed",
930
- optimizationApplied: false,
977
+ optimizationApplied: job.flightRecorderEntry.optimizationApplied ?? false,
931
978
  exitCode,
932
979
  httpStatus: job.transport === "http" ? (job.httpStatus ?? undefined) : undefined,
933
980
  errorMessage,
@@ -937,6 +984,7 @@ export class AsyncJobManager {
937
984
  cacheReadTokens: usage.cacheReadTokens,
938
985
  cacheCreationTokens: usage.cacheCreationTokens,
939
986
  costUsd: usage.costUsd,
987
+ costBasis: usage.costBasis,
940
988
  providerSessionId: providerMeta?.sessionId,
941
989
  stopReason: providerMeta?.stopReason,
942
990
  });
@@ -965,6 +1013,7 @@ export class AsyncJobManager {
965
1013
  inputTokens: u.inputTokens,
966
1014
  outputTokens: u.outputTokens,
967
1015
  cacheReadTokens: u.cacheReadTokens,
1016
+ cacheCreationTokens: u.cacheCreationTokens,
968
1017
  costUsd: u.costUsd,
969
1018
  };
970
1019
  }
@@ -1068,6 +1117,7 @@ export class AsyncJobManager {
1068
1117
  exited: row.status !== "running" && row.status !== "queued",
1069
1118
  metricsRecorded: true,
1070
1119
  outputFormat: row.outputFormat ?? undefined,
1120
+ compressResponse: row.compressResponse ?? null,
1071
1121
  ownerPrincipal: row.ownerPrincipal,
1072
1122
  outputDirty: false,
1073
1123
  lastOutputFlushAt: Date.now(),
@@ -1081,7 +1131,7 @@ export class AsyncJobManager {
1081
1131
  job = this.hydrateFromStore(jobId) ?? undefined;
1082
1132
  return job?.ownerPrincipal;
1083
1133
  }
1084
- startJob(cli, args, correlationId, cwd, idleTimeoutMs, outputFormat, forceRefresh, env, onComplete, flightRecorderEntry, extractUsage, writeFlightStart, stdin) {
1134
+ startJob(cli, args, correlationId, cwd, idleTimeoutMs, outputFormat, forceRefresh, env, onComplete, flightRecorderEntry, extractUsage, writeFlightStart, stdin, compressResponse) {
1085
1135
  return this.startJobWithDedup(cli, args, correlationId, {
1086
1136
  cwd,
1087
1137
  idleTimeoutMs,
@@ -1093,11 +1143,12 @@ export class AsyncJobManager {
1093
1143
  flightRecorderEntry,
1094
1144
  extractUsage,
1095
1145
  writeFlightStart,
1146
+ compressResponse,
1096
1147
  }).snapshot;
1097
1148
  }
1098
1149
  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);
1150
+ const { cwd, idleTimeoutMs, outputFormat, forceRefresh, env: extraEnv, stdin, onComplete, flightRecorderEntry, extractUsage, writeFlightStart, compressResponse, } = opts;
1151
+ const requestKey = this.buildRequestKey(cli, args, extraEnv, stdin, cwd, outputFormat, compressResponse);
1101
1152
  if (!forceRefresh) {
1102
1153
  const reused = this.tryReuseDedupedJob(requestKey, correlationId, cli, onComplete);
1103
1154
  if (reused)
@@ -1130,6 +1181,7 @@ export class AsyncJobManager {
1130
1181
  exited: false,
1131
1182
  metricsRecorded: false,
1132
1183
  outputFormat,
1184
+ compressResponse: compressResponse ?? null,
1133
1185
  onComplete,
1134
1186
  onCompleteFired: false,
1135
1187
  outputDirty: false,
@@ -1178,6 +1230,7 @@ export class AsyncJobManager {
1178
1230
  cli,
1179
1231
  args: [...args],
1180
1232
  outputFormat,
1233
+ compressResponse,
1181
1234
  startedAt,
1182
1235
  pid: null,
1183
1236
  ownerPrincipal,
@@ -1222,6 +1275,7 @@ export class AsyncJobManager {
1222
1275
  cwd,
1223
1276
  stdio: stdin === undefined ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"],
1224
1277
  env: { ...baseEnv, ...(extraEnv ?? {}) },
1278
+ logger: this.logger,
1225
1279
  });
1226
1280
  job.process = child;
1227
1281
  try {
@@ -1531,6 +1585,9 @@ export class AsyncJobManager {
1531
1585
  getJobOutputFormat(jobId) {
1532
1586
  return this.jobs.get(jobId)?.outputFormat;
1533
1587
  }
1588
+ getJobCompressResponse(jobId) {
1589
+ return this.jobs.get(jobId)?.compressResponse === true;
1590
+ }
1534
1591
  getJobCli(jobId) {
1535
1592
  return this.jobs.get(jobId)?.cli;
1536
1593
  }
@@ -147,7 +147,7 @@ export function buildClaudeMcpConfig(servers) {
147
147
  }
148
148
  catch (error) {
149
149
  const message = error instanceof Error ? error.message : String(error);
150
- throw new Error(`Failed to write Claude MCP config: ${message}`);
150
+ throw new Error(`Failed to write Claude MCP config: ${message}`, { cause: error });
151
151
  }
152
152
  return { path: configPath, enabled, missing };
153
153
  }
@@ -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,99 @@
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
+ function isSingleColumnAsciiFrame(text) {
49
+ for (const character of text) {
50
+ const codePoint = character.codePointAt(0);
51
+ if (codePoint !== 0x09 &&
52
+ codePoint !== 0x0d &&
53
+ (codePoint === undefined || codePoint < 0x20 || codePoint > 0x7e)) {
54
+ return false;
55
+ }
56
+ }
57
+ return true;
58
+ }
59
+ function overlayFrames(frames) {
60
+ const cells = [];
61
+ for (const frame of frames) {
62
+ for (let i = 0; i < frame.length; i += 1)
63
+ cells[i] = frame[i];
64
+ }
65
+ return cells.join("");
66
+ }
67
+ function collapseLine(line, out, counts) {
68
+ const crlf = line.endsWith("\r");
69
+ const body = crlf ? line.slice(0, -1) : line;
70
+ if (!body.includes("\r")) {
71
+ out.push(line);
72
+ return;
73
+ }
74
+ if (!isSingleColumnAsciiFrame(body)) {
75
+ out.push(line);
76
+ return;
77
+ }
78
+ const frames = body.split("\r");
79
+ const visible = overlayFrames(frames);
80
+ out.push(crlf ? `${visible}\r` : visible);
81
+ out.push(crMarker(frames.length - 1));
82
+ counts.folded += 1;
83
+ }
84
+ export function stripAnsi(text, counts) {
85
+ if (hasDangerousSequences(text))
86
+ return text;
87
+ return mapUnfenced(text, segment => {
88
+ const stripped = stripEscapes(segment);
89
+ const out = [];
90
+ for (const line of stripped.split("\n")) {
91
+ if (line.includes("`")) {
92
+ out.push(line);
93
+ continue;
94
+ }
95
+ collapseLine(line, out, counts);
96
+ }
97
+ return out.join("\n");
98
+ });
99
+ }
@@ -0,0 +1 @@
1
+ export declare function minifyJson(text: string): string | null;