@goodandready/dsh-context-lens 0.1.7 → 0.1.8

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
@@ -65,12 +65,15 @@ graph LR
65
65
  ## ✨ Key Capabilities & Modules
66
66
 
67
67
  ### 1. 🧬 Multi-Language AST Code Skeletonizer (`lib/ast/skeletonizer.js`)
68
- * Automatically extracts structural interfaces, function signatures, classes, types, and exports across **TypeScript, JavaScript, Python, and Go**;
68
+ * Automatically extracts structural interfaces, function signatures, classes, types, and exports across **TypeScript, JavaScript, Python, Go, Rust, and Java**;
69
+ * Supports `pub async fn`, `async fn`, `pub(crate)` and `pub(super)` in Rust;
70
+ * Retains JSDoc, docstrings, and structural comments preceding definitions;
69
71
  * Drops internal function implementations, loops, and repetitive boilerplate while preserving indentation and export declarations;
70
72
  * Allows the agent to understand entire multi-package repository architectures without loading tens of thousands of implementation tokens.
71
73
 
72
74
  ### 2. 🗜️ Fast Heuristic Log Condenser (`lib/compression/log-compressor.js`)
73
- * High-performance $O(n)$ heuristic line filter for test runners and build tools (Jest, Vitest, Pytest, Go test, NPM, Webpack, Cargo);
75
+ * High-performance $O(n)$ heuristic line filter for test runners and build tools (Jest, Vitest, Pytest, Go test, NPM, Webpack, Cargo, Maven/Gradle);
76
+ * Strips terminal ANSI escape color sequences before evaluating regex patterns;
74
77
  * Automatically filters out passing test noise (`PASS`, `✓`, `ok`) and build notices;
75
78
  * Retains critical error lines, stack traces, assertion failures (`Expected ... Received ...`), and failure context windows;
76
79
  * 3 Aggressiveness Modes: `raw`, `balanced`, and `aggressive`.
@@ -81,7 +84,8 @@ graph LR
81
84
 
82
85
  ### 4. 📊 Token Savings Tracking & Dashboard (`lib/tokens/tracker.js` & `lib/client.js`)
83
86
  * Measures exact token counts before and after compression;
84
- * Calculates cumulative session token savings and displays live efficiency percentage badges in the DSH interface.
87
+ * Calculates cumulative session token savings and displays live efficiency percentage badges in the DSH interface;
88
+ * Enforces Token Budget Guard limits with alert warnings when exceeding 90% budget.
85
89
 
86
90
  ---
87
91
 
@@ -90,9 +94,9 @@ graph LR
90
94
  | Tool Name | Parameters | Description |
91
95
  |---|---|---|
92
96
  | `context_lens_focus` | `paths: string[]`, `maxDepth?: number` | Designates active focus files/folders; collapses surrounding workspace into AST skeletons |
93
- | `context_lens_compress_log` | `log: string`, `mode?: "raw"\|"balanced"\|"aggressive"` | Condenses terminal/test outputs, keeping only stack traces and failure windows |
94
- | `context_lens_compress_code` | `code: string`, `language?: string`, `maxDepth?: number` | Generates a clean structural AST skeleton from raw source code |
95
- | `context_lens_stats` | *(none)* | Returns real-time cumulative token savings, original token count, and efficiency % |
97
+ | `context_lens_compress_log` | `text: string` *(or `log`)*, `mode?: "raw"\|"balanced"\|"aggressive"`, `maxLines?: number`, `auto?: boolean` | Condenses terminal/test outputs, keeping only stack traces and failure windows |
98
+ | `context_lens_compress_code` | `code: string`, `language?: string`, `maxDepth?: number`, `filePath?: string` | Generates a clean structural AST skeleton from raw source code |
99
+ | `context_lens_stats` | *(none)* | Returns real-time cumulative token savings, history, and budget status |
96
100
 
97
101
  ---
