@goodandready/dsh-context-lens 0.1.6 → 0.1.7

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,123 @@
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, and Go**;
69
+ * Drops internal function implementations, loops, and repetitive boilerplate while preserving indentation and export declarations;
70
+ * Allows the agent to understand entire multi-package repository architectures without loading tens of thousands of implementation tokens.
71
+
72
+ ### 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);
74
+ * Automatically filters out passing test noise (`PASS`, `✓`, `ok`) and build notices;
75
+ * Retains critical error lines, stack traces, assertion failures (`Expected ... Received ...`), and failure context windows;
76
+ * 3 Aggressiveness Modes: `raw`, `balanced`, and `aggressive`.
77
+
78
+ ### 3. 🎯 Active Path Focus Scoping (`context_lens_focus`)
79
+ * Dynamically sets a list of active files or directories currently being edited;
80
+ * Files outside the focus list are automatically presented to the agent as lightweight AST skeletons.
81
+
82
+ ### 4. 📊 Token Savings Tracking & Dashboard (`lib/tokens/tracker.js` & `lib/client.js`)
83
+ * Measures exact token counts before and after compression;
84
+ * Calculates cumulative session token savings and displays live efficiency percentage badges in the DSH interface.
85
+
86
+ ---
87
+
88
+ ## 🛠️ Agent Tools Reference (4 Tools)
89
+
90
+ | Tool Name | Parameters | Description |
91
+ |---|---|---|
92
+ | `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 % |
96
+
97
+ ---
98
+
99
+ ## 📦 Quick Installation
100
+
30
101
  ```bash
