@goodandready/dsh-context-lens 0.1.6 → 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
@@ -1,35 +1,141 @@
1
- # dsh-context-lens
1
+ # 📦 @goodandready/dsh-context-lens
2
2
 
3
- DSH plugin for AST context compression, test log filtering, and token budget guard for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
3
+ <div align="center">
4
4
 
5
- Compresses noisy test output (Jest/Pytest/Go/Vitest/npm) to failures + stacktraces and generates compact AST skeletons for large source files to save context tokens.
5
+ <h3>Intelligent AST Code Skeletonizer, Context Token Compressor & Log Condenser for DeepSeek Harness</h3>
6
6
 
7
- ## Tools
8
- - `context_lens_focus` — set focused files/folders (other context collapsed)
9
- - `context_lens_compress_log` — compress a log block (`text`, `mode` raw/balanced/aggressive, `maxLines`)
10
- - `context_lens_compress_code` — skeletonize source code (`code`, `language` js/ts/py/go, `maxDepth`)
11
- - `context_lens_stats` session token savings stats
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@goodandready/dsh-context-lens"><img src="https://img.shields.io/npm/v/@goodandready/dsh-context-lens.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
10
+ <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
11
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
12
+ </p>
12
13
 
13
- Status route: `GET /dsh-context-lens/status`
14
+ <!-- Showcase Catalog Button -->
15
+ <p align="center">
16
+ <a href="https://goodandready.app/"><img src="https://img.shields.io/badge/All_Author_Projects-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="All Author Projects"></a>
17
+ </p>
14
18
 
15
- ## Settings
16
- Located in **Settings → Plugins → Context Lens & Token Guard**:
17
- - `compressionMode`: `raw` | `balanced` | `aggressive` (default `balanced`)
18
- - `astSkeletonMaxDepth`: max skeleton depth (default `3`)
19
- - `tokenSavingsTracking`: track savings (default `true`)
19
+ <p align="center">
20
+ <a href="README.md"><b>🇬🇧 English</b></a>
21
+ <a href="README.ru.md"><b>🇷🇺 Русский</b></a>
22
+ <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
23
+ </p>
20
24
 
21
- Example config:
22
- ```yaml
23
- # `compressionMode` — aggressiveness, `astSkeletonMaxDepth` — skeleton depth
24
- compressionMode: balanced
25
- astSkeletonMaxDepth: 3
26
- tokenSavingsTracking: true
25
+ </div>
26
+
27
+ ---
28
+
29
+ ## ⚡ Overview
30
+
31
+ **`dsh-context-lens`** optimizes the context window and token budget of **DeepSeek Harness** agents.
32
+
33
+ Large context windows are expensive, prone to model distraction, and vulnerable to rate limits. When agents inspect multi-file codebases or run bulky test suites, thousands of tokens are wasted on boilerplate function bodies, passing test logs, and build artifacts.
34
+
35
+ `dsh-context-lens` introduces **active path focusing, AST structural code skeletonization (JS/TS/Python/Go), and fast O(n) heuristic log compression**, shrinking context consumption by **up to 85%** while keeping 100% of essential architectural interfaces and failure traces.
36
+
37
+ ```mermaid
38
+ graph LR
39
+ subgraph RawContext [Bulky Workspace & Terminal Streams]
40
+ Code[📁 Multi-File Codebase: Full Function Bodies] --> LensEngine[dsh-context-lens Compression Engine]
41
+ Logs[📋 Test & Build Logs: Thousands of Noise Lines] --> LensEngine
42
+ end
43
+
44
+ subgraph LensEngine [Context Lens Processing Pipelines]
45
+ LensEngine --> Focus{Active Focus Check}
46
+ Focus -->|Focused Target| RawKeep[Full Implementation Preserved]
47
+ Focus -->|Surrounding Workspace| AST[AST Skeletonizer: Types, Classes, Signatures]
48
+ LensEngine --> LogFilter[Heuristic Log Condenser: Stack Traces & Errors]
49
+ end
50
+
51
+ subgraph Savings [Token Economy & Agent Reasoning]
52
+ AST --> Agent[🤖 DSH Agent: Ultra-Compact High-Speed Context]
53
+ RawKeep --> Agent
54
+ LogFilter --> Agent
55
+ Agent --> Tracker[📊 Live Token Budget Savings Tracker]
56
+ end
57
+
58
+ style RawContext fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
59
+ style LensEngine fill:#181825,stroke:#cba6f7,stroke-width:2px,color:#cdd6f4
60
+ style Savings fill:#11111b,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
27
61
  ```
28
62
 
29
- ## Verification
63
+ ---
64
+
65
+ ## ✨ Key Capabilities & Modules
66
+
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, 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;
71
+ * Drops internal function implementations, loops, and repetitive boilerplate while preserving indentation and export declarations;
72
+ * Allows the agent to understand entire multi-package repository architectures without loading tens of thousands of implementation tokens.
73
+
74
+ ### 2. 🗜️ Fast Heuristic Log Condenser (`lib/compression/log-compressor.js`)
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;
77
+ * Automatically filters out passing test noise (`PASS`, `✓`, `ok`) and build notices;
78
+ * Retains critical error lines, stack traces, assertion failures (`Expected ... Received ...`), and failure context windows;
79
+ * 3 Aggressiveness Modes: `raw`, `balanced`, and `aggressive`.
80
+
81
+ ### 3. 🎯 Active Path Focus Scoping (`context_lens_focus`)
82
+ * Dynamically sets a list of active files or directories currently being edited;
83
+ * Files outside the focus list are automatically presented to the agent as lightweight AST skeletons.
84
+
85
+ ### 4. 📊 Token Savings Tracking & Dashboard (`lib/tokens/tracker.js` & `lib/client.js`)
86
+ * Measures exact token counts before and after compression;
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.
89
+
90
+ ---
91
+
92
+ ## 🛠️ Agent Tools Reference (4 Tools)
93
+
94
+ | Tool Name | Parameters | Description |
95
+ |---|---|---|
96
+ | `context_lens_focus` | `paths: string[]`, `maxDepth?: number` | Designates active focus files/folders; collapses surrounding workspace into AST skeletons |
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 |
100
+
101
+ ---
102
+
103
+ ## 📦 Quick Installation
104
+
30
105
  ```bash