98
102
 
@@ -111,13 +115,27 @@ dsh plugin --profile web add @goodandready/dsh-context-lens
111
115
 
112
116
  ```yaml
113
117
  dsh-context-lens:
114
- compressionMode: balanced # 'raw', 'balanced', or 'aggressive'
115
- astSkeletonMaxDepth: 3 # Maximum depth level for AST signature traversal
116
- tokenSavingsTracking: true # Track and display live token savings
118
+ compressionMode: balanced # 'raw', 'balanced', or 'aggressive'
119
+ astSkeletonMaxDepth: 3 # Maximum depth level for AST signature traversal (1..10)
120
+ tokenSavingsTracking: true # Track and display live token savings
121
+ autoCompressThreshold: 4000 # Auto-compression character threshold (0 to disable)
122
+ budgetLimit: 100000 # Session token budget limit
123
+ autoCollapse: true # Auto-collapse UI when budget is nearly exhausted
117
124
  ```
118
125
 
119
126
  ---
120
127
 
128
+ ## 📝 Version History
129
+
130
+ ### v0.1.8
131
+ * **Fix**: Support both `text` and `log` parameter names in `context_lens_compress_log`.
132
+ * **Fix**: Cross-platform path resolution in unit tests on Windows (`fileURLToPath`).
133
+ * **Fix**: Dynamic propagation of `budgetLimit` configuration into token tracker.
134
+ * **Fix**: ANSI terminal escape sequence stripping for colored logs.
135
+ * **Fix**: Expanded Rust syntax support (`pub async fn`, `pub(crate)`) and proper `#` comment prefix for Python.
136
+
137
+ ---
138
+
121
139
  ## 📄 License
122
140
 
123
141
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
package/README.ru.md CHANGED
@@ -65,13 +65,16 @@ graph LR
65
65
  ## ✨ Ключевые возможности
66
66
 
67
67
  ### 1. 🧬 Мультиязычный AST-скелетонизатор кода (`lib/ast/skeletonizer.js`)
68
- * Извлекает структурные интерфейсы, сигнатуры функций, методы классов, типы и экспорты для **TypeScript, JavaScript, Python и Go**;
68
+ * Извлекает структурные интерфейсы, сигнатуры функций, методы классов, типы и экспорты для **TypeScript, JavaScript, Python, Go, Rust и Java**;
69
+ * Поддерживает `pub async fn`, `async fn`, `pub(crate)` и `pub(super)` в Rust;
70
+ * Сохраняет JSDoc, docstrings и структурные комментарии над определениями;
69
71
  * Удаляет внутренние тела функций и комментарии реализации, оставляя точную архитектуру файла;
70
72
  * Позволяет агенту обозревать всю структуру проекта без загрузки лишних десятков тысяч токенов.
71
73
 
72
74
  ### 2. 🗜️ Быстрый компрессор логов (`lib/compression/log-compressor.js`)
73
- * Высокопроизводительный $O(n)$ фильтр шума логов тестирования и сборки (Jest, Vitest, Pytest, Go test, NPM, Webpack, Cargo);
74
- * Автоматически вырезает успешные проверки (`PASS`, `✓`, `ok`) и служебные уведомления;
75
+ * Высокопроизводительный $O(n)$ фильтр шума логов тестирования и сборки (Jest, Vitest, Pytest, Go test, NPM, Webpack, Cargo, Maven/Gradle);
76
+ * Автоматическая очистка терминальных ANSI-эскейп последовательностей;
77
+ * Вырезает успешные проверки (`PASS`, `✓`, `ok`) и служебные уведомления;
75
78
  * Сохраняет строки падений, стек-трейсы, расхождения в утверждениях (`Expected ... Received ...`) и контекстное окружение ошибки;
76
79
  * 3 Режима сжатия: `raw`, `balanced`, `aggressive`.
77
80
 
@@ -81,7 +84,8 @@ graph LR
81
84
 