31
- npm test
102
+ dsh plugin --profile web add @goodandready/dsh-context-lens
103
+ ```
104
+
105
+ > [!IMPORTANT]
106
+ > Restart DSH Web UI after installation (`systemctl --user restart dsh-web`) to activate context compression tools.
107
+
108
+ ---
109
+
110
+ ## ⚙️ Configuration Reference (`settings.yaml`)
111
+
112
+ ```yaml
113
+ 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
32
117
  ```
33
118
 
34
- ## License
35
- MIT
119
+ ---
120
+
121
+ ## 📄 License
122
+
123
+ MIT © [GooDAnDReaDY](https://github.com/GooDAnDReaDY)
package/README.ru.md ADDED
@@ -0,0 +1,120 @@
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**;
69
+ * Удаляет внутренние тела функций и комментарии реализации, оставляя точную архитектуру файла;
70
+ * Позволяет агенту обозревать всю структуру проекта без загрузки лишних десятков тысяч токенов.
71
+
72
+ ### 2. 🗜️ Быстрый компрессор логов (`lib/compression/log-compressor.js`)
73
+ * Высокопроизводительный $O(n)$ фильтр шума логов тестирования и сборки (Jest, Vitest, Pytest, Go test, NPM, Webpack, Cargo);
74
+ * Автоматически вырезает успешные проверки (`PASS`, `✓`, `ok`) и служебные уведомления;
75
+ * Сохраняет строки падений, стек-трейсы, расхождения в утверждениях (`Expected ... Received ...`) и контекстное окружение ошибки;
76
+ * 3 Режима сжатия: `raw`, `balanced`, `aggressive`.
77
+
78
+ ### 3. 🎯 Фокусировка на активных путях (`context_lens_focus`)
79
+ * Динамическое назначение рабочих файлов/директорий;
80
+ * Все внешние файлы проекта автоматически сворачиваются в легкие структурные скелеты.
81
+
82
+ ### 4. 📊 Трекер экономии токенов (`lib/tokens/tracker.js` & `lib/client.js`)
83
+ * Фиксация точного числа токенов до и после сжатия;
84
+ * Подсчёт накопленной экономии за сессию с отображением процента эффективности в Web UI.
85
+
86
+ ---
87
+
88
+ ## 🛠️ Инструменты агента (4 инструмента)
89
+
90
+ | Имя инструмента | Параметры | Описание |
91
+ |---|---|---|
92
+ | `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` | *(нет)* | Возвращает метрики сэкономленных токенов и процент эффективности |
96
+
97
+ ---
98
+
99
+ ## 📦 Быстрая установка
100
+
101
+ ```bash
102
+ dsh plugin --profile web add @goodandready/dsh-context-lens
103
+ ```
104
+
105
+ ---
106
+
107
+ ## ⚙️ Пример конфигурации (`settings.yaml`)
108
+
109
+ ```yaml
110
+ dsh-context-lens:
111
+ compressionMode: balanced # 'raw', 'balanced' или 'aggressive'
112
+ astSkeletonMaxDepth: 3 # Максимальная глубина обхода AST-сигнатур
113
+ tokenSavingsTracking: true # Включить трекинг сэкономленных токенов
114
+ ```
115
+
116
+ ---
117
+
118
+ ## 📄 Лицензия
119
+
120
+ 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,68 @@
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 IMPORT_RE = /^\s*import\s+.*from\s+['"].*['"]|^\s*import\s+['"].*['"]|^\s*export\s+.*from\s+['"]/;
6
+ const COMMENT_RE = /^\s*(\/\/.*|\/\*.*\*\/|#.*)/;
5
7
 
6
8
  function indentDepth(line) {
7
9
  const m = line.match(/^(\s*)/);
8
10
  return m ? Math.floor(m[1].replace(/\t/g, ' ').length / 2) : 0;
9
11
  }
10
12
 
11
- export function skeletonize(text, { maxDepth = 3, language } = {}) {
13
+ export function skeletonize(text, { maxDepth = 3, language, includeImports = true, includeComments = true } = {}) {
12
14
  if (!text || typeof text !== 'string') return '';
13
15
  const lines = text.split(/\r?\n/);
14
16
  const out = [];
15
17
  let seen = new Set();
16
- for (const raw of lines) {
18
+ let pendingComment = null;
19
+ for (let idx = 0; idx < lines.length; idx++) {
20
+ const raw = lines[idx];
17
21
  const line = raw.trimEnd();
18
- if (!line.trim()) continue;
22
+ if (!line.trim()) { pendingComment = null; continue; }
19
23
  const d = indentDepth(raw);
20
- if (d > maxDepth) continue;
24
+ if (d > maxDepth) { pendingComment = null; continue; }
25
+ // Handle imports
26
+ if (includeImports && IMPORT_RE.test(line)) {
27
+ const sig = line.trim().slice(0, 120);
28
+ if (!seen.has(sig)) {
29
+ seen.add(sig);
30
+ out.push(' '.repeat(Math.min(d, maxDepth)) + sig);
31
+ }
32
+ pendingComment = null;
33
+ continue;
34
+ }
35
+ // Handle comments (keep JSDoc or line comments directly above a definition)
36
+ if (includeComments && COMMENT_RE.test(line)) {
37
+ // Keep comment if next non-empty line is a definition
38
+ let nextIdx = idx + 1;
39
+ while (nextIdx < lines.length && !lines[nextIdx].trim()) nextIdx++;
40
+ if (nextIdx < lines.length) {
41
+ 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
+ pendingComment = line.trim().slice(0, 120);
44
+ continue;
45
+ }
46
+ }
47
+ // Otherwise, keep top-level comments (up to 5)
48
+ if (out.length < 5 && line.trim().startsWith('//')) {
49
+ const sig = line.trim().slice(0, 120);
50
+ if (!seen.has(sig)) {
51
+ seen.add(sig);
52
+ out.push(' '.repeat(Math.min(d, maxDepth)) + sig);
53
+ }
54
+ }
55
+ pendingComment = null;
56
+ continue;
57
+ }
21
58
  let sig = null;
22
59
  // try JS/TS
23
- 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*[:{])/);
60
+ if (!language || language === 'js' || language === 'ts' || language === 'rust' || language === 'java') {
61
+ 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+.*)/);
25
62
  if (m) sig = line.trim().replace(/\s*\{.*$/, '').replace(/\s+$/, '') + (line.trim().endsWith('{') ? '' : '');
26
- // fallback simple
27
63
  if (!sig && /^\s*(export\s+)?(async\s+)?function\s+\w+/.test(line)) sig = line.trim();
28
64
  if (!sig && /^\s*class\s+\w+/.test(line)) sig = line.trim();
65
+ if (!sig && /^\s*import\s+/.test(line)) sig = line.trim().slice(0, 120);
29
66
  }
30
67
  if (!sig && (!language || language === 'py' || language === 'python')) {
31
68
  const m = line.match(PY_RE);
@@ -35,11 +72,25 @@ export function skeletonize(text, { maxDepth = 3, language } = {}) {
35
72
  const m = line.match(GO_RE);
36
73
  if (m) sig = line.trim();
37
74
  }
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();
79
+ }
38
80
  // generic fallback: if no specific language, try all
39
81
  if (!sig && !language) {
40
82
  if (JS_FUNC_RE.test(line) || PY_RE.test(line) || GO_RE.test(line)) sig = line.trim();
41
83
  }
42
84
  if (sig) {
85
+ // If we have a pending comment, prepend it
86
+ if (pendingComment) {
87
+ const commentSig = pendingComment;
88
+ if (!seen.has(commentSig)) {
89
+ seen.add(commentSig);
90
+ out.push(' '.repeat(Math.min(d, maxDepth)) + commentSig);
91
+ }
92
+ pendingComment = null;
93
+ }
43
94
  // normalize: trim trailing { : etc, keep signature short
44
95
  sig = sig.replace(/\s*\{\s*$/, '').replace(/:\s*$/, '').trim();
45
96
  if (sig.length > 120) sig = sig.slice(0, 117) + '...';
@@ -48,6 +99,8 @@ export function skeletonize(text, { maxDepth = 3, language } = {}) {
48
99
  seen.add(key);
49
100
  out.push(' '.repeat(Math.min(d, maxDepth)) + sig);
50
101
  }
102
+ } else {
103
+ pendingComment = null;
51
104
  }
52
105
  }
53
106
  // if nothing found, fallback to first N non-empty lines truncated
@@ -55,6 +108,12 @@ export function skeletonize(text, { maxDepth = 3, language } = {}) {
55
108
  const fallback = lines.filter((l) => l.trim()).slice(0, Math.min(20, maxDepth * 6)).map((l) => l.trim().slice(0, 120));
56
109
  return fallback.join('\n');
57
110
  }
111
+ // Add highlight comment for what was removed
112
+ const total = lines.filter(l => l.trim()).length;
113
+ const removed = total - out.length;
114
+ if (removed > 0 && out.length > 0) {
115
+ out.push(`// ... ${removed} lines collapsed (skeleton, maxDepth=${maxDepth})`);
116
+ }
58
117
  return out.join('\n');
