@goodandready/dsh-context-lens 0.1.7 → 0.1.9
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 +27 -9
- package/README.ru.md +28 -10
- package/lib/ast/skeletonizer.js +15 -12
- package/lib/client.js +1 -2
- package/lib/compression/log-compressor.js +18 -8
- package/lib/index.js +12 -10
- package/lib/tokens/tracker.js +27 -6
- package/package.json +1 -2
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
|
|
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` | `
|
|
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,
|
|
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
|
|
115
|
-
astSkeletonMaxDepth: 3
|
|
116
|
-
tokenSavingsTracking: true
|
|
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 и
|
|
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
|
-
*
|
|
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` | `
|
|
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
|
|
112
|
-
astSkeletonMaxDepth: 3
|
|
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)
|
package/lib/ast/skeletonizer.js
CHANGED
|
@@ -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
|
|
6
|
-
const
|
|
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'
|
|
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+$/, '')
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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) =>
|
|
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]
|
|
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(
|
|
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/
|
|
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(
|
|
89
|
-
const effectiveMode = useAuto
|
|
90
|
-
const res = compressLog(
|
|
91
|
-
maybeTrack(
|
|
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
|
}
|
package/lib/tokens/tracker.js
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
39
|
-
const budgetPercent = Math.min(100, Math.round((budgetUsed /
|
|
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 {
|
|
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.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
"platform": "web",
|
|
45
45
|
"inject": [
|
|
46
46
|
"@deepseek-ai/dsh-client-locale",
|
|
47
|
-
"@deepseek-ai/dsh-client-ui-slots",
|
|
48
47
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
49
48
|
]
|
|
50
49
|
}
|