82
85
  ### 4. 📊 Трекер экономии токенов (`lib/tokens/tracker.js` & `lib/client.js`)
83
86
  * Фиксация точного числа токенов до и после сжатия;
84
- * Подсчёт накопленной экономии за сессию с отображением процента эффективности в Web UI.
87
+ * Подсчёт накопленной экономии за сессию с отображением процента эффективности в Web UI;
88
+ * Контроль сессионного бюджета токенов (Token Budget Guard) с предупреждением при превышении 90%.
85
89
 
86
90
  ---
87
91
 
@@ -90,9 +94,9 @@ graph LR
90
94
  | Имя инструмента | Параметры | Описание |
91
95
  |---|---|---|
92
96
  | `context_lens_focus` | `paths: string[]`, `maxDepth?: number` | Задаёт пути активного фокуса; всё остальное сворачивается в AST-скелеты |
93
- | `context_lens_compress_log` | `log: string`, `mode?: "raw"\|"balanced"\|"aggressive"` | Сжимает вывод тестов и терминала, сохраняя стек-трейсы и ошибки |
94
- | `context_lens_compress_code` | `code: string`, `language?: string`, `maxDepth?: number` | Генерирует структурный AST-скелет из переданного исходного кода |
95
- | `context_lens_stats` | *(нет)* | Возвращает метрики сэкономленных токенов и процент эффективности |
97
+ | `context_lens_compress_log` | `text: string` *(или `log`)*, `mode?: "raw"\|"balanced"\|"aggressive"`, `maxLines?: number`, `auto?: boolean` | Сжимает вывод тестов и терминала, сохраняя стек-трейсы и ошибки |
98
+ | `context_lens_compress_code` | `code: string`, `language?: string`, `maxDepth?: number`, `filePath?: string` | Генерирует структурный AST-скелет из переданного исходного кода |
99
+ | `context_lens_stats` | *(нет)* | Возвращает метрики сэкономленных токенов, историю и статус бюджета |
96
100
 
97
101
  ---
98
102
 
@@ -108,13 +112,27 @@ dsh plugin --profile web add @goodandready/dsh-context-lens
108
112
 
109
113
  ```yaml
110
114
  dsh-context-lens:
111
- compressionMode: balanced # 'raw', 'balanced' или 'aggressive'
112
- astSkeletonMaxDepth: 3 # Максимальная глубина обхода AST-сигнатур
113
- tokenSavingsTracking: true # Включить трекинг сэкономленных токенов
115
+ compressionMode: balanced # 'raw', 'balanced' или 'aggressive'
116
+ astSkeletonMaxDepth: 3 # Максимальная глубина обхода AST-сигнатур (1..10)
117
+ tokenSavingsTracking: true # Включить трекинг сэкономленных токенов
118
+ autoCompressThreshold: 4000 # Порог авто-сжатия в символах (0 для отключения)
119
+ budgetLimit: 100000 # Сессионный лимит бюджета токенов
120
+ autoCollapse: true # Автоматически сворачивать UI при исчерпании бюджета
114
121
  ```
115
122
 
116
123
  ---
117
124
 
125
+ ## 📝 История версий
126
+
127
+ ### v0.1.8
128
+ * **Fix**: Поддержка как `text`, так и `log` в параметрах инструмента `context_lens_compress_log`.
129
+ * **Fix**: Кроссплатформенное разрешение путей в тестах на Windows (`fileURLToPath`).
130
+ * **Fix**: Динамическая передача и учёт `budgetLimit` в трекере токенов.
131
+ * **Fix**: Корректная обработка цветных логов с терминальными ANSI-кодами.
132
+ * **Fix**: Расширена поддержка Rust (`pub async fn`, `pub(crate)`) и правильные комментарии `#` для Python в AST-скелетонизаторе.
133
+
134
+ ---
135
+
118
136
  ## 📄 Лицензия