59
118
  }
60
119
 
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,14 +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'),
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'),
61
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')
62
81
  )
63
82
  );
@@ -72,13 +91,14 @@ window.__ModuleLoader__.load({
72
91
  return () => clearInterval(id);
73
92
  }, []);
74
93
  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}%`);
94
+ const warn = !!stats.lowBudget;
95
+ 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
96
  }
77
97
 
78
98
  function PluginCard({ ctx: _ctx, t }) {
79
99
  const [expanded, setExpanded] = React.useState(false);
80
100
  // hooks must be before any return — React 310
81
- const [draft, setDraft] = React.useState({ compressionMode: 'balanced', astSkeletonMaxDepth: 3, tokenSavingsTracking: true });
101
+ const [draft, setDraft] = React.useState({ compressionMode: 'balanced', astSkeletonMaxDepth: 3, tokenSavingsTracking: true, budgetLimit: 100000, autoCollapse: true });
82
102
  const [status, setStatus] = React.useState('loading');
83
103
  const [saving, setSaving] = React.useState(false);
84
104
  const [saveErr, setSaveErr] = React.useState('');
@@ -119,7 +139,13 @@ window.__ModuleLoader__.load({
119
139
 
120
140
  React.useEffect(() => {
121
141
  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(() => {});
142
+ fetch('/dsh-context-lens/status').then((r) => r.ok ? r.json() : null).then((j) => {
143
+ if (j && j.stats) {
144
+ setStats(j.stats);
145
+ // Budget guard (#16): auto-collapse when nearly exhausted
146
+ if (j.stats.lowBudget && draft.autoCollapse !== false) setExpanded(false);
147
+ }
148
+ }).catch(() => {});
123
149
  }
124
150
  }, [expanded]);
125
151
 
@@ -1,8 +1,8 @@
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 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
+ const PASS_RE = /^(PASS|\s*✓|\s*✔|\s*ok\s|…+\s*$|\s*\.\s*$|test result:\s*ok|running \d+ test.*ok)/i;
4
+ const NOISE_RE = /^\s*(npm (notice|warn|info)|Browserslist|cached|Downloading|Done in|Compiling\s|cargo:.*Finished)/i;
5
+ const STACK_RE = /^\s*(at\s+|File ".*", line \d+|#\d+\s+0x|goroutine \d+ \[|thread '.*' panicked|note: run with)/;
6
6
 
7
7
  function keepLine(line, mode) {
8
8
  if (KEEP_RE.test(line) || STACK_RE.test(line)) return true;
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';
@@ -93,12 +95,12 @@ export function apply(ctx, config) {
93
95
 
94
96
  ctx.tools.register({
95
97
  name: 'context_lens_compress_code',
96
- description: 'Generate AST skeleton for a large source file (JS/TS/Python/Go) to save context',
98
+ description: 'Generate AST skeleton for a large source file (JS/TS/Python/Go/Rust/Java) to save context',
97
99
  parameters: {
98
100
  type: 'object',
99
101
  properties: {
100
102
  code: { type: 'string', description: 'Source code to skeletonize' },
101
- language: { type: 'string', enum: ['js', 'ts', 'py', 'python', 'go'], description: 'Language hint' },
103
+ language: { type: 'string', enum: ['js', 'ts', 'py', 'python', 'go', 'rust', 'java'], description: 'Language hint' },
102
104
  maxDepth: { type: 'number', description: 'Max depth' },
103
105
  filePath: { type: 'string', description: 'File path to check focus (if in focus, returns full code)' }
104
106
  },
@@ -127,8 +129,9 @@ export function apply(ctx, config) {
127
129
  output: { schema: OUTPUT_SCHEMA, render: renderOutput },
128
130
  execute: async () => {
129
131
  const stats = tracker.getStats();
132
+ const history = tracker.getHistory();
130
133
  const cfg = getConfig();
131
- return { success: true, ...stats, trackingEnabled: cfg.tokenSavingsTracking !== false, focus: focusState, autoCompressThreshold: cfg.autoCompressThreshold };
134
+ return { success: true, ...stats, history, trackingEnabled: cfg.tokenSavingsTracking !== false, autoCollapse: cfg.autoCollapse !== false, focus: focusState, autoCompressThreshold: cfg.autoCompressThreshold };
132
135
  }
133
136
  });
134
137
  }
@@ -140,7 +143,7 @@ export function apply(ctx, config) {
140
143
  path: '/dsh-context-lens/status',
141
144
  handler: (req, res) => {
142
145
  res.setHeader('content-type', 'application/json');
143
- res.end(JSON.stringify({ ok: true, plugin: 'dsh-context-lens', stats: tracker.getStats(), focus: focusState }));
146
+ res.end(JSON.stringify({ ok: true, plugin: 'dsh-context-lens', stats: tracker.getStats(), history: tracker.getHistory(), focus: focusState }));
144
147
  }
145
148
  }), 'dsh-context-lens status route');
146
149
  }
@@ -1,7 +1,10 @@
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
+ const BUDGET_LIMIT = 100000; // tokens, ~100k default budget
5
8
 
6
9
  export function estimateTokens(text) {
7
10
  return Math.ceil((text || '').length / 4);
@@ -10,22 +13,47 @@ export function estimateTokens(text) {
10
13
  export function record(originalText, compressedText) {
11
14
  const o = estimateTokens(originalText);
12
15
  const c = estimateTokens(compressedText);
16
+ const saved = Math.max(0, o - c);
13
17
  totalOriginal += o;
14
18
  totalCompressed += c;
15
19
  calls++;
16
- return { originalTokens: o, compressedTokens: c, savedTokens: Math.max(0, o - c) };
20
+ const entry = {
21
+ id: Date.now() + Math.random().toString(36).slice(2, 6),
22
+ originalTokens: o,
23
+ compressedTokens: c,
24
+ savedTokens: saved,
25
+ savedPercent: o ? Math.round((1 - c / o) * 100) : 0,
26
+ timestamp: new Date().toISOString(),
27
+ preview: (compressedText || '').slice(0, 120)
28
+ };
29
+ history.unshift(entry);
30
+ if (history.length > MAX_HISTORY) history.pop();
31
+ return { originalTokens: o, compressedTokens: c, savedTokens: saved };
17
32
  }
18
33
 
19
34
  export function getStats() {
20
35
  const saved = Math.max(0, totalOriginal - totalCompressed);
21
36
  const pct = totalOriginal ? Math.round((1 - totalCompressed / totalOriginal) * 100) : 0;
22
- return { totalOriginal, totalCompressed, savedTokens: saved, savedPercent: pct, calls };
37
+ const budgetUsed = totalCompressed;
38
+ const budgetRemaining = Math.max(0, BUDGET_LIMIT - budgetUsed);
39
+ const budgetPercent = Math.min(100, Math.round((budgetUsed / BUDGET_LIMIT) * 100));
40
+ const lowBudget = budgetPercent > 90;
41
+ return { totalOriginal, totalCompressed, savedTokens: saved, savedPercent: pct, calls, budgetUsed, budgetRemaining, budgetPercent, lowBudget, budgetLimit: BUDGET_LIMIT };
42
+ }
43
+
44
+ export function getHistory() {
45
+ return [...history];
46
+ }
47
+
48
+ export function clearHistory() {
49
+ history.length = 0;
23
50
  }
24
51
 
25
52
  export function reset() {
26
53
  totalOriginal = 0;
27
54
  totalCompressed = 0;
28
55
  calls = 0;
56
+ history.length = 0;
29
57
  }
30
58
 
31
- export default { estimateTokens, record, getStats, reset };
59
+ export default { estimateTokens, 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.7",
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",