31
- npm test
106
+ dsh plugin --profile web add @goodandready/dsh-context-lens
107
+ ```
108
+
109
+ > [!IMPORTANT]
110
+ > Restart DSH Web UI after installation (`systemctl --user restart dsh-web`) to activate context compression tools.
111
+
112
+ ---
113
+
114
+ ## ⚙️ Configuration Reference (`settings.yaml`)
115
+
116
+ ```yaml
117
+ dsh-context-lens:
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
32
124
  ```
33
125
 
34
- ## License
35
- MIT
126
+ ---
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
+
139
+ ## 📄 License
140
+
141
+ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
package/README.ru.md ADDED
@@ -0,0 +1,138 @@
1
+ # 📦 @goodandready/dsh-context-lens
2
+
3
+ <div align="center">
4
+
5
+ <h3>Интеллектуальный AST-скелетонизатор кода, оптимизатор контекста и компрессор логов для DeepSeek Harness</h3>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@goodandready/dsh-context-lens"><img src="https://img.shields.io/npm/v/@goodandready/dsh-context-lens.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
10
+ <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
11
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
12
+ </p>
13
+
14
+ <!-- Обязательная кнопка перехода на витрину всех проектов -->
15
+ <p align="center">
16
+ <a href="https://goodandready.app/"><img src="https://img.shields.io/badge/Все_проекты_автора-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="Все проекты автора"></a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="README.md"><b>🇬🇧 English</b></a> •
21
+ <a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
22
+ <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
23
+ </p>
24
+
25
+ </div>
26
+
27
+ ---
28
+
29
+ ## ⚡ Обзор
30
+
31
+ **`dsh-context-lens`** оптимизирует контекстное окно и бюджет токенов агентов **DeepSeek Harness**.
32
+
33
+ Большие объёмы контекста приводят к высоким затратам токенов, замедлению генерации и быстрой перегрузке лимитов. При изучении крупных репозиториев или прогоне тестов тысячи токенов тратятся на однотипные тела функций, логи успешных тестов и сборку.
34
+
35
+ Плагин внедряет **фокусировку на активных файлах, AST-скелетонизацию исходного кода (JS/TS/Python/Go) и сверхбыструю эвристическую компрессию логов**, снижая расход контекста **до 85%** при сохранении 100% сигнатур типов, интерфейсов и сообщений об ошибках.
36
+
37
+ ```mermaid
38
+ graph LR
39
+ subgraph RawContext [Исходные файлы и терминал]
40
+ Code[📁 Файлы проекта: Полные тела функций] --> LensEngine[Ядро сжатия dsh-context-lens]
41
+ Logs[📋 Логи тестов: Тысячи строк шума] --> LensEngine
42
+ end
43
+
44
+ subgraph LensEngine [Обработка контекста]
45
+ LensEngine --> Focus{Проверка фокуса}
46
+ Focus -->|Фокусный файл| RawKeep[Полный исходный код]
47
+ Focus -->|Остальной проект| AST[AST-скелетонизатор: сигнатуры, классы, типы]
48
+ LensEngine --> LogFilter[Компрессор логов: стек-трейсы и ошибки]
49
+ end
50
+
51
+ subgraph Savings [Экономия токенов]
52
+ AST --> Agent[🤖 Агент DSH: Компактный и быстрый контекст]
53
+ RawKeep --> Agent
54
+ LogFilter --> Agent
55
+ Agent --> Tracker[📊 Счётчик сэкономленных токенов]
56
+ end
57
+
58
+ style RawContext fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
59
+ style LensEngine fill:#181825,stroke:#cba6f7,stroke-width:2px,color:#cdd6f4
60
+ style Savings fill:#11111b,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
61
+ ```
62
+
63
+ ---
64
+
65
+ ## ✨ Ключевые возможности
66
+
67
+ ### 1. 🧬 Мультиязычный AST-скелетонизатор кода (`lib/ast/skeletonizer.js`)
68
+ * Извлекает структурные интерфейсы, сигнатуры функций, методы классов, типы и экспорты для **TypeScript, JavaScript, Python, Go, Rust и Java**;
69
+ * Поддерживает `pub async fn`, `async fn`, `pub(crate)` и `pub(super)` в Rust;
70
+ * Сохраняет JSDoc, docstrings и структурные комментарии над определениями;
71
+ * Удаляет внутренние тела функций и комментарии реализации, оставляя точную архитектуру файла;
72
+ * Позволяет агенту обозревать всю структуру проекта без загрузки лишних десятков тысяч токенов.
73
+
74
+ ### 2. 🗜️ Быстрый компрессор логов (`lib/compression/log-compressor.js`)
75
+ * Высокопроизводительный $O(n)$ фильтр шума логов тестирования и сборки (Jest, Vitest, Pytest, Go test, NPM, Webpack, Cargo, Maven/Gradle);
76
+ * Автоматическая очистка терминальных ANSI-эскейп последовательностей;
77
+ * Вырезает успешные проверки (`PASS`, `✓`, `ok`) и служебные уведомления;
78
+ * Сохраняет строки падений, стек-трейсы, расхождения в утверждениях (`Expected ... Received ...`) и контекстное окружение ошибки;
79
+ * 3 Режима сжатия: `raw`, `balanced`, `aggressive`.
80
+
81
+ ### 3. 🎯 Фокусировка на активных путях (`context_lens_focus`)
82
+ * Динамическое назначение рабочих файлов/директорий;
83
+ * Все внешние файлы проекта автоматически сворачиваются в легкие структурные скелеты.
84
+
85
+ ### 4. 📊 Трекер экономии токенов (`lib/tokens/tracker.js` & `lib/client.js`)
86
+ * Фиксация точного числа токенов до и после сжатия;
87
+ * Подсчёт накопленной экономии за сессию с отображением процента эффективности в Web UI;
88
+ * Контроль сессионного бюджета токенов (Token Budget Guard) с предупреждением при превышении 90%.
89
+
90
+ ---
91
+
92
+ ## 🛠️ Инструменты агента (4 инструмента)
93
+
94
+ | Имя инструмента | Параметры | Описание |
95
+ |---|---|---|
96
+ | `context_lens_focus` | `paths: string[]`, `maxDepth?: number` | Задаёт пути активного фокуса; всё остальное сворачивается в AST-скелеты |
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` | *(нет)* | Возвращает метрики сэкономленных токенов, историю и статус бюджета |
100
+
101
+ ---
102
+
103
+ ## 📦 Быстрая установка
104
+
105
+ ```bash
106
+ dsh plugin --profile web add @goodandready/dsh-context-lens
107
+ ```
108
+
109
+ ---
110
+
111
+ ## ⚙️ Пример конфигурации (`settings.yaml`)
112
+
113
+ ```yaml
114
+ dsh-context-lens:
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 при исчерпании бюджета
121
+ ```
122
+
123
+ ---
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
+
136
+ ## 📄 Лицензия
137
+
138
+ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
package/README.zh.md ADDED
@@ -0,0 +1,73 @@
1
+ # 📦 @goodandready/dsh-context-lens
2
+
3
+ <div align="center">
4
+
5
+ <h3>DeepSeek Harness 智能 AST 代码骨架提取器、上下文 Token 压缩与日志精简插件</h3>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@goodandready/dsh-context-lens"><img src="https://img.shields.io/npm/v/@goodandready/dsh-context-lens.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
10
+ <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
11
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
12
+ </p>
13
+
14
+ <!-- 官方展示中心跳转按钮 -->
15
+ <p align="center">
16
+ <a href="https://goodandready.app/"><img src="https://img.shields.io/badge/作者全部项目-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="作者全部项目"></a>
17
+ </p>
18
+
19
+ <p align="center">
20
+ <a href="README.md"><b>🇬🇧 English</b></a> •
21
+ <a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
22
+ <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
23
+ </p>
24
+
25
+ </div>
26
+
27
+ ---
28
+
29
+ ## ⚡ 插件概览
30
+
31
+ **`dsh-context-lens`** 为 **DeepSeek Harness** 智能体提供深度上下文窗口与 Token 预算优化。
32
+
33
+ 超长上下文不仅消耗高昂 Token 成本,还会导致模型注意力涣散并频繁触发限流。本插件通过**工作区文件焦点聚焦、跨语言 AST 结构骨架提取(支持 JS/TS/Python/Go)以及 $O(n)$ 终端测试日志启发式精简**,在保留 100% 架构接口与错误堆栈的前提下,将上下文体积削减**高达 85%**。
34
+
35
+ ```mermaid
36
+ graph LR
37
+ subgraph RawContext [原始工作区与终端输出流]
38
+ Code[📁 多文件源码: 包含冗长实现细节] --> LensEngine[dsh-context-lens 压缩引擎]
39
+ Logs[📋 测试与构建日志: 包含海量通过噪音] --> LensEngine
40
+ end
41
+
42
+ subgraph LensEngine [上下文加工管线]
43
+ LensEngine --> Focus{焦点状态研判}
44
+ Focus -->|当前聚焦文件| RawKeep[保留完整源码细节]
45
+ Focus -->|非聚焦代码库| AST[AST 骨架提取: 类型、类名、方法签名]
46
+ LensEngine --> LogFilter[日志精简器: 过滤噪音,保留报错堆栈]
47
+ end
48
+
49
+ subgraph Savings [Token 收益与推理加速]
50
+ AST --> Agent[🤖 DSH 智能体: 极速紧凑的高价值上下文]
51
+ RawKeep --> Agent
52
+ LogFilter --> Agent
53
+ Agent --> Tracker[📊 实时 Token 节省率监控]
54
+ end
55
+
56
+ style RawContext fill:#1e1e2e,stroke:#89b4fa,stroke-width:2px,color:#cdd6f4
57
+ style LensEngine fill:#181825,stroke:#cba6f7,stroke-width:2px,color:#cdd6f4
58
+ style Savings fill:#11111b,stroke:#a6e3a1,stroke-width:2px,color:#cdd6f4
59
+ ```
60
+
61
+ ---
62
+
63
+ ## 📦 安装指南
64
+
65
+ ```bash
66
+ dsh plugin --profile web add @goodandready/dsh-context-lens
67
+ ```
68
+
69
+ ---
70
+
71
+ ## 📄 开源协议
72
+
73
+ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
@@ -1,31 +1,69 @@
1
- // ponytail: regex skeletons, no tree-sitter — handles JS/TS/Python/Go signatures
1
+ // ponytail: regex skeletons, no tree-sitter — handles JS/TS/Python/Go/Rust/Java signatures + imports/comments
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 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*(\/\/.*|\/\*.*\*\/|\/\*.*|\*.*|#.*)/;
5
8
 
6
9
  function indentDepth(line) {
7
10
  const m = line.match(/^(\s*)/);
8
11
  return m ? Math.floor(m[1].replace(/\t/g, ' ').length / 2) : 0;
9
12
  }
10
13
 
11
- export function skeletonize(text, { maxDepth = 3, language } = {}) {
14
+ export function skeletonize(text, { maxDepth = 3, language, includeImports = true, includeComments = true } = {}) {
12
15
  if (!text || typeof text !== 'string') return '';
13
16
  const lines = text.split(/\r?\n/);
14
17
  const out = [];
15
18
  let seen = new Set();
16
- for (const raw of lines) {
19
+ let pendingComment = null;
20
+ for (let idx = 0; idx < lines.length; idx++) {
21
+ const raw = lines[idx];
17
22
  const line = raw.trimEnd();
18
- if (!line.trim()) continue;
23
+ if (!line.trim()) { pendingComment = null; continue; }
19
24
  const d = indentDepth(raw);
20
- if (d > maxDepth) continue;
25
+ if (d > maxDepth) { pendingComment = null; continue; }
26
+ // Handle imports
27
+ if (includeImports && IMPORT_RE.test(line)) {
28
+ const sig = line.trim().slice(0, 120);
29
+ if (!seen.has(sig)) {
30
+ seen.add(sig);
31
+ out.push(' '.repeat(Math.min(d, maxDepth)) + sig);
32
+ }
33
+ pendingComment = null;
34
+ continue;
35
+ }
36
+ // Handle comments (keep JSDoc or line comments directly above a definition)
37
+ if (includeComments && COMMENT_RE.test(line)) {
38
+ // Keep comment if next non-empty line is a definition
39
+ let nextIdx = idx + 1;
40
+ while (nextIdx < lines.length && !lines[nextIdx].trim()) nextIdx++;
41
+ if (nextIdx < lines.length) {
42
+ const nextLine = lines[nextIdx];
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)) {
44
+ pendingComment = line.trim().slice(0, 120);
45
+ continue;
46
+ }
47
+ }
48
+ // Otherwise, keep top-level comments (up to 5)
49
+ if (out.length < 5 && (line.trim().startsWith('//') || line.trim().startsWith('#') || line.trim().startsWith('/*'))) {
50
+ const sig = line.trim().slice(0, 120);
51
+ if (!seen.has(sig)) {
52
+ seen.add(sig);
53
+ out.push(' '.repeat(Math.min(d, maxDepth)) + sig);
54
+ }
55
+ }
56
+ pendingComment = null;
57
+ continue;
58
+ }
21
59
  let sig = null;
22
60
  // try JS/TS
23
61
  if (!language || language === 'js' || language === 'ts') {
24
- 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*[:{])/);
25
- if (m) sig = line.trim().replace(/\s*\{.*$/, '').replace(/\s+$/, '') + (line.trim().endsWith('{') ? '' : '');
26
- // fallback simple
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+.*)/);
63
+ if (m) sig = line.trim().replace(/\s*\{.*$/, '').replace(/\s+$/, '');
27
64
  if (!sig && /^\s*(export\s+)?(async\s+)?function\s+\w+/.test(line)) sig = line.trim();
28
65
  if (!sig && /^\s*class\s+\w+/.test(line)) sig = line.trim();
66
+ if (!sig && /^\s*import\s+/.test(line)) sig = line.trim().slice(0, 120);
29
67
  }
30
68
  if (!sig && (!language || language === 'py' || language === 'python')) {
31
69
  const m = line.match(PY_RE);
@@ -35,11 +73,26 @@ export function skeletonize(text, { maxDepth = 3, language } = {}) {
35
73
  const m = line.match(GO_RE);
36
74
  if (m) sig = line.trim();
37
75
  }
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();
81
+ }
38
82
  // generic fallback: if no specific language, try all
39
83
  if (!sig && !language) {
40
- 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();
41
85
  }
42
86
  if (sig) {
87
+ // If we have a pending comment, prepend it
88
+ if (pendingComment) {
89
+ const commentSig = pendingComment;
90
+ if (!seen.has(commentSig)) {
91
+ seen.add(commentSig);
92
+ out.push(' '.repeat(Math.min(d, maxDepth)) + commentSig);
93
+ }
94
+ pendingComment = null;
95
+ }
43
96
  // normalize: trim trailing { : etc, keep signature short
44
97
  sig = sig.replace(/\s*\{\s*$/, '').replace(/:\s*$/, '').trim();
45
98
  if (sig.length > 120) sig = sig.slice(0, 117) + '...';
@@ -48,6 +101,8 @@ export function skeletonize(text, { maxDepth = 3, language } = {}) {
48
101
  seen.add(key);
49
102
  out.push(' '.repeat(Math.min(d, maxDepth)) + sig);
50
103
  }
104
+ } else {
105
+ pendingComment = null;
51
106
  }
52
107
  }
53
108
  // if nothing found, fallback to first N non-empty lines truncated
@@ -55,6 +110,13 @@ export function skeletonize(text, { maxDepth = 3, language } = {}) {
55
110
  const fallback = lines.filter((l) => l.trim()).slice(0, Math.min(20, maxDepth * 6)).map((l) => l.trim().slice(0, 120));
56
111
  return fallback.join('\n');
57
112
  }
113
+ // Add highlight comment for what was removed
114
+ const total = lines.filter(l => l.trim()).length;
115
+ const removed = total - out.length;
116
+ if (removed > 0 && out.length > 0) {
117
+ const commentPrefix = (language === 'py' || language === 'python') ? '# ' : '// ';
118
+ out.push(`${commentPrefix}... ${removed} lines collapsed (skeleton, maxDepth=${maxDepth})`);
119
+ }
58
120
  return out.join('\n');
59
121
  }
60
122
 
package/lib/client.js CHANGED
@@ -17,7 +17,10 @@ window.__ModuleLoader__.load({
17
17
  tokens: 'tokens',
18
18
  placeholder: 'Paste Jest/Pytest log here…',
19
19
  saving: 'Saving…',
20
- ready: 'Ready'
20
+ ready: 'Ready',
21
+ budget: 'Budget',
22
+ history: 'Recent ops',
23
+ lowBudget: 'Budget nearly exhausted'
21
24
  };
22
25
  const ru = {
23
26
  title: 'Context Lens & Token Guard',
@@ -31,7 +34,10 @@ window.__ModuleLoader__.load({
31
34
  tokens: 'токенов',
32
35
  placeholder: 'Вставьте лог Jest/Pytest…',
33
36
  saving: 'Сохранение…',
34
- ready: 'Готово'
37
+ ready: 'Готово',
38
+ budget: 'Бюджет',
39
+ history: 'Последние операции',
40
+ lowBudget: 'Бюджет почти исчерпан'
35
41
  };
36
42
 
37
43
  // ponytail: client-side replica of compressor for preview (no import)
@@ -50,15 +56,27 @@ window.__ModuleLoader__.load({
50
56
 
51
57
  function LensTab({ ctx: _ctx, scope }) {
52
58
  const [stats, setStats] = React.useState(null);
59
+ const [history, setHistory] = React.useState([]);
53
60
  React.useEffect(() => {
54
- fetch('/dsh-context-lens/status').then(r => r.ok ? r.json() : null).then(j => { if (j && j.stats) setStats(j.stats); }).catch(() => {});
61
+ fetch('/dsh-context-lens/status').then(r => r.ok ? r.json() : null).then(j => { if (j && j.stats) { setStats(j.stats); setHistory(j.history || []); } }).catch(() => {});
55
62
  }, []);
63
+ const refresh = () => fetch('/dsh-context-lens/status').then(r=>r.json()).then(j=>{ if (j && j.stats) { setStats(j.stats); setHistory(j.history || []); } }).catch(()=>{});
64
+ const barColor = stats && stats.lowBudget ? '#d73a4a' : 'var(--dsw-alias-label-primary)';
56
65
  return React.createElement('div', { style: { padding: 12 } },
57
66
  React.createElement('div', { style: { fontSize: 13, fontWeight: 600, marginBottom: 8, color: 'var(--dsw-alias-label-primary)' } }, 'Context Lens'),
58
67
  stats ? React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, `Saved ${stats.savedTokens} tokens (${stats.savedPercent}%) · ${stats.calls} ops`) : React.createElement('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary)' } }, 'No data yet'),
68
+ stats && stats.budgetLimit ? React.createElement('div', { style: { marginTop: 8 } },
69
+ React.createElement('div', { style: { fontSize: 11, color: stats.lowBudget ? '#d73a4a' : 'var(--dsw-alias-label-secondary)', marginBottom: 4 } }, `Budget ${stats.budgetUsed}/${stats.budgetLimit} tokens (${stats.budgetPercent}%)${stats.lowBudget ? ' ⚠' : ''}`),
70
+ React.createElement('div', { style: { height: 6, borderRadius: 3, background: 'var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-3))', overflow: 'hidden' } },
71
+ React.createElement('div', { style: { height: '100%', width: Math.min(100, stats.budgetPercent) + '%', background: barColor, transition: 'width .2s' } })
72
+ )
73
+ ) : null,
74
+ history.length ? React.createElement('div', { style: { marginTop: 10 } },
75
+ React.createElement('div', { style: { fontSize: 11, color: 'var(--dsw-alias-label-tertiary)', marginBottom: 4 } }, 'Recent ops'),
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
+ ) : null,
59
78
  React.createElement('div', { style: { marginTop: 12, display: 'flex', gap: 8 } },
60
- React.createElement('button', { onClick: () => fetch('/dsh-context-lens/status').then(r=>r.json()).then(j=> setStats(j.stats)), 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'),
61
- 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')
62
80
  )
63
81
  );
64
82
  }
@@ -72,13 +90,14 @@ window.__ModuleLoader__.load({
72
90
  return () => clearInterval(id);
73
91
  }, []);
74
92
  if (!stats || !stats.savedTokens) return null;
75
- return React.createElement('span', { style: { fontSize: 11, padding: '2px 6px', borderRadius: 999, background: 'var(--dsw-alias-bg-layer-2)', color: 'var(--dsw-alias-label-secondary)', border: '1px solid var(--dsw-alias-border-l2)', marginLeft: 8 } }, `Lens ${stats.savedPercent}%`);
93
+ const warn = !!stats.lowBudget;
94
+ return React.createElement('span', { style: { fontSize: 11, padding: '2px 6px', borderRadius: 999, background: 'var(--dsw-alias-bg-layer-2)', color: warn ? '#d73a4a' : 'var(--dsw-alias-label-secondary)', border: '1px solid ' + (warn ? '#d73a4a' : 'var(--dsw-alias-border-l2)'), marginLeft: 8 } }, `Lens ${stats.savedPercent}%${warn ? ' ⚠' : ''}`);
76
95
  }
77
96
 
78
97
  function PluginCard({ ctx: _ctx, t }) {
79
98
  const [expanded, setExpanded] = React.useState(false);
80
99
  // hooks must be before any return — React 310
81
- const [draft, setDraft] = React.useState({ compressionMode: 'balanced', astSkeletonMaxDepth: 3, tokenSavingsTracking: true });
100
+ const [draft, setDraft] = React.useState({ compressionMode: 'balanced', astSkeletonMaxDepth: 3, tokenSavingsTracking: true, budgetLimit: 100000, autoCollapse: true });
82
101
  const [status, setStatus] = React.useState('loading');
83
102
  const [saving, setSaving] = React.useState(false);
84
103
  const [saveErr, setSaveErr] = React.useState('');
@@ -119,7 +138,13 @@ window.__ModuleLoader__.load({
119
138
 
120
139
  React.useEffect(() => {
121
140
  if (expanded) {
122
- fetch('/dsh-context-lens/status').then((r) => r.ok ? r.json() : null).then((j) => { if (j && j.stats) setStats(j.stats); }).catch(() => {});
141
+ fetch('/dsh-context-lens/status').then((r) => r.ok ? r.json() : null).then((j) => {
142
+ if (j && j.stats) {
143
+ setStats(j.stats);
144
+ // Budget guard (#16): auto-collapse when nearly exhausted
145
+ if (j.stats.lowBudget && draft.autoCollapse !== false) setExpanded(false);
146
+ }
147
+ }).catch(() => {});
123
148
  }
124
149
  }, [expanded]);
125
150
 
@@ -1,13 +1,17 @@
1
1
  // ponytail: heuristic line filter, not ML — O(n) scan, no deps
2
- 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)/i;
3
- const PASS_RE = /^(PASS|\s*✓|\s*✔|\s*ok\s|…+\s*$|\s*\.\s*$)/i;
4
- const NOISE_RE = /^(npm (notice|warn|info)|Browserslist|cached|Downloading|Done in)/i;
5
- const STACK_RE = /^\s*(at\s+|File ".*", line \d+|#\d+\s+0x|goroutine \d+ \[)/;
2
+ const ANSI_RE = /\u001b\[[0-9;]*[a-zA-Z]/g;
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;
4
+ const PASS_RE = /^(PASS|\s*✓|\s*✔|\s*ok\s|…+\s*$|\s*\.\s*$|test result:\s*ok|running \d+ test.*ok)/i;
5
+ const NOISE_RE = /^\s*(npm (notice|warn|info)|Browserslist|cached|Downloading|Done in|Compiling\s|cargo:.*Finished)/i;
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
@@ -10,7 +10,9 @@ export const Config = z.object({
10
10
  compressionMode: z.string().default('balanced').description('Log compression aggressiveness (raw/balanced/aggressive)'),
11
11
  astSkeletonMaxDepth: z.number().default(3).description('Max depth for AST skeleton generation'),
12
12
  tokenSavingsTracking: z.boolean().default(true).description('Track and display token budget savings'),
13
- autoCompressThreshold: z.number().default(4000).description('Auto-compress threshold in chars (0 to disable)')
13
+ autoCompressThreshold: z.number().default(4000).description('Auto-compress threshold in chars (0 to disable)'),
14
+ budgetLimit: z.number().default(100000).description('Session token budget (compressions stop counting after this)'),
15
+ autoCollapse: z.boolean().default(true).description('Auto-collapse UI when budget is nearly exhausted')
14
16
  });
15
17
 
16
18
  const NS = '@goodandready/dsh-context-lens';
@@ -65,40 +67,41 @@ export function apply(ctx, config) {
65
67
 
66
68
  ctx.tools.register({
67
69
  name: 'context_lens_compress_log',
68
- 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)',
69
71
  parameters: {
70
72
  type: 'object',
71
73
  properties: {
72
74
  text: { type: 'string', description: 'Raw log text to compress' },
75
+ log: { type: 'string', description: 'Alias for text' },
73
76
  mode: { type: 'string', enum: ['raw', 'balanced', 'aggressive'], description: 'Compression aggressiveness' },
74
77
  maxLines: { type: 'number', description: 'Max output lines' },
75
78
  auto: { type: 'boolean', description: 'Auto-compress if large (uses threshold)' }
76
- },
77
- required: ['text']
79
+ }
78
80
  },
79
81
  output: { schema: OUTPUT_SCHEMA, render: renderOutput },
80
82
  execute: async (params) => {
81
83
  const cfg = getConfig();
84
+ const inputText = params.text || params.log || '';
82
85
  const mode = params.mode || cfg.compressionMode || 'balanced';
83
86
  const maxLines = params.maxLines || 400;
84
87
  // Auto-compress check (#15)
85
88
  const threshold = cfg.autoCompressThreshold ?? 4000;
86
- const useAuto = params.auto !== false && shouldAutoCompress(params.text, threshold);
87
- const effectiveMode = useAuto ? mode : mode;
88
- const res = compressLog(params.text, { mode: effectiveMode, maxLines });
89
- 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);
90
93
  return { success: true, mode: effectiveMode, autoCompressed: useAuto, ...res };
91
94
  }
92
95
  });
93
96
 
94
97
  ctx.tools.register({
95
98
  name: 'context_lens_compress_code',
96
- description: 'Generate AST skeleton for a large source file (JS/TS/Python/Go) to save context',
99
+ description: 'Generate AST skeleton for a large source file (JS/TS/Python/Go/Rust/Java) to save context',
97
100
  parameters: {
98
101
  type: 'object',
99
102
  properties: {
100
103
  code: { type: 'string', description: 'Source code to skeletonize' },
101
- language: { type: 'string', enum: ['js', 'ts', 'py', 'python', 'go'], description: 'Language hint' },
104
+ language: { type: 'string', enum: ['js', 'ts', 'py', 'python', 'go', 'rust', 'java'], description: 'Language hint' },
102
105
  maxDepth: { type: 'number', description: 'Max depth' },
103
106
  filePath: { type: 'string', description: 'File path to check focus (if in focus, returns full code)' }
104
107
  },
@@ -126,9 +129,10 @@ export function apply(ctx, config) {
126
129
  parameters: { type: 'object', properties: {} },
127
130
  output: { schema: OUTPUT_SCHEMA, render: renderOutput },
128
131
  execute: async () => {
129
- const stats = tracker.getStats();
130
132
  const cfg = getConfig();
131
- return { success: true, ...stats, trackingEnabled: cfg.tokenSavingsTracking !== false, focus: focusState, autoCompressThreshold: cfg.autoCompressThreshold };
133
+ const stats = tracker.getStats(cfg.budgetLimit);
134
+ const history = tracker.getHistory();
135
+ return { success: true, ...stats, history, trackingEnabled: cfg.tokenSavingsTracking !== false, autoCollapse: cfg.autoCollapse !== false, focus: focusState, autoCompressThreshold: cfg.autoCompressThreshold };
132
136
  }
133
137
  });
134
138
  }
@@ -139,8 +143,9 @@ export function apply(ctx, config) {
139
143
  kind: 'exact',
140
144
  path: '/dsh-context-lens/status',
141
145
  handler: (req, res) => {
146
+ const cfg = getConfig();
142
147
  res.setHeader('content-type', 'application/json');
143
- res.end(JSON.stringify({ ok: true, plugin: 'dsh-context-lens', stats: tracker.getStats(), focus: focusState }));
148
+ res.end(JSON.stringify({ ok: true, plugin: 'dsh-context-lens', stats: tracker.getStats(cfg.budgetLimit), history: tracker.getHistory(), focus: focusState }));
144
149
  }
145
150
  }), 'dsh-context-lens status route');
146
151
  }
@@ -1,31 +1,80 @@
1
- // ponytail: in-memory per-process tracker, no DB
1
+ // ponytail: in-memory per-process tracker + history, no DB
2
2
  let totalOriginal = 0;
3
3
  let totalCompressed = 0;
4
4
  let calls = 0;
5
+ const history = []; // last 10
6
+ const MAX_HISTORY = 10;
7
+ let defaultBudgetLimit = 100000; // tokens, ~100k default budget
5
8
 
6
9
  export function estimateTokens(text) {
7
10
  return Math.ceil((text || '').length / 4);
8
11
  }
9
12
 
13
+ export function setBudgetLimit(limit) {
14
+ if (typeof limit === 'number' && limit > 0) {
15
+ defaultBudgetLimit = limit;
16
+ }
17
+ }
18
+
10
19
  export function record(originalText, compressedText) {
11
20
  const o = estimateTokens(originalText);
12
21
  const c = estimateTokens(compressedText);
22
+ const saved = Math.max(0, o - c);
13
23
  totalOriginal += o;
14
24
  totalCompressed += c;
15
25
  calls++;
16
- return { originalTokens: o, compressedTokens: c, savedTokens: Math.max(0, o - c) };
26
+ const entry = {
27
+ id: Date.now() + Math.random().toString(36).slice(2, 6),
28
+ originalTokens: o,
29
+ compressedTokens: c,
30
+ savedTokens: saved,
31
+ savedPercent: o ? Math.round((1 - c / o) * 100) : 0,
32
+ timestamp: new Date().toISOString(),
33
+ preview: (compressedText || '').slice(0, 120)
34
+ };
35
+ history.unshift(entry);
36
+ if (history.length > MAX_HISTORY) history.pop();
37
+ return { originalTokens: o, compressedTokens: c, savedTokens: saved };
17
38
  }
18
39
 
19
- export function getStats() {
40
+ export function getStats(budgetLimitOverride) {
41
+ const limit = (typeof budgetLimitOverride === 'number' && budgetLimitOverride > 0)
42
+ ? budgetLimitOverride
43
+ : defaultBudgetLimit;
20
44
  const saved = Math.max(0, totalOriginal - totalCompressed);
21
45
  const pct = totalOriginal ? Math.round((1 - totalCompressed / totalOriginal) * 100) : 0;
22
- return { totalOriginal, totalCompressed, savedTokens: saved, savedPercent: pct, calls };
46
+ const budgetUsed = totalCompressed;
47
+ const budgetRemaining = Math.max(0, limit - budgetUsed);
48
+ const budgetPercent = Math.min(100, Math.round((budgetUsed / limit) * 100));
49
+ const lowBudget = budgetPercent > 90;
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
+ };
62
+ }
63
+
64
+ export function getHistory() {
65
+ return [...history];
66
+ }
67
+
68
+ export function clearHistory() {
69
+ history.length = 0;
23
70
  }
24
71
 
25
72
  export function reset() {
26
73
  totalOriginal = 0;
27
74
  totalCompressed = 0;
28
75
  calls = 0;
76
+ history.length = 0;
77
+ defaultBudgetLimit = 100000;
29
78
  }
30
79
 
31
- export default { estimateTokens, record, getStats, 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.6",
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",