119
137
 
120
138
  MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
@@ -2,8 +2,9 @@
2
2
  const JS_FUNC_RE = /^\s*(export\s+)?(async\s+)?(function\s+(\w+)|const\s+(\w+)\s*=\s*(async\s+)?\([^)]*\)\s*=>|(\w+)\s*:\s*\([^)]*\)\s*=>|class\s+(\w+)|interface\s+(\w+)|type\s+(\w+)\s*=)/;
3
3
  const PY_RE = /^\s*(def\s+(\w+)\s*\([^)]*\)|class\s+(\w+).*?:|async def\s+(\w+)\s*\([^)]*\))/;
4
4
  const GO_RE = /^\s*(func\s+(\([^)]+\)\s+)?(\w+)\s*\([^)]*\)|type\s+(\w+)\s+(struct|interface))/;
5
- const IMPORT_RE = /^\s*import\s+.*from\s+['"].*['"]|^\s*import\s+['"].*['"]|^\s*export\s+.*from\s+['"]/;
6
- const COMMENT_RE = /^\s*(\/\/.*|\/\*.*\*\/|#.*)/;
5
+ const RUST_RE = /^\s*(pub(\([^)]+\))?\s+)?(async\s+)?(fn|struct|enum|impl|trait|type|const|static)\s+\w+/;
6
+ const IMPORT_RE = /^\s*import\s+.*from\s+['"].*['"]|^\s*import\s+['"].*['"]|^\s*export\s+.*from\s+['"]|^\s*use\s+[a-zA-Z0-9_:]+/;
7
+ const COMMENT_RE = /^\s*(\/\/.*|\/\*.*\*\/|\/\*.*|\*.*|#.*)/;
7
8
 
8
9
  function indentDepth(line) {
9
10
  const m = line.match(/^(\s*)/);
@@ -39,13 +40,13 @@ export function skeletonize(text, { maxDepth = 3, language, includeImports = tru
39
40
  while (nextIdx < lines.length && !lines[nextIdx].trim()) nextIdx++;
40
41
  if (nextIdx < lines.length) {
41
42
  const nextLine = lines[nextIdx];
42
- if (JS_FUNC_RE.test(nextLine) || PY_RE.test(nextLine) || GO_RE.test(nextLine) || /^\s*(class|function|interface|type|def |func )/.test(nextLine)) {
43
+ if (JS_FUNC_RE.test(nextLine) || PY_RE.test(nextLine) || GO_RE.test(nextLine) || RUST_RE.test(nextLine) || /^\s*(class|function|interface|type|def |func |pub |fn )/.test(nextLine)) {
43
44
  pendingComment = line.trim().slice(0, 120);
44
45
  continue;
45
46
  }
46
47
  }
47
48
  // Otherwise, keep top-level comments (up to 5)
48
- if (out.length < 5 && line.trim().startsWith('//')) {
49
+ if (out.length < 5 && (line.trim().startsWith('//') || line.trim().startsWith('#') || line.trim().startsWith('/*'))) {
49
50
  const sig = line.trim().slice(0, 120);
50
51
  if (!seen.has(sig)) {
51
52
  seen.add(sig);
@@ -57,9 +58,9 @@ export function skeletonize(text, { maxDepth = 3, language, includeImports = tru
57
58
  }
58
59
  let sig = null;
59
60
  // try JS/TS
60
- if (!language || language === 'js' || language === 'ts' || language === 'rust' || language === 'java') {
61
+ if (!language || language === 'js' || language === 'ts') {
61
62
  const m = line.match(/^\s*(export\s+)?(async\s+)?(function\s+\w+[^\n]*|const\s+\w+\s*=.*=>.*|class\s+\w+.*|interface\s+\w+.*|type\s+\w+\s*=.*|(?:public|private|protected)?\s*(async\s+)?\w+\s*\([^)]*\)\s*[:{]|import\s+.*|export\s+.*)/);
62
- if (m) sig = line.trim().replace(/\s*\{.*$/, '').replace(/\s+$/, '') + (line.trim().endsWith('{') ? '' : '');
63
+ if (m) sig = line.trim().replace(/\s*\{.*$/, '').replace(/\s+$/, '');
63
64
  if (!sig && /^\s*(export\s+)?(async\s+)?function\s+\w+/.test(line)) sig = line.trim();
64
65
  if (!sig && /^\s*class\s+\w+/.test(line)) sig = line.trim();
65
66
  if (!sig && /^\s*import\s+/.test(line)) sig = line.trim().slice(0, 120);
@@ -72,14 +73,15 @@ export function skeletonize(text, { maxDepth = 3, language, includeImports = tru
72
73
  const m = line.match(GO_RE);
73
74
  if (m) sig = line.trim();
74
75
  }
75
- // Rust/Java fallback
76
- if (!sig && (language === 'rust' || language === 'java')) {
77
- if (/^\s*(pub\s+)?(fn|struct|enum|impl|trait)\s+\w+/.test(line)) sig = line.trim();
78
- if (/^\s*(public\s+)?(class|interface|enum)\s+\w+/.test(line)) sig = line.trim();
76
+ if (!sig && (!language || language === 'rust')) {
77
+ if (RUST_RE.test(line)) sig = line.trim();
78
+ }
79
+ if (!sig && (!language || language === 'java')) {
80
+ if (/^\s*(public|protected|private|static|\s)+\s+(class|interface|enum|record|\w+)\s+\w+/.test(line)) sig = line.trim();
79
81
  }
80
82
  // generic fallback: if no specific language, try all
81
83
  if (!sig && !language) {
82
- if (JS_FUNC_RE.test(line) || PY_RE.test(line) || GO_RE.test(line)) sig = line.trim();
84
+ if (JS_FUNC_RE.test(line) || PY_RE.test(line) || GO_RE.test(line) || RUST_RE.test(line)) sig = line.trim();
83
85
  }
84
86
  if (sig) {
85
87
  // If we have a pending comment, prepend it
@@ -112,7 +114,8 @@ export function skeletonize(text, { maxDepth = 3, language, includeImports = tru
112
114
  const total = lines.filter(l => l.trim()).length;
113
115
  const removed = total - out.length;
114
116
  if (removed > 0 && out.length > 0) {
115
- out.push(`// ... ${removed} lines collapsed (skeleton, maxDepth=${maxDepth})`);
117
+ const commentPrefix = (language === 'py' || language === 'python') ? '# ' : '// ';
118
+ out.push(`${commentPrefix}... ${removed} lines collapsed (skeleton, maxDepth=${maxDepth})`);
116
119
  }
117
120
  return out.join('\n');
118
121
  }
package/lib/client.js CHANGED
@@ -76,8 +76,7 @@ window.__ModuleLoader__.load({
76
76
  ...history.slice(0, 3).map((h) => React.createElement('div', { key: h.id, style: { fontSize: 11, color: 'var(--dsw-alias-label-secondary)', padding: '2px 0' } }, `−${h.savedTokens} tk (${h.savedPercent}%) · ${(h.preview || '').slice(0, 40)}`))
77
77
  ) : null,
78
78
  React.createElement('div', { style: { marginTop: 12, display: 'flex', gap: 8 } },
79
- React.createElement('button', { onClick: refresh, style: { fontSize: 12, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', cursor: 'pointer' } }, 'Refresh'),
80
- React.createElement('button', { onClick: () => { try { _ctx.betterSidebar.openTab({ type: 'dsh-context-lens:tab', title: 'Lens' }) } catch(e){} }, style: { fontSize: 12, padding: '4px 8px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'transparent', cursor: 'pointer' } }, 'Settings')
79
+ React.createElement('button', { onClick: refresh, style: { fontSize: 12, padding: '4px 10px', borderRadius: 6, border: '1px solid var(--dsw-alias-border-l2)', background: 'var(--dsw-alias-bg-layer-3)', color: 'var(--dsw-alias-label-primary)', cursor: 'pointer' } }, 'Refresh')
81
80
  )
82
81
  );
83
82
  }
@@ -1,13 +1,17 @@
1
1
  // ponytail: heuristic line filter, not ML — O(n) scan, no deps
2
+ const ANSI_RE = /\u001b\[[0-9;]*[a-zA-Z]/g;
2
3
  const KEEP_RE = /(FAIL|FAILED|Error|AssertionError|Exception|Traceback|panic|npm ERR!|ERR!|Expected|Received|missing|×|●|✕|FAIL:|--- FAIL|not ok|at\s+.*:\d+:\d+|stack|Caused by|test result:\s*FAILED|running \d+ test|test .* \.\.\. FAILED|BUILD FAILED|Tests run:|FAILURE:|cargo:.*error|error\[E\d+\]|thread '.*' panicked)/i;
3
4
  const PASS_RE = /^(PASS|\s*✓|\s*✔|\s*ok\s|…+\s*$|\s*\.\s*$|test result:\s*ok|running \d+ test.*ok)/i;
4
5
  const NOISE_RE = /^\s*(npm (notice|warn|info)|Browserslist|cached|Downloading|Done in|Compiling\s|cargo:.*Finished)/i;
5
6
  const STACK_RE = /^\s*(at\s+|File ".*", line \d+|#\d+\s+0x|goroutine \d+ \[|thread '.*' panicked|note: run with)/;
6
7
 
7
- function keepLine(line, mode) {
8
- if (KEEP_RE.test(line) || STACK_RE.test(line)) return true;
9
- if (mode === 'raw') return !(PASS_RE.test(line) || NOISE_RE.test(line)) ? true : false;
10
- // balanced/aggressive rely on context window, not bare line
8
+ function cleanAnsi(str) {
9
+ return (str || '').replace(ANSI_RE, '');
10
+ }
11
+
12
+ function keepLine(line) {
13
+ const clean = cleanAnsi(line);
14
+ if (KEEP_RE.test(clean) || STACK_RE.test(clean)) return true;
11
15
  return false;
12
16
  }
13
17
 
@@ -20,8 +24,13 @@ export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
20
24
  const lines = text.split(/\r?\n/);
21
25
  const totalLines = lines.length;
22
26
  if (mode === 'raw') {
23
- // raw: drop only obvious noise, keep rest
24
- const filtered = lines.filter((l) => !NOISE_RE.test(l) || KEEP_RE.test(l));
27
+ // raw: drop only obvious noise and non-failure passes, keep rest
28
+ const filtered = lines.filter((l) => {
29
+ const clean = cleanAnsi(l);
30
+ if (KEEP_RE.test(clean) || STACK_RE.test(clean)) return true;
31
+ if (NOISE_RE.test(clean)) return false;
32
+ return true;
33
+ });
25
34
  const truncated = filtered.length > maxLines ? [...filtered.slice(0, maxLines - 1), `… truncated ${filtered.length - maxLines + 1} lines`] : filtered;
26
35
  const compressed = truncated.join('\n');
27
36
  const originalTokens = estimateTokens(text);
@@ -32,7 +41,7 @@ export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
32
41
  const keep = new Array(lines.length).fill(false);
33
42
  const context = mode === 'aggressive' ? 1 : 2;
34
43
  for (let i = 0; i < lines.length; i++) {
35
- if (keepLine(lines[i], mode)) {
44
+ if (keepLine(lines[i])) {
36
45
  const s = Math.max(0, i - context);
37
46
  const e = Math.min(lines.length - 1, i + context);
38
47
  for (let j = s; j <= e; j++) keep[j] = true;
@@ -50,8 +59,9 @@ export function compressLog(text, { mode = 'balanced', maxLines = 400 } = {}) {
50
59
  let out = [];
51
60
  for (let i = 0; i < lines.length; i++) if (keep[i]) {
52
61
  const l = lines[i];
62
+ const clean = cleanAnsi(l);
53
63
  // aggressive drops more noise even inside window
54
- if (mode === 'aggressive' && (PASS_RE.test(l) || NOISE_RE.test(l)) && !KEEP_RE.test(l) && !STACK_RE.test(l)) continue;
64
+ if (mode === 'aggressive' && (PASS_RE.test(clean) || NOISE_RE.test(clean)) && !KEEP_RE.test(clean) && !STACK_RE.test(clean)) continue;
55
65
  out.push(l);
56
66
  }
57
67
 
package/lib/index.js CHANGED
@@ -67,28 +67,29 @@ export function apply(ctx, config) {
67
67
 
68
68
  ctx.tools.register({
69
69
  name: 'context_lens_compress_log',
70
- description: 'Compress a large log/test output block, keeping failures and stacktraces (Jest/Pytest/Go/nnpm)',
70
+ description: 'Compress a large log/test output block, keeping failures and stacktraces (Jest/Pytest/Go/npm)',
71
71
  parameters: {
72
72
  type: 'object',
73
73
  properties: {
74
74
  text: { type: 'string', description: 'Raw log text to compress' },
75
+ log: { type: 'string', description: 'Alias for text' },
75
76
  mode: { type: 'string', enum: ['raw', 'balanced', 'aggressive'], description: 'Compression aggressiveness' },
76
77
  maxLines: { type: 'number', description: 'Max output lines' },
77
78
  auto: { type: 'boolean', description: 'Auto-compress if large (uses threshold)' }
78
- },
79
- required: ['text']
79
+ }
80
80
  },
81
81
  output: { schema: OUTPUT_SCHEMA, render: renderOutput },
82
82
  execute: async (params) => {
83
83
  const cfg = getConfig();
84
+ const inputText = params.text || params.log || '';
84
85
  const mode = params.mode || cfg.compressionMode || 'balanced';
85
86
  const maxLines = params.maxLines || 400;
86
87
  // Auto-compress check (#15)
87
88
  const threshold = cfg.autoCompressThreshold ?? 4000;
88
- const useAuto = params.auto !== false && shouldAutoCompress(params.text, threshold);
89
- const effectiveMode = useAuto ? mode : mode;
90
- const res = compressLog(params.text, { mode: effectiveMode, maxLines });
91
- maybeTrack(params.text, res.compressed);
89
+ const useAuto = params.auto !== false && shouldAutoCompress(inputText, threshold);
90
+ const effectiveMode = (useAuto && !params.mode) ? 'balanced' : mode;
91
+ const res = compressLog(inputText, { mode: effectiveMode, maxLines });
92
+ maybeTrack(inputText, res.compressed);
92
93
  return { success: true, mode: effectiveMode, autoCompressed: useAuto, ...res };
93
94
  }
94
95
  });
@@ -128,9 +129,9 @@ export function apply(ctx, config) {
128
129
  parameters: { type: 'object', properties: {} },
129
130
  output: { schema: OUTPUT_SCHEMA, render: renderOutput },
130
131
  execute: async () => {
131
- const stats = tracker.getStats();
132
- const history = tracker.getHistory();
133
132
  const cfg = getConfig();
133
+ const stats = tracker.getStats(cfg.budgetLimit);
134
+ const history = tracker.getHistory();
134
135
  return { success: true, ...stats, history, trackingEnabled: cfg.tokenSavingsTracking !== false, autoCollapse: cfg.autoCollapse !== false, focus: focusState, autoCompressThreshold: cfg.autoCompressThreshold };
135
136
  }
136
137
  });
@@ -142,8 +143,9 @@ export function apply(ctx, config) {
142
143
  kind: 'exact',
143
144
  path: '/dsh-context-lens/status',
144
145
  handler: (req, res) => {
146
+ const cfg = getConfig();
145
147
  res.setHeader('content-type', 'application/json');
146
- res.end(JSON.stringify({ ok: true, plugin: 'dsh-context-lens', stats: tracker.getStats(), history: tracker.getHistory(), focus: focusState }));
148
+ res.end(JSON.stringify({ ok: true, plugin: 'dsh-context-lens', stats: tracker.getStats(cfg.budgetLimit), history: tracker.getHistory(), focus: focusState }));
147
149
  }
148
150
  }), 'dsh-context-lens status route');
149
151
  }
@@ -4,12 +4,18 @@ let totalCompressed = 0;
4
4
  let calls = 0;
5
5
  const history = []; // last 10
6
6
  const MAX_HISTORY = 10;
7
- const BUDGET_LIMIT = 100000; // tokens, ~100k default budget
7
+ let defaultBudgetLimit = 100000; // tokens, ~100k default budget
8
8
 
9
9
  export function estimateTokens(text) {
10
10
  return Math.ceil((text || '').length / 4);
11
11
  }
12
12
 
13
+ export function setBudgetLimit(limit) {
14
+ if (typeof limit === 'number' && limit > 0) {
15
+ defaultBudgetLimit = limit;
16
+ }
17
+ }
18
+
13
19
  export function record(originalText, compressedText) {
14
20
  const o = estimateTokens(originalText);
15
21
  const c = estimateTokens(compressedText);
@@ -31,14 +37,28 @@ export function record(originalText, compressedText) {
31
37
  return { originalTokens: o, compressedTokens: c, savedTokens: saved };
32
38
  }
33
39
 
34
- export function getStats() {
40
+ export function getStats(budgetLimitOverride) {
41
+ const limit = (typeof budgetLimitOverride === 'number' && budgetLimitOverride > 0)
42
+ ? budgetLimitOverride
43
+ : defaultBudgetLimit;
35
44
  const saved = Math.max(0, totalOriginal - totalCompressed);
36
45
  const pct = totalOriginal ? Math.round((1 - totalCompressed / totalOriginal) * 100) : 0;
37
46
  const budgetUsed = totalCompressed;
38
- const budgetRemaining = Math.max(0, BUDGET_LIMIT - budgetUsed);
39
- const budgetPercent = Math.min(100, Math.round((budgetUsed / BUDGET_LIMIT) * 100));
47
+ const budgetRemaining = Math.max(0, limit - budgetUsed);
48
+ const budgetPercent = Math.min(100, Math.round((budgetUsed / limit) * 100));
40
49
  const lowBudget = budgetPercent > 90;
41
- return { totalOriginal, totalCompressed, savedTokens: saved, savedPercent: pct, calls, budgetUsed, budgetRemaining, budgetPercent, lowBudget, budgetLimit: BUDGET_LIMIT };
50
+ return {
51
+ totalOriginal,
52
+ totalCompressed,
53
+ savedTokens: saved,
54
+ savedPercent: pct,
55
+ calls,
56
+ budgetUsed,
57
+ budgetRemaining,
58
+ budgetPercent,
59
+ lowBudget,
60
+ budgetLimit: limit
61
+ };
42
62
  }
43
63
 
44
64
  export function getHistory() {
@@ -54,6 +74,7 @@ export function reset() {
54
74
  totalCompressed = 0;
55
75
  calls = 0;
56
76
  history.length = 0;
77
+ defaultBudgetLimit = 100000;
57
78
  }
58
79
 
59
- export default { estimateTokens, record, getStats, getHistory, clearHistory, reset };
80
+ export default { estimateTokens, setBudgetLimit, record, getStats, getHistory, clearHistory, reset };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-context-lens",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "DSH plugin for AST context compression, test log filtering, and token budget guard",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",